fix(llm): add fetch-models endpoint, fix SQL Lab INSERT (client_id truncation, sync mode, target_column, timestamp normalization)

- Add POST /api/llm/providers/fetch-models route with LLMClient.fetch_models()
- Add target_column to TranslationJob model/schema/service/orchestrator
- Fix SQL Lab execute: truncate client_id to 11 chars (varchar(11))
- Switch SQL Lab to sync mode (runAsync: false) — no Celery workers
- Fix polling: unwrap nested result from Superset query API
- Fix ClickHouse timestamp: normalize float timestamps to YYYY-MM-DD
This commit is contained in:
2026-05-13 20:06:15 +03:00
parent ffcfec9acf
commit 10803a5019
276 changed files with 1923 additions and 1822 deletions

View File

@@ -1,3 +1,3 @@
# #region SrcRoot [TYPE Module] # #region SrcRoot [TYPE Module] [SEMANTICS root, package]
# @BRIEF Canonical backend package root for application, scripts, and tests. # @BRIEF Canonical backend package root for application, scripts, and tests.
# #endregion SrcRoot # #endregion SrcRoot

View File

@@ -75,6 +75,69 @@ async def get_providers(
# #endregion get_providers # #endregion get_providers
# #region fetch_models [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.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION CALLS -> [LLMClient]
@router.post("/providers/fetch-models")
async def fetch_models(
payload: dict,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
):
from ...plugins.llm_analysis.service import LLMClient
from ...plugins.llm_analysis.models import LLMProviderType
base_url = (payload.get("base_url") or "").strip()
provider_type_str = (payload.get("provider_type") or "").strip()
api_key = payload.get("api_key") or ""
provider_id = payload.get("provider_id") or ""
# Resolve provider_id to stored credentials if no direct api_key given
if not api_key and provider_id:
service = LLMProviderService(db)
db_provider = service.get_provider(provider_id)
if not db_provider:
raise HTTPException(status_code=404, detail="Provider not found")
base_url = base_url or db_provider.base_url
provider_type_str = provider_type_str or db_provider.provider_type
stored_key = service.get_decrypted_api_key(provider_id)
if stored_key:
api_key = stored_key
if not base_url:
raise HTTPException(status_code=400, detail="base_url is required")
if not provider_type_str:
raise HTTPException(status_code=400, detail="provider_type is required")
try:
provider_type = LLMProviderType(provider_type_str)
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid provider_type: {provider_type_str}")
client = LLMClient(
provider_type=provider_type,
api_key=api_key or "sk-placeholder",
base_url=base_url,
default_model="",
)
try:
models = await client.fetch_models()
return {"models": models}
except Exception as e:
logger.warning(
f"[llm_routes.fetch_models] Failed to fetch models: {e}",
extra={"src": "llm_routes.fetch_models"},
)
raise HTTPException(status_code=502, detail=str(e))
# #endregion fetch_models
# #region get_llm_status [TYPE Function] # #region get_llm_status [TYPE Function]
# @BRIEF Returns whether LLM runtime is configured for dashboard validation. # @BRIEF Returns whether LLM runtime is configured for dashboard validation.
# @PRE: User is authenticated. # @PRE: User is authenticated.

View File

@@ -2,11 +2,12 @@
# @BRIEF Translation Run execution, history, status, records and batches routes. # @BRIEF Translation Run execution, history, status, records and batches routes.
# @LAYER: API # @LAYER: API
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status, Query from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ....core.database import get_db from ....core.database import get_db, SessionLocal
from ....core.logger import logger, belief_scope from ....core.logger import logger, belief_scope
from ....schemas.auth import User from ....schemas.auth import User
from ....dependencies import get_current_user, has_permission, get_config_manager from ....dependencies import get_current_user, has_permission, get_config_manager
@@ -39,11 +40,107 @@ async def run_translation(
try: try:
orch = TranslationOrchestrator(db, config_manager, current_user.username) orch = TranslationOrchestrator(db, config_manager, current_user.username)
run = orch.start_run(job_id=job_id, is_scheduled=False) run = orch.start_run(job_id=job_id, is_scheduled=False)
# Execute asynchronously in background # The request-scoped db session will be closed after this handler returns.
# The background thread must use its OWN session to avoid operating on a
# closed or expunged session.
import threading import threading
def _background_execute():
bg_db = SessionLocal()
try:
bg_orch = TranslationOrchestrator(
bg_db, config_manager,
current_user.username if current_user else None,
)
# Re-fetch the run within the background session to get a fresh,
# attached object
from ....models.translate import TranslationRun as TRModel
bg_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()
if bg_run is None:
logger.explore(
"Background execute: run not found",
extra={"src": "translate_routes", "run_id": run.id},
)
return
# execute_run internally calls self.db.commit()
bg_orch.execute_run(bg_run)
# ----- VERIFICATION -----
# After execute_run the run object from bg_db is detached after
# commit. Re-query to verify the status was persisted in the
# database and is a terminal state.
check_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()
if check_run and check_run.status in ("COMPLETED", "FAILED", "CANCELLED"):
logger.reason(
"Background execute verified",
extra={
"src": "translate_routes",
"run_id": run.id,
"status": check_run.status,
},
)
else:
# execute_run appeared to succeed but the run is still in a
# non-terminal state — manually fail it so the frontend
# polling can detect the terminal state and stop spinning.
actual_status = check_run.status if check_run else "NOT_FOUND"
logger.explore(
"Background execute: run not in terminal state after commit",
extra={
"src": "translate_routes",
"run_id": run.id,
"status": actual_status,
},
)
if check_run:
check_run.status = "FAILED"
check_run.error_message = (
"Background execution did not reach terminal state "
f"(was: {actual_status})"
)
bg_db.commit()
except Exception as bg_err:
logger.explore(
"Background execute failed",
extra={
"src": "translate_routes",
"run_id": run.id,
"error": str(bg_err),
},
)
# Mark the run as FAILED so the frontend polling can detect a
# terminal state and stop spinning.
try:
fb_db = SessionLocal()
try:
fb_run = fb_db.query(TRModel).filter(TRModel.id == run.id).first()
if fb_run:
fb_run.status = "FAILED"
fb_run.error_message = f"Background execute error: {bg_err}"
fb_run.completed_at = datetime.now(timezone.utc)
fb_db.commit()
logger.reason(
"Background execute: run marked FAILED",
extra={"src": "translate_routes", "run_id": run.id},
)
finally:
fb_db.close()
except Exception as fb_err:
logger.explore(
"Background execute: unable to mark run FAILED",
extra={
"src": "translate_routes",
"run_id": run.id,
"error": str(fb_err),
},
)
finally:
bg_db.close()
threading.Thread( threading.Thread(
target=orch.execute_run, target=_background_execute,
args=(run,),
daemon=True, daemon=True,
).start() ).start()
return _run_to_response(run) return _run_to_response(run)

View File

@@ -484,6 +484,24 @@ def _ensure_translation_jobs_columns(bind_engine):
) )
raise raise
if "target_database_id" not in existing_columns:
try:
with bind_engine.begin() as connection:
connection.execute(
text(
"ALTER TABLE translation_jobs "
"ADD COLUMN target_database_id VARCHAR"
)
)
logger.reflect(
"Added target_database_id column to translation_jobs",
)
except Exception as migration_error:
logger.explore(
"Failed to add target_database_id to translation_jobs",
extra={"error": str(migration_error)},
)
def _ensure_dataset_review_session_columns(bind_engine): def _ensure_dataset_review_session_columns(bind_engine):
with belief_scope("_ensure_dataset_review_session_columns"): with belief_scope("_ensure_dataset_review_session_columns"):

View File

@@ -47,7 +47,7 @@ class SupersetDatabasesMixin:
# @RELATION: CALLS -> [SupersetClientGetDatabases] # @RELATION: CALLS -> [SupersetClientGetDatabases]
def get_databases_summary(self) -> List[Dict]: def get_databases_summary(self) -> List[Dict]:
with belief_scope("SupersetClient.get_databases_summary"): with belief_scope("SupersetClient.get_databases_summary"):
query = {"columns": ["uuid", "database_name", "backend"]} query = {"columns": ["id", "uuid", "database_name", "backend"]}
_, databases = self.get_databases(query=query) _, databases = self.get_databases(query=query)
# Map 'backend' to 'engine' for consistency with contracts # Map 'backend' to 'engine' for consistency with contracts
for db in databases: for db in databases:

View File

@@ -243,11 +243,45 @@ class APIClient:
with belief_scope("authenticate"): with belief_scope("authenticate"):
app_logger.info("[authenticate][Enter] Authenticating to %s", self.base_url) app_logger.info("[authenticate][Enter] Authenticating to %s", self.base_url)
cached_tokens = SupersetAuthCache.get(self._auth_cache_key) cached_tokens = SupersetAuthCache.get(self._auth_cache_key)
if cached_tokens and cached_tokens.get("access_token") and cached_tokens.get("csrf_token"): if cached_tokens and cached_tokens.get("access_token"):
self._tokens = cached_tokens # Cache hit — we have a valid access_token, but we need to
self._authenticated = True # re-establish the session cookie in this new requests.Session.
app_logger.info("[authenticate][CacheHit] Reusing cached Superset auth tokens for %s", self.base_url) # Superset CSRF protection pairs the X-CSRFToken header with a
return self._tokens # Flask session cookie set during login. A fresh requests.Session
# lacks that cookie, so even a valid cached CSRF token would fail
# with "CSRF session token is missing". We re-fetch the CSRF
# token to both establish the session cookie AND get a fresh
# CSRF token for the current session.
try:
csrf_url = f"{self.api_base_url}/security/csrf_token/"
csrf_response = self.session.get(
csrf_url,
headers={"Authorization": f"Bearer {cached_tokens['access_token']}"},
timeout=self.request_settings["timeout"],
)
csrf_response.raise_for_status()
csrf_token = csrf_response.json()["result"]
self._tokens = {
"access_token": cached_tokens["access_token"],
"csrf_token": csrf_token,
}
self._authenticated = True
# Update cache with fresh CSRF token for this session
SupersetAuthCache.set(self._auth_cache_key, self._tokens)
app_logger.info(
"[authenticate][CacheHit] Reusing cached Superset auth tokens for %s "
"(CSRF token refreshed for new session)",
self.base_url,
)
return self._tokens
except Exception as refresh_err:
app_logger.warning(
"[authenticate][CacheRefreshFailed] CSRF token refresh failed, "
"falling back to full re-authentication: %s",
refresh_err,
)
SupersetAuthCache.invalidate(self._auth_cache_key)
# Fall through to full authentication below
try: try:
login_url = f"{self.api_base_url}/security/login" login_url = f"{self.api_base_url}/security/login"
# Log the payload keys and values (masking password) # Log the payload keys and values (masking password)

View File

@@ -42,7 +42,8 @@ class TranslationJob(Base):
# Column mapping # Column mapping
source_key_cols = Column(JSON, nullable=True, comment="Source key column names for composite key") source_key_cols = Column(JSON, nullable=True, comment="Source key column names for composite key")
target_key_cols = Column(JSON, nullable=True, comment="Target key column names for composite key") target_key_cols = Column(JSON, nullable=True, comment="Target key column names for composite key")
translation_column = Column(String, nullable=True, comment="The column whose values will be translated") translation_column = Column(String, nullable=True, comment="Source column whose values will be translated")
target_column = Column(String, nullable=True, comment="Target column for translated output (defaults to translation_column)")
context_columns = Column(JSON, nullable=True, comment="Context column names included in LLM prompt") context_columns = Column(JSON, nullable=True, comment="Context column names included in LLM prompt")
# LLM & processing settings # LLM & processing settings
@@ -53,6 +54,7 @@ class TranslationJob(Base):
# Environment association # Environment association
environment_id = Column(String, nullable=True, comment="Superset environment ID for datasource access") environment_id = Column(String, nullable=True, comment="Superset environment ID for datasource access")
target_database_id = Column(String, nullable=True, comment="Superset database ID for SQL Lab insert target")
created_by = Column(String, nullable=True) created_by = Column(String, nullable=True)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
@@ -119,6 +121,7 @@ class TranslationRecord(Base):
source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset source_object_type = Column(String, nullable=True) # query, dashboard, chart, dataset
source_object_id = Column(String, nullable=True) source_object_id = Column(String, nullable=True)
source_object_name = Column(String, nullable=True) source_object_name = Column(String, nullable=True)
source_data = Column(JSON, nullable=True, comment="Original source row key columns for upsert matching")
status = Column(String, nullable=False, default="PENDING") # PENDING, SUCCESS, FAILED, SKIPPED status = Column(String, nullable=False, default="PENDING") # PENDING, SUCCESS, FAILED, SKIPPED
error_message = Column(Text, nullable=True) error_message = Column(Text, nullable=True)
token_count_input = Column(Integer, nullable=True) token_count_input = Column(Integer, nullable=True)
@@ -174,6 +177,7 @@ class TranslationPreviewRecord(Base):
source_object_type = Column(String, nullable=True) source_object_type = Column(String, nullable=True)
source_object_id = Column(String, nullable=True) source_object_id = Column(String, nullable=True)
source_object_name = Column(String, nullable=True) source_object_name = Column(String, nullable=True)
source_data = Column(JSON, nullable=True, comment="Original source row key columns for upsert matching")
status = Column(String, nullable=False, default="PENDING") # PENDING, APPROVED, REJECTED status = Column(String, nullable=False, default="PENDING") # PENDING, APPROVED, REJECTED
feedback = Column(Text, nullable=True) feedback = Column(Text, nullable=True)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))

View File

@@ -1,5 +1,4 @@
# #region GitLLMExtensionModule [C:3] [TYPE Module] [SEMANTICS git, llm, commit, message, generation] # #region GitLLMExtensionModule [C:3] [TYPE Module] [SEMANTICS git, llm, commit, message, generation]
# @COMPLEXITY: 3
# @BRIEF LLM-based extensions for the Git plugin, specifically for commit message generation. # @BRIEF LLM-based extensions for the Git plugin, specifically for commit message generation.
# @LAYER: Domain # @LAYER: Domain
# @RELATION DEPENDS_ON -> [LLMClient] # @RELATION DEPENDS_ON -> [LLMClient]

View File

@@ -1,6 +1,5 @@
# region TestClientHeaders [TYPE Module] # region TestClientHeaders [TYPE Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @SEMANTICS: tests, llm-client, openrouter, headers # @SEMANTICS: tests, llm-client, openrouter, headers
# @PURPOSE: Verify OpenRouter client initialization includes provider-specific headers. # @PURPOSE: Verify OpenRouter client initialization includes provider-specific headers.

View File

@@ -1,6 +1,5 @@
# region TestScreenshotService [TYPE Module] # region TestScreenshotService [TYPE Module]
# @RELATION: VERIFIES ->[src.plugins.llm_analysis.service.ScreenshotService] # @RELATION: VERIFIES ->[src.plugins.llm_analysis.service.ScreenshotService]
# @COMPLEXITY: 3
# @SEMANTICS: tests, screenshot-service, navigation, timeout-regression # @SEMANTICS: tests, screenshot-service, navigation, timeout-regression
# @PURPOSE: Protect dashboard screenshot navigation from brittle networkidle waits. # @PURPOSE: Protect dashboard screenshot navigation from brittle networkidle waits.

View File

@@ -1,6 +1,5 @@
# region TestService [TYPE Module] # region TestService [TYPE Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @SEMANTICS: tests, llm-analysis, fallback, provider-error, unknown-status # @SEMANTICS: tests, llm-analysis, fallback, provider-error, unknown-status
# @PURPOSE: Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results. # @PURPOSE: Verify LLM analysis transport/provider failures do not masquerade as dashboard FAIL results.

View File

@@ -1,5 +1,4 @@
# #region LLMAnalysisPlugin [C:3] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation] # #region LLMAnalysisPlugin [C:3] [TYPE Module] [SEMANTICS llm, analysis, dashboard, validation, documentation]
# @COMPLEXITY: 3
# @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin. # @BRIEF Implements DashboardValidationPlugin and DocumentationPlugin.
# @LAYER: Domain # @LAYER: Domain
# @RELATION INHERITS -> [PluginBase] # @RELATION INHERITS -> [PluginBase]

View File

@@ -1,5 +1,4 @@
# #region LLMAnalysisScheduler [C:3] [TYPE Module] [SEMANTICS scheduler, llm, validation, task, cron] # #region LLMAnalysisScheduler [C:3] [TYPE Module] [SEMANTICS scheduler, llm, validation, task, cron]
# @COMPLEXITY: 3
# @BRIEF Provides helper functions to schedule LLM-based validation tasks. # @BRIEF Provides helper functions to schedule LLM-based validation tasks.
# @LAYER: Domain # @LAYER: Domain
# @RELATION DEPENDS_ON -> [SchedulerService] # @RELATION DEPENDS_ON -> [SchedulerService]

View File

@@ -1,5 +1,4 @@
# #region LLMAnalysisService [C:3] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity] # #region LLMAnalysisService [C:3] [TYPE Module] [SEMANTICS llm, screenshot, playwright, openai, tenacity]
# @COMPLEXITY: 3
# @BRIEF Services for LLM interaction and dashboard screenshots. # @BRIEF Services for LLM interaction and dashboard screenshots.
# @LAYER: Domain # @LAYER: Domain
# @RELATION DEPENDS_ON -> playwright # @RELATION DEPENDS_ON -> playwright
@@ -876,6 +875,29 @@ class LLMClient:
return await self.get_json_completion(messages) return await self.get_json_completion(messages)
# endregion LLMClient.test_runtime_connection # endregion LLMClient.test_runtime_connection
# region LLMClient.fetch_models [TYPE Function]
# @PURPOSE: Fetch available models from the provider's API.
# @PRE: Client is initialized with provider credentials.
# @POST: Returns a list of model ID strings.
# @SIDE_EFFECT: Calls external LLM API /v1/models endpoint.
async def fetch_models(self) -> List[str]:
with belief_scope("LLMClient.fetch_models"):
try:
response = await self.client.models.list()
model_ids = [m.id for m in response.data]
model_ids.sort()
logger.reason(
f"[LLMClient.fetch_models] Fetched {len(model_ids)} models from {self.base_url}",
extra={"src": "LLMClient.fetch_models"},
)
return model_ids
except Exception as e:
logger.warning(
f"[LLMClient.fetch_models] Failed to fetch models: {e}",
)
raise
# endregion LLMClient.fetch_models
# region LLMClient.analyze_dashboard [TYPE Function] # region LLMClient.analyze_dashboard [TYPE Function]
# @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis. # @PURPOSE: Sends dashboard data (screenshot + logs) to LLM for health analysis.
# @PRE: screenshot_path exists, logs is a list of strings. # @PRE: screenshot_path exists, logs is a list of strings.

View File

