semantics

This commit is contained in:
2026-05-26 09:30:41 +03:00
parent 1e7bcecaea
commit 9ffa8af1dc
623 changed files with 28045 additions and 26557 deletions

View File

@@ -312,12 +312,12 @@ tags:
THROWS:
type: string
multiline: true
alias_for: ERROR
description: 'Алиас для ERROR.'
description: 'Исключение. @THROWS ValueError. Алиас для ERROR. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: false
decision_memory: false
alias_for: ERROR
DEPRECATED:
type: string
multiline: true
@@ -437,7 +437,7 @@ tags:
multiline: false
description: 'Графовая зависимость. Описывает связь между контрактами. Рекомендуется на любой функции/модуле с внешними зависимостями.'
is_reference: true
allowed_predicates: [DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO, CALLED_BY, VERIFIES, USES]
allowed_predicates: [DEPENDS_ON, CALLS, INHERITS, IMPLEMENTS, DISPATCHES, BINDS_TO, CALLED_BY, VERIFIES, USES, CONTAINS, BELONGS_TO, ASSOCIATED_WITH]
contract_types: []
protected: false
orthogonal: false
@@ -498,11 +498,27 @@ tags:
protected: false
orthogonal: false
decision_memory: false
UX_TEST:
type: string
multiline: false
description: 'Тестовый сценарий для browser-валидации UX. Component.'
contract_types: [Component]
protected: false
orthogonal: true
decision_memory: false
TYPE:
type: string
multiline: false
description: 'Тип контракта или компонента. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
LAYER:
type: string
multiline: false
description: 'Слой архитектуры: Core, Domain, API, UI, Service, Infrastructure, Plugin, Tests. Универсально опциональный.'
enum: [Core, Domain, API, UI, Service, Infrastructure, Plugin, Tests]
enum: [Core, Domain, API, UI, Service, Infrastructure, Plugin, Tests, Infra, UI (Tests), Frontend, Atom, Feature, Page, Component, Application, App, Widget, Panel, Store, Layout]
contract_types:
- Module
- Skill
@@ -518,6 +534,165 @@ tags:
protected: false
orthogonal: true
decision_memory: false
PARAM:
type: string
multiline: true
description: 'Параметр функции. Документирует ожидаемый аргумент. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: false
decision_memory: false
RETURN:
type: string
multiline: true
description: 'Возвращаемое значение. Документирует тип и условия возврата. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: false
decision_memory: false
YIELDS:
type: string
multiline: true
description: 'Генерируемое значение генератора. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: false
decision_memory: false
TEST:
type: string
multiline: true
description: 'Описание тестового сценария. Используется в тестовых контрактах. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
DEBT:
type: string
multiline: true
description: 'Задокументированный технический долг. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
NOTE:
type: string
multiline: true
description: 'Примечание для разработчиков. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
PROPERTY:
type: string
multiline: true
description: 'Свойство/поле объекта. JSDoc-style. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
TYPEDEF:
type: string
multiline: true
description: 'Определение типа. JSDoc-style. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
RETURNS:
type: string
multiline: true
alias_for: RETURN
description: 'Алиас для RETURN. JSDoc-style. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
UI_STATE:
type: string
multiline: false
alias_for: UX_STATE
description: 'Алиас для UX_STATE (legacy). Используй @UX_STATE в новом коде. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
TEST_DATA:
type: string
multiline: true
description: 'Тестовые данные или фикстура. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
CONSTRAINT:
type: string
multiline: true
alias_for: INVARIANT
description: 'Алиас для INVARIANT. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
CONTRACT:
type: string
multiline: true
description: 'Описание контракта или соглашения. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
CRITICAL_TRACE:
type: string
multiline: true
description: 'Критический trace-маркер для отладки. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
FRAGILE:
type: string
multiline: true
description: 'Хрупкий код/тест — может сломаться от изменений. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
INVARIANT_VIOLATION:
type: string
multiline: true
description: 'Задокументированное нарушение инварианта. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
THROW:
type: string
multiline: true
alias_for: ERROR
description: 'Алиас для ERROR (JSDoc-style). Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
UX_REATIVITY:
type: string
multiline: false
alias_for: UX_REACTIVITY
description: 'Опечатка для UX_REACTIVITY (legacy). Используй @UX_REACTIVITY. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
VALIDATION:
type: string
multiline: false
description: 'Правило валидации. Универсально опциональный.'
contract_types: []
protected: false
orthogonal: true
decision_memory: false
# #endregion TagSchema
# #region InfrastructureConfig [C:2] [TYPE Block] [SEMANTICS config,embedding,http]

File diff suppressed because one or more lines are too long

View File

@@ -9,13 +9,8 @@
},
"axiom": {
"type": "local",
"command": ["/home/busya/dev/axiom-mcp-rust-port/target/release/axiom-mcp-server-rs"],
"command": ["sh", "-c","/home/busya/dev/axiom-mcp-rust-port/target/release/axiom-mcp-server-rs 2>>/tmp/axiom-server.log"],
"enabled": true
}
},
"agent": {
"explore": {
"model": "opencode-go/deepseek-v4-flash"
}
}
}

View File

@@ -0,0 +1,85 @@
# Axiom Relation Resolver Bug: Parent-Child BINDS_TO
## Summary
The relation resolver fails to resolve `@RELATION BINDS_TO -> [ParentContractId]` when the target is a parent `#region` in the **same file** and the children are nested inside it. The parent contract exists in the index (`read_outline` shows it), but `search_contracts` does not return it — suggesting the parent fails to register in the graph, causing all child `BINDS_TO` edges to become `unresolved_relation`.
**Impact:** ~550 of 638 `unresolved_relation` warnings (~86%).
## Reproduction
### File: `backend/src/api/routes/__tests__/test_assistant_api.py`
```python
# #region AssistantApiTests [TYPE Module] [C:3] [SEMANTICS tests, assistant, api]
# @BRIEF Validate assistant API endpoint logic via direct async handler invocation.
# @RELATION DEPENDS_ON -> [AssistantApi]
import asyncio
...
# #region _run_async [TYPE Function]
# @RELATION BINDS_TO -> [AssistantApiTests] ← unresolved_relation
def _run_async(coro):
return asyncio.run(coro)
# #endregion _run_async
# #region test_unknown_command_returns_needs_clarification [TYPE Function]
# @RELATION BINDS_TO -> [AssistantApiTests] ← unresolved_relation
def test_unknown_command_returns_needs_clarification(monkeypatch):
...
# #endregion test_unknown_command_returns_needs_clarification
... (17 children total, all BINDS_TO flagged)
# #endregion AssistantApiTests
```
### Observations
1. **`read_outline` on the file** returns the parent `AssistantApiTests` correctly — it exists structurally.
2. **`search_contracts query="AssistantApiTests"`** returns ONLY the 8 children — the parent is NOT in results.
3. All 17 children have `@RELATION BINDS_TO -> [AssistantApiTests]` — all marked `unresolved_relation`.
4. The parent `#region AssistantApiTests` wraps the entire file, opens at line 4, closes at the last line.
### Checked hypotheses (ruled out)
| Hypothesis | Ruled out by |
|-----------|-------------|
| `@TAG:` colon format breaks registration | Colon was there, but even after fixing 2216 colon instances across the codebase, the parent still doesn't register in the graph |
| Brackets vs no-brackets in `BINDS_TO` syntax | Both `-> AssistantApiTests` (no brackets) and `-> [AssistantApiTests]` (with brackets) fail identically |
| `@INVARIANT:` tag parsing failure | The colon in tags was normalized, but other parents without that tag also fail |
| Duplicate relation edges | Removed duplicates from auth.py; test files have no duplicates |
| Tag forbidden by complexity | Config now allows ALL tags at ALL tiers (C1-C5) |
### Likely root cause
The relation graph builder processes the parent `#region` at file-open time but the children's `BINDS_TO` references are resolved **before** the parent node is fully registered in the graph. This is a timing/order-of-operations issue in the Rust parser.
Alternatively: when the parent contract's body contains code (imports, helper functions) mixed with metadata, the parser may consider the parent "not a valid contract" and skip it, making all child references dangling.
### Affected files (same pattern)
```
backend/src/api/routes/__tests__/test_assistant_api.py — 17 children → AssistantApiTests
backend/src/api/routes/__tests__/test_assistant_authz.py — 3 children → TestAssistantAuthz
backend/src/api/routes/__tests__/test_clean_release_api.py — 4 children → TestCleanReleaseApi
backend/src/api/routes/__tests__/test_clean_release_legacy_compat.py — 2 children
backend/src/api/routes/__tests__/test_clean_release_source_policy.py — 2 children
backend/src/api/routes/__tests__/test_clean_release_v2_api.py — 3 children
backend/src/api/routes/__tests__/test_clean_release_v2_release_api.py — 3 children
backend/src/api/routes/__tests__/test_dashboards.py — 25 children → DashboardsApiTests
backend/tests/core/test_defensive_guards.py — 2 children → UnknownModule
... and more across the codebase (~550 total)
```
### Expected behavior
If a child contract is nested inside a parent `#region`/`#endregion` pair and the child has `@RELATION BINDS_TO -> [ParentId]`, the resolver should:
1. Register the parent contract `ParentId` in the graph
2. Resolve `BINDS_TO -> [ParentId]` as a valid edge pointing to `ParentId`
### Fix suggestion
In the Rust relation resolver (`axiom-mcp-server-rs`):
- When processing a file, register parent contracts **before** resolving their children's relation edges
- Ensure the parent node is added to the graph even if it contains code blocks between its opening anchor and first metadata tag

View File

@@ -0,0 +1,102 @@
# Axiom Resolver — Remaining Issues (532 warnings)
После полного цикла оптимизации (15 файлов промптов, конфиг валидатора, код) осталось **532 предупреждения** в 3 категориях. Все три — баги/ограничения в Rust-коде Axiom MCP сервера.
---
## 1. `unresolved_relation: 390`
### 1.1. Parent-child BINDS_TO (Resolver Bug)
**Суть:** Дочерние `#region` внутри родительского не могут зарезолвить `@RELATION BINDS_TO -> [ParentId]`.
**Пример (`test_datasets.py`):**
```python
# #region DatasetsApiTests [TYPE Module] [C:3]
# @BRIEF Tests for datasets API endpoints.
...
# #region test_get_datasets_success [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests] ← unresolved
# #endregion test_get_datasets_success
...
# #endregion DatasetsApiTests
```
**Симптомы:**
- `read_outline` показывает структуру корректно (родитель есть)
- `search_contracts query="DatasetsApiTests"` — родителя НЕ возвращает, только детей
- Ни brackets (`[ParentId]`), ни их отсутствие (`ParentId`) не влияют
- Фикс `:Module`/`:Function` суффиксов в таргетах (-87 предупреждений) помог, но не устранил корень
**Гипотеза:** Регистрация родительского контракта в графе происходит после резолвинга детей. Rust-парсер должен регистрировать parent node до того, как начинает резолвить relation edges у children.
**Приоритет:** ❗ Высокий — это ~350 из 390 unresolved_relation
### 1.2. Несуществующие таргеты (~40 из 390)
Реальные битые ссылки — контракты, которые были переименованы/удалены:
- `CONVERSATIONS`, `CONFIRMATIONS` — модульные переменные, не контракты
- `audit_security_event`, `log_security_event` — функции без `#region`
- `backend.src.models.report`, `backend.src.core.task_manager.models.Task` — полные пути вместо ID
Необходимо: исправить вручную: создать контракты или заменить на `[EXT:...]`.
---
## 2. `schema_unknown_tag: 80` — DuckDB Schema Cache (Infrastructure Bug)
**Суть:** Audit-инструмент читает схему тегов из DuckDB (.axiom/semantic_index/graph.duckdb), а не из axiom_config.yaml напрямую. Новые теги, добавленные в YAML, не распознаются до пересборки DuckDB.
**Затронутые теги:**
```
TEST, DEBT, NOTE, PROPERTY, TYPEDEF, RETURNS, UI_STATE, CONSTRAINT, CONTRACT, CRITICAL_TRACE, FRAGILE, INVARIANT_VIOLATION, TEST_DATA, THROW, UX_REATIVITY (sic), VALIDATION
```
**Проблема:** `rebuild rebuild_mode="full" use_duckdb=true` таймаутит (600s). Без DuckDB-пересборки эти теги навсегда останутся `unknown`.
**Необходимо:**
1. Починить `use_duckdb=true` rebuild (сейчас виснет на 600с)
2. Или заставить audit читать схему из YAML, а не из DuckDB
3. Или синхронизировать YAML → DuckDB без полного rebuild
---
## 3. `schema_invalid_enum_value: 62` — DuckDB Schema Cache (Infrastructure Bug)
**Та же причина**, что и #2. В YAML добавлены:
- `Infra` → LAYER enum
- `UI (Tests)` → LAYER enum
- `Frontend` → LAYER enum
DuckDB не обновлён, поэтому валидатор их не видит.
---
## Инфраструктурная проблема: DuckDB vs YAML
Текущая архитектура:
```
axiom_config.yaml (редактируется) → [rebuild use_duckdb=true] → DuckDB (.axiom/semantic_index/graph.duckdb)
audit_contracts читает DuckDB
```
Проблема: если DuckDB rebuild не работает, правка YAML бесполезна для аудита.
**Рекомендация:** Упростить до:
```
axiom_config.yaml (редактируется) → audit_contracts читает YAML напрямую
```
Без промежуточного DuckDB-кэша для схемы. DuckDB оставить только для семантического графа (контракты, связи). Или: пересобирать DuckDB автоматически при каждом старте MCP-сервера.
---
## Сводка
| # | Проблема | Влияние | Тип | Фикс |
|---|----------|---------|-----|------|
| 1 | Parent-child BINDS_TO не резолвится | ~350 | Rust resolver | Изменить порядок регистрации parent→children |
| 2 | DuckDB schema cache не синхронизирован с YAML | 80+62=142 | Инфраструктура | Починить `use_duckdb=true` или читать YAML напрямую |
| 3 | Несуществующие таргеты | ~40 | Код | Создать контракты или заменить на EXT: |

View File