@@ -1,5 +1,4 @@
# region ClickHouseInsertIntegration [TYPE Module] # region ClickHouseInsertIntegration [TYPE Module]
# @COMPLEXITY: 3
# @SEMANTICS: test, clickhouse, integration, insert, join # @SEMANTICS: test, clickhouse, integration, insert, join
# @PURPOSE: Integration tests for ClickHouse INSERT with timestamp normalization and JOIN verification. # @PURPOSE: Integration tests for ClickHouse INSERT with timestamp normalization and JOIN verification.
# @LAYER: Test # @LAYER: Test
@@ -369,6 +368,8 @@ class TestOrchestratorInsertFlow:
) as MockExecutor: ) as MockExecutor:
mock_executor = MagicMock() mock_executor = MagicMock()
MockExecutor.return_value = mock_executor MockExecutor.return_value = mock_executor
mock_executor.resolve_database_id.return_value = None
mock_executor.get_database_backend.return_value = None
mock_executor.execute_and_poll.return_value = { mock_executor.execute_and_poll.return_value = {
"status": "success", "status": "success",
"query_id": "q-123", "query_id": "q-123",

View File

@@ -1,5 +1,4 @@
# region DictionaryTests [TYPE Module] # region DictionaryTests [TYPE Module]
# @COMPLEXITY: 3
# @SEMANTICS: tests, dictionary, crud, import, filter # @SEMANTICS: tests, dictionary, crud, import, filter
# @PURPOSE: Validate DictionaryManager CRUD, import, deletion guards, and batch filtering. # @PURPOSE: Validate DictionaryManager CRUD, import, deletion guards, and batch filtering.
# @RELATION: BINDS_TO -> [DictionaryManager:Class] # @RELATION: BINDS_TO -> [DictionaryManager:Class]
@@ -38,7 +37,6 @@ from src.plugins.translate._utils import _normalize_term, _detect_delimiter
# region _FakeJob [TYPE Class] # region _FakeJob [TYPE Class]
# @COMPLEXITY: 1
# @PURPOSE: Helper to create inline TranslationJob records. # @PURPOSE: Helper to create inline TranslationJob records.
class _FakeJob: class _FakeJob:
pass pass

View File

@@ -1,5 +1,4 @@
# region OrchestratorTests [TYPE Module] # region OrchestratorTests [TYPE Module]
# @COMPLEXITY: 3
# @SEMANTICS: test, translate, orchestrator, events # @SEMANTICS: test, translate, orchestrator, events
# @PURPOSE: Tests for TranslationOrchestrator: run lifecycle, partial failure, batch retry, event invariants, NULL handling. # @PURPOSE: Tests for TranslationOrchestrator: run lifecycle, partial failure, batch retry, event invariants, NULL handling.
# @LAYER: Test # @LAYER: Test
@@ -10,9 +9,10 @@
# @TEST_FIXTURE: mock_config_manager -> MagicMock ConfigManager # @TEST_FIXTURE: mock_config_manager -> MagicMock ConfigManager
# @TEST_EDGE: missing_preview -> raises ValueError # @TEST_EDGE: missing_preview -> raises ValueError
# @TEST_EDGE: invalid_run_status -> raises ValueError # @TEST_EDGE: invalid_run_status -> raises ValueError
# @TEST_EDGE: executor_failure -> run is marked FAILED
import pytest import pytest
from unittest.mock import MagicMock from unittest.mock import MagicMock, patch
from datetime import datetime, timezone from datetime import datetime, timezone
from src.plugins.translate.orchestrator import TranslationOrchestrator from src.plugins.translate.orchestrator import TranslationOrchestrator
@@ -234,6 +234,210 @@ class TestTranslationOrchestrator:
with pytest.raises(ValueError, match="PENDING"): with pytest.raises(ValueError, match="PENDING"):
orch.execute_run(run) orch.execute_run(run)
# region test_execute_run_success [TYPE Function]
# @BRIEF Happy path: executor completes, SQL generated, Superset submits.
def test_execute_run_success(self, mock_job: MagicMock) -> None:
db = MagicMock()
config_manager = MagicMock()
mock_job.target_table = "target_tbl"
# Mock job query — first call returns mock_job, final re-query returns completed_run
completed_run_response = MagicMock()
completed_run_response.id = "run-1"
completed_run_response.job_id = "job-123"
completed_run_response.status = "COMPLETED"
completed_run_response.total_records = 10
completed_run_response.successful_records = 10
completed_run_response.failed_records = 0
completed_run_response.skipped_records = 0
completed_run_response.completed_at = datetime.now(timezone.utc)
completed_run_response.error_message = None
completed_run_response.insert_status = "success"
completed_run_response.superset_execution_id = "q-1"
completed_run_response.superset_execution_log = {}
# query job -> mock_job, query TranslationRun -> completed_run_response
db.query.return_value.filter.return_value.first.side_effect = [
mock_job,
completed_run_response,
]
# Create a PENDING input run
run = MagicMock()
run.id = "run-1"
run.job_id = "job-123"
run.status = "PENDING"
with patch(
"src.plugins.translate.orchestrator.TranslationExecutor"
) as MockExecutor:
mock_executor_instance = MagicMock()
mock_executor_instance.execute_run.return_value = completed_run_response
MockExecutor.return_value = mock_executor_instance
orch = TranslationOrchestrator(db, config_manager, "test-user")
with patch.object(orch, "event_log"):
with patch.object(
orch, "_generate_and_insert_sql",
return_value={"status": "success", "query_id": "q-1", "rows_affected": 10},
):
result = orch.execute_run(run)
assert result.status == "COMPLETED"
assert result.insert_status == "success"
assert result.superset_execution_id == "q-1"
assert result.total_records == 10
assert result.successful_records == 10
# endregion test_execute_run_success
# region test_execute_run_executor_failure [TYPE Function]
# @BRIEF Executor raises exception, run is marked FAILED.
def test_execute_run_executor_failure(self, mock_job: MagicMock) -> None:
db = MagicMock()
config_manager = MagicMock()
# Mock job query
db.query.return_value.filter.return_value.first.return_value = mock_job
run = MagicMock()
run.id = "run-1"
run.job_id = "job-123"
run.status = "PENDING"
run.error_message = None
with patch(
"src.plugins.translate.orchestrator.TranslationExecutor"
) as MockExecutor:
mock_executor_instance = MagicMock()
mock_executor_instance.execute_run.side_effect = ValueError(
"LLM provider unavailable"
)
MockExecutor.return_value = mock_executor_instance
orch = TranslationOrchestrator(db, config_manager, "test-user")
with patch.object(orch, "event_log"):
result = orch.execute_run(run)
assert result.status == "FAILED"
assert "LLM provider unavailable" in (result.error_message or "")
# endregion test_execute_run_executor_failure
# region test_execute_run_skip_insert [TYPE Function]
# @BRIEF skip_insert=True completes run without SQL generation or Superset submission.
def test_execute_run_skip_insert(self, mock_job: MagicMock) -> None:
db = MagicMock()
config_manager = MagicMock()
db.query.return_value.filter.return_value.first.return_value = mock_job
run = MagicMock()
run.id = "run-1"
run.job_id = "job-123"
run.status = "PENDING"
run.completed_at = None
completed_run = MagicMock()
completed_run.id = "run-1"
completed_run.job_id = "job-123"
completed_run.status = "COMPLETED"
completed_run.total_records = 5
completed_run.successful_records = 5
completed_run.failed_records = 0
completed_run.skipped_records = 0
completed_run.completed_at = datetime.now(timezone.utc)
completed_run.error_message = None
with patch(
"src.plugins.translate.orchestrator.TranslationExecutor"
) as MockExecutor:
mock_executor_instance = MagicMock()
mock_executor_instance.execute_run.return_value = completed_run
MockExecutor.return_value = mock_executor_instance
orch = TranslationOrchestrator(db, config_manager, "test-user")
with patch.object(orch, "_generate_and_insert_sql") as mock_gen_sql:
with patch.object(orch, "event_log"):
result = orch.execute_run(run, skip_insert=True)
assert result.status == "COMPLETED"
# _generate_and_insert_sql should NOT be called in skip_insert mode
mock_gen_sql.assert_not_called()
assert result.total_records == 5
# endregion test_execute_run_skip_insert
# region test_execute_run_no_job [TYPE Function]
# @BRIEF Job not found raises ValueError.
def test_execute_run_no_job(self) -> None:
db = MagicMock()
config_manager = MagicMock()
# Mock job query returns None (job not found)
db.query.return_value.filter.return_value.first.return_value = None
run = MagicMock()
run.id = "run-1"
run.job_id = "job-missing"
run.status = "PENDING"
orch = TranslationOrchestrator(db, config_manager, "test-user")
with pytest.raises(ValueError, match="not found"):
orch.execute_run(run)
# endregion test_execute_run_no_job
# region test_execute_run_insert_failure [TYPE Function]
# @BRIEF SQL generation/insert returns failure, run still completes but with error logged.
def test_execute_run_insert_failure(self, mock_job: MagicMock) -> None:
db = MagicMock()
config_manager = MagicMock()
# First query returns mock_job, re-query returns completed_run
completed_run_response = MagicMock()
completed_run_response.id = "run-1"
completed_run_response.job_id = "job-123"
completed_run_response.status = "COMPLETED"
completed_run_response.total_records = 3
completed_run_response.successful_records = 3
completed_run_response.failed_records = 0
completed_run_response.skipped_records = 0
completed_run_response.completed_at = datetime.now(timezone.utc)
completed_run_response.error_message = None
completed_run_response.insert_status = "failed"
completed_run_response.superset_execution_id = ""
completed_run_response.superset_execution_log = {}
db.query.return_value.filter.return_value.first.side_effect = [
mock_job,
completed_run_response,
]
run = MagicMock()
run.id = "run-1"
run.job_id = "job-123"
run.status = "PENDING"
with patch(
"src.plugins.translate.orchestrator.TranslationExecutor"
) as MockExecutor:
mock_executor_instance = MagicMock()
mock_executor_instance.execute_run.return_value = completed_run_response
MockExecutor.return_value = mock_executor_instance
orch = TranslationOrchestrator(db, config_manager, "test-user")
with patch.object(
orch, "_generate_and_insert_sql",
return_value={"status": "failed", "error_message": "timeout", "query_id": None},
):
with patch.object(orch, "event_log"):
result = orch.execute_run(run)
assert result.status == "COMPLETED"
assert result.insert_status == "failed"
assert "timeout" in (result.error_message or "")
# endregion test_execute_run_insert_failure
# region test_cancel_run [TYPE Function] # region test_cancel_run [TYPE Function]
# @PURPOSE: Cancel a run changes status to CANCELLED. # @PURPOSE: Cancel a run changes status to CANCELLED.
def test_cancel_run(self) -> None: def test_cancel_run(self) -> None:

View File

@@ -1,5 +1,4 @@
# region TranslationPreviewTests [TYPE Module] # region TranslationPreviewTests [TYPE Module]
# @COMPLEXITY: 3
# @SEMANTICS: test, translate, preview, session # @SEMANTICS: test, translate, preview, session
# @PURPOSE: Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate. # @PURPOSE: Test TranslationPreview service: preview, approve/edit/reject state transitions, cost estimation, acceptance gate.
# @LAYER: Test # @LAYER: Test

View File

@@ -1,5 +1,4 @@
# region SQLGeneratorTests [TYPE Module] # region SQLGeneratorTests [TYPE Module]
# @COMPLEXITY: 3
# @SEMANTICS: test, translate, sql_generator # @SEMANTICS: test, translate, sql_generator
# @PURPOSE: Tests for SQLGenerator: PostgreSQL INSERT + UPSERT, ClickHouse INSERT, dialect quoting, NULL handling, injection safety. # @PURPOSE: Tests for SQLGenerator: PostgreSQL INSERT + UPSERT, ClickHouse INSERT, dialect quoting, NULL handling, injection safety.
# @LAYER: Test # @LAYER: Test

View File

@@ -32,7 +32,6 @@ from ._utils import _normalize_term, _detect_delimiter
# @SIDE_EFFECT: Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards. # @SIDE_EFFECT: Creates, updates, deletes TerminologyDictionary and DictionaryEntry rows; enforces deletion guards.
class DictionaryManager: class DictionaryManager:
# region DictionaryManager.create_dictionary [TYPE Function] # region DictionaryManager.create_dictionary [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Create a new terminology dictionary. # @PURPOSE: Create a new terminology dictionary.
# @PRE: payload contains name, source_dialect, target_dialect. # @PRE: payload contains name, source_dialect, target_dialect.
# @POST: New TerminologyDictionary row is created and returned. # @POST: New TerminologyDictionary row is created and returned.
@@ -60,7 +59,6 @@ class DictionaryManager:
# endregion DictionaryManager.create_dictionary # endregion DictionaryManager.create_dictionary
# region DictionaryManager.update_dictionary [TYPE Function] # region DictionaryManager.update_dictionary [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Update an existing terminology dictionary. # @PURPOSE: Update an existing terminology dictionary.
# @PRE: dict_id exists in terminology_dictionaries table. # @PRE: dict_id exists in terminology_dictionaries table.
# @POST: Dictionary metadata is updated and returned. # @POST: Dictionary metadata is updated and returned.
@@ -92,7 +90,6 @@ class DictionaryManager:
# endregion DictionaryManager.update_dictionary # endregion DictionaryManager.update_dictionary
# region DictionaryManager.delete_dictionary [TYPE Function] # region DictionaryManager.delete_dictionary [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Delete a dictionary, blocked if attached to active/scheduled jobs. # @PURPOSE: Delete a dictionary, blocked if attached to active/scheduled jobs.
# @PRE: dict_id exists. # @PRE: dict_id exists.
# @POST: Dictionary and its entries are deleted, unless attached to ACTIVE/READY/RUNNING/SCHEDULED jobs. # @POST: Dictionary and its entries are deleted, unless attached to ACTIVE/READY/RUNNING/SCHEDULED jobs.
@@ -136,7 +133,6 @@ class DictionaryManager:
# endregion DictionaryManager.delete_dictionary # endregion DictionaryManager.delete_dictionary
# region DictionaryManager.get_dictionary [TYPE Function] # region DictionaryManager.get_dictionary [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Get a single dictionary by ID with entry count. # @PURPOSE: Get a single dictionary by ID with entry count.
# @PRE: dict_id exists. # @PRE: dict_id exists.
# @POST: Returns dict with dictionary + entry_count or raises ValueError. # @POST: Returns dict with dictionary + entry_count or raises ValueError.
@@ -149,7 +145,6 @@ class DictionaryManager:
# endregion DictionaryManager.get_dictionary # endregion DictionaryManager.get_dictionary
# region DictionaryManager.list_dictionaries [TYPE Function] # region DictionaryManager.list_dictionaries [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: List dictionaries with pagination and entry counts. # @PURPOSE: List dictionaries with pagination and entry counts.
# @PRE: page >= 1, page_size between 1 and 100. # @PRE: page >= 1, page_size between 1 and 100.
# @POST: Returns (list of dicts, total_count). # @POST: Returns (list of dicts, total_count).
@@ -169,7 +164,6 @@ class DictionaryManager:
# endregion DictionaryManager.list_dictionaries # endregion DictionaryManager.list_dictionaries
# region DictionaryManager.add_entry [TYPE Function] # region DictionaryManager.add_entry [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Add an entry to a dictionary, enforcing unique source_term_normalized. # @PURPOSE: Add an entry to a dictionary, enforcing unique source_term_normalized.
# @PRE: dict_id exists. source_term and target_term are non-empty. # @PRE: dict_id exists. source_term and target_term are non-empty.
# @POST: New DictionaryEntry row is created or raises on duplicate. # @POST: New DictionaryEntry row is created or raises on duplicate.
@@ -210,7 +204,6 @@ class DictionaryManager:
# endregion DictionaryManager.add_entry # endregion DictionaryManager.add_entry
# region DictionaryManager.edit_entry [TYPE Function] # region DictionaryManager.edit_entry [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Edit an existing dictionary entry with duplicate-aware normalization. # @PURPOSE: Edit an existing dictionary entry with duplicate-aware normalization.
# @PRE: entry_id exists. # @PRE: entry_id exists.
# @POST: Entry fields are updated. # @POST: Entry fields are updated.
@@ -255,7 +248,6 @@ class DictionaryManager:
# endregion DictionaryManager.edit_entry # endregion DictionaryManager.edit_entry
# region DictionaryManager.delete_entry [TYPE Function] # region DictionaryManager.delete_entry [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Delete a single dictionary entry. # @PURPOSE: Delete a single dictionary entry.
# @PRE: entry_id exists. # @PRE: entry_id exists.
# @POST: Entry is deleted. # @POST: Entry is deleted.
@@ -272,7 +264,6 @@ class DictionaryManager:
# endregion DictionaryManager.delete_entry # endregion DictionaryManager.delete_entry
# region DictionaryManager.clear_entries [TYPE Function] # region DictionaryManager.clear_entries [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Delete all entries for a dictionary. # @PURPOSE: Delete all entries for a dictionary.
# @PRE: dict_id exists. # @PRE: dict_id exists.
# @POST: All entries for the dictionary are deleted. # @POST: All entries for the dictionary are deleted.
@@ -290,7 +281,6 @@ class DictionaryManager:
# endregion DictionaryManager.clear_entries # endregion DictionaryManager.clear_entries
# region DictionaryManager.list_entries [TYPE Function] # region DictionaryManager.list_entries [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: List entries for a dictionary with pagination. # @PURPOSE: List entries for a dictionary with pagination.
# @PRE: dict_id exists. # @PRE: dict_id exists.
# @POST: Returns (list of entries, total_count). # @POST: Returns (list of entries, total_count).
@@ -316,7 +306,6 @@ class DictionaryManager:
# endregion DictionaryManager.list_entries # endregion DictionaryManager.list_entries
# region DictionaryManager.import_entries [TYPE Function] # region DictionaryManager.import_entries [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Import entries from CSV/TSV content with duplicate detection and conflict resolution. # @PURPOSE: Import entries from CSV/TSV content with duplicate detection and conflict resolution.
# @PRE: content is valid CSV or TSV. dict_id exists. # @PRE: content is valid CSV or TSV. dict_id exists.
# @POST: Entries are created/updated/skipped per conflict mode. Returns result summary. # @POST: Entries are created/updated/skipped per conflict mode. Returns result summary.
@@ -456,7 +445,6 @@ class DictionaryManager:
# endregion DictionaryManager.import_entries # endregion DictionaryManager.import_entries
# region DictionaryManager.filter_for_batch [TYPE Function] # region DictionaryManager.filter_for_batch [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job. # @PURPOSE: Scan batch texts for case-insensitive, word-boundary-aware matches against all dictionaries attached to a job.
# @PRE: job_id exists and source_texts is a list of strings. # @PRE: job_id exists and source_texts is a list of strings.
# @POST: Returns list of matched entries with match info, sorted by priority across attached dictionaries. # @POST: Returns list of matched entries with match info, sorted by priority across attached dictionaries.
@@ -555,7 +543,6 @@ class DictionaryManager:
# region DictionaryManager.submit_correction [TYPE Function] # region DictionaryManager.submit_correction [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Submit a term correction from a run result. Validates language match, detects conflicts. # @PURPOSE: Submit a term correction from a run result. Validates language match, detects conflicts.
# @PRE: source_term, incorrect_target_term, corrected_target_term are non-empty. dict_id exists. # @PRE: source_term, incorrect_target_term, corrected_target_term are non-empty. dict_id exists.
# @POST: Entry created or updated; origin tracking populated. Returns action + conflict info. # @POST: Entry created or updated; origin tracking populated. Returns action + conflict info.
@@ -653,7 +640,6 @@ class DictionaryManager:
# endregion DictionaryManager.submit_correction # endregion DictionaryManager.submit_correction
# region DictionaryManager.submit_bulk_corrections [TYPE Function] # region DictionaryManager.submit_bulk_corrections [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Submit multiple term corrections atomically — all succeed or all fail. # @PURPOSE: Submit multiple term corrections atomically — all succeed or all fail.
# @PRE: corrections list is non-empty. dict_id exists. # @PRE: corrections list is non-empty. dict_id exists.
# @POST: All corrections applied or none applied with conflict list. # @POST: All corrections applied or none applied with conflict list.

View File

@@ -48,7 +48,6 @@ class TranslationEventLog:
self.db = db self.db = db
# region log_event [TYPE Function] # region log_event [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Write an immutable event. Enforces terminal event invariant for non-null run_id. # @PURPOSE: Write an immutable event. Enforces terminal event invariant for non-null run_id.
# @PRE: event_type must be a known type. If run_id is not None, enforce terminal invariant. # @PRE: event_type must be a known type. If run_id is not None, enforce terminal invariant.
# @POST: TranslationEvent row is created. # @POST: TranslationEvent row is created.

View File

@@ -164,12 +164,102 @@ class TranslationExecutor:
# endregion execute_run # endregion execute_run
# region _fetch_source_rows [TYPE Function] # region _fetch_source_rows [TYPE Function]
# @PURPOSE: Fetch source rows from the accepted preview session for this job. # @PURPOSE: Fetch full source dataset from Superset (via datasource) for full translation.
# @PRE: job_id exists. # @PRE: job_id exists. Job may have source_datasource_id for full fetch.
# @POST: Returns list of dicts with source data. # @POST: Returns list of dicts with source data (all rows from the source datasource).
# @SIDE_EFFECT: Makes HTTP call to Superset chart data API when datasource is configured.
def _fetch_source_rows(self, job_id: str, run_id: str) -> List[Dict[str, Any]]: def _fetch_source_rows(self, job_id: str, run_id: str) -> List[Dict[str, Any]]:
with belief_scope("TranslationExecutor._fetch_source_rows"): with belief_scope("TranslationExecutor._fetch_source_rows"):
# Get the latest APPLIED preview session job = self.db.query(TranslationJob).filter(TranslationJob.id == job_id).first()
# If source_datasource_id is configured, fetch ALL rows from the Superset chart data API
if job and job.source_datasource_id:
try:
logger.reason("Fetching full dataset from Superset datasource", {
"run_id": run_id,
"datasource_id": job.source_datasource_id,
"environment_id": job.environment_id,
})
# Determine environment
environments = self.config_manager.get_environments()
target_env_id = job.environment_id or job.source_dialect or ""
env_config = next(
(e for e in environments if e.id == target_env_id),
None,
)
if not env_config and environments:
env_config = environments[0]
if env_config:
from ...core.superset_client import SupersetClient
client = SupersetClient(env_config)
# Fetch dataset detail to build proper query context
dataset_detail = client.get_dataset_detail(int(job.source_datasource_id))
# Build query context (same approach as preview but without row_limit)
query_context = client.build_dataset_preview_query_context(
dataset_id=int(job.source_datasource_id),
dataset_record=dataset_detail,
template_params={},
effective_filters=[],
)
# Remove row_limit to get ALL rows; use result_type="samples"
queries = query_context.get("queries", [])
if queries:
queries[0].pop("row_limit", None)
queries[0].pop("result_type", None)
queries[0]["metrics"] = []
query_context["result_type"] = "samples"
form_data = query_context.get("form_data", {})
form_data.pop("query_mode", None)
response = client.network.request(
method="POST",
endpoint="/api/v1/chart/data",
data=json.dumps(query_context),
headers={"Content-Type": "application/json"},
)
# Extract rows
rows = self._extract_chart_data_rows(response)
if rows:
logger.reason(f"Fetched {len(rows)} rows from Superset datasource", {
"run_id": run_id,
})
# Map rows to source_rows format
source_rows = []
for idx, row in enumerate(rows):
source_data_dict = dict(row) if row else None
source_text = str(row.get(job.translation_column, "")) if job.translation_column else json.dumps(row)
source_rows.append({
"row_index": str(idx),
"source_text": source_text,
"approved_translation": None,
"source_object_name": f"Row {idx}",
"source_data": source_data_dict,
})
return source_rows
else:
logger.explore("Superset datasource returned no rows", {
"run_id": run_id,
"datasource_id": job.source_datasource_id,
})
else:
logger.explore("No environment config found for datasource fetch", {
"env_id": target_env_id,
})
except Exception as e:
logger.explore("Failed to fetch full dataset from Superset, falling back to preview", {
"run_id": run_id,
"error": str(e),
})
# Fall through to preview-based fetch
# Fallback: get the latest APPLIED preview session
session = ( session = (
self.db.query(TranslationPreviewSession) self.db.query(TranslationPreviewSession)
.filter( .filter(
@@ -195,20 +285,48 @@ class TranslationExecutor:
source_rows = [] source_rows = []
for rec in records: for rec in records:
source_data_dict = None
if hasattr(rec, "source_data") and rec.source_data:
source_data_dict = dict(rec.source_data)
source_rows.append({ source_rows.append({
"row_index": rec.source_object_id or "0", "row_index": rec.source_object_id or "0",
"source_text": rec.source_sql or "", "source_text": rec.source_sql or "",
"approved_translation": rec.target_sql if rec.status == "APPROVED" else None, "approved_translation": rec.target_sql if rec.status == "APPROVED" else None,
"source_object_name": rec.source_object_name or "", "source_object_name": rec.source_object_name or "",
"source_data": source_data_dict,
}) })
logger.reason(f"Fetched {len(source_rows)} source rows from preview", { logger.reason(f"Fetched {len(source_rows)} source rows from preview fallback", {
"run_id": run_id, "run_id": run_id,
"session_id": session.id, "session_id": session.id,
}) })
return source_rows return source_rows
# endregion _fetch_source_rows # endregion _fetch_source_rows
# region _extract_chart_data_rows [TYPE Function]
# @PURPOSE: Extract data rows from Superset chart data API response.
# @POST: Returns list of dicts with column-value pairs.
@staticmethod
def _extract_chart_data_rows(response: Dict[str, Any]) -> List[Dict[str, Any]]:
result = response.get("result")
if isinstance(result, list):
for item in result:
if isinstance(item, dict):
data = item.get("data")
if isinstance(data, list) and data:
return data
if isinstance(result, dict):
data = result.get("data")
if isinstance(data, list) and data:
return data
data = response.get("data")
if isinstance(data, list) and data:
return data
if isinstance(result, list):
return result
return []
# endregion _extract_chart_data_rows
# region _process_batch [TYPE Function] # region _process_batch [TYPE Function]
# @PURPOSE: Process a single batch: filter dict, build prompt, call LLM, persist records. # @PURPOSE: Process a single batch: filter dict, build prompt, call LLM, persist records.
# @PRE: job and batch_rows are valid. # @PRE: job and batch_rows are valid.
@@ -272,6 +390,7 @@ class TranslationExecutor:
source_object_type="table_row", source_object_type="table_row",
source_object_id=row.get("row_index"), source_object_id=row.get("row_index"),
source_object_name=row.get("source_object_name", ""), source_object_name=row.get("source_object_name", ""),
source_data=row.get("source_data"),
status="SUCCESS", status="SUCCESS",
) )
self.db.add(record) self.db.add(record)
@@ -398,6 +517,7 @@ class TranslationExecutor:
source_object_type="table_row", source_object_type="table_row",
source_object_id=row.get("row_index"), source_object_id=row.get("row_index"),
source_object_name=row.get("source_object_name", ""), source_object_name=row.get("source_object_name", ""),
source_data=row.get("source_data"),
status="FAILED", status="FAILED",
error_message=f"LLM call failed after {retries} retries: {last_error}", error_message=f"LLM call failed after {retries} retries: {last_error}",
) )
@@ -425,6 +545,7 @@ class TranslationExecutor:
source_object_type="table_row", source_object_type="table_row",
source_object_id=row.get("row_index"), source_object_id=row.get("row_index"),
source_object_name=row.get("source_object_name", ""), source_object_name=row.get("source_object_name", ""),
source_data=row.get("source_data"),
status="SKIPPED", status="SKIPPED",
error_message=f"LLM parse failure: {e}", error_message=f"LLM parse failure: {e}",
) )
@@ -456,6 +577,7 @@ class TranslationExecutor:
source_object_type="table_row", source_object_type="table_row",
source_object_id=row.get("row_index"), source_object_id=row.get("row_index"),
source_object_name=row.get("source_object_name", ""), source_object_name=row.get("source_object_name", ""),
source_data=row.get("source_data"),
status="SKIPPED", status="SKIPPED",
error_message="NULL translation returned by LLM", error_message="NULL translation returned by LLM",
) )
@@ -474,6 +596,7 @@ class TranslationExecutor:
source_object_type="table_row", source_object_type="table_row",
source_object_id=row.get("row_index"), source_object_id=row.get("row_index"),
source_object_name=row.get("source_object_name", ""), source_object_name=row.get("source_object_name", ""),
source_data=row.get("source_data"),
status="SKIPPED", status="SKIPPED",
error_message="Empty translation returned by LLM", error_message="Empty translation returned by LLM",
) )
@@ -490,6 +613,7 @@ class TranslationExecutor:
source_object_type="table_row", source_object_type="table_row",
source_object_id=row.get("row_index"), source_object_id=row.get("row_index"),
source_object_name=row.get("source_object_name", ""), source_object_name=row.get("source_object_name", ""),
source_data=row.get("source_data"),
status="SUCCESS", status="SUCCESS",
) )
self.db.add(record) self.db.add(record)