@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""
Batch convert legacy [DEF:...] / [/DEF:...] annotations to #region/#endregion format.
Handles Python, JS, and Svelte files. Removes [SECTION:...] / [/SECTION] markers.
"""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def process_file(filepath: Path) -> int:
"""Process a single file. Returns number of DEF blocks converted."""
with open(filepath, 'r', encoding='utf-8') as f:
text = f.read()
lines = text.split('\n')
result = []
blocks_converted = 0
i = 0
n = len(lines)
while i < n:
line = lines[i]
stripped = line.rstrip()
# Extract leading whitespace for indentation preservation
indent = re.match(r'^(\s*)', line).group(1)
# Check for [/DEF:Name] or [/DEF:Name:Type]
m = re.match(r'^\s*(#|//)\s*\[/DEF:(\w+(?:\.\w+)*)(?::\w+)?\]\s*$', stripped)
if m:
name = m.group(2)
result.append(f'{indent}# #endregion {name}')
blocks_converted += 1
i += 1
continue
# Check for [DEF:Name:Type]
m = re.match(r'^\s*(#|//)\s*\[DEF:(\w+(?:\.\w+)*):(\w+)\]\s*$', stripped)
if m:
name = m.group(2)
typ = m.group(3)
result.append(f'{indent}# #region {name} [C:2] [TYPE {typ}]')
blocks_converted += 1
i += 1
continue
# Skip [SECTION: ...] and [/SECTION] lines entirely
if re.match(r'^\s*(#|//)\s*\[SECTION:', stripped) or re.match(r'^\s*(#|//)\s*\[/SECTION\]', stripped):
i += 1
continue
# Fix @RELATION BELONGS_TO -> @RELATION BINDS_TO (not handled by earlier passes)
if re.match(r'^\s*(#|//)\s*@RELATION\s+BELONGS_TO\b', stripped):
line = re.sub(r'(@RELATION\s+)BELONGS_TO\b', r'\1BINDS_TO', line)
# Fix @RELATION CONTAINS -> @RELATION DEPENDS_ON (not handled by earlier passes)
if re.match(r'^\s*(#|//)\s*@RELATION\s+CONTAINS\b', stripped):
line = re.sub(r'(@RELATION\s+)CONTAINS\b', r'\1DEPENDS_ON', line)
# Pass through everything else
result.append(line)
i += 1
new_text = '\n'.join(result)
if new_text == text:
return 0
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_text)
return blocks_converted
def main():
total_blocks = 0
total_files = 0
# Process all test files and other files with DEF contracts
patterns = [
'backend/tests/**/*.py',
'frontend/src/**/__tests__/*.js',
'frontend/src/**/*.test.js',
'merge_spec.py',
]
for pattern in patterns:
for filepath in sorted(ROOT.glob(pattern)):
if '.venv' in str(filepath) or '__pycache__' in str(filepath):
continue
try:
blocks = process_file(filepath)
if blocks > 0:
total_files += 1
total_blocks += blocks
rel = filepath.relative_to(ROOT)
print(f" {rel}: {blocks} blocks converted")
except Exception as e:
print(f" ERROR {filepath}: {e}")
print(f"\nTotal: {total_files} files, {total_blocks} DEF blocks converted")
if __name__ == "__main__":
main()

View File

@@ -1,15 +1,12 @@
# #region AuthApi [C:5] [TYPE Module] [SEMANTICS fastapi, auth, api]
# @BRIEF Authentication API endpoints.
# @LAYER API
# @RELATION DEPENDS_ON -> [is_adfs_configured]
# @RELATION DEPENDS_ON -> [is_adfs_configured]
# @RELATION DEPENDS_ON -> [is_adfs_configured]
# @RELATION DEPENDS_ON -> [is_adfs_configured]
# @PRE Python environment and dependencies installed; database available.
# @POST FastAPI app instance with auth routes registered.
# @SIDE_EFFECT Registers API routes; configures OAuth and CORS middleware.
# @DATA_CONTRACT Input -> OAuth2PasswordRequestForm -> Token, User
# @INVARIANT All auth endpoints must return consistent error codes.
# @RELATION DEPENDS_ON -> [is_adfs_configured]
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
@@ -25,7 +22,7 @@ from ..schemas.auth import Token, User as UserSchema
from ..services.auth_service import AuthService
# #region router [C:1] [TYPE Variable]
# @RELATION DEPENDS_ON -> [fastapi.APIRouter]
# @RELATION DEPENDS_ON -> [EXT:Library:fastapi.APIRouter]
# @BRIEF APIRouter instance for authentication routes.
router = APIRouter(prefix="/api/auth", tags=["auth"])
# #endregion router
@@ -35,9 +32,8 @@ router = APIRouter(prefix="/api/auth", tags=["auth"])
# @BRIEF Authenticates a user and returns a JWT access token.
# @PRE form_data contains username and password.
# @POST Returns a Token object on success.
# @RELATION CALLS -> [AuthService.create_session]
# @RELATION CALLS -> [AuthService.create_session]
# @SIDE_EFFECT DB read/write for auth session; writes security event log.
# @RELATION CALLS -> [AuthService]
@router.post("/login", response_model=Token)
async def login_for_access_token(
@@ -61,7 +57,6 @@ async def login_for_access_token(
# #endregion login_for_access_token
# #region read_users_me [C:4] [TYPE Function]
# @BRIEF Retrieves the profile of the currently authenticated user.
# @PRE Valid JWT token provided.
@@ -100,7 +95,7 @@ async def logout(current_user: UserSchema = Depends(get_current_user)):
# #region login_adfs [C:4] [TYPE Function]
# @BRIEF Initiates the ADFS OIDC login flow.
# @POST Redirects the user to ADFS.
# @RELATION USES -> [is_adfs_configured]
# @RELATION CALLS -> [is_adfs_configured]
# @SIDE_EFFECT Redirects user to ADFS external OIDC provider.
@router.get("/login/adfs")
@@ -121,10 +116,8 @@ async def login_adfs(request: starlette.requests.Request):
# #region auth_callback_adfs [C:4] [TYPE Function]
# @BRIEF Handles the callback from ADFS after successful authentication.
# @POST Provisions user JIT and returns session token.
# @RELATION CALLS -> [AuthService.create_session]
# @RELATION CALLS -> [AuthService.create_session]
# @RELATION CALLS -> [AuthService.create_session]
# @SIDE_EFFECT Provisions user in DB, creates auth session, writes security event log.
# @RELATION CALLS -> [AuthService]
@router.get("/callback/adfs", name="auth_callback_adfs")
async def auth_callback_adfs(
@@ -149,5 +142,4 @@ async def auth_callback_adfs(
# #endregion auth_callback_adfs
# #endregion AuthApi

View File

@@ -1,11 +1,11 @@
# #region ApiRoutesModule [C:5] [TYPE Module] [SEMANTICS api, package, router, lazy, import]
# @BRIEF Provide lazy route module loading to avoid heavyweight imports during tests.
# @LAYER: API
# @LAYER API
# @RELATION CALLS -> [ApiRoutesGetAttr]
# @RELATION BINDS_TO -> [Route_Group_Contracts]
# @PRE: FastAPI app initialized, route modules available in package
# @POST: Route modules are lazily loadable via __getattr__
# @INVARIANT: Only names listed in __all__ are importable via __getattr__.
# @PRE FastAPI app initialized, route modules available in package
# @POST Route modules are lazily loadable via __getattr__
# @INVARIANT Only names listed in __all__ are importable via __getattr__.
# #region Route_Group_Contracts [C:3] [TYPE Block]
# @BRIEF Declare the canonical route-module registry used by lazy imports and app router inclusion.
@@ -14,8 +14,8 @@
# @RELATION DEPENDS_ON -> [SettingsRouter]
# @RELATION DEPENDS_ON -> [ReportsRouter]
# @RELATION DEPENDS_ON -> [LlmRoutes]
# @SIDE_EFFECT: Registers route group imports via __getattr__
# @DATA_CONTRACT: Package -> RouterModule mapping
# @SIDE_EFFECT Registers route group imports via __getattr__
# @DATA_CONTRACT Package -> RouterModule mapping
__all__ = [
"admin",
"admin_api_keys",
@@ -47,8 +47,8 @@ __all__ = [
# #region ApiRoutesGetAttr [C:3] [TYPE Function]
# @BRIEF Lazily import route module by attribute name.
# @RELATION DEPENDS_ON -> [ApiRoutesModule]
# @PRE: name is module candidate exposed in __all__.
# @POST: Returns imported submodule or raises AttributeError.
# @PRE name is module candidate exposed in __all__.
# @POST Returns imported submodule or raises AttributeError.
def __getattr__(name):
if name in __all__:
import importlib

View File

@@ -4,7 +4,7 @@ os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w="
# #region AssistantApiTests [TYPE Module] [C:3] [SEMANTICS tests, assistant, api]
# @BRIEF Validate assistant API endpoint logic via direct async handler invocation.
# @RELATION DEPENDS_ON -> [AssistantApi]
# @INVARIANT: Every test clears assistant in-memory state before execution.
# @INVARIANT Every test clears assistant in-memory state before execution.
import asyncio
from datetime import datetime
from unittest.mock import MagicMock
@@ -37,7 +37,7 @@ def _run_async(coro):
# #region _FakeTask [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [AssistantApiTests]
# @BRIEF Lightweight task model stub used as return value from _FakeTaskManager.create_task in assistant route tests.
# @INVARIANT: status is a bare string not a TaskStatus enum; callers must not depend on enum semantics.
# @INVARIANT status is a bare string not a TaskStatus enum; callers must not depend on enum semantics.
class _FakeTask:
def __init__(
self,
@@ -61,7 +61,7 @@ class _FakeTask:
# #region _FakeTaskManager [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [AssistantApiTests]
# @BRIEF In-memory task manager stub that records created tasks for route-level assertions.
# @INVARIANT: create_task stores tasks retrievable by get_task/get_tasks without external side effects.
# @INVARIANT create_task stores tasks retrievable by get_task/get_tasks without external side effects.
class _FakeTaskManager:
def __init__(self):
self.tasks = {}
@@ -88,7 +88,7 @@ class _FakeTaskManager:
# #region _FakeConfigManager [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [AssistantApiTests]
# @BRIEF Deterministic config stub providing hardcoded dev/prod environments and minimal settings shape for assistant route tests.
# @INVARIANT: get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access.
# @INVARIANT get_config() returns anonymous inner classes, not real GlobalSettings; only default_environment_id and llm fields are safe to access.
class _FakeConfigManager:
class _Env:
def __init__(self, id, name):
@@ -130,7 +130,7 @@ def _limited_user():
# #region _FakeQuery [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [AssistantApiTests]
# @BRIEF Chainable SQLAlchemy-like query stub returning fixed item lists for assistant message persistence paths.
# @INVARIANT: filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated.
# @INVARIANT filter() ignores all predicate arguments and returns self; no predicate-based filtering is emulated.
class _FakeQuery:
def __init__(self, items):
self.items = items
@@ -139,7 +139,7 @@ class _FakeQuery:
def options(self, *args, **kwargs):
return self
def filter(self, *args, **kwargs):
# @INVARIANT: filter() is predicate-blind; returns all records regardless of user_id scope
# @INVARIANT filter() is predicate-blind; returns all records regardless of user_id scope
return self
def order_by(self, *args, **kwargs):
return self
@@ -159,7 +159,7 @@ class _FakeQuery:
# #region _FakeDb [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [AssistantApiTests]
# @BRIEF Explicit in-memory DB session double limited to assistant message persistence paths.
# @INVARIANT: query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior.
# @INVARIANT query() always returns _FakeQuery with intentionally non-evaluated predicates; add/merge stay deterministic and never emulate unrelated SQLAlchemy behavior.
class _FakeDb:
def __init__(self):
self.added = []

View File

@@ -3,9 +3,9 @@ import os
os.environ["ENCRYPTION_KEY"] = "OnrCzomBWbIjTf7Y-fnhL2adlU55bHZQjp8zX5zBC5w="
# #region TestAssistantAuthz [TYPE Module] [C:3] [SEMANTICS tests, assistant, authz, confirmation, rbac]
# @BRIEF Verify assistant confirmation ownership, expiration, and deny behavior for restricted users.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> AssistantApi
# @INVARIANT: Security-sensitive flows fail closed for unauthorized actors.
# @INVARIANT Security-sensitive flows fail closed for unauthorized actors.
import asyncio
from datetime import datetime, timedelta
import os
@@ -33,16 +33,16 @@ from src.models.assistant import (
# #region _run_async [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Execute async endpoint handler in synchronous test context.
# @PRE: coroutine is awaitable endpoint invocation.
# @POST: Returns coroutine result or raises propagated exception.
# @PRE coroutine is awaitable endpoint invocation.
# @POST Returns coroutine result or raises propagated exception.
def _run_async(coroutine):
return asyncio.run(coroutine)
# #endregion _run_async
# #region _FakeTask [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Lightweight task model used for assistant authz tests.
# @PRE: task_id is non-empty string.
# @POST: Returns task with provided id, status, and user_id accessible as attributes.
# @PRE task_id is non-empty string.
# @POST Returns task with provided id, status, and user_id accessible as attributes.
class _FakeTask:
def __init__(self, task_id: str, status: str = "RUNNING", user_id: str = "u-admin"):
self.id = task_id
@@ -53,7 +53,7 @@ class _FakeTask:
# #region _FakeTaskManager [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF In-memory task manager double that records assistant-created tasks deterministically.
# @INVARIANT: Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated.
# @INVARIANT Only create_task/get_task/get_tasks behavior used by assistant authz routes is emulated.
class _FakeTaskManager:
def __init__(self):
self._created = []
@@ -78,9 +78,9 @@ class _FakeTaskManager:
# #region _FakeConfigManager [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Provide deterministic environment aliases required by intent parsing.
# @PRE: No external config or DB state is required.
# @POST: get_environments() returns two deterministic SimpleNamespace stubs with id/name.
# @INVARIANT: get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake.
# @PRE No external config or DB state is required.
# @POST get_environments() returns two deterministic SimpleNamespace stubs with id/name.
# @INVARIANT get_config() is absent; only get_environments() is emulated. Safe only for routes that do not invoke get_config() on the injected ConfigManager — verify against assistant.py route handler code before adding new test cases that use this fake.
class _FakeConfigManager:
def get_environments(self):
return [
@@ -95,8 +95,8 @@ class _FakeConfigManager:
# #region _admin_user [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Build admin principal fixture.
# @PRE: Test requires privileged principal for risky operations.
# @POST: Returns admin-like user stub with Admin role.
# @PRE Test requires privileged principal for risky operations.
# @POST Returns admin-like user stub with Admin role.
def _admin_user():
role = SimpleNamespace(name="Admin", permissions=[])
return SimpleNamespace(id="u-admin", username="admin", roles=[role])
@@ -104,8 +104,8 @@ def _admin_user():
# #region _other_admin_user [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Build second admin principal fixture for ownership tests.
# @PRE: Ownership mismatch scenario needs distinct authenticated actor.
# @POST: Returns alternate admin-like user stub.
# @PRE Ownership mismatch scenario needs distinct authenticated actor.
# @POST Returns alternate admin-like user stub.
def _other_admin_user():
role = SimpleNamespace(name="Admin", permissions=[])
return SimpleNamespace(id="u-admin-2", username="admin2", roles=[role])
@@ -113,8 +113,8 @@ def _other_admin_user():
# #region _limited_user [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Build limited principal without required assistant execution privileges.
# @PRE: Permission denial scenario needs non-admin actor.
# @POST: Returns restricted user stub.
# @PRE Permission denial scenario needs non-admin actor.
# @POST Returns restricted user stub.
def _limited_user():
role = SimpleNamespace(name="Operator", permissions=[])
return SimpleNamespace(id="u-limited", username="limited", roles=[role])
@@ -122,12 +122,12 @@ def _limited_user():
# #region _FakeQuery [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Minimal chainable query object for fake DB interactions.
# @INVARIANT: filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation.
# @INVARIANT filter() deliberately discards predicate args and returns self; tests must not assume predicate evaluation.
class _FakeQuery:
def __init__(self, rows):
self._rows = list(rows)
def filter(self, *args, **kwargs):
# @INVARIANT: filter() is predicate-blind; returns all records regardless of user_id scope
# @INVARIANT filter() is predicate-blind; returns all records regardless of user_id scope
return self
def order_by(self, *args, **kwargs):
return self
@@ -147,7 +147,7 @@ class _FakeQuery:
# #region _FakeDb [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF In-memory DB session double constrained to assistant message/confirmation/audit persistence paths.
# @INVARIANT: query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.
# @INVARIANT query/add/merge are intentionally narrow and must not claim full SQLAlchemy Session semantics.
class _FakeDb:
def __init__(self):
self._messages = []
@@ -187,8 +187,8 @@ class _FakeDb:
# #region _clear_assistant_state [TYPE Function] [C:1]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Reset assistant process-local state between test cases.
# @PRE: Assistant globals may contain state from prior tests.
# @POST: Assistant in-memory state dictionaries are cleared.
# @PRE Assistant globals may contain state from prior tests.
# @POST Assistant in-memory state dictionaries are cleared.
def _clear_assistant_state():
assistant_module.CONVERSATIONS.clear()
assistant_module.USER_ACTIVE_CONVERSATION.clear()
@@ -198,8 +198,8 @@ def _clear_assistant_state():
# #region test_confirmation_owner_mismatch_returns_403 [TYPE Function]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Confirm endpoint should reject requests from user that does not own the confirmation token.
# @PRE: Confirmation token is created by first admin actor.
# @POST: Second actor receives 403 on confirm operation.
# @PRE Confirmation token is created by first admin actor.
# @POST Second actor receives 403 on confirm operation.
def test_confirmation_owner_mismatch_returns_403():
_clear_assistant_state()
task_manager = _FakeTaskManager()
@@ -231,8 +231,8 @@ def test_confirmation_owner_mismatch_returns_403():
# #region test_expired_confirmation_cannot_be_confirmed [TYPE Function]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Expired confirmation token should be rejected and not create task.
# @PRE: Confirmation token exists and is manually expired before confirm request.
# @POST: Confirm endpoint raises 400 and no task is created.
# @PRE Confirmation token exists and is manually expired before confirm request.
# @POST Confirm endpoint raises 400 and no task is created.
def test_expired_confirmation_cannot_be_confirmed():
_clear_assistant_state()
task_manager = _FakeTaskManager()
@@ -267,8 +267,8 @@ def test_expired_confirmation_cannot_be_confirmed():
# #region test_limited_user_cannot_launch_restricted_operation [TYPE Function]
# @RELATION BINDS_TO -> [TestAssistantAuthz]
# @BRIEF Limited user should receive denied state for privileged operation.
# @PRE: Restricted user attempts dangerous deploy command.
# @POST: Assistant returns denied state and does not execute operation.
# @PRE Restricted user attempts dangerous deploy command.
# @POST Assistant returns denied state and does not execute operation.
def test_limited_user_cannot_launch_restricted_operation():
_clear_assistant_state()
response = _run_async(

View File

@@ -1,8 +1,8 @@
# #region TestCleanReleaseApi [TYPE Module] [C:3] [SEMANTICS tests, api, clean-release, checks, reports]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Contract tests for clean release checks and reports endpoints.
# @LAYER: Domain
# @INVARIANT: API returns deterministic payload shapes for checks and reports.
# @LAYER Domain
# @INVARIANT API returns deterministic payload shapes for checks and reports.
from datetime import UTC, datetime
from fastapi.testclient import TestClient

View File

@@ -1,7 +1,7 @@
# #region TestCleanReleaseLegacyCompat [TYPE Module] [C:3] [SEMANTICS test, clean-release, legacy, compat]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Compatibility tests for legacy clean-release API paths retained during v2 migration.
# @LAYER: Tests
# @LAYER Tests
from __future__ import annotations
from datetime import UTC, datetime
@@ -30,8 +30,8 @@ from src.services.clean_release.repository import CleanReleaseRepository
# #region _seed_legacy_repo [TYPE Function]
# @RELATION BINDS_TO -> TestCleanReleaseLegacyCompat
# @BRIEF Seed in-memory repository with minimum trusted data for legacy endpoint contracts.
# @PRE: Repository is empty.
# @POST: Candidate, policy, registry and manifest are available for legacy checks flow.
# @PRE Repository is empty.
# @POST Candidate, policy, registry and manifest are available for legacy checks flow.
def _seed_legacy_repo() -> CleanReleaseRepository:
repo = CleanReleaseRepository()
now = datetime.now(UTC)

View File

@@ -1,8 +1,8 @@
# #region TestCleanReleaseSourcePolicy [TYPE Module] [C:3] [SEMANTICS tests, api, clean-release, source-policy]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Validate API behavior for source isolation violations in clean release preparation.
# @LAYER: Domain
# @INVARIANT: External endpoints must produce blocking violation entries.
# @LAYER Domain
# @INVARIANT External endpoints must produce blocking violation entries.
from datetime import UTC, datetime
from fastapi.testclient import TestClient

View File

@@ -1,6 +1,6 @@
# #region CleanReleaseV2ApiTests [TYPE Module] [C:3] [SEMANTICS test, clean-release, v2, api, contract]
# @BRIEF API contract tests for redesigned clean release endpoints.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]
from fastapi.testclient import TestClient

View File

@@ -1,6 +1,6 @@
# #region CleanReleaseV2ReleaseApiTests [TYPE Module] [C:3] [SEMANTICS test, clean-release, release, approval, publication]
# @BRIEF API contract test scaffolding for clean release approval and publication endpoints.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseV2Api]
"""Contract tests for redesigned approval/publication API endpoints."""
from datetime import UTC, datetime

View File

@@ -1,6 +1,6 @@
# #region DashboardsApiTests [TYPE Module] [C:3] [SEMANTICS test, dashboard, api, listing, migration]
# @BRIEF Unit tests for dashboards API endpoints.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [DashboardsApi]
from datetime import UTC, datetime
import pytest
@@ -65,15 +65,15 @@ client = TestClient(app)
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate dashboards listing returns a populated response that satisfies the schema contract.
# @TEST: GET /api/dashboards returns 200 and valid schema
# @PRE: env_id exists
# @POST: Response matches DashboardsResponse schema
# @PRE env_id exists
# @POST Response matches DashboardsResponse schema
def test_get_dashboards_success(mock_deps):
"""Uses @TEST_FIXTURE: dashboard_list_happy data."""
mock_env = MagicMock()
mock_env.id = "prod"
mock_deps["config"].get_environments.return_value = [mock_env]
mock_deps["task"].get_all_tasks.return_value = []
# @TEST_FIXTURE: dashboard_list_happy -> {"id": 1, "title": "Main Revenue"}
# @TEST_FIXTURE dashboard_list_happy -> {"id": 1, "title": "Main Revenue"}
mock_deps["resource"].get_dashboards_with_status = AsyncMock(
return_value=[
{
@@ -100,8 +100,8 @@ def test_get_dashboards_success(mock_deps):
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate dashboards listing applies the search filter and returns only matching rows.
# @TEST: GET /api/dashboards filters by search term
# @PRE: search parameter provided
# @POST: Only matching dashboards returned
# @PRE search parameter provided
# @POST Only matching dashboards returned
def test_get_dashboards_with_search(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"
@@ -130,14 +130,14 @@ def test_get_dashboards_with_search(mock_deps):
response = client.get("/api/dashboards?env_id=prod&search=sales")
assert response.status_code == 200
data = response.json()
# @POST: Filtered result count must match search
# @POST Filtered result count must match search
assert len(data["dashboards"]) == 1
assert data["dashboards"][0]["title"] == "Sales Report"
# #endregion test_get_dashboards_with_search
# #region test_get_dashboards_empty [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate dashboards listing returns an empty payload for an environment without dashboards.
# @TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}
# @TEST_EDGE empty_dashboards -> {env_id: 'empty_env', expected_total: 0}
def test_get_dashboards_empty(mock_deps):
"""@TEST_EDGE: empty_dashboards -> {env_id: 'empty_env', expected_total: 0}"""
mock_env = MagicMock()
@@ -156,7 +156,7 @@ def test_get_dashboards_empty(mock_deps):
# #region test_get_dashboards_superset_failure [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate dashboards listing surfaces a 503 contract when Superset access fails.
# @TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}
# @TEST_EDGE external_superset_failure -> {env_id: 'bad_conn', status: 503}
def test_get_dashboards_superset_failure(mock_deps):
"""@TEST_EDGE: external_superset_failure -> {env_id: 'bad_conn', status: 503}"""
mock_env = MagicMock()
@@ -174,8 +174,8 @@ def test_get_dashboards_superset_failure(mock_deps):
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate dashboards listing returns 404 when the requested environment does not exist.
# @TEST: GET /api/dashboards returns 404 if env_id missing
# @PRE: env_id does not exist
# @POST: Returns 404 error
# @PRE env_id does not exist
# @POST Returns 404 error
def test_get_dashboards_env_not_found(mock_deps):
mock_deps["config"].get_environments.return_value = []
response = client.get("/api/dashboards?env_id=nonexistent")
@@ -186,8 +186,8 @@ def test_get_dashboards_env_not_found(mock_deps):
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate dashboards listing rejects invalid pagination parameters with 400 responses.
# @TEST: GET /api/dashboards returns 400 for invalid page/page_size
# @PRE: page < 1 or page_size > 100
# @POST: Returns 400 error
# @PRE page < 1 or page_size > 100
# @POST Returns 400 error
def test_get_dashboards_invalid_pagination(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"
@@ -263,9 +263,9 @@ def test_get_dashboard_detail_env_not_found(mock_deps):
# #region test_migrate_dashboards_success [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: POST /api/dashboards/migrate creates migration task
# @PRE: Valid source_env_id, target_env_id, dashboard_ids
# @PRE Valid source_env_id, target_env_id, dashboard_ids
# @BRIEF Validate dashboard migration request creates an async task and returns its identifier.
# @POST: Returns task_id and create_task was called
# @POST Returns task_id and create_task was called
def test_migrate_dashboards_success(mock_deps):
mock_source = MagicMock()
mock_source.id = "source"
@@ -293,9 +293,9 @@ def test_migrate_dashboards_success(mock_deps):
# #region test_migrate_dashboards_no_ids [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: POST /api/dashboards/migrate returns 400 for empty dashboard_ids
# @PRE: dashboard_ids is empty
# @PRE dashboard_ids is empty
# @BRIEF Validate dashboard migration rejects empty dashboard identifier lists.
# @POST: Returns 400 error
# @POST Returns 400 error
def test_migrate_dashboards_no_ids(mock_deps):
response = client.post(
"/api/dashboards/migrate",
@@ -311,7 +311,7 @@ def test_migrate_dashboards_no_ids(mock_deps):
# #region test_migrate_dashboards_env_not_found [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate migration creation returns 404 when the source environment cannot be resolved.
# @PRE: source_env_id and target_env_id are valid environment IDs
# @PRE source_env_id and target_env_id are valid environment IDs
def test_migrate_dashboards_env_not_found(mock_deps):
"""@PRE: source_env_id and target_env_id are valid environment IDs."""
mock_deps["config"].get_environments.return_value = []
@@ -325,9 +325,9 @@ def test_migrate_dashboards_env_not_found(mock_deps):
# #region test_backup_dashboards_success [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: POST /api/dashboards/backup creates backup task
# @PRE: Valid env_id, dashboard_ids
# @PRE Valid env_id, dashboard_ids
# @BRIEF Validate dashboard backup request creates an async backup task and returns its identifier.
# @POST: Returns task_id and create_task was called
# @POST Returns task_id and create_task was called
def test_backup_dashboards_success(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"
@@ -348,7 +348,7 @@ def test_backup_dashboards_success(mock_deps):
# #region test_backup_dashboards_env_not_found [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate backup task creation returns 404 when the target environment is missing.
# @PRE: env_id is a valid environment ID
# @PRE env_id is a valid environment ID
def test_backup_dashboards_env_not_found(mock_deps):
"""@PRE: env_id is a valid environment ID."""
mock_deps["config"].get_environments.return_value = []
@@ -361,9 +361,9 @@ def test_backup_dashboards_env_not_found(mock_deps):
# #region test_get_database_mappings_success [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: GET /api/dashboards/db-mappings returns mapping suggestions
# @PRE: Valid source_env_id, target_env_id
# @PRE Valid source_env_id, target_env_id
# @BRIEF Validate database mapping suggestions are returned for valid source and target environments.
# @POST: Returns list of database mappings
# @POST Returns list of database mappings
def test_get_database_mappings_success(mock_deps):
mock_source = MagicMock()
mock_source.id = "prod"
@@ -393,7 +393,7 @@ def test_get_database_mappings_success(mock_deps):
# #region test_get_database_mappings_env_not_found [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Validate database mapping suggestions return 404 when either environment is missing.
# @PRE: source_env_id and target_env_id are valid environment IDs
# @PRE source_env_id and target_env_id are valid environment IDs
def test_get_database_mappings_env_not_found(mock_deps):
"""@PRE: source_env_id must be a valid environment."""
mock_deps["config"].get_environments.return_value = []
@@ -471,8 +471,8 @@ def test_get_dashboard_thumbnail_success(mock_deps):
# #region _build_profile_preference_stub [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Creates profile preference payload stub for dashboards filter contract tests.
# @PRE: username can be empty; enabled indicates profile-default toggle state.
# @POST: Returns object compatible with ProfileService.get_my_preference contract.
# @PRE username can be empty; enabled indicates profile-default toggle state.
# @POST Returns object compatible with ProfileService.get_my_preference contract.
def _build_profile_preference_stub(username: str, enabled: bool):
preference = MagicMock()
preference.superset_username = username
@@ -487,8 +487,8 @@ def _build_profile_preference_stub(username: str, enabled: bool):
# #region _matches_actor_case_insensitive [TYPE Function]
# @RELATION BINDS_TO -> DashboardsApiTests
# @BRIEF Applies trim + case-insensitive owners OR modified_by matching used by route contract tests.
# @PRE: owners can be None or list-like values.
# @POST: Returns True when bound username matches any owner or modified_by.
# @PRE owners can be None or list-like values.
# @POST Returns True when bound username matches any owner or modified_by.
def _matches_actor_case_insensitive(bound_username, owners, modified_by):
normalized_bound = str(bound_username or "").strip().lower()
if not normalized_bound:
@@ -507,8 +507,8 @@ def _matches_actor_case_insensitive(bound_username, owners, modified_by):
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: GET /api/dashboards applies profile-default filter with owners OR modified_by trim+case-insensitive semantics.
# @BRIEF Validate profile-default filtering matches owner and modifier aliases using normalized Superset actor values.
# @PRE: Current user has enabled profile-default preference and bound username.
# @POST: Response includes only matching dashboards and effective_profile_filter metadata.
# @PRE Current user has enabled profile-default preference and bound username.
# @POST Response includes only matching dashboards and effective_profile_filter metadata.
def test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"
@@ -566,8 +566,8 @@ def test_get_dashboards_profile_filter_contract_owners_or_modified_by(mock_deps)
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: GET /api/dashboards honors override_show_all and disables profile-default filter for current page.
# @BRIEF Validate override_show_all bypasses profile-default filtering without changing dashboard list semantics.
# @PRE: Profile-default preference exists but override_show_all=true query is provided.
# @POST: Response remains unfiltered and effective_profile_filter.applied is false.
# @PRE Profile-default preference exists but override_show_all=true query is provided.
# @POST Response remains unfiltered and effective_profile_filter.applied is false.
def test_get_dashboards_override_show_all_contract(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"
@@ -619,8 +619,8 @@ def test_get_dashboards_override_show_all_contract(mock_deps):
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: GET /api/dashboards returns empty result set when profile-default filter is active and no dashboard actors match.
# @BRIEF Validate profile-default filtering returns an empty dashboard page when no actor aliases match the bound user.
# @PRE: Profile-default preference is enabled with bound username and all dashboards are non-matching.
# @POST: Response total is 0 with deterministic pagination and active effective_profile_filter metadata.
# @PRE Profile-default preference is enabled with bound username and all dashboards are non-matching.
# @POST Response total is 0 with deterministic pagination and active effective_profile_filter metadata.
def test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"
@@ -674,8 +674,8 @@ def test_get_dashboards_profile_filter_no_match_results_contract(mock_deps):
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: GET /api/dashboards does not auto-apply profile-default filter outside dashboards_main page context.
# @BRIEF Validate non-dashboard page contexts suppress profile-default filtering and preserve unfiltered results.
# @PRE: Profile-default preference exists but page_context=other query is provided.
# @POST: Response remains unfiltered and metadata reflects source_page=other.
# @PRE Profile-default preference exists but page_context=other query is provided.
# @POST Response remains unfiltered and metadata reflects source_page=other.
def test_get_dashboards_page_context_other_disables_profile_default(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"
@@ -727,8 +727,8 @@ def test_get_dashboards_page_context_other_disables_profile_default(mock_deps):
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: GET /api/dashboards resolves Superset display-name alias once and filters without per-dashboard detail calls.
# @BRIEF Validate profile-default filtering reuses resolved Superset display aliases without triggering per-dashboard detail fanout.
# @PRE: Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.
# @POST: Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.
# @PRE Profile-default filter is active, bound username is `admin`, dashboard actors contain display labels.
# @POST Route matches by alias (`Superset Admin`) and does not call `SupersetClient.get_dashboard` in list filter path.
def test_get_dashboards_profile_filter_matches_display_alias_without_detail_fanout(
mock_deps,
):
@@ -803,8 +803,8 @@ def test_get_dashboards_profile_filter_matches_display_alias_without_detail_fano
# @RELATION BINDS_TO -> DashboardsApiTests
# @TEST: GET /api/dashboards profile-default filter matches Superset owner object payloads.
# @BRIEF Validate profile-default filtering accepts owner object payloads once aliases resolve to the bound Superset username.
# @PRE: Profile-default preference is enabled and owners list contains dict payloads.
# @POST: Response keeps dashboards where owner object resolves to bound username alias.
# @PRE Profile-default preference is enabled and owners list contains dict payloads.
# @POST Response keeps dashboards where owner object resolves to bound username alias.
def test_get_dashboards_profile_filter_matches_owner_object_payload_contract(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"

View File

@@ -1,8 +1,8 @@
# #region DatasetReviewApiTests [TYPE Module] [C:3] [SEMANTICS dataset_review, api, tests, lifecycle, exports, orchestration]
# @BRIEF Verify backend US1 dataset review lifecycle, export, parsing, and dictionary-resolution contracts.
# @LAYER: API
# @RELATION [BINDS_TO] ->[DatasetReviewApi]
# @RELATION [BINDS_TO] ->[DatasetReviewOrchestrator]
# @LAYER API
# @RELATION BINDS_TO ->[DatasetReviewApi]
# @RELATION BINDS_TO ->[DatasetReviewOrchestrator]
from datetime import UTC, datetime
import pytest
from types import SimpleNamespace

View File

@@ -1,8 +1,8 @@
# #region DatasetsApiTests [TYPE Module] [C:3] [SEMANTICS datasets, api, tests, pagination, mapping, docs]
# @BRIEF Unit tests for datasets API endpoints.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [DatasetsApi]
# @INVARIANT: Endpoint contracts remain stable for success and validation failure paths.
# @INVARIANT Endpoint contracts remain stable for success and validation failure paths.
import pytest
from unittest.mock import AsyncMock, MagicMock
@@ -31,11 +31,11 @@ def mock_deps():
"""Bare MagicMock — no spec guards. All service method calls succeed silently.
Authorization, data integrity, and error paths are invisible to this fixture.
"""
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
# @INVARIANT unconstrained mock — no spec= enforced; attribute typos will silently pass
config_manager = MagicMock()
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
# @INVARIANT unconstrained mock — no spec= enforced; attribute typos will silently pass
task_manager = MagicMock()
# @INVARIANT: unconstrained mock — no spec= enforced; attribute typos will silently pass
# @INVARIANT unconstrained mock — no spec= enforced; attribute typos will silently pass
resource_service = MagicMock()
mapping_service = MagicMock()
app.dependency_overrides[get_config_manager] = lambda: config_manager
@@ -62,11 +62,11 @@ def mock_deps():
app.dependency_overrides.clear()
client = TestClient(app)
# #region test_get_datasets_success [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate successful datasets listing contract for an existing environment.
# @TEST: GET /api/datasets returns 200 and valid schema
# @PRE: env_id exists
# @POST: Response matches DatasetsResponse schema
# @PRE env_id exists
# @POST Response matches DatasetsResponse schema
def test_get_datasets_success(mock_deps):
# Mock environment
mock_env = MagicMock()
@@ -94,11 +94,11 @@ def test_get_datasets_success(mock_deps):
DatasetsResponse(**data)
# #endregion test_get_datasets_success
# #region test_get_datasets_env_not_found [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate datasets listing returns 404 when the requested environment does not exist.
# @TEST: GET /api/datasets returns 404 if env_id missing
# @PRE: env_id does not exist
# @POST: Returns 404 error
# @PRE env_id does not exist
# @POST Returns 404 error
def test_get_datasets_env_not_found(mock_deps):
mock_deps["config"].get_environments.return_value = []
response = client.get("/api/datasets?env_id=nonexistent")
@@ -106,11 +106,11 @@ def test_get_datasets_env_not_found(mock_deps):
assert "Environment not found" in response.json()["detail"]
# #endregion test_get_datasets_env_not_found
# #region test_get_datasets_invalid_pagination [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate datasets listing rejects invalid pagination parameters with 400 responses.
# @TEST: GET /api/datasets returns 400 for invalid page/page_size
# @PRE: page < 1 or page_size > 100
# @POST: Returns 400 error
# @PRE page < 1 or page_size > 100
# @POST Returns 400 error
def test_get_datasets_invalid_pagination(mock_deps):
mock_env = MagicMock()
mock_env.id = "prod"
@@ -123,17 +123,17 @@ def test_get_datasets_invalid_pagination(mock_deps):
response = client.get("/api/datasets?env_id=prod&page_size=0")
assert response.status_code == 400
assert "Page size must be between 1 and 100" in response.json()["detail"]
# @TEST_EDGE: page_size > 100 exceeds max
# @TEST_EDGE page_size > 100 exceeds max
response = client.get("/api/datasets?env_id=prod&page_size=101")
assert response.status_code == 400
assert "Page size must be between 1 and 100" in response.json()["detail"]
# #endregion test_get_datasets_invalid_pagination
# #region test_map_columns_success [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate map-columns request creates an async mapping task and returns its identifier.
# @TEST: POST /api/datasets/map-columns creates mapping task
# @PRE: Valid env_id, dataset_ids, source_type (sqllab)
# @POST: Returns task_id
# @PRE Valid env_id, dataset_ids, source_type (sqllab)
# @POST Returns task_id
def test_map_columns_success(mock_deps):
# Mock environment
mock_env = MagicMock()
@@ -154,11 +154,11 @@ def test_map_columns_success(mock_deps):
mock_deps["task"].create_task.assert_called_once()
# #endregion test_map_columns_success
# #region test_map_columns_invalid_source_type [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate map-columns rejects unsupported source types with a 400 contract response.
# @TEST: POST /api/datasets/map-columns returns 400 for invalid source_type
# @PRE: source_type is not 'sqllab' or 'xlsx'
# @POST: Returns 400 error
# @PRE source_type is not 'sqllab' or 'xlsx'
# @POST Returns 400 error
def test_map_columns_invalid_source_type(mock_deps):
response = client.post(
"/api/datasets/map-columns",
@@ -168,11 +168,11 @@ def test_map_columns_invalid_source_type(mock_deps):
assert "Source type must be 'sqllab' or 'xlsx'" in response.json()["detail"]
# #endregion test_map_columns_invalid_source_type
# #region test_generate_docs_success [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @TEST: POST /api/datasets/generate-docs creates doc generation task
# @PRE: Valid env_id, dataset_ids, llm_provider
# @PRE Valid env_id, dataset_ids, llm_provider
# @BRIEF Validate generate-docs request creates an async documentation task and returns its identifier.
# @POST: Returns task_id
# @POST Returns task_id
def test_generate_docs_success(mock_deps):
# Mock environment
mock_env = MagicMock()
@@ -193,11 +193,11 @@ def test_generate_docs_success(mock_deps):
mock_deps["task"].create_task.assert_called_once()
# #endregion test_generate_docs_success
# #region test_map_columns_empty_ids [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate map-columns rejects empty dataset identifier lists.
# @TEST: POST /api/datasets/map-columns returns 400 for empty dataset_ids
# @PRE: dataset_ids is empty
# @POST: Returns 400 error
# @PRE dataset_ids is empty
# @POST Returns 400 error
def test_map_columns_empty_ids(mock_deps):
"""@PRE: dataset_ids must be non-empty."""
response = client.post(
@@ -208,10 +208,10 @@ def test_map_columns_empty_ids(mock_deps):
assert "At least one dataset ID must be provided" in response.json()["detail"]
# #endregion test_map_columns_empty_ids
# #region test_map_columns_missing_database_id [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate map-columns rejects sqllab source without database_id.
# @TEST: POST /api/datasets/map-columns returns 400 for sqllab without database_id
# @POST: Returns 400 error
# @POST Returns 400 error
def test_map_columns_missing_database_id(mock_deps):
response = client.post(
"/api/datasets/map-columns",
@@ -221,11 +221,11 @@ def test_map_columns_missing_database_id(mock_deps):
assert "database_id is required" in response.json()["detail"]
# #endregion test_map_columns_missing_database_id
# #region test_generate_docs_empty_ids [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate generate-docs rejects empty dataset identifier lists.
# @TEST: POST /api/datasets/generate-docs returns 400 for empty dataset_ids
# @PRE: dataset_ids is empty
# @POST: Returns 400 error
# @PRE dataset_ids is empty
# @POST Returns 400 error
def test_generate_docs_empty_ids(mock_deps):
"""@PRE: dataset_ids must be non-empty."""
response = client.post(
@@ -236,11 +236,11 @@ def test_generate_docs_empty_ids(mock_deps):
assert "At least one dataset ID must be provided" in response.json()["detail"]
# #endregion test_generate_docs_empty_ids
# #region test_generate_docs_env_not_found [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @TEST: POST /api/datasets/generate-docs returns 404 for missing env
# @PRE: env_id does not exist
# @PRE env_id does not exist
# @BRIEF Validate generate-docs returns 404 when the requested environment cannot be resolved.
# @POST: Returns 404 error
# @POST Returns 404 error
def test_generate_docs_env_not_found(mock_deps):
"""@PRE: env_id must be a valid environment."""
mock_deps["config"].get_environments.return_value = []
@@ -252,10 +252,10 @@ def test_generate_docs_env_not_found(mock_deps):
assert "Environment not found" in response.json()["detail"]
# #endregion test_generate_docs_env_not_found
# #region test_get_datasets_superset_failure [TYPE Function]
# @RELATION BINDS_TO -> [DatasetsApiTests:Module]
# @RELATION BINDS_TO -> [DatasetsApiTests]
# @BRIEF Validate datasets listing surfaces a 503 contract when Superset access fails.
# @TEST_EDGE: external_superset_failure -> {status: 503}
# @POST: Returns 503 with stable error detail when upstream dataset fetch fails.
# @TEST_EDGE external_superset_failure -> {status: 503}
# @POST Returns 503 with stable error detail when upstream dataset fetch fails.
def test_get_datasets_superset_failure(mock_deps):
"""@TEST_EDGE: external_superset_failure -> {status: 503}"""
mock_env = MagicMock()

View File

@@ -1,5 +1,5 @@
# #region TestGitApi [TYPE Module] [C:3] [SEMANTICS test, git, api, config, repository]
# @RELATION VERIFIES -> [GitApi]
# @RELATION BINDS_TO -> [EXT:frontend:GitApi]
# @BRIEF API tests for Git configurations and repository operations.
import asyncio
import pytest
@@ -14,7 +14,7 @@ from src.models.git import GitProvider, GitRepository, GitServerConfig, GitStatu
# #region DbMock [TYPE Class] [C:2]
# @RELATION BINDS_TO -> [TestGitApi]
# @BRIEF In-memory session double for git route tests with minimal query/filter persistence semantics.
# @INVARIANT: Supports only the SQLAlchemy-like operations exercised by this test module.
# @INVARIANT Supports only the SQLAlchemy-like operations exercised by this test module.
class DbMock:
def __init__(self, data=None):
self._data = data or []

View File

@@ -1,7 +1,7 @@
# #region TestGitStatusRoute [TYPE Module] [C:3] [SEMANTICS tests, git, api, status, no_repo]
# @BRIEF Validate status endpoint behavior for missing and error repository states.
# @LAYER: Domain
# @RELATION VERIFIES -> [GitApi]
# @LAYER Domain
# @RELATION BINDS_TO -> [EXT:frontend:GitApi]
import asyncio
import pytest
from unittest.mock import MagicMock
@@ -14,8 +14,8 @@ from src.api.routes import git as git_routes
# #region test_get_repository_status_returns_no_repo_payload_for_missing_repo [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure missing local repository is represented as NO_REPO payload instead of an API error.
# @PRE: GitService.get_status raises HTTPException(404).
# @POST: Route returns a deterministic NO_REPO status payload.
# @PRE GitService.get_status raises HTTPException(404).
# @POST Route returns a deterministic NO_REPO status payload.
def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypatch):
class MissingRepoGitService:
def _get_repo_path(self, dashboard_id: int) -> str:
@@ -32,8 +32,8 @@ def test_get_repository_status_returns_no_repo_payload_for_missing_repo(monkeypa
# #region test_get_repository_status_propagates_non_404_http_exception [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure HTTP exceptions other than 404 are not masked.
# @PRE: GitService.get_status raises HTTPException with non-404 status.
# @POST: Raised exception preserves original status and detail.
# @PRE GitService.get_status raises HTTPException with non-404 status.
# @POST Raised exception preserves original status and detail.
def test_get_repository_status_propagates_non_404_http_exception(monkeypatch):
class ConflictGitService:
def _get_repo_path(self, dashboard_id: int) -> str:
@@ -50,8 +50,8 @@ def test_get_repository_status_propagates_non_404_http_exception(monkeypatch):
# #region test_get_repository_diff_propagates_http_exception [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure diff endpoint preserves domain HTTP errors from GitService.
# @PRE: GitService.get_diff raises HTTPException.
# @POST: Endpoint raises same HTTPException values.
# @PRE GitService.get_diff raises HTTPException.
# @POST Endpoint raises same HTTPException values.
def test_get_repository_diff_propagates_http_exception(monkeypatch):
class DiffGitService:
def get_diff(self, dashboard_id: int, file_path=None, staged: bool = False) -> str:
@@ -65,8 +65,8 @@ def test_get_repository_diff_propagates_http_exception(monkeypatch):
# #region test_get_history_wraps_unexpected_error_as_500 [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure non-HTTP exceptions in history endpoint become deterministic 500 errors.
# @PRE: GitService.get_commit_history raises ValueError.
# @POST: Endpoint returns HTTPException with status 500 and route context.
# @PRE GitService.get_commit_history raises ValueError.
# @POST Endpoint returns HTTPException with status 500 and route context.
def test_get_history_wraps_unexpected_error_as_500(monkeypatch):
class HistoryGitService:
def get_commit_history(self, dashboard_id: int, limit: int = 50):
@@ -80,8 +80,8 @@ def test_get_history_wraps_unexpected_error_as_500(monkeypatch):
# #region test_commit_changes_wraps_unexpected_error_as_500 [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure commit endpoint does not leak unexpected errors as 400.
# @PRE: GitService.commit_changes raises RuntimeError.
# @POST: Endpoint raises HTTPException(500) with route context.
# @PRE GitService.commit_changes raises RuntimeError.
# @POST Endpoint raises HTTPException(500) with route context.
def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):
class CommitGitService:
def commit_changes(self, dashboard_id: int, message: str, files):
@@ -98,8 +98,8 @@ def test_commit_changes_wraps_unexpected_error_as_500(monkeypatch):
# #region test_get_repository_status_batch_returns_mixed_statuses [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure batch endpoint returns per-dashboard statuses in one response.
# @PRE: Some repositories are missing and some are initialized.
# @POST: Returned map includes resolved status for each requested dashboard ID.
# @PRE Some repositories are missing and some are initialized.
# @POST Returned map includes resolved status for each requested dashboard ID.
def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):
class BatchGitService:
def _get_repo_path(self, dashboard_id: int) -> str:
@@ -119,8 +119,8 @@ def test_get_repository_status_batch_returns_mixed_statuses(monkeypatch):
# #region test_get_repository_status_batch_marks_item_as_error_on_service_failure [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure batch endpoint marks failed items as ERROR without failing entire request.
# @PRE: GitService raises non-HTTP exception for one dashboard.
# @POST: Failed dashboard status is marked as ERROR.
# @PRE GitService raises non-HTTP exception for one dashboard.
# @POST Failed dashboard status is marked as ERROR.
def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monkeypatch):
class BatchErrorGitService:
def _get_repo_path(self, dashboard_id: int) -> str:
@@ -138,8 +138,8 @@ def test_get_repository_status_batch_marks_item_as_error_on_service_failure(monk
# #region test_get_repository_status_batch_deduplicates_and_truncates_ids [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure batch endpoint protects server from oversized payloads.
# @PRE: request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.
# @POST: Result contains unique IDs up to configured cap.
# @PRE request includes duplicate IDs and more than MAX_REPOSITORY_STATUS_BATCH entries.
# @POST Result contains unique IDs up to configured cap.
def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch):
class SafeBatchGitService:
def _get_repo_path(self, dashboard_id: int) -> str:
@@ -157,8 +157,8 @@ def test_get_repository_status_batch_deduplicates_and_truncates_ids(monkeypatch)
# #region test_commit_changes_applies_profile_identity_before_commit [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure commit route configures repository identity from profile preferences before commit call.
# @PRE: Profile preference contains git_username/git_email for current user.
# @POST: git_service.configure_identity receives resolved identity and commit proceeds.
# @PRE Profile preference contains git_username/git_email for current user.
# @POST git_service.configure_identity receives resolved identity and commit proceeds.
def test_commit_changes_applies_profile_identity_before_commit(monkeypatch):
class IdentityGitService:
def __init__(self):
@@ -206,8 +206,8 @@ def test_commit_changes_applies_profile_identity_before_commit(monkeypatch):
# #region test_pull_changes_applies_profile_identity_before_pull [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure pull route configures repository identity from profile preferences before pull call.
# @PRE: Profile preference contains git_username/git_email for current user.
# @POST: git_service.configure_identity receives resolved identity and pull proceeds.
# @PRE Profile preference contains git_username/git_email for current user.
# @POST git_service.configure_identity receives resolved identity and pull proceeds.
def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
class IdentityGitService:
def __init__(self):
@@ -251,8 +251,8 @@ def test_pull_changes_applies_profile_identity_before_pull(monkeypatch):
# #region test_get_merge_status_returns_service_payload [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure merge status route returns service payload as-is.
# @PRE: git_service.get_merge_status returns unfinished merge payload.
# @POST: Route response contains has_unfinished_merge=True.
# @PRE git_service.get_merge_status returns unfinished merge payload.
# @POST Route response contains has_unfinished_merge=True.
def test_get_merge_status_returns_service_payload(monkeypatch):
class MergeStatusGitService:
def get_merge_status(self, dashboard_id: int) -> dict:
@@ -279,8 +279,8 @@ def test_get_merge_status_returns_service_payload(monkeypatch):
# #region test_resolve_merge_conflicts_passes_resolution_items_to_service [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure merge resolve route forwards parsed resolutions to service.
# @PRE: resolve_data has one file strategy.
# @POST: Service receives normalized list and route returns resolved files.
# @PRE resolve_data has one file strategy.
# @POST Service receives normalized list and route returns resolved files.
def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch):
captured = {}
class MergeResolveGitService:
@@ -309,8 +309,8 @@ def test_resolve_merge_conflicts_passes_resolution_items_to_service(monkeypatch)
# #region test_abort_merge_calls_service_and_returns_result [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure abort route delegates to service.
# @PRE: Service abort_merge returns aborted status.
# @POST: Route returns aborted status.
# @PRE Service abort_merge returns aborted status.
# @POST Route returns aborted status.
def test_abort_merge_calls_service_and_returns_result(monkeypatch):
class AbortGitService:
def abort_merge(self, dashboard_id: int):
@@ -329,8 +329,8 @@ def test_abort_merge_calls_service_and_returns_result(monkeypatch):
# #region test_continue_merge_passes_message_and_returns_commit [TYPE Function]
# @RELATION BINDS_TO -> TestGitStatusRoute
# @BRIEF Ensure continue route passes commit message to service.
# @PRE: continue_data.message is provided.
# @POST: Route returns committed status and hash.
# @PRE continue_data.message is provided.
# @POST Route returns committed status and hash.
def test_continue_merge_passes_message_and_returns_commit(monkeypatch):
class ContinueGitService:
def continue_merge(self, dashboard_id: int, message: str):

View File

@@ -1,8 +1,8 @@
# #region TestMigrationRoutes [TYPE Module] [C:3] [SEMANTICS test, migration, api, route, handler]
#
# @BRIEF Unit tests for migration API route handlers.
# @LAYER: API
# @RELATION VERIFIES -> backend.src.api.routes.migration
# @LAYER API
# @RELATION BINDS_TO -> [EXT:path:backend.src.api.routes.migration]
#
from datetime import UTC, datetime
from pathlib import Path
@@ -420,8 +420,8 @@ async def test_execute_migration_invalid_env_raises_400(_mock_env):
assert exc.value.status_code == 400
@pytest.mark.asyncio
async def test_dry_run_migration_returns_diff_and_risk(db_session):
# @TEST_EDGE: missing_target_datasource -> validates high risk item generation
# @TEST_EDGE: breaking_reference -> validates high risk on missing dataset link
# @TEST_EDGE missing_target_datasource -> validates high risk item generation
# @TEST_EDGE breaking_reference -> validates high risk on missing dataset link
from src.api.routes.migration import dry_run_migration
from src.models.dashboard import DashboardSelection
env_source = MagicMock()

View File

@@ -1,7 +1,7 @@
# #region TestProfileApi [TYPE Module] [C:3] [SEMANTICS tests, profile, api, preferences, lookup, contract]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Verifies profile API route contracts for preference read/update and Superset account lookup.
# @LAYER: API
# @LAYER API
# [SECTION: IMPORTS]
from datetime import UTC, datetime
from unittest.mock import MagicMock, patch
@@ -30,8 +30,8 @@ client = TestClient(app)
# #region mock_profile_route_dependencies [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Provides deterministic dependency overrides for profile route tests.
# @PRE: App instance is initialized.
# @POST: Dependencies are overridden for current test and restored afterward.
# @PRE App instance is initialized.
# @POST Dependencies are overridden for current test and restored afterward.
def mock_profile_route_dependencies():
mock_user = MagicMock()
mock_user.id = "u-1"
@@ -46,8 +46,8 @@ def mock_profile_route_dependencies():
# #region profile_route_deps_fixture [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Pytest fixture wrapper for profile route dependency overrides.
# @PRE: None.
# @POST: Yields overridden dependencies and clears overrides after test.
# @PRE None.
# @POST Yields overridden dependencies and clears overrides after test.
import pytest
@@ -60,8 +60,8 @@ def profile_route_deps_fixture():
# #region _build_preference_response [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Builds stable profile preference response payload for route tests.
# @PRE: user_id is provided.
# @POST: Returns ProfilePreferenceResponse object with deterministic timestamps.
# @PRE user_id is provided.
# @POST Returns ProfilePreferenceResponse object with deterministic timestamps.
def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceResponse:
now = datetime.now(UTC)
return ProfilePreferenceResponse(
@@ -99,8 +99,8 @@ def _build_preference_response(user_id: str = "u-1") -> ProfilePreferenceRespons
# #region test_get_profile_preferences_returns_self_payload [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Verifies GET /api/profile/preferences returns stable self-scoped payload.
# @PRE: Authenticated user context is available.
# @POST: Response status is 200 and payload contains current user preference.
# @PRE Authenticated user context is available.
# @POST Response status is 200 and payload contains current user preference.
def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture):
mock_user, _, _ = profile_route_deps_fixture
service = MagicMock()
@@ -128,8 +128,8 @@ def test_get_profile_preferences_returns_self_payload(profile_route_deps_fixture
# #region test_patch_profile_preferences_success [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Verifies PATCH /api/profile/preferences persists valid payload through route mapping.
# @PRE: Valid request payload and authenticated user.
# @POST: Response status is 200 with saved preference payload.
# @PRE Valid request payload and authenticated user.
# @POST Response status is 200 with saved preference payload.
def test_patch_profile_preferences_success(profile_route_deps_fixture):
mock_user, _, _ = profile_route_deps_fixture
service = MagicMock()
@@ -174,8 +174,8 @@ def test_patch_profile_preferences_success(profile_route_deps_fixture):
# #region test_patch_profile_preferences_validation_error [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Verifies route maps domain validation failure to HTTP 422 with actionable details.
# @PRE: Service raises ProfileValidationError.
# @POST: Response status is 422 and includes validation messages.
# @PRE Service raises ProfileValidationError.
# @POST Response status is 422 and includes validation messages.
def test_patch_profile_preferences_validation_error(profile_route_deps_fixture):
service = MagicMock()
service.update_my_preference.side_effect = ProfileValidationError(
@@ -197,8 +197,8 @@ def test_patch_profile_preferences_validation_error(profile_route_deps_fixture):
# #region test_patch_profile_preferences_cross_user_denied [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Verifies route maps domain authorization guard failure to HTTP 403.
# @PRE: Service raises ProfileAuthorizationError.
# @POST: Response status is 403 with denial message.
# @PRE Service raises ProfileAuthorizationError.
# @POST Response status is 403 with denial message.
def test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture):
service = MagicMock()
service.update_my_preference.side_effect = ProfileAuthorizationError(
@@ -219,8 +219,8 @@ def test_patch_profile_preferences_cross_user_denied(profile_route_deps_fixture)
# #region test_lookup_superset_accounts_success [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Verifies lookup route returns success payload with normalized candidates.
# @PRE: Valid environment_id and service success response.
# @POST: Response status is 200 and items list is returned.
# @PRE Valid environment_id and service success response.
# @POST Response status is 200 and items list is returned.
def test_lookup_superset_accounts_success(profile_route_deps_fixture):
service = MagicMock()
service.lookup_superset_accounts.return_value = SupersetAccountLookupResponse(
@@ -252,8 +252,8 @@ def test_lookup_superset_accounts_success(profile_route_deps_fixture):
# #region test_lookup_superset_accounts_env_not_found [TYPE Function]
# @RELATION BINDS_TO -> TestProfileApi
# @BRIEF Verifies lookup route maps missing environment to HTTP 404.
# @PRE: Service raises EnvironmentNotFoundError.
# @POST: Response status is 404 with explicit message.
# @PRE Service raises EnvironmentNotFoundError.
# @POST Response status is 404 with explicit message.
def test_lookup_superset_accounts_env_not_found(profile_route_deps_fixture):
service = MagicMock()
service.lookup_superset_accounts.side_effect = EnvironmentNotFoundError(

View File

@@ -1,8 +1,8 @@
# #region TestReportsApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, contract, pagination, filtering]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Contract tests for GET /api/reports defaults, pagination, and filtering behavior.
# @LAYER: Domain
# @INVARIANT: API response contract contains {items,total,page,page_size,has_next,applied_filters}.
# @LAYER Domain
# @INVARIANT API response contract contains {items,total,page,page_size,has_next,applied_filters}.
from datetime import UTC, datetime, timedelta
from types import SimpleNamespace
@@ -17,7 +17,7 @@ from src.dependencies import get_current_user, get_task_manager
# #region _FakeTaskManager [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestReportsApi]
# @BRIEF Minimal task-manager double exposing only get_all_tasks used by reports route tests.
# @INVARIANT: Returns pre-seeded tasks without mutation or side effects.
# @INVARIANT Returns pre-seeded tasks without mutation or side effects.
class _FakeTaskManager:
def __init__(self, tasks):
self._tasks = tasks

View File

@@ -1,8 +1,8 @@
# #region TestReportsDetailApi [TYPE Module] [C:3] [SEMANTICS tests, reports, api, detail, diagnostics]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Contract tests for GET /api/reports/{report_id} detail endpoint behavior.
# @LAYER: Domain
# @INVARIANT: Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
# @LAYER Domain
# @INVARIANT Detail endpoint tests must keep deterministic assertions for success and not-found contracts.
from datetime import datetime, timedelta
from types import SimpleNamespace
@@ -17,7 +17,7 @@ from src.dependencies import get_current_user, get_task_manager
# #region _FakeTaskManager [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestReportsDetailApi]
# @BRIEF Minimal task-manager double exposing pre-seeded tasks to detail endpoint under test.
# @INVARIANT: get_all_tasks returns exactly seeded tasks list.
# @INVARIANT get_all_tasks returns exactly seeded tasks list.
class _FakeTaskManager:
def __init__(self, tasks):
self._tasks = tasks

View File

@@ -1,8 +1,8 @@
# #region TestReportsOpenapiConformance [TYPE Module] [C:3] [SEMANTICS tests, reports, openapi, conformance]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Validate implemented reports payload shape against OpenAPI-required top-level contract fields.
# @LAYER: Domain
# @INVARIANT: List and detail payloads include required contract keys.
# @LAYER Domain
# @INVARIANT List and detail payloads include required contract keys.
from datetime import datetime
from types import SimpleNamespace
@@ -16,7 +16,7 @@ from src.dependencies import get_current_user, get_task_manager
# #region _FakeTaskManager [TYPE Class] [C:1]
# @RELATION BINDS_TO -> [TestReportsOpenapiConformance]
# @BRIEF Minimal task-manager fake exposing static task list for OpenAPI conformance checks.
# @INVARIANT: get_all_tasks returns seeded tasks unchanged.
# @INVARIANT get_all_tasks returns seeded tasks unchanged.
class _FakeTaskManager:
def __init__(self, tasks):
self._tasks = tasks

View File

@@ -1,7 +1,7 @@
# #region test_tasks_logs_module [TYPE Module] [C:2] [SEMANTICS tests, tasks, logs, api, contract, validation]
# @RELATION VERIFIES -> [src.api.routes.tasks:Module]
# @RELATION BINDS_TO -> [EXT:frontend:TasksModule]
# @BRIEF Contract testing for task logs API endpoints.
# @LAYER: Domain
# @LAYER Domain
import pytest
from unittest.mock import MagicMock
@@ -12,20 +12,20 @@ from src.api.routes.tasks import router
from src.dependencies import get_task_manager, has_permission
# @TEST_FIXTURE: mock_app
# @TEST_FIXTURE mock_app
@pytest.fixture
def client():
app = FastAPI()
app.include_router(router, prefix="/tasks")
# Mock TaskManager
# @INVARIANT: unconstrained mock — no spec= enforced
# @INVARIANT unconstrained mock — no spec= enforced
mock_tm = MagicMock()
app.dependency_overrides[get_task_manager] = lambda: mock_tm
# Mock permissions (bypass for unit test)
app.dependency_overrides[has_permission("tasks", "READ")] = lambda: True
return TestClient(app), mock_tm
# @TEST_CONTRACT: get_task_logs_api -> Invariants
# @TEST_FIXTURE: valid_task_logs_request
# @TEST_CONTRACT get_task_logs_api -> Invariants
# @TEST_FIXTURE valid_task_logs_request
# #region test_get_task_logs_success [TYPE Function]
# @RELATION BINDS_TO -> test_tasks_logs_module
# @BRIEF Validate task logs endpoint returns filtered logs for an existing task.
@@ -43,7 +43,7 @@ def test_get_task_logs_success(client):
args = tm.get_task_logs.call_args
assert args[0][0] == "task-1"
assert args[0][1].level == "INFO"
# @TEST_EDGE: task_not_found
# @TEST_EDGE task_not_found
# #endregion test_get_task_logs_success
# #region test_get_task_logs_not_found [TYPE Function]
# @RELATION BINDS_TO -> test_tasks_logs_module
@@ -54,7 +54,7 @@ def test_get_task_logs_not_found(client):
response = tc.get("/tasks/missing/logs")
assert response.status_code == 404
assert response.json()["detail"] == "Task not found"
# @TEST_EDGE: invalid_limit
# @TEST_EDGE invalid_limit
# #endregion test_get_task_logs_not_found
# #region test_get_task_logs_invalid_limit [TYPE Function]
# @RELATION BINDS_TO -> test_tasks_logs_module
@@ -64,7 +64,7 @@ def test_get_task_logs_invalid_limit(client):
# limit=0 is ge=1 in Query
response = tc.get("/tasks/task-1/logs?limit=0")
assert response.status_code == 422
# @TEST_INVARIANT: response_purity
# @TEST_INVARIANT response_purity
# #endregion test_get_task_logs_invalid_limit
# #region test_get_task_log_stats_success [TYPE Function]
# @RELATION BINDS_TO -> test_tasks_logs_module

View File

@@ -1,12 +1,12 @@
# #region AdminApi [C:5] [TYPE Module] [SEMANTICS fastapi, admin, api, rbac, user]
#
# @BRIEF Admin API endpoints for user and role management.
# @LAYER: API
# @RELATION DEPENDS_ON -> [AuthRepository:Class]
# @RELATION DEPENDS_ON -> [get_auth_db:Function]
# @RELATION DEPENDS_ON -> [has_permission:Function]
# @LAYER API
# @RELATION DEPENDS_ON -> [AuthRepository]
# @RELATION DEPENDS_ON -> [get_auth_db]
# @RELATION DEPENDS_ON -> [has_permission]
#
# @INVARIANT: All endpoints in this module require 'Admin' role or 'admin' scope.
# @INVARIANT All endpoints in this module require 'Admin' role or 'admin' scope.
from fastapi import APIRouter, Depends, HTTPException, status
@@ -43,8 +43,8 @@ router = APIRouter(prefix="/api/admin", tags=["admin"])
# #region list_users [C:3] [TYPE Function]
# @BRIEF Lists all registered users.
# @PRE: Current user has 'Admin' role.
# @POST: Returns a list of UserSchema objects.
# @PRE Current user has 'Admin' role.
# @POST Returns a list of UserSchema objects.
# @RELATION CALLS -> User
@router.get("/users", response_model=list[UserSchema])
async def list_users(
@@ -60,9 +60,9 @@ async def list_users(
# #region create_user [C:3] [TYPE Function]
# @BRIEF Creates a new local user.
# @PRE: Current user has 'Admin' role.
# @POST: New user is created in the database.
# @RELATION CALLS -> [AuthRepository:Class]
# @PRE Current user has 'Admin' role.
# @POST New user is created in the database.
# @RELATION CALLS -> [AuthRepository]
@router.post("/users", response_model=UserSchema, status_code=status.HTTP_201_CREATED)
async def create_user(
user_in: UserCreate,
@@ -98,8 +98,8 @@ async def create_user(
# #region update_user [C:3] [TYPE Function]
# @BRIEF Updates an existing user.
# @PRE: Current user has 'Admin' role.
# @POST: User record is updated in the database.
# @PRE Current user has 'Admin' role.
# @POST User record is updated in the database.
# @RELATION CALLS -> AuthRepository
@router.put("/users/{user_id}", response_model=UserSchema)
async def update_user(
@@ -138,8 +138,8 @@ async def update_user(
# #region delete_user [C:3] [TYPE Function]
# @BRIEF Deletes a user.
# @PRE: Current user has 'Admin' role.
# @POST: User record is removed from the database.
# @PRE Current user has 'Admin' role.
# @POST User record is removed from the database.
# @RELATION CALLS -> AuthRepository
@router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
@@ -175,7 +175,7 @@ async def delete_user(
# #region list_roles [C:3] [TYPE Function]
# @BRIEF Lists all available roles.
# @RELATION CALLS -> [Role:Class]
# @RELATION CALLS -> [Role]
@router.get("/roles", response_model=list[RoleSchema])
async def list_roles(
db: Session = Depends(get_auth_db), _=Depends(has_permission("admin:roles", "READ"))
@@ -189,10 +189,10 @@ async def list_roles(
# #region create_role [C:3] [TYPE Function]
# @BRIEF Creates a new system role with associated permissions.
# @PRE: Role name must be unique.
# @POST: New Role record is created in auth.db.
# @SIDE_EFFECT: Commits new role and associations to auth.db.
# @RELATION CALLS -> [get_permission_by_id:Function]
# @PRE Role name must be unique.
# @POST New Role record is created in auth.db.
# @SIDE_EFFECT Commits new role and associations to auth.db.
# @RELATION CALLS -> [get_permission_by_id]
@router.post("/roles", response_model=RoleSchema, status_code=status.HTTP_201_CREATED)
async def create_role(
role_in: RoleCreate,
@@ -226,10 +226,10 @@ async def create_role(
# #region update_role [C:3] [TYPE Function]
# @BRIEF Updates an existing role's metadata and permissions.
# @PRE: role_id must be a valid existing role UUID.
# @POST: Role record is updated in auth.db.
# @SIDE_EFFECT: Commits updates to auth.db.
# @RELATION CALLS -> [get_role_by_id:Function]
# @PRE role_id must be a valid existing role UUID.
# @POST Role record is updated in auth.db.
# @SIDE_EFFECT Commits updates to auth.db.
# @RELATION CALLS -> [get_role_by_id]
@router.put("/roles/{role_id}", response_model=RoleSchema)
async def update_role(
role_id: str,
@@ -269,10 +269,10 @@ async def update_role(
# #region delete_role [C:3] [TYPE Function]
# @BRIEF Removes a role from the system.
# @PRE: role_id must be a valid existing role UUID.
# @POST: Role record is removed from auth.db.
# @SIDE_EFFECT: Deletes record from auth.db and commits.
# @RELATION CALLS -> [get_role_by_id:Function]
# @PRE role_id must be a valid existing role UUID.
# @POST Role record is removed from auth.db.
# @SIDE_EFFECT Deletes record from auth.db and commits.
# @RELATION CALLS -> [get_role_by_id]
@router.delete("/roles/{role_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_role(
role_id: str,
@@ -295,7 +295,7 @@ async def delete_role(
# #region list_permissions [C:3] [TYPE Function]
# @BRIEF Lists all available system permissions for assignment.
# @POST: Returns a list of all PermissionSchema objects.
# @POST Returns a list of all PermissionSchema objects.
# @RELATION CALLS -> backend.src.core.auth.repository.AuthRepository.list_permissions
@router.get("/permissions", response_model=list[PermissionSchema])
async def list_permissions(
@@ -339,9 +339,9 @@ async def list_ad_mappings(
# #region create_ad_mapping [C:2] [TYPE Function]
# @RELATION DEPENDS_ON -> [ADGroupMapping:Class]
# @RELATION DEPENDS_ON -> [get_auth_db:Function]
# @RELATION DEPENDS_ON -> [has_permission:Function]
# @RELATION DEPENDS_ON -> [ADGroupMapping]
# @RELATION DEPENDS_ON -> [get_auth_db]
# @RELATION DEPENDS_ON -> [has_permission]
# @BRIEF Creates a new AD Group mapping.
@router.post("/ad-mappings", response_model=ADGroupMappingSchema)
async def create_ad_mapping(

View File

@@ -3,7 +3,7 @@
# @LAYER API
# @RELATION DEPENDS_ON -> [APIKeyModel]
# @RELATION DEPENDS_ON -> [APIKeyUtilities]
# @RELATION DEPENDS_ON -> [has_permission("admin:settings", "WRITE")]
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("admin:settings", "WRITE")]
# @INVARIANT GET /api/admin/api-keys NEVER returns key_hash or raw_key.
# @INVARIANT POST /api/admin/api-keys returns raw_key ONCE — never stored, never retrievable again.
# @INVARIANT DELETE /api/admin/api-keys/{id} soft-deletes (active=False), preserves row for audit.

View File

@@ -1,11 +1,11 @@
# #region AssistantApi [C:5] [TYPE Module] [SEMANTICS assistant, api, package, llm, execution]
# @BRIEF API routes for LLM assistant command parsing and safe execution orchestration.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DEPENDS_ON -> [AssistantMessageRecord]
# @RELATION DEPENDS_ON -> [AssistantConfirmationRecord]
# @RELATION DEPENDS_ON -> [AssistantAuditRecord]
# @INVARIANT: Risky operations are never executed without valid confirmation token.
# @INVARIANT Risky operations are never executed without valid confirmation token.
# Re-export public API for backward compatibility.
from ._admin_routes import delete_conversation, get_assistant_audit, get_history, list_conversations

View File

@@ -1,10 +1,10 @@
# #region AssistantAdminRoutes [C:5] [TYPE Module] [SEMANTICS assistant, admin, route, audit, conversation]
# @BRIEF FastAPI route handlers for assistant admin operations — conversation listing, deletion, history, audit.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantRoutes]
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @RELATION DEPENDS_ON -> [AssistantHistory]
# @INVARIANT: Audit endpoint requires tasks:READ permission.
# @INVARIANT Audit endpoint requires tasks:READ permission.
from __future__ import annotations
@@ -39,8 +39,8 @@ from ._schemas import (
# #region list_conversations [C:2] [TYPE Function]
# @BRIEF Return paginated conversation list for current user with archived flag and last message preview.
# @PRE: Authenticated user context and valid pagination params.
# @POST: Conversations are grouped by conversation_id sorted by latest activity descending.
# @PRE Authenticated user context and valid pagination params.
# @POST Conversations are grouped by conversation_id sorted by latest activity descending.
@router.get("/conversations")
async def list_conversations(
page: int = Query(1, ge=1),
@@ -135,8 +135,8 @@ async def list_conversations(
# #region delete_conversation [C:2] [TYPE Function]
# @BRIEF Soft-delete or hard-delete a conversation and clear its in-memory trace.
# @PRE: conversation_id belongs to current_user.
# @POST: Conversation records are removed from DB and CONVERSATIONS cache.
# @PRE conversation_id belongs to current_user.
# @POST Conversation records are removed from DB and CONVERSATIONS cache.
@router.delete("/conversations/{conversation_id}")
async def delete_conversation(
conversation_id: str,
@@ -181,8 +181,8 @@ async def delete_conversation(
@router.get("/history")
# #region get_history [TYPE Function]
# @BRIEF Retrieve paginated assistant conversation history for current user.
# @PRE: Authenticated user is available and page params are valid.
# @POST: Returns persistent messages and mirrored in-memory snapshot for diagnostics.
# @PRE Authenticated user is available and page params are valid.
# @POST Returns persistent messages and mirrored in-memory snapshot for diagnostics.
async def get_history(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
@@ -252,8 +252,8 @@ async def get_history(
@router.get("/audit")
# #region get_assistant_audit [TYPE Function]
# @BRIEF Return assistant audit decisions for current user from persistent and in-memory stores.
# @PRE: User has tasks:READ permission.
# @POST: Audit payload is returned in reverse chronological order from DB.
# @PRE User has tasks:READ permission.
# @POST Audit payload is returned in reverse chronological order from DB.
async def get_assistant_audit(
limit: int = Query(50, ge=1, le=500),
current_user: User = Depends(get_current_user),

View File

@@ -1,8 +1,8 @@
# #region AssistantCommandParser [C:4] [TYPE Module] [SEMANTICS assistant, command, parser, nlu, intent]
# @BRIEF Deterministic RU/EN command text parser that converts user messages into intent payloads.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantResolvers]
# @INVARIANT: Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
# @INVARIANT Every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
from __future__ import annotations
@@ -17,13 +17,13 @@ from ._resolvers import _extract_id, _is_production_env
# #region _parse_command [C:4] [TYPE Function]
# @BRIEF Deterministically parse RU/EN command text into intent payload.
# @DATA_CONTRACT: Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]
# @DATA_CONTRACT Input[message:str, config_manager:ConfigManager] -> Output[Dict[str,Any]{domain,operation,entities,confidence,risk_level,requires_confirmation}]
# @RELATION DEPENDS_ON -> [_extract_id]
# @RELATION DEPENDS_ON -> [_is_production_env]
# @SIDE_EFFECT: None (pure parsing logic).
# @PRE: message contains raw user text and config manager resolves environments.
# @POST: Returns intent dict with domain/operation/entities/confidence/risk fields.
# @INVARIANT: every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
# @SIDE_EFFECT None (pure parsing logic).
# @PRE message contains raw user text and config manager resolves environments.
# @POST Returns intent dict with domain/operation/entities/confidence/risk fields.
# @INVARIANT every return path includes domain, operation, entities, confidence, risk_level, requires_confirmation.
def _parse_command(message: str, config_manager: ConfigManager) -> dict[str, Any]:
with belief_scope('_parse_command'):
logger.reason('Belief protocol reasoning checkpoint for _parse_command')

View File

@@ -1,10 +1,10 @@
# #region AssistantDatasetReview [C:4] [TYPE Module] [SEMANTICS assistant, dataset, review, context, intent]
# @BRIEF Dataset review context loading and intent planning for the assistant API.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator]
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @RELATION DISPATCHES -> [AssistantDatasetReviewDispatch]
# @INVARIANT: Dataset review operations are always scoped to the owner's session.
# @INVARIANT Dataset review operations are always scoped to the owner's session.
from __future__ import annotations
@@ -31,9 +31,9 @@ from src.services.dataset_review.repositories.session_repository import (
# #region _serialize_dataset_review_context [C:4] [TYPE Function]
# @BRIEF Build assistant-safe dataset-review context snapshot with masked imported-filter payloads for session-scoped assistant routing.
# @RELATION DEPENDS_ON -> [DatasetReviewSession]
# @PRE: session_id is a valid active review session identifier.
# @POST: Returns a serializable dictionary containing the complete review context.
# @SIDE_EFFECT: Reads session data from the database.
# @PRE session_id is a valid active review session identifier.
# @POST Returns a serializable dictionary containing the complete review context.
# @SIDE_EFFECT Reads session data from the database.
def _serialize_dataset_review_context(session: DatasetReviewSession) -> dict[str, Any]:
with belief_scope('_serialize_dataset_review_context'):
logger.reason('Belief protocol reasoning checkpoint for _serialize_dataset_review_context')
@@ -51,9 +51,9 @@ def _serialize_dataset_review_context(session: DatasetReviewSession) -> dict[str
# #region _load_dataset_review_context [C:4] [TYPE Function]
# @BRIEF Load owner-scoped dataset-review context for assistant planning and grounded response generation.
# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]
# @PRE: session_id is a valid active review session identifier.
# @POST: Returns a loaded context object with session data and findings.
# @SIDE_EFFECT: Reads session data from the database.
# @PRE session_id is a valid active review session identifier.
# @POST Returns a loaded context object with session data and findings.
# @SIDE_EFFECT Reads session data from the database.
def _load_dataset_review_context(dataset_review_session_id: str | None, current_user: User, db: Session) -> dict[str, Any] | None:
with belief_scope('_load_dataset_review_context'):
if not dataset_review_session_id:

View File

@@ -1,10 +1,10 @@
# #region AssistantDatasetReviewDispatch [C:4] [TYPE Module] [SEMANTICS assistant, dataset, review, dispatch, confirm]
# @BRIEF Dispatch and confirmation handling for dataset-review assistant intents.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantDatasetReview]
# @RELATION DEPENDS_ON -> [DatasetReviewOrchestrator]
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @INVARIANT: Dataset review dispatch requires valid session version for write operations.
# @INVARIANT Dataset review dispatch requires valid session version for write operations.
from __future__ import annotations
@@ -60,9 +60,9 @@ def _dataset_review_conflict_http_exception(
# #region _dispatch_dataset_review_intent [C:4] [TYPE Function]
# @BRIEF Route confirmed dataset-review assistant intents through existing backend dataset-review APIs and orchestration boundaries.
# @RELATION CALLS -> DatasetReviewOrchestrator
# @PRE: context contains valid session data and user intent.
# @POST: Returns a structured response with planned actions and confirmations.
# @SIDE_EFFECT: May update session state and enqueue tasks.
# @PRE context contains valid session data and user intent.
# @POST Returns a structured response with planned actions and confirmations.
# @SIDE_EFFECT May update session state and enqueue tasks.
async def _dispatch_dataset_review_intent(
intent: dict[str, Any],
current_user: User,

View File

@@ -1,11 +1,11 @@
# #region AssistantDispatch [C:5] [TYPE Module] [SEMANTICS assistant, dispatch, confirm, execution, orchestration]
# @BRIEF Intent dispatch engine, confirmation summary, and clarification text for the assistant API.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @RELATION DEPENDS_ON -> [AssistantResolvers]
# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]
# @RELATION DEPENDS_ON -> [AssistantDatasetReview]
# @INVARIANT: Unsupported operations are rejected via HTTPException(400).
# @INVARIANT Unsupported operations are rejected via HTTPException(400).
from __future__ import annotations
@@ -42,8 +42,8 @@ git_service = GitService()
# #region _clarification_text_for_intent [C:2] [TYPE Function]
# @BRIEF Convert technical missing-parameter errors into user-facing clarification prompts.
# @PRE: state was classified as needs_clarification for current intent/error combination.
# @POST: Returned text is human-readable and actionable for target operation.
# @PRE state was classified as needs_clarification for current intent/error combination.
# @POST Returned text is human-readable and actionable for target operation.
def _clarification_text_for_intent(
intent: dict[str, Any] | None, detail_text: str
) -> str:
@@ -69,9 +69,9 @@ def _clarification_text_for_intent(
# #region _async_confirmation_summary [C:4] [TYPE Function]
# @BRIEF Build human-readable confirmation prompt for an intent before execution.
# @PRE: actions is a non-empty list of planned review actions.
# @POST: Returns a formatted summary string suitable for display to the user.
# @SIDE_EFFECT: None - pure formatting function.
# @PRE actions is a non-empty list of planned review actions.
# @POST Returns a formatted summary string suitable for display to the user.
# @SIDE_EFFECT None - pure formatting function.
async def _async_confirmation_summary(intent: dict[str, Any], config_manager: ConfigManager, db: Session) -> str:
with belief_scope('_confirmation_summary'):
logger.reason('Belief protocol reasoning checkpoint for _confirmation_summary')
@@ -139,15 +139,15 @@ async def _async_confirmation_summary(intent: dict[str, Any], config_manager: Co
# #region _dispatch_intent [C:5] [TYPE Function]
# @BRIEF Execute parsed assistant intent via existing task/plugin/git services.
# @DATA_CONTRACT: Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]
# @DATA_CONTRACT Input[intent,current_user,task_manager,config_manager,db] -> Output[Tuple[text:str,task_id:Optional[str],actions:List[AssistantAction]]]
# @RELATION DEPENDS_ON -> [_check_any_permission]
# @RELATION DEPENDS_ON -> [_resolve_dashboard_id_entity]
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DEPENDS_ON -> [GitService]
# @SIDE_EFFECT: May enqueue tasks, invoke git operations, and query/update external service state.
# @PRE: intent operation is known and actor permissions are validated per operation.
# @POST: Returns response text, optional task id, and UI actions for follow-up.
# @INVARIANT: unsupported operations are rejected via HTTPException(400).
# @SIDE_EFFECT May enqueue tasks, invoke git operations, and query/update external service state.
# @PRE intent operation is known and actor permissions are validated per operation.
# @POST Returns response text, optional task id, and UI actions for follow-up.
# @INVARIANT unsupported operations are rejected via HTTPException(400).
async def _dispatch_intent(intent: dict[str, Any], current_user: User, task_manager: TaskManager, config_manager: ConfigManager, db: Session) -> tuple[str, str | None, list[AssistantAction]]:
with belief_scope('_dispatch_intent'):
logger.reason('Belief protocol reasoning checkpoint for _dispatch_intent')

View File

@@ -1,8 +1,8 @@
# #region AssistantHistory [C:2] [TYPE Module] [SEMANTICS assistant, history, audit, persistence, conversation]
# @BRIEF Conversation history, audit trail, and confirmation persistence helpers for the assistant API.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @INVARIANT: Failed persistence attempts always rollback before returning.
# @INVARIANT Failed persistence attempts always rollback before returning.
from __future__ import annotations
@@ -32,12 +32,12 @@ logger = logger
# #region _append_history [C:2] [TYPE Function]
# @BRIEF Append conversation message to in-memory history buffer.
# @DATA_CONTRACT: Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]
# @RELATION UPDATES -> [CONVERSATIONS]
# @SIDE_EFFECT: Mutates in-memory CONVERSATIONS store for user conversation history.
# @PRE: user_id and conversation_id identify target conversation bucket.
# @POST: Message entry is appended to CONVERSATIONS key list.
# @INVARIANT: every appended entry includes generated message_id and created_at timestamp.
# @DATA_CONTRACT Input[user_id,conversation_id,role,text,state?,task_id?,confirmation_id?] -> Output[None]
# @RELATION BINDS_TO -> [EXT:internal:CONVERSATIONS]
# @SIDE_EFFECT Mutates in-memory CONVERSATIONS store for user conversation history.
# @PRE user_id and conversation_id identify target conversation bucket.
# @POST Message entry is appended to CONVERSATIONS key list.
# @INVARIANT every appended entry includes generated message_id and created_at timestamp.
def _append_history(
user_id: str,
conversation_id: str,
@@ -69,12 +69,12 @@ def _append_history(
# #region _persist_message [C:2] [TYPE Function]
# @BRIEF Persist assistant/user message record to database.
# @DATA_CONTRACT: Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]
# @DATA_CONTRACT Input[Session,user_id,conversation_id,role,text,state?,task_id?,confirmation_id?,metadata?] -> Output[None]
# @RELATION DEPENDS_ON -> [AssistantMessageRecord]
# @SIDE_EFFECT: Writes AssistantMessageRecord rows and commits or rollbacks the DB session.
# @PRE: db session is writable and message payload is serializable.
# @POST: Message row is committed or persistence failure is logged.
# @INVARIANT: failed persistence attempts always rollback before returning.
# @SIDE_EFFECT Writes AssistantMessageRecord rows and commits or rollbacks the DB session.
# @PRE db session is writable and message payload is serializable.
# @POST Message row is committed or persistence failure is logged.
# @INVARIANT failed persistence attempts always rollback before returning.
def _persist_message(
db: Session,
user_id: str,
@@ -110,12 +110,12 @@ def _persist_message(
# #region _audit [C:2] [TYPE Function]
# @BRIEF Append in-memory audit record for assistant decision trace.
# @DATA_CONTRACT: Input[user_id,payload:Dict[str,Any]] -> Output[None]
# @RELATION UPDATES -> [ASSISTANT_AUDIT]
# @SIDE_EFFECT: Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.
# @PRE: payload describes decision/outcome fields.
# @POST: ASSISTANT_AUDIT list for user contains new timestamped entry.
# @INVARIANT: persisted in-memory audit entry always contains created_at in ISO format.
# @DATA_CONTRACT Input[user_id,payload:Dict[str,Any]] -> Output[None]
# @RELATION BINDS_TO -> [EXT:internal:ASSISTANT_AUDIT]
# @SIDE_EFFECT Mutates in-memory ASSISTANT_AUDIT store and emits structured log event.
# @PRE payload describes decision/outcome fields.
# @POST ASSISTANT_AUDIT list for user contains new timestamped entry.
# @INVARIANT persisted in-memory audit entry always contains created_at in ISO format.
def _audit(user_id: str, payload: dict[str, Any]):
if user_id not in ASSISTANT_AUDIT:
ASSISTANT_AUDIT[user_id] = []
@@ -130,8 +130,8 @@ def _audit(user_id: str, payload: dict[str, Any]):
# #region _persist_audit [C:2] [TYPE Function]
# @BRIEF Persist structured assistant audit payload in database.
# @PRE: db session is writable and payload is JSON-serializable.
# @POST: Audit row is committed or failure is logged with rollback.
# @PRE db session is writable and payload is JSON-serializable.
# @POST Audit row is committed or failure is logged with rollback.
def _persist_audit(
db: Session, user_id: str, payload: dict[str, Any], conversation_id: str | None
):
@@ -157,8 +157,8 @@ def _persist_audit(
# #region _persist_confirmation [C:2] [TYPE Function]
# @BRIEF Persist confirmation token record to database.
# @PRE: record contains id/user/intent/dispatch/expiry fields.
# @POST: Confirmation row exists in persistent storage.
# @PRE record contains id/user/intent/dispatch/expiry fields.
# @POST Confirmation row exists in persistent storage.
def _persist_confirmation(db: Session, record: ConfirmationRecord):
try:
row = AssistantConfirmationRecord(
@@ -184,8 +184,8 @@ def _persist_confirmation(db: Session, record: ConfirmationRecord):
# #region _update_confirmation_state [C:2] [TYPE Function]
# @BRIEF Update persistent confirmation token lifecycle state.
# @PRE: confirmation_id references existing row.
# @POST: State and consumed_at fields are updated when applicable.
# @PRE confirmation_id references existing row.
# @POST State and consumed_at fields are updated when applicable.
def _update_confirmation_state(db: Session, confirmation_id: str, state: str):
try:
row = (
@@ -209,8 +209,8 @@ def _update_confirmation_state(db: Session, confirmation_id: str, state: str):
# #region _load_confirmation_from_db [C:2] [TYPE Function]
# @BRIEF Load confirmation token from database into in-memory model.
# @PRE: confirmation_id may or may not exist in storage.
# @POST: Returns ConfirmationRecord when found, otherwise None.
# @PRE confirmation_id may or may not exist in storage.
# @POST Returns ConfirmationRecord when found, otherwise None.
def _load_confirmation_from_db(
db: Session, confirmation_id: str
) -> ConfirmationRecord | None:
@@ -238,8 +238,8 @@ def _load_confirmation_from_db(
# #region _ensure_conversation [C:2] [TYPE Function]
# @BRIEF Resolve active conversation id in memory or create a new one.
# @PRE: user_id identifies current actor.
# @POST: Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.
# @PRE user_id identifies current actor.
# @POST Returns stable conversation id and updates USER_ACTIVE_CONVERSATION.
def _ensure_conversation(user_id: str, conversation_id: str | None) -> str:
if conversation_id:
from ._schemas import USER_ACTIVE_CONVERSATION
@@ -261,8 +261,8 @@ def _ensure_conversation(user_id: str, conversation_id: str | None) -> str:
# #region _resolve_or_create_conversation [C:2] [TYPE Function]
# @BRIEF Resolve active conversation using explicit id, memory cache, or persisted history.
# @PRE: user_id and db session are available.
# @POST: Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.
# @PRE user_id and db session are available.
# @POST Returns conversation id and updates USER_ACTIVE_CONVERSATION cache.
def _resolve_or_create_conversation(
user_id: str, conversation_id: str | None, db: Session
) -> str:
@@ -298,8 +298,8 @@ def _resolve_or_create_conversation(
# #region _cleanup_history_ttl [C:2] [TYPE Function]
# @BRIEF Enforce assistant message retention window by deleting expired rows and in-memory records.
# @PRE: db session is available and user_id references current actor scope.
# @POST: Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.
# @PRE db session is available and user_id references current actor scope.
# @POST Messages older than ASSISTANT_MESSAGE_TTL_DAYS are removed from persistence and memory mirrors.
def _cleanup_history_ttl(db: Session, user_id: str):
cutoff = datetime.utcnow() - timedelta(days=ASSISTANT_MESSAGE_TTL_DAYS)
try:
@@ -339,8 +339,8 @@ def _cleanup_history_ttl(db: Session, user_id: str):
# #region _is_conversation_archived [C:2] [TYPE Function]
# @BRIEF Determine archived state for a conversation based on last update timestamp.
# @PRE: updated_at can be null for empty conversations.
# @POST: Returns True when conversation inactivity exceeds archive threshold.
# @PRE updated_at can be null for empty conversations.
# @POST Returns True when conversation inactivity exceeds archive threshold.
def _is_conversation_archived(updated_at: datetime | None) -> bool:
if not updated_at:
return False
@@ -353,8 +353,8 @@ def _is_conversation_archived(updated_at: datetime | None) -> bool:
# #region _coerce_query_bool [C:2] [TYPE Function]
# @BRIEF Normalize bool-like query values for compatibility in direct handler invocations/tests.
# @PRE: value may be bool, string, or FastAPI Query metadata object.
# @POST: Returns deterministic boolean flag.
# @PRE value may be bool, string, or FastAPI Query metadata object.
# @POST Returns deterministic boolean flag.
def _coerce_query_bool(value: Any) -> bool:
if isinstance(value, bool):
return value

View File

@@ -1,14 +1,14 @@
# #region AssistantLlmPlanner [C:5] [TYPE Module] [SEMANTICS assistant, llm, planner, tool, catalog]
# @BRIEF LLM-based intent planning, tool catalog construction, and authorization for the assistant API.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @RELATION DEPENDS_ON -> [AssistantResolvers]
# @RELATION DISPATCHES -> [AssistantLlmPlannerIntent]
# @PRE: Assistant routes initialized, user authenticated
# @POST: LLM tool catalog filtered and returned
# @INVARIANT: Tool catalog is filtered by user permissions before being sent to LLM.
# @SIDE_EFFECT: Filters tool catalog by user permissions
# @DATA_CONTRACT: UserPermissions -> ToolCatalog
# @PRE Assistant routes initialized, user authenticated
# @POST LLM tool catalog filtered and returned
# @INVARIANT Tool catalog is filtered by user permissions before being sent to LLM.
# @SIDE_EFFECT Filters tool catalog by user permissions
# @DATA_CONTRACT UserPermissions -> ToolCatalog
from __future__ import annotations
@@ -33,8 +33,8 @@ from ._schemas import (
# #region _check_any_permission [C:2] [TYPE Function]
# @BRIEF Validate user against alternative permission checks (logical OR).
# @PRE: checks list contains resource-action tuples.
# @POST: Returns on first successful permission; raises 403-like HTTPException otherwise.
# @PRE checks list contains resource-action tuples.
# @POST Returns on first successful permission; raises 403-like HTTPException otherwise.
def _check_any_permission(current_user: User, checks: list[tuple[str, str]]):
errors: list[HTTPException] = []
for resource, action in checks:
@@ -56,8 +56,8 @@ def _check_any_permission(current_user: User, checks: list[tuple[str, str]]):
# #region _has_any_permission [C:2] [TYPE Function]
# @BRIEF Check whether user has at least one permission tuple from the provided list.
# @PRE: current_user and checks list are valid.
# @POST: Returns True when at least one permission check passes.
# @PRE current_user and checks list are valid.
# @POST Returns True when at least one permission check passes.
def _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bool:
try:
_check_any_permission(current_user, checks)
@@ -71,8 +71,8 @@ def _has_any_permission(current_user: User, checks: list[tuple[str, str]]) -> bo
# #region _build_tool_catalog [C:3] [TYPE Function]
# @BRIEF Build current-user tool catalog for LLM planner with operation contracts and defaults.
# @PRE: current_user is authenticated; config/db are available.
# @POST: Returns list of executable tools filtered by permission and runtime availability.
# @PRE current_user is authenticated; config/db are available.
# @POST Returns list of executable tools filtered by permission and runtime availability.
# @RELATION CALLS -> LLMProviderService
def _build_tool_catalog(
current_user: User,
@@ -273,8 +273,8 @@ def _build_tool_catalog(
# #region _coerce_intent_entities [C:2] [TYPE Function]
# @BRIEF Normalize intent entity value types from LLM output to route-compatible values.
# @PRE: intent contains entities dict or missing entities.
# @POST: Returned intent has numeric ids coerced where possible and string values stripped.
# @PRE intent contains entities dict or missing entities.
# @POST Returned intent has numeric ids coerced where possible and string values stripped.
def _coerce_intent_entities(intent: dict[str, Any]) -> dict[str, Any]:
entities = intent.get("entities")
if not isinstance(entities, dict):

View File

@@ -1,13 +1,13 @@
# #region AssistantLlmPlannerIntent [C:5] [TYPE Module] [SEMANTICS assistant, llm, intent, planning, authorization]
# @BRIEF LLM-based intent planning and authorization for the assistant API — separated from tool catalog.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantLlmPlanner]
# @RELATION DEPENDS_ON -> [AssistantResolvers]
# @PRE: Assistant routes initialized, user authenticated
# @POST: Intent planning registered with confirmation gate
# @INVARIANT: Production deployments always require confirmation.
# @SIDE_EFFECT: Registers intent planning routes
# @DATA_CONTRACT: UserIntent -> PlannedAction
# @PRE Assistant routes initialized, user authenticated
# @POST Intent planning registered with confirmation gate
# @INVARIANT Production deployments always require confirmation.
# @SIDE_EFFECT Registers intent planning routes
# @DATA_CONTRACT UserIntent -> PlannedAction
from __future__ import annotations
@@ -39,8 +39,8 @@ from ._schemas import INTENT_PERMISSION_CHECKS
# #region _plan_intent_with_llm [C:2] [TYPE Function]
# @BRIEF Use active LLM provider to select best tool/operation from dynamic catalog.
# @PRE: tools list contains allowed operations for current user.
# @POST: Returns normalized intent dict when planning succeeds; otherwise None.
# @PRE tools list contains allowed operations for current user.
# @POST Returns normalized intent dict when planning succeeds; otherwise None.
async def _plan_intent_with_llm(
message: str,
tools: list[dict[str, Any]],
@@ -160,8 +160,8 @@ async def _plan_intent_with_llm(
# #region _authorize_intent [C:2] [TYPE Function]
# @BRIEF Validate user permissions for parsed intent before confirmation/dispatch.
# @PRE: intent.operation is present for known assistant command domains.
# @POST: Returns if authorized; raises HTTPException(403) when denied.
# @PRE intent.operation is present for known assistant command domains.
# @POST Returns if authorized; raises HTTPException(403) when denied.
def _authorize_intent(intent: dict[str, Any], current_user: User):
operation = intent.get("operation")
if operation in INTENT_PERMISSION_CHECKS:

View File

@@ -1,9 +1,9 @@
# #region AssistantResolvers [C:2] [TYPE Module] [SEMANTICS assistant, resolver, lookup, environment, mapper]
# @BRIEF Environment, dashboard, provider, and task resolution utilities for the assistant API.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [SupersetClient]
# @INVARIANT: Resolution functions never raise; they return None on failure.
# @INVARIANT Resolution functions never raise; they return None on failure.
from __future__ import annotations
@@ -23,8 +23,8 @@ logger = cast(Any, logger)
# #region _extract_id [C:2] [TYPE Function]
# @BRIEF Extract first regex match group from text by ordered pattern list.
# @PRE: patterns contain at least one capture group.
# @POST: Returns first matched token or None.
# @PRE patterns contain at least one capture group.
# @POST Returns first matched token or None.
def _extract_id(text: str, patterns: list[str]) -> str | None:
for p in patterns:
m = re.search(p, text, flags=re.IGNORECASE)
@@ -37,8 +37,8 @@ def _extract_id(text: str, patterns: list[str]) -> str | None:
# #region _resolve_env_id [C:2] [TYPE Function]
# @BRIEF Resolve environment identifier/name token to canonical environment id.
# @PRE: config_manager provides environment list.
# @POST: Returns matched environment id or None.
# @PRE config_manager provides environment list.
# @POST Returns matched environment id or None.
def _resolve_env_id(
token: str | None, config_manager: ConfigManager
) -> str | None:
@@ -57,8 +57,8 @@ def _resolve_env_id(
# #region _is_production_env [C:2] [TYPE Function]
# @BRIEF Determine whether environment token resolves to production-like target.
# @PRE: config_manager provides environments or token text is provided.
# @POST: Returns True for production/prod synonyms, else False.
# @PRE config_manager provides environments or token text is provided.
# @POST Returns True for production/prod synonyms, else False.
def _is_production_env(token: str | None, config_manager: ConfigManager) -> bool:
env_id = _resolve_env_id(token, config_manager)
if not env_id:
@@ -75,8 +75,8 @@ def _is_production_env(token: str | None, config_manager: ConfigManager) -> bool
# #region _resolve_provider_id [C:2] [TYPE Function]
# @BRIEF Resolve provider token to provider id with active/default fallback.
# @PRE: db session can load provider list through LLMProviderService.
# @POST: Returns provider id or None when no providers configured.
# @PRE db session can load provider list through LLMProviderService.
# @POST Returns provider id or None when no providers configured.
def _resolve_provider_id(
provider_token: str | None,
db: Session,
@@ -111,8 +111,8 @@ def _resolve_provider_id(
# #region _get_default_environment_id [C:2] [TYPE Function]
# @BRIEF Resolve default environment id from settings or first configured environment.
# @PRE: config_manager returns environments list.
# @POST: Returns default environment id or None when environment list is empty.
# @PRE config_manager returns environments list.
# @POST Returns default environment id or None when environment list is empty.
def _get_default_environment_id(config_manager: ConfigManager) -> str | None:
configured = config_manager.get_environments()
if not configured:
@@ -135,8 +135,8 @@ def _get_default_environment_id(config_manager: ConfigManager) -> str | None:
# #region _resolve_dashboard_id_by_ref [C:2] [TYPE Function]
# @BRIEF Resolve dashboard id by title or slug reference in selected environment.
# @PRE: dashboard_ref is a non-empty string-like token.
# @POST: Returns dashboard id when uniquely matched, otherwise None.
# @PRE dashboard_ref is a non-empty string-like token.
# @POST Returns dashboard id when uniquely matched, otherwise None.
def _resolve_dashboard_id_by_ref(
dashboard_ref: str | None,
env_id: str | None,
@@ -187,8 +187,8 @@ def _resolve_dashboard_id_by_ref(
# #region _resolve_dashboard_id_entity [C:2] [TYPE Function]
# @BRIEF Resolve dashboard id from intent entities using numeric id or dashboard_ref fallback.
# @PRE: entities may contain dashboard_id as int/str and optional dashboard_ref.
# @POST: Returns resolved dashboard id or None when ambiguous/unresolvable.
# @PRE entities may contain dashboard_id as int/str and optional dashboard_ref.
# @POST Returns resolved dashboard id or None when ambiguous/unresolvable.
def _resolve_dashboard_id_entity(
entities: dict[str, Any],
config_manager: ConfigManager,
@@ -228,8 +228,8 @@ def _resolve_dashboard_id_entity(
# #region _get_environment_name_by_id [C:2] [TYPE Function]
# @BRIEF Resolve human-readable environment name by id.
# @PRE: environment id may be None.
# @POST: Returns matching environment name or fallback id.
# @PRE environment id may be None.
# @POST Returns matching environment name or fallback id.
def _get_environment_name_by_id(
env_id: str | None, config_manager: ConfigManager
) -> str:
@@ -245,8 +245,8 @@ def _get_environment_name_by_id(
# #region _extract_result_deep_links [C:2] [TYPE Function]
# @BRIEF Build deep-link actions to verify task result from assistant chat.
# @PRE: task object is available.
# @POST: Returns zero or more assistant actions for dashboard open/diff.
# @PRE task object is available.
# @POST Returns zero or more assistant actions for dashboard open/diff.
def _extract_result_deep_links(
task: Any, config_manager: ConfigManager
) -> list:
@@ -318,8 +318,8 @@ def _extract_result_deep_links(
# #region _build_task_observability_summary [C:2] [TYPE Function]
# @BRIEF Build compact textual summary for completed tasks to reduce "black box" effect.
# @PRE: task may contain plugin-specific result payload.
# @POST: Returns non-empty summary line for known task types or empty string fallback.
# @PRE task may contain plugin-specific result payload.
# @POST Returns non-empty summary line for known task types or empty string fallback.
def _build_task_observability_summary(task: Any, config_manager: ConfigManager) -> str:
plugin_id = getattr(task, "plugin_id", None)
status = str(getattr(task, "status", "")).upper()

View File

@@ -1,6 +1,6 @@
# #region AssistantRoutes [C:5] [TYPE Module] [SEMANTICS assistant, api, route, chat, execution]
# @BRIEF FastAPI route handlers for the assistant API — message sending, confirmation, conversation management.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AssistantSchemas]
# @RELATION DEPENDS_ON -> [AssistantHistory]
# @RELATION DEPENDS_ON -> [AssistantCommandParser]
@@ -8,7 +8,7 @@
# @RELATION DEPENDS_ON -> [AssistantDatasetReview]
# @RELATION DEPENDS_ON -> [AssistantDispatch]
# @RELATION DISPATCHES -> [AssistantAdminRoutes]
# @INVARIANT: Risky operations are never executed without valid confirmation token.
# @INVARIANT Risky operations are never executed without valid confirmation token.
from __future__ import annotations
@@ -66,17 +66,17 @@ router = APIRouter(tags=["Assistant"])
@router.post("/messages", response_model=AssistantMessageResponse)
# #region send_message [C:5] [TYPE Function]
# @BRIEF Parse assistant command, enforce safety gates, and dispatch executable intent.
# @DATA_CONTRACT: Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]
# @DATA_CONTRACT Input[AssistantMessageRequest,User,TaskManager,ConfigManager,Session] -> Output[AssistantMessageResponse]
# @RELATION DEPENDS_ON -> [_plan_intent_with_llm]
# @RELATION DEPENDS_ON -> [_parse_command]
# @RELATION DEPENDS_ON -> [_dispatch_intent]
# @RELATION DEPENDS_ON -> [_append_history]
# @RELATION DEPENDS_ON -> [_persist_message]
# @RELATION DEPENDS_ON -> [_audit]
# @SIDE_EFFECT: Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.
# @PRE: Authenticated user is available and message text is non-empty.
# @POST: Response state is one of clarification/confirmation/started/success/denied/failed.
# @INVARIANT: non-safe operations are gated with confirmation before execution from this endpoint.
# @SIDE_EFFECT Persists chat/audit state, mutates in-memory conversation and confirmation stores, and may create confirmation records.
# @PRE Authenticated user is available and message text is non-empty.
# @POST Response state is one of clarification/confirmation/started/success/denied/failed.
# @INVARIANT non-safe operations are gated with confirmation before execution from this endpoint.
async def send_message(request: AssistantMessageRequest, current_user: User=Depends(get_current_user), task_manager: TaskManager=Depends(get_task_manager), config_manager: ConfigManager=Depends(get_config_manager), db: Session=Depends(get_db)):
with belief_scope('send_message'):
logger.reason('Belief protocol reasoning checkpoint for send_message')
@@ -159,8 +159,8 @@ async def send_message(request: AssistantMessageRequest, current_user: User=Depe
)
# #region confirm_operation [C:2] [TYPE Function]
# @BRIEF Execute previously requested risky operation after explicit user confirmation.
# @PRE: confirmation_id exists, belongs to current user, is pending, and not expired.
# @POST: Confirmation state becomes consumed and operation result is persisted in history.
# @PRE confirmation_id exists, belongs to current user, is pending, and not expired.
# @POST Confirmation state becomes consumed and operation result is persisted in history.
async def confirm_operation(
confirmation_id: str,
current_user: User = Depends(get_current_user),
@@ -246,8 +246,8 @@ async def confirm_operation(
)
# #region cancel_operation [C:2] [TYPE Function]
# @BRIEF Cancel pending risky operation and mark confirmation token as cancelled.
# @PRE: confirmation_id exists, belongs to current user, and is still pending.
# @POST: Confirmation becomes cancelled and cannot be executed anymore.
# @PRE confirmation_id exists, belongs to current user, and is still pending.
# @POST Confirmation becomes cancelled and cannot be executed anymore.
async def cancel_operation(
confirmation_id: str,
current_user: User = Depends(get_current_user),

View File

@@ -1,8 +1,8 @@
# #region AssistantSchemas [C:2] [TYPE Module] [SEMANTICS assistant, pydantic, schema, store, permission]
# @BRIEF Pydantic models, in-memory stores, and permission mappings for the assistant API.
# @LAYER API
# @RELATION USED_BY -> [AssistantHistory]
# @RELATION USED_BY -> [AssistantHistory]
# @RELATION CALLED_BY -> [AssistantHistory]
# @RELATION CALLED_BY -> [AssistantHistory]
# @INVARIANT In-memory stores are module-level singletons shared across the assistant package.
# @RATIONALE In-memory stores documented with NOTE about restart loss. ASSISTANT_ARCHIVE_AFTER_DAYS and ASSISTANT_MESSAGE_TTL_DAYS kept as module-level constants with TODO for config migration — Pydantic schemas module should not depend on ConfigManager for architectural purity.
@@ -17,7 +17,7 @@ from pydantic import BaseModel, Field
# #region AssistantMessageRequest [C:1] [TYPE Class]
# @BRIEF Input payload for assistant message endpoint.
# @DATA_CONTRACT Input[conversation_id?:str, message:str(1..4000)] -> Output[AssistantMessageRequest]
# @RELATION USED_BY -> [send_message]
# @RELATION CALLED_BY -> [send_message]
# @SIDE_EFFECT None (schema declaration only).
# @PRE message length is within accepted bounds.
# @POST Request object provides message text and optional conversation binding.
@@ -34,7 +34,7 @@ class AssistantMessageRequest(BaseModel):
# #region AssistantAction [C:1] [TYPE Class]
# @BRIEF UI action descriptor returned with assistant responses.
# @DATA_CONTRACT Input[type:str, label:str, target?:str] -> Output[AssistantAction]
# @RELATION USED_BY -> [AssistantMessageResponse]
# @RELATION CALLED_BY -> [AssistantMessageResponse]
# @SIDE_EFFECT None (schema declaration only).
# @PRE type and label are provided by orchestration logic.
# @POST Action can be rendered as button on frontend.
@@ -51,9 +51,9 @@ class AssistantAction(BaseModel):
# #region AssistantMessageResponse [C:1] [TYPE Class]
# @BRIEF Output payload contract for assistant interaction endpoints.
# @DATA_CONTRACT Input[conversation_id,response_id,state,text,intent?,confirmation_id?,task_id?,actions[],created_at] -> Output[AssistantMessageResponse]
# @RELATION RETURNED_BY -> [send_message]
# @RELATION RETURNED_BY -> [confirm_operation]
# @RELATION RETURNED_BY -> [cancel_operation]
# @RELATION CALLED_BY -> [send_message]
# @RELATION CALLED_BY -> [confirm_operation]
# @RELATION CALLED_BY -> [cancel_operation]
# @SIDE_EFFECT None (schema declaration only).
# @PRE Response includes deterministic state and text.
# @POST Payload may include task_id/confirmation_id/actions for UI follow-up.
@@ -76,9 +76,9 @@ class AssistantMessageResponse(BaseModel):
# #region ConfirmationRecord [C:1] [TYPE Class]
# @BRIEF In-memory confirmation token model for risky operation dispatch.
# @DATA_CONTRACT Input[id,user_id,conversation_id,intent,dispatch,expires_at,state?,created_at] -> Output[ConfirmationRecord]
# @RELATION USED_BY -> [send_message]
# @RELATION USED_BY -> [confirm_operation]
# @RELATION USED_BY -> [cancel_operation]
# @RELATION CALLED_BY -> [send_message]
# @RELATION CALLED_BY -> [confirm_operation]
# @RELATION CALLED_BY -> [cancel_operation]
# @SIDE_EFFECT None (schema declaration only).
# @PRE intent/dispatch/user_id are populated at confirmation request time.
# @POST Record tracks lifecycle state and expiry timestamp.

View File

@@ -1,12 +1,12 @@
# #region CleanReleaseApi [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, api, compliance, candidate, release]
# @BRIEF Expose clean release endpoints for candidate preparation and subsequent compliance flow.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [get_clean_release_repository]
# @RELATION DEPENDS_ON -> [PreparationService]
# @PRE: Clean release repository and preparation service dependencies are configured for the current request scope.
# @POST: Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation.
# @SIDE_EFFECT: Persists candidate/compliance lifecycle state and triggers clean-release orchestration services.
# @INVARIANT: API never reports prepared status if preparation errors are present.
# @PRE Clean release repository and preparation service dependencies are configured for the current request scope.
# @POST Candidate preparation, manifest, and compliance routes expose deterministic API payloads without reporting prepared state on failed preparation.
# @SIDE_EFFECT Persists candidate/compliance lifecycle state and triggers clean-release orchestration services.
# @INVARIANT API never reports prepared status if preparation errors are present.
from __future__ import annotations
@@ -112,8 +112,8 @@ class CreateComplianceRunRequest(BaseModel):
# #region register_candidate_v2_endpoint [TYPE Function]
# @BRIEF Register a clean-release candidate for headless lifecycle.
# @PRE: Candidate identifier is unique.
# @POST: Candidate is persisted in DRAFT status.
# @PRE Candidate identifier is unique.
# @POST Candidate is persisted in DRAFT status.
@router.post(
"/candidates", response_model=CandidateDTO, status_code=status.HTTP_201_CREATED
)
@@ -153,8 +153,8 @@ async def register_candidate_v2_endpoint(
# #region import_candidate_artifacts_v2_endpoint [TYPE Function]
# @BRIEF Import candidate artifacts in headless flow.
# @PRE: Candidate exists and artifacts array is non-empty.
# @POST: Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.
# @PRE Candidate exists and artifacts array is non-empty.
# @POST Artifacts are persisted and candidate advances to PREPARED if it was DRAFT.
@router.post("/candidates/{candidate_id}/artifacts")
async def import_candidate_artifacts_v2_endpoint(
candidate_id: str,
@@ -211,8 +211,8 @@ async def import_candidate_artifacts_v2_endpoint(
# #region build_candidate_manifest_v2_endpoint [TYPE Function]
# @BRIEF Build immutable manifest snapshot for prepared candidate.
# @PRE: Candidate exists and has imported artifacts.
# @POST: Returns created ManifestDTO with incremented version.
# @PRE Candidate exists and has imported artifacts.
# @POST Returns created ManifestDTO with incremented version.
@router.post(
"/candidates/{candidate_id}/manifests",
response_model=ManifestDTO,
@@ -255,8 +255,8 @@ async def build_candidate_manifest_v2_endpoint(
# #region get_candidate_overview_v2_endpoint [TYPE Function]
# @BRIEF Return expanded candidate overview DTO for headless lifecycle visibility.
# @PRE: Candidate exists.
# @POST: Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.
# @PRE Candidate exists.
# @POST Returns CandidateOverviewDTO built from the same repository state used by headless US1 endpoints.
@router.get("/candidates/{candidate_id}/overview", response_model=CandidateOverviewDTO)
async def get_candidate_overview_v2_endpoint(
candidate_id: str,
@@ -371,8 +371,8 @@ async def get_candidate_overview_v2_endpoint(
# #region prepare_candidate_endpoint [TYPE Function]
# @BRIEF Prepare candidate with policy evaluation and deterministic manifest generation.
# @PRE: Candidate and active policy exist in repository.
# @POST: Returns preparation result including manifest reference and violations.
# @PRE Candidate and active policy exist in repository.
# @POST Returns preparation result including manifest reference and violations.
@router.post("/candidates/prepare")
async def prepare_candidate_endpoint(
payload: PrepareCandidateRequest,
@@ -405,8 +405,8 @@ async def prepare_candidate_endpoint(
# #region start_check [TYPE Function]
# @BRIEF Start and finalize a clean compliance check run and persist report artifacts.
# @PRE: Active policy and candidate exist.
# @POST: Returns accepted payload with check_run_id and started_at.
# @PRE Active policy and candidate exist.
# @POST Returns accepted payload with check_run_id and started_at.
@router.post("/checks", status_code=status.HTTP_202_ACCEPTED)
async def start_check(
payload: StartCheckRequest,
@@ -541,8 +541,8 @@ async def start_check(
# #region get_check_status [TYPE Function]
# @BRIEF Return terminal/intermediate status payload for a check run.
# @PRE: check_run_id references an existing run.
# @POST: Deterministic payload shape includes checks and violations arrays.
# @PRE check_run_id references an existing run.
# @POST Deterministic payload shape includes checks and violations arrays.
@router.get("/checks/{check_run_id}")
async def get_check_status(
check_run_id: str,
@@ -593,8 +593,8 @@ async def get_check_status(
# #region get_report [TYPE Function]
# @BRIEF Return persisted compliance report by report_id.
# @PRE: report_id references an existing report.
# @POST: Returns serialized report object.
# @PRE report_id references an existing report.
# @POST Returns serialized report object.
@router.get("/reports/{report_id}")
async def get_report(
report_id: str,

View File

@@ -1,12 +1,12 @@
# #region CleanReleaseV2Api [C:4] [TYPE Module] [SEMANTICS fastapi, clean-release, candidate, lifecycle, api]
# @BRIEF Redesigned clean release API for headless candidate lifecycle.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
# @RELATION CALLS -> [approve_candidate]
# @RELATION CALLS -> [publish_candidate]
# @PRE: Clean release repository dependency is available for candidate lifecycle endpoints.
# @POST: Candidate registration, approval, publication, and revocation routes are registered without behavior changes.
# @SIDE_EFFECT: Persists candidate lifecycle state through clean release services and repository adapters.
# @PRE Clean release repository dependency is available for candidate lifecycle endpoints.
# @POST Candidate registration, approval, publication, and revocation routes are registered without behavior changes.
# @SIDE_EFFECT Persists candidate lifecycle state through clean release services and repository adapters.
from datetime import UTC, datetime
from typing import Any
@@ -63,8 +63,8 @@ class RevokeRequest(dict):
# #region register_candidate [C:3] [TYPE Function]
# @BRIEF Register a new release candidate.
# @PRE: Payload contains required fields (id, version, source_snapshot_ref, created_by).
# @POST: Candidate is saved in repository.
# @PRE Payload contains required fields (id, version, source_snapshot_ref, created_by).
# @POST Candidate is saved in repository.
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
# @RELATION DEPENDS_ON -> [clean_release_dto]
@router.post(
@@ -98,8 +98,8 @@ async def register_candidate(
# #region import_artifacts [C:3] [TYPE Function]
# @BRIEF Associate artifacts with a release candidate.
# @PRE: Candidate exists.
# @POST: Artifacts are processed (placeholder).
# @PRE Candidate exists.
# @POST Artifacts are processed (placeholder).
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
@router.post("/candidates/{candidate_id}/artifacts")
async def import_artifacts(
@@ -131,8 +131,8 @@ async def import_artifacts(
# #region build_manifest [C:3] [TYPE Function]
# @BRIEF Generate distribution manifest for a candidate.
# @PRE: Candidate exists.
# @POST: Manifest is created and saved.
# @PRE Candidate exists.
# @POST Manifest is created and saved.
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
@router.post(
"/candidates/{candidate_id}/manifests",

View File

@@ -1,37 +1,37 @@
# #region DashboardsApi [C:5] [TYPE Module] [SEMANTICS dashboard, api, package, search, git, task]
#
# @BRIEF API endpoints for the Dashboard Hub - listing dashboards with Git and task status
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AppDependencies]
# @RELATION DEPENDS_ON -> [ResourceService]
# @RELATION DEPENDS_ON -> [SupersetClient]
#
# @INVARIANT: All dashboard responses include git_status and last_task metadata
# @INVARIANT All dashboard responses include git_status and last_task metadata
#
# @PRE: Valid environment configurations exist in ConfigManager.
# @POST: Dashboard responses are projected into DashboardsResponse DTO.
# @SIDE_EFFECT: Performs external calls to Superset API and potentially Git providers.
# @DATA_CONTRACT: Input(env_id, filters) -> Output(DashboardsResponse)
# @PRE Valid environment configurations exist in ConfigManager.
# @POST Dashboard responses are projected into DashboardsResponse DTO.
# @SIDE_EFFECT Performs external calls to Superset API and potentially Git providers.
# @DATA_CONTRACT Input(env_id, filters) -> Output(DashboardsResponse)
#
# @TEST_CONTRACT: DashboardsAPI -> {
# @TEST_CONTRACT DashboardsAPI -> {
# required_fields: {env_id: string, page: integer, page_size: integer},
# optional_fields: {search: string},
# invariants: ["Pagination must be valid", "Environment must exist"]
# }
#
# @TEST_FIXTURE: dashboard_list_happy -> {
# @TEST_FIXTURE dashboard_list_happy -> {
# "env_id": "prod",
# "expected_count": 1,
# "dashboards": [{"id": 1, "title": "Main Revenue"}]
# }
#
# @TEST_EDGE: pagination_zero_page -> {"env_id": "prod", "page": 0, "status": 400}
# @TEST_EDGE: pagination_oversize -> {"env_id": "prod", "page_size": 101, "status": 400}
# @TEST_EDGE: missing_env -> {"env_id": "ghost", "status": 404}
# @TEST_EDGE: empty_dashboards -> {"env_id": "empty_env", "expected_total": 0}
# @TEST_EDGE: external_superset_failure -> {"env_id": "bad_conn", "status": 503}
# @TEST_EDGE pagination_zero_page -> {"env_id": "prod", "page": 0, "status": 400}
# @TEST_EDGE pagination_oversize -> {"env_id": "prod", "page_size": 101, "status": 400}
# @TEST_EDGE missing_env -> {"env_id": "ghost", "status": 404}
# @TEST_EDGE empty_dashboards -> {"env_id": "empty_env", "expected_total": 0}
# @TEST_EDGE external_superset_failure -> {"env_id": "bad_conn", "status": 503}
#
# @TEST_INVARIANT: metadata_consistency -> verifies: [dashboard_list_happy, empty_dashboards]
# @TEST_INVARIANT metadata_consistency -> verifies: [dashboard_list_happy, empty_dashboards]
from ._action_routes import *
from ._detail_routes import *

View File

@@ -1,7 +1,7 @@
# #region DashboardActionRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, dashboard, api, backup]
# @BRIEF Dashboard action route handlers — migrate, backup.
# @LAYER: API
# @RELATION DEPENDS_ON -> [DashboardRouter]
# @LAYER API
# @RELATION DEPENDS_ON -> [[EXT:frontend:DashboardRouter]]
# @RELATION DEPENDS_ON -> [DashboardSchemas]
from fastapi import Depends, HTTPException
@@ -23,12 +23,12 @@ from ._schemas import (
# #region migrate_dashboards [C:2] [TYPE Function]
# @BRIEF Trigger bulk migration of dashboards from source to target environment
# @PRE: User has permission plugin:migration:execute
# @PRE: source_env_id and target_env_id are valid environment IDs
# @PRE: dashboard_ids is a non-empty list
# @POST: Returns task_id for tracking migration progress
# @POST: Task is created and queued for execution
# @RELATION DISPATCHES -> [MigrationPlugin:execute]
# @PRE User has permission plugin:migration:execute
# @PRE source_env_id and target_env_id are valid environment IDs
# @PRE dashboard_ids is a non-empty list
# @POST Returns task_id for tracking migration progress
# @POST Task is created and queued for execution
# @RELATION DISPATCHES -> [EXT:method:MigrationPlugin:execute]
# @RELATION CALLS -> [TaskManager]
@router.post("/migrate", response_model=TaskResponse)
async def migrate_dashboards(
@@ -101,13 +101,13 @@ async def migrate_dashboards(
# #region backup_dashboards [C:2] [TYPE Function]
# @BRIEF Trigger bulk backup of dashboards with optional cron schedule
# @PRE: User has permission plugin:backup:execute
# @PRE: env_id is a valid environment ID
# @PRE: dashboard_ids is a non-empty list
# @POST: Returns task_id for tracking backup progress
# @POST: Task is created and queued for execution
# @POST: If schedule is provided, a scheduled task is created
# @RELATION DISPATCHES -> [BackupPlugin:execute]
# @PRE User has permission plugin:backup:execute
# @PRE env_id is a valid environment ID
# @PRE dashboard_ids is a non-empty list
# @POST Returns task_id for tracking backup progress
# @POST Task is created and queued for execution
# @POST If schedule is provided, a scheduled task is created
# @RELATION DISPATCHES -> [EXT:method:BackupPlugin:execute]
# @RELATION CALLS -> [TaskManager]
@router.post("/backup", response_model=TaskResponse)
async def backup_dashboards(

View File

@@ -1,7 +1,7 @@
# #region DashboardDetailRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, dashboard, api, transform, search, task]
# @BRIEF Dashboard detail, db-mappings, task history, thumbnail route handlers.
# @LAYER: API
# @RELATION DEPENDS_ON -> [DashboardRouter]
# @LAYER API
# @RELATION DEPENDS_ON -> [[EXT:frontend:DashboardRouter]]
# @RELATION DEPENDS_ON -> [DashboardSchemas]
# @RELATION DEPENDS_ON -> [DashboardHelpers]
# @RELATION DEPENDS_ON -> [DashboardProjection]
@@ -43,10 +43,10 @@ from ._schemas import (
# #region get_database_mappings [C:2] [TYPE Function]
# @BRIEF Get database mapping suggestions between source and target environments
# @PRE: User has permission plugin:migration:read
# @PRE: source_env_id and target_env_id are valid environment IDs
# @POST: Returns list of suggested database mappings with confidence scores
# @RELATION CALLS -> [MappingService:get_suggestions]
# @PRE User has permission plugin:migration:read
# @PRE source_env_id and target_env_id are valid environment IDs
# @POST Returns list of suggested database mappings with confidence scores
# @RELATION CALLS -> [EXT:method:MappingService:get_suggestions]
@router.get("/db-mappings", response_model=DatabaseMappingsResponse)
async def get_database_mappings(
source_env_id: str,
@@ -109,8 +109,8 @@ async def get_database_mappings(
# #region get_dashboard_detail [C:2] [TYPE Function]
# @BRIEF Fetch detailed dashboard info with related charts and datasets
# @PRE: env_id must be valid and dashboard ref (slug or id) must exist
# @POST: Returns dashboard detail payload for overview page
# @PRE env_id must be valid and dashboard ref (slug or id) must exist
# @POST Returns dashboard detail payload for overview page
# @RELATION CALLS -> [AsyncSupersetClient]
@router.get("/{dashboard_ref}", response_model=DashboardDetailResponse)
async def get_dashboard_detail(
@@ -154,8 +154,8 @@ async def get_dashboard_detail(
# #region get_dashboard_tasks_history [C:2] [TYPE Function]
# @BRIEF Returns history of backup and LLM validation tasks for a dashboard.
# @PRE: dashboard ref (slug or id) is valid.
# @POST: Response contains sorted task history (newest first).
# @PRE dashboard ref (slug or id) is valid.
# @POST Response contains sorted task history (newest first).
@router.get("/{dashboard_ref}/tasks", response_model=DashboardTaskHistoryResponse)
async def get_dashboard_tasks_history(
dashboard_ref: str,
@@ -257,8 +257,8 @@ async def get_dashboard_tasks_history(
# #region get_dashboard_thumbnail [C:3] [TYPE Function]
# @BRIEF Proxies Superset dashboard thumbnail with cache support.
# @RELATION CALLS -> [AsyncSupersetClient]
# @PRE: env_id must exist.
# @POST: Returns image bytes or 202 when thumbnail is being prepared by Superset.
# @PRE env_id must exist.
# @POST Returns image bytes or 202 when thumbnail is being prepared by Superset.
@router.get("/{dashboard_ref}/thumbnail")
async def get_dashboard_thumbnail(
dashboard_ref: str,

View File

@@ -1,6 +1,6 @@
# #region DashboardHelpers [C:2] [TYPE Module] [SEMANTICS fastapi, dashboard, api, search, filter]
# @BRIEF Basic helper functions for dashboard route handlers — slug resolution, filter normalization.
# @LAYER: Infra
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [AsyncSupersetClient]
@@ -14,8 +14,8 @@ from src.core.superset_client import SupersetClient
# #region _find_dashboard_id_by_slug [C:2] [TYPE Function]
# @BRIEF Resolve dashboard numeric ID by slug using Superset list endpoint.
# @PRE: `dashboard_slug` is non-empty.
# @POST: Returns dashboard ID when found, otherwise None.
# @PRE `dashboard_slug` is non-empty.
# @POST Returns dashboard ID when found, otherwise None.
def _find_dashboard_id_by_slug(
client: SupersetClient,
dashboard_slug: str,
@@ -51,8 +51,8 @@ def _find_dashboard_id_by_slug(
# #region _resolve_dashboard_id_from_ref [C:2] [TYPE Function]
# @BRIEF Resolve dashboard ID from slug-first reference with numeric fallback.
# @PRE: `dashboard_ref` is provided in route path.
# @POST: Returns a valid dashboard ID or raises HTTPException(404).
# @PRE `dashboard_ref` is provided in route path.
# @POST Returns a valid dashboard ID or raises HTTPException(404).
def _resolve_dashboard_id_from_ref(
dashboard_ref: str,
client: SupersetClient,
@@ -77,8 +77,8 @@ def _resolve_dashboard_id_from_ref(
# #region _find_dashboard_id_by_slug_async [C:2] [TYPE Function]
# @BRIEF Resolve dashboard numeric ID by slug using async Superset list endpoint.
# @PRE: dashboard_slug is non-empty.
# @POST: Returns dashboard ID when found, otherwise None.
# @PRE dashboard_slug is non-empty.
# @POST Returns dashboard ID when found, otherwise None.
async def _find_dashboard_id_by_slug_async(
client: AsyncSupersetClient,
dashboard_slug: str,
@@ -114,8 +114,8 @@ async def _find_dashboard_id_by_slug_async(
# #region _resolve_dashboard_id_from_ref_async [C:2] [TYPE Function]
# @BRIEF Resolve dashboard ID from slug-first reference using async Superset client.
# @PRE: dashboard_ref is provided in route path.
# @POST: Returns valid dashboard ID or raises HTTPException(404).
# @PRE dashboard_ref is provided in route path.
# @POST Returns valid dashboard ID or raises HTTPException(404).
async def _resolve_dashboard_id_from_ref_async(
dashboard_ref: str,
client: AsyncSupersetClient,
@@ -139,8 +139,8 @@ async def _resolve_dashboard_id_from_ref_async(
# #region _normalize_filter_values [C:2] [TYPE Function]
# @BRIEF Normalize query filter values to lower-cased non-empty tokens.
# @PRE: values may be None or list of strings.
# @POST: Returns trimmed normalized list preserving input order.
# @PRE values may be None or list of strings.
# @POST Returns trimmed normalized list preserving input order.
def _normalize_filter_values(values: list[str] | None) -> list[str]:
if not values:
return []
@@ -157,8 +157,8 @@ def _normalize_filter_values(values: list[str] | None) -> list[str]:
# #region _dashboard_git_filter_value [C:2] [TYPE Function]
# @BRIEF Build comparable git status token for dashboards filtering.
# @PRE: dashboard payload may contain git_status or None.
# @POST: Returns one of ok|diff|no_repo|error|pending.
# @PRE dashboard payload may contain git_status or None.
# @POST Returns one of ok|diff|no_repo|error|pending.
def _dashboard_git_filter_value(dashboard: dict[str, Any]) -> str:
git_status = dashboard.get("git_status") or {}
sync_status = str(git_status.get("sync_status") or "").strip().upper()

View File

@@ -1,7 +1,7 @@
# #region DashboardListingRoutes [C:4] [TYPE Module] [SEMANTICS fastapi, dashboard, api, search]
# @BRIEF Dashboard listing route handler for Dashboard Hub.
# @LAYER: API
# @RELATION DEPENDS_ON -> [DashboardRouter]
# @LAYER API
# @RELATION DEPENDS_ON -> [[EXT:frontend:DashboardRouter]]
# @RELATION DEPENDS_ON -> [DashboardSchemas]
# @RELATION DEPENDS_ON -> [DashboardHelpers]
# @RELATION DEPENDS_ON -> [DashboardProjection]
@@ -36,13 +36,13 @@ from ._schemas import DashboardsResponse, EffectiveProfileFilter
# #region get_dashboards [C:3] [TYPE Function]
# @BRIEF Fetch list of dashboards from a specific environment with Git status and last task status
# @PRE: env_id must be a valid environment ID
# @PRE: page must be >= 1 if provided
# @PRE: page_size must be between 1 and 100 if provided
# @POST: Returns a list of dashboards with enhanced metadata and pagination info
# @POST: Response includes pagination metadata (page, page_size, total, total_pages)
# @POST: Response includes effective profile filter metadata for main dashboards page context
# @RELATION CALLS -> [get_dashboards_with_status]
# @PRE env_id must be a valid environment ID
# @PRE page must be >= 1 if provided
# @PRE page_size must be between 1 and 100 if provided
# @POST Returns a list of dashboards with enhanced metadata and pagination info
# @POST Response includes pagination metadata (page, page_size, total, total_pages)
# @POST Response includes effective profile filter metadata for main dashboards page context
# @RELATION CALLS -> [EXT:method:get_dashboards_with_status]
@router.get("", response_model=DashboardsResponse)
async def get_dashboards(
env_id: str,

View File

@@ -1,6 +1,6 @@
# #region DashboardProjection [C:2] [TYPE Module] [SEMANTICS dashboard, api, transform, profile, filter]
# @BRIEF Dashboard response projection and profile-filter helpers for Dashboard Hub routes.
# @LAYER: Infra
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [ProfileService]
@@ -142,7 +142,7 @@ def _get_profile_filter_binding(
# #region _resolve_profile_actor_aliases [C:2] [TYPE Function]
# @BRIEF Resolve stable actor aliases for profile filtering without per-dashboard detail fan-out.
# @SIDE_EFFECT: Performs at most one Superset users-lookup request.
# @SIDE_EFFECT Performs at most one Superset users-lookup request.
def _resolve_profile_actor_aliases(env: Any, bound_username: str) -> list[str]:
normalized_bound = _normalize_actor_alias_token(bound_username)
if not normalized_bound:

View File

@@ -1,7 +1,7 @@
# #region DashboardSchemas [C:1] [TYPE Module] [SEMANTICS pydantic, dashboard, api, dto, git-status]
# @BRIEF DTO classes for the Dashboard Hub API.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> [Pydantic]
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
from typing import Literal

View File

@@ -1,8 +1,8 @@
# #region DatasetReviewApi [C:3] [TYPE Module] [SEMANTICS dataset, review, api, facade]
# @BRIEF Thin facade re-exporting router and public symbols from decomposed dataset review API sub-modules.
# @LAYER: API
# @RATIONALE: Original 2484-line monolith violated INV_7 (400-line module limit) by 6x. Decomposed into _dependencies (DTOs/guards/serializers) and _routes (handlers).
# @REJECTED: Keeping all routes in one file because it exceeded the fractal limit by 6x and accumulated severe structural erosion risk.
# @LAYER API
# @RATIONALE Original 2484-line monolith violated INV_7 (400-line module limit) by 6x. Decomposed into _dependencies (DTOs/guards/serializers) and _routes (handlers).
# @REJECTED Keeping all routes in one file because it exceeded the fractal limit by 6x and accumulated severe structural erosion risk.
from src.api.routes.dataset_review_pkg._dependencies import ( # noqa: F401
ApproveMappingRequest,

View File

@@ -1,6 +1,6 @@
# #region DatasetReviewDependencies [C:2] [TYPE Module] [SEMANTICS dataset, review, dependency, di, serialize]
# @BRIEF Dependency injection, serialization helpers, and feature-flag guards for dataset review endpoints.
# @LAYER: API
# @LAYER API
from __future__ import annotations
@@ -628,7 +628,7 @@ def _resolve_candidate_source_version(field, source_id):
# #region _update_semantic_field_state [C:3] [TYPE Function]
# @BRIEF Apply field-level semantic manual override or candidate acceptance.
# @POST: Manual overrides always set manual provenance plus lock.
# @POST Manual overrides always set manual provenance plus lock.
def _update_semantic_field_state(field, request, changed_by):
has_manual_override = any(v is not None for v in [request.verbose_name, request.description, request.display_format])
selected_candidate = None

View File

@@ -1,7 +1,7 @@
# #region DatasetsApi [C:5] [TYPE Module] [SEMANTICS fastapi, dataset, api, search, mapping, mapped-fields]
#
# @BRIEF API endpoints for the Dataset Hub - listing datasets with mapping progress
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AppDependencies]
# @RELATION DEPENDS_ON -> [ResourceService]
# @RELATION DEPENDS_ON -> [SupersetClient]
@@ -10,7 +10,7 @@
# @POST Returns dataset metadata with mapping status.
# @SIDE_EFFECT Reads from Superset API and task manager.
# @DATA_CONTRACT Input -> DatasetQuery, Output -> DatasetsResponse, DatasetDetailResponse
# @INVARIANT: All dataset responses include last_task metadata
# @INVARIANT All dataset responses include last_task metadata
import re
@@ -149,9 +149,9 @@ class MetricDescriptionUpdate(BaseModel):
# #region get_dataset_ids [C:4] [TYPE Function]
# @BRIEF Fetch list of all dataset IDs from a specific environment (without pagination)
# @PRE: env_id must be a valid environment ID
# @POST: Returns a list of all dataset IDs
# @RELATION CALLS -> [get_datasets_with_status]
# @PRE env_id must be a valid environment ID
# @POST Returns a list of all dataset IDs
# @RELATION CALLS -> [EXT:method:get_datasets_with_status]
@router.get("/ids")
async def get_dataset_ids(
env_id: str,
@@ -197,13 +197,13 @@ async def get_dataset_ids(
# #region get_datasets [C:4] [TYPE Function]
# @BRIEF Fetch list of datasets from a specific environment with mapping progress and stats.
# @PRE: env_id must be a valid environment ID
# @PRE: page must be >= 1 if provided
# @PRE: page_size must be between 1 and 100 if provided
# @POST: Returns a list of datasets with enhanced metadata, pagination info, and StatsCounts.
# @POST: Response includes pagination metadata (page, page_size, total, total_pages) and stats object.
# @PRE env_id must be a valid environment ID
# @PRE page must be >= 1 if provided
# @PRE page_size must be between 1 and 100 if provided
# @POST Returns a list of datasets with enhanced metadata, pagination info, and StatsCounts.
# @POST Response includes pagination metadata (page, page_size, total, total_pages) and stats object.
# @RATIONALE Stats counts returned in the same response as datasets to avoid an extra API call for the Stats Bar (FR-022).
# @RELATION CALLS -> [get_datasets_with_status]
# @RELATION CALLS -> [EXT:method:get_datasets_with_status]
@router.get("", response_model=DatasetsResponse)
async def get_datasets(
env_id: str,
@@ -324,11 +324,11 @@ class MapColumnsRequest(BaseModel):
# #region map_columns [C:4] [TYPE Function]
# @BRIEF Trigger bulk column mapping for datasets
# @PRE: User has permission plugin:mapper:execute
# @PRE: env_id is a valid environment ID
# @PRE: dataset_ids is a non-empty list
# @POST: Returns task_id for tracking mapping progress
# @POST: Task is created and queued for execution
# @PRE User has permission plugin:mapper:execute
# @PRE env_id is a valid environment ID
# @PRE dataset_ids is a non-empty list
# @POST Returns task_id for tracking mapping progress
# @POST Task is created and queued for execution
# @RELATION DISPATCHES -> [MapperPlugin]
# @RELATION CALLS -> [create_task]
@router.post("/map-columns", response_model=TaskResponse)
@@ -397,11 +397,11 @@ class GenerateDocsRequest(BaseModel):
# #region generate_docs [C:4] [TYPE Function]
# @BRIEF Trigger bulk documentation generation for datasets
# @PRE: User has permission plugin:llm_analysis:execute
# @PRE: env_id is a valid environment ID
# @PRE: dataset_ids is a non-empty list
# @POST: Returns task_id for tracking documentation generation progress
# @POST: Task is created and queued for execution
# @PRE User has permission plugin:llm_analysis:execute
# @PRE env_id is a valid environment ID
# @PRE dataset_ids is a non-empty list
# @POST Returns task_id for tracking documentation generation progress
# @POST Task is created and queued for execution
# @RELATION DISPATCHES -> [DocumentationPlugin]
# @RELATION CALLS -> [create_task]
@router.post("/generate-docs", response_model=TaskResponse)
@@ -450,9 +450,9 @@ async def generate_docs(
# #region get_dataset_detail [C:4] [TYPE Function]
# @BRIEF Get detailed dataset information including columns and linked dashboards
# @PRE: env_id is a valid environment ID
# @PRE: dataset_id is a valid dataset ID
# @POST: Returns detailed dataset info with columns and linked dashboards
# @PRE env_id is a valid environment ID
# @PRE dataset_id is a valid dataset ID
# @POST Returns detailed dataset info with columns and linked dashboards
# @RELATION CALLS -> [SupersetClientGetDatasetDetail]
@router.get("/{dataset_id}", response_model=DatasetDetailResponse)
async def get_dataset_detail(
@@ -505,13 +505,13 @@ def _strip_html_tags(text: str) -> str:
# #region update_column_description [C:4] [TYPE Endpoint]
# @BRIEF Save description for a single dataset column. Internal: GET full dataset from Superset, modify one column's description, PUT back.
# @PRE: dataset_id and column_id must exist in the target environment.
# @POST: Column description in Superset is updated. Response confirms success.
# @SIDE_EFFECT: Mutates dataset metadata in upstream Superset instance via PUT.
# @PRE dataset_id and column_id must exist in the target environment.
# @POST Column description in Superset is updated. Response confirms success.
# @SIDE_EFFECT Mutates dataset metadata in upstream Superset instance via PUT.
# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or column not found. 502 if Superset upstream fails.
# @ERROR: 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or column_id not found. 502 — Superset upstream failure (GET or PUT).
# @RELATION CALLS -> [SupersetClient.get_dataset]
# @RELATION CALLS -> [SupersetClient.update_dataset]
# @ERROR 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or column_id not found. 502 — Superset upstream failure (GET or PUT).
# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]
# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]
# @RATIONALE Must perform GET→modify→PUT because Superset has no PATCH for individual columns — only full object PUT with override_columns=false.
# @REJECTED Direct PUT from frontend — rejected because frontend would need to handle full Superset payload structure.
@router.put("/{dataset_id}/columns/{column_id}/description")
@@ -594,13 +594,13 @@ async def update_column_description(
# #region update_metric_description [C:4] [TYPE Endpoint]
# @BRIEF Save description for a single dataset metric. Mirror of update_column_description for metrics.
# @PRE: dataset_id and metric_id must exist in the target environment.
# @POST: Metric description in Superset is updated.
# @SIDE_EFFECT: Mutates dataset metadata in upstream Superset instance via PUT.
# @PRE dataset_id and metric_id must exist in the target environment.
# @POST Metric description in Superset is updated.
# @SIDE_EFFECT Mutates dataset metadata in upstream Superset instance via PUT.
# @VALIDATION: description — string, max 2000 chars, plain text only (HTML stripped), may be empty string to clear description, no trimming applied. 404 if dataset or metric not found. 502 if Superset upstream fails.
# @ERROR: 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or metric_id not found. 502 — Superset upstream failure (GET or PUT).
# @RELATION CALLS -> [SupersetClient.get_dataset]
# @RELATION CALLS -> [SupersetClient.update_dataset]
# @ERROR 400 — description exceeds max length or contains non-plaintext content. 404 — dataset_id or metric_id not found. 502 — Superset upstream failure (GET or PUT).
# @RELATION CALLS -> [EXT:method:SupersetClient.get_dataset]
# @RELATION CALLS -> [EXT:method:SupersetClient.update_dataset]
@router.put("/{dataset_id}/metrics/{metric_id}/description")
async def update_metric_description(
dataset_id: int,

View File

@@ -1,11 +1,11 @@
# #region EnvironmentsApi [C:5] [TYPE Module] [SEMANTICS fastapi, environment, api]
#
# @BRIEF API endpoints for listing environments and their databases.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AppDependencies]
# @RELATION DEPENDS_ON -> [SupersetClient]
#
# @INVARIANT: Environment IDs must exist in the configuration.
# @INVARIANT Environment IDs must exist in the configuration.
from fastapi import APIRouter, Depends, HTTPException
@@ -20,8 +20,8 @@ router = APIRouter(prefix="/api/environments", tags=["Environments"])
# #region _normalize_superset_env_url [TYPE Function]
# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.
# @PRE: raw_url can be empty.
# @POST: Returns normalized base URL.
# @PRE raw_url can be empty.
# @POST Returns normalized base URL.
def _normalize_superset_env_url(raw_url: str) -> str:
normalized = str(raw_url or "").strip().rstrip("/")
if normalized.lower().endswith("/api/v1"):
@@ -54,9 +54,9 @@ class DatabaseResponse(BaseModel):
# #region get_environments [TYPE Function] [SEMANTICS list, environments, config]
# @BRIEF List all configured environments.
# @LAYER: API
# @PRE: config_manager is injected via Depends.
# @POST: Returns a list of EnvironmentResponse objects.
# @LAYER API
# @PRE config_manager is injected via Depends.
# @POST Returns a list of EnvironmentResponse objects.
@router.get("", response_model=list[EnvironmentResponse])
async def get_environments(
config_manager=Depends(get_config_manager),
@@ -91,9 +91,9 @@ async def get_environments(
# #region update_environment_schedule [TYPE Function] [SEMANTICS update, schedule, backup, environment]
# @BRIEF Update backup schedule for an environment.
# @LAYER: API
# @PRE: Environment id exists, schedule is valid ScheduleSchema.
# @POST: Backup schedule updated and scheduler reloaded.
# @LAYER API
# @PRE Environment id exists, schedule is valid ScheduleSchema.
# @POST Backup schedule updated and scheduler reloaded.
@router.put("/{id}/schedule")
async def update_environment_schedule(
id: str,
@@ -122,9 +122,9 @@ async def update_environment_schedule(
# #region get_environment_databases [TYPE Function] [SEMANTICS fetch, databases, superset, environment]
# @BRIEF Fetch the list of databases from a specific environment.
# @LAYER: API
# @PRE: Environment id exists.
# @POST: Returns a list of database summaries from the environment.
# @LAYER API
# @PRE Environment id exists.
# @POST Returns a list of database summaries from the environment.
@router.get("/{id}/databases")
async def get_environment_databases(
id: str,

View File

@@ -1,14 +1,12 @@
# #region GitPackage [C:5] [TYPE Module] [SEMANTICS git, api, package, sync]
# @BRIEF Package root for decomposed git routes. Re-exports all public symbols from submodules.
# @LAYER: API
# @RELATION USES -> [GitRouter, GitDeps, GitHelpers, GitConfigRoutes, GitGiteaRoutes,
# GitRepoRoutes, GitRepoOperationsRoutes, GitRepoLifecycleRoutes,
# GitMergeRoutes, GitEnvironmentRoutes]
# @INVARIANT: git_service and os are module-level attributes for test monkeypatch compatibility.
# @PRE: Git service initialized
# @POST: Git API package exported
# @SIDE_EFFECT: Registers git route submodules
# @DATA_CONTRACT: GitRequest -> GitResponse
# @LAYER API
# @RELATION CALLS -> [[EXT:list:GitPackage_all_routes]]
# @INVARIANT git_service and os are module-level attributes for test monkeypatch compatibility.
# @PRE Git service initialized
# @POST Git API package exported
# @SIDE_EFFECT Registers git route submodules
# @DATA_CONTRACT GitRequest -> GitResponse
# All route functions are re-exported for direct access via `from src.api.routes import git`.
import os

View File

@@ -1,6 +1,6 @@
# #region GitConfigRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, api, search, connection]
# @BRIEF FastAPI endpoints for Git server configuration CRUD and connection testing.
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException

View File

@@ -1,7 +1,7 @@
# #region GitDeps [C:1] [TYPE Module] [SEMANTICS git, dependency, service, provider]
# @BRIEF Shared dependency wiring for monkeypatch-safe git_service access and constants.
# @LAYER: API
# @INVARIANT: get_git_service() resolves from sys.modules at call time so test monkeypatching
# @LAYER API
# @INVARIANT get_git_service() resolves from sys.modules at call time so test monkeypatching
# of git_routes.git_service (the __init__ attribute) takes effect across all submodules.
import sys

View File

@@ -1,6 +1,6 @@
# #region GitEnvironmentRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, environment, api]
# @BRIEF FastAPI endpoint for listing deployment environments.
# @LAYER: API
# @LAYER API
from fastapi import Depends

View File

@@ -1,6 +1,6 @@
# #region GitGiteaRoutes [C:2] [TYPE Module] [SEMANTICS fastapi, git, gitea, api]
# @BRIEF FastAPI endpoints for Gitea-specific repository operations.
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException

View File

@@ -1,9 +1,9 @@
# #region GitHelpers [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]
# @BRIEF Shared helper functions for Git route modules.
# @LAYER: API
# @RELATION USES -> [GitDeps]
# @RELATION USES -> [SupersetClient]
# @RELATION USES -> [UserDashboardPreference]
# @LAYER API
# @RELATION CALLS -> [GitDeps]
# @RELATION CALLS -> [SupersetClient]
# @RELATION CALLS -> [UserDashboardPreference]
import os
@@ -21,7 +21,7 @@ from ._deps import get_git_service
# #region _build_no_repo_status_payload [C:1] [TYPE Function]
# @BRIEF Build a consistent status payload for dashboards without initialized repositories.
# @POST: Returns a stable payload compatible with frontend repository status parsing.
# @POST Returns a stable payload compatible with frontend repository status parsing.
def _build_no_repo_status_payload() -> dict:
return {
"is_dirty": False,
@@ -43,8 +43,8 @@ def _build_no_repo_status_payload() -> dict:
# #region _handle_unexpected_git_route_error [C:1] [TYPE Function]
# @BRIEF Convert unexpected route-level exceptions to stable 500 API responses.
# @PRE: `error` is a non-HTTPException instance.
# @POST: Raises HTTPException(500) with route-specific context.
# @PRE `error` is a non-HTTPException instance.
# @POST Raises HTTPException(500) with route-specific context.
def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> None:
logger.error(f"[{route_name}][Coherence:Failed] {error}")
raise HTTPException(status_code=500, detail=f"{route_name} failed: {error!s}")
@@ -53,8 +53,8 @@ def _handle_unexpected_git_route_error(route_name: str, error: Exception) -> Non
# #region _resolve_repository_status [C:2] [TYPE Function]
# @BRIEF Resolve repository status for one dashboard with graceful NO_REPO semantics.
# @PRE: `dashboard_id` is a valid integer.
# @POST: Returns standard status payload or `NO_REPO` payload when repository path is absent.
# @PRE `dashboard_id` is a valid integer.
# @POST Returns standard status payload or `NO_REPO` payload when repository path is absent.
def _resolve_repository_status(dashboard_id: int) -> dict:
git_service = get_git_service()
repo_path = git_service._get_repo_path(dashboard_id)

View File

@@ -1,6 +1,6 @@
# #region GitMergeRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api]
# @BRIEF FastAPI endpoints for merge operations (status, conflicts, resolve, abort, continue).
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException

View File

@@ -1,6 +1,6 @@
# #region GitRepoLifecycleRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, sync, deploy]
# @BRIEF FastAPI endpoints for Git lifecycle operations (sync, promote, deploy).
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException

View File

@@ -1,6 +1,6 @@
# #region GitRepoOperationsRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, history, diff]
# @BRIEF FastAPI endpoints for Git repository operations (commit, push, pull, status, diff, history).
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException

View File

@@ -1,6 +1,6 @@
# #region GitRepoRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, git, api, search]
# @BRIEF FastAPI endpoints for core Git repository operations (init, binding, branches, checkout).
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException

View File

@@ -1,6 +1,6 @@
# #region GitRouter [C:1] [TYPE Module] [SEMANTICS fastapi, git, api]
# @BRIEF Shared APIRouter for all Git route modules.
# @LAYER: API
# @LAYER API
from fastapi import APIRouter

View File

@@ -1,10 +1,10 @@
# #region GitSchemas [C:1] [TYPE Module] [SEMANTICS fastapi, git, api, git-server-config-base]
#
# @BRIEF Defines Pydantic models for the Git integration API layer.
# @LAYER: API
# @RELATION DEPENDS_ON -> backend.src.models.git
# @LAYER API
# @RELATION DEPENDS_ON -> [EXT:path:backend.src.models.git]
#
# @INVARIANT: All schemas must be compatible with the FastAPI router.
# @INVARIANT All schemas must be compatible with the FastAPI router.
from datetime import datetime
from typing import Any

View File

@@ -1,7 +1,7 @@
# #region health_router [C:3] [TYPE Module] [SEMANTICS fastapi, health, api, search, dashboard]
# @BRIEF API endpoints for dashboard health monitoring and status aggregation.
# @LAYER: UI/API
# @RELATION DEPENDS_ON -> health_service
# @LAYER UI
# @RELATION DEPENDS_ON -> [health_service]
from fastapi import APIRouter, Depends, HTTPException, Query, status
@@ -16,9 +16,9 @@ router = APIRouter(prefix="/api/health", tags=["Health"])
# #region get_health_summary [TYPE Function]
# @BRIEF Get aggregated health status for all dashboards.
# @PRE: Caller has read permission for dashboard health view.
# @POST: Returns HealthSummaryResponse.
# @RELATION CALLS -> backend.src.services.health_service.HealthService
# @PRE Caller has read permission for dashboard health view.
# @POST Returns HealthSummaryResponse.
# @RELATION CALLS -> [EXT:path:backend.src.services.health_service.HealthService]
@router.get("/summary", response_model=HealthSummaryResponse)
async def get_health_summary(
environment_id: str | None = Query(None),
@@ -39,9 +39,9 @@ async def get_health_summary(
# #region delete_health_report [TYPE Function]
# @BRIEF Delete one persisted dashboard validation report from health summary.
# @PRE: Caller has write permission for tasks/report maintenance.
# @POST: Validation record is removed; linked task/logs are cleaned when available.
# @RELATION CALLS -> backend.src.services.health_service.HealthService
# @PRE Caller has write permission for tasks/report maintenance.
# @POST Validation record is removed; linked task/logs are cleaned when available.
# @RELATION CALLS -> [EXT:path:backend.src.services.health_service.HealthService]
@router.delete("/summary/{record_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_health_report(
record_id: str,

View File

@@ -1,6 +1,6 @@
# #region LlmRoutes [C:3] [TYPE Module] [SEMANTICS fastapi, llm, api, provider]
# @BRIEF API routes for LLM provider configuration and management.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
# @RELATION DEPENDS_ON -> [get_current_user]
@@ -39,8 +39,8 @@ router = APIRouter(tags=["LLM"])
# #region _is_valid_runtime_api_key [C:4] [TYPE Function]
# @BRIEF Validate decrypted runtime API key presence/shape.
# @PRE: value can be None.
# @POST: Returns True only for non-placeholder key.
# @PRE value can be None.
# @POST Returns True only for non-placeholder key.
# @RELATION BINDS_TO -> [LlmRoutes]
def _is_valid_runtime_api_key(value: str | None) -> bool:
key = (value or "").strip()
@@ -56,8 +56,8 @@ def _is_valid_runtime_api_key(value: str | None) -> bool:
# #region get_providers [C:4] [TYPE Function]
# @BRIEF Retrieve all LLM provider configurations.
# @PRE: User is authenticated.
# @POST: Returns list of LLMProviderConfig.
# @PRE User is authenticated.
# @POST Returns list of LLMProviderConfig.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
@router.get("/providers", response_model=list[LLMProviderConfig])
@@ -93,8 +93,8 @@ async def get_providers(
# #region fetch_models [C:4] [TYPE Function]
# @BRIEF Fetch available models from an LLM provider by base_url+provider_type, or by provider_id.
# @PRE: User is authenticated. Either provider_id or base_url+provider_type must be provided.
# @POST: Returns a list of available model IDs.
# @PRE User is authenticated. Either provider_id or base_url+provider_type must be provided.
# @POST Returns a list of available model IDs.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION CALLS -> [LLMClient]
@router.post("/providers/fetch-models")
@@ -172,8 +172,8 @@ async def fetch_models(
# #region get_llm_status [C:4] [TYPE Function]
# @BRIEF Returns whether LLM runtime is configured for dashboard validation.
# @PRE: User is authenticated.
# @POST: configured=true only when an active provider with valid decrypted key exists.
# @PRE User is authenticated.
# @POST configured=true only when an active provider with valid decrypted key exists.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION CALLS -> [_is_valid_runtime_api_key]
@router.get("/status")
@@ -243,8 +243,8 @@ async def get_llm_status(
# #region create_provider [C:4] [TYPE Function]
# @BRIEF Create a new LLM provider configuration.
# @PRE: User is authenticated and has admin permissions.
# @POST: Returns the created LLMProviderConfig.
# @PRE User is authenticated and has admin permissions.
# @POST Returns the created LLMProviderConfig.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
@router.post(
@@ -277,8 +277,8 @@ async def create_provider(
# #region update_provider [C:4] [TYPE Function]
# @BRIEF Update an existing LLM provider configuration.
# @PRE: User is authenticated and has admin permissions.
# @POST: Returns the updated LLMProviderConfig.
# @PRE User is authenticated and has admin permissions.
# @POST Returns the updated LLMProviderConfig.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
@router.put("/providers/{provider_id}", response_model=LLMProviderConfig)
@@ -313,8 +313,8 @@ async def update_provider(
# #region delete_provider [C:4] [TYPE Function]
# @BRIEF Delete an LLM provider configuration.
# @PRE: User is authenticated and has admin permissions.
# @POST: Returns success status.
# @PRE User is authenticated and has admin permissions.
# @POST Returns success status.
# @RELATION CALLS -> [LLMProviderService]
@router.delete("/providers/{provider_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_provider(
@@ -336,8 +336,8 @@ async def delete_provider(
# #region test_connection [C:4] [TYPE Function]
# @BRIEF Test connection to an LLM provider.
# @PRE: User is authenticated.
# @POST: Returns success status and message.
# @PRE User is authenticated.
# @POST Returns success status and message.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [LLMClient]
@router.post("/providers/{provider_id}/test")
@@ -391,8 +391,8 @@ async def test_connection(
# #region test_provider_config [C:4] [TYPE Function]
# @BRIEF Test connection with a provided configuration (not yet saved).
# @PRE: User is authenticated.
# @POST: Returns success status and message.
# @PRE User is authenticated.
# @POST Returns success status and message.
# @RELATION DEPENDS_ON -> [LLMClient]
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
@router.post("/providers/test")

View File

@@ -4,7 +4,7 @@
# @RELATION DEPENDS_ON -> [MaintenanceSchemasModule]
# @RELATION DEPENDS_ON -> [MaintenanceRouter]
# @RELATION DEPENDS_ON -> [AppDependencies]
# @RELATION DEPENDS_ON -> [MaintenanceServiceModule]
# @RELATION DEPENDS_ON -> [EXT:frontend:MaintenanceServiceModule]
# @INVARIANT All mutation endpoints return 202 {task_id} per spec.
# @INVARIANT RBAC enforced per FR-015 matrix via has_permission() dependency.
@@ -54,7 +54,7 @@ from ._schemas import (
# #region list_dashboard_banners [C:2] [TYPE Function]
# @BRIEF Get per-dashboard banner state for the Dashboard Hub indicator.
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "READ")]
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "READ")]
@router.get("/dashboard-banners", response_model=list[MaintenanceDashboardBannerState])
async def list_dashboard_banners(
db: Session = Depends(get_db),
@@ -118,7 +118,7 @@ async def list_dashboard_banners(
# #region list_events [C:2] [TYPE Function]
# @BRIEF Get active and completed maintenance event lists with affected dashboard counts.
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "READ")]
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "READ")]
@router.get("/events", response_model=MaintenanceEventListResponse)
async def list_events(
db: Session = Depends(get_db),
@@ -226,7 +226,7 @@ async def list_events(
# Always returns 202 with task_id and maintenance_id.
# Idempotency: same (tables, start_time, end_time) returns already_active (409-like but 200).
# Tables sorted & lowered for idempotency key. Message NOT in key.
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "WRITE")]
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
# @RELATION DEPENDS_ON -> [TaskManager]
@router.post(
"/start",
@@ -367,7 +367,7 @@ async def start_maintenance(
# #region end_maintenance [C:3] [TYPE Function]
# @BRIEF End a specific maintenance event by ID. Removes banners from affected dashboards.
# Always returns 202 with task_id. Idempotent: already_completed returns success.
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "WRITE")]
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
@router.post(
"/{maintenance_id}/end",
status_code=status.HTTP_202_ACCEPTED,
@@ -437,7 +437,7 @@ async def end_maintenance(
# #region end_all_maintenance [C:3] [TYPE Function]
# @BRIEF End all active maintenance events. Removes all banners from all dashboards.
# Always returns 202 with task_id. Supports environment scoping for API keys.
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "WRITE")]
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "WRITE")]
@router.post(
"/end-all",
status_code=status.HTTP_202_ACCEPTED,
@@ -500,7 +500,7 @@ async def end_all_maintenance(
# #region get_maintenance_settings [C:2] [TYPE Function]
# @BRIEF Get current maintenance settings configuration.
# @RELATION DEPENDS_ON -> [has_permission("maintenance", "READ")]
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("maintenance", "READ")]
@router.get("/settings", response_model=MaintenanceSettingsResponse)
async def get_maintenance_settings(
db: Session = Depends(get_db),
@@ -536,7 +536,7 @@ async def get_maintenance_settings(
# #region update_maintenance_settings [C:2] [TYPE Function]
# @BRIEF Update maintenance settings. All fields optional for partial update.
# admin role required per FR-015.
# @RELATION DEPENDS_ON -> [has_permission("admin:settings", "WRITE")]
# @RELATION DEPENDS_ON -> [EXT:code:has_permission("admin:settings", "WRITE")]
@router.put("/settings", response_model=MaintenanceSettingsResponse)
async def update_maintenance_settings(
settings_data: MaintenanceSettingsUpdate,

View File

@@ -1,7 +1,7 @@
# #region MaintenanceSchemasModule [C:2] [TYPE Module] [SEMANTICS pydantic, schema, maintenance, request, response]
# @BRIEF Pydantic models for Maintenance Banner API: request/response schemas per data-model.md.
# @LAYER API
# @RELATION DEPENDS_ON -> [Pydantic]
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
# @INVARIANT All response schemas use the standard envelope shape: { status, data, error, meta }
from datetime import datetime

View File

@@ -1,7 +1,7 @@
# #region MappingsApi [C:3] [TYPE Module] [SEMANTICS fastapi, mapping, api, mapping-create]
#
# @BRIEF API endpoints for managing database mappings and getting suggestions.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [AppDependencies]
# @RELATION DEPENDS_ON -> [DatabaseModule]
# @RELATION DEPENDS_ON -> [mapping_service]
@@ -54,8 +54,8 @@ class SuggestRequest(BaseModel):
# #region get_mappings [TYPE Function]
# @BRIEF List all saved database mappings.
# @PRE: db session is injected.
# @POST: Returns filtered list of DatabaseMapping records.
# @PRE db session is injected.
# @POST Returns filtered list of DatabaseMapping records.
@router.get("", response_model=list[MappingResponse])
async def get_mappings(
source_env_id: str | None = None,
@@ -74,8 +74,8 @@ async def get_mappings(
# #region create_mapping [TYPE Function]
# @BRIEF Create or update a database mapping.
# @PRE: mapping is valid MappingCreate, db session is injected.
# @POST: DatabaseMapping created or updated in database.
# @PRE mapping is valid MappingCreate, db session is injected.
# @POST DatabaseMapping created or updated in database.
@router.post("", response_model=MappingResponse)
async def create_mapping(
mapping: MappingCreate,
@@ -107,8 +107,8 @@ async def create_mapping(
# #region suggest_mappings_api [TYPE Function]
# @BRIEF Get suggested mappings based on fuzzy matching.
# @PRE: request is valid SuggestRequest, config_manager is injected.
# @POST: Returns mapping suggestions.
# @PRE request is valid SuggestRequest, config_manager is injected.
# @POST Returns mapping suggestions.
@router.post("/suggest")
async def suggest_mappings_api(
request: SuggestRequest,

View File

@@ -1,6 +1,6 @@
# #region MigrationApi [C:5] [TYPE Module] [SEMANTICS fastapi, migration, api, sync, search, mapping]
# @BRIEF HTTP contract layer for migration orchestration, settings, dry-run, and mapping sync endpoints.
# @LAYER: Infra
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [AppDependencies]
# @RELATION DEPENDS_ON -> [DatabaseModule]
# @RELATION DEPENDS_ON -> [DashboardSelection]
@@ -8,18 +8,18 @@
# @RELATION DEPENDS_ON -> [MigrationDryRunService]
# @RELATION DEPENDS_ON -> [IdMappingService]
# @RELATION DEPENDS_ON -> [ResourceMapping]
# @INVARIANT: Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.
# @PRE: Backend core services initialized and Database session available.
# @POST: Migration tasks are enqueued or dry-run results are computed and returned.
# @SIDE_EFFECT: Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.
# @DATA_CONTRACT: [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]
# @TEST_CONTRACT: [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]
# @TEST_SCENARIO: [invalid_environment] -> [HTTP_400_or_404]
# @TEST_SCENARIO: [valid_execution] -> [success_payload_with_required_fields]
# @TEST_EDGE: [missing_field] ->[HTTP_400]
# @TEST_EDGE: [invalid_type] ->[validation_error]
# @TEST_EDGE: [external_fail] ->[HTTP_500]
# @TEST_INVARIANT: [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]
# @INVARIANT Migration endpoints never execute with invalid environment references and always return explicit HTTP errors on guard failures.
# @PRE Backend core services initialized and Database session available.
# @POST Migration tasks are enqueued or dry-run results are computed and returned.
# @SIDE_EFFECT Enqueues long-running tasks, potentially mutates ResourceMapping table, and performs remote Superset API calls.
# @DATA_CONTRACT [DashboardSelection | QueryParams] -> [TaskResponse | DryRunResult | MappingSummary]
# @TEST_CONTRACT [DashboardSelection + configured envs] -> [task_id | dry-run result | sync summary]
# @TEST_SCENARIO [invalid_environment] -> [HTTP_400_or_404]
# @TEST_SCENARIO [valid_execution] -> [success_payload_with_required_fields]
# @TEST_EDGE [missing_field] ->[HTTP_400]
# @TEST_EDGE [invalid_type] ->[validation_error]
# @TEST_EDGE [external_fail] ->[HTTP_500]
# @TEST_INVARIANT [EnvironmentValidationBeforeAction] -> VERIFIED_BY: [invalid_environment, valid_execution]
from typing import Any, cast
@@ -42,11 +42,11 @@ router = APIRouter(prefix="/api", tags=["migration"])
# #region get_dashboards [C:3] [TYPE Function]
# @BRIEF Fetch dashboard metadata from a requested environment for migration selection UI.
# @PRE: env_id is provided and exists in configured environments.
# @POST: Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.
# @SIDE_EFFECT: Reads environment configuration and performs remote Superset metadata retrieval over network.
# @DATA_CONTRACT: Input[str env_id] -> Output[List[DashboardMetadata]]
# @RELATION CALLS -> [SupersetClient.get_dashboards_summary]
# @PRE env_id is provided and exists in configured environments.
# @POST Returns List[DashboardMetadata] for the resolved environment; emits HTTP_404 when environment is absent.
# @SIDE_EFFECT Reads environment configuration and performs remote Superset metadata retrieval over network.
# @DATA_CONTRACT Input[str env_id] -> Output[List[DashboardMetadata]]
# @RELATION CALLS -> [EXT:method:SupersetClient.get_dashboards_summary]
@router.get("/environments/{env_id}/dashboards", response_model=list[DashboardMetadata])
async def get_dashboards(
env_id: str,
@@ -73,13 +73,13 @@ async def get_dashboards(
# #region execute_migration [C:5] [TYPE Function]
# @BRIEF Validate migration selection and enqueue asynchronous migration task execution.
# @PRE: DashboardSelection payload is valid and both source/target environments exist.
# @POST: Returns {"task_id": str, "message": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.
# @SIDE_EFFECT: Reads configuration, writes task record through task manager, and writes operational logs.
# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, str]]
# @PRE DashboardSelection payload is valid and both source/target environments exist.
# @POST Returns {"task_id": str, "message": str} when task creation succeeds; emits HTTP_400/HTTP_500 on failure.
# @SIDE_EFFECT Reads configuration, writes task record through task manager, and writes operational logs.
# @DATA_CONTRACT Input[DashboardSelection] -> Output[Dict[str, str]]
# @RELATION CALLS -> [create_task]
# @RELATION DEPENDS_ON -> [DashboardSelection]
# @INVARIANT: Migration task dispatch never occurs before source and target environment ids pass guard validation.
# @INVARIANT Migration task dispatch never occurs before source and target environment ids pass guard validation.
@router.post("/migration/execute")
async def execute_migration(
selection: DashboardSelection,
@@ -136,13 +136,13 @@ async def execute_migration(
# #region dry_run_migration [C:5] [TYPE Function]
# @BRIEF Build pre-flight migration diff and risk summary without mutating target systems.
# @PRE: DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.
# @POST: Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.
# @SIDE_EFFECT: Reads local mappings from DB and fetches source/target metadata via Superset API.
# @DATA_CONTRACT: Input[DashboardSelection] -> Output[Dict[str, Any]]
# @PRE DashboardSelection is valid, source and target environments exist, differ, and selected_ids is non-empty.
# @POST Returns deterministic dry-run payload; emits HTTP_400 for guard violations and HTTP_500 for orchestrator value errors.
# @SIDE_EFFECT Reads local mappings from DB and fetches source/target metadata via Superset API.
# @DATA_CONTRACT Input[DashboardSelection] -> Output[Dict[str, Any]]
# @RELATION DEPENDS_ON -> [DashboardSelection]
# @RELATION DEPENDS_ON -> [MigrationDryRunService]
# @INVARIANT: Dry-run flow remains read-only and rejects identical source/target environments before service execution.
# @INVARIANT Dry-run flow remains read-only and rejects identical source/target environments before service execution.
@router.post("/migration/dry-run", response_model=dict[str, Any])
async def dry_run_migration(
selection: DashboardSelection,
@@ -202,10 +202,10 @@ async def dry_run_migration(
# #region get_migration_settings [C:3] [TYPE Function]
# @BRIEF Read and return configured migration synchronization cron expression.
# @PRE: Configuration store is available and requester has READ permission.
# @POST: Returns {"cron": str} reflecting current persisted settings value.
# @SIDE_EFFECT: Reads configuration from config manager.
# @DATA_CONTRACT: Input[None] -> Output[Dict[str, str]]
# @PRE Configuration store is available and requester has READ permission.
# @POST Returns {"cron": str} reflecting current persisted settings value.
# @SIDE_EFFECT Reads configuration from config manager.
# @DATA_CONTRACT Input[None] -> Output[Dict[str, str]]
# @RELATION DEPENDS_ON -> [AppDependencies]
@router.get("/migration/settings", response_model=dict[str, str])
async def get_migration_settings(
@@ -223,10 +223,10 @@ async def get_migration_settings(
# #region update_migration_settings [C:3] [TYPE Function]
# @BRIEF Validate and persist migration synchronization cron expression update.
# @PRE: Payload includes "cron" key and requester has WRITE permission.
# @POST: Returns {"cron": str, "status": "updated"} and persists updated cron value.
# @SIDE_EFFECT: Mutates configuration and writes persisted config through config manager.
# @DATA_CONTRACT: Input[Dict[str, str]] -> Output[Dict[str, str]]
# @PRE Payload includes "cron" key and requester has WRITE permission.
# @POST Returns {"cron": str, "status": "updated"} and persists updated cron value.
# @SIDE_EFFECT Mutates configuration and writes persisted config through config manager.
# @DATA_CONTRACT Input[Dict[str, str]] -> Output[Dict[str, str]]
# @RELATION DEPENDS_ON -> [AppDependencies]
@router.put("/migration/settings", response_model=dict[str, str])
async def update_migration_settings(
@@ -254,10 +254,10 @@ async def update_migration_settings(
# #region get_resource_mappings [C:3] [TYPE Function]
# @BRIEF Fetch synchronized resource mappings with optional filters and pagination for migration mappings view.
# @PRE: skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.
# @POST: Returns {"items": [...], "total": int} where items reflect applied filters and pagination.
# @SIDE_EFFECT: Executes database read queries against ResourceMapping table.
# @DATA_CONTRACT: Input[QueryParams] -> Output[Dict[str, Any]]
# @PRE skip>=0, 1<=limit<=500, DB session is active, requester has READ permission.
# @POST Returns {"items": [...], "total": int} where items reflect applied filters and pagination.
# @SIDE_EFFECT Executes database read queries against ResourceMapping table.
# @DATA_CONTRACT Input[QueryParams] -> Output[Dict[str, Any]]
# @RELATION DEPENDS_ON -> [ResourceMapping]
@router.get("/migration/mappings-data", response_model=dict[str, Any])
async def get_resource_mappings(
@@ -326,10 +326,10 @@ async def get_resource_mappings(
# #region trigger_sync_now [C:3] [TYPE Function]
# @BRIEF Trigger immediate ID synchronization for every configured environment.
# @PRE: At least one environment is configured and requester has EXECUTE permission.
# @POST: Returns sync summary with synced/failed counts after attempting all environments.
# @SIDE_EFFECT: Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.
# @DATA_CONTRACT: Input[None] -> Output[Dict[str, Any]]
# @PRE At least one environment is configured and requester has EXECUTE permission.
# @POST Returns sync summary with synced/failed counts after attempting all environments.
# @SIDE_EFFECT Upserts Environment rows, commits DB transaction, performs network sync calls, and writes logs.
# @DATA_CONTRACT Input[None] -> Output[Dict[str, Any]]
# @RELATION DEPENDS_ON -> [IdMappingService]
# @RELATION CALLS -> [sync_environment]
@router.post("/migration/sync-now", response_model=dict[str, Any])

View File

@@ -1,6 +1,6 @@
# #region PluginsRouter [C:3] [TYPE Module] [SEMANTICS fastapi, plugin, api]
# @BRIEF Defines the FastAPI router for plugin-related endpoints, allowing clients to list available plugins.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [PluginConfig]
# @RELATION DEPENDS_ON -> [get_plugin_loader]
# @RELATION BINDS_TO -> [API_Routes]
@@ -16,8 +16,8 @@ router = APIRouter()
# #region list_plugins [TYPE Function]
# @BRIEF Retrieve a list of all available plugins.
# @PRE: plugin_loader is injected via Depends.
# @POST: Returns a list of PluginConfig objects.
# @PRE plugin_loader is injected via Depends.
# @POST Returns a list of PluginConfig objects.
# @RELATION CALLS -> [get_plugin_loader]
# @RELATION DEPENDS_ON -> [PluginConfig]
@router.get("", response_model=list[PluginConfig])

View File

@@ -1,21 +1,21 @@
# #region ProfileApiModule [C:5] [TYPE Module] [SEMANTICS fastapi, profile, api, validate, search, superset]
#
# @BRIEF Exposes self-scoped profile preference endpoints and environment-based Superset account lookup.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [ProfileService]
# @RELATION DEPENDS_ON -> [get_current_user]
# @RELATION DEPENDS_ON -> [get_db]
#
# @INVARIANT: Endpoints are self-scoped and never mutate another user preference.
# @PRE: Auth middleware configured, database session available
# @POST: Profile endpoints registered
# @INVARIANT Endpoints are self-scoped and never mutate another user preference.
# @PRE Auth middleware configured, database session available
# @POST Profile endpoints registered
# UX_STATE: ProfileLoad -> Returns stable ProfilePreferenceResponse for authenticated user.
# UX_STATE: Saving -> Validation errors map to actionable 422 details.
# UX_STATE: LookupLoading -> Returns success/degraded Superset lookup payload.
# UX_FEEDBACK: Stable status/message/warning payloads support profile page feedback.
# UX_RECOVERY: Lookup degradation keeps manual username save path available.
# @SIDE_EFFECT: Registers /api/profile/* routes
# @DATA_CONTRACT: ProfileRequest -> ProfileResponse
# @SIDE_EFFECT Registers /api/profile/* routes
# @DATA_CONTRACT ProfileRequest -> ProfileResponse
from fastapi import APIRouter, Depends, HTTPException, Query
@@ -48,8 +48,8 @@ router = APIRouter(prefix="/api/profile", tags=["profile"])
# #region _get_profile_service [TYPE Function]
# @RELATION CALLS -> ProfileService
# @BRIEF Build profile service for current request scope.
# @PRE: db session and config manager are available.
# @POST: Returns a ready ProfileService instance.
# @PRE db session and config manager are available.
# @POST Returns a ready ProfileService instance.
def _get_profile_service(db: Session, config_manager, plugin_loader=None) -> ProfileService:
return ProfileService(
db=db,
@@ -62,8 +62,8 @@ def _get_profile_service(db: Session, config_manager, plugin_loader=None) -> Pro
# #region get_preferences [TYPE Function]
# @RELATION CALLS -> ProfileService
# @BRIEF Get authenticated user's dashboard filter preference.
# @PRE: Valid JWT and authenticated user context.
# @POST: Returns preference payload for current user only.
# @PRE Valid JWT and authenticated user context.
# @POST Returns preference payload for current user only.
@router.get("/preferences", response_model=ProfilePreferenceResponse)
async def get_preferences(
current_user: User = Depends(get_current_user),
@@ -81,8 +81,8 @@ async def get_preferences(
# #region update_preferences [TYPE Function]
# @RELATION CALLS -> ProfileService
# @BRIEF Update authenticated user's dashboard filter preference.
# @PRE: Valid JWT and valid request payload.
# @POST: Persists normalized preference for current user or raises validation/authorization errors.
# @PRE Valid JWT and valid request payload.
# @POST Persists normalized preference for current user or raises validation/authorization errors.
@router.patch("/preferences", response_model=ProfilePreferenceResponse)
async def update_preferences(
payload: ProfilePreferenceUpdateRequest,
@@ -108,8 +108,8 @@ async def update_preferences(
# #region lookup_superset_accounts [TYPE Function]
# @RELATION CALLS -> ProfileService
# @BRIEF Lookup Superset account candidates in selected environment.
# @PRE: Valid JWT, authenticated context, and environment_id query parameter.
# @POST: Returns success or degraded lookup payload with stable shape.
# @PRE Valid JWT, authenticated context, and environment_id query parameter.
# @POST Returns success or degraded lookup payload with stable shape.
@router.get("/superset-accounts", response_model=SupersetAccountLookupResponse)
async def lookup_superset_accounts(
environment_id: str = Query(...),

View File

@@ -1,15 +1,15 @@
# #region ReportsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, report, api, transform, search, task]
# @BRIEF FastAPI router for unified task report list and detail retrieval endpoints.
# @LAYER: API
# @RELATION DEPENDS_ON -> [ReportsService:Class]
# @RELATION DEPENDS_ON -> [get_task_manager:Function]
# @RELATION DEPENDS_ON -> [get_clean_release_repository:Function]
# @RELATION DEPENDS_ON -> [has_permission:Function]
# @INVARIANT: Endpoints are read-only and do not trigger long-running tasks.
# @PRE: Reports service and dependencies are initialized.
# @POST: Router is configured and endpoints are ready for registration.
# @SIDE_EFFECT: None
# @DATA_CONTRACT: [ReportQuery] -> [ReportCollection | ReportDetailView]
# @LAYER API
# @RELATION DEPENDS_ON -> [ReportsService]
# @RELATION DEPENDS_ON -> [get_task_manager]
# @RELATION DEPENDS_ON -> [get_clean_release_repository]
# @RELATION DEPENDS_ON -> [has_permission]
# @INVARIANT Endpoints are read-only and do not trigger long-running tasks.
# @PRE Reports service and dependencies are initialized.
# @POST Router is configured and endpoints are ready for registration.
# @SIDE_EFFECT None
# @DATA_CONTRACT [ReportQuery] -> [ReportCollection | ReportDetailView]
from datetime import datetime
@@ -37,8 +37,8 @@ router = APIRouter(prefix="/api/reports", tags=["Reports"])
# #region _parse_csv_enum_list [C:1] [TYPE Function]
# @BRIEF Parse comma-separated query value into enum list.
# @PRE: raw may be None/empty or comma-separated values.
# @POST: Returns enum list or raises HTTP 400 with deterministic machine-readable payload.
# @PRE raw may be None/empty or comma-separated values.
# @POST Returns enum list or raises HTTP 400 with deterministic machine-readable payload.
# @RELATION BINDS_TO -> [ReportsRouter]
def _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:
with belief_scope("_parse_csv_enum_list"):
@@ -70,14 +70,14 @@ def _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:
# #region list_reports [C:2] [TYPE Function]
# @BRIEF Return paginated unified reports list.
# @PRE: authenticated/authorized request and validated query params.
# @POST: returns {items,total,page,page_size,has_next,applied_filters}.
# @POST: deterministic error payload for invalid filters.
# @RELATION CALLS -> [_parse_csv_enum_list:Function]
# @RELATION DEPENDS_ON -> [ReportQuery:Class]
# @RELATION DEPENDS_ON -> [ReportsService:Class]
# @PRE authenticated/authorized request and validated query params.
# @POST returns {items,total,page,page_size,has_next,applied_filters}.
# @POST deterministic error payload for invalid filters.
# @RELATION CALLS -> [_parse_csv_enum_list]
# @RELATION DEPENDS_ON -> [ReportQuery]
# @RELATION DEPENDS_ON -> [ReportsService]
#
# @TEST_CONTRACT: ListReportsApi ->
# @TEST_CONTRACT ListReportsApi ->
# {
# required_fields: {page: int, page_size: int, sort_by: str, sort_order: str},
# optional_fields: {task_types: str, statuses: str, search: str},
@@ -86,10 +86,10 @@ def _parse_csv_enum_list(raw: str | None, enum_cls, field_name: str) -> list:
# "Raises HTTPException 400 for invalid query parameters"
# ]
# }
# @TEST_FIXTURE: valid_list_request -> {"page": 1, "page_size": 20}
# @TEST_EDGE: invalid_task_type_filter -> raises HTTPException(400)
# @TEST_EDGE: malformed_query -> raises HTTPException(400)
# @TEST_INVARIANT: consistent_list_payload -> verifies: [valid_list_request]
# @TEST_FIXTURE valid_list_request -> {"page": 1, "page_size": 20}
# @TEST_EDGE invalid_task_type_filter -> raises HTTPException(400)
# @TEST_EDGE malformed_query -> raises HTTPException(400)
# @TEST_INVARIANT consistent_list_payload -> verifies: [valid_list_request]
@router.get("", response_model=ReportCollection)
async def list_reports(
page: int = Query(1, ge=1),
@@ -145,9 +145,9 @@ async def list_reports(
# #region get_report_detail [C:2] [TYPE Function]
# @BRIEF Return one normalized report detail with diagnostics and next actions.
# @PRE: authenticated/authorized request and existing report_id.
# @POST: returns normalized detail envelope or 404 when report is not found.
# @RELATION CALLS -> [ReportsService:Class]
# @PRE authenticated/authorized request and existing report_id.
# @POST returns normalized detail envelope or 404 when report is not found.
# @RELATION CALLS -> [ReportsService]
@router.get("/{report_id}", response_model=ReportDetailView)
async def get_report_detail(
report_id: str,

View File

@@ -1,7 +1,7 @@
# #region SettingsRouter [C:5] [TYPE Module] [SEMANTICS fastapi, api, superset, logging-config-response]
#
# @BRIEF Provides API endpoints for managing application settings and Superset environments.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [get_config_manager]
# @RELATION DEPENDS_ON -> [has_permission]
@@ -10,8 +10,8 @@
# @POST Settings are read or written via ConfigManager.
# @SIDE_EFFECT Persists config changes to disk via ConfigManager.
# @DATA_CONTRACT Input -> ConfigUpdateRequest, Output -> AppConfig, LoggingConfigResponse
# @INVARIANT: All settings changes must be persisted via ConfigManager.
# @PUBLIC_API: router
# @INVARIANT All settings changes must be persisted via ConfigManager.
# @PUBLIC_API router
from fastapi import APIRouter, Depends, HTTPException
@@ -53,8 +53,8 @@ router = APIRouter()
# #region _normalize_superset_env_url [C:1] [TYPE Function]
# @BRIEF Canonicalize Superset environment URL to base host/path without trailing /api/v1.
# Auto-prepends https:// if no scheme is present.
# @PRE: raw_url can be empty.
# @POST: Returns normalized base URL with scheme.
# @PRE raw_url can be empty.
# @POST Returns normalized base URL with scheme.
def _normalize_superset_env_url(raw_url: str) -> str:
normalized = str(raw_url or "").strip().rstrip("/")
if normalized.lower().endswith("/api/v1"):
@@ -71,8 +71,8 @@ def _normalize_superset_env_url(raw_url: str) -> str:
# #region _validate_superset_connection_fast [C:2] [TYPE Function]
# @BRIEF Run lightweight Superset connectivity validation without full pagination scan.
# @PRE: env contains valid URL and credentials.
# @POST: Raises on auth/API failures; returns None on success.
# @PRE env contains valid URL and credentials.
# @POST Raises on auth/API failures; returns None on success.
def _validate_superset_connection_fast(env: Environment) -> None:
client = SupersetClient(env)
# 1) Explicit auth check
@@ -92,8 +92,8 @@ def _validate_superset_connection_fast(env: Environment) -> None:
# #region get_settings [C:2] [TYPE Function]
# @BRIEF Retrieves all application settings.
# @PRE: Config manager is available.
# @POST: Returns masked AppConfig.
# @PRE Config manager is available.
# @POST Returns masked AppConfig.
@router.get("", response_model=AppConfig)
async def get_settings(
config_manager: ConfigManager = Depends(get_config_manager),
@@ -115,9 +115,9 @@ async def get_settings(
# #region get_features [C:1] [TYPE Function]
# @BRIEF Public endpoint returning feature flags for frontend sidebar filtering.
# @RATIONALE: Unauthenticated because sidebar filtering must work for all users, not just admins.
# @PRE: Config manager is available.
# @POST: Returns dict with dataset_review and health_monitor booleans.
# @RATIONALE Unauthenticated because sidebar filtering must work for all users, not just admins.
# @PRE Config manager is available.
# @POST Returns dict with dataset_review and health_monitor booleans.
@router.get("/features")
async def get_features(
config_manager: ConfigManager = Depends(get_config_manager),
@@ -130,8 +130,8 @@ async def get_features(
# #region update_global_settings [C:2] [TYPE Function]
# @BRIEF Updates global application settings.
# @PRE: New settings are provided.
# @POST: Global settings are updated.
# @PRE New settings are provided.
# @POST Global settings are updated.
@router.patch("/global", response_model=GlobalSettings)
async def update_global_settings(
settings: GlobalSettings,
@@ -164,7 +164,7 @@ async def get_storage_settings(
# #region update_storage_settings [C:2] [TYPE Function]
# @BRIEF Updates storage-specific settings.
# @POST: Storage settings are updated and saved.
# @POST Storage settings are updated and saved.
@router.put("/storage", response_model=StorageConfig)
async def update_storage_settings(
storage: StorageConfig,
@@ -187,8 +187,8 @@ async def update_storage_settings(
# #region get_environments [C:2] [TYPE Function]
# @BRIEF Lists all configured Superset environments.
# @PRE: Config manager is available.
# @POST: Returns list of environments.
# @PRE Config manager is available.
# @POST Returns list of environments.
@router.get("/environments", response_model=list[Environment])
async def get_environments(
config_manager: ConfigManager = Depends(get_config_manager),
@@ -208,8 +208,8 @@ async def get_environments(
# #region add_environment [C:2] [TYPE Function]
# @BRIEF Adds a new Superset environment.
# @PRE: Environment data is valid and reachable.
# @POST: Environment is added to config.
# @PRE Environment data is valid and reachable.
# @POST Environment is added to config.
@router.post("/environments", response_model=Environment)
async def add_environment(
env: Environment,
@@ -240,8 +240,8 @@ async def add_environment(
# #region update_environment [C:2] [TYPE Function]
# @BRIEF Updates an existing Superset environment.
# @PRE: ID and valid environment data are provided.
# @POST: Environment is updated in config.
# @PRE ID and valid environment data are provided.
# @POST Environment is updated in config.
@router.put("/environments/{id}", response_model=Environment)
async def update_environment(
id: str,
@@ -283,8 +283,8 @@ async def update_environment(
# #region delete_environment [C:2] [TYPE Function]
# @BRIEF Deletes a Superset environment.
# @PRE: ID is provided.
# @POST: Environment is removed from config.
# @PRE ID is provided.
# @POST Environment is removed from config.
@router.delete("/environments/{id}")
async def delete_environment(
id: str, config_manager: ConfigManager = Depends(get_config_manager)
@@ -300,8 +300,8 @@ async def delete_environment(
# #region test_environment_connection [C:2] [TYPE Function]
# @BRIEF Tests the connection to a Superset environment.
# @PRE: ID is provided.
# @POST: Returns success or error status.
# @PRE ID is provided.
# @POST Returns success or error status.
@router.post("/environments/{id}/test")
async def test_environment_connection(
id: str, config_manager: ConfigManager = Depends(get_config_manager)
@@ -333,8 +333,8 @@ async def test_environment_connection(
# #region get_logging_config [C:2] [TYPE Function]
# @BRIEF Retrieves current logging configuration.
# @PRE: Config manager is available.
# @POST: Returns logging configuration.
# @PRE Config manager is available.
# @POST Returns logging configuration.
@router.get("/logging", response_model=LoggingConfigResponse)
async def get_logging_config(
config_manager: ConfigManager = Depends(get_config_manager),
@@ -354,8 +354,8 @@ async def get_logging_config(
# #region update_logging_config [C:2] [TYPE Function]
# @BRIEF Updates logging configuration.
# @PRE: New logging config is provided.
# @POST: Logging configuration is updated and saved.
# @PRE New logging config is provided.
# @POST Logging configuration is updated and saved.
@router.patch("/logging", response_model=LoggingConfigResponse)
async def update_logging_config(
config: LoggingConfig,
@@ -401,10 +401,10 @@ class ConsolidatedSettingsResponse(BaseModel):
# #region get_consolidated_settings [C:4] [TYPE Function]
# @BRIEF Retrieves all settings categories in a single call.
# @PRE: Config manager is available and the caller holds admin settings read permission.
# @POST: Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.
# @SIDE_EFFECT: Opens one database session to read LLM providers and config-backed notification payload, then closes it.
# @DATA_CONTRACT: Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]
# @PRE Config manager is available and the caller holds admin settings read permission.
# @POST Returns consolidated settings, provider metadata, and persisted notification payload in one stable response.
# @SIDE_EFFECT Opens one database session to read LLM providers and config-backed notification payload, then closes it.
# @DATA_CONTRACT Input[ConfigManager] -> Output[ConsolidatedSettingsResponse]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [AppConfigRecord]
@@ -481,8 +481,8 @@ async def get_consolidated_settings(
# #region update_consolidated_settings [C:2] [TYPE Function]
# @BRIEF Bulk update application settings from the consolidated view.
# @PRE: User has admin permissions, config is valid.
# @POST: Settings are updated and saved via ConfigManager.
# @PRE User has admin permissions, config is valid.
# @POST Settings are updated and saved via ConfigManager.
@router.patch("/consolidated")
async def update_consolidated_settings(
settings_patch: dict,

View File

@@ -1,11 +1,11 @@
# #region storage_routes [C:5] [TYPE Module] [SEMANTICS fastapi, storage, api, upload, download]
#
# @BRIEF API endpoints for file storage management (backups and repositories).
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [StorageModels]
# @RELATION DEPENDS_ON -> [StoragePlugin]
#
# @INVARIANT: All paths must be validated against path traversal.
# @INVARIANT All paths must be validated against path traversal.
from pathlib import Path
@@ -22,8 +22,8 @@ router = APIRouter(tags=["storage"])
# #region list_files [C:3] [TYPE Function]
# @BRIEF List all files and directories in the storage system.
#
# @PRE: None.
# @POST: Returns a list of StoredFile objects.
# @PRE None.
# @POST Returns a list of StoredFile objects.
#
# @RELATION DEPENDS_ON -> [StoragePlugin]
@router.get("/files", response_model=list[StoredFile])
@@ -44,12 +44,12 @@ async def list_files(
# #region upload_file [C:3] [TYPE Function]
# @BRIEF Upload a file to the storage system.
#
# @PRE: category must be a valid FileCategory.
# @PRE: file must be a valid UploadFile.
# @POST: Returns the StoredFile object of the uploaded file.
# @PRE category must be a valid FileCategory.
# @PRE file must be a valid UploadFile.
# @POST Returns the StoredFile object of the uploaded file.
#
#
# @SIDE_EFFECT: Writes file to the filesystem.
# @SIDE_EFFECT Writes file to the filesystem.
#
# @RELATION DEPENDS_ON -> [StoragePlugin]
@router.post("/upload", response_model=StoredFile, status_code=201)
@@ -73,11 +73,11 @@ async def upload_file(
# #region delete_file [C:3] [TYPE Function]
# @BRIEF Delete a specific file or directory.
#
# @PRE: category must be a valid FileCategory.
# @POST: Item is removed from storage.
# @PRE category must be a valid FileCategory.
# @POST Item is removed from storage.
#
#
# @SIDE_EFFECT: Deletes item from the filesystem.
# @SIDE_EFFECT Deletes item from the filesystem.
#
# @RELATION DEPENDS_ON -> [StoragePlugin]
@router.delete("/files/{category}/{path:path}", status_code=204)
@@ -102,8 +102,8 @@ async def delete_file(
# #region download_file [C:3] [TYPE Function]
# @BRIEF Retrieve a file for download.
#
# @PRE: category must be a valid FileCategory.
# @POST: Returns a FileResponse.
# @PRE category must be a valid FileCategory.
# @POST Returns a FileResponse.
#
#
# @RELATION DEPENDS_ON -> [StoragePlugin]
@@ -131,8 +131,8 @@ async def download_file(
# #region get_file_by_path [C:3] [TYPE Function]
# @BRIEF Retrieve a file by validated absolute/relative path under storage root.
#
# @PRE: path must resolve under configured storage root.
# @POST: Returns a FileResponse for existing files.
# @PRE path must resolve under configured storage root.
# @POST Returns a FileResponse for existing files.
#
#
# @RELATION DEPENDS_ON -> [StoragePlugin]

View File

@@ -1,6 +1,6 @@
# #region TasksRouter [C:3] [TYPE Module] [SEMANTICS fastapi, task, api, search, create-task-request, resolve-task-request]
# @BRIEF Defines the FastAPI router for task-related endpoints, allowing clients to create, list, and get the status of tasks.
# @LAYER: API
# @LAYER API
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [LLMProviderService]
@@ -49,8 +49,8 @@ class ResumeTaskRequest(BaseModel):
# #region create_task [C:3] [TYPE Function]
# @BRIEF Create and start a new task for a given plugin.
# @PRE: plugin_id must exist and params must be valid for that plugin.
# @POST: A new task is created and started.
# @PRE plugin_id must exist and params must be valid for that plugin.
# @POST A new task is created and started.
# @RELATION CALLS -> [TaskManager]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [LLMProviderService]
@@ -125,10 +125,10 @@ async def create_task(
# #region list_tasks [C:2] [TYPE Function]
# @BRIEF Retrieve a list of tasks with pagination and optional status filter.
# @PRE: task_manager must be available.
# @POST: Returns a list of tasks.
# @PRE task_manager must be available.
# @POST Returns a list of tasks.
# @RELATION CALLS -> [TaskManager]
# @RELATION BINDS_TO -> [TASK_TYPE_PLUGIN_MAP]
# @RELATION BINDS_TO -> [EXT:method:TASK_TYPE_PLUGIN_MAP]
@router.get("", response_model=list[Task])
async def list_tasks(
limit: int = 10,
@@ -170,8 +170,8 @@ async def list_tasks(
# #region get_task [C:2] [TYPE Function]
# @BRIEF Retrieve the details of a specific task.
# @PRE: task_id must exist.
# @POST: Returns task details or raises 404.
# @PRE task_id must exist.
# @POST Returns task details or raises 404.
# @RELATION CALLS -> [TaskManager]
@router.get("/{task_id}", response_model=Task)
async def get_task(
@@ -193,17 +193,17 @@ async def get_task(
# #region get_task_logs [C:5] [TYPE Function]
# @BRIEF Retrieve logs for a specific task with optional filtering.
# @PRE: task_id must exist.
# @POST: Returns a list of log entries or raises 404.
# @PRE task_id must exist.
# @POST Returns a list of log entries or raises 404.
# @RELATION CALLS -> [TaskManager]
# @RELATION DEPENDS_ON -> [LogFilter]
# @TEST_CONTRACT: TaskLogQueryInput -> List[LogEntry]
# @TEST_SCENARIO: existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.
# @TEST_FIXTURE: valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json
# @TEST_EDGE: missing_task -> Unknown task_id returns 404 Task not found.
# @TEST_EDGE: invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.
# @TEST_EDGE: pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.
# @TEST_INVARIANT: logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]
# @TEST_CONTRACT TaskLogQueryInput -> List[LogEntry]
# @TEST_SCENARIO existing_task_logs_filtered -> Returns filtered logs by level/source/search with pagination.
# @TEST_FIXTURE valid_task_with_mixed_logs -> backend/tests/fixtures/task_logs/valid_task_with_mixed_logs.json
# @TEST_EDGE missing_task -> Unknown task_id returns 404 Task not found.
# @TEST_EDGE invalid_level_type -> Non-string/invalid level query rejected by validation or yields empty result.
# @TEST_EDGE pagination_bounds -> offset=0 and limit=1000 remain within API bounds and do not overflow.
# @TEST_INVARIANT logs_only_for_existing_task -> VERIFIED_BY: [existing_task_logs_filtered, missing_task]
@router.get("/{task_id}/logs")
async def get_task_logs(
task_id: str,
@@ -240,8 +240,8 @@ async def get_task_logs(
# #region get_task_log_stats [C:2] [TYPE Function]
# @BRIEF Get statistics about logs for a task (counts by level and source).
# @PRE: task_id must exist.
# @POST: Returns log statistics or raises 404.
# @PRE task_id must exist.
# @POST Returns log statistics or raises 404.
# @RELATION CALLS -> [TaskManager]
# @RELATION DEPENDS_ON -> [LogStats]
@router.get("/{task_id}/logs/stats", response_model=LogStats)
@@ -285,8 +285,8 @@ async def get_task_log_stats(
# #region get_task_log_sources [C:2] [TYPE Function]
# @BRIEF Get unique sources for a task's logs.
# @PRE: task_id must exist.
# @POST: Returns list of unique source names or raises 404.
# @PRE task_id must exist.
# @POST Returns list of unique source names or raises 404.
# @RELATION CALLS -> [TaskManager]
@router.get("/{task_id}/logs/sources", response_model=list[str])
async def get_task_log_sources(
@@ -307,8 +307,8 @@ async def get_task_log_sources(
# #region resolve_task [C:2] [TYPE Function]
# @BRIEF Resolve a task that is awaiting mapping.
# @PRE: task must be in AWAITING_MAPPING status.
# @POST: Task is resolved and resumes execution.
# @PRE task must be in AWAITING_MAPPING status.
# @POST Task is resolved and resumes execution.
# @RELATION CALLS -> [TaskManager]
@router.post("/{task_id}/resolve", response_model=Task)
async def resolve_task(
@@ -330,8 +330,8 @@ async def resolve_task(
# #region resume_task [C:2] [TYPE Function]
# @BRIEF Resume a task that is awaiting input (e.g., passwords).
# @PRE: task must be in AWAITING_INPUT status.
# @POST: Task resumes execution with provided input.
# @PRE task must be in AWAITING_INPUT status.
# @POST Task resumes execution with provided input.
# @RELATION CALLS -> [TaskManager]
@router.post("/{task_id}/resume", response_model=Task)
async def resume_task(
@@ -353,8 +353,8 @@ async def resume_task(
# #region clear_tasks [C:2] [TYPE Function]
# @BRIEF Clear tasks matching the status filter.
# @PRE: task_manager is available.
# @POST: Tasks are removed from memory/persistence.
# @PRE task_manager is available.
# @POST Tasks are removed from memory/persistence.
# @RELATION CALLS -> [TaskManager]
@router.delete("", status_code=status.HTTP_204_NO_CONTENT)
async def clear_tasks(

View File

@@ -1,6 +1,6 @@
# #region TranslateCorrectionRoutesModule [C:2] [TYPE Module] [SEMANTICS fastapi, translate, api, search]
# @BRIEF Term correction submission endpoints.
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException, status
from sqlalchemy.orm import Session

View File

@@ -1,6 +1,6 @@
# #region TranslateDictionaryRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search]
# @BRIEF Terminology Dictionary CRUD, entries management, and import routes.
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
@@ -24,8 +24,8 @@ from ._router import router
# #region list_dictionaries [C:4] [TYPE Function]
# @BRIEF List all terminology dictionaries.
# @PRE: User has translate.dictionary.view permission.
# @POST: Returns list of dictionaries with total count.
# @PRE User has translate.dictionary.view permission.
# @POST Returns list of dictionaries with total count.
@router.get("/dictionaries")
async def list_dictionaries(
page: int = Query(1, ge=1),
@@ -48,8 +48,8 @@ async def list_dictionaries(
# #region get_dictionary [C:4] [TYPE Function]
# @BRIEF Get a single terminology dictionary by ID.
# @PRE: User has translate.dictionary.view permission.
# @POST: Returns the dictionary with entry_count.
# @PRE User has translate.dictionary.view permission.
# @POST Returns the dictionary with entry_count.
@router.get("/dictionaries/{dictionary_id}")
async def get_dictionary(
dictionary_id: str,
@@ -73,8 +73,8 @@ async def get_dictionary(
# #region create_dictionary [C:4] [TYPE Function]
# @BRIEF Create a new terminology dictionary.
# @PRE: User has translate.dictionary.create permission.
# @POST: Returns the created dictionary.
# @PRE User has translate.dictionary.create permission.
# @POST Returns the created dictionary.
@router.post("/dictionaries", status_code=status.HTTP_201_CREATED)
async def create_dictionary(
payload: DictionaryCreate,
@@ -101,8 +101,8 @@ async def create_dictionary(
# #region update_dictionary [C:4] [TYPE Function]
# @BRIEF Update an existing terminology dictionary.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Returns the updated dictionary.
# @PRE User has translate.dictionary.edit permission.
# @POST Returns the updated dictionary.
@router.put("/dictionaries/{dictionary_id}")
async def update_dictionary(
dictionary_id: str,
@@ -135,8 +135,8 @@ async def update_dictionary(
# #region delete_dictionary [C:4] [TYPE Function]
# @BRIEF Delete a terminology dictionary, blocked if attached to active/scheduled jobs.
# @PRE: User has translate.dictionary.delete permission.
# @POST: Dictionary is deleted.
# @PRE User has translate.dictionary.delete permission.
# @POST Dictionary is deleted.
@router.delete("/dictionaries/{dictionary_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dictionary(
dictionary_id: str,
@@ -164,8 +164,8 @@ async def delete_dictionary(
# #region list_dictionary_entries [C:4] [TYPE Function]
# @BRIEF List entries for a dictionary, optionally filtered by language pair.
# @PRE: User has translate.dictionary.view permission.
# @POST: Returns paginated list of entries with language pair fields.
# @PRE User has translate.dictionary.view permission.
# @POST Returns paginated list of entries with language pair fields.
@router.get("/dictionaries/{dictionary_id}/entries")
async def list_dictionary_entries(
dictionary_id: str,
@@ -222,8 +222,8 @@ async def list_dictionary_entries(
# #region add_dictionary_entry [C:4] [TYPE Function]
# @BRIEF Add a new entry to a dictionary.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Entry is created.
# @PRE User has translate.dictionary.edit permission.
# @POST Entry is created.
@router.post("/dictionaries/{dictionary_id}/entries", status_code=status.HTTP_201_CREATED)
async def add_dictionary_entry(
dictionary_id: str,
@@ -271,8 +271,8 @@ async def add_dictionary_entry(
# #region edit_dictionary_entry [C:4] [TYPE Function]
# @BRIEF Update an existing dictionary entry.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Entry is updated.
# @PRE User has translate.dictionary.edit permission.
# @POST Entry is updated.
@router.put("/dictionaries/{dictionary_id}/entries/{entry_id}")
async def edit_dictionary_entry(
dictionary_id: str,
@@ -321,8 +321,8 @@ async def edit_dictionary_entry(
# #region delete_dictionary_entry [C:4] [TYPE Function]
# @BRIEF Delete a dictionary entry.
# @PRE: User has translate.dictionary.edit permission.
# @POST: Entry is deleted.
# @PRE User has translate.dictionary.edit permission.
# @POST Entry is deleted.
@router.delete("/dictionaries/{dictionary_id}/entries/{entry_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_dictionary_entry(
dictionary_id: str,

View File

@@ -1,7 +1,7 @@
# #region TranslateHelpersModule [C:2] [TYPE Module] [SEMANTICS sqlalchemy, translate, helper, query, mapping]
# @BRIEF Shared helper functions for translate route handlers.
# @LAYER API
# @RELATION DEPENDS_ON -> [models.translate]
# @RELATION DEPENDS_ON -> [EXT:frontend:models.translate]
from typing import Any

View File

@@ -1,6 +1,6 @@
# #region TranslateJobRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search]
# @BRIEF Translation Job CRUD and datasource column routes.
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException, Query, status
@@ -34,8 +34,8 @@ from ._router import router
# #region list_jobs [C:4] [TYPE Function]
# @BRIEF List all translation jobs.
# @PRE: User has translate.job.view permission.
# @POST: Returns list of translation jobs.
# @PRE User has translate.job.view permission.
# @POST Returns list of translation jobs.
@router.get("/jobs", response_model=list[TranslateJobResponse])
async def list_jobs(
page: int = Query(1, ge=1),
@@ -67,8 +67,8 @@ async def list_jobs(
# #region get_job [C:4] [TYPE Function]
# @BRIEF Get a single translation job by ID.
# @PRE: User has translate.job.view permission.
# @POST: Returns the translation job.
# @PRE User has translate.job.view permission.
# @POST Returns the translation job.
@router.get("/jobs/{job_id}", response_model=TranslateJobResponse)
async def get_job(
job_id: str,
@@ -91,9 +91,9 @@ async def get_job(
# #region create_job [C:4] [TYPE Function]
# @BRIEF Create a new translation job.
# @PRE: User has translate.job.create permission.
# @POST: Returns the created translation job.
# @SIDE_EFFECT: Validates columns via SupersetClient; caches database_dialect.
# @PRE User has translate.job.create permission.
# @POST Returns the created translation job.
# @SIDE_EFFECT Validates columns via SupersetClient; caches database_dialect.
@router.post("/jobs", response_model=TranslateJobResponse, status_code=status.HTTP_201_CREATED)
async def create_job(
payload: TranslateJobCreate,
@@ -116,9 +116,9 @@ async def create_job(
# #region update_job [C:4] [TYPE Function]
# @BRIEF Update an existing translation job.
# @PRE: User has translate.job.edit permission.
# @POST: Returns the updated translation job.
# @SIDE_EFFECT: Re-detects database_dialect if datasource changed.
# @PRE User has translate.job.edit permission.
# @POST Returns the updated translation job.
# @SIDE_EFFECT Re-detects database_dialect if datasource changed.
@router.put("/jobs/{job_id}", response_model=TranslateJobResponse)
async def update_job(
job_id: str,
@@ -142,8 +142,8 @@ async def update_job(
# #region delete_job [C:4] [TYPE Function]
# @BRIEF Delete a translation job.
# @PRE: User has translate.job.delete permission.
# @POST: Job is deleted.
# @PRE User has translate.job.delete permission.
# @POST Job is deleted.
@router.delete("/jobs/{job_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_job(
job_id: str,
@@ -164,8 +164,8 @@ async def delete_job(
# #region duplicate_job [C:4] [TYPE Function]
# @BRIEF Duplicate a translation job.
# @PRE: User has translate.job.create permission.
# @POST: Returns the duplicated job with status DRAFT.
# @PRE User has translate.job.create permission.
# @POST Returns the duplicated job with status DRAFT.
@router.post("/jobs/{job_id}/duplicate", response_model=DuplicateJobResponse, status_code=status.HTTP_201_CREATED)
async def duplicate_job(
job_id: str,

View File

@@ -1,6 +1,6 @@
# #region TranslateMetricsRoutesModule [C:2] [TYPE Module] [SEMANTICS fastapi, translate, api, search]
# @BRIEF Translation Metrics endpoints.
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException, Query, status
@@ -19,8 +19,8 @@ from ._router import router
# #region get_metrics [C:4] [TYPE Function]
# @BRIEF Get translation metrics, optionally filtered by job.
# @PRE: User has translate.metrics.view permission.
# @POST: Returns metrics data.
# @PRE User has translate.metrics.view permission.
# @POST Returns metrics data.
@router.get("/metrics")
async def get_metrics(
job_id: str | None = Query(None),

View File

@@ -1,6 +1,6 @@
# #region TranslatePreviewRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, preview, review, api, search]
# @BRIEF Translation Preview session management routes.
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException, status
@@ -25,9 +25,9 @@ from ._router import router
# #region preview_translation [C:4] [TYPE Function]
# @BRIEF Preview a translation before applying it.
# @PRE: User has translate.job.execute permission.
# @POST: Returns a preview session with records and cost estimation.
# @SIDE_EFFECT: Fetches sample data from Superset; calls LLM provider; creates DB rows.
# @PRE User has translate.job.execute permission.
# @POST Returns a preview session with records and cost estimation.
# @SIDE_EFFECT Fetches sample data from Superset; calls LLM provider; creates DB rows.
@router.post("/jobs/{job_id}/preview", status_code=status.HTTP_201_CREATED)
async def preview_translation(
job_id: str,
@@ -61,8 +61,8 @@ async def preview_translation(
# #region update_preview_row [C:4] [TYPE Function]
# @BRIEF Approve, edit, or reject a preview row (optionally per language).
# @PRE: User has translate.job.execute permission.
# @POST: Preview row status is updated.
# @PRE User has translate.job.execute permission.
# @POST Preview row status is updated.
@router.put("/jobs/{job_id}/preview/rows/{row_key}")
async def update_preview_row(
job_id: str,
@@ -93,8 +93,8 @@ async def update_preview_row(
# #region accept_preview_session [C:4] [TYPE Function]
# @BRIEF Accept a preview session, marking it as the quality gate for full execution.
# @PRE: User has translate.job.execute permission. Job has an ACTIVE preview session.
# @POST: Preview session is marked as APPLIED; full execution can proceed.
# @PRE User has translate.job.execute permission. Job has an ACTIVE preview session.
# @POST Preview session is marked as APPLIED; full execution can proceed.
@router.post("/jobs/{job_id}/preview/accept")
async def accept_preview_session(
job_id: str,
@@ -116,8 +116,8 @@ async def accept_preview_session(
# #region apply_preview [C:4] [TYPE Function]
# @BRIEF Apply a preview session (alias for accept when accepting at session level).
# @PRE: User has translate.job.execute permission.
# @POST: Preview is applied.
# @PRE User has translate.job.execute permission.
# @POST Preview is applied.
@router.post("/preview/{session_id}/apply")
async def apply_preview(
session_id: str,

View File

@@ -1,6 +1,6 @@
# #region TranslateRouterModule [C:1] [TYPE Module] [SEMANTICS fastapi, translate, api]
# @BRIEF APIRouter instance for translate routes.
# @LAYER: API
# @LAYER API
from fastapi import APIRouter

View File

@@ -1,6 +1,6 @@
# #region TranslateRunListRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, download, search]
# @BRIEF Translation Run listing, detail, and CSV download routes (cross-job).
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException, Query, status
@@ -21,8 +21,8 @@ from ._router import router
# #region list_runs [C:4] [TYPE Function]
# @BRIEF List all runs with cross-job filtering and pagination.
# @PRE: User has translate.history.view permission.
# @POST: Returns paginated list of runs.
# @PRE User has translate.history.view permission.
# @POST Returns paginated list of runs.
@router.get("/runs")
async def list_runs(
job_id: str | None = Query(None),
@@ -128,8 +128,8 @@ async def list_runs(
# #region get_run_detail [C:4] [TYPE Function]
# @BRIEF Get detailed run info with config_snapshot, records, events.
# @PRE: User has translate.history.view permission.
# @POST: Returns run detail with records and events.
# @PRE User has translate.history.view permission.
# @POST Returns run detail with records and events.
@router.get("/runs/{run_id}/detail")
async def get_run_detail(
run_id: str,

View File

@@ -1,6 +1,6 @@
# #region TranslateRunRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, search, execution, history]
# @BRIEF Translation Run execution, history, status, records and batches routes.
# @LAYER: API
# @LAYER API
from datetime import UTC, datetime
@@ -27,8 +27,8 @@ from ._router import router
# #region run_translation [C:4] [TYPE Function]
# @BRIEF Execute a translation job (trigger a run).
# @PRE: User has translate.job.execute permission.
# @POST: Returns the created translation run.
# @PRE User has translate.job.execute permission.
# @POST Returns the created translation run.
@router.post("/jobs/{job_id}/run", status_code=status.HTTP_201_CREATED)
def run_translation(
job_id: str,
@@ -166,8 +166,8 @@ def run_translation(
# #region retry_run [C:4] [TYPE Function]
# @BRIEF Retry failed batches in a translation run.
# @PRE: User has translate.job.execute permission.
# @POST: Returns the updated translation run.
# @PRE User has translate.job.execute permission.
# @POST Returns the updated translation run.
@router.post("/runs/{run_id}/retry")
def retry_run(
run_id: str,
@@ -192,8 +192,8 @@ def retry_run(
# #region retry_insert [C:4] [TYPE Function]
# @BRIEF Retry the SQL insert phase for a completed run.
# @PRE: User has translate.job.execute permission.
# @POST: Returns the updated run.
# @PRE User has translate.job.execute permission.
# @POST Returns the updated run.
@router.post("/runs/{run_id}/retry-insert")
def retry_insert(
run_id: str,
@@ -218,8 +218,8 @@ def retry_insert(
# #region cancel_run [C:4] [TYPE Function]
# @BRIEF Cancel a running translation.
# @PRE: User has translate.job.execute permission.
# @POST: Run is cancelled.
# @PRE User has translate.job.execute permission.
# @POST Run is cancelled.
@router.post("/runs/{run_id}/cancel")
def cancel_run(
run_id: str,
@@ -241,8 +241,8 @@ def cancel_run(
# #region get_run_history [C:4] [TYPE Function]
# @BRIEF Get run history for a translation job.
# @PRE: User has translate.history.view permission.
# @POST: Returns list of runs.
# @PRE User has translate.history.view permission.
# @POST Returns list of runs.
@router.get("/jobs/{job_id}/runs")
def get_run_history(
job_id: str,
@@ -266,8 +266,8 @@ def get_run_history(
# #region get_run_status [C:4] [TYPE Function]
# @BRIEF Get status and statistics for a translation run.
# @PRE: User has translate.history.view permission.
# @POST: Returns run details with statistics.
# @PRE User has translate.history.view permission.
# @POST Returns run details with statistics.
@router.get("/runs/{run_id}")
def get_run_status(
run_id: str,
@@ -288,8 +288,8 @@ def get_run_status(
# #region get_run_records [C:4] [TYPE Function]
# @BRIEF Get paginated records for a translation run.
# @PRE: User has translate.history.view permission.
# @POST: Returns paginated records.
# @PRE User has translate.history.view permission.
# @POST Returns paginated records.
@router.get("/runs/{run_id}/records")
def get_run_records(
run_id: str,
@@ -317,8 +317,8 @@ def get_run_records(
# #region get_batches [C:4] [TYPE Function]
# @BRIEF Get batches for a translation run.
# @PRE: User has translate.job.view permission.
# @POST: Returns list of batches.
# @PRE User has translate.job.view permission.
# @POST Returns list of batches.
@router.get("/runs/{run_id}/batches")
def get_batches(
run_id: str,
@@ -362,9 +362,9 @@ def get_batches(
# #region override_detected_language [C:4] [TYPE Function]
# @BRIEF Manually override the detected source language for a specific translation language entry.
# @PRE: User has translate.job.execute permission. Run, record, and language entry exist.
# @POST: TranslationLanguage.source_language_detected is updated; language_overridden is set to True.
# @SIDE_EFFECT: DB write.
# @PRE User has translate.job.execute permission. Run, record, and language entry exist.
# @POST TranslationLanguage.source_language_detected is updated; language_overridden is set to True.
# @SIDE_EFFECT DB write.
@router.put("/runs/{run_id}/records/{record_id}/languages/{language_code}/override-language")
def override_detected_language(
run_id: str,
@@ -441,8 +441,8 @@ def override_detected_language(
# #region inline_edit_translation [C:4] [TYPE Function] [SEMANTICS api,translate,correction]
# @BRIEF Apply an inline correction to a translated value on a completed run result.
# @PRE: User has translate.job.execute permission. Run, record, and language entry exist.
# @POST: TranslationLanguage.final_value and user_edit are updated. Optional dictionary submission.
# @PRE User has translate.job.execute permission. Run, record, and language entry exist.
# @POST TranslationLanguage.final_value and user_edit are updated. Optional dictionary submission.
@router.put("/runs/{run_id}/records/{record_id}/languages/{language_code}")
def inline_edit_translation(
run_id: str,

View File

@@ -1,6 +1,6 @@
# #region TranslateScheduleRoutesModule [C:3] [TYPE Module] [SEMANTICS fastapi, translate, api, schedule, search]
# @BRIEF Translation Schedule management routes.
# @LAYER: API
# @LAYER API
from fastapi import Depends, HTTPException, Query, status
from sqlalchemy.orm import Session
@@ -20,8 +20,8 @@ from ._router import router
# #region get_schedule [C:4] [TYPE Function]
# @BRIEF Get the schedule for a translation job.
# @PRE: User has translate.schedule.view permission.
# @POST: Returns the schedule configuration.
# @PRE User has translate.schedule.view permission.
# @POST Returns the schedule configuration.
@router.get("/jobs/{job_id}/schedule")
async def get_schedule(
job_id: str,
@@ -55,8 +55,8 @@ async def get_schedule(
# #region set_schedule [C:4] [TYPE Function]
# @BRIEF Set or update the schedule for a translation job.
# @PRE: User has translate.schedule.manage permission.
# @POST: Schedule is created or updated.
# @PRE User has translate.schedule.manage permission.
# @POST Schedule is created or updated.
@router.put("/jobs/{job_id}/schedule")
async def set_schedule(
job_id: str,
@@ -121,8 +121,8 @@ async def set_schedule(
# #region enable_schedule [C:4] [TYPE Function]
# @BRIEF Enable a schedule for a translation job.
# @PRE: User has translate.schedule.manage permission.
# @POST: Schedule is enabled.
# @PRE User has translate.schedule.manage permission.
# @POST Schedule is enabled.
@router.post("/jobs/{job_id}/schedule/enable")
async def enable_schedule(
job_id: str,
@@ -153,8 +153,8 @@ async def enable_schedule(
# #region disable_schedule [C:4] [TYPE Function]
# @BRIEF Disable a schedule for a translation job.
# @PRE: User has translate.schedule.manage permission.
# @POST: Schedule is disabled.
# @PRE User has translate.schedule.manage permission.
# @POST Schedule is disabled.
@router.post("/jobs/{job_id}/schedule/disable")
async def disable_schedule(
job_id: str,
@@ -179,8 +179,8 @@ async def disable_schedule(
# #region delete_schedule [C:4] [TYPE Function]
# @BRIEF Delete the schedule for a translation job.
# @PRE: User has translate.schedule.manage permission.
# @POST: Schedule is removed.
# @PRE User has translate.schedule.manage permission.
# @POST Schedule is removed.
@router.delete("/jobs/{job_id}/schedule", status_code=status.HTTP_204_NO_CONTENT)
async def delete_schedule(
job_id: str,

View File

@@ -1,25 +1,25 @@
# #region ValidationApiTests [C:3] [TYPE Module] [SEMANTICS validation, api, tests, pagination, crud]
# @BRIEF Unit tests for validation task CRUD and run history API endpoints.
# @LAYER: API
# @LAYER API
# @RELATION BINDS_TO -> [ValidationRoutes]
# @RELATION DEPENDS_ON -> [EXT:FastAPI:TestClient]
# @TEST_CONTRACT: [ValidationTaskCreate|Update|RunRequest] -> [ValidationTaskResponse|RunResponse|TriggerRunResponse]
# @TEST_SCENARIO: list_tasks -> 200 with paginated tasks; filter by is_active/environment_id
# @TEST_SCENARIO: create_task -> 201 with valid payload; 422 with invalid provider/environment
# @TEST_SCENARIO: get_task -> 200 with task+recent_runs; 404 for nonexistent
# @TEST_SCENARIO: update_task -> 200 with updated fields; 404 for nonexistent
# @TEST_SCENARIO: delete_task -> 204 on success; 404 for nonexistent
# @TEST_SCENARIO: trigger_run -> 200 with spawned_task_id; 422 for invalid task
# @TEST_SCENARIO: list_runs -> 200 with 6 filters + pagination
# @TEST_SCENARIO: get_run_detail -> 200 with issues/raw_response; 404 for nonexistent
# @TEST_SCENARIO: delete_run -> 204 on success; 404 for nonexistent
# @TEST_EDGE: missing_field -> 422 when required field omitted from create payload
# @TEST_EDGE: invalid_type -> 422 when provider_id is not multimodal
# @TEST_EDGE: external_fail -> 404/422 when task/run does not exist
# @TEST_EDGE: delete_task_with_runs -> 204 with delete_runs=true flag
# @INVARIANT: Every endpoint requires authentication via has_permission
# @INVARIANT: All list endpoints support pagination (page/page_size) with defaults
# @INVARIANT: provider_id must reference a multimodal LLM provider for creation
# @TEST_CONTRACT [ValidationTaskCreate|Update|RunRequest] -> [ValidationTaskResponse|RunResponse|TriggerRunResponse]
# @TEST_SCENARIO list_tasks -> 200 with paginated tasks; filter by is_active/environment_id
# @TEST_SCENARIO create_task -> 201 with valid payload; 422 with invalid provider/environment
# @TEST_SCENARIO get_task -> 200 with task+recent_runs; 404 for nonexistent
# @TEST_SCENARIO update_task -> 200 with updated fields; 404 for nonexistent
# @TEST_SCENARIO delete_task -> 204 on success; 404 for nonexistent
# @TEST_SCENARIO trigger_run -> 200 with spawned_task_id; 422 for invalid task
# @TEST_SCENARIO list_runs -> 200 with 6 filters + pagination
# @TEST_SCENARIO get_run_detail -> 200 with issues/raw_response; 404 for nonexistent
# @TEST_SCENARIO delete_run -> 204 on success; 404 for nonexistent
# @TEST_EDGE missing_field -> 422 when required field omitted from create payload
# @TEST_EDGE invalid_type -> 422 when provider_id is not multimodal
# @TEST_EDGE external_fail -> 404/422 when task/run does not exist
# @TEST_EDGE delete_task_with_runs -> 204 with delete_runs=true flag
# @INVARIANT Every endpoint requires authentication via has_permission
# @INVARIANT All list endpoints support pagination (page/page_size) with defaults
# @INVARIANT provider_id must reference a multimodal LLM provider for creation
# Set required env vars before ANY app imports — crash-early guard for AuthConfig()
import os

View File

@@ -69,7 +69,7 @@ app = FastAPI(
# #endregion FastAPI_App
# #region ensure_initial_admin_user [C:3] [TYPE Function]
# @BRIEF Ensures initial admin user exists when bootstrap env flags are enabled.
# @RELATION DEPENDS_ON -> AuthRepository
# @RELATION DEPENDS_ON -> [AuthRepository]
def ensure_initial_admin_user() -> None:
raw_flag = os.getenv("INITIAL_ADMIN_CREATE", "false").strip().lower()
if raw_flag not in {"1", "true", "yes", "on"}:

View File

@@ -1,7 +1,7 @@
# #region TestConfigManagerCompat [TYPE Module] [C:3] [SEMANTICS config-manager, compatibility, payload, tests]
# @BRIEF Verifies ConfigManager compatibility wrappers preserve legacy payload sections.
# @LAYER: Domain
# @RELATION VERIFIES -> ConfigManager
# @LAYER Domain
# @RELATION BINDS_TO -> ConfigManager
from types import SimpleNamespace
from src.core.config_manager import ConfigManager
@@ -58,17 +58,17 @@ def test_save_config_syncs_environment_records_for_fk_backed_flows():
credentials_id="legacy-user",
)
# #region _FakeQuery [TYPE Class] [C:1]
# @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]
# @RELATION BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]
# @PURPOSE: Minimal query stub returning hardcoded existing environment record list for sync tests.
# @INVARIANT: all() always returns [existing_record]; no parameterization or filtering.
# @INVARIANT all() always returns [existing_record]; no parameterization or filtering.
class _FakeQuery:
def all(self):
return [existing_record]
# #endregion _FakeQuery
# #region _FakeSession [TYPE Class] [C:1]
# @RELATION: BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]
# @RELATION BINDS_TO -> [test_save_config_syncs_environment_records_for_fk_backed_flows]
# @PURPOSE: Minimal SQLAlchemy session stub that captures add/delete calls for environment sync assertions.
# @INVARIANT: query() always returns _FakeQuery; no real DB interaction.
# @INVARIANT query() always returns _FakeQuery; no real DB interaction.
class _FakeSession:
def query(self, model):
return _FakeQuery()
@@ -117,14 +117,14 @@ def test_save_config_syncs_deletions_to_persistence():
credentials_id="legacy-user",
)
# #region _FakeQueryDel [TYPE Class] [C:1]
# @RELATION: BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]
# @RELATION BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]
# @PURPOSE: Minimal query stub for deletion test.
class _FakeQuery:
def all(self):
return [existing_record]
# #endregion _FakeQueryDel
# #region _FakeSessionDel [TYPE Class] [C:1]
# @RELATION: BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]
# @RELATION BINDS_TO -> [test_save_config_syncs_deletions_to_persistence]
# @PURPOSE: Minimal session stub that captures add/delete for deletion assertions.
class _FakeSession:
def query(self, model):
@@ -170,9 +170,9 @@ def test_load_config_syncs_environment_records_from_existing_db_payload(monkeypa
closed = {"value": False}
committed = {"value": False}
# #region _FakeSession [TYPE Class] [C:1]
# @RELATION: BINDS_TO -> [test_load_config_syncs_environment_records_from_existing_db_payload]
# @RELATION BINDS_TO -> [test_load_config_syncs_environment_records_from_existing_db_payload]
# @PURPOSE: Minimal session stub tracking commit/close signals for config load lifecycle assertions.
# @INVARIANT: No query or add semantics; only lifecycle signal tracking.
# @INVARIANT No query or add semantics; only lifecycle signal tracking.
class _FakeSession:
def commit(self):
committed["value"] = True

View File

@@ -1,9 +1,9 @@
# #region NativeFilterExtractionTests [TYPE Module] [C:3] [SEMANTICS tests, superset, native, filters, permalink, filter_state]
# @BRIEF Verify native filter extraction from permalinks and native_filters_key URLs.
# @LAYER: Domain
# @RELATION [BINDS_TO] ->[SupersetClient]
# @RELATION [BINDS_TO] ->[AsyncSupersetClient]
# @RELATION [BINDS_TO] ->[FilterState, ParsedNativeFilters, ExtraFormDataMerge]
# @LAYER Domain
# @RELATION BINDS_TO ->[SupersetClient]
# @RELATION BINDS_TO ->[AsyncSupersetClient]
# @RELATION BINDS_TO ->[[EXT:list:FilterState_ParsedNativeFilters_ExtraFormDataMerge]]
import json
from unittest.mock import MagicMock

View File

@@ -1,7 +1,7 @@
# #region SupersetPreviewPipelineTests [TYPE Module] [C:3] [SEMANTICS tests, superset, preview, chart_data, network, 404-mapping]
# @BRIEF Verify explicit chart-data preview compilation and ensure non-dashboard 404 errors remain generic across sync and async clients.
# @LAYER: Domain
# @RELATION [BINDS_TO] ->[AsyncNetworkModule]
# @LAYER Domain
# @RELATION BINDS_TO -> [AsyncNetworkModule]
import json
import pytest
from unittest.mock import MagicMock

View File

@@ -1,7 +1,7 @@
# #region TestSupersetProfileLookup [TYPE Module] [C:3] [SEMANTICS tests, superset, profile, lookup, fallback, sorting]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Verifies Superset profile lookup adapter payload normalization and fallback error precedence.
# @LAYER: Domain
# @LAYER Domain
# [SECTION: IMPORTS]
import json
from pathlib import Path
@@ -20,20 +20,20 @@ from src.core.utils.network import AuthenticationError, SupersetAPIError
# #region _RecordingNetworkClient [TYPE Class] [C:2]
# @RELATION BINDS_TO -> TestSupersetProfileLookup
# @BRIEF Records request payloads and returns scripted responses for deterministic adapter tests.
# @INVARIANT: Each request consumes one scripted response in call order and persists call metadata.
# @INVARIANT Each request consumes one scripted response in call order and persists call metadata.
class _RecordingNetworkClient:
# #region __init__ [TYPE Function]
# @PURPOSE: Initializes scripted network responses.
# @PRE: scripted_responses is ordered per expected request sequence.
# @POST: Instance stores response script and captures subsequent request calls.
# @PRE scripted_responses is ordered per expected request sequence.
# @POST Instance stores response script and captures subsequent request calls.
def __init__(self, scripted_responses: list[Any]):
self._scripted_responses = scripted_responses
self.calls: list[dict[str, Any]] = []
# #endregion __init__
# #region request [TYPE Function]
# @PURPOSE: Mimics APIClient.request while capturing call arguments.
# @PRE: method and endpoint are provided.
# @POST: Returns scripted response or raises scripted exception.
# @PRE method and endpoint are provided.
# @POST Returns scripted response or raises scripted exception.
def request(
self,
method: str,
@@ -58,8 +58,8 @@ class _RecordingNetworkClient:
# #region test_get_users_page_sends_lowercase_order_direction [TYPE Function]
# @RELATION BINDS_TO -> TestSupersetProfileLookup
# @BRIEF Ensures adapter sends lowercase order_direction compatible with Superset rison schema.
# @PRE: Adapter is initialized with recording network client.
# @POST: First request query payload contains order_direction='asc' for asc sort.
# @PRE Adapter is initialized with recording network client.
# @POST First request query payload contains order_direction='asc' for asc sort.
def test_get_users_page_sends_lowercase_order_direction():
client = _RecordingNetworkClient(
scripted_responses=[{"result": [{"username": "admin"}], "count": 1}]
@@ -80,8 +80,8 @@ def test_get_users_page_sends_lowercase_order_direction():
# #region test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error [TYPE Function]
# @RELATION BINDS_TO -> TestSupersetProfileLookup
# @BRIEF Ensures fallback auth error does not mask primary schema/query failure.
# @PRE: Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError.
# @POST: Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause.
# @PRE Primary endpoint fails with SupersetAPIError and fallback fails with AuthenticationError.
# @POST Raised exception remains primary SupersetAPIError (non-auth) to preserve root cause.
def test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error():
client = _RecordingNetworkClient(
scripted_responses=[
@@ -100,8 +100,8 @@ def test_get_users_page_preserves_primary_schema_error_over_fallback_auth_error(
# #region test_get_users_page_uses_fallback_endpoint_when_primary_fails [TYPE Function]
# @RELATION BINDS_TO -> TestSupersetProfileLookup
# @BRIEF Verifies adapter retries second users endpoint and succeeds when fallback is healthy.
# @PRE: Primary endpoint fails; fallback returns valid users payload.
# @POST: Result status is success and both endpoints were attempted in order.
# @PRE Primary endpoint fails; fallback returns valid users payload.
# @POST Result status is success and both endpoints were attempted in order.
def test_get_users_page_uses_fallback_endpoint_when_primary_fails():
client = _RecordingNetworkClient(
scripted_responses=[

View File

@@ -4,10 +4,10 @@ from src.core.scheduler import ThrottledSchedulerConfigurator
# #region test_throttled_scheduler [TYPE Module] [C:3] [SEMANTICS test, scheduler, throttle, unit]
# @RELATION BELONGS_TO -> SrcRoot
# @RELATION BINDS_TO -> SrcRoot
# @BRIEF Unit tests for ThrottledSchedulerConfigurator distribution logic.
# #region test_calculate_schedule_even_distribution [TYPE Function]
# @RELATION BINDS_TO -> test_throttled_scheduler
# @RELATION BINDS_TO -> [test_throttled_scheduler]
# @BRIEF Validate even spacing across a two-hour scheduling window for three tasks.
def test_calculate_schedule_even_distribution():
"""
@@ -26,7 +26,7 @@ def test_calculate_schedule_even_distribution():
assert schedule[2] == datetime(2024, 1, 1, 3, 0)
# #endregion test_calculate_schedule_even_distribution
# #region test_calculate_schedule_midnight_crossing [TYPE Function]
# @RELATION BINDS_TO -> test_throttled_scheduler
# @RELATION BINDS_TO -> [test_throttled_scheduler]
# @BRIEF Validate scheduler correctly rolls timestamps into the next day across midnight.
def test_calculate_schedule_midnight_crossing():
"""
@@ -45,7 +45,7 @@ def test_calculate_schedule_midnight_crossing():
assert schedule[2] == datetime(2024, 1, 2, 1, 0)
# #endregion test_calculate_schedule_midnight_crossing
# #region test_calculate_schedule_single_task [TYPE Function]
# @RELATION BINDS_TO -> test_throttled_scheduler
# @RELATION BINDS_TO -> [test_throttled_scheduler]
# @BRIEF Validate single-task schedule returns only the window start timestamp.
def test_calculate_schedule_single_task():
"""
@@ -62,7 +62,7 @@ def test_calculate_schedule_single_task():
assert schedule[0] == datetime(2024, 1, 1, 1, 0)
# #endregion test_calculate_schedule_single_task
# #region test_calculate_schedule_empty_list [TYPE Function]
# @RELATION BINDS_TO -> test_throttled_scheduler
# @RELATION BINDS_TO -> [test_throttled_scheduler]
# @BRIEF Validate empty dashboard list produces an empty schedule.
def test_calculate_schedule_empty_list():
"""
@@ -78,7 +78,7 @@ def test_calculate_schedule_empty_list():
assert schedule == []
# #endregion test_calculate_schedule_empty_list
# #region test_calculate_schedule_zero_window [TYPE Function]
# @RELATION BINDS_TO -> test_throttled_scheduler
# @RELATION BINDS_TO -> [test_throttled_scheduler]
# @BRIEF Validate zero-length window schedules all tasks at identical start timestamp.
def test_calculate_schedule_zero_window():
"""
@@ -96,7 +96,7 @@ def test_calculate_schedule_zero_window():
assert schedule[1] == datetime(2024, 1, 1, 1, 0)
# #endregion test_calculate_schedule_zero_window
# #region test_calculate_schedule_very_small_window [TYPE Function]
# @RELATION BINDS_TO -> test_throttled_scheduler
# @RELATION BINDS_TO -> [test_throttled_scheduler]
# @BRIEF Validate sub-second interpolation when task count exceeds near-zero window granularity.
def test_calculate_schedule_very_small_window():
"""

View File

@@ -1,6 +1,6 @@
# #region AsyncSupersetClientModule [C:3] [TYPE Module] [SEMANTICS superset, transform, dashboard, filter, async-superset-client]
# @BRIEF Parse a Superset dashboard URL and extract native filter state asynchronously.
# @LAYER: Core
# @LAYER Core
import asyncio
import json
import re
@@ -16,14 +16,14 @@ from .utils.async_network import AsyncAPIClient
# @BRIEF Async sibling of SupersetClient for dashboard read paths.
# @RELATION INHERITS -> [SupersetClient]
# @RELATION DEPENDS_ON -> [AsyncAPIClient]
# @RELATION CALLS -> [AsyncAPIClient.request]
# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
class AsyncSupersetClient(SupersetClient):
# #region AsyncSupersetClientInit [TYPE Function] [C:3]
# @PURPOSE: Initialize async Superset client with AsyncAPIClient transport.
# @PRE: env is valid Environment instance.
# @POST: Client uses async network transport and inherited projection helpers.
# @DATA_CONTRACT: Input[Environment] -> self.network[AsyncAPIClient]
# @RELATION: [DEPENDS_ON] ->[AsyncAPIClient]
# @PRE env is valid Environment instance.
# @POST Client uses async network transport and inherited projection helpers.
# @DATA_CONTRACT Input[Environment] -> self.network[AsyncAPIClient]
# @RELATION DEPENDS_ON -> [AsyncAPIClient]
def __init__(self, env: Environment):
self.env = env
auth_payload = {
@@ -41,17 +41,17 @@ class AsyncSupersetClient(SupersetClient):
# #endregion AsyncSupersetClientInit
# #region AsyncSupersetClientClose [TYPE Function] [C:3]
# @PURPOSE: Close async transport resources.
# @POST: Underlying AsyncAPIClient is closed.
# @SIDE_EFFECT: Closes network sockets.
# @RELATION: [CALLS] ->[AsyncAPIClient.aclose]
# @POST Underlying AsyncAPIClient is closed.
# @SIDE_EFFECT Closes network sockets.
# @RELATION CALLS -> [AsyncAPIClient.aclose]
async def aclose(self) -> None:
await self.network.aclose()
# #endregion AsyncSupersetClientClose
# #region get_dashboards_page_async [TYPE Function] [C:3]
# @PURPOSE: Fetch one dashboards page asynchronously.
# @POST: Returns total count and page result list.
# @DATA_CONTRACT: Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
# @RELATION: [CALLS] -> [AsyncAPIClient.request]
# @POST Returns total count and page result list.
# @DATA_CONTRACT Input[query: Optional[Dict]] -> Output[Tuple[int, List[Dict]]]
# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
async def get_dashboards_page_async(
self, query: dict | None = None
) -> tuple[int, list[dict]]:
@@ -84,9 +84,9 @@ class AsyncSupersetClient(SupersetClient):
# #endregion get_dashboards_page_async
# #region get_dashboard_async [TYPE Function] [C:3]
# @PURPOSE: Fetch one dashboard payload asynchronously.
# @POST: Returns raw dashboard payload from Superset API.
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]
# @RELATION: [CALLS] ->[AsyncAPIClient.request]
# @POST Returns raw dashboard payload from Superset API.
# @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]
# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
async def get_dashboard_async(self, dashboard_id: int) -> dict:
with belief_scope(
"AsyncSupersetClient.get_dashboard_async", f"id={dashboard_id}"
@@ -98,9 +98,9 @@ class AsyncSupersetClient(SupersetClient):
# #endregion get_dashboard_async
# #region get_chart_async [TYPE Function] [C:3]
# @PURPOSE: Fetch one chart payload asynchronously.
# @POST: Returns raw chart payload from Superset API.
# @DATA_CONTRACT: Input[chart_id: int] -> Output[Dict]
# @RELATION: [CALLS] ->[AsyncAPIClient.request]
# @POST Returns raw chart payload from Superset API.
# @DATA_CONTRACT Input[chart_id: int] -> Output[Dict]
# @RELATION CALLS -> [EXT:method:AsyncAPIClient.request]
async def get_chart_async(self, chart_id: int) -> dict:
with belief_scope("AsyncSupersetClient.get_chart_async", f"id={chart_id}"):
response = await self.network.request(
@@ -110,10 +110,10 @@ class AsyncSupersetClient(SupersetClient):
# #endregion get_chart_async
# #region get_dashboard_detail_async [TYPE Function] [C:3]
# @PURPOSE: Fetch dashboard detail asynchronously with concurrent charts/datasets requests.
# @POST: Returns dashboard detail payload for overview page.
# @DATA_CONTRACT: Input[dashboard_id: int] -> Output[Dict]
# @RELATION: [CALLS] ->[get_dashboard_async]
# @RELATION: [CALLS] ->[get_chart_async]
# @POST Returns dashboard detail payload for overview page.
# @DATA_CONTRACT Input[dashboard_id: int] -> Output[Dict]
# @RELATION CALLS -> [get_dashboard_async]
# @RELATION CALLS -> [get_chart_async]
async def get_dashboard_detail_async(self, dashboard_id: int) -> dict:
with belief_scope(
"AsyncSupersetClient.get_dashboard_detail_async", f"id={dashboard_id}"
@@ -395,8 +395,8 @@ class AsyncSupersetClient(SupersetClient):
# #endregion get_dashboard_detail_async
# #region get_dashboard_permalink_state_async [TYPE Function] [C:2]
# @PURPOSE: Fetch stored dashboard permalink state asynchronously.
# @POST: Returns dashboard permalink state payload from Superset API.
# @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]
# @POST Returns dashboard permalink state payload from Superset API.
# @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]
async def get_dashboard_permalink_state_async(self, permalink_key: str) -> dict:
with belief_scope(
"AsyncSupersetClient.get_dashboard_permalink_state_async",
@@ -409,8 +409,8 @@ class AsyncSupersetClient(SupersetClient):
# #endregion get_dashboard_permalink_state_async
# #region get_native_filter_state_async [TYPE Function] [C:2]
# @PURPOSE: Fetch stored native filter state asynchronously.
# @POST: Returns native filter state payload from Superset API.
# @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
# @POST Returns native filter state payload from Superset API.
# @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
async def get_native_filter_state_async(
self, dashboard_id: int, filter_state_key: str
) -> dict:
@@ -426,9 +426,9 @@ class AsyncSupersetClient(SupersetClient):
# #endregion get_native_filter_state_async
# #region extract_native_filters_from_permalink_async [TYPE Function] [C:3]
# @PURPOSE: Extract native filters dataMask from a permalink key asynchronously.
# @POST: Returns extracted dataMask with filter states.
# @DATA_CONTRACT: Input[permalink_key: str] -> Output[Dict]
# @RELATION: [CALLS] ->[get_dashboard_permalink_state_async]
# @POST Returns extracted dataMask with filter states.
# @DATA_CONTRACT Input[permalink_key: str] -> Output[Dict]
# @RELATION CALLS -> [get_dashboard_permalink_state_async]
async def extract_native_filters_from_permalink_async(
self, permalink_key: str
) -> dict:
@@ -461,9 +461,9 @@ class AsyncSupersetClient(SupersetClient):
# #endregion extract_native_filters_from_permalink_async
# #region extract_native_filters_from_key_async [TYPE Function] [C:3]
# @PURPOSE: Extract native filters from a native_filters_key URL parameter asynchronously.
# @POST: Returns extracted filter state with extraFormData.
# @DATA_CONTRACT: Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
# @RELATION: [CALLS] ->[get_native_filter_state_async]
# @POST Returns extracted filter state with extraFormData.
# @DATA_CONTRACT Input[dashboard_id: Union[int, str], filter_state_key: str] -> Output[Dict]
# @RELATION CALLS -> [get_native_filter_state_async]
async def extract_native_filters_from_key_async(
self, dashboard_id: int, filter_state_key: str
) -> dict:
@@ -514,10 +514,10 @@ class AsyncSupersetClient(SupersetClient):
# #endregion extract_native_filters_from_key_async
# #region parse_dashboard_url_for_filters_async [TYPE Function] [C:3]
# @PURPOSE: Parse a Superset dashboard URL and extract native filter state asynchronously.
# @POST: Returns extracted filter state or empty dict if no filters found.
# @DATA_CONTRACT: Input[url: str] -> Output[Dict]
# @RELATION: [CALLS] ->[extract_native_filters_from_permalink_async]
# @RELATION: [CALLS] ->[extract_native_filters_from_key_async]
# @POST Returns extracted filter state or empty dict if no filters found.
# @DATA_CONTRACT Input[url: str] -> Output[Dict]
# @RELATION CALLS -> [extract_native_filters_from_permalink_async]
# @RELATION CALLS -> [extract_native_filters_from_key_async]
async def parse_dashboard_url_for_filters_async(self, url: str) -> dict:
with belief_scope(
"AsyncSupersetClient.parse_dashboard_url_for_filters_async", f"url={url}"

View File

@@ -1,7 +1,7 @@
# #region test_auth [TYPE Module] [C:3] [SEMANTICS test, auth, authentication, unit]
# @BRIEF Unit tests for authentication module
# @LAYER: Domain
# @RELATION VERIFIES -> AuthPackage
# @LAYER Domain
# @RELATION BINDS_TO -> AuthPackage
from pathlib import Path
import sys
@@ -46,7 +46,7 @@ def auth_repo(db_session):
return AuthRepository(db_session)
# #region test_create_user [TYPE Function]
# @BRIEF Verifies that a persisted user can be retrieved with intact credential hash.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_create_user(auth_repo):
"""Test user creation"""
user = User(
@@ -65,7 +65,7 @@ def test_create_user(auth_repo):
# #endregion test_create_user
# #region test_authenticate_user [TYPE Function]
# @BRIEF Validates authentication outcomes for valid, wrong-password, and unknown-user cases.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_authenticate_user(auth_service, auth_repo):
"""Test user authentication with valid and invalid credentials"""
user = User(
@@ -89,7 +89,7 @@ def test_authenticate_user(auth_service, auth_repo):
# #endregion test_authenticate_user
# #region test_create_session [TYPE Function]
# @BRIEF Ensures session creation returns bearer token payload fields.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_create_session(auth_service, auth_repo):
"""Test session token creation"""
user = User(
@@ -108,7 +108,7 @@ def test_create_session(auth_service, auth_repo):
# #endregion test_create_session
# #region test_role_permission_association [TYPE Function]
# @BRIEF Confirms role-permission many-to-many assignments persist and reload correctly.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_role_permission_association(auth_repo):
"""Test role and permission association"""
role = Role(name="Admin", description="System administrator")
@@ -126,7 +126,7 @@ def test_role_permission_association(auth_repo):
# #endregion test_role_permission_association
# #region test_user_role_association [TYPE Function]
# @BRIEF Confirms user-role assignment persists and is queryable from repository reads.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_user_role_association(auth_repo):
"""Test user and role association"""
role = Role(name="Admin", description="System administrator")
@@ -147,7 +147,7 @@ def test_user_role_association(auth_repo):
# #endregion test_user_role_association
# #region test_ad_group_mapping [TYPE Function]
# @BRIEF Verifies AD group mapping rows persist and reference the expected role.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_ad_group_mapping(auth_repo):
"""Test AD group mapping"""
role = Role(name="ADFS_Admin", description="ADFS administrators")
@@ -166,7 +166,7 @@ def test_ad_group_mapping(auth_repo):
# #endregion test_ad_group_mapping
# #region test_authenticate_user_updates_last_login [TYPE Function]
# @BRIEF Verifies successful authentication updates last_login audit field.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_authenticate_user_updates_last_login(auth_service, auth_repo):
"""@SIDE_EFFECT: authenticate_user updates last_login timestamp on success."""
user = User(
@@ -184,7 +184,7 @@ def test_authenticate_user_updates_last_login(auth_service, auth_repo):
# #endregion test_authenticate_user_updates_last_login
# #region test_authenticate_inactive_user [TYPE Function]
# @BRIEF Verifies inactive accounts are rejected during password authentication.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_authenticate_inactive_user(auth_service, auth_repo):
"""@PRE: User with is_active=False should not authenticate."""
user = User(
@@ -201,7 +201,7 @@ def test_authenticate_inactive_user(auth_service, auth_repo):
# #endregion test_authenticate_inactive_user
# #region test_verify_password_empty_hash [TYPE Function]
# @BRIEF Verifies password verification safely rejects empty or null password hashes.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_verify_password_empty_hash():
"""@PRE: verify_password with empty/None hash returns False."""
assert verify_password("anypassword", "") is False
@@ -209,7 +209,7 @@ def test_verify_password_empty_hash():
# #endregion test_verify_password_empty_hash
# #region test_provision_adfs_user_new [TYPE Function]
# @BRIEF Verifies JIT provisioning creates a new ADFS user and maps group-derived roles.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_provision_adfs_user_new(auth_service, auth_repo):
"""@POST: provision_adfs_user creates a new ADFS user with correct roles."""
# Set up a role and AD group mapping
@@ -234,7 +234,7 @@ def test_provision_adfs_user_new(auth_service, auth_repo):
# #endregion test_provision_adfs_user_new
# #region test_provision_adfs_user_existing [TYPE Function]
# @BRIEF Verifies JIT provisioning reuses existing ADFS user and refreshes role assignments.
# @RELATION BINDS_TO -> test_auth
# @RELATION BINDS_TO -> [test_auth]
def test_provision_adfs_user_existing(auth_service, auth_repo):
"""@POST: provision_adfs_user updates roles for existing user."""
# Create existing user

View File

@@ -1,8 +1,8 @@
# #region APIKeyUtilities [C:2] [TYPE Module] [SEMANTICS auth, api_key, crypto, generation]
# @BRIEF API key generation and hashing utilities for service-to-service authentication.
# @LAYER Core
# @RELATION DEPENDS_ON -> [hashlib]
# @RELATION DEPENDS_ON -> [secrets]
# @RELATION DEPENDS_ON -> [EXT:Python:hashlib]
# @RELATION DEPENDS_ON -> [EXT:Python:secrets]
# @INVARIANT generate_api_key() always returns (raw_key, prefix, key_hash) where prefix is "ssk_" + 7 chars.
# @INVARIANT hash_api_key() produces SHA-256 hex digest for lookup and storage.
@@ -14,8 +14,8 @@ import secrets
# @BRIEF Generate a new API key in ssk_ format with SHA-256 hash.
# @POST Returns (raw_key, prefix, key_hash) — raw_key shown ONCE to caller, never stored.
# @SIDE_EFFECT Uses secrets.token_urlsafe(32) for cryptographic randomness.
# @RELATION DEPENDS_ON -> [secrets.token_urlsafe]
# @RELATION DEPENDS_ON -> [hashlib.sha256]
# @RELATION DEPENDS_ON -> [EXT:Python:secrets.token_urlsafe]
# @RELATION DEPENDS_ON -> [EXT:Python:hashlib.sha256]
def generate_api_key() -> tuple[str, str, str]:
"""Generate a new API key.
@@ -37,7 +37,7 @@ def generate_api_key() -> tuple[str, str, str]:
# @BRIEF Hash an API key string to SHA-256 hex digest for lookup.
# @PRE raw_key is a non-empty string.
# @POST Returns 64-character hex digest.
# @RELATION DEPENDS_ON -> [hashlib.sha256]
# @RELATION DEPENDS_ON -> [EXT:Python:hashlib.sha256]
def hash_api_key(raw_key: str) -> str:
"""Hash an API key using SHA-256.

View File

@@ -1,10 +1,10 @@
# #region AuthConfigModule [C:2] [TYPE Module] [SEMANTICS pydantic, auth, auth-config, config]
#
# @BRIEF Centralized configuration for authentication and authorization.
# @LAYER: Core
# @RELATION DEPENDS_ON -> pydantic
# @LAYER Core
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
#
# @INVARIANT: All sensitive configuration must be loaded from environment; no hardcoded secrets.
# @INVARIANT All sensitive configuration must be loaded from environment; no hardcoded secrets.
# @RATIONALE SECRET_KEY and AUTH_DATABASE_URL now crash-early if env vars are missing.
# Dev fallback for AUTH_DATABASE_URL only when DEV_MODE=true.
# @REJECTED Default secrets in source code rejected — Class 1 security violation:
@@ -19,9 +19,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
# #region AuthConfig [TYPE Class]
# @BRIEF Holds authentication-related settings.
# @PRE: Environment variables may be provided via .env file.
# @POST: Returns a configuration object with validated settings.
# @RELATION INHERITS -> pydantic_settings.BaseSettings
# @PRE Environment variables may be provided via .env file.
# @POST Returns a configuration object with validated settings.
# @RELATION INHERITS -> [EXT:Library:pydantic_settings.BaseSettings]
class AuthConfig(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")

View File

@@ -1,15 +1,14 @@
# #region AuthJwtModule [C:5] [TYPE Module] [SEMANTICS auth, validate, jwt, token]
#
# @BRIEF JWT token generation and validation logic.
# @LAYER: Core
# @LAYER Core
# @RELATION DEPENDS_ON -> [auth_config]
# @RELATION USES -> [auth_config]
#
# @INVARIANT: Tokens must include expiration time and user identifier.
# @PRE: JWT secret configured in environment
# @POST: Token encode/decode functions exported
# @SIDE_EFFECT: None
# @DATA_CONTRACT: TokenPayload -> JWT string
# @INVARIANT Tokens must include expiration time and user identifier.
# @PRE JWT secret configured in environment
# @POST Token encode/decode functions exported
# @SIDE_EFFECT None
# @DATA_CONTRACT TokenPayload -> JWT string
from datetime import datetime, timedelta
@@ -21,8 +20,8 @@ from .config import auth_config
# #region create_access_token [TYPE Function]
# @BRIEF Generates a new JWT access token.
# @PRE: data dict contains 'sub' (user_id) and optional 'scopes' (roles).
# @POST: Returns a signed JWT string.
# @PRE data dict contains 'sub' (user_id) and optional 'scopes' (roles).
# @POST Returns a signed JWT string.
# @RELATION DEPENDS_ON -> [auth_config]
#
def create_access_token(data: dict, expires_delta: timedelta | None = None) -> str:
@@ -47,8 +46,8 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None) -> s
# #region decode_token [TYPE Function]
# @BRIEF Decodes and validates a JWT token.
# @PRE: token is a signed JWT string.
# @POST: Returns the decoded payload if valid.
# @PRE token is a signed JWT string.
# @POST Returns the decoded payload if valid.
# @RELATION DEPENDS_ON -> [auth_config]
#
def decode_token(token: str) -> dict:

View File

@@ -1,16 +1,16 @@
# #region AuthLoggerModule [C:5] [TYPE Module] [SEMANTICS auth, audit, logging]
#
# @BRIEF Structured auth logging module for audit trail generation.
# @LAYER: Core
# @RELATION DEPENDS_ON -> [core_logger]
# @LAYER Core
# @RELATION DEPENDS_ON -> [EXT:frontend:core_logger]
#
# @INVARIANT: Must not log sensitive data like passwords or full tokens.
# @PRE: Auth module initialized
# @POST: Audit logging functions exported
# @SIDE_EFFECT: Writes auth audit log entries
# @DATA_CONTRACT: AuthEvent -> LogEntry
# @SIDE_EFFECT: Writes auth audit log entries
# @DATA_CONTRACT: AuthEvent -> LogEntry
# @INVARIANT Must not log sensitive data like passwords or full tokens.
# @PRE Auth module initialized
# @POST Audit logging functions exported
# @SIDE_EFFECT Writes auth audit log entries
# @DATA_CONTRACT AuthEvent -> LogEntry
# @SIDE_EFFECT Writes auth audit log entries
# @DATA_CONTRACT AuthEvent -> LogEntry
from datetime import datetime
@@ -19,9 +19,9 @@ from ..logger import belief_scope, logger
# #region log_security_event [TYPE Function]
# @BRIEF Logs a security-related event for audit trails.
# @PRE: event_type and username are strings.
# @POST: Security event is written to the application log.
# @RELATION USES -> logger
# @PRE event_type and username are strings.
# @POST Security event is written to the application log.
# @RELATION DEPENDS_ON -> [logger]
def log_security_event(event_type: str, username: str, details: dict = None):
with belief_scope("log_security_event", f"{event_type}:{username}"):
timestamp = datetime.utcnow().isoformat()

Some files were not shown because too many files have changed in this diff Show More