View File

@@ -63,7 +63,6 @@ class TranslationOrchestrator:
self._job: Optional[TranslationJob] = None self._job: Optional[TranslationJob] = None
# region start_run [TYPE Function] # region start_run [TYPE Function]
# @COMPLEXITY: 5
# @PURPOSE: Start a new translation run for a job. # @PURPOSE: Start a new translation run for a job.
# @PRE: job_id exists. For manual runs, there must be an accepted preview session. # @PRE: job_id exists. For manual runs, there must be an accepted preview session.
# @POST: TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded. # @POST: TranslationRun is created in PENDING status with hash fields and config snapshot. Events are recorded.
@@ -105,6 +104,7 @@ class TranslationOrchestrator:
"source_key_cols": job.source_key_cols, "source_key_cols": job.source_key_cols,
"target_key_cols": job.target_key_cols, "target_key_cols": job.target_key_cols,
"translation_column": job.translation_column, "translation_column": job.translation_column,
"target_column": job.target_column,
"context_columns": job.context_columns, "context_columns": job.context_columns,
"target_language": job.target_language, "target_language": job.target_language,
"provider_id": job.provider_id, "provider_id": job.provider_id,
@@ -263,7 +263,11 @@ class TranslationOrchestrator:
run.insert_status = insert_result.get("status") run.insert_status = insert_result.get("status")
run.superset_execution_id = str(insert_result.get("query_id") or "") run.superset_execution_id = str(insert_result.get("query_id") or "")
run.superset_execution_log = insert_result run.superset_execution_log = insert_result
run.status = "COMPLETED" if insert_result.get("status") == "success" else "COMPLETED" # Preserve the translation-phase status. If the executor already
# marked the run FAILED (all LLM calls failed) we do NOT upgrade it
# to COMPLETED just because the insert phase ran.
if run.status != "FAILED":
run.status = "COMPLETED"
if insert_result.get("error_message"): if insert_result.get("error_message"):
run.error_message = insert_result["error_message"] run.error_message = insert_result["error_message"]
run.completed_at = datetime.now(timezone.utc) run.completed_at = datetime.now(timezone.utc)
@@ -288,7 +292,9 @@ class TranslationOrchestrator:
) )
self.db.commit() self.db.commit()
self.db.refresh(run) # Re-query run after commit — refresh may fail if the object was
# created in a different session or became detached during commit.
run = self.db.query(TranslationRun).filter(TranslationRun.id == run.id).first()
logger.reflect("Run execution complete", { logger.reflect("Run execution complete", {
"run_id": run.id, "run_id": run.id,
@@ -329,11 +335,20 @@ class TranslationOrchestrator:
"dialect": job.database_dialect or job.target_dialect, "dialect": job.database_dialect or job.target_dialect,
}) })
# Build rows for SQL generation # Determine effective target column for INSERT (defaults to translation_column)
effective_target = job.target_column or job.translation_column
# Build columns for SQL generation
columns = job.context_columns or [] columns = job.context_columns or []
# Always include translation_column (original text) if it's different from target
if job.translation_column and job.translation_column not in columns: if job.translation_column and job.translation_column not in columns:
columns.append(job.translation_column) columns.append(job.translation_column)
# Add target_column separately if it differs from translation_column
if effective_target and effective_target != job.translation_column and effective_target not in columns:
columns.append(effective_target)
# Also include key columns if used for upsert # Also include key columns if used for upsert
if job.target_key_cols: if job.target_key_cols:
for k in job.target_key_cols: for k in job.target_key_cols:
@@ -343,27 +358,57 @@ class TranslationOrchestrator:
rows_for_sql = [] rows_for_sql = []
for rec in records: for rec in records:
row_data = {} row_data = {}
source_data = rec.source_data or {}
# Context columns from source data
if job.context_columns: if job.context_columns:
for col in job.context_columns: for col in job.context_columns:
row_data[col] = "" row_data[col] = source_data.get(col, "")
if job.translation_column:
row_data[job.translation_column] = rec.target_sql or "" # Original text column (translation_column)
if job.translation_column and job.translation_column not in (job.target_key_cols or []):
# If target_column differs, keep the original value from source_data;
# otherwise use the translated value
if effective_target and effective_target != job.translation_column:
row_data[job.translation_column] = source_data.get(job.translation_column, "")
else:
row_data[job.translation_column] = rec.target_sql or ""
# Translated text goes into target_column (may be same as translation_column)
if effective_target:
row_data[effective_target] = rec.target_sql or ""
# Key columns from source data
if job.target_key_cols: if job.target_key_cols:
source_data = rec.source_data or {}
for k in job.target_key_cols: for k in job.target_key_cols:
# Use source_data value if available, fall back to empty string
row_data[k] = source_data.get(k, "") row_data[k] = source_data.get(k, "")
rows_for_sql.append(row_data) rows_for_sql.append(row_data)
if not columns: if not columns:
# Use target_sql as the sole column # Use target_sql as the sole column
columns = [job.translation_column or "translated_text"] columns = [effective_target or "translated_text"]
rows_for_sql = [{columns[0]: rec.target_sql or ""} for rec in records] rows_for_sql = [{columns[0]: rec.target_sql or ""} for rec in records]
# Resolve the real database backend engine from Superset
try:
env_id = job.environment_id or job.source_dialect or ""
executor = SupersetSqlLabExecutor(self.config_manager, env_id)
executor.resolve_database_id(
target_database_id=job.target_database_id,
)
real_backend = executor.get_database_backend()
except Exception as e:
logger.explore("Failed to resolve database backend, falling back to job dialet", {
"error": str(e),
})
real_backend = None
dialect = real_backend or job.database_dialect or job.target_dialect or "postgresql"
# Generate SQL # Generate SQL
try: try:
sql, row_count = SQLGenerator.generate( sql, row_count = SQLGenerator.generate(
dialect=job.database_dialect or job.target_dialect or "postgresql", dialect=dialect,
target_schema=job.target_schema, target_schema=job.target_schema,
target_table=job.target_table or "translated_data", target_table=job.target_table or "translated_data",
columns=columns, columns=columns,
@@ -375,19 +420,24 @@ class TranslationOrchestrator:
logger.explore("SQL generation failed", {"error": str(e)}) logger.explore("SQL generation failed", {"error": str(e)})
return {"status": "failed", "error_message": str(e), "query_id": None} return {"status": "failed", "error_message": str(e), "query_id": None}
logger.reason("SQL generated with dialect", {
"dialect": dialect,
"real_backend": real_backend,
"job_database_dialect": job.database_dialect,
"job_target_dialect": job.target_dialect,
})
# Log insert phase start # Log insert phase start
self.event_log.log_event( self.event_log.log_event(
job_id=job.id, job_id=job.id,
run_id=run.id, run_id=run.id,
event_type="INSERT_PHASE_STARTED", event_type="INSERT_PHASE_STARTED",
payload={"sql_length": len(sql), "row_count": row_count}, payload={"sql_length": len(sql), "row_count": row_count, "dialect": dialect},
created_by=self.current_user, created_by=self.current_user,
) )
# Submit to Superset # Submit to Superset
try: try:
env_id = job.environment_id or job.source_dialect or ""
executor = SupersetSqlLabExecutor(self.config_manager, env_id)
result = executor.execute_and_poll( result = executor.execute_and_poll(
sql=sql, sql=sql,
max_polls=30, max_polls=30,

View File

@@ -134,7 +134,6 @@ class TranslationPreview:
self.current_user = current_user self.current_user = current_user
# region preview_rows [TYPE Function] # region preview_rows [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Fetch sample rows from Superset dataset, send to LLM for translation, create preview session with records. # @PURPOSE: Fetch sample rows from Superset dataset, send to LLM for translation, create preview session with records.
# @PRE: job_id exists and job has source_datasource_id, translation_column configured. # @PRE: job_id exists and job has source_datasource_id, translation_column configured.
# @POST: Returns TranslationPreviewResponse with records, cost estimation, and persistent session. # @POST: Returns TranslationPreviewResponse with records, cost estimation, and persistent session.
@@ -290,6 +289,19 @@ class TranslationPreview:
status = "PENDING" status = "PENDING"
feedback = None feedback = None
# Extract source_data: store original row key columns for upsert matching
source_row = meta.get("source_row", {})
source_data = None
if job.target_key_cols:
source_data = {
k: source_row.get(k)
for k in job.target_key_cols
if k in source_row
}
elif source_row:
# No key columns configured — store the full row as fallback
source_data = dict(source_row)
record = TranslationPreviewRecord( record = TranslationPreviewRecord(
id=str(uuid.uuid4()), id=str(uuid.uuid4()),
session_id=session.id, session_id=session.id,
@@ -298,6 +310,7 @@ class TranslationPreview:
source_object_type="table_row", source_object_type="table_row",
source_object_id=str(idx), source_object_id=str(idx),
source_object_name=f"Row {idx + 1}", source_object_name=f"Row {idx + 1}",
source_data=source_data,
status=status, status=status,
feedback=feedback, feedback=feedback,
created_at=datetime.now(timezone.utc), created_at=datetime.now(timezone.utc),
@@ -516,7 +529,6 @@ class TranslationPreview:
# endregion get_preview_session # endregion get_preview_session
# region _fetch_sample_rows [TYPE Function] # region _fetch_sample_rows [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Fetch sample rows from the Superset dataset for preview. # @PURPOSE: Fetch sample rows from the Superset dataset for preview.
# @PRE: job has source_datasource_id and translation_column. # @PRE: job has source_datasource_id and translation_column.
# @POST: Returns list of dicts with row data. # @POST: Returns list of dicts with row data.
@@ -636,7 +648,6 @@ class TranslationPreview:
# endregion _extract_data_rows # endregion _extract_data_rows
# region _call_llm [TYPE Function] # region _call_llm [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Call the configured LLM provider with the preview prompt. # @PURPOSE: Call the configured LLM provider with the preview prompt.
# @PRE: job has a valid provider_id. # @PRE: job has a valid provider_id.
# @POST: Returns raw LLM response string. # @POST: Returns raw LLM response string.

View File

@@ -241,12 +241,14 @@ class TranslateJobService:
source_key_cols=payload.source_key_cols or [], source_key_cols=payload.source_key_cols or [],
target_key_cols=payload.target_key_cols or [], target_key_cols=payload.target_key_cols or [],
translation_column=payload.translation_column, translation_column=payload.translation_column,
target_column=payload.target_column,
context_columns=payload.context_columns or [], context_columns=payload.context_columns or [],
target_language=payload.target_language, target_language=payload.target_language,
provider_id=payload.provider_id, provider_id=payload.provider_id,
batch_size=payload.batch_size, batch_size=payload.batch_size,
upsert_strategy=payload.upsert_strategy, upsert_strategy=payload.upsert_strategy,
environment_id=payload.environment_id, environment_id=payload.environment_id,
target_database_id=payload.target_database_id,
status="DRAFT", status="DRAFT",
created_by=self.current_user, created_by=self.current_user,
) )
@@ -360,6 +362,7 @@ class TranslateJobService:
source_key_cols=source.source_key_cols, source_key_cols=source.source_key_cols,
target_key_cols=source.target_key_cols, target_key_cols=source.target_key_cols,
translation_column=source.translation_column, translation_column=source.translation_column,
target_column=source.target_column,
context_columns=source.context_columns, context_columns=source.context_columns,
target_language=source.target_language, target_language=source.target_language,
provider_id=source.provider_id, provider_id=source.provider_id,
@@ -461,6 +464,7 @@ def job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) -
source_key_cols=job.source_key_cols or [], source_key_cols=job.source_key_cols or [],
target_key_cols=job.target_key_cols or [], target_key_cols=job.target_key_cols or [],
translation_column=job.translation_column, translation_column=job.translation_column,
target_column=job.target_column,
context_columns=job.context_columns or [], context_columns=job.context_columns or [],
target_language=job.target_language, target_language=job.target_language,
provider_id=job.provider_id, provider_id=job.provider_id,
@@ -472,6 +476,7 @@ def job_to_response(job: TranslationJob, dict_ids: Optional[List[str]] = None) -
updated_at=job.updated_at, updated_at=job.updated_at,
dictionary_ids=dict_ids or [], dictionary_ids=dict_ids or [],
environment_id=job.environment_id, environment_id=job.environment_id,
target_database_id=job.target_database_id,
) )
# #endregion job_to_response # #endregion job_to_response

View File

@@ -90,11 +90,16 @@ def _encode_sql_value(value: Any, dialect: Optional[str] = None) -> str:
if isinstance(value, bool): if isinstance(value, bool):
return "TRUE" if value else "FALSE" return "TRUE" if value else "FALSE"
# For ClickHouse: try to normalize timestamp-like string values # For ClickHouse: try to normalize timestamp-like values (both string and numeric)
if dialect in CLICKHOUSE_DIALECTS and isinstance(value, str) and value: if dialect in CLICKHOUSE_DIALECTS:
normalized = _normalize_timestamp_value(value) if isinstance(value, str) and value:
if normalized: normalized = _normalize_timestamp_value(value)
return f"'{normalized}'" if normalized:
return f"'{normalized}'"
elif isinstance(value, (int, float)):
normalized = _normalize_timestamp_value(value)
if normalized:
return f"'{normalized}'"
if isinstance(value, (int, float)): if isinstance(value, (int, float)):
return str(value) return str(value)

View File

@@ -32,6 +32,7 @@ class SupersetSqlLabExecutor:
self.env_id = env_id self.env_id = env_id
self._client: Optional[SupersetClient] = None self._client: Optional[SupersetClient] = None
self._database_id: Optional[int] = None self._database_id: Optional[int] = None
self._database_backend: Optional[str] = None
# region _get_client [TYPE Function] # region _get_client [TYPE Function]
# @PURPOSE: Lazy-initialize SupersetClient for the configured environment. # @PURPOSE: Lazy-initialize SupersetClient for the configured environment.
@@ -53,15 +54,43 @@ class SupersetSqlLabExecutor:
# endregion _get_client # endregion _get_client
# region resolve_database_id [TYPE Function] # region resolve_database_id [TYPE Function]
# @PURPOSE: Resolve the target database ID from the environment. # @PURPOSE: Resolve the target database ID from the environment and fetch its backend engine.
# @PRE: database_name or database_id should be known. # @PRE: database_name or database_id should be known.
# @POST: Returns database_id integer or raises ValueError. # @POST: Returns database_id integer or raises ValueError. Also sets self._database_backend.
# @SIDE_EFFECT: Fetches databases list from Superset. # @SIDE_EFFECT: Fetches databases list from Superset; optionally fetches single DB for backend.
def resolve_database_id(self, database_name: Optional[str] = None) -> int: def resolve_database_id(
self,
database_name: Optional[str] = None,
target_database_id: Optional[str] = None,
) -> int:
with belief_scope("SupersetSqlLabExecutor.resolve_database_id"): with belief_scope("SupersetSqlLabExecutor.resolve_database_id"):
client = self._get_client() client = self._get_client()
# If a specific target_database_id is provided, use it directly
if target_database_id:
try:
db_id = int(target_database_id)
# Fetch full DB info to get backend
db_info = client.get_database(db_id)
result = db_info.get("result", db_info)
self._database_id = result.get("id", db_id)
self._database_backend = result.get("backend") or result.get("engine")
logger.reason("Resolved database ID by target_database_id", {
"target_database_id": target_database_id,
"database_id": self._database_id,
"backend": self._database_backend,
})
return self._database_id
except Exception as e:
logger.explore("Failed to resolve by target_database_id, falling back", {
"target_database_id": target_database_id,
"error": str(e),
})
# Fall through to default resolution
# Fetch full database list with backend info
_, databases = client.get_databases( _, databases = client.get_databases(
query={"columns": ["id", "database_name"]} query={"columns": ["id", "database_name", "backend"]}
) )
if not databases: if not databases:
raise ValueError("No databases found in Superset environment") raise ValueError("No databases found in Superset environment")
@@ -70,9 +99,11 @@ class SupersetSqlLabExecutor:
for db in databases: for db in databases:
if db.get("database_name", "").lower() == database_name.lower(): if db.get("database_name", "").lower() == database_name.lower():
self._database_id = db["id"] self._database_id = db["id"]
self._database_backend = db.get("backend")
logger.reason("Resolved database ID by name", { logger.reason("Resolved database ID by name", {
"database_name": database_name, "database_name": database_name,
"database_id": self._database_id, "database_id": self._database_id,
"backend": self._database_backend,
}) })
return self._database_id return self._database_id
raise ValueError( raise ValueError(
@@ -81,13 +112,22 @@ class SupersetSqlLabExecutor:
# Default: use first database # Default: use first database
self._database_id = databases[0]["id"] self._database_id = databases[0]["id"]
self._database_backend = databases[0].get("backend")
logger.reason("Using default database", { logger.reason("Using default database", {
"database_name": databases[0].get("database_name"), "database_name": databases[0].get("database_name"),
"database_id": self._database_id, "database_id": self._database_id,
"backend": self._database_backend,
}) })
return self._database_id return self._database_id
# endregion resolve_database_id # endregion resolve_database_id
# region get_database_backend [TYPE Function]
# @PURPOSE: Return the cached database backend/engine string.
# @POST: Returns backend string or None if not yet resolved.
def get_database_backend(self) -> Optional[str]:
return self._database_backend
# endregion get_database_backend
# region execute_sql [TYPE Function] # region execute_sql [TYPE Function]
# @PURPOSE: Submit SQL to Superset SQL Lab and return execution tracking info. # @PURPOSE: Submit SQL to Superset SQL Lab and return execution tracking info.
# @PRE: sql is valid SQL string. database_id is a valid Superset DB ID. # @PRE: sql is valid SQL string. database_id is a valid Superset DB ID.
@@ -117,27 +157,75 @@ class SupersetSqlLabExecutor:
"runAsync": run_async, "runAsync": run_async,
"schema": None, "schema": None,
"tab": "translation-insert", "tab": "translation-insert",
"client_id": f"trl-{uuid.uuid4().hex[:7]}", # max 11 chars for Superset varchar(11)
} }
try: # Try multiple SQL Lab endpoints — some Superset versions use /sqllab/execute/,
response = client.network.request( # others use /sql_lab/execute/ (with underscore)
method="POST", candidate_endpoints = ["/api/v1/sqllab/execute/", "/api/v1/sql_lab/execute/"]
endpoint="/api/v1/sqllab/execute/", response = None
data=json.dumps(payload), last_error = None
headers={"Content-Type": "application/json"}, for endpoint in candidate_endpoints:
) try:
except Exception as e: response = client.network.request(
logger.explore("SQL Lab execute failed", {"error": str(e)}) method="POST",
raise ValueError(f"Superset SQL Lab execute failed: {e}") endpoint=endpoint,
data=json.dumps(payload),
headers={"Content-Type": "application/json"},
)
if isinstance(response, dict) and response:
logger.reason(f"SQL Lab endpoint succeeded", extra={
"endpoint": endpoint,
})
break
except Exception as ep_err:
last_error = ep_err
logger.explore(f"SQL Lab endpoint failed, trying next", extra={
"error": str(ep_err),
"endpoint": endpoint,
})
response = None
# Parse response if response is None:
error_msg = f"All SQL Lab endpoints failed. Last error: {last_error}"
logger.explore("SQL Lab execute failed", extra={"error": error_msg})
raise ValueError(f"Superset SQL Lab execute failed: {error_msg}")
# Parse response — try multiple known Superset response formats
result = response if isinstance(response, dict) else {} result = response if isinstance(response, dict) else {}
query_id = result.get("query_id") or result.get("id")
# Superset may return query_id at top level, nested in "result", as "id", or in "query" block
query_id = (
result.get("query_id")
or result.get("id")
or (result.get("result") or {}).get("query_id")
or (result.get("result") or {}).get("id")
or (result.get("query") or {}).get("query_id")
or (result.get("query") or {}).get("id")
)
status = result.get("status", "unknown") status = result.get("status", "unknown")
# Log full response for debugging if query_id is missing
if not query_id:
logger.explore("No query_id from SQL Lab execute", extra={
"database_id": db_id,
"status": status,
"raw_response_keys": list(result.keys()),
"raw_response_preview": json.dumps(result)[:2000],
})
logger.reason("SQL Lab execute response (no query_id)", extra={
"database_id": db_id,
"status": status,
"response_keys": list(result.keys()),
"has_result": "result" in result,
"has_query": "query" in result,
"raw_preview": json.dumps(result)[:2000],
})
logger.reason("SQL Lab execute response", { logger.reason("SQL Lab execute response", {
"query_id": query_id, "query_id": query_id,
"status": status, "status": status,
"response_keys": list(result.keys()),
}) })
return { return {
@@ -174,7 +262,9 @@ class SupersetSqlLabExecutor:
method="GET", method="GET",
endpoint=f"/api/v1/query/{query_id}", endpoint=f"/api/v1/query/{query_id}",
) )
result = response if isinstance(response, dict) else {} raw = response if isinstance(response, dict) else {}
# Superset wraps query data in a nested "result" field
result = raw.get("result", raw)
status = result.get("status", "unknown") status = result.get("status", "unknown")
state = result.get("state", result.get("status", "")) state = result.get("state", result.get("status", ""))
@@ -255,9 +345,27 @@ class SupersetSqlLabExecutor:
exec_result = self.execute_sql( exec_result = self.execute_sql(
sql=sql, sql=sql,
database_id=database_id, database_id=database_id,
run_async=True, run_async=False, # Sync mode — Superset returns result directly
) )
status = exec_result.get("status", "unknown")
# Sync mode: response already has the final status and data
if status == "success":
logger.reason("SQL execution completed (sync)", {
"query_id": exec_result.get("query_id"),
})
return {
"query_id": exec_result.get("query_id"),
"status": "success",
"state": "success",
"rows_affected": exec_result.get("raw_response", {}).get("query", {}).get("rows"),
"error_message": None,
"results": exec_result.get("raw_response", {}).get("data"),
"completed_on": exec_result.get("raw_response", {}).get("query", {}).get("endDttm"),
}
# For async mode (fallback): poll for completion
query_id = exec_result.get("query_id") query_id = exec_result.get("query_id")
if not query_id: if not query_id:
logger.explore("No query_id from SQL Lab execute", { logger.explore("No query_id from SQL Lab execute", {

View File

@@ -23,7 +23,8 @@ class TranslateJobCreate(BaseModel):
target_table: Optional[str] = Field(None, description="Target table name") target_table: Optional[str] = Field(None, description="Target table name")
source_key_cols: Optional[List[str]] = Field(default_factory=list, description="Source key column names") source_key_cols: Optional[List[str]] = Field(default_factory=list, description="Source key column names")
target_key_cols: Optional[List[str]] = Field(default_factory=list, description="Target key column names") target_key_cols: Optional[List[str]] = Field(default_factory=list, description="Target key column names")
translation_column: Optional[str] = Field(None, description="Column to translate") translation_column: Optional[str] = Field(None, description="Source column to translate")
target_column: Optional[str] = Field(None, description="Target column for translated output (defaults to translation_column)")
context_columns: Optional[List[str]] = Field(default_factory=list, description="Context column names") context_columns: Optional[List[str]] = Field(default_factory=list, description="Context column names")
target_language: Optional[str] = Field(None, description="Target language code") target_language: Optional[str] = Field(None, description="Target language code")
provider_id: Optional[str] = Field(None, description="LLM provider ID") provider_id: Optional[str] = Field(None, description="LLM provider ID")
@@ -31,6 +32,7 @@ class TranslateJobCreate(BaseModel):
upsert_strategy: str = Field("MERGE", description="UPSERT strategy: MERGE, INSERT, UPDATE") upsert_strategy: str = Field("MERGE", description="UPSERT strategy: MERGE, INSERT, UPDATE")
dictionary_ids: Optional[List[str]] = Field(default_factory=list, description="Associated terminology dictionary IDs") dictionary_ids: Optional[List[str]] = Field(default_factory=list, description="Associated terminology dictionary IDs")
environment_id: Optional[str] = Field(None, description="Superset environment ID") environment_id: Optional[str] = Field(None, description="Superset environment ID")
target_database_id: Optional[str] = Field(None, description="Superset database ID for SQL Lab insert target")
# #endregion TranslateJobCreate # #endregion TranslateJobCreate
@@ -49,6 +51,7 @@ class TranslateJobUpdate(BaseModel):
source_key_cols: Optional[List[str]] = None source_key_cols: Optional[List[str]] = None
target_key_cols: Optional[List[str]] = None target_key_cols: Optional[List[str]] = None
translation_column: Optional[str] = None translation_column: Optional[str] = None
target_column: Optional[str] = None
context_columns: Optional[List[str]] = None context_columns: Optional[List[str]] = None
target_language: Optional[str] = None target_language: Optional[str] = None
provider_id: Optional[str] = None provider_id: Optional[str] = None
@@ -57,6 +60,7 @@ class TranslateJobUpdate(BaseModel):
status: Optional[str] = None status: Optional[str] = None
dictionary_ids: Optional[List[str]] = None dictionary_ids: Optional[List[str]] = None
environment_id: Optional[str] = None environment_id: Optional[str] = None
target_database_id: Optional[str] = None
# #endregion TranslateJobUpdate # #endregion TranslateJobUpdate
@@ -76,6 +80,7 @@ class TranslateJobResponse(BaseModel):
source_key_cols: Optional[List[str]] = None source_key_cols: Optional[List[str]] = None
target_key_cols: Optional[List[str]] = None target_key_cols: Optional[List[str]] = None
translation_column: Optional[str] = None translation_column: Optional[str] = None
target_column: Optional[str] = None
context_columns: Optional[List[str]] = None context_columns: Optional[List[str]] = None
target_language: Optional[str] = None target_language: Optional[str] = None
provider_id: Optional[str] = None provider_id: Optional[str] = None
@@ -87,6 +92,7 @@ class TranslateJobResponse(BaseModel):
updated_at: Optional[datetime] = None updated_at: Optional[datetime] = None
dictionary_ids: Optional[List[str]] = None dictionary_ids: Optional[List[str]] = None
environment_id: Optional[str] = None environment_id: Optional[str] = None
target_database_id: Optional[str] = None
class Config: class Config:
from_attributes = True from_attributes = True

View File

@@ -1,6 +1,5 @@
# region test_encryption_manager [TYPE Module] # region test_encryption_manager [TYPE Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @SEMANTICS: encryption, security, fernet, api-keys, tests # @SEMANTICS: encryption, security, fernet, api-keys, tests
# @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality. # @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -5,7 +5,6 @@ from src.services.health_service import HealthService
from src.models.llm import ValidationRecord from src.models.llm import ValidationRecord
# region test_health_service [TYPE Module] # region test_health_service [TYPE Module]
# @COMPLEXITY: 3
# @PURPOSE: Unit tests for HealthService aggregation logic. # @PURPOSE: Unit tests for HealthService aggregation logic.
# @RELATION: VERIFIES ->[src.services.health_service.HealthService] # @RELATION: VERIFIES ->[src.services.health_service.HealthService]

View File

@@ -1,6 +1,5 @@
# region test_llm_plugin_persistence [TYPE Module] # region test_llm_plugin_persistence [TYPE Module]
# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class] # @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]
# @COMPLEXITY: 3
# @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context. # @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context.
import types import types
@@ -11,7 +10,6 @@ from src.plugins.llm_analysis import plugin as plugin_module
# region _DummyLogger [TYPE Class] # region _DummyLogger [TYPE Class]
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module] # @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
# @COMPLEXITY: 1
# @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests. # @PURPOSE: Minimal logger shim for TaskContext-like objects used in tests.
# @INVARIANT: Logging methods are no-ops and must not mutate test state. # @INVARIANT: Logging methods are no-ops and must not mutate test state.
class _DummyLogger: class _DummyLogger:
@@ -36,7 +34,6 @@ class _DummyLogger:
# region _FakeDBSession [TYPE Class] # region _FakeDBSession [TYPE Class]
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module] # @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
# @COMPLEXITY: 2
# @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin. # @PURPOSE: Captures persisted records for assertion and mimics SQLAlchemy session methods used by plugin.
# @INVARIANT: add/commit/close provide only persistence signals asserted by this test. # @INVARIANT: add/commit/close provide only persistence signals asserted by this test.
class _FakeDBSession: class _FakeDBSession:
@@ -61,7 +58,6 @@ class _FakeDBSession:
# region test_dashboard_validation_plugin_persists_task_and_environment_ids [TYPE Function] # region test_dashboard_validation_plugin_persists_task_and_environment_ids [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module] # @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class] # @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]
# @COMPLEXITY: 2
# @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id. # @PURPOSE: Ensure db ValidationRecord includes context.task_id and params.environment_id.
# @INVARIANT: Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals. # @INVARIANT: Assertions remain restricted to persisted task/environment identity fields and session lifecycle signals.
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -82,7 +78,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# region _FakeProviderService [TYPE Class] # region _FakeProviderService [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @COMPLEXITY: 1
# @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests. # @PURPOSE: LLM provider service stub returning deterministic provider and decrypted API key for plugin tests.
# @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic. # @INVARIANT: Returns same provider and key regardless of provider_id argument; no lookup logic.
class _FakeProviderService: class _FakeProviderService:
@@ -99,7 +94,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# region _FakeScreenshotService [TYPE Class] # region _FakeScreenshotService [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @COMPLEXITY: 1
# @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects. # @PURPOSE: Screenshot service stub that accepts capture_dashboard calls without side effects.
# @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values. # @INVARIANT: capture_dashboard is intentionally permissive for this persistence-focused test and does not validate argument values.
class _FakeScreenshotService: class _FakeScreenshotService:
@@ -113,7 +107,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# region _FakeLLMClient [TYPE Class] # region _FakeLLMClient [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @COMPLEXITY: 2
# @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions. # @PURPOSE: Deterministic LLM client double returning canonical analysis payload for persistence-path assertions.
# @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result. # @INVARIANT: analyze_dashboard is side-effect free and returns schema-compatible PASS result.
class _FakeLLMClient: class _FakeLLMClient:
@@ -137,7 +130,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# region _FakeNotificationService [TYPE Class] # region _FakeNotificationService [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @COMPLEXITY: 1
# @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects. # @PURPOSE: Notification service stub that accepts plugin dispatch_report payload without introducing side effects.
# @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema. # @INVARIANT: dispatch_report accepts arbitrary keyword payloads because this test verifies persistence fields, not notification payload schema.
class _FakeNotificationService: class _FakeNotificationService:
@@ -151,7 +143,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# region _FakeConfigManager [TYPE Class] # region _FakeConfigManager [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @COMPLEXITY: 1
# @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path. # @PURPOSE: Config manager stub providing storage root path and minimal settings for plugin execution path.
# @INVARIANT: Only storage.root_path and llm fields are safe to access; all other settings fields are absent. # @INVARIANT: Only storage.root_path and llm fields are safe to access; all other settings fields are absent.
class _FakeConfigManager: class _FakeConfigManager:
@@ -170,7 +161,6 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# region _FakeSupersetClient [TYPE Class] # region _FakeSupersetClient [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function] # @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @COMPLEXITY: 1
# @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list. # @PURPOSE: Superset client stub exposing network.request as a lambda that returns empty result list.
# @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency. # @INVARIANT: network.request intentionally accepts arbitrary keyword payloads because response shape, not request signature, is the persistence-path dependency.
class _FakeSupersetClient: class _FakeSupersetClient:

View File

@@ -1,5 +1,4 @@
# region test_llm_prompt_templates [TYPE Module] # region test_llm_prompt_templates [TYPE Module]
# @COMPLEXITY: 3
# @SEMANTICS: tests, llm, prompts, templates, settings # @SEMANTICS: tests, llm, prompts, templates, settings
# @PURPOSE: Validate normalization and rendering behavior for configurable LLM prompt templates. # @PURPOSE: Validate normalization and rendering behavior for configurable LLM prompt templates.
# @LAYER: Domain Tests # @LAYER: Domain Tests
@@ -19,7 +18,6 @@ from src.services.llm_prompt_templates import (
# region test_normalize_llm_settings_adds_default_prompts [TYPE Function] # region test_normalize_llm_settings_adds_default_prompts [TYPE Function]
# @RELATION: BINDS_TO -> test_llm_prompt_templates # @RELATION: BINDS_TO -> test_llm_prompt_templates
# @COMPLEXITY: 3
# @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults. # @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults.
# @PRE: Input llm settings do not contain complete prompts object. # @PRE: Input llm settings do not contain complete prompts object.
# @POST: Returned structure includes required prompt templates with fallback defaults. # @POST: Returned structure includes required prompt templates with fallback defaults.
@@ -43,7 +41,6 @@ def test_normalize_llm_settings_adds_default_prompts():
# region test_normalize_llm_settings_keeps_custom_prompt_values [TYPE Function] # region test_normalize_llm_settings_keeps_custom_prompt_values [TYPE Function]
# @RELATION: BINDS_TO -> test_llm_prompt_templates # @RELATION: BINDS_TO -> test_llm_prompt_templates
# @COMPLEXITY: 3
# @PURPOSE: Ensure user-customized prompt values are preserved during normalization. # @PURPOSE: Ensure user-customized prompt values are preserved during normalization.
# @PRE: Input llm settings contain custom prompt override. # @PRE: Input llm settings contain custom prompt override.
# @POST: Custom prompt value remains unchanged in normalized output. # @POST: Custom prompt value remains unchanged in normalized output.
@@ -59,7 +56,6 @@ def test_normalize_llm_settings_keeps_custom_prompt_values():
# region test_render_prompt_replaces_known_placeholders [TYPE Function] # region test_render_prompt_replaces_known_placeholders [TYPE Function]
# @RELATION: BINDS_TO -> test_llm_prompt_templates # @RELATION: BINDS_TO -> test_llm_prompt_templates
# @COMPLEXITY: 3
# @PURPOSE: Ensure template placeholders are deterministically replaced. # @PURPOSE: Ensure template placeholders are deterministically replaced.
# @PRE: Template contains placeholders matching provided variables. # @PRE: Template contains placeholders matching provided variables.
# @POST: Rendered prompt string contains substituted values. # @POST: Rendered prompt string contains substituted values.
@@ -77,7 +73,6 @@ def test_render_prompt_replaces_known_placeholders():
# region test_is_multimodal_model_detects_known_vision_models [TYPE Function] # region test_is_multimodal_model_detects_known_vision_models [TYPE Function]
# @RELATION: BINDS_TO -> test_llm_prompt_templates # @RELATION: BINDS_TO -> test_llm_prompt_templates
# @COMPLEXITY: 2
# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names. # @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names.
def test_is_multimodal_model_detects_known_vision_models(): def test_is_multimodal_model_detects_known_vision_models():
assert is_multimodal_model("gpt-4o") is True assert is_multimodal_model("gpt-4o") is True
@@ -91,7 +86,6 @@ def test_is_multimodal_model_detects_known_vision_models():
# region test_resolve_bound_provider_id_prefers_binding_then_default [TYPE Function] # region test_resolve_bound_provider_id_prefers_binding_then_default [TYPE Function]
# @RELATION: BINDS_TO -> test_llm_prompt_templates # @RELATION: BINDS_TO -> test_llm_prompt_templates
# @COMPLEXITY: 2
# @PURPOSE: Verify provider binding resolution priority. # @PURPOSE: Verify provider binding resolution priority.
def test_resolve_bound_provider_id_prefers_binding_then_default(): def test_resolve_bound_provider_id_prefers_binding_then_default():
settings = { settings = {
@@ -107,7 +101,6 @@ def test_resolve_bound_provider_id_prefers_binding_then_default():
# region test_normalize_llm_settings_keeps_assistant_planner_settings [TYPE Function] # region test_normalize_llm_settings_keeps_assistant_planner_settings [TYPE Function]
# @RELATION: BINDS_TO -> test_llm_prompt_templates # @RELATION: BINDS_TO -> test_llm_prompt_templates
# @COMPLEXITY: 2
# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized. # @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized.
def test_normalize_llm_settings_keeps_assistant_planner_settings(): def test_normalize_llm_settings_keeps_assistant_planner_settings():
normalized = normalize_llm_settings( normalized = normalize_llm_settings(

View File

@@ -1,6 +1,5 @@
# region test_llm_provider [TYPE Module] # region test_llm_provider [TYPE Module]
# @RELATION: VERIFIES -> [src.services.llm_provider:Module] # @RELATION: VERIFIES -> [src.services.llm_provider:Module]
# @COMPLEXITY: 3
# @SEMANTICS: tests, llm-provider, encryption, contract # @SEMANTICS: tests, llm-provider, encryption, contract
# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager # @PURPOSE: Contract testing for LLMProviderService and EncryptionManager
# endregion test_llm_provider # endregion test_llm_provider
@@ -68,7 +67,6 @@ def test_decrypt_invalid_data():
# region mock_db [TYPE Fixture] # region mock_db [TYPE Fixture]
# @RELATION: BINDS_TO -> [test_llm_provider:Module] # @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @COMPLEXITY: 1
# @PURPOSE: MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests. # @PURPOSE: MagicMock(spec=Session) fixture providing a constrained DB session double for LLMProviderService tests.
# @INVARIANT: Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced. # @INVARIANT: Chained calls beyond Session spec create unconstrained intermediate mocks; only top-level query/add/commit are spec-enforced.
@pytest.fixture @pytest.fixture
@@ -82,7 +80,6 @@ def mock_db():
# region service [TYPE Fixture] # region service [TYPE Fixture]
# @RELATION: BINDS_TO -> [test_llm_provider:Module] # @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @COMPLEXITY: 1
# @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests. # @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests.
@pytest.fixture @pytest.fixture
def service(mock_db): def service(mock_db):

View File

@@ -1,6 +1,5 @@
# region test_rbac_permission_catalog [TYPE Module] # region test_rbac_permission_catalog [TYPE Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @SEMANTICS: tests, rbac, permissions, catalog, discovery, sync # @SEMANTICS: tests, rbac, permissions, catalog, discovery, sync
# @PURPOSE: Verifies RBAC permission catalog discovery and idempotent synchronization behavior. # @PURPOSE: Verifies RBAC permission catalog discovery and idempotent synchronization behavior.
# @LAYER: Service Tests # @LAYER: Service Tests

View File

@@ -1,5 +1,4 @@
# region TestResourceService [TYPE Module] # region TestResourceService [TYPE Module]
# @COMPLEXITY: 3
# @SEMANTICS: resource-service, tests, dashboards, datasets, activity # @SEMANTICS: resource-service, tests, dashboards, datasets, activity
# @PURPOSE: Unit tests for ResourceService # @PURPOSE: Unit tests for ResourceService
# @LAYER: Service # @LAYER: Service

View File

@@ -31,7 +31,6 @@ from ..core.logger import belief_scope
# @RELATION DEPENDS_ON -> [Role] # @RELATION DEPENDS_ON -> [Role]
class AuthService: class AuthService:
# region AuthService_init [TYPE Function] # region AuthService_init [TYPE Function]
# @COMPLEXITY: 1
# @PURPOSE: Initializes the authentication service with repository access over an active DB session. # @PURPOSE: Initializes the authentication service with repository access over an active DB session.
# @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context. # @PRE: db is a valid SQLAlchemy Session instance bound to the auth persistence context.
# @POST: self.repo is initialized and ready for auth user/role CRUD operations. # @POST: self.repo is initialized and ready for auth user/role CRUD operations.
@@ -45,7 +44,6 @@ class AuthService:
# endregion AuthService_init # endregion AuthService_init
# region AuthService.authenticate_user [TYPE Function] # region AuthService.authenticate_user [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Validates credentials and account state for local username/password authentication. # @PURPOSE: Validates credentials and account state for local username/password authentication.
# @PRE: username and password are non-empty credential inputs. # @PRE: username and password are non-empty credential inputs.
# @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None. # @POST: Returns User only when user exists, is active, and password hash verification succeeds; otherwise returns None.
@@ -76,7 +74,6 @@ class AuthService:
# endregion AuthService.authenticate_user # endregion AuthService.authenticate_user
# region AuthService.create_session [TYPE Function] # region AuthService.create_session [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Issues an access token payload for an already authenticated user. # @PURPOSE: Issues an access token payload for an already authenticated user.
# @PRE: user is a valid User entity containing username and iterable roles with role.name values. # @PRE: user is a valid User entity containing username and iterable roles with role.name values.
# @POST: Returns session dict with non-empty access_token and token_type='bearer'. # @POST: Returns session dict with non-empty access_token and token_type='bearer'.
@@ -98,7 +95,6 @@ class AuthService:
# endregion AuthService.create_session # endregion AuthService.create_session
# region AuthService.provision_adfs_user [TYPE Function] # region AuthService.provision_adfs_user [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings. # @PURPOSE: Performs ADFS Just-In-Time provisioning and role synchronization from AD group mappings.
# @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent. # @PRE: user_info contains identity claims where at least one of 'upn' or 'email' is present; 'groups' may be absent.
# @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state. # @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.

View File

@@ -1,6 +1,5 @@
# region TestAuditService [TYPE Module] # region TestAuditService [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[AuditService] # @RELATION: [DEPENDS_ON] ->[AuditService]
# @COMPLEXITY: 3
# @SEMANTICS: tests, clean-release, audit, logging # @SEMANTICS: tests, clean-release, audit, logging
# @PURPOSE: Validate audit hooks emit expected log patterns for clean release lifecycle. # @PURPOSE: Validate audit hooks emit expected log patterns for clean release lifecycle.
# @LAYER: Infra # @LAYER: Infra

View File

@@ -1,6 +1,5 @@
# region TestComplianceOrchestrator [TYPE Module] # region TestComplianceOrchestrator [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[ComplianceOrchestrator] # @RELATION: [DEPENDS_ON] ->[ComplianceOrchestrator]
# @COMPLEXITY: 3
# @SEMANTICS: tests, clean-release, orchestrator, stage-state-machine # @SEMANTICS: tests, clean-release, orchestrator, stage-state-machine
# @PURPOSE: Validate compliance orchestrator stage transitions and final status derivation. # @PURPOSE: Validate compliance orchestrator stage transitions and final status derivation.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -1,5 +1,4 @@
# region TestManifestBuilder [TYPE Module] # region TestManifestBuilder [TYPE Module]
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, manifest, deterministic # @SEMANTICS: tests, clean-release, manifest, deterministic
# @PURPOSE: Validate deterministic manifest generation behavior for US1. # @PURPOSE: Validate deterministic manifest generation behavior for US1.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -1,5 +1,4 @@
# region TestPreparationService [TYPE Module] # region TestPreparationService [TYPE Module]
# @COMPLEXITY: 3
# @SEMANTICS: tests, clean-release, preparation, flow # @SEMANTICS: tests, clean-release, preparation, flow
# @PURPOSE: Validate release candidate preparation flow, including policy evaluation and manifest persisting. # @PURPOSE: Validate release candidate preparation flow, including policy evaluation and manifest persisting.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -1,6 +1,5 @@
# region TestReportBuilder [TYPE Module] # region TestReportBuilder [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[ReportBuilder] # @RELATION: [DEPENDS_ON] ->[ReportBuilder]
# @COMPLEXITY: 3
# @SEMANTICS: tests, clean-release, report-builder, counters # @SEMANTICS: tests, clean-release, report-builder, counters
# @PURPOSE: Validate compliance report builder counter integrity and blocked-run constraints. # @PURPOSE: Validate compliance report builder counter integrity and blocked-run constraints.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -1,6 +1,5 @@
# region TestSourceIsolation [TYPE Module] # region TestSourceIsolation [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[SourceIsolation] # @RELATION: [DEPENDS_ON] ->[SourceIsolation]
# @COMPLEXITY: 3
# @SEMANTICS: tests, clean-release, source-isolation, internal-only # @SEMANTICS: tests, clean-release, source-isolation, internal-only
# @PURPOSE: Verify internal source registry validation behavior. # @PURPOSE: Verify internal source registry validation behavior.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -1,6 +1,5 @@
# region TestStages [TYPE Module] # region TestStages [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[ComplianceStages] # @RELATION: [DEPENDS_ON] ->[ComplianceStages]
# @COMPLEXITY: 3
# @SEMANTICS: tests, clean-release, compliance, stages # @SEMANTICS: tests, clean-release, compliance, stages
# @PURPOSE: Validate final status derivation logic from stage results. # @PURPOSE: Validate final status derivation logic from stage results.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -19,7 +19,6 @@ class ManifestRepository:
"""Repository for distribution manifest persistence.""" """Repository for distribution manifest persistence."""
# region ManifestRepository.__init__ [TYPE Function] # region ManifestRepository.__init__ [TYPE Function]
# @COMPLEXITY: 1
# @PURPOSE: Initialize repository with an active SQLAlchemy session. # @PURPOSE: Initialize repository with an active SQLAlchemy session.
# @PRE: db is a valid SQLAlchemy Session instance. # @PRE: db is a valid SQLAlchemy Session instance.
# @POST: Repository is ready for database operations. # @POST: Repository is ready for database operations.
@@ -28,7 +27,6 @@ class ManifestRepository:
# endregion ManifestRepository.__init__ # endregion ManifestRepository.__init__
# region ManifestRepository.save [TYPE Function] # region ManifestRepository.save [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Persist a DistributionManifest to the database. # @PURPOSE: Persist a DistributionManifest to the database.
# @PRE: manifest is a valid DistributionManifest instance with required fields populated. # @PRE: manifest is a valid DistributionManifest instance with required fields populated.
# @POST: Manifest is committed to database and refreshed with generated ID. # @POST: Manifest is committed to database and refreshed with generated ID.
@@ -43,7 +41,6 @@ class ManifestRepository:
# endregion ManifestRepository.save # endregion ManifestRepository.save
# region ManifestRepository.get_by_id [TYPE Function] # region ManifestRepository.get_by_id [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Retrieve a single DistributionManifest by its primary key. # @PURPOSE: Retrieve a single DistributionManifest by its primary key.
# @PRE: manifest_id is a valid string identifier. # @PRE: manifest_id is a valid string identifier.
# @POST: Returns DistributionManifest if found, None otherwise. # @POST: Returns DistributionManifest if found, None otherwise.
@@ -56,7 +53,6 @@ class ManifestRepository:
# endregion ManifestRepository.get_by_id # endregion ManifestRepository.get_by_id
# region ManifestRepository.get_latest_for_candidate [TYPE Function] # region ManifestRepository.get_latest_for_candidate [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Retrieve the most recent manifest version for a given candidate. # @PURPOSE: Retrieve the most recent manifest version for a given candidate.
# @PRE: candidate_id is a valid string identifier. # @PRE: candidate_id is a valid string identifier.
# @POST: Returns the highest manifest_version manifest for the candidate, or None. # @POST: Returns the highest manifest_version manifest for the candidate, or None.
@@ -72,7 +68,6 @@ class ManifestRepository:
# endregion ManifestRepository.get_latest_for_candidate # endregion ManifestRepository.get_latest_for_candidate
# region ManifestRepository.list_by_candidate [TYPE Function] # region ManifestRepository.list_by_candidate [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: List all manifests for a specific candidate, ordered by version. # @PURPOSE: List all manifests for a specific candidate, ordered by version.
# @PRE: candidate_id is a valid string identifier. # @PRE: candidate_id is a valid string identifier.
# @POST: Returns a list of DistributionManifest instances (may be empty). # @POST: Returns a list of DistributionManifest instances (may be empty).

View File

@@ -105,7 +105,6 @@ class ClarificationAnswerCommand:
# @SIDE_EFFECT: Mutates clarification answers, session flags, and related clarification findings. # @SIDE_EFFECT: Mutates clarification answers, session flags, and related clarification findings.
class ClarificationEngine: class ClarificationEngine:
# region ClarificationEngine_init [TYPE Function] # region ClarificationEngine_init [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Bind repository dependency for clarification persistence operations. # @PURPOSE: Bind repository dependency for clarification persistence operations.
def __init__(self, repository: DatasetReviewSessionRepository) -> None: def __init__(self, repository: DatasetReviewSessionRepository) -> None:
self.repository = repository self.repository = repository
@@ -113,7 +112,6 @@ class ClarificationEngine:
# endregion ClarificationEngine_init # endregion ClarificationEngine_init
# region build_question_payload [TYPE Function] # region build_question_payload [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Return the one active highest-priority clarification question payload. # @PURPOSE: Return the one active highest-priority clarification question payload.
# @PRE: Session contains unresolved clarification state or a resumable clarification session. # @PRE: Session contains unresolved clarification state or a resumable clarification session.
# @POST: Returns exactly one active/open question payload or None when no unresolved question remains. # @POST: Returns exactly one active/open question payload or None when no unresolved question remains.
@@ -173,7 +171,6 @@ class ClarificationEngine:
# endregion build_question_payload # endregion build_question_payload
# region record_answer [TYPE Function] # region record_answer [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Persist one clarification answer before any pointer/readiness mutation. # @PURPOSE: Persist one clarification answer before any pointer/readiness mutation.
# @PRE: Target question belongs to the session's active clarification session and is still open. # @PRE: Target question belongs to the session's active clarification session and is still open.
# @POST: Answer row is persisted before current-question pointer advances. # @POST: Answer row is persisted before current-question pointer advances.
@@ -260,7 +257,6 @@ class ClarificationEngine:
# endregion record_answer # endregion record_answer
# region summarize_progress [TYPE Function] # region summarize_progress [TYPE Function]
# @COMPLEXITY: 1
# @PURPOSE: Produce a compact progress summary for pause/resume and completion UX. # @PURPOSE: Produce a compact progress summary for pause/resume and completion UX.
def summarize_progress(self, clarification_session: ClarificationSession) -> str: def summarize_progress(self, clarification_session: ClarificationSession) -> str:
resolved = count_resolved_questions(clarification_session) resolved = count_resolved_questions(clarification_session)
@@ -270,7 +266,6 @@ class ClarificationEngine:
# endregion summarize_progress # endregion summarize_progress
# region _get_latest_clarification_session [TYPE Function] # region _get_latest_clarification_session [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Select the latest clarification session for the current dataset review aggregate. # @PURPOSE: Select the latest clarification session for the current dataset review aggregate.
def _get_latest_clarification_session(self, session: DatasetReviewSession) -> Optional[ClarificationSession]: def _get_latest_clarification_session(self, session: DatasetReviewSession) -> Optional[ClarificationSession]:
if not session.clarification_sessions: if not session.clarification_sessions:
@@ -281,7 +276,6 @@ class ClarificationEngine:
# endregion _get_latest_clarification_session # endregion _get_latest_clarification_session
# region _find_question [TYPE Function] # region _find_question [TYPE Function]
# @COMPLEXITY: 1
# @PURPOSE: Resolve a clarification question from the active clarification aggregate. # @PURPOSE: Resolve a clarification question from the active clarification aggregate.
def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> Optional[ClarificationQuestion]: def _find_question(self, clarification_session: ClarificationSession, question_id: str) -> Optional[ClarificationQuestion]:
for q in clarification_session.questions: for q in clarification_session.questions:

View File

@@ -45,14 +45,12 @@ class SessionEventPayload:
# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent] # @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]
class SessionEventLogger: class SessionEventLogger:
# region SessionEventLogger_init [TYPE Function] # region SessionEventLogger_init [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Bind a live SQLAlchemy session to the session-event logger. # @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.
def __init__(self, db: Session) -> None: def __init__(self, db: Session) -> None:
self.db = db self.db = db
# endregion SessionEventLogger_init # endregion SessionEventLogger_init
# region log_event [TYPE Function] # region log_event [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation. # @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.
# @RELATION: [DEPENDS_ON] ->[SessionEvent] # @RELATION: [DEPENDS_ON] ->[SessionEvent]
# @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty. # @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.
@@ -125,7 +123,6 @@ class SessionEventLogger:
# endregion log_event # endregion log_event
# region log_for_session [TYPE Function] # region log_for_session [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root. # @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.
# @RELATION: [CALLS] ->[SessionEventLogger.log_event] # @RELATION: [CALLS] ->[SessionEventLogger.log_event]
def log_for_session( def log_for_session(

View File

@@ -105,7 +105,6 @@ logger = cast(Any, logger)
# @INVARIANT: session ownership is preserved on every mutation and recovery remains explicit when partial. # @INVARIANT: session ownership is preserved on every mutation and recovery remains explicit when partial.
class DatasetReviewOrchestrator: class DatasetReviewOrchestrator:
# region DatasetReviewOrchestrator_init [TYPE Function] # region DatasetReviewOrchestrator_init [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary. # @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.
# @PRE: repository/config_manager are valid collaborators for the current request scope. # @PRE: repository/config_manager are valid collaborators for the current request scope.
# @POST: Instance holds collaborator references used by start/preview/launch orchestration methods. # @POST: Instance holds collaborator references used by start/preview/launch orchestration methods.
@@ -124,7 +123,6 @@ class DatasetReviewOrchestrator:
# endregion DatasetReviewOrchestrator_init # endregion DatasetReviewOrchestrator_init
# region start_session [TYPE Function] # region start_session [TYPE Function]
# @COMPLEXITY: 5
# @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery. # @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery.
# @RELATION: CALLS -> [SupersetContextExtractor.parse_superset_link] # @RELATION: CALLS -> [SupersetContextExtractor.parse_superset_link]
# @RELATION: CALLS -> [TaskManager.create_task] # @RELATION: CALLS -> [TaskManager.create_task]
@@ -279,7 +277,6 @@ class DatasetReviewOrchestrator:
# endregion start_session # endregion start_session
# region prepare_launch_preview [TYPE Function] # region prepare_launch_preview [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation. # @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.
# @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview] # @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview]
# @PRE: all required variables have candidate values or explicitly accepted defaults. # @PRE: all required variables have candidate values or explicitly accepted defaults.
@@ -359,7 +356,6 @@ class DatasetReviewOrchestrator:
# endregion prepare_launch_preview # endregion prepare_launch_preview
# region launch_dataset [TYPE Function] # region launch_dataset [TYPE Function]
# @COMPLEXITY: 5
# @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay. # @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay.
# @RELATION: CALLS -> [SupersetCompilationAdapter.create_sql_lab_session] # @RELATION: CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]
# @PRE: session is run-ready and compiled preview is current. # @PRE: session is run-ready and compiled preview is current.
@@ -459,7 +455,6 @@ class DatasetReviewOrchestrator:
# endregion launch_dataset # endregion launch_dataset
# region _build_recovery_bootstrap [TYPE Function] # region _build_recovery_bootstrap [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation. # @PURPOSE: Recover and materialize initial imported filters, template variables, and draft execution mappings after session creation.
# @PRE: session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope. # @PRE: session belongs to the just-created review aggregate and parsed_context was produced for the same environment scope.
# @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly. # @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.
@@ -559,7 +554,6 @@ class DatasetReviewOrchestrator:
# endregion _build_recovery_bootstrap # endregion _build_recovery_bootstrap
# region _enqueue_recovery_task [TYPE Function] # region _enqueue_recovery_task [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Link session start to observable async recovery when task infrastructure is available. # @PURPOSE: Link session start to observable async recovery when task infrastructure is available.
# @PRE: session is already persisted. # @PRE: session is already persisted.
# @POST: returns task identifier when a task could be enqueued, otherwise None. # @POST: returns task identifier when a task could be enqueued, otherwise None.

View File

@@ -28,14 +28,12 @@ from src.services.dataset_review.repositories.session_repository import (
# region SessionRepositoryTests [TYPE Module] # region SessionRepositoryTests [TYPE Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 2
# @PURPOSE: Unit tests for DatasetReviewSessionRepository. # @PURPOSE: Unit tests for DatasetReviewSessionRepository.
@pytest.fixture @pytest.fixture
def db_session(): def db_session():
# region db_session [TYPE Function] # region db_session [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows. # @PURPOSE: Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows.
# @RELATION: BINDS_TO -> [SessionRepositoryTests] # @RELATION: BINDS_TO -> [SessionRepositoryTests]
engine = create_engine("sqlite:///:memory:") engine = create_engine("sqlite:///:memory:")

View File

@@ -64,7 +64,6 @@ class DatasetReviewSessionVersionConflictError(ValueError):
# @SIDE_EFFECT: mutates and queries the persistence layer through the injected database session. # @SIDE_EFFECT: mutates and queries the persistence layer through the injected database session.
class DatasetReviewSessionRepository: class DatasetReviewSessionRepository:
# region init_repo [TYPE Function] # region init_repo [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Bind one live SQLAlchemy session to the repository instance. # @PURPOSE: Bind one live SQLAlchemy session to the repository instance.
# @PRE: db_session is not None # @PRE: db_session is not None
# @POST: Repository instance initialized with valid session # @POST: Repository instance initialized with valid session
@@ -75,7 +74,6 @@ class DatasetReviewSessionRepository:
# endregion init_repo # endregion init_repo
# region get_owned_session [TYPE Function] # region get_owned_session [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths. # @PURPOSE: Resolve one owner-scoped dataset review session for mutation paths.
# @PRE: session_id and user_id are non-empty identifiers from the authenticated ownership scope. # @PRE: session_id and user_id are non-empty identifiers from the authenticated ownership scope.
# @POST: returns the owned session or raises a deterministic access error. # @POST: returns the owned session or raises a deterministic access error.
@@ -96,7 +94,6 @@ class DatasetReviewSessionRepository:
# endregion get_owned_session # endregion get_owned_session
# region create_sess [TYPE Function] # region create_sess [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Persist an initial dataset review session shell. # @PURPOSE: Persist an initial dataset review session shell.
# @POST: session is committed, refreshed, and returned with persisted identifiers. # @POST: session is committed, refreshed, and returned with persisted identifiers.
def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession: def create_session(self, session: DatasetReviewSession) -> DatasetReviewSession:
@@ -111,7 +108,6 @@ class DatasetReviewSessionRepository:
# endregion create_sess # endregion create_sess
# region require_session_version [TYPE Function] # region require_session_version [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted. # @PURPOSE: Enforce optimistic-lock version matching before a session mutation is persisted.
# @POST: returns the same session when versions match; otherwise raises deterministic conflict error. # @POST: returns the same session when versions match; otherwise raises deterministic conflict error.
def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession: def require_session_version(self, session: DatasetReviewSession, expected_version: int) -> DatasetReviewSession:
@@ -127,7 +123,6 @@ class DatasetReviewSessionRepository:
# endregion require_session_version # endregion require_session_version
# region bump_session_version [TYPE Function] # region bump_session_version [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled. # @PURPOSE: Increment optimistic-lock version after a successful session mutation is assembled.
# @POST: session version increments monotonically. # @POST: session version increments monotonically.
def bump_session_version(self, session: DatasetReviewSession) -> int: def bump_session_version(self, session: DatasetReviewSession) -> int:
@@ -141,7 +136,6 @@ class DatasetReviewSessionRepository:
# endregion bump_session_version # endregion bump_session_version
# region commit_session_mutation [TYPE Function] # region commit_session_mutation [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts. # @PURPOSE: Commit one prepared session mutation and translate stale writes into deterministic conflicts.
# @POST: session mutation is committed with one version increment or a deterministic conflict error is raised. # @POST: session mutation is committed with one version increment or a deterministic conflict error is raised.
def commit_session_mutation( def commit_session_mutation(
@@ -168,7 +162,6 @@ class DatasetReviewSessionRepository:
# endregion commit_session_mutation # endregion commit_session_mutation
# region load_detail [TYPE Function] # region load_detail [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Return the full session aggregate for API and frontend resume flows. # @PURPOSE: Return the full session aggregate for API and frontend resume flows.
# @POST: Returns SessionDetail with all fields populated or None. # @POST: Returns SessionDetail with all fields populated or None.
def load_session_detail(self, session_id: str, user_id: str) -> Optional[DatasetReviewSession]: def load_session_detail(self, session_id: str, user_id: str) -> Optional[DatasetReviewSession]:
@@ -202,7 +195,6 @@ class DatasetReviewSessionRepository:
# endregion load_detail # endregion load_detail
# region save_profile_and_findings [TYPE Function] # region save_profile_and_findings [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Persist profile state and replace validation findings for an owned session. # @PURPOSE: Persist profile state and replace validation findings for an owned session.
# @POST: stored profile matches the current session and findings are replaced. # @POST: stored profile matches the current session and findings are replaced.
def save_profile_and_findings( def save_profile_and_findings(
@@ -217,7 +209,6 @@ class DatasetReviewSessionRepository:
# endregion save_profile_and_findings # endregion save_profile_and_findings
# region save_recovery_state [TYPE Function] # region save_recovery_state [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Persist imported filters, template variables, and initial execution mappings. # @PURPOSE: Persist imported filters, template variables, and initial execution mappings.
def save_recovery_state( def save_recovery_state(
self, session_id: str, user_id: str, imported_filters: List[ImportedFilter], self, session_id: str, user_id: str, imported_filters: List[ImportedFilter],
@@ -234,7 +225,6 @@ class DatasetReviewSessionRepository:
# endregion save_recovery_state # endregion save_recovery_state
# region save_preview [TYPE Function] # region save_preview [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Persist a preview snapshot and mark prior session previews stale. # @PURPOSE: Persist a preview snapshot and mark prior session previews stale.
def save_preview( def save_preview(
self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: Optional[int] = None, self, session_id: str, user_id: str, preview: CompiledPreview, expected_version: Optional[int] = None,
@@ -248,7 +238,6 @@ class DatasetReviewSessionRepository:
# endregion save_preview # endregion save_preview
# region save_run_context [TYPE Function] # region save_run_context [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Persist an immutable launch audit snapshot for an owned session. # @PURPOSE: Persist an immutable launch audit snapshot for an owned session.
def save_run_context( def save_run_context(
self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: Optional[int] = None, self, session_id: str, user_id: str, run_context: DatasetRunContext, expected_version: Optional[int] = None,
@@ -262,7 +251,6 @@ class DatasetReviewSessionRepository:
# endregion save_run_context # endregion save_run_context
# region list_user_sess [TYPE Function] # region list_user_sess [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: List review sessions owned by a specific user ordered by most recent update. # @PURPOSE: List review sessions owned by a specific user ordered by most recent update.
def list_sessions_for_user(self, user_id: str) -> List[DatasetReviewSession]: def list_sessions_for_user(self, user_id: str) -> List[DatasetReviewSession]:
with belief_scope("DatasetReviewSessionRepository.list_sessions_for_user"): with belief_scope("DatasetReviewSessionRepository.list_sessions_for_user"):

View File

@@ -47,14 +47,12 @@ class DictionaryResolutionResult:
# @SIDE_EFFECT: emits semantic trace logs for ranking and fallback decisions. # @SIDE_EFFECT: emits semantic trace logs for ranking and fallback decisions.
class SemanticSourceResolver: class SemanticSourceResolver:
# region resolve_from_file [TYPE Function] # region resolve_from_file [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Normalize uploaded semantic file records into field-level candidates. # @PURPOSE: Normalize uploaded semantic file records into field-level candidates.
def resolve_from_file(self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]]) -> DictionaryResolutionResult: def resolve_from_file(self, source_payload: Mapping[str, Any], fields: Iterable[Mapping[str, Any]]) -> DictionaryResolutionResult:
return DictionaryResolutionResult(source_ref=str(source_payload.get("source_ref") or "uploaded_file")) return DictionaryResolutionResult(source_ref=str(source_payload.get("source_ref") or "uploaded_file"))
# endregion resolve_from_file # endregion resolve_from_file
# region resolve_from_dictionary [TYPE Function] # region resolve_from_dictionary [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Resolve candidates from connected tabular dictionary sources. # @PURPOSE: Resolve candidates from connected tabular dictionary sources.
# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry] # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]
# @RELATION: [DEPENDS_ON] ->[SemanticCandidate] # @RELATION: [DEPENDS_ON] ->[SemanticCandidate]
@@ -212,7 +210,6 @@ class SemanticSourceResolver:
# endregion resolve_from_dictionary # endregion resolve_from_dictionary
# region resolve_from_reference_dataset [TYPE Function] # region resolve_from_reference_dataset [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Reuse semantic metadata from trusted Superset datasets. # @PURPOSE: Reuse semantic metadata from trusted Superset datasets.
def resolve_from_reference_dataset( def resolve_from_reference_dataset(
self, self,
@@ -223,7 +220,6 @@ class SemanticSourceResolver:
# endregion resolve_from_reference_dataset # endregion resolve_from_reference_dataset
# region rank_candidates [TYPE Function] # region rank_candidates [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Apply confidence ordering and determine best candidate per field. # @PURPOSE: Apply confidence ordering and determine best candidate per field.
# @RELATION: [DEPENDS_ON] ->[SemanticCandidate] # @RELATION: [DEPENDS_ON] ->[SemanticCandidate]
def rank_candidates(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def rank_candidates(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
@@ -241,14 +237,12 @@ class SemanticSourceResolver:
# endregion rank_candidates # endregion rank_candidates
# region detect_conflicts [TYPE Function] # region detect_conflicts [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Mark competing candidate sets that require explicit user review. # @PURPOSE: Mark competing candidate sets that require explicit user review.
def detect_conflicts(self, candidates: List[Dict[str, Any]]) -> bool: def detect_conflicts(self, candidates: List[Dict[str, Any]]) -> bool:
return len(candidates) > 1 return len(candidates) > 1
# endregion detect_conflicts # endregion detect_conflicts
# region apply_field_decision [TYPE Function] # region apply_field_decision [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Accept, reject, or manually override a field-level semantic value. # @PURPOSE: Accept, reject, or manually override a field-level semantic value.
def apply_field_decision(self, field_state: Mapping[str, Any], decision: Mapping[str, Any]) -> Dict[str, Any]: def apply_field_decision(self, field_state: Mapping[str, Any], decision: Mapping[str, Any]) -> Dict[str, Any]:
merged = dict(field_state) merged = dict(field_state)
@@ -257,7 +251,6 @@ class SemanticSourceResolver:
# endregion apply_field_decision # endregion apply_field_decision
# region propagate_source_version_update [TYPE Function] # region propagate_source_version_update [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values. # @PURPOSE: Propagate a semantic source version change to unlocked field entries without silently overwriting manual or locked values.
# @RELATION: [DEPENDS_ON] ->[SemanticSource] # @RELATION: [DEPENDS_ON] ->[SemanticSource]
# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry] # @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]
@@ -314,7 +307,6 @@ class SemanticSourceResolver:
# endregion propagate_source_version_update # endregion propagate_source_version_update
# region _normalize_dictionary_row [TYPE Function] # region _normalize_dictionary_row [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Normalize one dictionary row into a consistent lookup structure. # @PURPOSE: Normalize one dictionary row into a consistent lookup structure.
def _normalize_dictionary_row(self, row: Mapping[str, Any]) -> Dict[str, Any]: def _normalize_dictionary_row(self, row: Mapping[str, Any]) -> Dict[str, Any]:
field_name = ( field_name = (
@@ -334,7 +326,6 @@ class SemanticSourceResolver:
# endregion _normalize_dictionary_row # endregion _normalize_dictionary_row
# region _find_fuzzy_matches [TYPE Function] # region _find_fuzzy_matches [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Produce confidence-scored fuzzy matches while keeping them reviewable. # @PURPOSE: Produce confidence-scored fuzzy matches while keeping them reviewable.
def _find_fuzzy_matches(self, field_name: str, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: def _find_fuzzy_matches(self, field_name: str, rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
normalized_target = self._normalize_key(field_name) normalized_target = self._normalize_key(field_name)
@@ -352,7 +343,6 @@ class SemanticSourceResolver:
# endregion _find_fuzzy_matches # endregion _find_fuzzy_matches
# region _build_candidate_payload [TYPE Function] # region _build_candidate_payload [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Project normalized dictionary rows into semantic candidate payloads. # @PURPOSE: Project normalized dictionary rows into semantic candidate payloads.
def _build_candidate_payload( def _build_candidate_payload(
self, self,
@@ -373,7 +363,6 @@ class SemanticSourceResolver:
# endregion _build_candidate_payload # endregion _build_candidate_payload
# region _match_priority [TYPE Function] # region _match_priority [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention. # @PURPOSE: Encode trusted-confidence ordering so exact dictionary reuse beats fuzzy invention.
def _match_priority(self, match_type: Optional[str]) -> int: def _match_priority(self, match_type: Optional[str]) -> int:
priority = { priority = {
@@ -386,7 +375,6 @@ class SemanticSourceResolver:
# endregion _match_priority # endregion _match_priority
# region _normalize_key [TYPE Function] # region _normalize_key [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Normalize field identifiers for stable exact/fuzzy comparisons. # @PURPOSE: Normalize field identifiers for stable exact/fuzzy comparisons.
def _normalize_key(self, value: str) -> str: def _normalize_key(self, value: str) -> str:
return "".join(ch for ch in str(value or "").strip().lower() if ch.isalnum() or ch == "_") return "".join(ch for ch in str(value or "").strip().lower() if ch.isalnum() or ch == "_")

View File

@@ -13,7 +13,6 @@ from src.core.logger import logger, belief_scope
# @BRIEF Mixin providing repository status, diff, and commit history for GitService. # @BRIEF Mixin providing repository status, diff, and commit history for GitService.
class GitServiceStatusMixin: class GitServiceStatusMixin:
# region _parse_status_porcelain [TYPE Function] # region _parse_status_porcelain [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists. # @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists.
# @PRE: `repo` is an open GitPython Repo instance. # @PRE: `repo` is an open GitPython Repo instance.
# @POST: Returns (staged, modified, untracked) tuple of file path lists. # @POST: Returns (staged, modified, untracked) tuple of file path lists.

View File

@@ -1,5 +1,4 @@
# #region git_service [C:1] [TYPE Module:Tombstone] [SEMANTICS git, service, shim, re-export, decomissioned] # #region git_service [C:1] [TYPE Module:Tombstone] [SEMANTICS git, service, shim, re-export, decomissioned]
# @COMPLEXITY: 1
# @BRIEF Re-export shim — GitService has been decomposed into services/git/ package. # @BRIEF Re-export shim — GitService has been decomposed into services/git/ package.
# All consumers continue to import from this same path without changes. # All consumers continue to import from this same path without changes.
# @RELATION REDIRECTS_TO -> [GitServiceModule] # @RELATION REDIRECTS_TO -> [GitServiceModule]

View File

@@ -46,7 +46,6 @@ class HealthService:
""" """
# region HealthService_init [TYPE Function] # region HealthService_init [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution. # @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.
# @PRE: db is a valid SQLAlchemy session. # @PRE: db is a valid SQLAlchemy session.
# @POST: Service is ready to aggregate summaries and delete health reports. # @POST: Service is ready to aggregate summaries and delete health reports.
@@ -61,7 +60,6 @@ class HealthService:
# endregion HealthService_init # endregion HealthService_init
# region _prime_dashboard_meta_cache [TYPE Function] # region _prime_dashboard_meta_cache [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment. # @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.
# @PRE: records may contain mixed numeric and slug dashboard identifiers. # @PRE: records may contain mixed numeric and slug dashboard identifiers.
# @POST: Numeric dashboard ids for known environments are cached when discoverable. # @POST: Numeric dashboard ids for known environments are cached when discoverable.
@@ -153,7 +151,6 @@ class HealthService:
# endregion _prime_dashboard_meta_cache # endregion _prime_dashboard_meta_cache
# region _resolve_dashboard_meta [TYPE Function] # region _resolve_dashboard_meta [TYPE Function]
# @COMPLEXITY: 1
# @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record. # @PURPOSE: Resolve slug/title for a dashboard referenced by persisted validation record.
# @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty. # @PRE: dashboard_id may be numeric or slug-like; environment_id may be empty.
# @POST: Returns dict with `slug` and `title` keys, using cache when possible. # @POST: Returns dict with `slug` and `title` keys, using cache when possible.
@@ -184,7 +181,6 @@ class HealthService:
# endregion _resolve_dashboard_meta # endregion _resolve_dashboard_meta
# region get_health_summary [TYPE Function] # region get_health_summary [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title. # @PURPOSE: Aggregate latest validation status per dashboard and enrich rows with dashboard slug/title.
# @PRE: environment_id may be omitted to aggregate across all environments. # @PRE: environment_id may be omitted to aggregate across all environments.
# @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard. # @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard.
@@ -288,7 +284,6 @@ class HealthService:
# endregion get_health_summary # endregion get_health_summary
# region delete_validation_report [TYPE Function] # region delete_validation_report [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts. # @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.
# @PRE: record_id is a validation record identifier. # @PRE: record_id is a validation record identifier.
# @POST: Returns True only when a matching record was deleted. # @POST: Returns True only when a matching record was deleted.

View File

@@ -123,7 +123,6 @@ class LLMProviderService:
# endregion LLMProviderService_init # endregion LLMProviderService_init
# region get_all_providers [TYPE Function] # region get_all_providers [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Returns all configured LLM providers. # @PURPOSE: Returns all configured LLM providers.
# @PRE: Database connection must be active. # @PRE: Database connection must be active.
# @POST: Returns list of all LLMProvider records. # @POST: Returns list of all LLMProvider records.
@@ -135,7 +134,6 @@ class LLMProviderService:
# endregion get_all_providers # endregion get_all_providers
# region get_provider [TYPE Function] # region get_provider [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Returns a single LLM provider by ID. # @PURPOSE: Returns a single LLM provider by ID.
# @PRE: provider_id must be a valid string. # @PRE: provider_id must be a valid string.
# @POST: Returns LLMProvider or None if not found. # @POST: Returns LLMProvider or None if not found.
@@ -149,7 +147,6 @@ class LLMProviderService:
# endregion get_provider # endregion get_provider
# region create_provider [TYPE Function] # region create_provider [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Creates a new LLM provider with encrypted API key. # @PURPOSE: Creates a new LLM provider with encrypted API key.
# @PRE: config must contain valid provider configuration. # @PRE: config must contain valid provider configuration.
# @POST: New provider created and persisted to database. # @POST: New provider created and persisted to database.
@@ -175,7 +172,6 @@ class LLMProviderService:
# endregion create_provider # endregion create_provider
# region update_provider [TYPE Function] # region update_provider [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Updates an existing LLM provider. # @PURPOSE: Updates an existing LLM provider.
# @PRE: provider_id must exist, config must be valid. # @PRE: provider_id must exist, config must be valid.
# @POST: Provider updated and persisted to database. # @POST: Provider updated and persisted to database.
@@ -210,7 +206,6 @@ class LLMProviderService:
# endregion update_provider # endregion update_provider
# region delete_provider [TYPE Function] # region delete_provider [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Deletes an LLM provider. # @PURPOSE: Deletes an LLM provider.
# @PRE: provider_id must exist. # @PRE: provider_id must exist.
# @POST: Provider removed from database. # @POST: Provider removed from database.
@@ -227,7 +222,6 @@ class LLMProviderService:
# endregion delete_provider # endregion delete_provider
# region get_decrypted_api_key [TYPE Function] # region get_decrypted_api_key [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Returns the decrypted API key for a provider. # @PURPOSE: Returns the decrypted API key for a provider.
# @PRE: provider_id must exist with valid encrypted key. # @PRE: provider_id must exist with valid encrypted key.
# @POST: Returns decrypted API key or None on failure. # @POST: Returns decrypted API key or None on failure.

View File

@@ -28,7 +28,6 @@ from ..core.utils.matching import suggest_mappings
class MappingService: class MappingService:
# region init [TYPE Function] # region init [TYPE Function]
# @PURPOSE: Initializes the mapping service with a config manager. # @PURPOSE: Initializes the mapping service with a config manager.
# @COMPLEXITY: 3
# @PRE: config_manager is provided. # @PRE: config_manager is provided.
# @PARAM: config_manager (ConfigManager) - The configuration manager. # @PARAM: config_manager (ConfigManager) - The configuration manager.
# @POST: Service is initialized. # @POST: Service is initialized.
@@ -41,7 +40,6 @@ class MappingService:
# region _get_client [TYPE Function] # region _get_client [TYPE Function]
# @PURPOSE: Helper to get an initialized SupersetClient for an environment. # @PURPOSE: Helper to get an initialized SupersetClient for an environment.
# @COMPLEXITY: 3
# @PARAM: env_id (str) - The ID of the environment. # @PARAM: env_id (str) - The ID of the environment.
# @PRE: environment must exist in config. # @PRE: environment must exist in config.
# @POST: Returns an initialized SupersetClient. # @POST: Returns an initialized SupersetClient.
@@ -60,7 +58,6 @@ class MappingService:
# region get_suggestions [TYPE Function] # region get_suggestions [TYPE Function]
# @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions. # @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.
# @COMPLEXITY: 3
# @PARAM: source_env_id (str) - Source environment ID. # @PARAM: source_env_id (str) - Source environment ID.
# @PARAM: target_env_id (str) - Target environment ID. # @PARAM: target_env_id (str) - Target environment ID.
# @PRE: Both environments must be accessible. # @PRE: Both environments must be accessible.

View File

@@ -1,5 +1,4 @@
# region test_notification_service [TYPE Module] # region test_notification_service [TYPE Module]
# @COMPLEXITY: 2
# @PURPOSE: Unit tests for NotificationService routing and dispatch logic. # @PURPOSE: Unit tests for NotificationService routing and dispatch logic.
# @RELATION: TESTS ->[NotificationService:Class] # @RELATION: TESTS ->[NotificationService:Class]

View File

@@ -44,7 +44,6 @@ from .providers import (
# @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None] # @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]
class NotificationService: class NotificationService:
# region NotificationService_init [TYPE Function] # region NotificationService_init [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Bind DB and configuration collaborators used for provider initialization and routing. # @PURPOSE: Bind DB and configuration collaborators used for provider initialization and routing.
# @RELATION: [BINDS_TO] ->[NotificationService] # @RELATION: [BINDS_TO] ->[NotificationService]
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy] # @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
@@ -57,7 +56,6 @@ class NotificationService:
# endregion NotificationService_init # endregion NotificationService_init
# region _initialize_providers [TYPE Function] # region _initialize_providers [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Materialize configured notification channel adapters once per service lifetime. # @PURPOSE: Materialize configured notification channel adapters once per service lifetime.
# @RELATION: [DEPENDS_ON] ->[SMTPProvider] # @RELATION: [DEPENDS_ON] ->[SMTPProvider]
# @RELATION: [DEPENDS_ON] ->[TelegramProvider] # @RELATION: [DEPENDS_ON] ->[TelegramProvider]
@@ -83,7 +81,6 @@ class NotificationService:
# endregion _initialize_providers # endregion _initialize_providers
# region dispatch_report [TYPE Function] # region dispatch_report [TYPE Function]
# @COMPLEXITY: 4
# @PURPOSE: Route one validation record to resolved owners and configured custom channels. # @PURPOSE: Route one validation record to resolved owners and configured custom channels.
# @RELATION: [CALLS] ->[_initialize_providers] # @RELATION: [CALLS] ->[_initialize_providers]
# @RELATION: [CALLS] ->[_should_notify] # @RELATION: [CALLS] ->[_should_notify]
@@ -143,7 +140,6 @@ class NotificationService:
# endregion dispatch_report # endregion dispatch_report
# region _should_notify [TYPE Function] # region _should_notify [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Evaluate record status against effective alert policy. # @PURPOSE: Evaluate record status against effective alert policy.
# @RELATION: [DEPENDS_ON] ->[ValidationRecord] # @RELATION: [DEPENDS_ON] ->[ValidationRecord]
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy] # @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
@@ -161,7 +157,6 @@ class NotificationService:
# endregion _should_notify # endregion _should_notify
# region _resolve_targets [TYPE Function] # region _resolve_targets [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Resolve owner and policy-defined delivery targets for one validation record. # @PURPOSE: Resolve owner and policy-defined delivery targets for one validation record.
# @RELATION: [CALLS] ->[_find_dashboard_owners] # @RELATION: [CALLS] ->[_find_dashboard_owners]
# @RELATION: [DEPENDS_ON] ->[ValidationRecord] # @RELATION: [DEPENDS_ON] ->[ValidationRecord]
@@ -198,7 +193,6 @@ class NotificationService:
# endregion _resolve_targets # endregion _resolve_targets
# region _find_dashboard_owners [TYPE Function] # region _find_dashboard_owners [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Load candidate dashboard owners from persisted profile preferences. # @PURPOSE: Load candidate dashboard owners from persisted profile preferences.
# @RELATION: [DEPENDS_ON] ->[ValidationRecord] # @RELATION: [DEPENDS_ON] ->[ValidationRecord]
# @RELATION: [DEPENDS_ON] ->[UserDashboardPreference] # @RELATION: [DEPENDS_ON] ->[UserDashboardPreference]
@@ -220,7 +214,6 @@ class NotificationService:
# endregion _find_dashboard_owners # endregion _find_dashboard_owners
# region _build_body [TYPE Function] # region _build_body [TYPE Function]
# @COMPLEXITY: 2
# @PURPOSE: Format one validation record into provider-ready body text. # @PURPOSE: Format one validation record into provider-ready body text.
# @RELATION: [DEPENDS_ON] ->[ValidationRecord] # @RELATION: [DEPENDS_ON] ->[ValidationRecord]
def _build_body(self, record: ValidationRecord) -> str: def _build_body(self, record: ValidationRecord) -> str:

View File

@@ -1,5 +1,4 @@
# region test_report_normalizer [TYPE Module] # region test_report_normalizer [TYPE Module]
# @COMPLEXITY: 2
# @SEMANTICS: tests, reports, normalizer, fallback # @SEMANTICS: tests, reports, normalizer, fallback
# @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior. # @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior.
# @RELATION: TESTS ->[normalize_report:Function] # @RELATION: TESTS ->[normalize_report:Function]

View File

@@ -1,5 +1,4 @@
# region test_report_service [TYPE Module] # region test_report_service [TYPE Module]
# @COMPLEXITY: 2
# @PURPOSE: Unit tests for ReportsService list/detail operations # @PURPOSE: Unit tests for ReportsService list/detail operations
# @RELATION: TESTS ->[ReportsService:Class] # @RELATION: TESTS ->[ReportsService:Class]
# @LAYER: Domain # @LAYER: Domain

View File

@@ -57,7 +57,6 @@ from .normalizer import normalize_task_report
# @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service] # @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service]
class ReportsService: class ReportsService:
# region init [TYPE Function] # region init [TYPE Function]
# @COMPLEXITY: 5
# @PURPOSE: Initialize service with TaskManager dependency. # @PURPOSE: Initialize service with TaskManager dependency.
# @PRE: task_manager is a live TaskManager instance. # @PRE: task_manager is a live TaskManager instance.
# @POST: self.task_manager is assigned and ready for read operations. # @POST: self.task_manager is assigned and ready for read operations.

View File

@@ -21,7 +21,6 @@ from ..core.logger import logger, belief_scope
class ResourceService: class ResourceService:
# region ResourceService_init [TYPE Function] # region ResourceService_init [TYPE Function]
# @COMPLEXITY: 1
# @PURPOSE: Initialize the resource service with dependencies # @PURPOSE: Initialize the resource service with dependencies
# @PRE: None # @PRE: None
# @POST: ResourceService is ready to fetch resources # @POST: ResourceService is ready to fetch resources
@@ -32,7 +31,6 @@ class ResourceService:
# endregion ResourceService_init # endregion ResourceService_init
# region get_dashboards_with_status [TYPE Function] # region get_dashboards_with_status [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Fetch dashboards from environment with Git status and last task status # @PURPOSE: Fetch dashboards from environment with Git status and last task status
# @PRE: env is a valid Environment object # @PRE: env is a valid Environment object
# @POST: Returns list of dashboards with enhanced metadata # @POST: Returns list of dashboards with enhanced metadata
@@ -82,7 +80,6 @@ class ResourceService:
# endregion get_dashboards_with_status # endregion get_dashboards_with_status
# region get_dashboards_page_with_status [TYPE Function] # region get_dashboards_page_with_status [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata. # @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.
# @PRE: env is valid; page >= 1; page_size > 0. # @PRE: env is valid; page >= 1; page_size > 0.
# @POST: Returns page items plus total counters without scanning all pages locally. # @POST: Returns page items plus total counters without scanning all pages locally.
@@ -149,7 +146,6 @@ class ResourceService:
# endregion get_dashboards_page_with_status # endregion get_dashboards_page_with_status
# region _get_last_llm_task_for_dashboard [TYPE Function] # region _get_last_llm_task_for_dashboard [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Get most recent LLM validation task for a dashboard in an environment # @PURPOSE: Get most recent LLM validation task for a dashboard in an environment
# @PRE: dashboard_id is a valid integer identifier # @PRE: dashboard_id is a valid integer identifier
# @POST: Returns the newest llm_dashboard_validation task summary or None # @POST: Returns the newest llm_dashboard_validation task summary or None
@@ -233,7 +229,6 @@ class ResourceService:
# endregion _get_last_llm_task_for_dashboard # endregion _get_last_llm_task_for_dashboard
# region _normalize_task_status [TYPE Function] # region _normalize_task_status [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Normalize task status to stable uppercase values for UI/API projections # @PURPOSE: Normalize task status to stable uppercase values for UI/API projections
# @PRE: raw_status can be enum or string # @PRE: raw_status can be enum or string
# @POST: Returns uppercase status without enum class prefix # @POST: Returns uppercase status without enum class prefix
@@ -251,7 +246,6 @@ class ResourceService:
# endregion _normalize_task_status # endregion _normalize_task_status
# region _normalize_validation_status [TYPE Function] # region _normalize_validation_status [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN # @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN
# @PRE: raw_status can be any scalar type # @PRE: raw_status can be any scalar type
# @POST: Returns normalized validation status token or None # @POST: Returns normalized validation status token or None
@@ -268,7 +262,6 @@ class ResourceService:
# endregion _normalize_validation_status # endregion _normalize_validation_status
# region _normalize_datetime_for_compare [TYPE Function] # region _normalize_datetime_for_compare [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons. # @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.
# @PRE: value may be datetime or any scalar. # @PRE: value may be datetime or any scalar.
# @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime. # @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.
@@ -285,7 +278,6 @@ class ResourceService:
# endregion _normalize_datetime_for_compare # endregion _normalize_datetime_for_compare
# region get_datasets_with_status [TYPE Function] # region get_datasets_with_status [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Fetch datasets from environment with mapping progress and last task status # @PURPOSE: Fetch datasets from environment with mapping progress and last task status
# @PRE: env is a valid Environment object # @PRE: env is a valid Environment object
# @POST: Returns list of datasets with enhanced metadata # @POST: Returns list of datasets with enhanced metadata
@@ -324,7 +316,6 @@ class ResourceService:
# endregion get_datasets_with_status # endregion get_datasets_with_status
# region get_activity_summary [TYPE Function] # region get_activity_summary [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Get summary of active and recent tasks for the activity indicator # @PURPOSE: Get summary of active and recent tasks for the activity indicator
# @PRE: tasks is a list of Task objects # @PRE: tasks is a list of Task objects
# @POST: Returns summary with active_count and recent_tasks # @POST: Returns summary with active_count and recent_tasks
@@ -366,7 +357,6 @@ class ResourceService:
# endregion get_activity_summary # endregion get_activity_summary
# region _get_git_status_for_dashboard [TYPE Function] # region _get_git_status_for_dashboard [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Get Git sync status for a dashboard # @PURPOSE: Get Git sync status for a dashboard
# @PRE: dashboard_id is a valid integer # @PRE: dashboard_id is a valid integer
# @POST: Returns git status or None if no repo exists # @POST: Returns git status or None if no repo exists
@@ -426,7 +416,6 @@ class ResourceService:
# endregion _get_git_status_for_dashboard # endregion _get_git_status_for_dashboard
# region _get_last_task_for_resource [TYPE Function] # region _get_last_task_for_resource [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Get the most recent task for a specific resource # @PURPOSE: Get the most recent task for a specific resource
# @PRE: resource_id is a valid string # @PRE: resource_id is a valid string
# @POST: Returns task summary or None if no tasks found # @POST: Returns task summary or None if no tasks found
@@ -465,7 +454,6 @@ class ResourceService:
# endregion _get_last_task_for_resource # endregion _get_last_task_for_resource
# region _extract_resource_name_from_task [TYPE Function] # region _extract_resource_name_from_task [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Extract resource name from task params # @PURPOSE: Extract resource name from task params
# @PRE: task is a valid Task object # @PRE: task is a valid Task object
# @POST: Returns resource name or task ID # @POST: Returns resource name or task ID
@@ -478,7 +466,6 @@ class ResourceService:
# endregion _extract_resource_name_from_task # endregion _extract_resource_name_from_task
# region _extract_resource_type_from_task [TYPE Function] # region _extract_resource_type_from_task [TYPE Function]
# @COMPLEXITY: 3
# @PURPOSE: Extract resource type from task params # @PURPOSE: Extract resource type from task params
# @PRE: task is a valid Task object # @PRE: task is a valid Task object
# @POST: Returns resource type or 'unknown' # @POST: Returns resource type or 'unknown'

View File

@@ -1,6 +1,5 @@
# [DEF:TestArchiveParser:Module] # [DEF:TestArchiveParser:Module]
# #
# @COMPLEXITY: 3
# @PURPOSE: Unit tests for MigrationArchiveParser ZIP extraction contract. # @PURPOSE: Unit tests for MigrationArchiveParser ZIP extraction contract.
# @LAYER: Domain # @LAYER: Domain
# @RELATION: DEPENDS_ON -> [MigrationArchiveParserModule] # @RELATION: DEPENDS_ON -> [MigrationArchiveParserModule]

View File

@@ -1,6 +1,5 @@
# [DEF:TestDryRunOrchestrator:Module] # [DEF:TestDryRunOrchestrator:Module]
# #
# @COMPLEXITY: 3
# @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts. # @PURPOSE: Unit tests for MigrationDryRunService diff and risk computation contracts.
# @LAYER: Domain # @LAYER: Domain
# @RELATION: DEPENDS_ON -> [MigrationDryRunOrchestratorModule] # @RELATION: DEPENDS_ON -> [MigrationDryRunOrchestratorModule]

View File

@@ -1,6 +1,5 @@
# [DEF:TestGitServiceGiteaPr:Module] # [DEF:TestGitServiceGiteaPr:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @SEMANTICS: tests, git, gitea, pull_request, fallback # @SEMANTICS: tests, git, gitea, pull_request, fallback
# @PURPOSE: Validate Gitea PR creation fallback behavior when configured server URL is stale. # @PURPOSE: Validate Gitea PR creation fallback behavior when configured server URL is stale.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -1,6 +1,5 @@
# [DEF:TestMappingService:Module] # [DEF:TestMappingService:Module]
# #
# @COMPLEXITY: 3
# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs. # @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.
# @LAYER: Domain # @LAYER: Domain
# @RELATION: VERIFIES ->[src.core.mapping_service.IdMappingService] # @RELATION: VERIFIES ->[src.core.mapping_service.IdMappingService]

View File

@@ -1,6 +1,5 @@
# [DEF:TestMigrationEngine:Module] # [DEF:TestMigrationEngine:Module]
# #
# @COMPLEXITY: 3
# @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms. # @PURPOSE: Unit tests for MigrationEngine's cross-filter patching algorithms.
# @LAYER: Domain # @LAYER: Domain
# @RELATION: VERIFIES -> [src.core.migration_engine:Module] # @RELATION: VERIFIES -> [src.core.migration_engine:Module]
@@ -29,7 +28,6 @@ from src.models.mapping import ResourceType
# [DEF:MockMappingService:Class] # [DEF:MockMappingService:Class]
# @RELATION: BINDS_TO -> [TestMigrationEngine:Module] # @RELATION: BINDS_TO -> [TestMigrationEngine:Module]
# @COMPLEXITY: 2
# @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios. # @PURPOSE: Deterministic mapping service double for native filter ID remapping scenarios.
# @INVARIANT: Returns mappings only for requested UUID keys present in seeded map. # @INVARIANT: Returns mappings only for requested UUID keys present in seeded map.
class MockMappingService: class MockMappingService:

View File

@@ -1,6 +1,5 @@
# [DEF:test_clean_release_cli:Module] # [DEF:test_clean_release_cli:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @PURPOSE: Smoke tests for the redesigned clean release CLI. # @PURPOSE: Smoke tests for the redesigned clean release CLI.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -1,6 +1,5 @@
# [DEF:TestCleanReleaseTui:Module] # [DEF:TestCleanReleaseTui:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @SEMANTICS: tests, tui, clean-release, curses # @SEMANTICS: tests, tui, clean-release, curses
# @PURPOSE: Unit tests for the interactive curses TUI of the clean release process. # @PURPOSE: Unit tests for the interactive curses TUI of the clean release process.
# @LAYER: Scripts # @LAYER: Scripts

View File

@@ -1,6 +1,5 @@
# [DEF:test_clean_release_tui_v2:Module] # [DEF:test_clean_release_tui_v2:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 3
# @PURPOSE: Smoke tests for thin-client TUI action dispatch and blocked transition behavior. # @PURPOSE: Smoke tests for thin-client TUI action dispatch and blocked transition behavior.
# @LAYER: Domain # @LAYER: Domain

View File

@@ -1,6 +1,5 @@
# [DEF:TestApprovalService:Module] # [DEF:TestApprovalService:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, approval, lifecycle, gate # @SEMANTICS: tests, clean-release, approval, lifecycle, gate
# @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence. # @PURPOSE: Define approval gate contracts for approve/reject operations over immutable compliance evidence.
# @LAYER: Tests # @LAYER: Tests

View File

@@ -1,6 +1,5 @@
# [DEF:test_candidate_manifest_services:Module] # [DEF:test_candidate_manifest_services:Module]
# @RELATION: BELONGS_TO -> [SrcRoot:Module] # @RELATION: BELONGS_TO -> [SrcRoot:Module]
# @COMPLEXITY: 3
# @PURPOSE: Test lifecycle and manifest versioning for release candidates. # @PURPOSE: Test lifecycle and manifest versioning for release candidates.
# @LAYER: Tests # @LAYER: Tests

View File

@@ -1,6 +1,5 @@
# [DEF:TestComplianceExecutionService:Module] # [DEF:TestComplianceExecutionService:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization # @SEMANTICS: tests, clean-release, compliance, pipeline, run-finalization
# @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution. # @PURPOSE: Validate stage pipeline and run finalization contracts for compliance execution.
# @LAYER: Tests # @LAYER: Tests

View File

@@ -1,6 +1,5 @@
# [DEF:TestComplianceTaskIntegration:Module] # [DEF:TestComplianceTaskIntegration:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, compliance, task-manager, integration # @SEMANTICS: tests, clean-release, compliance, task-manager, integration
# @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes. # @PURPOSE: Verify clean release compliance runs execute through TaskManager lifecycle with observable success/failure outcomes.
# @LAYER: Tests # @LAYER: Tests
@@ -144,7 +143,6 @@ class CleanReleaseCompliancePlugin:
# [DEF:_PluginLoaderStub:Class] # [DEF:_PluginLoaderStub:Class]
# @RELATION: BINDS_TO -> TestComplianceTaskIntegration # @RELATION: BINDS_TO -> TestComplianceTaskIntegration
# @COMPLEXITY: 2
# @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests. # @PURPOSE: Provide minimal plugin loader contract used by TaskManager in integration tests.
# @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id. # @INVARIANT: has_plugin/get_plugin only acknowledge the seeded compliance plugin id.
class _PluginLoaderStub: class _PluginLoaderStub:

View File

@@ -1,5 +1,4 @@
# [DEF:TestDemoModeIsolation:Module] # [DEF:TestDemoModeIsolation:Module]
# @COMPLEXITY: 3
# @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository # @SEMANTICS: clean-release, demo-mode, isolation, namespace, repository
# @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration. # @PURPOSE: Verify demo and real mode namespace isolation contracts before TUI integration.
# @LAYER: Tests # @LAYER: Tests

View File

@@ -1,5 +1,4 @@
# [DEF:TestPolicyResolutionService:Module] # [DEF:TestPolicyResolutionService:Module]
# @COMPLEXITY: 5
# @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts # @SEMANTICS: clean-release, policy-resolution, trusted-snapshots, contracts
# @PURPOSE: Verify trusted policy snapshot resolution contract and error guards. # @PURPOSE: Verify trusted policy snapshot resolution contract and error guards.
# @LAYER: Tests # @LAYER: Tests
@@ -24,7 +23,6 @@ from src.services.clean_release.repository import CleanReleaseRepository
# [DEF:_config_manager:Function] # [DEF:_config_manager:Function]
# @RELATION: BINDS_TO -> [TestPolicyResolutionService] # @RELATION: BINDS_TO -> [TestPolicyResolutionService]
# @COMPLEXITY: 1
# @PURPOSE: Build deterministic ConfigManager-like stub for tests. # @PURPOSE: Build deterministic ConfigManager-like stub for tests.
# @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError. # @INVARIANT: Only settings.clean_release.active_policy_id and active_registry_id are populated; any other settings field access raises AttributeError.
# @PRE: policy_id and registry_id may be None or non-empty strings. # @PRE: policy_id and registry_id may be None or non-empty strings.

View File

@@ -1,6 +1,5 @@
# [DEF:TestPublicationService:Module] # [DEF:TestPublicationService:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, publication, revoke, gate # @SEMANTICS: tests, clean-release, publication, revoke, gate
# @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records. # @PURPOSE: Define publication gate contracts over approved candidates and immutable publication records.
# @LAYER: Tests # @LAYER: Tests

View File

@@ -1,6 +1,5 @@
# [DEF:TestReportAuditImmutability:Module] # [DEF:TestReportAuditImmutability:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: tests, clean-release, report, audit, immutability, append-only # @SEMANTICS: tests, clean-release, report, audit, immutability, append-only
# @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2. # @PURPOSE: Validate report snapshot immutability expectations and append-only audit hook behavior for US2.
# @LAYER: Tests # @LAYER: Tests

View File

@@ -1,5 +1,4 @@
# [DEF:SupersetCompatibilityMatrixTests:Module] # [DEF:SupersetCompatibilityMatrixTests:Module]
# @COMPLEXITY: 3
# @SEMANTICS: dataset_review, superset, compatibility_matrix, preview, sql_lab, tests # @SEMANTICS: dataset_review, superset, compatibility_matrix, preview, sql_lab, tests
# @PURPOSE: Verifies Superset preview and SQL Lab endpoint fallback strategy used by dataset-review orchestration. # @PURPOSE: Verifies Superset preview and SQL Lab endpoint fallback strategy used by dataset-review orchestration.
# @LAYER: Tests # @LAYER: Tests
@@ -21,7 +20,6 @@ from src.core.utils.superset_compilation_adapter import (
# [DEF:make_adapter:Function] # [DEF:make_adapter:Function]
# @COMPLEXITY: 2
# @PURPOSE: Build an adapter with a mock Superset client and deterministic environment for compatibility tests. # @PURPOSE: Build an adapter with a mock Superset client and deterministic environment for compatibility tests.
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter] # @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
def make_adapter(): def make_adapter():
@@ -43,7 +41,6 @@ def make_adapter():
# [DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function] # [DEF:test_preview_prefers_supported_client_method_before_network_fallback:Function]
# @COMPLEXITY: 2
# @PURPOSE: Confirms preview compilation uses a supported client method first when the capability exists. # @PURPOSE: Confirms preview compilation uses a supported client method first when the capability exists.
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter] # @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
def test_preview_prefers_supported_client_method_before_network_fallback(): def test_preview_prefers_supported_client_method_before_network_fallback():
@@ -69,7 +66,6 @@ def test_preview_prefers_supported_client_method_before_network_fallback():
# [DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function] # [DEF:test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql:Function]
# @COMPLEXITY: 3
# @PURPOSE: Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL. # @PURPOSE: Confirms preview fallback walks the compatibility matrix from preferred to legacy endpoints until one returns compiled SQL.
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter] # @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
def test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql(): def test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql():
@@ -104,7 +100,6 @@ def test_preview_falls_back_across_matrix_until_supported_endpoint_returns_sql()
# [DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function] # [DEF:test_sql_lab_launch_falls_back_to_legacy_execute_endpoint:Function]
# @COMPLEXITY: 3
# @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction. # @PURPOSE: Confirms SQL Lab launch falls back from modern to legacy execute endpoint and preserves canonical session reference extraction.
# @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter] # @RELATION: [DEPENDS_ON] ->[SupersetCompilationAdapter]
def test_sql_lab_launch_falls_back_to_legacy_execute_endpoint(): def test_sql_lab_launch_falls_back_to_legacy_execute_endpoint():

View File

@@ -1,5 +1,4 @@
# [DEF:TestAuth:Module] # [DEF:TestAuth:Module]
# @COMPLEXITY: 3
# @PURPOSE: Covers authentication service/repository behavior and auth bootstrap helpers. # @PURPOSE: Covers authentication service/repository behavior and auth bootstrap helpers.
# @LAYER: Test # @LAYER: Test
# @RELATION: TESTS -> AuthService # @RELATION: TESTS -> AuthService

View File

@@ -1,6 +1,5 @@
# [DEF:TestDashboardsApi:Module] # [DEF:TestDashboardsApi:Module]
# @RELATION: VERIFIES ->[src.api.routes.dashboards] # @RELATION: VERIFIES ->[src.api.routes.dashboards]
# @COMPLEXITY: 3
# @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API # @PURPOSE: Comprehensive contract-driven tests for Dashboard Hub API
# @LAYER: Domain (Tests) # @LAYER: Domain (Tests)
# @SEMANTICS: tests, dashboards, api, contract, remediation # @SEMANTICS: tests, dashboards, api, contract, remediation
@@ -561,7 +560,7 @@ def test_backup_dashboards_with_schedule(mock_deps):
# --- 8. Internal logic: _task_matches_dashboard --- # --- 8. Internal logic: _task_matches_dashboard ---
# [/DEF:test_backup_dashboards_with_schedule:Function] # [/DEF:test_backup_dashboards_with_schedule:Function]
from src.api.routes.dashboards import _task_matches_dashboard from src.api.routes.dashboards._projection import _task_matches_dashboard
# [DEF:test_task_matches_dashboard_logic:Function] # [DEF:test_task_matches_dashboard_logic:Function]

View File

@@ -3,7 +3,6 @@
# @SEMANTICS: test, log, persistence, unit_test # @SEMANTICS: test, log, persistence, unit_test
# @PURPOSE: Unit tests for TaskLogPersistenceService. # @PURPOSE: Unit tests for TaskLogPersistenceService.
# @LAYER: Test # @LAYER: Test
# @COMPLEXITY: 5
# [SECTION: IMPORTS] # [SECTION: IMPORTS]
from datetime import datetime from datetime import datetime
@@ -20,7 +19,6 @@ from src.core.task_manager.models import LogEntry, LogFilter
# [DEF:TestLogPersistence:Class] # [DEF:TestLogPersistence:Class]
# @RELATION: BINDS_TO -> test_log_persistence # @RELATION: BINDS_TO -> test_log_persistence
# @PURPOSE: Test suite for TaskLogPersistenceService. # @PURPOSE: Test suite for TaskLogPersistenceService.
# @COMPLEXITY: 5
# @TEST_DATA: log_entry -> {"task_id": "test-task-1", "level": "INFO", "source": "test_source", "message": "Test message"} # @TEST_DATA: log_entry -> {"task_id": "test-task-1", "level": "INFO", "source": "test_source", "message": "Test message"}
class TestLogPersistence: class TestLogPersistence:

View File

@@ -1,5 +1,4 @@
# [DEF:TestLogger:Module] # [DEF:TestLogger:Module]
# @COMPLEXITY: 3
# @SEMANTICS: logging, tests, belief_state, cot, json # @SEMANTICS: logging, tests, belief_state, cot, json
# @PURPOSE: Unit tests for the custom logger CoT JSON formatters and configuration context manager. # @PURPOSE: Unit tests for the custom logger CoT JSON formatters and configuration context manager.
# @LAYER: Logging (Tests) # @LAYER: Logging (Tests)

View File

@@ -1,7 +1,6 @@
# [DEF:TestResourceHubs:Module] # [DEF:TestResourceHubs:Module]
# @RELATION: DEPENDS_ON -> [DashboardsApi] # @RELATION: DEPENDS_ON -> [DashboardsApi]
# @RELATION: DEPENDS_ON -> [DatasetsApi] # @RELATION: DEPENDS_ON -> [DatasetsApi]
# @COMPLEXITY: 3
# @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api # @SEMANTICS: tests, resource-hubs, dashboards, datasets, pagination, api
# @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation. # @PURPOSE: Contract tests for resource hub dashboards/datasets listing and pagination boundary validation.
# @LAYER: Domain (Tests) # @LAYER: Domain (Tests)

View File

@@ -1,5 +1,4 @@
# [DEF:TestSmokeApp:Module] # [DEF:TestSmokeApp:Module]
# @COMPLEXITY: 2
# @SEMANTICS: tests, smoke, app, imports, fastapi # @SEMANTICS: tests, smoke, app, imports, fastapi
# @PURPOSE: Minimal smoke tests that verify the full application import chain # @PURPOSE: Minimal smoke tests that verify the full application import chain
# succeeds without SyntaxError, IndentationError, ImportError, or # succeeds without SyntaxError, IndentationError, ImportError, or

View File

@@ -1,6 +1,5 @@
# [DEF:test_task_manager:Module] # [DEF:test_task_manager:Module]
# @RELATION: BELONGS_TO -> SrcRoot # @RELATION: BELONGS_TO -> SrcRoot
# @COMPLEXITY: 5
# @SEMANTICS: task-manager, lifecycle, CRUD, log-buffer, filtering, tests # @SEMANTICS: task-manager, lifecycle, CRUD, log-buffer, filtering, tests
# @PURPOSE: Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering. # @PURPOSE: Unit tests for TaskManager lifecycle, CRUD, log buffering, and filtering.
# @LAYER: Core # @LAYER: Core

View File

@@ -3,7 +3,6 @@
# @SEMANTICS: test, task, persistence, unit_test # @SEMANTICS: test, task, persistence, unit_test
# @PURPOSE: Unit tests for TaskPersistenceService. # @PURPOSE: Unit tests for TaskPersistenceService.
# @LAYER: Test # @LAYER: Test
# @COMPLEXITY: 5
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"} # @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
# [SECTION: IMPORTS] # [SECTION: IMPORTS]
@@ -23,7 +22,6 @@ from src.core.task_manager.models import Task, TaskStatus, LogEntry
# [DEF:TestTaskPersistenceHelpers:Class] # [DEF:TestTaskPersistenceHelpers:Class]
# @RELATION: BINDS_TO -> test_task_persistence # @RELATION: BINDS_TO -> test_task_persistence
# @PURPOSE: Test suite for TaskPersistenceService static helper methods. # @PURPOSE: Test suite for TaskPersistenceService static helper methods.
# @COMPLEXITY: 5
class TestTaskPersistenceHelpers: class TestTaskPersistenceHelpers:
# [DEF:test_json_load_if_needed_none:Function] # [DEF:test_json_load_if_needed_none:Function]
@@ -113,7 +111,6 @@ class TestTaskPersistenceHelpers:
# [DEF:TestTaskPersistenceService:Class] # [DEF:TestTaskPersistenceService:Class]
# @RELATION: BINDS_TO -> test_task_persistence # @RELATION: BINDS_TO -> test_task_persistence
# @PURPOSE: Test suite for TaskPersistenceService CRUD operations. # @PURPOSE: Test suite for TaskPersistenceService CRUD operations.
# @COMPLEXITY: 5
# @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"} # @TEST_DATA: valid_task -> {"id": "test-uuid-1", "plugin_id": "backup", "status": "PENDING"}
class TestTaskPersistenceService: class TestTaskPersistenceService:

View File

@@ -1,5 +1,4 @@
# [DEF:TranslateCorrectionTests:Module] # [DEF:TranslateCorrectionTests:Module]
# @COMPLEXITY: 4
# @SEMANTICS: tests, translate, corrections, dictionary # @SEMANTICS: tests, translate, corrections, dictionary
# @PURPOSE: Tests for term correction API endpoints and DictionaryManager correction methods. # @PURPOSE: Tests for term correction API endpoints and DictionaryManager correction methods.
# @LAYER: Test # @LAYER: Test

View File

@@ -1,5 +1,4 @@
# [DEF:TranslateHistoryTests:Module] # [DEF:TranslateHistoryTests:Module]
# @COMPLEXITY: 4
# @SEMANTICS: tests, translate, history, metrics # @SEMANTICS: tests, translate, history, metrics
# @PURPOSE: Tests for run history list/detail endpoints and metrics aggregation. # @PURPOSE: Tests for run history list/detail endpoints and metrics aggregation.
# @LAYER: Test # @LAYER: Test

View File

@@ -1,5 +1,4 @@
# [DEF:TranslateJobTests:Module] # [DEF:TranslateJobTests:Module]
# @COMPLEXITY: 4
# @SEMANTICS: tests, translate, jobs, crud, validation # @SEMANTICS: tests, translate, jobs, crud, validation
# @PURPOSE: Tests for translation job CRUD endpoints and service layer with column validation. # @PURPOSE: Tests for translation job CRUD endpoints and service layer with column validation.
# @LAYER: Test # @LAYER: Test

View File

@@ -1,5 +1,4 @@
# [DEF:TranslateSchedulerTests:Module] # [DEF:TranslateSchedulerTests:Module]
# @COMPLEXITY: 4
# @SEMANTICS: tests, translate, scheduler # @SEMANTICS: tests, translate, scheduler
# @PURPOSE: Tests for TranslationScheduler CRUD and APScheduler integration. # @PURPOSE: Tests for TranslationScheduler CRUD and APScheduler integration.
# @LAYER: Test # @LAYER: Test

View File

@@ -4,7 +4,6 @@
<!-- <!--
@UX_STATE: Loading -> Default @UX_STATE: Loading -> Default
@COMPLEXITY: 3
@SEMANTICS: dashboard, grid, selection, pagination @SEMANTICS: dashboard, grid, selection, pagination
@PURPOSE: Displays a grid of dashboards with selection and pagination. @PURPOSE: Displays a grid of dashboards with selection and pagination.
@LAYER: Component @LAYER: Component
@@ -43,7 +42,7 @@
let validatingIds: Set<number> = $state(new Set()); let validatingIds: Set<number> = $state(new Set());
// [/SECTION] // [/SECTION]
// [DEF:handleValidate:Function] // #region handleValidate:Function [TYPE Function]
/** /**
* @purpose Triggers dashboard validation task. * @purpose Triggers dashboard validation task.
*/ */
@@ -70,7 +69,7 @@
validatingIds = new Set(validatingIds); validatingIds = new Set(validatingIds);
} }
} }
// [/DEF:handleValidate:Function] // #endregion handleValidate:Function
// [SECTION: DERIVED] // [SECTION: DERIVED]
let filteredDashboards = $derived( let filteredDashboards = $derived(
@@ -111,7 +110,7 @@
); );
// [/SECTION] // [/SECTION]
// [DEF:handleSort:Function] // #region handleSort:Function [TYPE Function]
// @PURPOSE: Toggles sort direction or changes sort column. // @PURPOSE: Toggles sort direction or changes sort column.
// @PRE: column name is provided. // @PRE: column name is provided.
// @POST: sortColumn and sortDirection state updated. // @POST: sortColumn and sortDirection state updated.
@@ -123,9 +122,9 @@
sortDirection = "asc"; sortDirection = "asc";
} }
} }
// [/DEF:handleSort:Function] // #endregion handleSort:Function
// [DEF:handleSelectionChange:Function] // #region handleSelectionChange:Function [TYPE Function]
// @PURPOSE: Handles individual checkbox changes. // @PURPOSE: Handles individual checkbox changes.
// @PRE: dashboard ID and checked status provided. // @PRE: dashboard ID and checked status provided.
// @POST: selectedIds array updated. // @POST: selectedIds array updated.
@@ -138,9 +137,9 @@
} }
selectedIds = newSelected; selectedIds = newSelected;
} }
// [/DEF:handleSelectionChange:Function] // #endregion handleSelectionChange:Function
// [DEF:handleSelectAll:Function] // #region handleSelectAll:Function [TYPE Function]
// @PURPOSE: Handles select all checkbox. // @PURPOSE: Handles select all checkbox.
// @PRE: checked status provided. // @PRE: checked status provided.
// @POST: selectedIds array updated for all paginated items. // @POST: selectedIds array updated for all paginated items.
@@ -157,9 +156,9 @@
} }
selectedIds = newSelected; selectedIds = newSelected;
} }
// [/DEF:handleSelectAll:Function] // #endregion handleSelectAll:Function
// [DEF:goToPage:Function] // #region goToPage:Function [TYPE Function]
// @PURPOSE: Changes current page. // @PURPOSE: Changes current page.
// @PRE: page index is provided. // @PRE: page index is provided.
// @POST: currentPage state updated if within valid range. // @POST: currentPage state updated if within valid range.
@@ -168,9 +167,9 @@
currentPage = page; currentPage = page;
} }
} }
// [/DEF:goToPage:Function] // #endregion goToPage:Function
// [DEF:openGit:Function] // #region openGit:Function [TYPE Function]
/** /**
* @purpose Opens the Git management modal for a dashboard. * @purpose Opens the Git management modal for a dashboard.
*/ */
@@ -179,7 +178,7 @@
gitDashboardTitle = dashboard.title; gitDashboardTitle = dashboard.title;
showGitManager = true; showGitManager = true;
} }
// [/DEF:openGit:Function] // #endregion openGit:Function
</script> </script>
<!-- [SECTION: TEMPLATE] --> <!-- [SECTION: TEMPLATE] -->

View File

@@ -20,7 +20,7 @@
let formData = {}; let formData = {};
// [DEF:handleSubmit:Function] // #region handleSubmit:Function [TYPE Function]
/** /**
* @purpose Emits submitted form data via callback prop. * @purpose Emits submitted form data via callback prop.
* @pre formData contains user input. * @pre formData contains user input.
@@ -30,9 +30,9 @@
console.log("[DynamicForm][Action] Submitting form data.", { formData }); console.log("[DynamicForm][Action] Submitting form data.", { formData });
onsubmit(formData); onsubmit(formData);
} }
// [/DEF:handleSubmit:Function] // #endregion handleSubmit:Function
// [DEF:initializeForm:Function] // #region initializeForm:Function [TYPE Function]
/** /**
* @purpose Initialize form data with default values from the schema. * @purpose Initialize form data with default values from the schema.
* @pre schema is provided and contains properties. * @pre schema is provided and contains properties.
@@ -45,7 +45,7 @@
} }
} }
} }
// [/DEF:initializeForm:Function] // #endregion initializeForm:Function
initializeForm(); initializeForm();
</script> </script>

View File

@@ -25,7 +25,7 @@
// [/SECTION] // [/SECTION]
// [DEF:handleSelect:Function] // #region handleSelect:Function [TYPE Function]
/** /**
* @purpose Propagates the selection change through a callback prop. * @purpose Propagates the selection change through a callback prop.
* @pre event.target must be an HTMLSelectElement. * @pre event.target must be an HTMLSelectElement.
@@ -37,7 +37,7 @@
selectedId = target.value; selectedId = target.value;
onchange({ id: selectedId }); onchange({ id: selectedId });
} }
// [/DEF:handleSelect:Function] // #endregion handleSelect:Function
</script> </script>
<!-- [SECTION: TEMPLATE] --> <!-- [SECTION: TEMPLATE] -->

View File

@@ -1,8 +1,8 @@
<!-- #region Footer [C:3] [TYPE Component] [SEMANTICS footer, layout, copyright] --> <!-- #region Footer [C:3] [TYPE Component] [SEMANTICS footer, layout, copyright] -->
<!-- @BRIEF Component component: components/Footer.svelte --> <!-- @BRIEF Application footer displaying copyright information. -->
<!-- @LAYER UI --> <!-- @LAYER UI -->
<!-- @RELATION DEPENDS_ON -> [RootLayout] -->
<!-- <!--
@COMPLEXITY: 1
@SEMANTICS: footer, layout, copyright @SEMANTICS: footer, layout, copyright
@PURPOSE: Displays the application footer with copyright information. @PURPOSE: Displays the application footer with copyright information.
@LAYER: UI @LAYER: UI

View File

@@ -26,7 +26,7 @@
// [/SECTION] // [/SECTION]
// [DEF:updateMapping:Function] // #region updateMapping:Function [TYPE Function]
/** /**
* @purpose Updates a mapping for a specific source database. * @purpose Updates a mapping for a specific source database.
* @pre sourceUuid and targetUuid are provided. * @pre sourceUuid and targetUuid are provided.
@@ -44,9 +44,9 @@
engine: sDb?.engine || "" engine: sDb?.engine || ""
}); });
} }
// [/DEF:updateMapping:Function] // #endregion updateMapping:Function
// [DEF:getSuggestion:Function] // #region getSuggestion:Function [TYPE Function]
/** /**
* @purpose Finds a suggestion for a source database. * @purpose Finds a suggestion for a source database.
* @pre sourceUuid is provided. * @pre sourceUuid is provided.
@@ -55,7 +55,7 @@
function getSuggestion(sourceUuid: string) { function getSuggestion(sourceUuid: string) {
return suggestions.find(s => s.source_db_uuid === sourceUuid); return suggestions.find(s => s.source_db_uuid === sourceUuid);
} }
// [/DEF:getSuggestion:Function] // #endregion getSuggestion:Function
</script> </script>
<!-- [SECTION: TEMPLATE] --> <!-- [SECTION: TEMPLATE] -->

View File

@@ -26,7 +26,7 @@
let selectedTargetUuid = $state(""); let selectedTargetUuid = $state("");
// [DEF:resolve:Function] // #region resolve:Function [TYPE Function]
// @PURPOSE: Resolves the missing mapping via callback prop. // @PURPOSE: Resolves the missing mapping via callback prop.
// @PRE: selectedTargetUuid must be set. // @PRE: selectedTargetUuid must be set.
// @POST: Parent callback receives mapping payload and modal closes. // @POST: Parent callback receives mapping payload and modal closes.
@@ -39,9 +39,9 @@
}); });
show = false; show = false;
} }
// [/DEF:resolve:Function] // #endregion resolve:Function
// [DEF:cancel:Function] // #region cancel:Function [TYPE Function]
// @PURPOSE: Cancels the mapping resolution modal. // @PURPOSE: Cancels the mapping resolution modal.
// @PRE: Modal is open. // @PRE: Modal is open.
// @POST: Parent cancel callback is invoked and modal is hidden. // @POST: Parent cancel callback is invoked and modal is hidden.
@@ -49,7 +49,7 @@
oncancel(); oncancel();
show = false; show = false;
} }
// [/DEF:cancel:Function] // #endregion cancel:Function
</script> </script>
<!-- [SECTION: TEMPLATE] --> <!-- [SECTION: TEMPLATE] -->

View File

@@ -2,7 +2,6 @@
<!-- @BRIEF Component component: components/Navbar.svelte --> <!-- @BRIEF Component component: components/Navbar.svelte -->
<!-- @LAYER UI --> <!-- @LAYER UI -->
<!-- <!--
@COMPLEXITY: 3
@SEMANTICS: navbar, navigation, header, layout @SEMANTICS: navbar, navigation, header, layout
@PURPOSE: Main navigation bar for the application. @PURPOSE: Main navigation bar for the application.
@LAYER: UI @LAYER: UI

View File

@@ -21,7 +21,7 @@
let passwords = $state({}); let passwords = $state({});
let submitting = $state(false); let submitting = $state(false);
// [DEF:handleSubmit:Function] // #region handleSubmit:Function [TYPE Function]
// @PURPOSE: Validates and forwards passwords to resume the task. // @PURPOSE: Validates and forwards passwords to resume the task.
// @PRE: All database passwords must be entered. // @PRE: All database passwords must be entered.
// @POST: Parent resume callback receives passwords payload. // @POST: Parent resume callback receives passwords payload.
@@ -39,9 +39,9 @@
onresume({ passwords }); onresume({ passwords });
// Reset submitting state is handled by parent or on close // Reset submitting state is handled by parent or on close
} }
// [/DEF:handleSubmit:Function] // #endregion handleSubmit:Function
// [DEF:handleCancel:Function] // #region handleCancel:Function [TYPE Function]
// @PURPOSE: Cancels the password prompt. // @PURPOSE: Cancels the password prompt.
// @PRE: Modal is open. // @PRE: Modal is open.
// @POST: Parent cancel callback is invoked and show is set to false. // @POST: Parent cancel callback is invoked and show is set to false.
@@ -49,7 +49,7 @@
oncancel(); oncancel();
show = false; show = false;
} }
// [/DEF:handleCancel:Function] // #endregion handleCancel:Function
// Reset passwords when modal opens/closes // Reset passwords when modal opens/closes
$effect(() => { $effect(() => {

View File

@@ -2,7 +2,6 @@
<!-- @BRIEF Component component: components/RepositoryDashboardGrid.svelte --> <!-- @BRIEF Component component: components/RepositoryDashboardGrid.svelte -->
<!-- @LAYER UI --> <!-- @LAYER UI -->
<!-- <!--
@COMPLEXITY: 3
@SEMANTICS: dashboard, grid, selection, pagination @SEMANTICS: dashboard, grid, selection, pagination
@PURPOSE: Displays a grid of dashboards with selection and pagination. @PURPOSE: Displays a grid of dashboards with selection and pagination.
@LAYER: Component @LAYER: Component
@@ -98,7 +97,7 @@
); );
// [/SECTION] // [/SECTION]
// [DEF:handleSort:Function] // #region handleSort:Function [TYPE Function]
// @PURPOSE: Toggles sort direction or changes sort column. // @PURPOSE: Toggles sort direction or changes sort column.
// @PRE: column name is provided. // @PRE: column name is provided.
// @POST: sortColumn and sortDirection state updated. // @POST: sortColumn and sortDirection state updated.
@@ -110,9 +109,9 @@
sortDirection = "asc"; sortDirection = "asc";
} }
} }
// [/DEF:handleSort:Function] // #endregion handleSort:Function
// [DEF:handleSelectionChange:Function] // #region handleSelectionChange:Function [TYPE Function]
// @PURPOSE: Handles individual checkbox changes. // @PURPOSE: Handles individual checkbox changes.
// @PRE: dashboard ID and checked status provided. // @PRE: dashboard ID and checked status provided.
// @POST: selectedIds array updated. // @POST: selectedIds array updated.
@@ -125,9 +124,9 @@
} }
selectedIds = newSelected; selectedIds = newSelected;
} }
// [/DEF:handleSelectionChange:Function] // #endregion handleSelectionChange:Function
// [DEF:handleSelectAll:Function] // #region handleSelectAll:Function [TYPE Function]
// @PURPOSE: Handles select all checkbox. // @PURPOSE: Handles select all checkbox.
// @PRE: checked status provided. // @PRE: checked status provided.
// @POST: selectedIds array updated for all paginated items. // @POST: selectedIds array updated for all paginated items.
@@ -144,9 +143,9 @@
} }
selectedIds = newSelected; selectedIds = newSelected;
} }
// [/DEF:handleSelectAll:Function] // #endregion handleSelectAll:Function
// [DEF:goToPage:Function] // #region goToPage:Function [TYPE Function]
// @PURPOSE: Changes current page. // @PURPOSE: Changes current page.
// @PRE: page index is provided. // @PRE: page index is provided.
// @POST: currentPage state updated if within valid range. // @POST: currentPage state updated if within valid range.
@@ -155,9 +154,9 @@
currentPage = page; currentPage = page;
} }
} }
// [/DEF:goToPage:Function] // #endregion goToPage:Function
// [DEF:getRepositoryStatusToken:Function] // #region getRepositoryStatusToken:Function [TYPE Function]
/** /**
* @purpose Returns normalized repository status token for a dashboard. * @purpose Returns normalized repository status token for a dashboard.
* @pre Dashboard exists. * @pre Dashboard exists.
@@ -166,17 +165,17 @@
function getRepositoryStatusToken(dashboardId: number): string { function getRepositoryStatusToken(dashboardId: number): string {
return repositoryStatusByDashboardId[dashboardId] || "loading"; return repositoryStatusByDashboardId[dashboardId] || "loading";
} }
// [/DEF:getRepositoryStatusToken:Function] // #endregion getRepositoryStatusToken:Function
// [DEF:isRepositoryReady:Function] // #region isRepositoryReady:Function [TYPE Function]
// @PURPOSE: Determines whether git actions can run for a dashboard. // @PURPOSE: Determines whether git actions can run for a dashboard.
function isRepositoryReady(dashboardId: number): boolean { function isRepositoryReady(dashboardId: number): boolean {
const token = getRepositoryStatusToken(dashboardId); const token = getRepositoryStatusToken(dashboardId);
return token !== "loading" && token !== "no_repo" && token !== "error"; return token !== "loading" && token !== "no_repo" && token !== "error";
} }
// [/DEF:isRepositoryReady:Function] // #endregion isRepositoryReady:Function
// [DEF:invalidateRepositoryStatuses:Function] // #region invalidateRepositoryStatuses:Function [TYPE Function]
// @PURPOSE: Marks dashboard statuses as loading so they are refetched. // @PURPOSE: Marks dashboard statuses as loading so they are refetched.
function invalidateRepositoryStatuses(dashboardIds: number[]): void { function invalidateRepositoryStatuses(dashboardIds: number[]): void {
if (dashboardIds.length === 0) return; if (dashboardIds.length === 0) return;
@@ -186,9 +185,9 @@
}); });
repositoryStatusByDashboardId = nextStatuses; repositoryStatusByDashboardId = nextStatuses;
} }
// [/DEF:invalidateRepositoryStatuses:Function] // #endregion invalidateRepositoryStatuses:Function
// [DEF:resolveRepositoryStatusToken:Function] // #region resolveRepositoryStatusToken:Function [TYPE Function]
/** /**
* @purpose Converts git status payload into a stable UI status token. * @purpose Converts git status payload into a stable UI status token.
*/ */
@@ -220,9 +219,9 @@
return hasChanges ? "changes" : "synced"; return hasChanges ? "changes" : "synced";
} }
// [/DEF:resolveRepositoryStatusToken:Function] // #endregion resolveRepositoryStatusToken:Function
// [DEF:loadRepositoryStatuses:Function] // #region loadRepositoryStatuses:Function [TYPE Function]
/** /**
* @purpose Hydrates repository status map for dashboards in repository mode. * @purpose Hydrates repository status map for dashboards in repository mode.
*/ */
@@ -279,9 +278,9 @@
...Object.fromEntries(entries), ...Object.fromEntries(entries),
}; };
} }
// [/DEF:loadRepositoryStatuses:Function] // #endregion loadRepositoryStatuses:Function
// [DEF:runBulkGitAction:Function] // #region runBulkGitAction:Function [TYPE Function]
// @PURPOSE: Executes git action for selected dashboards with limited parallelism. // @PURPOSE: Executes git action for selected dashboards with limited parallelism.
async function runBulkGitAction( async function runBulkGitAction(
actionToken: string, actionToken: string,
@@ -333,15 +332,15 @@
bulkActionRunning = false; bulkActionRunning = false;
} }
} }
// [/DEF:runBulkGitAction:Function] // #endregion runBulkGitAction:Function
// [DEF:handleBulkSync:Function] // #region handleBulkSync:Function [TYPE Function]
async function handleBulkSync(): Promise<void> { async function handleBulkSync(): Promise<void> {
await runBulkGitAction("sync", (dashboardId) => gitService.sync(dashboardId, null, envId)); await runBulkGitAction("sync", (dashboardId) => gitService.sync(dashboardId, null, envId));
} }
// [/DEF:handleBulkSync:Function] // #endregion handleBulkSync:Function
// [DEF:handleBulkCommit:Function] // #region handleBulkCommit:Function [TYPE Function]
async function handleBulkCommit(): Promise<void> { async function handleBulkCommit(): Promise<void> {
const message = prompt($t.git?.commit_message); const message = prompt($t.git?.commit_message);
if (!message?.trim()) return; if (!message?.trim()) return;
@@ -349,21 +348,21 @@
gitService.commit(dashboardId, message.trim(), [], envId), gitService.commit(dashboardId, message.trim(), [], envId),
); );
} }
// [/DEF:handleBulkCommit:Function] // #endregion handleBulkCommit:Function
// [DEF:handleBulkPull:Function] // #region handleBulkPull:Function [TYPE Function]
async function handleBulkPull(): Promise<void> { async function handleBulkPull(): Promise<void> {
await runBulkGitAction("pull", (dashboardId) => gitService.pull(dashboardId, envId)); await runBulkGitAction("pull", (dashboardId) => gitService.pull(dashboardId, envId));
} }
// [/DEF:handleBulkPull:Function] // #endregion handleBulkPull:Function
// [DEF:handleBulkPush:Function] // #region handleBulkPush:Function [TYPE Function]
async function handleBulkPush(): Promise<void> { async function handleBulkPush(): Promise<void> {
await runBulkGitAction("push", (dashboardId) => gitService.push(dashboardId, envId)); await runBulkGitAction("push", (dashboardId) => gitService.push(dashboardId, envId));
} }
// [/DEF:handleBulkPush:Function] // #endregion handleBulkPush:Function
// [DEF:handleBulkDelete:Function] // #region handleBulkDelete:Function [TYPE Function]
// @PURPOSE: Removes selected repositories from storage and binding table. // @PURPOSE: Removes selected repositories from storage and binding table.
async function handleBulkDelete(): Promise<void> { async function handleBulkDelete(): Promise<void> {
if (!confirm($t.git?.confirm_delete_repo || "Удалить выбранные репозитории?")) return; if (!confirm($t.git?.confirm_delete_repo || "Удалить выбранные репозитории?")) return;
@@ -374,9 +373,9 @@
dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id)); dashboards = dashboards.filter((dashboard) => !idsToDelete.includes(dashboard.id));
selectedIds = []; selectedIds = [];
} }
// [/DEF:handleBulkDelete:Function] // #endregion handleBulkDelete:Function
// [DEF:handleManageSelected:Function] // #region handleManageSelected:Function [TYPE Function]
// @PURPOSE: Opens Git manager for exactly one selected dashboard. // @PURPOSE: Opens Git manager for exactly one selected dashboard.
async function handleManageSelected(): Promise<void> { async function handleManageSelected(): Promise<void> {
if (selectedIds.length !== 1) { if (selectedIds.length !== 1) {
@@ -390,9 +389,9 @@
); );
openGitManagerForDashboard(selectedDashboard || null); openGitManagerForDashboard(selectedDashboard || null);
} }
// [/DEF:handleManageSelected:Function] // #endregion handleManageSelected:Function
// [DEF:resolveDashboardRef:Function] // #region resolveDashboardRef:Function [TYPE Function]
// @PURPOSE: Resolves dashboard slug from payload fields. // @PURPOSE: Resolves dashboard slug from payload fields.
// @PRE: Dashboard metadata is provided. // @PRE: Dashboard metadata is provided.
// @POST: Returns slug string or null if unavailable. // @POST: Returns slug string or null if unavailable.
@@ -411,9 +410,9 @@
if (!slugMatch?.[1]) return null; if (!slugMatch?.[1]) return null;
return decodeURIComponent(slugMatch[1]); return decodeURIComponent(slugMatch[1]);
} }
// [/DEF:resolveDashboardRef:Function] // #endregion resolveDashboardRef:Function
// [DEF:openGitManagerForDashboard:Function] // #region openGitManagerForDashboard:Function [TYPE Function]
// @PURPOSE: Opens Git manager for provided dashboard metadata. // @PURPOSE: Opens Git manager for provided dashboard metadata.
function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void { function openGitManagerForDashboard(dashboard: DashboardMetadata | null): void {
if (!dashboard) return; if (!dashboard) return;
@@ -426,9 +425,9 @@
gitDashboardTitle = dashboard.title || ""; gitDashboardTitle = dashboard.title || "";
showGitManager = true; showGitManager = true;
} }
// [/DEF:openGitManagerForDashboard:Function] // #endregion openGitManagerForDashboard:Function
// [DEF:handleInitializeRepositories:Function] // #region handleInitializeRepositories:Function [TYPE Function]
// @PURPOSE: Opens Git manager from bulk actions to initialize selected repository. // @PURPOSE: Opens Git manager from bulk actions to initialize selected repository.
async function handleInitializeRepositories(): Promise<void> { async function handleInitializeRepositories(): Promise<void> {
if (selectedIds.length !== 1) { if (selectedIds.length !== 1) {
@@ -439,9 +438,9 @@
const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null; const selectedDashboard = dashboards.find((dashboard) => dashboard.id === selectedDashboardId) || null;
openGitManagerForDashboard(selectedDashboard); openGitManagerForDashboard(selectedDashboard);
} }
// [/DEF:handleInitializeRepositories:Function] // #endregion handleInitializeRepositories:Function
// [DEF:getSortStatusValue:Function] // #region getSortStatusValue:Function [TYPE Function]
/** /**
* @purpose Returns sort value for status column based on mode. * @purpose Returns sort value for status column based on mode.
*/ */
@@ -451,9 +450,9 @@
} }
return String(dashboard.status || "").toLowerCase(); return String(dashboard.status || "").toLowerCase();
} }
// [/DEF:getSortStatusValue:Function] // #endregion getSortStatusValue:Function
// [DEF:getStatusLabel:Function] // #region getStatusLabel:Function [TYPE Function]
/** /**
* @purpose Returns localized label for status column. * @purpose Returns localized label for status column.
*/ */
@@ -464,9 +463,9 @@
const token = getRepositoryStatusToken(dashboard.id); const token = getRepositoryStatusToken(dashboard.id);
return $t.git?.repo_status?.[token] || token; return $t.git?.repo_status?.[token] || token;
} }
// [/DEF:getStatusLabel:Function] // #endregion getStatusLabel:Function
// [DEF:getStatusBadgeClass:Function] // #region getStatusBadgeClass:Function [TYPE Function]
/** /**
* @purpose Returns badge style for status column. * @purpose Returns badge style for status column.
*/ */
@@ -487,7 +486,7 @@
if (token === "diverged") return "bg-purple-100 text-purple-800"; if (token === "diverged") return "bg-purple-100 text-purple-800";
return "bg-rose-100 text-rose-800"; return "bg-rose-100 text-rose-800";
} }
// [/DEF:getStatusBadgeClass:Function] // #endregion getStatusBadgeClass:Function
$effect(() => { $effect(() => {
dashboards; dashboards;

View File

@@ -3,7 +3,6 @@
<!-- @LAYER UI --> <!-- @LAYER UI -->
<script> <script>
/** /**
* @COMPLEXITY: 3
* @PURPOSE: Blocking startup wizard for creating the first Superset environment from zero-state screens. * @PURPOSE: Blocking startup wizard for creating the first Superset environment from zero-state screens.
* @LAYER: UI * @LAYER: UI
* @RELATION: CALLS -> api * @RELATION: CALLS -> api

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