semantics

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

View File

@@ -1,9 +1,9 @@
# region test_encryption_manager [TYPE Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @RELATION BELONGS_TO -> SrcRoot
# @SEMANTICS: encryption, security, fernet, api-keys, tests
# @PURPOSE: Unit tests for EncryptionManager encrypt/decrypt functionality.
# @LAYER: Domain
# @INVARIANT: Encrypt+decrypt roundtrip always returns original plaintext.
# @LAYER Domain
# @INVARIANT Encrypt+decrypt roundtrip always returns original plaintext.
from pathlib import Path
import sys
@@ -17,10 +17,10 @@ from cryptography.fernet import Fernet
# region TestEncryptionManager [TYPE Class]
# @RELATION: BINDS_TO -> test_encryption_manager
# @RELATION BINDS_TO -> test_encryption_manager
# @PURPOSE: Validate EncryptionManager encrypt/decrypt roundtrip, uniqueness, and error handling.
# @PRE: cryptography package installed.
# @POST: All encrypt/decrypt invariants verified.
# @PRE cryptography package installed.
# @POST All encrypt/decrypt invariants verified.
class TestEncryptionManager:
"""Tests for the EncryptionManager class."""
@@ -44,8 +44,8 @@ class TestEncryptionManager:
# region test_encrypt_decrypt_roundtrip [TYPE Function]
# @PURPOSE: Encrypt then decrypt returns original plaintext.
# @PRE: Valid plaintext string.
# @POST: Decrypted output equals original input.
# @PRE Valid plaintext string.
# @POST Decrypted output equals original input.
def test_encrypt_decrypt_roundtrip(self):
mgr = self._make_manager()
original = "my-secret-api-key-12345"
@@ -57,8 +57,8 @@ class TestEncryptionManager:
# region test_encrypt_produces_different_output [TYPE Function]
# @PURPOSE: Same plaintext produces different ciphertext (Fernet uses random IV).
# @PRE: Two encrypt calls with same input.
# @POST: Ciphertexts differ but both decrypt to same value.
# @PRE Two encrypt calls with same input.
# @POST Ciphertexts differ but both decrypt to same value.
def test_encrypt_produces_different_output(self):
mgr = self._make_manager()
ct1 = mgr.encrypt("same-key")
@@ -69,8 +69,8 @@ class TestEncryptionManager:
# region test_different_inputs_yield_different_ciphertext [TYPE Function]
# @PURPOSE: Different inputs produce different ciphertexts.
# @PRE: Two different plaintext values.
# @POST: Encrypted outputs differ.
# @PRE Two different plaintext values.
# @POST Encrypted outputs differ.
def test_different_inputs_yield_different_ciphertext(self):
mgr = self._make_manager()
ct1 = mgr.encrypt("key-one")
@@ -80,8 +80,8 @@ class TestEncryptionManager:
# region test_decrypt_invalid_data_raises [TYPE Function]
# @PURPOSE: Decrypting invalid data raises InvalidToken.
# @PRE: Invalid ciphertext string.
# @POST: Exception raised.
# @PRE Invalid ciphertext string.
# @POST Exception raised.
def test_decrypt_invalid_data_raises(self):
mgr = self._make_manager()
with pytest.raises(Exception):
@@ -90,8 +90,8 @@ class TestEncryptionManager:
# region test_encrypt_empty_string [TYPE Function]
# @PURPOSE: Encrypting and decrypting an empty string works.
# @PRE: Empty string input.
# @POST: Decrypted output equals empty string.
# @PRE Empty string input.
# @POST Decrypted output equals empty string.
def test_encrypt_empty_string(self):
mgr = self._make_manager()
encrypted = mgr.encrypt("")
@@ -102,8 +102,8 @@ class TestEncryptionManager:
# region test_missing_key_fails_fast [TYPE Function]
# @PURPOSE: Missing ENCRYPTION_KEY must abort initialization instead of using a fallback secret.
# @PRE: ENCRYPTION_KEY is unset.
# @POST: RuntimeError raised during EncryptionManager construction.
# @PRE ENCRYPTION_KEY is unset.
# @POST RuntimeError raised during EncryptionManager construction.
def test_missing_key_fails_fast(self):
from src.services.llm_provider import EncryptionManager
@@ -113,8 +113,8 @@ class TestEncryptionManager:
# region test_custom_key_roundtrip [TYPE Function]
# @PURPOSE: Custom Fernet key produces valid roundtrip.
# @PRE: Generated Fernet key.
# @POST: Encrypt/decrypt with custom key succeeds.
# @PRE Generated Fernet key.
# @POST Encrypt/decrypt with custom key succeeds.
def test_custom_key_roundtrip(self):
custom_key = Fernet.generate_key()
fernet = Fernet(custom_key)

View File

@@ -5,9 +5,9 @@ from unittest.mock import MagicMock, patch
from src.models.llm import ValidationRecord
from src.services.health_service import HealthService
# region test_health_service [TYPE Module]
# region [EXT:internal:test_health_service] [TYPE Module]
# @PURPOSE: Unit tests for HealthService aggregation logic.
# @RELATION: VERIFIES ->[src.services.health_service.HealthService]
# @RELATION BINDS_TO ->[HealthService]
@pytest.mark.asyncio
@@ -163,7 +163,7 @@ async def test_get_health_summary_reuses_dashboard_metadata_cache_across_service
# region test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks [TYPE Function]
# @RELATION: BINDS_TO ->[test_health_service]
# @RELATION BINDS_TO ->[[EXT:internal:test_health_service]]
# @PURPOSE: Verify that deleting a validation report also removes dashboard scope and linked tasks.
def test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks():
db = MagicMock()
@@ -239,7 +239,7 @@ def test_delete_validation_report_deletes_dashboard_scope_and_linked_tasks():
# region test_delete_validation_report_returns_false_for_unknown_record [TYPE Function]
# @RELATION: BINDS_TO ->[test_health_service]
# @RELATION BINDS_TO ->[[EXT:internal:test_health_service]]
# @PURPOSE: Verify delete returns False when validation record does not exist.
def test_delete_validation_report_returns_false_for_unknown_record():
db = MagicMock()
@@ -254,7 +254,7 @@ def test_delete_validation_report_returns_false_for_unknown_record():
# region test_delete_validation_report_swallows_linked_task_cleanup_failure [TYPE Function]
# @RELATION: BINDS_TO ->[test_health_service]
# @RELATION BINDS_TO ->[[EXT:internal:test_health_service]]
# @PURPOSE: Verify delete swallows exceptions when cleaning up linked tasks.
def test_delete_validation_report_swallows_linked_task_cleanup_failure():
db = MagicMock()
@@ -306,4 +306,4 @@ def test_delete_validation_report_swallows_linked_task_cleanup_failure():
# endregion test_delete_validation_report_swallows_linked_task_cleanup_failure
# endregion test_health_service
# endregion [EXT:internal:test_health_service]

View File

@@ -1,5 +1,5 @@
# region test_llm_plugin_persistence [TYPE Module]
# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]
# @RELATION BINDS_TO -> [DashboardValidationPlugin]
# @PURPOSE: Regression test for ValidationRecord persistence fields populated from task context.
import pytest
@@ -9,9 +9,9 @@ from src.plugins.llm_analysis import plugin as plugin_module
# region _DummyLogger [TYPE Class]
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_plugin_persistence]
# @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:
def with_source(self, _source: str):
return self
@@ -33,9 +33,9 @@ class _DummyLogger:
# region _FakeDBSession [TYPE Class]
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_plugin_persistence]
# @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:
def __init__(self):
self.added = None
@@ -56,10 +56,10 @@ class _FakeDBSession:
# region test_dashboard_validation_plugin_persists_task_and_environment_ids [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_plugin_persistence:Module]
# @RELATION: VERIFIES -> [DashboardValidationPlugin:Class]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_plugin_persistence]
# @RELATION BINDS_TO -> [DashboardValidationPlugin]
# @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
async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
tmp_path, monkeypatch
@@ -78,9 +78,9 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
)
# region _FakeProviderService [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @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:
def __init__(self, _db):
return None
@@ -94,9 +94,9 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# endregion _FakeProviderService
# region _FakeScreenshotService [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @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:
def __init__(self, _env):
return None
@@ -107,9 +107,9 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# endregion _FakeScreenshotService
# region _FakeLLMClient [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @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:
"""Fake LLM client for persistence tests.
@@ -130,9 +130,9 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# endregion _FakeLLMClient
# region _FakeNotificationService [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @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:
def __init__(self, *_args, **_kwargs):
return None
@@ -143,9 +143,9 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# endregion _FakeNotificationService
# region _FakeConfigManager [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @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:
def get_environment(self, _env_id):
return env
@@ -161,9 +161,9 @@ async def test_dashboard_validation_plugin_persists_task_and_environment_ids(
# endregion _FakeConfigManager
# region _FakeSupersetClient [TYPE Class]
# @RELATION: BINDS_TO -> [test_dashboard_validation_plugin_persists_task_and_environment_ids:Function]
# @RELATION BINDS_TO -> [EXT:frontend:test_dashboard_validation_plugin_persists_task_and_environment_ids]
# @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:
def __init__(self, _env):
self.network = types.SimpleNamespace(

View File

@@ -1,9 +1,9 @@
# region test_llm_prompt_templates [TYPE Module]
# @SEMANTICS: tests, llm, prompts, templates, settings
# @PURPOSE: Validate normalization and rendering behavior for configurable LLM prompt templates.
# @LAYER: Domain Tests
# @RELATION: DEPENDS_ON -> [llm_prompt_templates]
# @INVARIANT: All required prompt keys remain available after normalization.
# @LAYER Domain Tests
# @RELATION DEPENDS_ON -> [llm_prompt_templates]
# @INVARIANT All required prompt keys remain available after normalization.
from src.services.llm_prompt_templates import (
DEFAULT_LLM_ASSISTANT_SETTINGS,
@@ -17,10 +17,10 @@ from src.services.llm_prompt_templates import (
# region test_normalize_llm_settings_adds_default_prompts [TYPE Function]
# @RELATION: BINDS_TO -> test_llm_prompt_templates
# @RELATION BINDS_TO -> test_llm_prompt_templates
# @PURPOSE: Ensure legacy/partial llm settings are expanded with all prompt defaults.
# @PRE: Input llm settings do not contain complete prompts object.
# @POST: Returned structure includes required prompt templates with fallback defaults.
# @PRE Input llm settings do not contain complete prompts object.
# @POST Returned structure includes required prompt templates with fallback defaults.
def test_normalize_llm_settings_adds_default_prompts():
normalized = normalize_llm_settings({"default_provider": "x"})
@@ -40,10 +40,10 @@ def test_normalize_llm_settings_adds_default_prompts():
# 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
# @PURPOSE: Ensure user-customized prompt values are preserved during normalization.
# @PRE: Input llm settings contain custom prompt override.
# @POST: Custom prompt value remains unchanged in normalized output.
# @PRE Input llm settings contain custom prompt override.
# @POST Custom prompt value remains unchanged in normalized output.
def test_normalize_llm_settings_keeps_custom_prompt_values():
custom = "Doc for {dataset_name} using {columns_json}"
normalized = normalize_llm_settings({"prompts": {"documentation_prompt": custom}})
@@ -55,10 +55,10 @@ def test_normalize_llm_settings_keeps_custom_prompt_values():
# region test_render_prompt_replaces_known_placeholders [TYPE Function]
# @RELATION: BINDS_TO -> test_llm_prompt_templates
# @RELATION BINDS_TO -> test_llm_prompt_templates
# @PURPOSE: Ensure template placeholders are deterministically replaced.
# @PRE: Template contains placeholders matching provided variables.
# @POST: Rendered prompt string contains substituted values.
# @PRE Template contains placeholders matching provided variables.
# @POST Rendered prompt string contains substituted values.
def test_render_prompt_replaces_known_placeholders():
rendered = render_prompt(
"Hello {name}, diff={diff}",
@@ -72,7 +72,7 @@ def test_render_prompt_replaces_known_placeholders():
# 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
# @PURPOSE: Ensure multimodal model detection recognizes common vision-capable model names.
def test_is_multimodal_model_detects_known_vision_models():
assert is_multimodal_model("gpt-4o") is True
@@ -85,7 +85,7 @@ def test_is_multimodal_model_detects_known_vision_models():
# 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
# @PURPOSE: Verify provider binding resolution priority.
def test_resolve_bound_provider_id_prefers_binding_then_default():
settings = {
@@ -100,7 +100,7 @@ def test_resolve_bound_provider_id_prefers_binding_then_default():
# 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
# @PURPOSE: Ensure assistant planner provider/model fields are preserved and normalized.
def test_normalize_llm_settings_keeps_assistant_planner_settings():
normalized = normalize_llm_settings(

View File

@@ -1,5 +1,5 @@
# region test_llm_provider [TYPE Module]
# @RELATION: VERIFIES -> [src.services.llm_provider:Module]
# @RELATION BINDS_TO -> [LLMProvider]
# @SEMANTICS: tests, llm-provider, encryption, contract
# @PURPOSE: Contract testing for LLMProviderService and EncryptionManager
# endregion test_llm_provider
@@ -22,15 +22,15 @@ from src.services.llm_provider import (
# region _test_encryption_key_fixture [TYPE Global]
# @PURPOSE: Ensure encryption-dependent provider tests run with a valid Fernet key.
# @RELATION: DEPENDS_ON -> [pytest:Module]
# @RELATION DEPENDS_ON -> [EXT:Library:pytest]
os.environ.setdefault("ENCRYPTION_KEY", Fernet.generate_key().decode())
# endregion _test_encryption_key_fixture
# region test_provider_type_enum_values [TYPE Function]
# @RELATION: VERIFIES -> [LLMProviderType]
# @RELATION BINDS_TO -> [LLMProviderType]
# @PURPOSE: Regression guard — prevent accidental value drift when enum variants are added or renamed.
# @INVARIANT: Every LLMProviderType member must have a value matching its lowercase name.
# @INVARIANT Every LLMProviderType member must have a value matching its lowercase name.
def test_provider_type_enum_values():
"""Verify all LLMProviderType enum values match their expected strings."""
assert LLMProviderType.OPENAI.value == "openai"
@@ -47,10 +47,10 @@ def test_provider_type_enum_values():
# endregion test_provider_type_enum_values
# @TEST_CONTRACT: EncryptionManagerModel -> Invariants
# @TEST_INVARIANT: symmetric_encryption
# @TEST_CONTRACT EncryptionManagerModel -> Invariants
# @TEST_INVARIANT symmetric_encryption
# region test_encryption_cycle [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify EncryptionManager round-trip encryption/decryption invariant for non-empty secrets.
def test_encryption_cycle():
"""Verify encrypted data can be decrypted back to original string."""
@@ -61,12 +61,12 @@ def test_encryption_cycle():
assert manager.decrypt(encrypted) == original
# @TEST_EDGE: empty_string_encryption
# @TEST_EDGE empty_string_encryption
# endregion test_encryption_cycle
# region test_empty_string_encryption [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify EncryptionManager preserves empty-string payloads through encrypt/decrypt cycle.
def test_empty_string_encryption():
manager = EncryptionManager()
@@ -75,12 +75,12 @@ def test_empty_string_encryption():
assert manager.decrypt(encrypted) == ""
# @TEST_EDGE: decrypt_invalid_data
# @TEST_EDGE decrypt_invalid_data
# endregion test_empty_string_encryption
# region test_decrypt_invalid_data [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure decrypt rejects invalid ciphertext input by raising an exception.
def test_decrypt_invalid_data():
manager = EncryptionManager()
@@ -88,14 +88,14 @@ def test_decrypt_invalid_data():
manager.decrypt("not-encrypted-string")
# @TEST_FIXTURE: mock_db_session
# @TEST_FIXTURE mock_db_session
# endregion test_decrypt_invalid_data
# region mock_db [TYPE Fixture]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @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
def mock_db():
# @RISK: query() returns unconstrained MagicMock — chain beyond query() has no spec protection. Consider create_autospec(Session) for full chain safety.
@@ -106,7 +106,7 @@ def mock_db():
# region service [TYPE Fixture]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: LLMProviderService fixture wired to mock_db for provider CRUD tests.
@pytest.fixture
def service(mock_db):
@@ -117,7 +117,7 @@ def service(mock_db):
# region test_get_all_providers [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify provider list retrieval issues query/all calls on the backing DB session.
def test_get_all_providers(service, mock_db):
service.get_all_providers()
@@ -129,7 +129,7 @@ def test_get_all_providers(service, mock_db):
# region test_create_provider [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure provider creation persists entity and stores API key in encrypted form.
def test_create_provider(service, mock_db):
config = LLMProviderConfig(
@@ -155,7 +155,7 @@ def test_create_provider(service, mock_db):
# region test_get_decrypted_api_key [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify service decrypts stored provider API key for an existing provider record.
def test_get_decrypted_api_key(service, mock_db):
# Setup mock provider
@@ -171,7 +171,7 @@ def test_get_decrypted_api_key(service, mock_db):
# region test_get_decrypted_api_key_not_found [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify missing provider lookup returns None instead of attempting decryption.
def test_get_decrypted_api_key_not_found(service, mock_db):
mock_db.query().filter().first.return_value = None
@@ -182,7 +182,7 @@ def test_get_decrypted_api_key_not_found(service, mock_db):
# region test_update_provider_ignores_masked_placeholder_api_key [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Ensure masked placeholder API keys do not overwrite previously encrypted provider secrets.
def test_update_provider_ignores_masked_placeholder_api_key(service, mock_db):
existing_encrypted = EncryptionManager().encrypt("secret-value")
@@ -218,15 +218,15 @@ def test_update_provider_ignores_masked_placeholder_api_key(service, mock_db):
# region test_mask_api_key [TYPE Function]
# @RELATION: VERIFIES -> [mask_api_key]
# @RELATION BINDS_TO -> [mask_api_key]
# @PURPOSE: Verify mask_api_key produces correct masked strings for various key lengths and edge cases.
# @INVARIANT: mask_api_key never reveals more than 4 chars from any position.
# @TEST_EDGE: None -> ""
# @TEST_EDGE: "" -> ""
# @TEST_EDGE: "abcd" -> "****"
# @TEST_EDGE: "abcdef" -> "ab...ef"
# @TEST_EDGE: "abcdefgh" -> "ab...gh"
# @TEST_EDGE: "sk-test-key-1234abcd" -> "sk-t...abcd"
# @INVARIANT mask_api_key never reveals more than 4 chars from any position.
# @TEST_EDGE None -> ""
# @TEST_EDGE "" -> ""
# @TEST_EDGE "abcd" -> "****"
# @TEST_EDGE "abcdef" -> "ab...ef"
# @TEST_EDGE "abcdefgh" -> "ab...gh"
# @TEST_EDGE "sk-test-key-1234abcd" -> "sk-t...abcd"
def test_mask_api_key():
assert mask_api_key(None) == ""
assert mask_api_key("") == ""
@@ -240,16 +240,16 @@ def test_mask_api_key():
# region test_is_masked_or_placeholder [TYPE Function]
# @RELATION: VERIFIES -> [is_masked_or_placeholder]
# @RELATION BINDS_TO -> [is_masked_or_placeholder]
# @PURPOSE: Verify predicate correctly identifies all forms of masked/placeholder API keys.
# @INVARIANT: Real API keys (no "..." pattern) always return False.
# @TEST_EDGE: None -> True
# @TEST_EDGE: "" -> True
# @TEST_EDGE: "********" -> True
# @TEST_EDGE: "sk-...abcd" -> True
# @TEST_EDGE: "...xyz" -> True
# @TEST_EDGE: "sk-real-key-1234" -> False
# @TEST_EDGE: "short" -> False
# @INVARIANT Real API keys (no "..." pattern) always return False.
# @TEST_EDGE None -> True
# @TEST_EDGE "" -> True
# @TEST_EDGE "********" -> True
# @TEST_EDGE "sk-...abcd" -> True
# @TEST_EDGE "...xyz" -> True
# @TEST_EDGE "sk-real-key-1234" -> False
# @TEST_EDGE "short" -> False
def test_is_masked_or_placeholder():
assert is_masked_or_placeholder(None) is True
assert is_masked_or_placeholder("") is True
@@ -264,7 +264,7 @@ def test_is_masked_or_placeholder():
# region test_create_provider_with_multimodal_flag [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify provider creation persists the is_multimodal flag.
def test_create_provider_with_multimodal_flag(service, mock_db):
"""Verify is_multimodal=True is stored during provider creation."""
@@ -289,7 +289,7 @@ def test_create_provider_with_multimodal_flag(service, mock_db):
# region test_create_provider_without_multimodal_flag [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify is_multimodal defaults to False when not specified.
def test_create_provider_without_multimodal_flag(service, mock_db):
"""Verify is_multimodal defaults to False."""
@@ -311,7 +311,7 @@ def test_create_provider_without_multimodal_flag(service, mock_db):
# region test_update_provider_preserves_is_multimodal [TYPE Function]
# @RELATION: BINDS_TO -> [test_llm_provider:Module]
# @RELATION BINDS_TO -> [EXT:frontend:test_llm_provider]
# @PURPOSE: Verify updating a provider preserves and correctly sets is_multimodal.
def test_update_provider_preserves_is_multimodal(service, mock_db):
"""Verify is_multimodal is updated correctly."""
@@ -349,7 +349,7 @@ def test_update_provider_preserves_is_multimodal(service, mock_db):
# region test_llm_provider_config_multimodal_default [TYPE Function]
# @RELATION: VERIFIES -> [LLMProviderConfig]
# @RELATION BINDS_TO -> [LLMProviderConfig]
# @PURPOSE: Verify LLMProviderConfig.is_multimodal defaults to False for schema stability.
def test_llm_provider_config_multimodal_default():
"""Verify default is_multimodal is False in schema."""
@@ -367,7 +367,7 @@ def test_llm_provider_config_multimodal_default():
# region test_llm_provider_config_multimodal_explicit [TYPE Function]
# @RELATION: VERIFIES -> [LLMProviderConfig]
# @RELATION BINDS_TO -> [LLMProviderConfig]
# @PURPOSE: Verify LLMProviderConfig accepts explicit is_multimodal=True.
def test_llm_provider_config_multimodal_explicit():
"""Verify setting is_multimodal=True explicitly works."""

View File

@@ -1,9 +1,9 @@
# region test_rbac_permission_catalog [TYPE Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @RELATION BELONGS_TO -> SrcRoot
# @SEMANTICS: tests, rbac, permissions, catalog, discovery, sync
# @PURPOSE: Verifies RBAC permission catalog discovery and idempotent synchronization behavior.
# @LAYER: Service Tests
# @INVARIANT: Synchronization adds only missing normalized permission pairs.
# @LAYER Service Tests
# @INVARIANT Synchronization adds only missing normalized permission pairs.
# [SECTION: IMPORTS]
from types import SimpleNamespace
@@ -15,10 +15,10 @@ import src.services.rbac_permission_catalog as catalog
# region test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests [TYPE Function]
# @RELATION: BINDS_TO -> test_rbac_permission_catalog
# @RELATION BINDS_TO -> test_rbac_permission_catalog
# @PURPOSE: Ensures route-scanner extracts has_permission pairs from route files and skips __tests__.
# @PRE: Temporary route directory contains route and test files.
# @POST: Returned set includes production route permissions and excludes test-only declarations.
# @PRE Temporary route directory contains route and test files.
# @POST Returned set includes production route permissions and excludes test-only declarations.
def test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests(tmp_path, monkeypatch):
routes_dir = tmp_path / "routes"
routes_dir.mkdir(parents=True, exist_ok=True)
@@ -53,10 +53,10 @@ def test_discover_route_permissions_extracts_declared_pairs_and_ignores_tests(tm
# region test_discover_declared_permissions_unions_route_and_plugin_permissions [TYPE Function]
# @RELATION: BINDS_TO -> test_rbac_permission_catalog
# @RELATION BINDS_TO -> test_rbac_permission_catalog
# @PURPOSE: Ensures full catalog includes route-level permissions plus dynamic plugin EXECUTE rights.
# @PRE: Route discovery and plugin loader both return permission sources.
# @POST: Result set contains union of both sources.
# @PRE Route discovery and plugin loader both return permission sources.
# @POST Result set contains union of both sources.
def test_discover_declared_permissions_unions_route_and_plugin_permissions(monkeypatch):
monkeypatch.setattr(
catalog,
@@ -80,10 +80,10 @@ def test_discover_declared_permissions_unions_route_and_plugin_permissions(monke
# region test_sync_permission_catalog_inserts_only_missing_normalized_pairs [TYPE Function]
# @RELATION: BINDS_TO -> test_rbac_permission_catalog
# @RELATION BINDS_TO -> test_rbac_permission_catalog
# @PURPOSE: Ensures synchronization inserts only missing pairs and normalizes action/resource tokens.
# @PRE: DB already contains subset of permissions.
# @POST: Only missing normalized pairs are inserted and commit is executed once.
# @PRE DB already contains subset of permissions.
# @POST Only missing normalized pairs are inserted and commit is executed once.
def test_sync_permission_catalog_inserts_only_missing_normalized_pairs():
db = MagicMock()
db.query.return_value.all.return_value = [
@@ -114,10 +114,10 @@ def test_sync_permission_catalog_inserts_only_missing_normalized_pairs():
# region test_sync_permission_catalog_is_noop_when_all_permissions_exist [TYPE Function]
# @RELATION: BINDS_TO -> test_rbac_permission_catalog
# @RELATION BINDS_TO -> test_rbac_permission_catalog
# @PURPOSE: Ensures synchronization is idempotent when all declared pairs already exist.
# @PRE: DB contains full declared permission set.
# @POST: No inserts are added and commit is not called.
# @PRE DB contains full declared permission set.
# @POST No inserts are added and commit is not called.
def test_sync_permission_catalog_is_noop_when_all_permissions_exist():
db = MagicMock()
db.query.return_value.all.return_value = [

View File

@@ -1,9 +1,9 @@
# region TestResourceService [TYPE Module]
# @SEMANTICS: resource-service, tests, dashboards, datasets, activity
# @PURPOSE: Unit tests for ResourceService
# @LAYER: Service
# @RELATION: VERIFIES ->[src.services.resource_service.ResourceService]
# @INVARIANT: Resource summaries preserve task linkage and status projection behavior.
# @LAYER Service
# @RELATION BINDS_TO ->[ResourceService]
# @INVARIANT Resource summaries preserve task linkage and status projection behavior.
from datetime import UTC, datetime
import pytest
@@ -11,11 +11,11 @@ from unittest.mock import MagicMock, patch
# region test_get_dashboards_with_status [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @PURPOSE: Validate dashboard enrichment includes git/task status projections.
# @TEST: get_dashboards_with_status returns dashboards with git and task status
# @PRE: SupersetClient returns dashboard list
# @POST: Each dashboard has git_status and last_task fields
# @PRE SupersetClient returns dashboard list
# @POST Each dashboard has git_status and last_task fields
@pytest.mark.asyncio
async def test_get_dashboards_with_status():
with (
@@ -76,10 +76,10 @@ async def test_get_dashboards_with_status():
# region test_get_datasets_with_status [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_datasets_with_status returns datasets with task status
# @PRE: SupersetClient returns dataset list
# @POST: Each dataset has last_task field
# @PRE SupersetClient returns dataset list
# @POST Each dataset has last_task field
# @PURPOSE: Verify ResourceService.get_datasets_with_status returns datasets grouped by validation status.
@pytest.mark.asyncio
async def test_get_datasets_with_status():
@@ -117,10 +117,10 @@ async def test_get_datasets_with_status():
# region test_get_activity_summary [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_activity_summary returns active count and recent tasks
# @PRE: tasks list provided
# @POST: Returns dict with active_count and recent_tasks
# @PRE tasks list provided
# @POST Returns dict with active_count and recent_tasks
# @PURPOSE: Verify ResourceService.get_activity_summary returns recent task activity.
def test_get_activity_summary():
from src.services.resource_service import ResourceService
@@ -156,10 +156,10 @@ def test_get_activity_summary():
# region test_get_git_status_for_dashboard_no_repo [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_git_status_for_dashboard returns None when no repo exists
# @PRE: GitService returns None for repo
# @POST: Returns None
# @PRE GitService returns None for repo
# @POST Returns None
# @PURPOSE: Verify get_git_status_for_dashboard returns None when no repo exists.
def test_get_git_status_for_dashboard_no_repo():
with patch("src.services.resource_service.GitService") as mock_git:
@@ -179,10 +179,10 @@ def test_get_git_status_for_dashboard_no_repo():
# region test_get_last_task_for_resource [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_last_task_for_resource returns most recent task for resource
# @PRE: tasks list with matching resource_id
# @POST: Returns task summary with task_id and status
# @PRE tasks list with matching resource_id
# @POST Returns task summary with task_id and status
# @PURPOSE: Verify get_last_task_for_resource returns the most recent task for a given resource.
def test_get_last_task_for_resource():
from src.services.resource_service import ResourceService
@@ -213,10 +213,10 @@ def test_get_last_task_for_resource():
# region test_extract_resource_name_from_task [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _extract_resource_name_from_task extracts name from params
# @PRE: task has resource_name in params
# @POST: Returns resource name or fallback
# @PRE task has resource_name in params
# @POST Returns resource name or fallback
# @PURPOSE: Verify extract_resource_name_from_task correctly parses resource names from task identifiers.
def test_extract_resource_name_from_task():
from src.services.resource_service import ResourceService
@@ -244,10 +244,10 @@ def test_extract_resource_name_from_task():
# region test_get_last_task_for_resource_empty_tasks [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_last_task_for_resource returns None for empty tasks list
# @PRE: tasks is empty list
# @POST: Returns None
# @PRE tasks is empty list
# @POST Returns None
# @PURPOSE: Verify get_last_task_for_resource returns None when tasks list is empty.
def test_get_last_task_for_resource_empty_tasks():
from src.services.resource_service import ResourceService
@@ -262,10 +262,10 @@ def test_get_last_task_for_resource_empty_tasks():
# region test_get_last_task_for_resource_no_match [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_last_task_for_resource returns None when no tasks match resource_id
# @PRE: tasks list has no matching resource_id
# @POST: Returns None
# @PRE tasks list has no matching resource_id
# @POST Returns None
# @PURPOSE: Verify get_last_task_for_resource returns None when no task matches the resource.
def test_get_last_task_for_resource_no_match():
from src.services.resource_service import ResourceService
@@ -286,10 +286,10 @@ def test_get_last_task_for_resource_no_match():
# region test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_dashboards_with_status handles mixed naive/aware datetimes without comparison errors.
# @PRE: Task list includes both timezone-aware and timezone-naive timestamps.
# @POST: Latest task is selected deterministically and no exception is raised.
# @PRE Task list includes both timezone-aware and timezone-naive timestamps.
# @POST Latest task is selected deterministically and no exception is raised.
# @PURPOSE: Verify get_dashboards_with_status handles mixed naive and aware datetimes without crashing.
@pytest.mark.asyncio
async def test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_datetimes():
@@ -330,10 +330,10 @@ async def test_get_dashboards_with_status_handles_mixed_naive_and_aware_task_dat
# region test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_dashboards_with_status keeps latest task identity while falling back to older decisive validation status.
# @PRE: Same dashboard has older WARN and newer UNKNOWN validation tasks.
# @POST: Returned last_task points to newest task but preserves WARN as last meaningful validation state.
# @PRE Same dashboard has older WARN and newer UNKNOWN validation tasks.
# @POST Returned last_task points to newest task but preserves WARN as last meaningful validation state.
# @PURPOSE: Verify status ranking prefers decisive validation over newer unknown status.
@pytest.mark.anyio
async def test_get_dashboards_with_status_prefers_latest_decisive_validation_status_over_newer_unknown():
@@ -380,10 +380,10 @@ async def test_get_dashboards_with_status_prefers_latest_decisive_validation_sta
# region test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: get_dashboards_with_status still returns newest UNKNOWN when no decisive validation exists.
# @PRE: Same dashboard has only UNKNOWN validation tasks.
# @POST: Returned last_task keeps newest UNKNOWN task.
# @PRE Same dashboard has only UNKNOWN validation tasks.
# @POST Returned last_task keeps newest UNKNOWN task.
# @PURPOSE: Verify fallback to latest unknown status when no decisive history exists.
@pytest.mark.anyio
async def test_get_dashboards_with_status_falls_back_to_latest_unknown_without_decisive_history():
@@ -429,10 +429,10 @@ async def test_get_dashboards_with_status_falls_back_to_latest_unknown_without_d
# region test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at [TYPE Function]
# @RELATION: BINDS_TO ->[TestResourceService]
# @RELATION BINDS_TO ->[EXT:frontend:TestResourceService]
# @TEST: _get_last_task_for_resource handles mixed naive/aware created_at values.
# @PRE: Matching tasks include naive and aware created_at timestamps.
# @POST: Latest task is returned without raising datetime comparison errors.
# @PRE Matching tasks include naive and aware created_at timestamps.
# @POST Latest task is returned without raising datetime comparison errors.
# @PURPOSE: Verify get_last_task_for_resource correctly sorts mixed naive and aware created_at timestamps.
def test_get_last_task_for_resource_handles_mixed_naive_and_aware_created_at():
from src.services.resource_service import ResourceService

View File

@@ -1,16 +1,16 @@
# #region auth_service [C:5] [TYPE Module] [SEMANTICS sqlalchemy, auth, credential, session, jwt]
# @BRIEF Orchestrates credential authentication and ADFS JIT user provisioning.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [AuthRepository]
# @RELATION DEPENDS_ON -> [verify_password]
# @RELATION DEPENDS_ON -> [create_access_token]
# @RELATION DEPENDS_ON -> [User]
# @RELATION DEPENDS_ON -> [Role]
# @INVARIANT: Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles.
# @PRE: Core auth models and security utilities available.
# @POST: User identity verified and session tokens issued according to role scopes.
# @SIDE_EFFECT: Writes last login timestamps and JIT-provisions external users.
# @DATA_CONTRACT: [Credentials | ADFSClaims] -> [UserEntity | SessionToken]
# @INVARIANT Authentication succeeds only for active users with valid credentials; issued sessions encode subject and scopes from assigned roles.
# @PRE Core auth models and security utilities available.
# @POST User identity verified and session tokens issued according to role scopes.
# @SIDE_EFFECT Writes last login timestamps and JIT-provisions external users.
# @DATA_CONTRACT [Credentials | ADFSClaims] -> [UserEntity | SessionToken]
from datetime import datetime
from typing import Any
@@ -33,11 +33,11 @@ from ..models.auth import User
class AuthService:
# region AuthService_init [TYPE Function]
# @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.
# @POST: self.repo is initialized and ready for auth user/role CRUD operations.
# @SIDE_EFFECT: Allocates AuthRepository and binds it to the provided Session.
# @DATA_CONTRACT: Input(Session) -> Model(AuthRepository)
# @PARAM: db (Session) - SQLAlchemy session.
# @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.
# @SIDE_EFFECT Allocates AuthRepository and binds it to the provided Session.
# @DATA_CONTRACT Input(Session) -> Model(AuthRepository)
# @PARAM db (Session) - SQLAlchemy session.
def __init__(self, db: Session):
self.db = db
self.repo = AuthRepository(db)
@@ -46,16 +46,16 @@ class AuthService:
# region AuthService.authenticate_user [TYPE Function]
# @PURPOSE: Validates credentials and account state for local username/password authentication.
# @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.
# @SIDE_EFFECT: Persists last_login update for successful authentications via repository.
# @DATA_CONTRACT: Input(str username, str password) -> Output(User | None)
# @RELATION: [DEPENDS_ON] ->[AuthRepository]
# @RELATION: [CALLS] ->[verify_password]
# @RELATION: [DEPENDS_ON] ->[User]
# @PARAM: username (str) - The username.
# @PARAM: password (str) - The plain password.
# @RETURN: Optional[User] - The authenticated user or None.
# @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.
# @SIDE_EFFECT Persists last_login update for successful authentications via repository.
# @DATA_CONTRACT Input(str username, str password) -> Output(User | None)
# @RELATION DEPENDS_ON ->[AuthRepository]
# @RELATION CALLS ->[verify_password]
# @RELATION DEPENDS_ON ->[User]
# @PARAM username (str) - The username.
# @PARAM password (str) - The plain password.
# @RETURN Optional[User] - The authenticated user or None.
def authenticate_user(self, username: str, password: str) -> User | None:
with belief_scope("auth.authenticate_user"):
user = self.repo.get_user_by_username(username)
@@ -76,15 +76,15 @@ class AuthService:
# region AuthService.create_session [TYPE Function]
# @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.
# @POST: Returns session dict with non-empty access_token and token_type='bearer'.
# @SIDE_EFFECT: Generates signed JWT via auth JWT provider.
# @DATA_CONTRACT: Input(User) -> Output(Dict[str, str]{access_token, token_type})
# @RELATION: [CALLS] ->[create_access_token]
# @RELATION: [DEPENDS_ON] ->[User]
# @RELATION: [DEPENDS_ON] ->[Role]
# @PARAM: user (User) - The authenticated user.
# @RETURN: Dict[str, str] - Session data.
# @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'.
# @SIDE_EFFECT Generates signed JWT via auth JWT provider.
# @DATA_CONTRACT Input(User) -> Output(Dict[str, str]{access_token, token_type})
# @RELATION CALLS ->[create_access_token]
# @RELATION DEPENDS_ON ->[User]
# @RELATION DEPENDS_ON ->[Role]
# @PARAM user (User) - The authenticated user.
# @RETURN Dict[str, str] - Session data.
def create_session(self, user: User) -> dict[str, str]:
with belief_scope("auth.create_session"):
roles = [role.name for role in user.roles]
@@ -97,15 +97,15 @@ class AuthService:
# region AuthService.provision_adfs_user [TYPE Function]
# @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.
# @POST: Returns persisted user entity with roles synchronized to mapped AD groups and refreshed state.
# @SIDE_EFFECT: May insert new User, mutate user.roles, commit transaction, and refresh ORM state.
# @DATA_CONTRACT: Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)
# @RELATION: [DEPENDS_ON] ->[AuthRepository]
# @RELATION: [DEPENDS_ON] ->[User]
# @RELATION: [DEPENDS_ON] ->[Role]
# @PARAM: user_info (Dict[str, Any]) - Claims from ADFS token.
# @RETURN: User - The provisioned user.
# @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.
# @SIDE_EFFECT May insert new User, mutate user.roles, commit transaction, and refresh ORM state.
# @DATA_CONTRACT Input(Dict[str, Any]{upn|email, email, groups[]}) -> Output(User persisted)
# @RELATION DEPENDS_ON ->[AuthRepository]
# @RELATION DEPENDS_ON ->[User]
# @RELATION DEPENDS_ON ->[Role]
# @PARAM user_info (Dict[str, Any]) - Claims from ADFS token.
# @RETURN User - The provisioned user.
def provision_adfs_user(self, user_info: dict[str, Any]) -> User:
with belief_scope("auth.provision_adfs_user"):
username = user_info.get("upn") or user_info.get("email")

View File

@@ -1,6 +1,6 @@
# #region CleanReleaseContracts [C:3] [TYPE Module] [SEMANTICS clean-release, compliance, package, subsystem]
# @BRIEF Publish the canonical semantic root for the clean-release backend service cluster.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [ComplianceOrchestrator]
# @RELATION DEPENDS_ON -> [ManifestBuilder]
# @RELATION DEPENDS_ON -> [PolicyEngine]

View File

@@ -1,8 +1,8 @@
# region TestAuditService [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[AuditService]
# @RELATION DEPENDS_ON ->[AuditService]
# @SEMANTICS: tests, clean-release, audit, logging
# @PURPOSE: Validate audit hooks emit expected log patterns for clean release lifecycle.
# @LAYER: Infra
# @LAYER Infrastructure
from unittest.mock import patch
@@ -15,7 +15,7 @@ from src.services.clean_release.audit_service import (
@patch("src.services.clean_release.audit_service.logger")
# region test_audit_preparation [TYPE Function]
# @RELATION: BINDS_TO -> TestAuditService
# @RELATION BINDS_TO -> TestAuditService
# @PURPOSE: Verify audit preparation stage correctly initializes and validates candidate state.
def test_audit_preparation(mock_logger):
audit_preparation("cand-1", "PREPARED")
@@ -29,7 +29,7 @@ def test_audit_preparation(mock_logger):
@patch("src.services.clean_release.audit_service.logger")
# region test_audit_check_run [TYPE Function]
# @RELATION: BINDS_TO -> TestAuditService
# @RELATION BINDS_TO -> TestAuditService
# @PURPOSE: Verify audit check run executes all checks and collects results.
def test_audit_check_run(mock_logger):
audit_check_run("check-1", "COMPLIANT")
@@ -43,7 +43,7 @@ def test_audit_check_run(mock_logger):
@patch("src.services.clean_release.audit_service.logger")
# region test_audit_report [TYPE Function]
# @RELATION: BINDS_TO -> TestAuditService
# @RELATION BINDS_TO -> TestAuditService
# @PURPOSE: Verify audit report generation aggregates check results into a structured report.
def test_audit_report(mock_logger):
audit_report("rep-1", "cand-1")

View File

@@ -1,9 +1,9 @@
# region TestComplianceOrchestrator [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[ComplianceOrchestrator]
# @RELATION DEPENDS_ON ->[ComplianceOrchestrator]
# @SEMANTICS: tests, clean-release, orchestrator, stage-state-machine
# @PURPOSE: Validate compliance orchestrator stage transitions and final status derivation.
# @LAYER: Domain
# @INVARIANT: Failed mandatory stage forces BLOCKED terminal status.
# @LAYER Domain
# @INVARIANT Failed mandatory stage forces BLOCKED terminal status.
import pytest
from unittest.mock import patch
@@ -22,7 +22,7 @@ from src.services.clean_release.repository import CleanReleaseRepository
# region test_orchestrator_stage_failure_blocks_release [TYPE Function]
# @RELATION: BINDS_TO -> TestComplianceOrchestrator
# @RELATION BINDS_TO -> TestComplianceOrchestrator
# @PURPOSE: Verify mandatory stage failure forces BLOCKED final status.
def test_orchestrator_stage_failure_blocks_release():
repository = CleanReleaseRepository()
@@ -68,7 +68,7 @@ def test_orchestrator_stage_failure_blocks_release():
# region test_orchestrator_compliant_candidate [TYPE Function]
# @RELATION: BINDS_TO -> TestComplianceOrchestrator
# @RELATION BINDS_TO -> TestComplianceOrchestrator
# @PURPOSE: Verify happy path where all mandatory stages pass yields COMPLIANT.
def test_orchestrator_compliant_candidate():
repository = CleanReleaseRepository()
@@ -114,7 +114,7 @@ def test_orchestrator_compliant_candidate():
# region test_orchestrator_missing_stage_result [TYPE Function]
# @RELATION: BINDS_TO -> TestComplianceOrchestrator
# @RELATION BINDS_TO -> TestComplianceOrchestrator
# @PURPOSE: Verify incomplete mandatory stage set cannot end as COMPLIANT and results in FAILED.
def test_orchestrator_missing_stage_result():
repository = CleanReleaseRepository()
@@ -140,7 +140,7 @@ def test_orchestrator_missing_stage_result():
# region test_orchestrator_report_generation_error [TYPE Function]
# @RELATION: BINDS_TO -> TestComplianceOrchestrator
# @RELATION BINDS_TO -> TestComplianceOrchestrator
# @PURPOSE: Verify downstream report errors do not mutate orchestrator final status.
def test_orchestrator_report_generation_error():
repository = CleanReleaseRepository()

View File

@@ -1,22 +1,22 @@
# region TestManifestBuilder [TYPE Module]
# @SEMANTICS: tests, clean-release, manifest, deterministic
# @PURPOSE: Validate deterministic manifest generation behavior for US1.
# @LAYER: Domain
# @RELATION: [DEPENDS_ON] ->[ManifestBuilder]
# @INVARIANT: Same input artifacts produce identical deterministic hash.
# @PRE: Test fixtures are properly initialized
# @POST: All test assertions pass
# @SIDE_EFFECT: None - test isolation
# @DATA_CONTRACT: TestInput -> TestOutput
# @LAYER Domain
# @RELATION DEPENDS_ON ->[ManifestBuilder]
# @INVARIANT Same input artifacts produce identical deterministic hash.
# @PRE Test fixtures are properly initialized
# @POST All test assertions pass
# @SIDE_EFFECT None - test isolation
# @DATA_CONTRACT TestInput -> TestOutput
from src.services.clean_release.manifest_builder import build_distribution_manifest
# region test_manifest_deterministic_hash_for_same_input [TYPE Function]
# @RELATION: BINDS_TO -> TestManifestBuilder
# @RELATION BINDS_TO -> TestManifestBuilder
# @PURPOSE: Ensure hash is stable for same candidate/policy/artifact input.
# @PRE: Same input lists are passed twice.
# @POST: Hash and summary remain identical.
# @PRE Same input lists are passed twice.
# @POST Hash and summary remain identical.
def test_manifest_deterministic_hash_for_same_input():
artifacts = [
{

View File

@@ -1,5 +1,5 @@
# region TestPolicyEngine [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[PolicyEngine]
# @RELATION DEPENDS_ON ->[PolicyEngine]
# @PURPOSE: Contract testing for CleanPolicyEngine
# endregion TestPolicyEngine
@@ -16,7 +16,7 @@ from src.models.clean_release import (
from src.services.clean_release.policy_engine import CleanPolicyEngine
# @TEST_FIXTURE: policy_enterprise_clean
# @TEST_FIXTURE policy_enterprise_clean
@pytest.fixture
def enterprise_clean_setup():
policy = CleanProfilePolicy(
@@ -48,9 +48,9 @@ def enterprise_clean_setup():
return policy, registry
# @TEST_SCENARIO: policy_valid
# @TEST_SCENARIO policy_valid
# region test_policy_valid [TYPE Function]
# @RELATION: BINDS_TO -> TestPolicyEngine
# @RELATION BINDS_TO -> TestPolicyEngine
# @PURPOSE: Verify policy validation passes when all required fields are present and valid.
def test_policy_valid(enterprise_clean_setup):
policy, registry = enterprise_clean_setup
@@ -60,12 +60,12 @@ def test_policy_valid(enterprise_clean_setup):
assert not result.blocking_reasons
# @TEST_EDGE: missing_registry_ref
# @TEST_EDGE missing_registry_ref
# endregion test_policy_valid
# region test_missing_registry_ref [TYPE Function]
# @RELATION: BINDS_TO -> TestPolicyEngine
# @RELATION BINDS_TO -> TestPolicyEngine
# @PURPOSE: Verify policy validation fails when registry_ref is missing.
def test_missing_registry_ref(enterprise_clean_setup):
policy, registry = enterprise_clean_setup
@@ -76,12 +76,12 @@ def test_missing_registry_ref(enterprise_clean_setup):
assert "Policy missing internal_source_registry_ref" in result.blocking_reasons
# @TEST_EDGE: conflicting_registry
# @TEST_EDGE conflicting_registry
# endregion test_missing_registry_ref
# region test_conflicting_registry [TYPE Function]
# @RELATION: BINDS_TO -> TestPolicyEngine
# @RELATION BINDS_TO -> TestPolicyEngine
# @PURPOSE: Verify policy engine rejects conflicting registry references.
def test_conflicting_registry(enterprise_clean_setup):
policy, registry = enterprise_clean_setup
@@ -95,12 +95,12 @@ def test_conflicting_registry(enterprise_clean_setup):
)
# @TEST_INVARIANT: deterministic_classification
# @TEST_INVARIANT deterministic_classification
# endregion test_conflicting_registry
# region test_classify_artifact [TYPE Function]
# @RELATION: BINDS_TO -> TestPolicyEngine
# @RELATION BINDS_TO -> TestPolicyEngine
# @PURPOSE: Verify policy engine correctly classifies artifacts based on source and type.
def test_classify_artifact(enterprise_clean_setup):
policy, registry = enterprise_clean_setup
@@ -120,12 +120,12 @@ def test_classify_artifact(enterprise_clean_setup):
assert engine.classify_artifact({"category": "others", "path": "p3"}) == "allowed"
# @TEST_EDGE: external_endpoint
# @TEST_EDGE external_endpoint
# endregion test_classify_artifact
# region test_validate_resource_source [TYPE Function]
# @RELATION: BINDS_TO -> TestPolicyEngine
# @RELATION BINDS_TO -> TestPolicyEngine
# @PURPOSE: Verify validate_resource_source correctly validates or rejects resource source identifiers.
def test_validate_resource_source(enterprise_clean_setup):
policy, registry = enterprise_clean_setup
@@ -146,7 +146,7 @@ def test_validate_resource_source(enterprise_clean_setup):
# region test_evaluate_candidate [TYPE Function]
# @RELATION: BINDS_TO -> TestPolicyEngine
# @RELATION BINDS_TO -> TestPolicyEngine
# @PURPOSE: Verify policy engine evaluates release candidates against configured policies.
def test_evaluate_candidate(enterprise_clean_setup):
policy, registry = enterprise_clean_setup

View File

@@ -1,9 +1,9 @@
# region TestPreparationService [TYPE Module]
# @SEMANTICS: tests, clean-release, preparation, flow
# @PURPOSE: Validate release candidate preparation flow, including policy evaluation and manifest persisting.
# @LAYER: Domain
# @RELATION: [DEPENDS_ON] ->[PreparationService]
# @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically.
# @LAYER Domain
# @RELATION DEPENDS_ON ->[PreparationService]
# @INVARIANT Candidate preparation always persists manifest and candidate status deterministically.
from datetime import UTC, datetime
import pytest
@@ -21,7 +21,7 @@ from src.services.clean_release.preparation_service import prepare_candidate
# region _mock_policy [TYPE Function]
# @RELATION: BINDS_TO -> TestPreparationService
# @RELATION BINDS_TO -> TestPreparationService
# @PURPOSE: Build a valid clean profile policy fixture for preparation tests.
def _mock_policy() -> CleanProfilePolicy:
return CleanProfilePolicy(
@@ -41,7 +41,7 @@ def _mock_policy() -> CleanProfilePolicy:
# region _mock_registry [TYPE Function]
# @RELATION: BINDS_TO -> TestPreparationService
# @RELATION BINDS_TO -> TestPreparationService
# @PURPOSE: Build an internal-only source registry fixture for preparation tests.
def _mock_registry() -> ResourceSourceRegistry:
return ResourceSourceRegistry(
@@ -65,7 +65,7 @@ def _mock_registry() -> ResourceSourceRegistry:
# region _mock_candidate [TYPE Function]
# @RELATION: BINDS_TO -> TestPreparationService
# @RELATION BINDS_TO -> TestPreparationService
# @PURPOSE: Build a draft release candidate fixture with provided identifier.
def _mock_candidate(candidate_id: str) -> ReleaseCandidate:
return ReleaseCandidate(
@@ -83,13 +83,13 @@ def _mock_candidate(candidate_id: str) -> ReleaseCandidate:
# region test_prepare_candidate_success [TYPE Function]
# @RELATION: BINDS_TO -> TestPreparationService
# @RELATION BINDS_TO -> TestPreparationService
# @PURPOSE: Verify candidate transitions to PREPARED when evaluation returns no violations.
# @TEST_CONTRACT: [valid_candidate + active_policy + internal_sources + no_violations] -> [status=PREPARED, manifest_persisted, candidate_saved]
# @TEST_SCENARIO: [prepare_success] -> [prepared status and persistence side effects are produced]
# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE: [external_fail] -> [none; dependency interactions mocked and successful]
# @TEST_INVARIANT: [prepared_flow_persists_state] -> VERIFIED_BY: [prepare_success]
# @TEST_CONTRACT [valid_candidate + active_policy + internal_sources + no_violations] -> [status=PREPARED, manifest_persisted, candidate_saved]
# @TEST_SCENARIO [prepare_success] -> [prepared status and persistence side effects are produced]
# @TEST_FIXTURE [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE [external_fail] -> [none; dependency interactions mocked and successful]
# @TEST_INVARIANT [prepared_flow_persists_state] -> VERIFIED_BY: [prepare_success]
def test_prepare_candidate_success():
# Setup
repository = MagicMock()
@@ -135,13 +135,13 @@ def test_prepare_candidate_success():
# region test_prepare_candidate_with_violations [TYPE Function]
# @RELATION: BINDS_TO -> TestPreparationService
# @RELATION BINDS_TO -> TestPreparationService
# @PURPOSE: Verify candidate transitions to BLOCKED when evaluation returns blocking violations.
# @TEST_CONTRACT: [valid_candidate + active_policy + evaluation_with_violations] -> [status=BLOCKED, violations_exposed]
# @TEST_SCENARIO: [prepare_blocked_due_to_policy] -> [blocked status and violation list are produced]
# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE: [external_fail] -> [none; dependency interactions mocked and successful]
# @TEST_INVARIANT: [blocked_flow_reports_violations] -> VERIFIED_BY: [prepare_blocked_due_to_policy]
# @TEST_CONTRACT [valid_candidate + active_policy + evaluation_with_violations] -> [status=BLOCKED, violations_exposed]
# @TEST_SCENARIO [prepare_blocked_due_to_policy] -> [blocked status and violation list are produced]
# @TEST_FIXTURE [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE [external_fail] -> [none; dependency interactions mocked and successful]
# @TEST_INVARIANT [blocked_flow_reports_violations] -> VERIFIED_BY: [prepare_blocked_due_to_policy]
def test_prepare_candidate_with_violations():
# Setup
repository = MagicMock()
@@ -186,13 +186,13 @@ def test_prepare_candidate_with_violations():
# region test_prepare_candidate_not_found [TYPE Function]
# @RELATION: BINDS_TO -> TestPreparationService
# @RELATION BINDS_TO -> TestPreparationService
# @PURPOSE: Verify preparation raises ValueError when candidate does not exist.
# @TEST_CONTRACT: [missing_candidate] -> [ValueError('Candidate not found')]
# @TEST_SCENARIO: [prepare_missing_candidate] -> [raises candidate not found error]
# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE: [missing_field] -> [candidate lookup returns None]
# @TEST_INVARIANT: [missing_candidate_is_rejected] -> VERIFIED_BY: [prepare_missing_candidate]
# @TEST_CONTRACT [missing_candidate] -> [ValueError('Candidate not found')]
# @TEST_SCENARIO [prepare_missing_candidate] -> [raises candidate not found error]
# @TEST_FIXTURE [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE [missing_field] -> [candidate lookup returns None]
# @TEST_INVARIANT [missing_candidate_is_rejected] -> VERIFIED_BY: [prepare_missing_candidate]
def test_prepare_candidate_not_found():
repository = MagicMock()
repository.get_candidate.return_value = None
@@ -205,13 +205,13 @@ def test_prepare_candidate_not_found():
# region test_prepare_candidate_no_active_policy [TYPE Function]
# @RELATION: BINDS_TO -> TestPreparationService
# @RELATION BINDS_TO -> TestPreparationService
# @PURPOSE: Verify preparation raises ValueError when no active policy is available.
# @TEST_CONTRACT: [candidate_present + missing_active_policy] -> [ValueError('Active clean policy not found')]
# @TEST_SCENARIO: [prepare_missing_policy] -> [raises active policy missing error]
# @TEST_FIXTURE: [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE: [invalid_type] -> [policy dependency resolves to None]
# @TEST_INVARIANT: [active_policy_required] -> VERIFIED_BY: [prepare_missing_policy]
# @TEST_CONTRACT [candidate_present + missing_active_policy] -> [ValueError('Active clean policy not found')]
# @TEST_SCENARIO [prepare_missing_policy] -> [raises active policy missing error]
# @TEST_FIXTURE [INLINE_MOCKS] -> INLINE_JSON
# @TEST_EDGE [invalid_type] -> [policy dependency resolves to None]
# @TEST_INVARIANT [active_policy_required] -> VERIFIED_BY: [prepare_missing_policy]
def test_prepare_candidate_no_active_policy():
repository = MagicMock()
repository.get_candidate.return_value = _mock_candidate("cand-1")

View File

@@ -1,9 +1,9 @@
# region TestReportBuilder [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[ReportBuilder]
# @RELATION DEPENDS_ON ->[ReportBuilder]
# @SEMANTICS: tests, clean-release, report-builder, counters
# @PURPOSE: Validate compliance report builder counter integrity and blocked-run constraints.
# @LAYER: Domain
# @INVARIANT: blocked run requires at least one blocking violation.
# @LAYER Domain
# @INVARIANT blocked run requires at least one blocking violation.
from datetime import UTC, datetime
import pytest
@@ -21,7 +21,7 @@ from src.services.clean_release.repository import CleanReleaseRepository
# region _terminal_run [TYPE Function]
# @RELATION: BINDS_TO -> TestReportBuilder
# @RELATION BINDS_TO -> TestReportBuilder
# @PURPOSE: Build terminal/non-terminal run fixtures for report builder tests.
def _terminal_run(status: CheckFinalStatus) -> ComplianceCheckRun:
return ComplianceCheckRun(
@@ -41,7 +41,7 @@ def _terminal_run(status: CheckFinalStatus) -> ComplianceCheckRun:
# region _blocking_violation [TYPE Function]
# @RELATION: BINDS_TO -> TestReportBuilder
# @RELATION BINDS_TO -> TestReportBuilder
# @PURPOSE: Build a blocking violation fixture for blocked report scenarios.
def _blocking_violation() -> ComplianceViolation:
return ComplianceViolation(
@@ -60,7 +60,7 @@ def _blocking_violation() -> ComplianceViolation:
# region test_report_builder_blocked_requires_blocking_violations [TYPE Function]
# @RELATION: BINDS_TO -> TestReportBuilder
# @RELATION BINDS_TO -> TestReportBuilder
# @PURPOSE: Verify BLOCKED run requires at least one blocking violation.
def test_report_builder_blocked_requires_blocking_violations():
builder = ComplianceReportBuilder(CleanReleaseRepository())
@@ -74,7 +74,7 @@ def test_report_builder_blocked_requires_blocking_violations():
# region test_report_builder_blocked_with_two_violations [TYPE Function]
# @RELATION: BINDS_TO -> TestReportBuilder
# @RELATION BINDS_TO -> TestReportBuilder
# @PURPOSE: Verify report builder generates conformant payload for a BLOCKED run with violations.
def test_report_builder_blocked_with_two_violations():
builder = ComplianceReportBuilder(CleanReleaseRepository())
@@ -97,7 +97,7 @@ def test_report_builder_blocked_with_two_violations():
# region test_report_builder_counter_consistency [TYPE Function]
# @RELATION: BINDS_TO -> TestReportBuilder
# @RELATION BINDS_TO -> TestReportBuilder
# @PURPOSE: Verify violations counters remain consistent for blocking payload.
def test_report_builder_counter_consistency():
builder = ComplianceReportBuilder(CleanReleaseRepository())
@@ -112,7 +112,7 @@ def test_report_builder_counter_consistency():
# region test_missing_operator_summary [TYPE Function]
# @RELATION: BINDS_TO -> TestReportBuilder
# @RELATION BINDS_TO -> TestReportBuilder
# @PURPOSE: Validate non-terminal run prevents operator summary/report generation.
def test_missing_operator_summary():
builder = ComplianceReportBuilder(CleanReleaseRepository())

View File

@@ -1,9 +1,9 @@
# region TestSourceIsolation [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[SourceIsolation]
# @RELATION DEPENDS_ON ->[SourceIsolation]
# @SEMANTICS: tests, clean-release, source-isolation, internal-only
# @PURPOSE: Verify internal source registry validation behavior.
# @LAYER: Domain
# @INVARIANT: External endpoints always produce blocking violations.
# @LAYER Domain
# @INVARIANT External endpoints always produce blocking violations.
from datetime import UTC, datetime
@@ -12,7 +12,7 @@ from src.services.clean_release.source_isolation import validate_internal_source
# region _registry [TYPE Function]
# @RELATION: BINDS_TO -> TestSourceIsolation
# @RELATION BINDS_TO -> TestSourceIsolation
def _registry() -> ResourceSourceRegistry:
return ResourceSourceRegistry(
registry_id="registry-internal-v1",
@@ -43,7 +43,7 @@ def _registry() -> ResourceSourceRegistry:
# region test_validate_internal_sources_all_internal_ok [TYPE Function]
# @RELATION: BINDS_TO -> TestSourceIsolation
# @RELATION BINDS_TO -> TestSourceIsolation
# @PURPOSE: Verify validate_internal_sources passes when all sources are internal and allowed.
def test_validate_internal_sources_all_internal_ok():
result = validate_internal_sources(
@@ -58,7 +58,7 @@ def test_validate_internal_sources_all_internal_ok():
# region test_validate_internal_sources_external_blocked [TYPE Function]
# @RELATION: BINDS_TO -> TestSourceIsolation
# @RELATION BINDS_TO -> TestSourceIsolation
# @PURPOSE: Verify validate_internal_sources blocks external sources when policy requires internal-only.
def test_validate_internal_sources_external_blocked():
result = validate_internal_sources(

View File

@@ -1,8 +1,8 @@
# region TestStages [TYPE Module]
# @RELATION: [DEPENDS_ON] ->[ComplianceStages]
# @RELATION DEPENDS_ON ->[ComplianceStages]
# @SEMANTICS: tests, clean-release, compliance, stages
# @PURPOSE: Validate final status derivation logic from stage results.
# @LAYER: Domain
# @LAYER Domain
from src.models.clean_release import (
CheckFinalStatus,
@@ -13,7 +13,7 @@ from src.services.clean_release.stages import MANDATORY_STAGE_ORDER, derive_fina
# region test_derive_final_status_compliant [TYPE Function]
# @RELATION: BINDS_TO -> TestStages
# @RELATION BINDS_TO -> TestStages
# @PURPOSE: Verify derive_final_status returns compliant when all stages pass.
def test_derive_final_status_compliant():
results = [
@@ -27,7 +27,7 @@ def test_derive_final_status_compliant():
# region test_derive_final_status_blocked [TYPE Function]
# @RELATION: BINDS_TO -> TestStages
# @RELATION BINDS_TO -> TestStages
# @PURPOSE: Verify derive_final_status returns blocked when any stage fails.
def test_derive_final_status_blocked():
results = [
@@ -42,7 +42,7 @@ def test_derive_final_status_blocked():
# region test_derive_final_status_failed_missing [TYPE Function]
# @RELATION: BINDS_TO -> TestStages
# @RELATION BINDS_TO -> TestStages
# @PURPOSE: Verify derive_final_status returns failed when required stages are missing.
def test_derive_final_status_failed_missing():
results = [
@@ -57,7 +57,7 @@ def test_derive_final_status_failed_missing():
# region test_derive_final_status_failed_skipped [TYPE Function]
# @RELATION: BINDS_TO -> TestStages
# @RELATION BINDS_TO -> TestStages
# @PURPOSE: Verify derive_final_status returns failed when critical stages are skipped.
def test_derive_final_status_failed_skipped():
results = [

View File

@@ -1,14 +1,14 @@
# #region ApprovalService [C:5] [TYPE Module] [SEMANTICS clean-release, approval, gate, compliance]
# @BRIEF Enforce approval/rejection gates over immutable compliance reports.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @RELATION DEPENDS_ON -> [AuditService]
# @INVARIANT: Approval is allowed only for PASSED report bound to candidate; decisions are append-only.
# @PRE: Report with PASSED final_status exists for candidate
# @POST: Approval decision appended; candidate lifecycle advanced
# @SIDE_EFFECT: Persists approval decisions, transitions candidate status
# @DATA_CONTRACT: ApprovalRequest -> ApprovalDecision
# @INVARIANT Approval is allowed only for PASSED report bound to candidate; decisions are append-only.
# @PRE Report with PASSED final_status exists for candidate
# @POST Approval decision appended; candidate lifecycle advanced
# @SIDE_EFFECT Persists approval decisions, transitions candidate status
# @DATA_CONTRACT ApprovalRequest -> ApprovalDecision
from __future__ import annotations
@@ -25,8 +25,8 @@ from .repository import CleanReleaseRepository
# #region _get_or_init_decisions_store [TYPE Function]
# @BRIEF Provide append-only in-memory storage for approval decisions.
# @PRE: repository is initialized.
# @POST: Returns mutable decision list attached to repository.
# @PRE repository is initialized.
# @POST Returns mutable decision list attached to repository.
def _get_or_init_decisions_store(
repository: CleanReleaseRepository,
) -> list[ApprovalDecision]:
@@ -42,8 +42,8 @@ def _get_or_init_decisions_store(
# #region _latest_decision_for_candidate [TYPE Function]
# @BRIEF Resolve latest approval decision for candidate from append-only store.
# @PRE: candidate_id is non-empty.
# @POST: Returns latest ApprovalDecision or None.
# @PRE candidate_id is non-empty.
# @POST Returns latest ApprovalDecision or None.
def _latest_decision_for_candidate(
repository: CleanReleaseRepository, candidate_id: str
) -> ApprovalDecision | None:
@@ -63,8 +63,8 @@ def _latest_decision_for_candidate(
# #region _resolve_candidate_and_report [TYPE Function]
# @BRIEF Validate candidate/report existence and ownership prior to decision persistence.
# @PRE: candidate_id and report_id are non-empty.
# @POST: Returns tuple(candidate, report); raises ApprovalGateError on contract violation.
# @PRE candidate_id and report_id are non-empty.
# @POST Returns tuple(candidate, report); raises ApprovalGateError on contract violation.
def _resolve_candidate_and_report(
repository: CleanReleaseRepository,
*,
@@ -90,8 +90,8 @@ def _resolve_candidate_and_report(
# #region approve_candidate [TYPE Function]
# @BRIEF Persist immutable APPROVED decision and advance candidate lifecycle to APPROVED.
# @PRE: Candidate exists, report belongs to candidate, report final_status is PASSED, candidate not already APPROVED.
# @POST: Approval decision is appended and candidate transitions to APPROVED.
# @PRE Candidate exists, report belongs to candidate, report final_status is PASSED, candidate not already APPROVED.
# @POST Approval decision is appended and candidate transitions to APPROVED.
def approve_candidate(
*,
repository: CleanReleaseRepository,
@@ -166,8 +166,8 @@ def approve_candidate(
# #region reject_candidate [TYPE Function]
# @BRIEF Persist immutable REJECTED decision without promoting candidate lifecycle.
# @PRE: Candidate exists and report belongs to candidate.
# @POST: Rejected decision is appended; candidate lifecycle is unchanged.
# @PRE Candidate exists and report belongs to candidate.
# @POST Rejected decision is appended; candidate lifecycle is unchanged.
def reject_candidate(
*,
repository: CleanReleaseRepository,

View File

@@ -1,10 +1,10 @@
# #region ArtifactCatalogLoader [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, artifact, catalog, manifest]
# @BRIEF Load bootstrap artifact catalogs for clean release real-mode flows.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @INVARIANT: Artifact catalog must produce deterministic CandidateArtifact entries with required identity and checksum fields.
# @SIDE_EFFECT: Reads JSON file from filesystem
# @DATA_CONTRACT: FilePath -> CandidateArtifact[]
# @INVARIANT Artifact catalog must produce deterministic CandidateArtifact entries with required identity and checksum fields.
# @SIDE_EFFECT Reads JSON file from filesystem
# @DATA_CONTRACT FilePath -> CandidateArtifact[]
from __future__ import annotations
@@ -16,8 +16,8 @@ from ...models.clean_release import CandidateArtifact
# #region load_bootstrap_artifacts [TYPE Function]
# @BRIEF Parse artifact catalog JSON into CandidateArtifact models for TUI/bootstrap flows.
# @PRE: path points to readable JSON file; payload is list[artifact] or {"artifacts": list[artifact]}.
# @POST: Returns non-mutated CandidateArtifact models with required fields populated.
# @PRE path points to readable JSON file; payload is list[artifact] or {"artifacts": list[artifact]}.
# @POST Returns non-mutated CandidateArtifact models with required fields populated.
def load_bootstrap_artifacts(path: str, candidate_id: str) -> list[CandidateArtifact]:
if not path or not path.strip():
return []

View File

@@ -1,12 +1,12 @@
# #region AuditService [C:3] [TYPE Module] [SEMANTICS clean-release, audit, report, trail, release]
# @BRIEF Provide lightweight audit hooks for clean release preparation/check/report lifecycle.
# @LAYER: Infrastructure
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [LoggerModule]
# @INVARIANT: Audit hooks are append-only log actions.
# @PRE: Logger configured
# @POST: Audit events appended to log
# @SIDE_EFFECT: Writes audit events to logger and repository
# @DATA_CONTRACT: AuditAction -> LogEntry
# @INVARIANT Audit hooks are append-only log actions.
# @PRE Logger configured
# @POST Audit events appended to log
# @SIDE_EFFECT Writes audit events to logger and repository
# @DATA_CONTRACT AuditAction -> LogEntry
from __future__ import annotations

View File

@@ -1,11 +1,11 @@
# #region candidate_service [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, candidate, lifecycle, release]
# @BRIEF Register release candidates with validated artifacts and advance lifecycle through legal transitions.
# @LAYER: Domain
# @RELATION DEPENDS_ON -> backend.src.services.clean_release.repository
# @RELATION DEPENDS_ON -> backend.src.models.clean_release
# @PRE: candidate_id must be unique; artifacts input must be non-empty and valid.
# @POST: candidate and artifacts are persisted; candidate transitions DRAFT -> PREPARED only.
# @INVARIANT: Candidate lifecycle transitions are delegated to domain guard logic.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [EXT:path:backend.src.services.clean_release.repository]
# @RELATION DEPENDS_ON -> [EXT:path:backend.src.models.clean_release]
# @PRE candidate_id must be unique; artifacts input must be non-empty and valid.
# @POST candidate and artifacts are persisted; candidate transitions DRAFT -> PREPARED only.
# @INVARIANT Candidate lifecycle transitions are delegated to domain guard logic.
from __future__ import annotations
@@ -20,8 +20,8 @@ from .repository import CleanReleaseRepository
# #region _validate_artifacts [TYPE Function]
# @BRIEF Validate raw artifact payload list for required fields and shape.
# @PRE: artifacts payload is provided by caller.
# @POST: Returns normalized artifact list or raises ValueError.
# @PRE artifacts payload is provided by caller.
# @POST Returns normalized artifact list or raises ValueError.
def _validate_artifacts(artifacts: Iterable[dict[str, Any]]) -> list[dict[str, Any]]:
normalized = list(artifacts)
if not normalized:
@@ -48,8 +48,8 @@ def _validate_artifacts(artifacts: Iterable[dict[str, Any]]) -> list[dict[str, A
# #region register_candidate [TYPE Function]
# @BRIEF Register a candidate and persist its artifacts with legal lifecycle transition.
# @PRE: candidate_id must be unique and artifacts must pass validation.
# @POST: Candidate exists in repository with PREPARED status and artifacts persisted.
# @PRE candidate_id must be unique and artifacts must pass validation.
# @POST Candidate exists in repository with PREPARED status and artifacts persisted.
def register_candidate(
repository: CleanReleaseRepository,
candidate_id: str,

View File

@@ -1,15 +1,15 @@
# #region ComplianceExecutionService [C:5] [TYPE Module] [SEMANTICS clean-release, execution, compliance, report, stage]
# @BRIEF Create and execute compliance runs with trusted snapshots, deterministic stages, violations and immutable report persistence.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> RepositoryRelations
# @RELATION DEPENDS_ON -> PolicyResolutionService
# @RELATION DEPENDS_ON -> ComplianceStages
# @RELATION DEPENDS_ON -> ReportBuilder
# @PRE: Repository adapters, trusted policy resolution, and deterministic stage implementations are available for the run request.
# @POST: Candidate-scoped compliance runs persist stage evidence, terminal status, and immutable report artifacts when execution succeeds.
# @SIDE_EFFECT: Persists runs, stage results, violations, and reports through repository adapters and audit helpers.
# @DATA_CONTRACT: Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult]
# @INVARIANT: A run binds to exactly one candidate/manifest/policy/registry snapshot set.
# @PRE Repository adapters, trusted policy resolution, and deterministic stage implementations are available for the run request.
# @POST Candidate-scoped compliance runs persist stage evidence, terminal status, and immutable report artifacts when execution succeeds.
# @SIDE_EFFECT Persists runs, stage results, violations, and reports through repository adapters and audit helpers.
# @DATA_CONTRACT Input[candidate_id, requested_by, manifest_id?, policy snapshots, stage results] -> Output[ComplianceExecutionResult]
# @INVARIANT A run binds to exactly one candidate/manifest/policy/registry snapshot set.
from __future__ import annotations
@@ -53,10 +53,10 @@ class ComplianceExecutionResult:
# #region ComplianceExecutionService [TYPE Class]
# @BRIEF Execute clean-release compliance lifecycle over trusted snapshots and immutable evidence.
# @PRE: Database session active, candidate registered
# @POST: Returns ComplianceReport with pass/fail status and violation details
# @SIDE_EFFECT: Updates compliance status in database, logs violations
# @DATA_CONTRACT: ComplianceCheckResult, ComplianceReport, Violation
# @PRE Database session active, candidate registered
# @POST Returns ComplianceReport with pass/fail status and violation details
# @SIDE_EFFECT Updates compliance status in database, logs violations
# @DATA_CONTRACT ComplianceCheckResult, ComplianceReport, Violation
class ComplianceExecutionService:
TASK_PLUGIN_ID = "clean-release-compliance"
@@ -74,8 +74,8 @@ class ComplianceExecutionService:
# region _resolve_manifest [TYPE Function]
# @PURPOSE: Resolve explicit manifest or fallback to latest candidate manifest.
# @PRE: candidate exists.
# @POST: Returns manifest snapshot or raises ComplianceRunError.
# @PRE candidate exists.
# @POST Returns manifest snapshot or raises ComplianceRunError.
def _resolve_manifest(
self, candidate_id: str, manifest_id: str | None
) -> DistributionManifest:
@@ -102,7 +102,7 @@ class ComplianceExecutionService:
# region _persist_stage_run [TYPE Function]
# @PURPOSE: Persist stage run if repository supports stage records.
# @POST: Stage run is persisted when adapter is available, otherwise no-op.
# @POST Stage run is persisted when adapter is available, otherwise no-op.
def _persist_stage_run(self, stage_run: ComplianceStageRun) -> None:
self.repository.save_stage_run(stage_run)
@@ -110,7 +110,7 @@ class ComplianceExecutionService:
# region _persist_violations [TYPE Function]
# @PURPOSE: Persist stage violations via repository adapters.
# @POST: Violations are appended to repository evidence store.
# @POST Violations are appended to repository evidence store.
def _persist_violations(self, violations: list[ComplianceViolation]) -> None:
for violation in violations:
self.repository.save_violation(violation)
@@ -119,8 +119,8 @@ class ComplianceExecutionService:
# region execute_run [TYPE Function]
# @PURPOSE: Execute compliance run stages and finalize immutable report on terminal success.
# @PRE: candidate exists and trusted policy/registry snapshots are resolvable.
# @POST: Run and evidence are persisted; report exists for SUCCEEDED runs.
# @PRE candidate exists and trusted policy/registry snapshots are resolvable.
# @POST Run and evidence are persisted; report exists for SUCCEEDED runs.
def execute_run(
self,
*,

View File

@@ -1,20 +1,20 @@
# #region ComplianceOrchestrator [C:5] [TYPE Module] [SEMANTICS clean-release, compliance, orchestration, stage]
# @BRIEF Execute mandatory clean compliance stages and produce final COMPLIANT/BLOCKED/FAILED outcome.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [ComplianceStages]
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @INVARIANT: COMPLIANT is impossible when any mandatory stage fails.
# @TEST_CONTRACT: ComplianceCheckRun -> ComplianceCheckRun
# @TEST_FIXTURE: compliant_candidate -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE: stage_failure_blocks_release -> Mandatory stage returns FAIL and final status becomes BLOCKED
# @TEST_EDGE: missing_stage_result -> Finalization with incomplete/empty mandatory stage set must not produce COMPLIANT
# @TEST_EDGE: report_generation_error -> Downstream reporting failure does not alter orchestrator status derivation contract
# @TEST_INVARIANT: compliant_requires_all_mandatory_pass -> VERIFIED_BY: [stage_failure_blocks_release]
# @PRE: ManifestService and PolicyEngine are available
# @POST: OrchestrationResult with compliance status
# @SIDE_EFFECT: Triggers compliance checks; may modify manifest state
# @DATA_CONTRACT: Manifest -> ComplianceReport
# @INVARIANT COMPLIANT is impossible when any mandatory stage fails.
# @TEST_CONTRACT ComplianceCheckRun -> ComplianceCheckRun
# @TEST_FIXTURE compliant_candidate -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE stage_failure_blocks_release -> Mandatory stage returns FAIL and final status becomes BLOCKED
# @TEST_EDGE missing_stage_result -> Finalization with incomplete/empty mandatory stage set must not produce COMPLIANT
# @TEST_EDGE report_generation_error -> Downstream reporting failure does not alter orchestrator status derivation contract
# @TEST_INVARIANT compliant_requires_all_mandatory_pass -> VERIFIED_BY: [stage_failure_blocks_release]
# @PRE ManifestService and PolicyEngine are available
# @POST OrchestrationResult with compliance status
# @SIDE_EFFECT Triggers compliance checks; may modify manifest state
# @DATA_CONTRACT Manifest -> ComplianceReport
from __future__ import annotations
@@ -39,10 +39,10 @@ from .stages import derive_final_status
class CleanComplianceOrchestrator:
# region __init__ [TYPE Function]
# @PURPOSE: Bind repository dependency used for orchestrator persistence and lookups.
# @PRE: repository is a valid CleanReleaseRepository instance with required methods.
# @POST: self.repository is assigned and used by all orchestration steps.
# @SIDE_EFFECT: Stores repository reference on orchestrator instance.
# @DATA_CONTRACT: Input -> CleanReleaseRepository, Output -> None
# @PRE repository is a valid CleanReleaseRepository instance with required methods.
# @POST self.repository is assigned and used by all orchestration steps.
# @SIDE_EFFECT Stores repository reference on orchestrator instance.
# @DATA_CONTRACT Input -> CleanReleaseRepository, Output -> None
def __init__(self, repository: CleanReleaseRepository):
with belief_scope("CleanComplianceOrchestrator.__init__"):
self.repository = repository
@@ -51,10 +51,10 @@ class CleanComplianceOrchestrator:
# region start_check_run [TYPE Function]
# @PURPOSE: Initiate a new compliance run session.
# @PRE: candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.
# @POST: Returns initialized ComplianceRun in RUNNING state persisted in repository.
# @SIDE_EFFECT: Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run.
# @DATA_CONTRACT: Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun
# @PRE candidate_id and policy_id are provided; legacy callers may omit persisted manifest/policy records.
# @POST Returns initialized ComplianceRun in RUNNING state persisted in repository.
# @SIDE_EFFECT Reads manifest/policy when present and writes new ComplianceRun via repository.save_check_run.
# @DATA_CONTRACT Input -> (candidate_id:str, policy_id:str, requested_by:str, manifest_id:str|None), Output -> ComplianceRun
def start_check_run(
self,
candidate_id: str,
@@ -146,10 +146,10 @@ class CleanComplianceOrchestrator:
# region execute_stages [TYPE Function]
# @PURPOSE: Execute or accept compliance stage outcomes and set intermediate/final check-run status fields.
# @PRE: check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.
# @POST: Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.
# @SIDE_EFFECT: Reads candidate/policy/registry/manifest and persists updated check_run.
# @DATA_CONTRACT: Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun
# @PRE check_run exists and references candidate/policy/registry/manifest identifiers resolvable by repository.
# @POST Returns persisted ComplianceRun with status FAILED on missing dependencies, otherwise SUCCEEDED with final_status set.
# @SIDE_EFFECT Reads candidate/policy/registry/manifest and persists updated check_run.
# @DATA_CONTRACT Input -> (check_run:ComplianceRun, forced_results:Optional[List[ComplianceStageRun]]), Output -> ComplianceRun
def execute_stages(
self,
check_run: ComplianceRun,
@@ -208,10 +208,10 @@ class CleanComplianceOrchestrator:
# region finalize_run [TYPE Function]
# @PURPOSE: Finalize run status based on cumulative stage results.
# @PRE: check_run was started and may already contain a derived final_status from stage execution.
# @POST: Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.
# @SIDE_EFFECT: Mutates check_run terminal fields and persists via repository.save_check_run.
# @DATA_CONTRACT: Input -> ComplianceRun, Output -> ComplianceRun
# @PRE check_run was started and may already contain a derived final_status from stage execution.
# @POST Returns persisted ComplianceRun in SUCCEEDED status with final_status guaranteed non-empty.
# @SIDE_EFFECT Mutates check_run terminal fields and persists via repository.save_check_run.
# @DATA_CONTRACT Input -> ComplianceRun, Output -> ComplianceRun
def finalize_run(self, check_run: ComplianceRun) -> ComplianceRun:
with belief_scope("finalize_run"):
if check_run.status == RunStatus.FAILED:
@@ -239,10 +239,10 @@ class CleanComplianceOrchestrator:
# #region run_check_legacy [TYPE Function]
# @BRIEF Legacy wrapper for compatibility with previous orchestrator call style.
# @PRE: repository and identifiers are valid and resolvable by orchestrator dependencies.
# @POST: Returns finalized ComplianceRun produced by orchestrator start->execute->finalize sequence.
# @SIDE_EFFECT: Reads/writes compliance entities through repository during orchestrator calls.
# @DATA_CONTRACT: Input -> (repository:CleanReleaseRepository, candidate_id:str, policy_id:str, requested_by:str, manifest_id:str), Output -> ComplianceRun
# @PRE repository and identifiers are valid and resolvable by orchestrator dependencies.
# @POST Returns finalized ComplianceRun produced by orchestrator start->execute->finalize sequence.
# @SIDE_EFFECT Reads/writes compliance entities through repository during orchestrator calls.
# @DATA_CONTRACT Input -> (repository:CleanReleaseRepository, candidate_id:str, policy_id:str, requested_by:str, manifest_id:str), Output -> ComplianceRun
def run_check_legacy(
repository: CleanReleaseRepository,
candidate_id: str,

View File

@@ -1,10 +1,10 @@
# #region DemoDataService [C:5] [TYPE Module] [SEMANTICS clean-release, demo, seed, fixture]
# @BRIEF Provide deterministic namespace helpers and isolated in-memory repository creation for demo and real modes.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @INVARIANT: Demo and real namespaces must never collide for generated physical identifiers.
# @SIDE_EFFECT: Writes demo entities to repository
# @DATA_CONTRACT: SeedConfig -> DemoEntities
# @INVARIANT Demo and real namespaces must never collide for generated physical identifiers.
# @SIDE_EFFECT Writes demo entities to repository
# @DATA_CONTRACT SeedConfig -> DemoEntities
from __future__ import annotations
@@ -13,8 +13,8 @@ from .repository import CleanReleaseRepository
# #region resolve_namespace [TYPE Function]
# @BRIEF Resolve canonical clean-release namespace for requested mode.
# @PRE: mode is a non-empty string identifying runtime mode.
# @POST: Returns deterministic namespace key for demo/real separation.
# @PRE mode is a non-empty string identifying runtime mode.
# @POST Returns deterministic namespace key for demo/real separation.
def resolve_namespace(mode: str) -> str:
normalized = (mode or "").strip().lower()
if normalized == "demo":
@@ -27,8 +27,8 @@ def resolve_namespace(mode: str) -> str:
# #region build_namespaced_id [TYPE Function]
# @BRIEF Build storage-safe physical identifier under mode namespace.
# @PRE: namespace and logical_id are non-empty strings.
# @POST: Returns deterministic "{namespace}::{logical_id}" identifier.
# @PRE namespace and logical_id are non-empty strings.
# @POST Returns deterministic "{namespace}::{logical_id}" identifier.
def build_namespaced_id(namespace: str, logical_id: str) -> str:
if not namespace or not namespace.strip():
raise ValueError("namespace must be non-empty")
@@ -42,8 +42,8 @@ def build_namespaced_id(namespace: str, logical_id: str) -> str:
# #region create_isolated_repository [TYPE Function]
# @BRIEF Create isolated in-memory repository instance for selected mode namespace.
# @PRE: mode is a valid runtime mode marker.
# @POST: Returns repository instance tagged with namespace metadata.
# @PRE mode is a valid runtime mode marker.
# @POST Returns repository instance tagged with namespace metadata.
def create_isolated_repository(mode: str) -> CleanReleaseRepository:
namespace = resolve_namespace(mode)
repository = CleanReleaseRepository()

View File

@@ -1,7 +1,7 @@
# #region clean_release_dto [C:3] [TYPE Module] [SEMANTICS pydantic, clean-release, dto, schema, transfer]
# @BRIEF Data Transfer Objects for clean release compliance subsystem.
# @LAYER: Application
# @RELATION DEPENDS_ON -> pydantic
# @LAYER Application
# @RELATION DEPENDS_ON -> [EXT:Library:pydantic]
from datetime import datetime
from typing import Any

View File

@@ -1,7 +1,7 @@
# #region clean_release_enums [C:3] [TYPE Module] [SEMANTICS clean-release, enum, lifecycle, status, compliance]
# @BRIEF Canonical enums for clean release lifecycle and compliance.
# @LAYER: Domain
# @RELATION DEPENDS_ON -> enum
# @LAYER Domain
# @RELATION DEPENDS_ON -> [EXT:Python:enum]
from enum import Enum

View File

@@ -1,7 +1,7 @@
# #region clean_release_exceptions [C:3] [TYPE Module] [SEMANTICS clean-release, exception, domain, error]
# @BRIEF Domain exceptions for clean release compliance subsystem.
# @LAYER: Domain
# @RELATION DEPENDS_ON -> Exception
# @LAYER Domain
# @RELATION DEPENDS_ON -> [EXT:Python:Exception]
class CleanReleaseError(Exception):
"""Base exception for clean release subsystem."""

View File

@@ -1,6 +1,6 @@
# #region clean_release_facade [C:3] [TYPE Module] [SEMANTICS clean-release, facade, orchestration, repository, crud]
# @BRIEF Unified entry point for clean release operations.
# @LAYER: Application
# @LAYER Application
# @RELATION DEPENDS_ON -> ComplianceOrchestrator

View File

@@ -1,10 +1,10 @@
# #region ManifestBuilder [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, build, artifact, catalog]
# @BRIEF Build deterministic distribution manifest from classified artifact input.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @INVARIANT: Equal semantic artifact sets produce identical deterministic hash values.
# @SIDE_EFFECT: Computes hash of artifact set
# @DATA_CONTRACT: ArtifactSet -> Manifest
# @INVARIANT Equal semantic artifact sets produce identical deterministic hash values.
# @SIDE_EFFECT Computes hash of artifact set
# @DATA_CONTRACT ArtifactSet -> Manifest
from __future__ import annotations
@@ -57,8 +57,8 @@ def _stable_hash_payload(
# #region build_distribution_manifest [TYPE Function]
# @BRIEF Build DistributionManifest with deterministic hash and validated counters.
# @PRE: artifacts list contains normalized classification values.
# @POST: Returns DistributionManifest with summary counts matching items cardinality.
# @PRE artifacts list contains normalized classification values.
# @POST Returns DistributionManifest with summary counts matching items cardinality.
def build_distribution_manifest(
manifest_id: str,
candidate_id: str,
@@ -114,8 +114,8 @@ def build_distribution_manifest(
# #region build_manifest [TYPE Function]
# @BRIEF Legacy compatibility wrapper for old manifest builder import paths.
# @PRE: Same as build_distribution_manifest.
# @POST: Returns DistributionManifest produced by canonical builder.
# @PRE Same as build_distribution_manifest.
# @POST Returns DistributionManifest produced by canonical builder.
def build_manifest(
manifest_id: str,
candidate_id: str,

View File

@@ -1,14 +1,14 @@
# #region ManifestService [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, verify, digest]
# @BRIEF Build immutable distribution manifests with deterministic digest and version increment.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @RELATION DEPENDS_ON -> [ManifestBuilder]
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @PRE: Candidate exists and is PREPARED or MANIFEST_BUILT; artifacts are present.
# @POST: New immutable manifest is persisted with incremented version and deterministic digest.
# @INVARIANT: Existing manifests are never mutated.
# @SIDE_EFFECT: May modify manifest state during processing
# @DATA_CONTRACT: Manifest -> ManifestRecord; Candidate -> ManifestRecord
# @PRE Candidate exists and is PREPARED or MANIFEST_BUILT; artifacts are present.
# @POST New immutable manifest is persisted with incremented version and deterministic digest.
# @INVARIANT Existing manifests are never mutated.
# @SIDE_EFFECT May modify manifest state during processing
# @DATA_CONTRACT Manifest -> ManifestRecord; Candidate -> ManifestRecord
from __future__ import annotations
@@ -22,8 +22,8 @@ from .repository import CleanReleaseRepository
# #region build_manifest_snapshot [TYPE Function]
# @BRIEF Create a new immutable manifest version for a candidate.
# @PRE: Candidate is prepared, artifacts are available, candidate_id is valid.
# @POST: Returns persisted DistributionManifest with monotonically incremented version.
# @PRE Candidate is prepared, artifacts are available, candidate_id is valid.
# @POST Returns persisted DistributionManifest with monotonically incremented version.
def build_manifest_snapshot(
repository: CleanReleaseRepository,
candidate_id: str,

View File

@@ -1,6 +1,6 @@
# #region clean_release_mappers [C:3] [TYPE Module] [SEMANTICS clean-release, mapper, dto, entity, transform]
# @BRIEF Map between domain entities (SQLAlchemy models) and DTOs.
# @LAYER: Application
# @LAYER Application
# @RELATION DEPENDS_ON -> clean_release_dto
from src.models.clean_release import ComplianceReport, ComplianceRun, DistributionManifest, ReleaseCandidate

View File

@@ -1,13 +1,13 @@
# #region PolicyEngine [C:5] [TYPE Module] [SEMANTICS clean-release, policy, validate, profile, artifact]
# @BRIEF Evaluate artifact/source policies for enterprise clean profile with deterministic outcomes.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @RELATION DEPENDS_ON -> [LoggerModule]
# @INVARIANT: Enterprise-clean policy always treats non-registry sources as violations.
# @DATA_CONTRACT: Candidate -> PolicyDecision
# @PRE: PolicyRepository is accessible
# @POST: PolicyDecision returned with approval status
# @SIDE_EFFECT: Read-only policy evaluation; no state changes
# @INVARIANT Enterprise-clean policy always treats non-registry sources as violations.
# @DATA_CONTRACT Candidate -> PolicyDecision
# @PRE PolicyRepository is accessible
# @POST PolicyDecision returned with approval status
# @SIDE_EFFECT Read-only policy evaluation; no state changes
from __future__ import annotations
@@ -36,15 +36,15 @@ class SourceValidationResult:
# #region CleanPolicyEngine [TYPE Class]
# @PRE: Active policy exists and is internally consistent.
# @POST: Deterministic classification and source validation are available.
# @TEST_CONTRACT: CandidateEvaluationInput -> PolicyValidationResult|SourceValidationResult
# @TEST_SCENARIO: policy_valid -> Enterprise clean policy with matching registry returns ok=True
# @TEST_FIXTURE: policy_enterprise_clean -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE: missing_registry_ref -> policy has empty registry_snapshot_id
# @TEST_EDGE: conflicting_registry -> policy registry ref does not match registry id
# @TEST_EDGE: external_endpoint -> endpoint not present in enabled internal registry entries
# @TEST_INVARIANT: deterministic_classification -> VERIFIED_BY: [policy_valid]
# @PRE Active policy exists and is internally consistent.
# @POST Deterministic classification and source validation are available.
# @TEST_CONTRACT CandidateEvaluationInput -> PolicyValidationResult|SourceValidationResult
# @TEST_SCENARIO policy_valid -> Enterprise clean policy with matching registry returns ok=True
# @TEST_FIXTURE policy_enterprise_clean -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE missing_registry_ref -> policy has empty registry_snapshot_id
# @TEST_EDGE conflicting_registry -> policy registry ref does not match registry id
# @TEST_EDGE external_endpoint -> endpoint not present in enabled internal registry entries
# @TEST_INVARIANT deterministic_classification -> VERIFIED_BY: [policy_valid]
class CleanPolicyEngine:
def __init__(
self,

View File

@@ -1,14 +1,14 @@
# #region PolicyResolutionService [C:5] [TYPE Module] [SEMANTICS clean-release, policy, resolution, registry]
# @BRIEF Resolve trusted policy and registry snapshots from ConfigManager without runtime overrides.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @RELATION DEPENDS_ON -> [clean_release_exceptions]
# @INVARIANT: Trusted snapshot resolution is based only on ConfigManager active identifiers.
# @DATA_CONTRACT: PolicyRequest -> ResolutionResult
# @PRE: PolicyRepository and Manifest are available
# @POST: ResolutionResult with matched policies
# @SIDE_EFFECT: Read-only policy evaluation; logs resolution decisions
# @INVARIANT Trusted snapshot resolution is based only on ConfigManager active identifiers.
# @DATA_CONTRACT PolicyRequest -> ResolutionResult
# @PRE PolicyRepository and Manifest are available
# @POST ResolutionResult with matched policies
# @SIDE_EFFECT Read-only policy evaluation; logs resolution decisions
from __future__ import annotations
@@ -19,9 +19,9 @@ from .repository import CleanReleaseRepository
# #region resolve_trusted_policy_snapshots [TYPE Function]
# @BRIEF Resolve immutable trusted policy and registry snapshots using active config IDs only.
# @PRE: ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots.
# @POST: Returns immutable policy and registry snapshots; runtime override attempts are rejected.
# @SIDE_EFFECT: None.
# @PRE ConfigManager provides active_policy_id and active_registry_id; repository contains referenced snapshots.
# @POST Returns immutable policy and registry snapshots; runtime override attempts are rejected.
# @SIDE_EFFECT None.
def resolve_trusted_policy_snapshots(
*,
config_manager,

View File

@@ -1,12 +1,12 @@
# #region PreparationService [C:5] [TYPE Module] [SEMANTICS clean-release, prepare, validate, policy, manifest]
# @BRIEF Prepare release candidate by policy evaluation and deterministic manifest creation.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [PolicyEngine]
# @RELATION DEPENDS_ON -> [ManifestBuilder]
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @INVARIANT: Candidate preparation always persists manifest and candidate status deterministically.
# @SIDE_EFFECT: Persists candidate and manifest
# @DATA_CONTRACT: PrepareRequest -> PrepareResult
# @INVARIANT Candidate preparation always persists manifest and candidate status deterministically.
# @SIDE_EFFECT Persists candidate and manifest
# @DATA_CONTRACT PrepareRequest -> PrepareResult
from __future__ import annotations
@@ -91,8 +91,8 @@ def prepare_candidate(
# #region prepare_candidate_legacy [TYPE Function]
# @BRIEF Legacy compatibility wrapper kept for migration period.
# @PRE: Same as prepare_candidate.
# @POST: Delegates to canonical prepare_candidate and preserves response shape.
# @PRE Same as prepare_candidate.
# @POST Delegates to canonical prepare_candidate and preserves response shape.
def prepare_candidate_legacy(
repository: CleanReleaseRepository,
candidate_id: str,

View File

@@ -1,11 +1,11 @@
# #region PublicationService [C:5] [TYPE Module] [SEMANTICS clean-release, publication, publish, release]
# @BRIEF Enforce publication and revocation gates with append-only publication records.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @RELATION DEPENDS_ON -> [ApprovalService]
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @RELATION DEPENDS_ON -> [AuditService]
# @INVARIANT: Publication records are append-only snapshots; revoke mutates only publication status for targeted record.
# @INVARIANT Publication records are append-only snapshots; revoke mutates only publication status for targeted record.
from __future__ import annotations
@@ -22,8 +22,8 @@ from .repository import CleanReleaseRepository
# #region _get_or_init_publications_store [TYPE Function]
# @BRIEF Provide in-memory append-only publication storage.
# @PRE: repository is initialized.
# @POST: Returns publication list attached to repository.
# @PRE repository is initialized.
# @POST Returns publication list attached to repository.
def _get_or_init_publications_store(
repository: CleanReleaseRepository,
) -> list[PublicationRecord]:
@@ -39,8 +39,8 @@ def _get_or_init_publications_store(
# #region _latest_publication_for_candidate [TYPE Function]
# @BRIEF Resolve latest publication record for candidate.
# @PRE: candidate_id is non-empty.
# @POST: Returns latest record or None.
# @PRE candidate_id is non-empty.
# @POST Returns latest record or None.
def _latest_publication_for_candidate(
repository: CleanReleaseRepository,
candidate_id: str,
@@ -64,8 +64,8 @@ def _latest_publication_for_candidate(
# #region _latest_approval_for_candidate [TYPE Function]
# @BRIEF Resolve latest approval decision from repository decision store.
# @PRE: candidate_id is non-empty.
# @POST: Returns latest decision object or None.
# @PRE candidate_id is non-empty.
# @POST Returns latest decision object or None.
def _latest_approval_for_candidate(
repository: CleanReleaseRepository, candidate_id: str
):
@@ -85,8 +85,8 @@ def _latest_approval_for_candidate(
# #region publish_candidate [TYPE Function]
# @BRIEF Create immutable publication record for approved candidate.
# @PRE: Candidate exists, report belongs to candidate, latest approval is APPROVED.
# @POST: New ACTIVE publication record is appended.
# @PRE Candidate exists, report belongs to candidate, latest approval is APPROVED.
# @POST New ACTIVE publication record is appended.
def publish_candidate(
*,
repository: CleanReleaseRepository,
@@ -165,8 +165,8 @@ def publish_candidate(
# #region revoke_publication [TYPE Function]
# @BRIEF Revoke existing publication record without deleting history.
# @PRE: publication_id exists in repository publication store.
# @POST: Target publication status becomes REVOKED and updated record is returned.
# @PRE publication_id exists in repository publication store.
# @POST Target publication status becomes REVOKED and updated record is returned.
def revoke_publication(
*,
repository: CleanReleaseRepository,

View File

@@ -1,19 +1,19 @@
# #region ReportBuilder [C:5] [TYPE Module] [SEMANTICS report, clean-release, compliance, builder, render]
# @BRIEF Build and persist compliance reports with consistent counter invariants.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @RELATION DEPENDS_ON -> [RepositoryRelations]
# @INVARIANT: blocking_violations_count never exceeds violations_count.
# @TEST_CONTRACT: ComplianceCheckRun,List[ComplianceViolation] -> ComplianceReport
# @TEST_FIXTURE: blocked_with_two_violations -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE: empty_violations_for_blocked -> BLOCKED run with zero blocking violations raises ValueError
# @TEST_EDGE: counter_mismatch -> blocking counter cannot exceed total violations counter
# @TEST_EDGE: missing_operator_summary -> non-terminal run prevents report creation and summary generation
# @TEST_INVARIANT: blocking_count_le_total_count -> VERIFIED_BY: [counter_mismatch, empty_violations_for_blocked]
# @DATA_CONTRACT: Input[ComplianceRun, List[ComplianceViolation]] -> Output[ComplianceReport]
# @PRE: Compliance run is terminal and repository persistence is available for report storage.
# @POST: Returns immutable report payloads with consistent violation counters and operator summary content.
# @SIDE_EFFECT: Writes report artifacts to repository when persistence helpers are invoked.
# @INVARIANT blocking_violations_count never exceeds violations_count.
# @TEST_CONTRACT ComplianceCheckRun,List[ComplianceViolation] -> ComplianceReport
# @TEST_FIXTURE blocked_with_two_violations -> file:backend/tests/fixtures/clean_release/fixtures_clean_release.json
# @TEST_EDGE empty_violations_for_blocked -> BLOCKED run with zero blocking violations raises ValueError
# @TEST_EDGE counter_mismatch -> blocking counter cannot exceed total violations counter
# @TEST_EDGE missing_operator_summary -> non-terminal run prevents report creation and summary generation
# @TEST_INVARIANT blocking_count_le_total_count -> VERIFIED_BY: [counter_mismatch, empty_violations_for_blocked]
# @DATA_CONTRACT Input[ComplianceRun, List[ComplianceViolation]] -> Output[ComplianceReport]
# @PRE Compliance run is terminal and repository persistence is available for report storage.
# @POST Returns immutable report payloads with consistent violation counters and operator summary content.
# @SIDE_EFFECT Writes report artifacts to repository when persistence helpers are invoked.
from __future__ import annotations

View File

@@ -1,6 +1,6 @@
# #region clean_release_repositories [C:3] [TYPE Module] [SEMANTICS clean-release, repository, package, export]
# @BRIEF Export all clean release repositories.
# @RELATION DEPENDS_ON -> sqlalchemy
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from .approval_repository import ApprovalRepository
from .artifact_repository import ArtifactRepository

View File

@@ -1,7 +1,7 @@
# #region approval_repository [C:3] [TYPE Module] [SEMANTICS clean-release, approval, repository, persistence, crud]
# @BRIEF Persist and query approval decisions.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> sqlalchemy
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from sqlalchemy.orm import Session

View File

@@ -1,7 +1,7 @@
# #region artifact_repository [C:3] [TYPE Module] [SEMANTICS clean-release, artifact, repository, persistence, crud]
# @BRIEF Persist and query candidate artifacts.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> sqlalchemy
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from sqlalchemy.orm import Session

View File

@@ -1,7 +1,7 @@
# #region audit_repository [C:3] [TYPE Module] [SEMANTICS clean-release, audit, repository, persistence, crud]
# @BRIEF Persist and query audit logs for clean release operations.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> sqlalchemy
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from sqlalchemy.orm import Session

View File

@@ -1,7 +1,7 @@
# #region candidate_repository [C:3] [TYPE Module] [SEMANTICS clean-release, candidate, repository, persistence, crud]
# @BRIEF Persist and query release candidates.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> sqlalchemy
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from sqlalchemy.orm import Session

View File

@@ -1,7 +1,7 @@
# #region compliance_repository [C:3] [TYPE Module] [SEMANTICS clean-release, compliance, repository, persistence, crud]
# @BRIEF Persist and query compliance runs, stage runs, and violations.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> sqlalchemy
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from sqlalchemy.orm import Session

View File

@@ -1,8 +1,8 @@
# #region ManifestRepositoryModule [C:3] [TYPE Module] [SEMANTICS clean-release, manifest, repository, persistence, crud]
# @BRIEF Persist and query distribution manifests.
# @LAYER: Infra
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> DistributionManifest
# @RELATION DEPENDS_ON -> sqlalchemy
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
# @RELATION DEPENDS_ON -> belief_scope
@@ -15,24 +15,24 @@ from src.models.clean_release import DistributionManifest
# #region ManifestRepository [C:3] [TYPE Class]
# @BRIEF Encapsulates database CRUD operations for DistributionManifest entities.
# @RELATION DEPENDS_ON -> DistributionManifest
# @RELATION DEPENDS_ON -> sqlalchemy.Session
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy.Session]
class ManifestRepository:
"""Repository for distribution manifest persistence."""
# region ManifestRepository.__init__ [TYPE Function]
# @PURPOSE: Initialize repository with an active SQLAlchemy session.
# @PRE: db is a valid SQLAlchemy Session instance.
# @POST: Repository is ready for database operations.
# @PRE db is a valid SQLAlchemy Session instance.
# @POST Repository is ready for database operations.
def __init__(self, db: Session):
self.db = db
# endregion ManifestRepository.__init__
# region ManifestRepository.save [TYPE Function]
# @PURPOSE: Persist a DistributionManifest to the database.
# @PRE: manifest is a valid DistributionManifest instance with required fields populated.
# @POST: Manifest is committed to database and refreshed with generated ID.
# @SIDE_EFFECT: Database commit via session.commit().
# @RELATION: DEPENDS_ON -> DistributionManifest
# @PRE manifest is a valid DistributionManifest instance with required fields populated.
# @POST Manifest is committed to database and refreshed with generated ID.
# @SIDE_EFFECT Database commit via session.commit().
# @RELATION DEPENDS_ON -> DistributionManifest
def save(self, manifest: DistributionManifest) -> DistributionManifest:
with belief_scope("ManifestRepository.save"):
self.db.add(manifest)
@@ -43,9 +43,9 @@ class ManifestRepository:
# region ManifestRepository.get_by_id [TYPE Function]
# @PURPOSE: Retrieve a single DistributionManifest by its primary key.
# @PRE: manifest_id is a valid string identifier.
# @POST: Returns DistributionManifest if found, None otherwise.
# @RELATION: DEPENDS_ON -> DistributionManifest
# @PRE manifest_id is a valid string identifier.
# @POST Returns DistributionManifest if found, None otherwise.
# @RELATION DEPENDS_ON -> DistributionManifest
def get_by_id(self, manifest_id: str) -> DistributionManifest | None:
with belief_scope("ManifestRepository.get_by_id"):
return self.db.query(DistributionManifest).filter(
@@ -55,9 +55,9 @@ class ManifestRepository:
# region ManifestRepository.get_latest_for_candidate [TYPE Function]
# @PURPOSE: Retrieve the most recent manifest version for a given candidate.
# @PRE: candidate_id is a valid string identifier.
# @POST: Returns the highest manifest_version manifest for the candidate, or None.
# @RELATION: DEPENDS_ON -> DistributionManifest
# @PRE candidate_id is a valid string identifier.
# @POST Returns the highest manifest_version manifest for the candidate, or None.
# @RELATION DEPENDS_ON -> DistributionManifest
def get_latest_for_candidate(self, candidate_id: str) -> DistributionManifest | None:
with belief_scope("ManifestRepository.get_latest_for_candidate"):
return (
@@ -70,9 +70,9 @@ class ManifestRepository:
# region ManifestRepository.list_by_candidate [TYPE Function]
# @PURPOSE: List all manifests for a specific candidate, ordered by version.
# @PRE: candidate_id is a valid string identifier.
# @POST: Returns a list of DistributionManifest instances (may be empty).
# @RELATION: DEPENDS_ON -> DistributionManifest
# @PRE candidate_id is a valid string identifier.
# @POST Returns a list of DistributionManifest instances (may be empty).
# @RELATION DEPENDS_ON -> DistributionManifest
def list_by_candidate(self, candidate_id: str) -> list[DistributionManifest]:
with belief_scope("ManifestRepository.list_by_candidate"):
return (

View File

@@ -1,7 +1,7 @@
# #region policy_repository [C:3] [TYPE Module] [SEMANTICS clean-release, policy, repository, persistence, crud]
# @BRIEF Persist and query policy and registry snapshots.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> sqlalchemy
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from sqlalchemy.orm import Session

View File

@@ -1,7 +1,7 @@
# #region publication_repository [C:3] [TYPE Module] [SEMANTICS clean-release, publication, repository, persistence, crud]
# @BRIEF Persist and query publication records.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> sqlalchemy
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from sqlalchemy.orm import Session

View File

@@ -1,7 +1,7 @@
# #region report_repository [C:3] [TYPE Module] [SEMANTICS clean-release, report, repository, persistence, crud]
# @BRIEF Persist and query compliance reports.
# @LAYER: Infra
# @RELATION DEPENDS_ON -> sqlalchemy
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy]
from sqlalchemy.orm import Session

View File

@@ -1,12 +1,12 @@
# #region RepositoryRelations [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, repository, relations, release]
# @BRIEF Provide repository adapter for clean release entities with deterministic access methods.
# @LAYER: Infrastructure
# @LAYER Infrastructure
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @INVARIANT: Repository operations are side-effect free outside explicit save/update calls.
# @PRE: In-memory storage initialized
# @POST: Repository operations exported
# @SIDE_EFFECT: Modifies in-memory state on save/update
# @DATA_CONTRACT: Entity -> RepositoryOperation
# @INVARIANT Repository operations are side-effect free outside explicit save/update calls.
# @PRE In-memory storage initialized
# @POST Repository operations exported
# @SIDE_EFFECT Modifies in-memory state on save/update
# @DATA_CONTRACT Entity -> RepositoryOperation
from __future__ import annotations

View File

@@ -1,12 +1,12 @@
# #region SourceIsolation [C:5] [TYPE Module] [SEMANTICS clean-release, source, isolation, validate, resource]
# @BRIEF Validate that all resource endpoints belong to the approved internal source registry.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @INVARIANT: Any endpoint outside enabled registry entries is treated as external-source violation.
# @PRE: Source registry configured
# @POST: Source isolation violations identified
# @SIDE_EFFECT: None (read-only check)
# @DATA_CONTRACT: SourceURL -> ViolationReport
# @INVARIANT Any endpoint outside enabled registry entries is treated as external-source violation.
# @PRE Source registry configured
# @POST Source isolation violations identified
# @SIDE_EFFECT None (read-only check)
# @DATA_CONTRACT SourceURL -> ViolationReport
from __future__ import annotations

View File

@@ -1,11 +1,11 @@
# #region ComplianceStages [C:5] [TYPE Module] [SEMANTICS clean-release, compliance, stage, package]
# @BRIEF Define compliance stage order and helper functions for deterministic run-state evaluation.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @RELATION DEPENDS_ON -> [ComplianceStageBase]
# @INVARIANT: Stage order remains deterministic for all compliance runs.
# @SIDE_EFFECT: Registers compliance stages
# @DATA_CONTRACT: StagePipeline -> ComplianceResult
# @INVARIANT Stage order remains deterministic for all compliance runs.
# @SIDE_EFFECT Registers compliance stages
# @DATA_CONTRACT StagePipeline -> ComplianceResult
from __future__ import annotations
@@ -35,8 +35,8 @@ MANDATORY_STAGE_ORDER: list[ComplianceStageName] = [
# #region build_default_stages [TYPE Function]
# @BRIEF Build default deterministic stage pipeline implementation order.
# @PRE: None.
# @POST: Returns stage instances in mandatory execution order.
# @PRE None.
# @POST Returns stage instances in mandatory execution order.
def build_default_stages() -> list[ComplianceStage]:
return [
DataPurityStage(),
@@ -51,8 +51,8 @@ def build_default_stages() -> list[ComplianceStage]:
# #region stage_result_map [TYPE Function]
# @BRIEF Convert stage result list to dictionary by stage name.
# @PRE: stage_results may be empty or contain unique stage names.
# @POST: Returns stage->status dictionary for downstream evaluation.
# @PRE stage_results may be empty or contain unique stage names.
# @POST Returns stage->status dictionary for downstream evaluation.
def stage_result_map(
stage_results: Iterable[ComplianceStageRun | CheckStageResult],
) -> dict[ComplianceStageName, CheckStageStatus]:
@@ -90,8 +90,8 @@ def stage_result_map(
# #region missing_mandatory_stages [TYPE Function]
# @BRIEF Identify mandatory stages that are absent from run results.
# @PRE: stage_status_map contains zero or more known stage statuses.
# @POST: Returns ordered list of missing mandatory stages.
# @PRE stage_status_map contains zero or more known stage statuses.
# @POST Returns ordered list of missing mandatory stages.
def missing_mandatory_stages(
stage_status_map: dict[ComplianceStageName, CheckStageStatus],
) -> list[ComplianceStageName]:
@@ -103,8 +103,8 @@ def missing_mandatory_stages(
# #region derive_final_status [TYPE Function]
# @BRIEF Derive final run status from stage results with deterministic blocking behavior.
# @PRE: Stage statuses correspond to compliance checks.
# @POST: Returns one of PASSED/BLOCKED/ERROR according to mandatory stage outcomes.
# @PRE Stage statuses correspond to compliance checks.
# @POST Returns one of PASSED/BLOCKED/ERROR according to mandatory stage outcomes.
def derive_final_status(
stage_results: Iterable[ComplianceStageRun | CheckStageResult],
) -> CheckFinalStatus:

View File

@@ -1,11 +1,11 @@
# #region ComplianceStageBase [C:5] [TYPE Module] [SEMANTICS pydantic, clean-release, stage, compliance, context]
# @BRIEF Define shared contracts and helpers for pluggable clean-release compliance stages.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [CleanReleaseModels]
# @RELATION DEPENDS_ON -> [LoggerModule]
# @INVARIANT: Stage execution is deterministic for equal input context.
# @SIDE_EFFECT: None (deterministic execution)
# @DATA_CONTRACT: Context -> StageResult
# @INVARIANT Stage execution is deterministic for equal input context.
# @SIDE_EFFECT None (deterministic execution)
# @DATA_CONTRACT Context -> StageResult
from __future__ import annotations
@@ -67,8 +67,8 @@ class ComplianceStage(Protocol):
# #region build_stage_run_record [TYPE Function]
# @BRIEF Build persisted stage run record from stage result.
# @PRE: run_id and stage_name are non-empty.
# @POST: Returns ComplianceStageRun with deterministic identifiers and timestamps.
# @PRE run_id and stage_name are non-empty.
# @POST Returns ComplianceStageRun with deterministic identifiers and timestamps.
def build_stage_run_record(
*,
run_id: str,
@@ -98,8 +98,8 @@ def build_stage_run_record(
# #region build_violation [TYPE Function]
# @BRIEF Construct a compliance violation with normalized defaults.
# @PRE: run_id, stage_name, code and message are non-empty.
# @POST: Returns immutable-style violation payload ready for persistence.
# @PRE run_id, stage_name, code and message are non-empty.
# @POST Returns immutable-style violation payload ready for persistence.
def build_violation(
*,
run_id: str,

View File

@@ -1,11 +1,11 @@
# #region data_purity [C:5] [TYPE Module] [SEMANTICS clean-release, data, purity, validate, manifest]
# @BRIEF Evaluate manifest purity counters and emit blocking violations for prohibited artifacts.
# @LAYER: Domain
# @LAYER Domain
# @RELATION IMPLEMENTS -> [ComplianceStage]
# @RELATION DEPENDS_ON -> [ComplianceStageBase]
# @INVARIANT: prohibited_detected_count > 0 always yields BLOCKED stage decision.
# @SIDE_EFFECT: None (read-only validation)
# @DATA_CONTRACT: Manifest -> DataPurityVerdict
# @INVARIANT prohibited_detected_count > 0 always yields BLOCKED stage decision.
# @SIDE_EFFECT None (read-only validation)
# @DATA_CONTRACT Manifest -> DataPurityVerdict
from __future__ import annotations
@@ -16,8 +16,8 @@ from .base import ComplianceStageContext, StageExecutionResult, build_violation
# #region DataPurityStage [TYPE Class]
# @BRIEF Validate manifest summary for prohibited artifacts.
# @PRE: context.manifest.content_json contains summary block or defaults to safe counters.
# @POST: Returns PASSED when no prohibited artifacts are detected, otherwise BLOCKED with violations.
# @PRE context.manifest.content_json contains summary block or defaults to safe counters.
# @POST Returns PASSED when no prohibited artifacts are detected, otherwise BLOCKED with violations.
class DataPurityStage:
stage_name = ComplianceStageName.DATA_PURITY

View File

@@ -1,11 +1,11 @@
# #region internal_sources_only [C:5] [TYPE Module] [SEMANTICS clean-release, internal, source, validate]
# @BRIEF Verify manifest-declared sources belong to trusted internal registry allowlist.
# @LAYER: Domain
# @LAYER Domain
# @RELATION IMPLEMENTS -> [ComplianceStage]
# @RELATION DEPENDS_ON -> [ComplianceStageBase]
# @INVARIANT: Any source host outside allowed_hosts yields BLOCKED decision with at least one violation.
# @SIDE_EFFECT: None (read-only validation)
# @DATA_CONTRACT: Sources -> SourceViolationReport
# @INVARIANT Any source host outside allowed_hosts yields BLOCKED decision with at least one violation.
# @SIDE_EFFECT None (read-only validation)
# @DATA_CONTRACT Sources -> SourceViolationReport
from __future__ import annotations
@@ -16,8 +16,8 @@ from .base import ComplianceStageContext, StageExecutionResult, build_violation
# #region InternalSourcesOnlyStage [TYPE Class]
# @BRIEF Enforce internal-source-only policy from trusted registry snapshot.
# @PRE: context.registry.allowed_hosts is available.
# @POST: Returns PASSED when all hosts are allowed; otherwise BLOCKED and violations captured.
# @PRE context.registry.allowed_hosts is available.
# @POST Returns PASSED when all hosts are allowed; otherwise BLOCKED and violations captured.
class InternalSourcesOnlyStage:
stage_name = ComplianceStageName.INTERNAL_SOURCES_ONLY

View File

@@ -1,11 +1,11 @@
# #region manifest_consistency [C:5] [TYPE Module] [SEMANTICS clean-release, manifest, consistency, validate]
# @BRIEF Ensure run is bound to the exact manifest snapshot and digest used at run creation time.
# @LAYER: Domain
# @LAYER Domain
# @RELATION IMPLEMENTS -> [ComplianceStage]
# @RELATION DEPENDS_ON -> [ComplianceStageBase]
# @INVARIANT: Digest mismatch between run and manifest yields ERROR with blocking violation evidence.
# @SIDE_EFFECT: None (read-only validation)
# @DATA_CONTRACT: RunData -> ConsistencyVerdict
# @INVARIANT Digest mismatch between run and manifest yields ERROR with blocking violation evidence.
# @SIDE_EFFECT None (read-only validation)
# @DATA_CONTRACT RunData -> ConsistencyVerdict
from __future__ import annotations
@@ -16,8 +16,8 @@ from .base import ComplianceStageContext, StageExecutionResult, build_violation
# #region ManifestConsistencyStage [TYPE Class]
# @BRIEF Validate run/manifest linkage consistency.
# @PRE: context.run and context.manifest are loaded from repository for same run.
# @POST: Returns PASSED when digests match, otherwise ERROR with one violation.
# @PRE context.run and context.manifest are loaded from repository for same run.
# @POST Returns PASSED when digests match, otherwise ERROR with one violation.
class ManifestConsistencyStage:
stage_name = ComplianceStageName.MANIFEST_CONSISTENCY

View File

@@ -1,11 +1,11 @@
# #region no_external_endpoints [C:5] [TYPE Module] [SEMANTICS clean-release, endpoint, validate, manifest, compliance]
# @BRIEF Block manifest payloads that expose external endpoints outside trusted schemes and hosts.
# @LAYER: Domain
# @LAYER Domain
# @RELATION IMPLEMENTS -> [ComplianceStage]
# @RELATION DEPENDS_ON -> [ComplianceStageBase]
# @INVARIANT: Endpoint outside allowed scheme/host always yields BLOCKED stage decision.
# @SIDE_EFFECT: None (read-only validation)
# @DATA_CONTRACT: Endpoints -> EndpointViolationReport
# @INVARIANT Endpoint outside allowed scheme/host always yields BLOCKED stage decision.
# @SIDE_EFFECT None (read-only validation)
# @DATA_CONTRACT Endpoints -> EndpointViolationReport
from __future__ import annotations
@@ -18,8 +18,8 @@ from .base import ComplianceStageContext, StageExecutionResult, build_violation
# #region NoExternalEndpointsStage [TYPE Class]
# @BRIEF Validate endpoint references from manifest against trusted registry.
# @PRE: context.registry includes allowed hosts and schemes.
# @POST: Returns PASSED when all endpoints are trusted, otherwise BLOCKED with endpoint violations.
# @PRE context.registry includes allowed hosts and schemes.
# @POST Returns PASSED when all endpoints are trusted, otherwise BLOCKED with endpoint violations.
class NoExternalEndpointsStage:
stage_name = ComplianceStageName.NO_EXTERNAL_ENDPOINTS

View File

@@ -1,7 +1,7 @@
# #region dataset_review [TYPE Module] [SEMANTICS dataset, review, orchestration, package]
#
# @BRIEF Provides services for dataset-centered orchestration flow.
# @RELATION EXPORTS -> [DatasetReviewOrchestrator:Class]
# @LAYER: Services
# @RELATION CALLS -> [DatasetReviewOrchestrator]
# @LAYER Service
#
# #endregion dataset_review

View File

@@ -1,19 +1,19 @@
# #region ClarificationEngine [C:4] [TYPE Module] [SEMANTICS pydantic, dataset, clarification, finding, resolution]
# @BRIEF Manage one-question-at-a-time clarification state, deterministic answer persistence, and readiness/finding updates.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]
# @RELATION DEPENDS_ON -> [ClarificationSession]
# @RELATION DEPENDS_ON -> [ClarificationQuestion]
# @RELATION DEPENDS_ON -> [ClarificationAnswer]
# @RELATION DEPENDS_ON -> [ValidationFinding]
# @RELATION DISPATCHES -> [ClarificationHelpers:Module]
# @PRE: Target session contains a persisted clarification aggregate in the current ownership scope.
# @POST: Active clarification payload exposes one highest-priority unresolved question, and each recorded answer is persisted before pointer/readiness mutation.
# @SIDE_EFFECT: Persists clarification answers, question/session states, and related readiness/finding changes.
# @DATA_CONTRACT: Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult]
# @INVARIANT: Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible.
# @RATIONALE: Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module.
# @REJECTED: Keeping all clarification logic in one file because it exceeded the fractal limit.
# @RELATION DISPATCHES -> [ClarificationHelpers]
# @PRE Target session contains a persisted clarification aggregate in the current ownership scope.
# @POST Active clarification payload exposes one highest-priority unresolved question, and each recorded answer is persisted before pointer/readiness mutation.
# @SIDE_EFFECT Persists clarification answers, question/session states, and related readiness/finding changes.
# @DATA_CONTRACT Input[DatasetReviewSession|ClarificationAnswerCommand] -> Output[ClarificationStateResult]
# @INVARIANT Only one active clarification question may exist at a time; skipped and expert-review items remain unresolved and visible.
# @RATIONALE Original 635-line file exceeded INV_7 (400-line module limit). Extracted pure helpers into _helpers sub-module.
# @REJECTED Keeping all clarification logic in one file because it exceeded the fractal limit.
from __future__ import annotations
@@ -98,10 +98,10 @@ class ClarificationAnswerCommand:
# #region ClarificationEngine [C:4] [TYPE Class]
# @BRIEF Provide deterministic one-question-at-a-time clarification selection and answer persistence.
# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]
# @RELATION CALLS -> [ClarificationHelpers:Module]
# @PRE: Repository is bound to the current request transaction scope.
# @POST: Returned clarification state is persistence-backed and aligned with session readiness/recommended action.
# @SIDE_EFFECT: Mutates clarification answers, session flags, and related clarification findings.
# @RELATION CALLS -> [ClarificationHelpers]
# @PRE Repository is bound to the current request transaction scope.
# @POST Returned clarification state is persistence-backed and aligned with session readiness/recommended action.
# @SIDE_EFFECT Mutates clarification answers, session flags, and related clarification findings.
class ClarificationEngine:
# region ClarificationEngine_init [TYPE Function]
# @PURPOSE: Bind repository dependency for clarification persistence operations.
@@ -112,9 +112,9 @@ class ClarificationEngine:
# region build_question_payload [TYPE Function]
# @PURPOSE: Return the one active highest-priority clarification question payload.
# @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.
# @SIDE_EFFECT: Normalizes the active-question pointer and clarification status in persistence.
# @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.
# @SIDE_EFFECT Normalizes the active-question pointer and clarification status in persistence.
def build_question_payload(
self, session: DatasetReviewSession,
) -> ClarificationQuestionPayload | None:
@@ -171,9 +171,9 @@ class ClarificationEngine:
# region record_answer [TYPE Function]
# @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.
# @POST: Answer row is persisted before current-question pointer advances.
# @SIDE_EFFECT: Inserts answer row, mutates question/session states, updates clarification findings, and commits.
# @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.
# @SIDE_EFFECT Inserts answer row, mutates question/session states, updates clarification findings, and commits.
def record_answer(self, command: ClarificationAnswerCommand) -> ClarificationStateResult:
with belief_scope("ClarificationEngine.record_answer"):
session = command.session

View File

@@ -1,6 +1,6 @@
# #region ClarificationHelpers [C:3] [TYPE Module] [SEMANTICS dataset, clarification, question, selection, normalization]
# @BRIEF Pure helper functions for clarification engine — question selection, counting, normalization, finding upsert, and readiness derivation.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [DatasetReviewModels]
from __future__ import annotations

View File

@@ -1,12 +1,12 @@
# #region SessionEventLoggerModule [C:4] [TYPE Module] [SEMANTICS sqlalchemy, dataset, event, logging, session]
# @BRIEF Persist explicit session mutation events for dataset-review audit trails without weakening ownership or approval invariants.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [SessionEvent]
# @RELATION DEPENDS_ON -> [DatasetReviewSession]
# @PRE: Caller provides an owned session scope and an authenticated actor identifier for each persisted mutation event.
# @POST: Every logged event is committed as an explicit, queryable audit record with deterministic event metadata.
# @SIDE_EFFECT: Inserts persisted session event rows and emits runtime belief-state logs for audit-sensitive mutations.
# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]
# @PRE Caller provides an owned session scope and an authenticated actor identifier for each persisted mutation event.
# @POST Every logged event is committed as an explicit, queryable audit record with deterministic event metadata.
# @SIDE_EFFECT Inserts persisted session event rows and emits runtime belief-state logs for audit-sensitive mutations.
# @DATA_CONTRACT Input[SessionEventPayload] -> Output[SessionEvent]
from __future__ import annotations
@@ -40,10 +40,10 @@ class SessionEventPayload:
# @BRIEF Persist explicit dataset-review session audit events with meaningful runtime reasoning logs.
# @RELATION DEPENDS_ON -> [SessionEvent]
# @RELATION DEPENDS_ON -> [SessionEventPayload]
# @PRE: The database session is live and payload identifiers are non-empty.
# @POST: Returns the committed session event row with a stable identifier and stored detail payload.
# @SIDE_EFFECT: Writes one audit row to persistence and emits logger.reason/logger.reflect traces.
# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]
# @PRE The database session is live and payload identifiers are non-empty.
# @POST Returns the committed session event row with a stable identifier and stored detail payload.
# @SIDE_EFFECT Writes one audit row to persistence and emits logger.reason/logger.reflect traces.
# @DATA_CONTRACT Input[SessionEventPayload] -> Output[SessionEvent]
class SessionEventLogger:
# region SessionEventLogger_init [TYPE Function]
# @PURPOSE: Bind a live SQLAlchemy session to the session-event logger.
@@ -53,11 +53,11 @@ class SessionEventLogger:
# region log_event [TYPE Function]
# @PURPOSE: Persist one explicit session event row for an owned dataset-review mutation.
# @RELATION: [DEPENDS_ON] ->[SessionEvent]
# @PRE: session_id, actor_user_id, event_type, and event_summary are non-empty.
# @POST: Returns the committed SessionEvent record with normalized detail payload.
# @SIDE_EFFECT: Inserts and commits one session_events row.
# @DATA_CONTRACT: Input[SessionEventPayload] -> Output[SessionEvent]
# @RELATION DEPENDS_ON ->[SessionEvent]
# @PRE session_id, actor_user_id, event_type, and event_summary are non-empty.
# @POST Returns the committed SessionEvent record with normalized detail payload.
# @SIDE_EFFECT Inserts and commits one session_events row.
# @DATA_CONTRACT Input[SessionEventPayload] -> Output[SessionEvent]
def log_event(self, payload: SessionEventPayload) -> SessionEvent:
with belief_scope("SessionEventLogger.log_event"):
session_id = str(payload.session_id or "").strip()
@@ -125,7 +125,7 @@ class SessionEventLogger:
# region log_for_session [TYPE Function]
# @PURPOSE: Convenience wrapper for logging an event directly from a session aggregate root.
# @RELATION: [CALLS] ->[SessionEventLogger.log_event]
# @RELATION CALLS ->[EXT:method:SessionEventLogger.log_event]
def log_for_session(
self,
session: DatasetReviewSession,

View File

@@ -1,20 +1,20 @@
# #region DatasetReviewOrchestrator [C:5] [TYPE Module] [SEMANTICS pydantic, dataset, review, orchestration, session]
# @BRIEF Coordinate dataset review session startup and lifecycle-safe intake recovery for one authenticated user.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [DatasetReviewSessionRepository]
# @RELATION DEPENDS_ON -> [SemanticSourceResolver]
# @RELATION DEPENDS_ON -> [SupersetContextExtractor]
# @RELATION DEPENDS_ON -> [SupersetCompilationAdapter]
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DISPATCHES -> [OrchestratorHelpers:Module]
# @RELATION DISPATCHES -> [OrchestratorCommands:Module]
# @PRE: session mutations must execute inside a persisted session boundary scoped to one authenticated user.
# @POST: state transitions are persisted atomically and emit observable progress for long-running steps.
# @SIDE_EFFECT: creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts.
# @DATA_CONTRACT: Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext]
# @INVARIANT: Launch is blocked unless a current session has no open blocking findings, all launch-sensitive mappings are approved, and a non-stale Superset-generated compiled preview matches the current input fingerprint.
# @RATIONALE: Original 1198-line monolith violated INV_7 (400-line module limit). Decomposed into commands and helpers sub-modules while preserving the orchestrator class as the single entry point.
# @REJECTED: Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x.
# @RELATION DISPATCHES -> [OrchestratorHelpers]
# @RELATION DISPATCHES -> [OrchestratorCommands]
# @PRE session mutations must execute inside a persisted session boundary scoped to one authenticated user.
# @POST state transitions are persisted atomically and emit observable progress for long-running steps.
# @SIDE_EFFECT creates task records, updates session aggregates, triggers upstream Superset calls, persists audit artifacts.
# @DATA_CONTRACT Input[SessionCommand] -> Output[DatasetReviewSession | CompiledPreview | DatasetRunContext]
# @INVARIANT Launch is blocked unless a current session has no open blocking findings, all launch-sensitive mappings are approved, and a non-stale Superset-generated compiled preview matches the current input fingerprint.
# @RATIONALE Original 1198-line monolith violated INV_7 (400-line module limit). Decomposed into commands and helpers sub-modules while preserving the orchestrator class as the single entry point.
# @REJECTED Keeping all orchestration logic in one file because it exceeded the fractal limit by 3x.
from __future__ import annotations
@@ -89,17 +89,17 @@ logger = cast(Any, logger)
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DEPENDS_ON -> [ConfigManager]
# @RELATION DEPENDS_ON -> [SemanticSourceResolver]
# @RELATION CALLS -> [OrchestratorHelpers:Module]
# @PRE: constructor dependencies are valid and tied to the current request/task scope.
# @POST: orchestrator instance can execute session-scoped mutations for one authenticated user.
# @SIDE_EFFECT: downstream operations may persist session/profile/finding state and enqueue background tasks.
# @DATA_CONTRACT: Input[StartSessionCommand] -> Output[StartSessionResult]
# @INVARIANT: session ownership is preserved on every mutation and recovery remains explicit when partial.
# @RELATION CALLS -> [OrchestratorHelpers]
# @PRE constructor dependencies are valid and tied to the current request/task scope.
# @POST orchestrator instance can execute session-scoped mutations for one authenticated user.
# @SIDE_EFFECT downstream operations may persist session/profile/finding state and enqueue background tasks.
# @DATA_CONTRACT Input[StartSessionCommand] -> Output[StartSessionResult]
# @INVARIANT session ownership is preserved on every mutation and recovery remains explicit when partial.
class DatasetReviewOrchestrator:
# region DatasetReviewOrchestrator_init [TYPE Function]
# @PURPOSE: Bind repository, config, and task dependencies required by the orchestration boundary.
# @PRE: repository/config_manager are valid collaborators for the current request scope.
# @POST: Instance holds collaborator references used by start/preview/launch orchestration methods.
# @PRE repository/config_manager are valid collaborators for the current request scope.
# @POST Instance holds collaborator references used by start/preview/launch orchestration methods.
def __init__(
self,
repository: DatasetReviewSessionRepository,
@@ -116,13 +116,13 @@ class DatasetReviewOrchestrator:
# region start_session [TYPE Function]
# @PURPOSE: Initialize a new session from a Superset link or dataset selection and trigger context recovery.
# @RELATION: CALLS -> [SupersetContextExtractor.parse_superset_link]
# @RELATION: CALLS -> [TaskManager.create_task]
# @PRE: source input is non-empty and environment is accessible.
# @POST: session exists in persisted storage with intake/recovery state and task linkage when async work is required.
# @SIDE_EFFECT: persists session and may enqueue recovery task.
# @DATA_CONTRACT: Input[StartSessionCommand] -> Output[StartSessionResult]
# @INVARIANT: no cross-user session leakage occurs; session and follow-up task remain owned by the authenticated user.
# @RELATION CALLS -> [EXT:method:SupersetContextExtractor.parse_superset_link]
# @RELATION CALLS -> [EXT:method:TaskManager.create_task]
# @PRE source input is non-empty and environment is accessible.
# @POST session exists in persisted storage with intake/recovery state and task linkage when async work is required.
# @SIDE_EFFECT persists session and may enqueue recovery task.
# @DATA_CONTRACT Input[StartSessionCommand] -> Output[StartSessionResult]
# @INVARIANT no cross-user session leakage occurs; session and follow-up task remain owned by the authenticated user.
def start_session(self, command: StartSessionCommand) -> StartSessionResult:
with belief_scope("DatasetReviewOrchestrator.start_session"):
normalized_source_kind = str(command.source_kind or "").strip()
@@ -270,11 +270,11 @@ class DatasetReviewOrchestrator:
# region prepare_launch_preview [TYPE Function]
# @PURPOSE: Assemble effective execution inputs and trigger Superset-side preview compilation.
# @RELATION: CALLS -> [SupersetCompilationAdapter.compile_preview]
# @PRE: all required variables have candidate values or explicitly accepted defaults.
# @POST: returns preview artifact in pending, ready, failed, or stale state.
# @SIDE_EFFECT: persists preview attempt and upstream compilation diagnostics.
# @DATA_CONTRACT: Input[PreparePreviewCommand] -> Output[PreparePreviewResult]
# @RELATION CALLS -> [SupersetCompilationAdapter.compile_preview]
# @PRE all required variables have candidate values or explicitly accepted defaults.
# @POST returns preview artifact in pending, ready, failed, or stale state.
# @SIDE_EFFECT persists preview attempt and upstream compilation diagnostics.
# @DATA_CONTRACT Input[PreparePreviewCommand] -> Output[PreparePreviewResult]
def prepare_launch_preview(self, command: PreparePreviewCommand) -> PreparePreviewResult:
with belief_scope("DatasetReviewOrchestrator.prepare_launch_preview"):
session = self.repository.load_session_detail(command.session_id, command.user.id)
@@ -349,12 +349,12 @@ class DatasetReviewOrchestrator:
# region launch_dataset [TYPE Function]
# @PURPOSE: Start the approved dataset execution through SQL Lab and persist run context for audit/replay.
# @RELATION: CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]
# @PRE: session is run-ready and compiled preview is current.
# @POST: returns persisted run context with SQL Lab session reference and launch outcome.
# @SIDE_EFFECT: creates SQL Lab execution session and audit snapshot.
# @DATA_CONTRACT: Input[LaunchDatasetCommand] -> Output[LaunchDatasetResult]
# @INVARIANT: launch remains blocked unless blocking findings are closed, approvals are satisfied, and the latest preview fingerprint matches current execution inputs.
# @RELATION CALLS -> [SupersetCompilationAdapter.create_sql_lab_session]
# @PRE session is run-ready and compiled preview is current.
# @POST returns persisted run context with SQL Lab session reference and launch outcome.
# @SIDE_EFFECT creates SQL Lab execution session and audit snapshot.
# @DATA_CONTRACT Input[LaunchDatasetCommand] -> Output[LaunchDatasetResult]
# @INVARIANT launch remains blocked unless blocking findings are closed, approvals are satisfied, and the latest preview fingerprint matches current execution inputs.
def launch_dataset(self, command: LaunchDatasetCommand) -> LaunchDatasetResult:
with belief_scope("DatasetReviewOrchestrator.launch_dataset"):
session = self.repository.load_session_detail(command.session_id, command.user.id)
@@ -448,9 +448,9 @@ class DatasetReviewOrchestrator:
# region _build_recovery_bootstrap [TYPE Function]
# @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.
# @POST: Returns bootstrap imported filters, template variables, execution mappings, and updated findings without persisting them directly.
# @SIDE_EFFECT: Performs Superset reads through the extractor and may append warning findings for incomplete recovery.
# @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.
# @SIDE_EFFECT Performs Superset reads through the extractor and may append warning findings for incomplete recovery.
def _build_recovery_bootstrap(
self,
environment,
@@ -547,9 +547,9 @@ class DatasetReviewOrchestrator:
# region _enqueue_recovery_task [TYPE Function]
# @PURPOSE: Link session start to observable async recovery when task infrastructure is available.
# @PRE: session is already persisted.
# @POST: returns task identifier when a task could be enqueued, otherwise None.
# @SIDE_EFFECT: may create one background task for progressive recovery.
# @PRE session is already persisted.
# @POST returns task identifier when a task could be enqueued, otherwise None.
# @SIDE_EFFECT may create one background task for progressive recovery.
def _enqueue_recovery_task(
self,
command: StartSessionCommand,

View File

@@ -1,6 +1,6 @@
# #region OrchestratorCommands [C:2] [TYPE Module] [SEMANTICS dataset, review, command, dataclass, boundary]
# @BRIEF Typed command and result dataclasses for dataset review orchestration boundary.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [DatasetReviewModels]
# @RELATION DEPENDS_ON -> [SupersetContextExtractor]

View File

@@ -1,10 +1,10 @@
# #region OrchestratorHelpers [C:4] [TYPE Module] [SEMANTICS dataset, review, snapshot, fingerprint, recovery]
# @BRIEF Pure helper methods extracted from DatasetReviewOrchestrator for INV_7 compliance — snapshot, blockers, fingerprint, recovery bootstrap.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [DatasetReviewModels]
# @RELATION DEPENDS_ON -> [SupersetContextExtractor]
# @PRE: Caller provides a loaded session aggregate with hydrated child collections.
# @POST: Helper results are deterministic and do not mutate persistence directly.
# @PRE Caller provides a loaded session aggregate with hydrated child collections.
# @POST Helper results are deterministic and do not mutate persistence directly.
from __future__ import annotations
@@ -95,8 +95,8 @@ def build_initial_profile(
# #region build_partial_recovery_findings [C:3] [TYPE Function]
# @BRIEF Project partial Superset intake recovery into explicit findings without blocking session usability.
# @PRE: parsed_context.partial_recovery is true.
# @POST: Returns warning-level findings that preserve usable but incomplete state.
# @PRE parsed_context.partial_recovery is true.
# @POST Returns warning-level findings that preserve usable but incomplete state.
def build_partial_recovery_findings(parsed_context: Any) -> list[ValidationFinding]:
findings: list[ValidationFinding] = []
for unresolved_ref in getattr(parsed_context, "unresolved_references", []):
@@ -138,8 +138,8 @@ def extract_effective_filter_value(
# #region build_execution_snapshot [C:4] [TYPE Function]
# @BRIEF Build effective filters, template params, approvals, and fingerprint for preview and launch gating.
# @PRE: Session aggregate includes imported filters, template variables, and current execution mappings.
# @POST: Returns deterministic execution snapshot for current session state without mutating persistence.
# @PRE Session aggregate includes imported filters, template variables, and current execution mappings.
# @POST Returns deterministic execution snapshot for current session state without mutating persistence.
def build_execution_snapshot(session: DatasetReviewSession) -> dict[str, Any]:
session_record = cast(Any, session)
filter_lookup = {
@@ -267,8 +267,8 @@ def build_execution_snapshot(session: DatasetReviewSession) -> dict[str, Any]:
# #region build_launch_blockers [C:3] [TYPE Function]
# @BRIEF Enforce launch gates from findings, approvals, and current preview truth.
# @PRE: execution_snapshot was computed from current session state.
# @POST: Returns explicit blocker codes for every unmet launch invariant.
# @PRE execution_snapshot was computed from current session state.
# @POST Returns explicit blocker codes for every unmet launch invariant.
def build_launch_blockers(
session: DatasetReviewSession,
execution_snapshot: dict[str, Any],

View File

@@ -27,7 +27,7 @@ from src.services.dataset_review.repositories.session_repository import (
)
# region SessionRepositoryTests [TYPE Module]
# @RELATION: BELONGS_TO -> SrcRoot
# @RELATION BELONGS_TO -> SrcRoot
# @PURPOSE: Unit tests for DatasetReviewSessionRepository.
@@ -35,7 +35,7 @@ from src.services.dataset_review.repositories.session_repository import (
def db_session():
# region db_session [TYPE Function]
# @PURPOSE: Build isolated in-memory SQLAlchemy session seeded with baseline user/environment rows.
# @RELATION: BINDS_TO -> [SessionRepositoryTests]
# @RELATION BINDS_TO -> [EXT:frontend:SessionRepositoryTests]
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
@@ -59,7 +59,7 @@ def db_session():
# region test_create_session [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
def test_create_session(db_session):
# @PURPOSE: Verify session creation and persistence.
repo = DatasetReviewSessionRepository(db_session)
@@ -86,7 +86,7 @@ def test_create_session(db_session):
# region test_require_session_version_conflict [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
# @PURPOSE: Verify optimistic-lock conflict is raised when caller version is stale.
def test_require_session_version_conflict(db_session):
repo = DatasetReviewSessionRepository(db_session)
@@ -111,7 +111,7 @@ def test_require_session_version_conflict(db_session):
# region test_bump_session_version_updates_last_activity [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
# @PURPOSE: Verify repository version bump increments monotonically and refreshes last activity.
def test_bump_session_version_updates_last_activity(db_session):
repo = DatasetReviewSessionRepository(db_session)
@@ -136,7 +136,7 @@ def test_bump_session_version_updates_last_activity(db_session):
# region test_save_recovery_state_preserves_raw_value_masked_flag [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
# @PURPOSE: Verify imported-filter masking metadata persists with recovery bootstrap state.
def test_save_recovery_state_preserves_raw_value_masked_flag(db_session):
repo = DatasetReviewSessionRepository(db_session)
@@ -177,7 +177,7 @@ def test_save_recovery_state_preserves_raw_value_masked_flag(db_session):
# region test_load_session_detail_ownership [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
def test_load_session_detail_ownership(db_session):
# @PURPOSE: Verify ownership enforcement in detail loading.
repo = DatasetReviewSessionRepository(db_session)
@@ -203,7 +203,7 @@ def test_load_session_detail_ownership(db_session):
# region test_load_session_detail_collaborator [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
def test_load_session_detail_collaborator(db_session):
# @PURPOSE: Verify collaborator access in detail loading.
repo = DatasetReviewSessionRepository(db_session)
@@ -240,7 +240,7 @@ def test_load_session_detail_collaborator(db_session):
# region test_save_preview_marks_stale [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
def test_save_preview_marks_stale(db_session):
# @PURPOSE: Verify that saving a new preview marks old ones as stale.
repo = DatasetReviewSessionRepository(db_session)
@@ -273,7 +273,7 @@ def test_save_preview_marks_stale(db_session):
# region test_save_preview_increments_session_version_once_per_call [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
# @PURPOSE: Verify preview persistence itself contributes exactly one optimistic-lock version increment so higher orchestration layers do not need to bump again for the same preview mutation.
def test_save_preview_increments_session_version_once_per_call(db_session):
repo = DatasetReviewSessionRepository(db_session)
@@ -315,7 +315,7 @@ def test_save_preview_increments_session_version_once_per_call(db_session):
# region test_save_profile_and_findings [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
def test_save_profile_and_findings(db_session):
# @PURPOSE: Verify persistence of profile and findings.
repo = DatasetReviewSessionRepository(db_session)
@@ -376,7 +376,7 @@ def test_save_profile_and_findings(db_session):
# region test_save_profile_and_findings_rejects_stale_concurrent_write [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
# @PURPOSE: Verify repository save path translates concurrent stale session writes into deterministic optimistic-lock conflicts.
def test_save_profile_and_findings_rejects_stale_concurrent_write(tmp_path: Path):
db_path = tmp_path / "dataset_review_session_repository.sqlite"
@@ -476,7 +476,7 @@ def test_save_profile_and_findings_rejects_stale_concurrent_write(tmp_path: Path
# region test_save_run_context [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
def test_save_run_context(db_session):
# @PURPOSE: Verify saving of run context.
repo = DatasetReviewSessionRepository(db_session)
@@ -511,7 +511,7 @@ def test_save_run_context(db_session):
# region test_ensure_dataset_review_session_columns_adds_missing_legacy_columns [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
# @PURPOSE: Verify additive dataset review migration creates missing legacy columns for session and imported-filter tables without dropping rows.
def test_ensure_dataset_review_session_columns_adds_missing_legacy_columns():
engine = create_engine("sqlite:///:memory:")
@@ -678,7 +678,7 @@ def test_ensure_dataset_review_session_columns_adds_missing_legacy_columns():
# region test_list_sessions_for_user [TYPE Function]
# @RELATION: BINDS_TO -> SessionRepositoryTests
# @RELATION BINDS_TO -> SessionRepositoryTests
def test_list_sessions_for_user(db_session):
# @PURPOSE: Verify listing of sessions by user.
repo = DatasetReviewSessionRepository(db_session)

View File

@@ -1,10 +1,10 @@
# #region SessionRepositoryMutations [C:4] [TYPE Module] [SEMANTICS dataset, repository, mutation, session, persistence]
# @BRIEF Persistence mutation operations for dataset review session aggregates — profile/findings, recovery state, preview, run context.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [DatasetReviewModels]
# @RELATION DEPENDS_ON -> [SessionEventLogger]
# @PRE: All mutations execute within authenticated request or task scope.
# @POST: Session aggregate writes preserve ownership and version semantics.
# @PRE All mutations execute within authenticated request or task scope.
# @POST Session aggregate writes preserve ownership and version semantics.
from __future__ import annotations
@@ -30,9 +30,9 @@ logger = cast(Any, logger)
# #region save_profile_and_findings [C:4] [TYPE Function]
# @BRIEF Persist profile state and replace validation findings for an owned session in one transaction.
# @PRE: session_id belongs to user_id and the supplied profile/findings belong to the same aggregate scope.
# @POST: stored profile matches the current session and findings are replaced by the supplied collection.
# @SIDE_EFFECT: updates profile rows, deletes stale findings, inserts current findings, and commits the transaction.
# @PRE session_id belongs to user_id and the supplied profile/findings belong to the same aggregate scope.
# @POST stored profile matches the current session and findings are replaced by the supplied collection.
# @SIDE_EFFECT updates profile rows, deletes stale findings, inserts current findings, and commits the transaction.
def save_profile_and_findings(
db: Session,
event_logger: SessionEventLogger,
@@ -73,9 +73,9 @@ def save_profile_and_findings(
# #region save_recovery_state [C:4] [TYPE Function]
# @BRIEF Persist imported filters, template variables, and initial execution mappings for one owned session.
# @PRE: session_id belongs to user_id.
# @POST: Recovery state persisted to database.
# @SIDE_EFFECT: Writes to database.
# @PRE session_id belongs to user_id.
# @POST Recovery state persisted to database.
# @SIDE_EFFECT Writes to database.
def save_recovery_state(
db: Session,
get_owned_session,
@@ -120,9 +120,9 @@ def save_recovery_state(
# #region save_preview [C:3] [TYPE Function]
# @BRIEF Persist a preview snapshot and mark prior session previews stale.
# @PRE: session_id belongs to user_id and preview is prepared for the same session aggregate.
# @POST: preview is persisted and the session points to the latest preview identifier.
# @SIDE_EFFECT: updates prior preview statuses, inserts a preview row, mutates the parent session, and commits.
# @PRE session_id belongs to user_id and preview is prepared for the same session aggregate.
# @POST preview is persisted and the session points to the latest preview identifier.
# @SIDE_EFFECT updates prior preview statuses, inserts a preview row, mutates the parent session, and commits.
def save_preview(
db: Session,
get_owned_session,
@@ -155,9 +155,9 @@ def save_preview(
# #region save_run_context [C:3] [TYPE Function]
# @BRIEF Persist an immutable launch audit snapshot for an owned session.
# @PRE: session_id belongs to user_id and run_context targets the same aggregate.
# @POST: run context is persisted and linked as the latest launch snapshot for the session.
# @SIDE_EFFECT: inserts a run-context row, mutates the parent session pointer, and commits.
# @PRE session_id belongs to user_id and run_context targets the same aggregate.
# @POST run context is persisted and linked as the latest launch snapshot for the session.
# @SIDE_EFFECT inserts a run-context row, mutates the parent session pointer, and commits.
def save_run_context(
db: Session,
get_owned_session,

View File

@@ -1,18 +1,18 @@
# #region DatasetReviewSessionRepository [C:5] [TYPE Module] [SEMANTICS dataset, repository, session, aggregate, persistence]
# @BRIEF Persist and retrieve dataset review session aggregates, including readiness, findings, semantic decisions, clarification state, previews, and run contexts.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [DatasetReviewSession]
# @RELATION DEPENDS_ON -> [DatasetProfile]
# @RELATION DEPENDS_ON -> [ValidationFinding]
# @RELATION DEPENDS_ON -> [CompiledPreview]
# @RELATION DISPATCHES -> [SessionRepositoryMutations:Module]
# @PRE: repository operations execute within authenticated request or task scope.
# @POST: session aggregate reads are structurally consistent and writes preserve ownership and version semantics.
# @SIDE_EFFECT: reads and writes SQLAlchemy-backed session aggregates.
# @DATA_CONTRACT: Input[SessionMutation] -> Output[PersistedSessionAggregate]
# @INVARIANT: answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.
# @RATIONALE: Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module.
# @REJECTED: Keeping all repository operations in one file because it exceeded the fractal limit.
# @RELATION DISPATCHES -> [SessionRepositoryMutations]
# @PRE repository operations execute within authenticated request or task scope.
# @POST session aggregate reads are structurally consistent and writes preserve ownership and version semantics.
# @SIDE_EFFECT reads and writes SQLAlchemy-backed session aggregates.
# @DATA_CONTRACT Input[SessionMutation] -> Output[PersistedSessionAggregate]
# @INVARIANT answers, mapping approvals, preview artifacts, and launch snapshots are never attributed to the wrong user or session.
# @RATIONALE Original 627-line file exceeded INV_7 (400-line module limit). Extracted mutation operations into _mutations sub-module.
# @REJECTED Keeping all repository operations in one file because it exceeded the fractal limit.
from datetime import datetime
from typing import Any, cast
@@ -60,14 +60,14 @@ class DatasetReviewSessionVersionConflictError(ValueError):
# @BRIEF Enforce ownership-scoped persistence and retrieval for dataset review session aggregates.
# @RELATION DEPENDS_ON -> [DatasetReviewSession]
# @RELATION DEPENDS_ON -> [SessionEventLogger]
# @PRE: constructor receives a live SQLAlchemy session and callers provide authenticated user scope.
# @POST: repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning.
# @SIDE_EFFECT: mutates and queries the persistence layer through the injected database session.
# @PRE constructor receives a live SQLAlchemy session and callers provide authenticated user scope.
# @POST repository methods return ownership-scoped aggregates or persisted child records without changing domain meaning.
# @SIDE_EFFECT mutates and queries the persistence layer through the injected database session.
class DatasetReviewSessionRepository:
# region init_repo [TYPE Function]
# @PURPOSE: Bind one live SQLAlchemy session to the repository instance.
# @PRE: db_session is not None
# @POST: Repository instance initialized with valid session
# @PRE db_session is not None
# @POST Repository instance initialized with valid session
def __init__(self, db: Session):
self.db = db
self.event_logger = SessionEventLogger(db)
@@ -76,8 +76,8 @@ class DatasetReviewSessionRepository:
# region get_owned_session [TYPE Function]
# @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.
# @POST: returns the owned session or raises a deterministic access error.
# @PRE session_id and user_id are non-empty identifiers from the authenticated ownership scope.
# @POST returns the owned session or raises a deterministic access error.
def _get_owned_session(self, session_id: str, user_id: str) -> DatasetReviewSession:
with belief_scope("DatasetReviewSessionRepository.get_owned_session"):
logger.reason("Resolving owner-scoped dataset review session", extra={"session_id": session_id, "user_id": user_id})
@@ -96,7 +96,7 @@ class DatasetReviewSessionRepository:
# region create_sess [TYPE Function]
# @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:
with belief_scope("DatasetReviewSessionRepository.create_session"):
logger.reason("Persisting dataset review session shell", extra={"user_id": session.user_id, "environment_id": session.environment_id})
@@ -110,7 +110,7 @@ class DatasetReviewSessionRepository:
# region require_session_version [TYPE Function]
# @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:
with belief_scope("DatasetReviewSessionRepository.require_session_version"):
actual_version = int(getattr(session, "version", 0) or 0)
@@ -125,7 +125,7 @@ class DatasetReviewSessionRepository:
# region bump_session_version [TYPE Function]
# @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:
with belief_scope("DatasetReviewSessionRepository.bump_session_version"):
next_version = int(getattr(session, "version", 0) or 0) + 1
@@ -138,7 +138,7 @@ class DatasetReviewSessionRepository:
# region commit_session_mutation [TYPE Function]
# @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(
self, session: DatasetReviewSession, *, refresh_targets: list[Any] | None = None, expected_version: int | None = None,
) -> DatasetReviewSession:
@@ -164,7 +164,7 @@ class DatasetReviewSessionRepository:
# region load_detail [TYPE Function]
# @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) -> DatasetReviewSession | None:
with belief_scope("DatasetReviewSessionRepository.load_session_detail"):
logger.reason("Loading dataset review session detail", extra={"session_id": session_id, "user_id": user_id})
@@ -197,7 +197,7 @@ class DatasetReviewSessionRepository:
# region save_profile_and_findings [TYPE Function]
# @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(
self, session_id: str, user_id: str, profile: DatasetProfile, findings: list[ValidationFinding], expected_version: int | None = None,
) -> DatasetReviewSession:

View File

@@ -1,14 +1,14 @@
# #region SemanticSourceResolver [C:4] [TYPE Module] [SEMANTICS pydantic, dataset, semantic, resolver, mapping]
# @BRIEF Resolve and rank semantic candidates from trusted dictionary-like sources before any inferred fallback.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [LLMProviderService]
# @RELATION DEPENDS_ON -> [SemanticSource]
# @RELATION DEPENDS_ON -> [SemanticFieldEntry]
# @RELATION DEPENDS_ON -> [SemanticCandidate]
# @PRE: selected source and target field set must be known.
# @POST: candidate ranking follows the configured confidence hierarchy and unresolved fuzzy matches remain reviewable.
# @SIDE_EFFECT: may create conflict findings and semantic candidate records.
# @INVARIANT: Manual overrides are never silently replaced by imported, inferred, or AI-generated values.
# @PRE selected source and target field set must be known.
# @POST candidate ranking follows the configured confidence hierarchy and unresolved fuzzy matches remain reviewable.
# @SIDE_EFFECT may create conflict findings and semantic candidate records.
# @INVARIANT Manual overrides are never silently replaced by imported, inferred, or AI-generated values.
from __future__ import annotations
@@ -45,9 +45,9 @@ class DictionaryResolutionResult:
# @BRIEF Resolve semantic candidates from trusted sources while preserving manual locks and confidence ordering.
# @RELATION DEPENDS_ON -> [SemanticFieldEntry]
# @RELATION DEPENDS_ON -> [SemanticCandidate]
# @PRE: source payload and target field collection are provided by the caller.
# @POST: result contains confidence-ranked candidates and does not overwrite manual locks implicitly.
# @SIDE_EFFECT: emits semantic trace logs for ranking and fallback decisions.
# @PRE source payload and target field collection are provided by the caller.
# @POST result contains confidence-ranked candidates and does not overwrite manual locks implicitly.
# @SIDE_EFFECT emits semantic trace logs for ranking and fallback decisions.
class SemanticSourceResolver:
# region resolve_from_file [TYPE Function]
# @PURPOSE: Normalize uploaded semantic file records into field-level candidates.
@@ -57,12 +57,12 @@ class SemanticSourceResolver:
# region resolve_from_dictionary [TYPE Function]
# @PURPOSE: Resolve candidates from connected tabular dictionary sources.
# @RELATION: [DEPENDS_ON] ->[SemanticFieldEntry]
# @RELATION: [DEPENDS_ON] ->[SemanticCandidate]
# @PRE: dictionary source exists and fields contain stable field_name values.
# @POST: returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit.
# @SIDE_EFFECT: emits belief-state logs describing trusted-match and partial-recovery outcomes.
# @DATA_CONTRACT: Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult]
# @RELATION DEPENDS_ON ->[SemanticFieldEntry]
# @RELATION DEPENDS_ON ->[SemanticCandidate]
# @PRE dictionary source exists and fields contain stable field_name values.
# @POST returns confidence-ranked candidates where exact dictionary matches outrank fuzzy matches and unresolved fields stay explicit.
# @SIDE_EFFECT emits belief-state logs describing trusted-match and partial-recovery outcomes.
# @DATA_CONTRACT Input[source_payload:Mapping,fields:Iterable] -> Output[DictionaryResolutionResult]
def resolve_from_dictionary(
self,
source_payload: Mapping[str, Any],
@@ -224,7 +224,7 @@ class SemanticSourceResolver:
# region rank_candidates [TYPE Function]
# @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]]:
ranked = sorted(
candidates,
@@ -255,12 +255,12 @@ class SemanticSourceResolver:
# region propagate_source_version_update [TYPE Function]
# @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] ->[SemanticFieldEntry]
# @PRE: source is persisted and fields belong to the same session aggregate.
# @POST: unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched.
# @SIDE_EFFECT: mutates in-memory field state for the caller to persist.
# @DATA_CONTRACT: Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]]
# @RELATION DEPENDS_ON ->[SemanticSource]
# @RELATION DEPENDS_ON ->[SemanticFieldEntry]
# @PRE source is persisted and fields belong to the same session aggregate.
# @POST unlocked fields linked to the source carry the new source version and are marked reviewable; manual or locked fields keep their active values untouched.
# @SIDE_EFFECT mutates in-memory field state for the caller to persist.
# @DATA_CONTRACT Input[SemanticSource,List[SemanticFieldEntry]] -> Output[Dict[str,int]]
def propagate_source_version_update(
self,
source: SemanticSource,

View File

@@ -1,5 +1,5 @@
# #region GitServiceModule [C:3] [TYPE Module] [SEMANTICS git, package, export, mixin, decomposition]
# @LAYER: Infra
# @LAYER Infrastructure
# @BRIEF Composed GitService via multiple inheritance from domain-specific mixins.
# @RELATION DEPENDS_ON -> [GitServiceBase]
# @RELATION DEPENDS_ON -> [GitServiceBranchMixin]
@@ -11,11 +11,11 @@
# @RELATION DEPENDS_ON -> [GitServiceGithubMixin]
# @RELATION DEPENDS_ON -> [GitServiceGitlabMixin]
#
# @RATIONALE: Decomposed from monolithic git_service.py (2101 lines) into
# @RATIONALE Decomposed from monolithic git_service.py (2101 lines) into
# domain-scoped mixins to satisfy INV_7 (module < 400 lines). The composed class
# preserves the original public API surface — all consumers continue to import
# `from src.services.git_service import GitService` without changes.
# @REJECTED: Keeping a single 2101-line file — violates fractal limit INV_7.
# @REJECTED Keeping a single 2101-line file — violates fractal limit INV_7.
from ._base import GitServiceBase
from ._branch import GitServiceBranchMixin

View File

@@ -1,5 +1,5 @@
# #region GitServiceBase [C:4] [TYPE Module] [SEMANTICS git, repository, clone, base, mixin, lock, http]
# @LAYER Infra
# @LAYER Infrastructure
# @BRIEF Core GitService base class — initialization, path resolution, repo lifecycle (init/delete/get), identity configuration, concurrent locking, and shared HTTP client pool.
# @RELATION DEPENDS_ON -> [GitRepository]
# @RELATION DEPENDS_ON -> [GitRepository]
@@ -348,12 +348,12 @@ class GitServiceBase:
with belief_scope("GitService.get_repo"):
repo_path = self._get_repo_path(dashboard_id)
if not os.path.exists(repo_path):
logger.error(f"[get_repo][Coherence:Failed] Repository for dashboard {dashboard_id} does not exist")
logger.error(f"[EXT:method:get_repo][Coherence:Failed] Repository for dashboard {dashboard_id} does not exist")
raise HTTPException(status_code=404, detail=f"Repository for dashboard {dashboard_id} not found")
try:
return Repo(repo_path)
except Exception as e:
logger.error(f"[get_repo][Coherence:Failed] Failed to open repository at {repo_path}: {e}")
logger.error(f"[EXT:method:get_repo][Coherence:Failed] Failed to open repository at {repo_path}: {e}")
raise HTTPException(status_code=500, detail="Failed to open local Git repository")
# endregion get_repo

View File

@@ -1,7 +1,7 @@
# #region GitServiceBranchMixin [C:4] [TYPE Module] [SEMANTICS git, branch, checkout, list, namespace, lock]
# @LAYER: Infra
# @LAYER Infrastructure
# @BRIEF Branch and commit operations for GitService — gitflow branches, list/create/checkout branches, commit changes (all concurrent-safe via per-dashboard locks).
# @RELATION USED_BY -> [GitService]
# @RELATION CALLED_BY -> [GitService]
from datetime import datetime
import os
@@ -17,8 +17,8 @@ from src.core.logger import belief_scope, logger
class GitServiceBranchMixin:
# region _ensure_gitflow_branches [C:4] [TYPE Function] [SEMANTICS git,gitflow,branch,lock]
# @PURPOSE: Ensure standard GitFlow branches (main/dev/preprod) exist locally and on origin.
# @PRE: repo is a valid GitPython Repo instance.
# @POST: main, dev, preprod are available in local repository and pushed to origin when available.
# @PRE repo is a valid GitPython Repo instance.
# @POST main, dev, preprod are available in local repository and pushed to origin when available.
# Active branch unchanged (no spurious checkout).
def _ensure_gitflow_branches(self, repo: Repo, dashboard_id: int) -> None:
with belief_scope("GitService._ensure_gitflow_branches"):
@@ -95,9 +95,9 @@ class GitServiceBranchMixin:
# region list_branches [C:4] [TYPE Function] [SEMANTICS git,branch,list,lock]
# @PURPOSE: List all branches (excluding tags) for a dashboard's repository, concurrent-safe.
# @PRE: Repository for dashboard_id exists.
# @POST: Returns a list of branch metadata dictionaries (no tag refs).
# @RETURN: List[dict]
# @PRE Repository for dashboard_id exists.
# @POST Returns a list of branch metadata dictionaries (no tag refs).
# @RETURN List[dict]
def list_branches(self, dashboard_id: int) -> list[dict]:
with self._locked(dashboard_id):
with belief_scope("GitService.list_branches"):
@@ -140,10 +140,10 @@ class GitServiceBranchMixin:
# region create_branch [C:4] [TYPE Function] [SEMANTICS git,branch,create,lock]
# @PURPOSE: Create a new branch from an existing one (concurrent-safe).
# @PARAM: name (str) - New branch name.
# @PARAM: from_branch (str) - Source branch.
# @PRE: Repository exists; name is valid; from_branch exists or repo is empty.
# @POST: A new branch is created in the repository.
# @PARAM name (str) - New branch name.
# @PARAM from_branch (str) - Source branch.
# @PRE Repository exists; name is valid; from_branch exists or repo is empty.
# @POST A new branch is created in the repository.
def create_branch(self, dashboard_id: int, name: str, from_branch: str = "main"):
with self._locked(dashboard_id):
with belief_scope("GitService.create_branch"):
@@ -172,8 +172,8 @@ class GitServiceBranchMixin:
# region checkout_branch [C:4] [TYPE Function] [SEMANTICS git,branch,checkout,lock]
# @PURPOSE: Switch to a specific branch (concurrent-safe).
# @PRE: Repository exists and the specified branch name exists.
# @POST: The repository working directory is updated to the specified branch.
# @PRE Repository exists and the specified branch name exists.
# @POST The repository working directory is updated to the specified branch.
def checkout_branch(self, dashboard_id: int, name: str):
with self._locked(dashboard_id):
with belief_scope("GitService.checkout_branch"):
@@ -184,10 +184,10 @@ class GitServiceBranchMixin:
# region commit_changes [C:4] [TYPE Function] [SEMANTICS git,commit,stage,lock]
# @PURPOSE: Stage and commit changes (concurrent-safe).
# @PARAM: message (str) - Commit message.
# @PARAM: files (List[str]) - Optional list of specific files to stage.
# @PRE: Repository exists and has changes (dirty) or files are specified.
# @POST: Changes are staged and a new commit is created.
# @PARAM message (str) - Commit message.
# @PARAM files (List[str]) - Optional list of specific files to stage.
# @PRE Repository exists and has changes (dirty) or files are specified.
# @POST Changes are staged and a new commit is created.
def commit_changes(self, dashboard_id: int, message: str, files: list[str] = None):
with self._locked(dashboard_id):
with belief_scope("GitService.commit_changes"):

View File

@@ -1,7 +1,7 @@
# #region GitServiceGiteaMixin [C:4] [TYPE Module] [SEMANTICS git, gitea, api, remote, connection, http_pool]
# @LAYER: Infra
# @LAYER Infrastructure
# @BRIEF Gitea API operations for GitService — connection testing, repository CRUD, and pull request creation. Uses shared self._http_client for connection pooling.
# @RELATION USED_BY -> [GitService]
# @RELATION CALLED_BY -> [GitService]
from typing import Any
from urllib.parse import quote
@@ -18,10 +18,10 @@ from src.models.git import GitProvider
class GitServiceGiteaMixin:
# region test_connection [TYPE Function]
# @PURPOSE: Test connection to Git provider using PAT.
# @PARAM: provider (GitProvider), url (str), pat (str)
# @PRE: provider is valid; url is a valid HTTP(S) URL; pat is provided.
# @POST: Returns True if connection to the provider's API succeeds.
# @RETURN: bool
# @PARAM provider (GitProvider), url (str), pat (str)
# @PRE provider is valid; url is a valid HTTP(S) URL; pat is provided.
# @POST Returns True if connection to the provider's API succeeds.
# @RETURN bool
async def test_connection(self, provider: GitProvider, url: str, pat: str) -> bool:
with belief_scope("GitService.test_connection"):
if ".local" in url or "localhost" in url:
@@ -59,9 +59,9 @@ class GitServiceGiteaMixin:
# region _gitea_headers [TYPE Function]
# @PURPOSE: Build Gitea API authorization headers.
# @PRE: pat is provided.
# @POST: Returns headers with token auth.
# @RETURN: Dict[str, str]
# @PRE pat is provided.
# @POST Returns headers with token auth.
# @RETURN Dict[str, str]
def _gitea_headers(self, pat: str) -> dict[str, str]:
token = (pat or "").strip()
if not token:
@@ -75,9 +75,9 @@ class GitServiceGiteaMixin:
# region _gitea_request [TYPE Function]
# @PURPOSE: Execute HTTP request against Gitea API with stable error mapping.
# @PRE: method and endpoint are valid.
# @POST: Returns decoded JSON payload.
# @RETURN: Any
# @PRE method and endpoint are valid.
# @POST Returns decoded JSON payload.
# @RETURN Any
async def _gitea_request(
self, method: str, server_url: str, pat: str, endpoint: str,
payload: dict[str, Any] | None = None,
@@ -106,9 +106,9 @@ class GitServiceGiteaMixin:
# region get_gitea_current_user [TYPE Function]
# @PURPOSE: Resolve current Gitea user for PAT.
# @PRE: server_url and pat are valid.
# @POST: Returns current username.
# @RETURN: str
# @PRE server_url and pat are valid.
# @POST Returns current username.
# @RETURN str
async def get_gitea_current_user(self, server_url: str, pat: str) -> str:
payload = await self._gitea_request("GET", server_url, pat, "/user")
username = payload.get("login") or payload.get("username")
@@ -119,9 +119,9 @@ class GitServiceGiteaMixin:
# region list_gitea_repositories [TYPE Function]
# @PURPOSE: List repositories visible to authenticated Gitea user.
# @PRE: server_url and pat are valid.
# @POST: Returns repository list from Gitea.
# @RETURN: List[dict]
# @PRE server_url and pat are valid.
# @POST Returns repository list from Gitea.
# @RETURN List[dict]
async def list_gitea_repositories(self, server_url: str, pat: str) -> list[dict]:
payload = await self._gitea_request("GET", server_url, pat, "/user/repos?limit=100&page=1")
if not isinstance(payload, list):
@@ -131,9 +131,9 @@ class GitServiceGiteaMixin:
# region create_gitea_repository [TYPE Function]
# @PURPOSE: Create repository in Gitea for authenticated user.
# @PRE: name is non-empty and PAT has repo creation permission.
# @POST: Returns created repository payload.
# @RETURN: dict
# @PRE name is non-empty and PAT has repo creation permission.
# @POST Returns created repository payload.
# @RETURN dict
async def create_gitea_repository(
self, server_url: str, pat: str, name: str, private: bool = True,
description: str | None = None, auto_init: bool = True, default_branch: str | None = "main",
@@ -151,8 +151,8 @@ class GitServiceGiteaMixin:
# region delete_gitea_repository [TYPE Function]
# @PURPOSE: Delete repository in Gitea.
# @PRE: owner and repo_name are non-empty.
# @POST: Repository deleted on Gitea server.
# @PRE owner and repo_name are non-empty.
# @POST Repository deleted on Gitea server.
async def delete_gitea_repository(self, server_url: str, pat: str, owner: str, repo_name: str) -> None:
if not owner or not repo_name:
raise HTTPException(status_code=400, detail="owner and repo_name are required")
@@ -161,9 +161,9 @@ class GitServiceGiteaMixin:
# region _gitea_branch_exists [TYPE Function]
# @PURPOSE: Check whether a branch exists in Gitea repository.
# @PRE: owner/repo/branch are non-empty.
# @POST: Returns True when branch exists, False when 404.
# @RETURN: bool
# @PRE owner/repo/branch are non-empty.
# @POST Returns True when branch exists, False when 404.
# @RETURN bool
async def _gitea_branch_exists(self, server_url: str, pat: str, owner: str, repo: str, branch: str) -> bool:
if not owner or not repo or not branch:
return False
@@ -179,9 +179,9 @@ class GitServiceGiteaMixin:
# region _build_gitea_pr_404_detail [TYPE Function]
# @PURPOSE: Build actionable error detail for Gitea PR 404 responses.
# @PRE: owner/repo/from_branch/to_branch are provided.
# @POST: Returns specific branch-missing message when detected.
# @RETURN: Optional[str]
# @PRE owner/repo/from_branch/to_branch are provided.
# @POST Returns specific branch-missing message when detected.
# @RETURN Optional[str]
async def _build_gitea_pr_404_detail(
self, server_url: str, pat: str, owner: str, repo: str, from_branch: str, to_branch: str,
) -> str | None:
@@ -200,9 +200,9 @@ class GitServiceGiteaMixin:
# region create_gitea_pull_request [TYPE Function]
# @PURPOSE: Create pull request in Gitea.
# @PRE: Config and remote URL are valid.
# @POST: Returns normalized PR metadata.
# @RETURN: Dict[str, Any]
# @PRE Config and remote URL are valid.
# @POST Returns normalized PR metadata.
# @RETURN Dict[str, Any]
async def create_gitea_pull_request(
self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,
title: str, description: str | None = None,

View File

@@ -1,7 +1,7 @@
# #region GitServiceMergeMixin [C:4] [TYPE Module] [SEMANTICS git, merge, branch, conflict, resolution, lock]
# @LAYER: Infra
# @LAYER Infrastructure
# @BRIEF Merge operations for GitService — conflict detection, resolution, abort, continue, and direct promote (all concurrent-safe via per-dashboard locks).
# @RELATION USED_BY -> [GitService]
# @RELATION CALLED_BY -> [GitService]
import os
from pathlib import Path
@@ -235,11 +235,11 @@ class GitServiceMergeMixin:
# region promote_direct_merge [C:4] [TYPE Function] [SEMANTICS git,merge,promote,branch,isolation]
# @PURPOSE: Perform direct merge between branches with branch isolation — original branch restored on error.
# @PRE: Repository exists and both branches are valid.
# @POST: Target branch contains merged changes from source branch. Active branch restored to original.
# @PRE Repository exists and both branches are valid.
# @POST Target branch contains merged changes from source branch. Active branch restored to original.
# Merge survives locally even if push fails (partial success).
# @SIDE_EFFECT: Changes local branch state during merge; restores original branch in finally block.
# @RETURN: Dict[str, Any]
# @SIDE_EFFECT Changes local branch state during merge; restores original branch in finally block.
# @RETURN Dict[str, Any]
def promote_direct_merge(self, dashboard_id: int, from_branch: str, to_branch: str) -> dict[str, Any]:
with self._locked(dashboard_id):
with belief_scope("GitService.promote_direct_merge"):

View File

@@ -1,7 +1,7 @@
# #region GitServiceRemoteMixin [C:4] [TYPE Module] [SEMANTICS git, provider, github, remote, url, http_pool]
# @LAYER: Infra
# @LAYER Infrastructure
# @BRIEF GitHub and GitLab provider operations for GitService — repository creation and PR/MR creation. Uses shared self._http_client for connection pooling.
# @RELATION USED_BY -> [GitService]
# @RELATION CALLED_BY -> [GitService]
from typing import Any
from urllib.parse import quote
@@ -17,9 +17,9 @@ from src.core.logger import logger
class GitServiceGithubMixin:
# region create_github_repository [TYPE Function]
# @PURPOSE: Create repository in GitHub or GitHub Enterprise.
# @PRE: PAT has repository create permission.
# @POST: Returns created repository payload.
# @RETURN: dict
# @PRE PAT has repository create permission.
# @POST Returns created repository payload.
# @RETURN dict
async def create_github_repository(
self, server_url: str, pat: str, name: str, private: bool = True,
description: str | None = None, auto_init: bool = True, default_branch: str | None = "main",
@@ -60,9 +60,9 @@ class GitServiceGithubMixin:
class GitServiceGitlabMixin:
# region create_gitlab_repository [TYPE Function]
# @PURPOSE: Create repository(project) in GitLab.
# @PRE: PAT has api scope.
# @POST: Returns created repository payload.
# @RETURN: dict
# @PRE PAT has api scope.
# @POST Returns created repository payload.
# @RETURN dict
async def create_gitlab_repository(
self, server_url: str, pat: str, name: str, private: bool = True,
description: str | None = None, auto_init: bool = True, default_branch: str | None = "main",
@@ -105,9 +105,9 @@ class GitServiceGitlabMixin:
# region create_gitlab_merge_request [TYPE Function]
# @PURPOSE: Create merge request in GitLab.
# @PRE: Config and remote URL are valid.
# @POST: Returns normalized MR metadata.
# @RETURN: Dict[str, Any]
# @PRE Config and remote URL are valid.
# @POST Returns normalized MR metadata.
# @RETURN Dict[str, Any]
async def create_gitlab_merge_request(
self, server_url: str, pat: str, remote_url: str, from_branch: str, to_branch: str,
title: str, description: str | None = None, remove_source_branch: bool = False,

View File

@@ -1,7 +1,7 @@
# #region GitServiceStatusMixin [C:4] [TYPE Module] [SEMANTICS git, status, diff, log, history, lock]
# @LAYER: Infra
# @LAYER Infrastructure
# @BRIEF Status, diff, and commit history operations for GitService (all concurrent-safe via per-dashboard locks).
# @RELATION USED_BY -> [GitService]
# @RELATION CALLED_BY -> [GitService]
from datetime import datetime
@@ -13,9 +13,9 @@ from src.core.logger import belief_scope, logger
class GitServiceStatusMixin:
# region _parse_status_porcelain [TYPE Function]
# @PURPOSE: Parse git status --porcelain output into staged, modified, and untracked file lists.
# @PRE: `repo` is an open GitPython Repo instance.
# @POST: Returns (staged, modified, untracked) tuple of file path lists.
# @RATIONALE: Avoids repo.is_dirty() / repo.index.diff("HEAD") which internally
# @PRE `repo` is an open GitPython Repo instance.
# @POST Returns (staged, modified, untracked) tuple of file path lists.
# @RATIONALE Avoids repo.is_dirty() / repo.index.diff("HEAD") which internally
# call git diff --cached, a flag unsupported in some Git environments (exit 129).
# Using git status --porcelain is self-contained and avoids the --cached flag entirely.
def _parse_status_porcelain(self, repo) -> tuple[list[str], list[str], list[str]]:
@@ -52,9 +52,9 @@ class GitServiceStatusMixin:
# region get_status [C:4] [TYPE Function] [SEMANTICS git,status,lock]
# @PURPOSE: Get current repository status (concurrent-safe).
# @PRE: Repository for dashboard_id exists.
# @POST: Returns a dictionary representing the Git status.
# @RETURN: dict
# @PRE Repository for dashboard_id exists.
# @POST Returns a dictionary representing the Git status.
# @RETURN dict
def get_status(self, dashboard_id: int) -> dict:
with self._locked(dashboard_id):
with belief_scope("GitService.get_status"):
@@ -113,11 +113,11 @@ class GitServiceStatusMixin:
# region get_diff [C:4] [TYPE Function] [SEMANTICS git,diff,lock]
# @PURPOSE: Generate diff for a file or the whole repository (concurrent-safe).
# @PARAM: file_path (str) - Optional specific file.
# @PARAM: staged (bool) - Whether to show staged changes.
# @PRE: Repository for dashboard_id exists.
# @POST: Returns the diff text as a string.
# @RETURN: str
# @PARAM file_path (str) - Optional specific file.
# @PARAM staged (bool) - Whether to show staged changes.
# @PRE Repository for dashboard_id exists.
# @POST Returns the diff text as a string.
# @RETURN str
def get_diff(self, dashboard_id: int, file_path: str = None, staged: bool = False) -> str:
with self._locked(dashboard_id):
with belief_scope("GitService.get_diff"):
@@ -132,10 +132,10 @@ class GitServiceStatusMixin:
# region get_commit_history [C:4] [TYPE Function] [SEMANTICS git,history,lock]
# @PURPOSE: Retrieve commit history for a repository (concurrent-safe).
# @PARAM: limit (int) - Max number of commits to return.
# @PRE: Repository for dashboard_id exists.
# @POST: Returns a list of dictionaries for each commit in history.
# @RETURN: List[dict]
# @PARAM limit (int) - Max number of commits to return.
# @PRE Repository for dashboard_id exists.
# @POST Returns a list of dictionaries for each commit in history.
# @RETURN List[dict]
def get_commit_history(self, dashboard_id: int, limit: int = 50) -> list[dict]:
with self._locked(dashboard_id):
with belief_scope("GitService.get_commit_history"):

View File

@@ -1,7 +1,7 @@
# #region GitServiceSyncMixin [C:4] [TYPE Module] [SEMANTICS git, sync, push, pull, remote, lock]
# @LAYER: Infra
# @LAYER Infrastructure
# @BRIEF Push and pull operations for GitService with origin host auto-alignment (concurrent-safe).
# @RELATION USED_BY -> [GitService]
# @RELATION CALLED_BY -> [GitService]
# @RELATION DEPENDS_ON -> [GitServiceUrlMixin]
import os
@@ -19,8 +19,8 @@ from src.models.git import GitRepository, GitServerConfig
class GitServiceSyncMixin:
# region push_changes [C:4] [TYPE Function] [SEMANTICS git,push,lock]
# @PURPOSE: Push local commits to remote (concurrent-safe).
# @PRE: Repository exists and has an 'origin' remote.
# @POST: Local branch commits are pushed to origin.
# @PRE Repository exists and has an 'origin' remote.
# @POST Local branch commits are pushed to origin.
def push_changes(self, dashboard_id: int):
with self._locked(dashboard_id):
with belief_scope("GitService.push_changes"):
@@ -113,8 +113,8 @@ class GitServiceSyncMixin:
# region pull_changes [C:4] [TYPE Function] [SEMANTICS git,pull,lock]
# @PURPOSE: Pull changes from remote (concurrent-safe).
# @PRE: Repository exists and has an 'origin' remote.
# @POST: Changes from origin are pulled and merged into the active branch.
# @PRE Repository exists and has an 'origin' remote.
# @POST Changes from origin are pulled and merged into the active branch.
def pull_changes(self, dashboard_id: int):
with self._locked(dashboard_id):
with belief_scope("GitService.pull_changes"):

View File

@@ -1,9 +1,9 @@
# #region GitServiceUrlMixin [C:3] [TYPE Module] [SEMANTICS git, url, parse, remote, endpoint]
# @LAYER: Infra
# @LAYER Infrastructure
# @BRIEF URL helper mixin for GitService — parse, normalize, align, and strip credentials from Git URLs.
# @RELATION USED_BY -> [GitServiceSyncMixin]
# @RELATION USED_BY -> [GitServiceGiteaMixin]
# @RELATION USED_BY -> [GitServiceRemoteMixin]
# @RELATION CALLED_BY -> [GitServiceSyncMixin]
# @RELATION CALLED_BY -> [GitServiceGiteaMixin]
# @RELATION CALLED_BY -> [GitServiceRemoteMixin]
from urllib.parse import quote, urlparse
@@ -19,9 +19,9 @@ from src.models.git import GitRepository
class GitServiceUrlMixin:
# region _extract_http_host [TYPE Function]
# @PURPOSE: Extract normalized host[:port] from HTTP(S) URL.
# @PRE: url_value may be empty.
# @POST: Returns lowercase host token or None.
# @RETURN: Optional[str]
# @PRE url_value may be empty.
# @POST Returns lowercase host token or None.
# @RETURN Optional[str]
def _extract_http_host(self, url_value: str | None) -> str | None:
normalized = str(url_value or "").strip()
if not normalized:
@@ -42,9 +42,9 @@ class GitServiceUrlMixin:
# region _strip_url_credentials [TYPE Function]
# @PURPOSE: Remove credentials from URL while preserving scheme/host/path.
# @PRE: url_value may contain credentials.
# @POST: Returns URL without username/password.
# @RETURN: str
# @PRE url_value may contain credentials.
# @POST Returns URL without username/password.
# @RETURN str
def _strip_url_credentials(self, url_value: str) -> str:
normalized = str(url_value or "").strip()
if not normalized:
@@ -63,9 +63,9 @@ class GitServiceUrlMixin:
# region _replace_host_in_url [TYPE Function]
# @PURPOSE: Replace source URL host with host from configured server URL.
# @PRE: source_url and config_url are HTTP(S) URLs.
# @POST: Returns source URL with updated host (credentials preserved) or None.
# @RETURN: Optional[str]
# @PRE source_url and config_url are HTTP(S) URLs.
# @POST Returns source URL with updated host (credentials preserved) or None.
# @RETURN Optional[str]
def _replace_host_in_url(self, source_url: str | None, config_url: str | None) -> str | None:
source = str(source_url or "").strip()
config = str(config_url or "").strip()
@@ -95,9 +95,9 @@ class GitServiceUrlMixin:
# region _align_origin_host_with_config [TYPE Function]
# @PURPOSE: Auto-align local origin host to configured Git server host when they drift.
# @PRE: origin remote exists.
# @POST: origin URL host updated and DB binding normalized when mismatch detected.
# @RETURN: Optional[str]
# @PRE origin remote exists.
# @POST origin URL host updated and DB binding normalized when mismatch detected.
# @RETURN Optional[str]
def _align_origin_host_with_config(
self,
dashboard_id: int,
@@ -151,9 +151,9 @@ class GitServiceUrlMixin:
# region _parse_remote_repo_identity [TYPE Function]
# @PURPOSE: Parse owner/repo from remote URL for Git server API operations.
# @PRE: remote_url is a valid git URL.
# @POST: Returns owner/repo tokens.
# @RETURN: Dict[str, str]
# @PRE remote_url is a valid git URL.
# @POST Returns owner/repo tokens.
# @RETURN Dict[str, str]
def _parse_remote_repo_identity(self, remote_url: str) -> dict[str, str]:
normalized = str(remote_url or "").strip()
if not normalized:
@@ -177,9 +177,9 @@ class GitServiceUrlMixin:
# region _derive_server_url_from_remote [TYPE Function]
# @PURPOSE: Build API base URL from remote repository URL without credentials.
# @PRE: remote_url may be any git URL.
# @POST: Returns normalized http(s) base URL or None when derivation is impossible.
# @RETURN: Optional[str]
# @PRE remote_url may be any git URL.
# @POST Returns normalized http(s) base URL or None when derivation is impossible.
# @RETURN Optional[str]
def _derive_server_url_from_remote(self, remote_url: str) -> str | None:
normalized = str(remote_url or "").strip()
if not normalized or normalized.startswith("git@"):
@@ -197,9 +197,9 @@ class GitServiceUrlMixin:
# region _normalize_git_server_url [TYPE Function]
# @PURPOSE: Normalize Git server URL for provider API calls.
# @PRE: raw_url is non-empty.
# @POST: Returns URL without trailing slash.
# @RETURN: str
# @PRE raw_url is non-empty.
# @POST Returns URL without trailing slash.
# @RETURN str
def _normalize_git_server_url(self, raw_url: str) -> str:
normalized = (raw_url or "").strip()
if not normalized:

View File

@@ -1,10 +1,10 @@
# #region git_service [C:1] [TYPE Module:Tombstone] [SEMANTICS git, service, shim, re-export, decomissioned]
# @BRIEF Re-export shim — GitService has been decomposed into services/git/ package.
# All consumers continue to import from this same path without changes.
# @RELATION REDIRECTS_TO -> [GitServiceModule]
# @RATIONALE: Monolithic GitService (2101 lines) was decomposed into 8 domain-specific mixins
# @RELATION CALLS -> [GitServiceModule]
# @RATIONALE Monolithic GitService (2101 lines) was decomposed into 8 domain-specific mixins
# under services/git/ to satisfy INV_7 (< 400 lines per module). This shim preserves
# the original import path for all 5 consumers.
# @REJECTED: Breaking 5 consumer imports to remove this shim — unacceptable migration cost.
# @REJECTED Breaking 5 consumer imports to remove this shim — unacceptable migration cost.
from src.services.git import GitService # noqa: F401
# #endregion git_service

View File

@@ -1,6 +1,6 @@
# #region health_service [C:3] [TYPE Module] [SEMANTICS sqlalchemy, health, dashboard, validation, aggregate]
# @BRIEF Business logic for aggregating dashboard health status from validation records.
# @LAYER: Domain/Service
# @LAYER Service
# @RELATION DEPENDS_ON -> [ValidationRecord]
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [TaskCleanupService]
@@ -28,10 +28,10 @@ def _empty_dashboard_meta() -> dict[str, str | None]:
# #region HealthService [C:4] [TYPE Class]
# @BRIEF Aggregate latest dashboard validation state and manage persisted health report lifecycle.
# @PRE: Service is constructed with a live SQLAlchemy session and optional config manager.
# @POST: Exposes health summary aggregation and validation report deletion operations.
# @SIDE_EFFECT: Maintains in-memory dashboard metadata caches and may coordinate cleanup through collaborators.
# @DATA_CONTRACT: Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool]
# @PRE Service is constructed with a live SQLAlchemy session and optional config manager.
# @POST Exposes health summary aggregation and validation report deletion operations.
# @SIDE_EFFECT Maintains in-memory dashboard metadata caches and may coordinate cleanup through collaborators.
# @DATA_CONTRACT Input[Session, Optional[Any]] -> Output[HealthSummaryResponse|bool]
# @RELATION DEPENDS_ON -> [ValidationRecord]
# @RELATION DEPENDS_ON -> [DashboardHealthItem]
# @RELATION DEPENDS_ON -> [HealthSummaryResponse]
@@ -50,11 +50,11 @@ class HealthService:
# region HealthService_init [TYPE Function]
# @PURPOSE: Initialize health service with DB session and optional config access for dashboard metadata resolution.
# @PRE: db is a valid SQLAlchemy session.
# @POST: Service is ready to aggregate summaries and delete health reports.
# @SIDE_EFFECT: Initializes per-instance dashboard metadata cache.
# @DATA_CONTRACT: Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]
# @RELATION: [BINDS_TO] ->[HealthService]
# @PRE db is a valid SQLAlchemy session.
# @POST Service is ready to aggregate summaries and delete health reports.
# @SIDE_EFFECT Initializes per-instance dashboard metadata cache.
# @DATA_CONTRACT Input[db: Session, config_manager: Optional[Any]] -> Output[HealthService]
# @RELATION BINDS_TO ->[HealthService]
def __init__(self, db: Session, config_manager=None):
self.db = db
self.config_manager = config_manager
@@ -64,13 +64,13 @@ class HealthService:
# region _prime_dashboard_meta_cache [TYPE Function]
# @PURPOSE: Warm dashboard slug/title cache with one Superset list fetch per environment.
# @PRE: records may contain mixed numeric and slug dashboard identifiers.
# @POST: Numeric dashboard ids for known environments are cached when discoverable.
# @SIDE_EFFECT: May call Superset dashboard list API once per referenced environment.
# @DATA_CONTRACT: Input[records: List[ValidationRecord]] -> Output[None]
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
# @RELATION: [DEPENDS_ON] ->[ConfigManager]
# @RELATION: [DEPENDS_ON] ->[SupersetClient]
# @PRE records may contain mixed numeric and slug dashboard identifiers.
# @POST Numeric dashboard ids for known environments are cached when discoverable.
# @SIDE_EFFECT May call Superset dashboard list API once per referenced environment.
# @DATA_CONTRACT Input[records: List[ValidationRecord]] -> Output[None]
# @RELATION DEPENDS_ON ->[ValidationRecord]
# @RELATION DEPENDS_ON ->[ConfigManager]
# @RELATION DEPENDS_ON ->[SupersetClient]
def _prime_dashboard_meta_cache(self, records: list[ValidationRecord]) -> None:
if not self.config_manager or not records:
return
@@ -142,7 +142,7 @@ class HealthService:
)
except Exception as exc:
logger.warning(
"[HealthService][_prime_dashboard_meta_cache] Failed to preload dashboard metadata for env=%s: %s",
"[HealthService][EXT:method:_prime_dashboard_meta_cache] Failed to preload dashboard metadata for env=%s: %s",
environment_id,
exc,
)
@@ -155,9 +155,9 @@ class HealthService:
# region _resolve_dashboard_meta [TYPE Function]
# @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.
# @POST: Returns dict with `slug` and `title` keys, using cache when possible.
# @SIDE_EFFECT: Writes default cache entries for unresolved numeric dashboard ids.
# @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.
# @SIDE_EFFECT Writes default cache entries for unresolved numeric dashboard ids.
def _resolve_dashboard_meta(
self, dashboard_id: str, environment_id: str | None
) -> dict[str, str | None]:
@@ -185,12 +185,12 @@ class HealthService:
# region get_health_summary [TYPE Function]
# @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.
# @POST: Returns HealthSummaryResponse with counts and latest record row per dashboard.
# @SIDE_EFFECT: May call Superset API to resolve dashboard metadata.
# @DATA_CONTRACT: Input[environment_id: Optional[str]] -> Output[HealthSummaryResponse]
# @RELATION: [CALLS] ->[_prime_dashboard_meta_cache]
# @RELATION: [CALLS] ->[_resolve_dashboard_meta]
# @PRE environment_id may be omitted to aggregate across all environments.
# @POST Returns HealthSummaryResponse with counts and latest record row per dashboard.
# @SIDE_EFFECT May call Superset API to resolve dashboard metadata.
# @DATA_CONTRACT Input[environment_id: Optional[str]] -> Output[HealthSummaryResponse]
# @RELATION CALLS ->[EXT:method:_prime_dashboard_meta_cache]
# @RELATION CALLS ->[EXT:method:_resolve_dashboard_meta]
async def get_health_summary(
self, environment_id: str = ""
) -> HealthSummaryResponse:
@@ -291,13 +291,13 @@ class HealthService:
# region delete_validation_report [TYPE Function]
# @PURPOSE: Delete one persisted health report and optionally clean linked task/log artifacts.
# @PRE: record_id is a validation record identifier.
# @POST: Returns True only when a matching record was deleted.
# @SIDE_EFFECT: Deletes DB rows, optional screenshot file, and optional task/log persistence.
# @DATA_CONTRACT: Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
# @RELATION: [DEPENDS_ON] ->[TaskManager]
# @RELATION: [DEPENDS_ON] ->[TaskCleanupService]
# @PRE record_id is a validation record identifier.
# @POST Returns True only when a matching record was deleted.
# @SIDE_EFFECT Deletes DB rows, optional screenshot file, and optional task/log persistence.
# @DATA_CONTRACT Input[record_id: str, task_manager: Optional[TaskManager]] -> Output[bool]
# @RELATION DEPENDS_ON ->[ValidationRecord]
# @RELATION DEPENDS_ON ->[TaskManager]
# @RELATION DEPENDS_ON ->[TaskCleanupService]
def delete_validation_report(
self, record_id: str, task_manager: TaskManager | None = None
) -> bool:

View File

@@ -1,8 +1,8 @@
# #region llm_prompt_templates [C:2] [TYPE Module] [SEMANTICS llm, prompt, template, normalization]
# @BRIEF Provide default LLM prompt templates and normalization helpers for runtime usage.
# @LAYER: Domain
# @RELATION DEPENDS_ON -> [backend.src.core.config_manager:Function]
# @INVARIANT: All required prompt template keys are always present after normalization.
# @LAYER Domain
# @RELATION DEPENDS_ON -> [ConfigManager]
# @INVARIANT All required prompt template keys are always present after normalization.
from __future__ import annotations
@@ -81,8 +81,8 @@ DEFAULT_LLM_ASSISTANT_SETTINGS: dict[str, str] = {
# #region normalize_llm_settings [C:3] [TYPE Function]
# @BRIEF Ensure llm settings contain stable schema with prompts section and default templates.
# @PRE: llm_settings is dictionary-like value or None.
# @POST: Returned dict contains prompts with all required template keys.
# @PRE llm_settings is dictionary-like value or None.
# @POST Returned dict contains prompts with all required template keys.
# @RELATION DEPENDS_ON -> LLMProviderService
def normalize_llm_settings(llm_settings: Any) -> dict[str, Any]:
normalized: dict[str, Any] = {
@@ -175,8 +175,8 @@ def is_multimodal_model(model_name: str, provider_type: str | None = None) -> bo
# #region resolve_bound_provider_id [C:3] [TYPE Function]
# @BRIEF Resolve provider id configured for a task binding with fallback to default provider.
# @PRE: llm_settings is normalized or raw dict from config.
# @POST: Returns configured provider id or fallback id/empty string when not defined.
# @PRE llm_settings is normalized or raw dict from config.
# @POST Returns configured provider id or fallback id/empty string when not defined.
# @RELATION DEPENDS_ON -> LLMProviderService
def resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str:
normalized = normalize_llm_settings(llm_settings)
@@ -191,8 +191,8 @@ def resolve_bound_provider_id(llm_settings: Any, task_key: str) -> str:
# #region render_prompt [C:3] [TYPE Function]
# @BRIEF Render prompt template using deterministic placeholder replacement with graceful fallback.
# @PRE: template is a string and variables values are already stringifiable.
# @POST: Returns rendered prompt text with known placeholders substituted. Warns about unfilled placeholders.
# @PRE template is a string and variables values are already stringifiable.
# @POST Returns rendered prompt text with known placeholders substituted. Warns about unfilled placeholders.
# @RELATION DEPENDS_ON -> LLMProviderService
def render_prompt(template: str, variables: dict[str, Any]) -> str:
rendered = template

View File

@@ -1,6 +1,6 @@
# #region llm_provider [C:3] [TYPE Module] [SEMANTICS sqlalchemy, llm, provider, encryption, config]
# @BRIEF Service for managing LLM provider configurations with encrypted API keys.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [LLMProvider]
# @RELATION DEPENDS_ON -> [EncryptionManager]
# @RELATION DEPENDS_ON -> [LLMProviderConfig]
@@ -22,8 +22,8 @@ MASKED_API_KEY_PLACEHOLDER = "********"
# #region mask_api_key [C:2] [TYPE Function]
# @BRIEF Mask an API key for safe display, showing first 4 and last 4 characters.
# @PRE: api_key is a plaintext string or None.
# @POST: Returns "****" for very short keys; "{first 2}...{last 2}" for <=8 chars;
# @PRE api_key is a plaintext string or None.
# @POST Returns "****" for very short keys; "{first 2}...{last 2}" for <=8 chars;
# "{first 4}...{last 4}" for longer keys; "" for None/empty.
def mask_api_key(api_key: str | None) -> str:
if not api_key:
@@ -40,8 +40,8 @@ def mask_api_key(api_key: str | None) -> str:
# #region is_masked_or_placeholder [C:2] [TYPE Function]
# @BRIEF Predicate: True when api_key is None, empty, "********", or contains "...".
# @PRE: api_key can be None.
# @POST: Returns True only for non-real-key values.
# @PRE api_key can be None.
# @POST Returns True only for non-real-key values.
def is_masked_or_placeholder(api_key: str | None) -> bool:
if not api_key:
return True
@@ -53,12 +53,12 @@ def is_masked_or_placeholder(api_key: str | None) -> bool:
# #region _require_fernet_key [C:5] [TYPE Function]
# @BRIEF Load and validate the Fernet key used for secret encryption.
# @PRE: ENCRYPTION_KEY environment variable must be set to a valid Fernet key.
# @POST: Returns validated key bytes ready for Fernet initialization.
# @RELATION DEPENDS_ON -> [backend.src.core.logger:Function]
# @SIDE_EFFECT: Emits belief-state logs for missing or invalid encryption configuration.
# @DATA_CONTRACT: Input[ENCRYPTION_KEY:str] -> Output[bytes]
# @INVARIANT: Encryption initialization never falls back to a hardcoded secret.
# @PRE ENCRYPTION_KEY environment variable must be set to a valid Fernet key.
# @POST Returns validated key bytes ready for Fernet initialization.
# @RELATION DEPENDS_ON -> [LoggerModule]
# @SIDE_EFFECT Emits belief-state logs for missing or invalid encryption configuration.
# @DATA_CONTRACT Input[ENCRYPTION_KEY:str] -> Output[bytes]
# @INVARIANT Encryption initialization never falls back to a hardcoded secret.
def _require_fernet_key() -> bytes:
with belief_scope("_require_fernet_key"):
raw_key = os.getenv("ENCRYPTION_KEY", "").strip()
@@ -87,28 +87,28 @@ def _require_fernet_key() -> bytes:
# #region EncryptionManager [C:5] [TYPE Class]
# @BRIEF Handles encryption and decryption of sensitive data like API keys.
# @RELATION CALLS -> [_require_fernet_key]
# @PRE: ENCRYPTION_KEY is configured with a valid Fernet key before instantiation.
# @POST: Manager exposes reversible encrypt/decrypt operations for persisted secrets.
# @SIDE_EFFECT: Initializes Fernet cryptography state from process environment.
# @DATA_CONTRACT: Input[str] -> Output[str]
# @INVARIANT: Uses only a validated secret key from environment.
# @PRE ENCRYPTION_KEY is configured with a valid Fernet key before instantiation.
# @POST Manager exposes reversible encrypt/decrypt operations for persisted secrets.
# @SIDE_EFFECT Initializes Fernet cryptography state from process environment.
# @DATA_CONTRACT Input[str] -> Output[str]
# @INVARIANT Uses only a validated secret key from environment.
#
# @TEST_CONTRACT: EncryptionManagerModel ->
# @TEST_CONTRACT EncryptionManagerModel ->
# {
# required_fields: {},
# invariants: [
# "encrypted data can be decrypted back to the original string"
# ]
# }
# @TEST_FIXTURE: basic_encryption_cycle -> {"data": "my_secret_key"}
# @TEST_EDGE: decrypt_invalid_data -> raises Exception
# @TEST_EDGE: empty_string_encryption -> {"data": ""}
# @TEST_INVARIANT: symmetric_encryption -> verifies: [basic_encryption_cycle, empty_string_encryption]
# @TEST_FIXTURE basic_encryption_cycle -> {"data": "my_secret_key"}
# @TEST_EDGE decrypt_invalid_data -> raises Exception
# @TEST_EDGE empty_string_encryption -> {"data": ""}
# @TEST_INVARIANT symmetric_encryption -> verifies: [basic_encryption_cycle, empty_string_encryption]
class EncryptionManager:
# region EncryptionManager_init [TYPE Function]
# @PURPOSE: Initialize the encryption manager with a Fernet key.
# @PRE: ENCRYPTION_KEY env var must be set to a valid Fernet key.
# @POST: Fernet instance ready for encryption/decryption.
# @PRE ENCRYPTION_KEY env var must be set to a valid Fernet key.
# @POST Fernet instance ready for encryption/decryption.
def __init__(self):
self.key = _require_fernet_key()
self.fernet = Fernet(self.key)
@@ -117,8 +117,8 @@ class EncryptionManager:
# region encrypt [TYPE Function]
# @PURPOSE: Encrypt a plaintext string.
# @PRE: data must be a non-empty string.
# @POST: Returns encrypted string.
# @PRE data must be a non-empty string.
# @POST Returns encrypted string.
def encrypt(self, data: str) -> str:
with belief_scope("encrypt"):
return self.fernet.encrypt(data.encode()).decode()
@@ -127,8 +127,8 @@ class EncryptionManager:
# region decrypt [TYPE Function]
# @PURPOSE: Decrypt an encrypted string.
# @PRE: encrypted_data must be a valid Fernet-encrypted string.
# @POST: Returns original plaintext string.
# @PRE encrypted_data must be a valid Fernet-encrypted string.
# @POST Returns original plaintext string.
def decrypt(self, encrypted_data: str) -> str:
with belief_scope("decrypt"):
return self.fernet.decrypt(encrypted_data.encode()).decode()
@@ -147,9 +147,9 @@ class EncryptionManager:
class LLMProviderService:
# region LLMProviderService_init [TYPE Function]
# @PURPOSE: Initialize the service with database session.
# @PRE: db must be a valid SQLAlchemy Session.
# @POST: Service ready for provider operations.
# @RELATION: [DEPENDS_ON] ->[EncryptionManager]
# @PRE db must be a valid SQLAlchemy Session.
# @POST Service ready for provider operations.
# @RELATION DEPENDS_ON ->[EncryptionManager]
def __init__(self, db: Session):
self.db = db
self.encryption = EncryptionManager()
@@ -158,9 +158,9 @@ class LLMProviderService:
# region get_all_providers [TYPE Function]
# @PURPOSE: Returns all configured LLM providers.
# @PRE: Database connection must be active.
# @POST: Returns list of all LLMProvider records.
# @RELATION: [DEPENDS_ON] ->[LLMProvider]
# @PRE Database connection must be active.
# @POST Returns list of all LLMProvider records.
# @RELATION DEPENDS_ON ->[LLMProvider]
def get_all_providers(self) -> list[LLMProvider]:
with belief_scope("get_all_providers"):
return self.db.query(LLMProvider).all()
@@ -169,9 +169,9 @@ class LLMProviderService:
# region get_provider [TYPE Function]
# @PURPOSE: Returns a single LLM provider by ID.
# @PRE: provider_id must be a valid string.
# @POST: Returns LLMProvider or None if not found.
# @RELATION: [DEPENDS_ON] ->[LLMProvider]
# @PRE provider_id must be a valid string.
# @POST Returns LLMProvider or None if not found.
# @RELATION DEPENDS_ON ->[LLMProvider]
def get_provider(self, provider_id: str) -> LLMProvider | None:
with belief_scope("get_provider"):
return (
@@ -182,11 +182,11 @@ class LLMProviderService:
# region create_provider [TYPE Function]
# @PURPOSE: Creates a new LLM provider with encrypted API key.
# @PRE: config must contain valid provider configuration.
# @POST: New provider created and persisted to database.
# @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]
# @RELATION: [DEPENDS_ON] ->[LLMProvider]
# @RELATION: [CALLS] ->[encrypt]
# @PRE config must contain valid provider configuration.
# @POST New provider created and persisted to database.
# @RELATION DEPENDS_ON ->[LLMProviderConfig]
# @RELATION DEPENDS_ON ->[LLMProvider]
# @RELATION CALLS ->[EXT:method:encrypt]
def create_provider(self, config: "LLMProviderConfig") -> LLMProvider:
with belief_scope("create_provider"):
encrypted_key = self.encryption.encrypt(config.api_key)
@@ -208,11 +208,11 @@ class LLMProviderService:
# region update_provider [TYPE Function]
# @PURPOSE: Updates an existing LLM provider.
# @PRE: provider_id must exist, config must be valid.
# @POST: Provider updated and persisted to database.
# @RELATION: [DEPENDS_ON] ->[LLMProviderConfig]
# @RELATION: [DEPENDS_ON] ->[LLMProvider]
# @RELATION: [CALLS] ->[encrypt]
# @PRE provider_id must exist, config must be valid.
# @POST Provider updated and persisted to database.
# @RELATION DEPENDS_ON ->[LLMProviderConfig]
# @RELATION DEPENDS_ON ->[LLMProvider]
# @RELATION CALLS ->[EXT:method:encrypt]
def update_provider(
self, provider_id: str, config: "LLMProviderConfig"
) -> LLMProvider | None:
@@ -239,9 +239,9 @@ class LLMProviderService:
# region delete_provider [TYPE Function]
# @PURPOSE: Deletes an LLM provider.
# @PRE: provider_id must exist.
# @POST: Provider removed from database.
# @RELATION: [DEPENDS_ON] ->[LLMProvider]
# @PRE provider_id must exist.
# @POST Provider removed from database.
# @RELATION DEPENDS_ON ->[LLMProvider]
def delete_provider(self, provider_id: str) -> bool:
with belief_scope("delete_provider"):
db_provider = self.get_provider(provider_id)
@@ -255,10 +255,10 @@ class LLMProviderService:
# region get_decrypted_api_key [TYPE Function]
# @PURPOSE: Returns the decrypted API key for a provider.
# @PRE: provider_id must exist with valid encrypted key.
# @POST: Returns decrypted API key or None on failure.
# @RELATION: [DEPENDS_ON] ->[LLMProvider]
# @RELATION: [CALLS] ->[decrypt]
# @PRE provider_id must exist with valid encrypted key.
# @POST Returns decrypted API key or None on failure.
# @RELATION DEPENDS_ON ->[LLMProvider]
# @RELATION CALLS ->[EXT:method:decrypt]
def get_decrypted_api_key(self, provider_id: str) -> str | None:
with belief_scope("get_decrypted_api_key"):
db_provider = self.get_provider(provider_id)

View File

@@ -1,15 +1,15 @@
# #region mapping_service [C:5] [TYPE Module] [SEMANTICS mapping, database, superset, fuzzy, suggestion]
#
# @BRIEF Orchestrates database fetching and fuzzy matching suggestions.
# @LAYER: Service
# @PRE: source/target environment identifiers are provided by caller.
# @POST: Exposes stateless mapping suggestion orchestration over configured environments.
# @SIDE_EFFECT: Performs remote metadata reads through Superset API clients.
# @DATA_CONTRACT: Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]]
# @LAYER Service
# @PRE source/target environment identifiers are provided by caller.
# @POST Exposes stateless mapping suggestion orchestration over configured environments.
# @SIDE_EFFECT Performs remote metadata reads through Superset API clients.
# @DATA_CONTRACT Input[source_env_id: str, target_env_id: str] -> Output[List[Dict]]
# @RELATION DEPENDS_ON -> SupersetClient
# @RELATION DEPENDS_ON -> suggest_mappings
#
# @INVARIANT: Suggestions are based on database names.
# @INVARIANT Suggestions are based on database names.
from ..core.logger import belief_scope
@@ -19,19 +19,19 @@ from ..core.utils.matching import suggest_mappings
# #region MappingService [C:3] [TYPE Class]
# @BRIEF Service for handling database mapping logic.
# @PRE: config_manager exposes get_environments() with environment objects containing id.
# @POST: Provides client resolution and mapping suggestion methods.
# @SIDE_EFFECT: Instantiates Superset clients and performs upstream metadata reads.
# @DATA_CONTRACT: Input[config_manager] -> Output[List[Dict]]
# @PRE config_manager exposes get_environments() with environment objects containing id.
# @POST Provides client resolution and mapping suggestion methods.
# @SIDE_EFFECT Instantiates Superset clients and performs upstream metadata reads.
# @DATA_CONTRACT Input[config_manager] -> Output[List[Dict]]
# @RELATION DEPENDS_ON -> SupersetClient
# @RELATION DEPENDS_ON -> suggest_mappings
class MappingService:
# region init [TYPE Function]
# @PURPOSE: Initializes the mapping service with a config manager.
# @PRE: config_manager is provided.
# @PARAM: config_manager (ConfigManager) - The configuration manager.
# @POST: Service is initialized.
# @RELATION: DEPENDS_ON -> MappingService
# @PRE config_manager is provided.
# @PARAM config_manager (ConfigManager) - The configuration manager.
# @POST Service is initialized.
# @RELATION DEPENDS_ON -> MappingService
def __init__(self, config_manager):
with belief_scope("MappingService.__init__"):
self.config_manager = config_manager
@@ -40,11 +40,11 @@ class MappingService:
# region _get_client [TYPE Function]
# @PURPOSE: Helper to get an initialized SupersetClient for an environment.
# @PARAM: env_id (str) - The ID of the environment.
# @PRE: environment must exist in config.
# @POST: Returns an initialized SupersetClient.
# @RETURN: SupersetClient - Initialized client.
# @RELATION: CALLS -> SupersetClient
# @PARAM env_id (str) - The ID of the environment.
# @PRE environment must exist in config.
# @POST Returns an initialized SupersetClient.
# @RETURN SupersetClient - Initialized client.
# @RELATION CALLS -> SupersetClient
def _get_client(self, env_id: str) -> SupersetClient:
with belief_scope("MappingService._get_client", f"env_id={env_id}"):
envs = self.config_manager.get_environments()
@@ -58,13 +58,13 @@ class MappingService:
# region get_suggestions [TYPE Function]
# @PURPOSE: Fetches databases from both environments and returns fuzzy matching suggestions.
# @PARAM: source_env_id (str) - Source environment ID.
# @PARAM: target_env_id (str) - Target environment ID.
# @PRE: Both environments must be accessible.
# @POST: Returns fuzzy-matched database suggestions.
# @RETURN: List[Dict] - Suggested mappings.
# @RELATION: CALLS -> _get_client
# @RELATION: CALLS -> suggest_mappings
# @PARAM source_env_id (str) - Source environment ID.
# @PARAM target_env_id (str) - Target environment ID.
# @PRE Both environments must be accessible.
# @POST Returns fuzzy-matched database suggestions.
# @RETURN List[Dict] - Suggested mappings.
# @RELATION CALLS -> _get_client
# @RELATION CALLS -> suggest_mappings
async def get_suggestions(
self, source_env_id: str, target_env_id: str
) -> list[dict]:

View File

@@ -1,4 +1,4 @@
# #region notifications [TYPE Package] [SEMANTICS notification, package, service]
# #region notifications [C:1] [TYPE Package] [SEMANTICS notification, package, service]
# @BRIEF Notification service package root.
# @RELATION EXPORTS -> [NotificationService:Class]
# @RELATION CALLS -> [NotificationService]
# #endregion notifications

View File

@@ -1,6 +1,6 @@
# region test_notification_service [TYPE Module]
# @PURPOSE: Unit tests for NotificationService routing and dispatch logic.
# @RELATION: TESTS ->[NotificationService:Class]
# @RELATION BINDS_TO ->[NotificationService]
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

View File

@@ -1,20 +1,20 @@
# #region providers [C:5] [TYPE Module] [SEMANTICS notification, provider, smtp, telegram, slack]
#
# @BRIEF Defines abstract base and concrete implementations for external notification delivery.
# @RELATION DEPENDED_ON_BY -> [NotificationService]
# @RELATION CALLED_BY -> [NotificationService]
# @RELATION DEPENDS_ON -> [NotificationProvider]
# @RELATION DEPENDS_ON -> [SMTPProvider]
# @RELATION DEPENDS_ON -> [TelegramProvider]
# @RELATION DEPENDS_ON -> [SlackProvider]
# @LAYER: Infra
# @PRE: Provider configuration dictionaries are supplied by trusted configuration sources.
# @POST: Each provider exposes async send contract returning boolean delivery outcome.
# @SIDE_EFFECT: Performs outbound network I/O to SMTP or HTTP endpoints.
# @DATA_CONTRACT: Input[target, subject, body, context?] -> Output[bool]
# @INVARIANT: Concrete providers preserve boolean send contract and swallow transport exceptions into False.
# @LAYER Infrastructure
# @PRE Provider configuration dictionaries are supplied by trusted configuration sources.
# @POST Each provider exposes async send contract returning boolean delivery outcome.
# @SIDE_EFFECT Performs outbound network I/O to SMTP or HTTP endpoints.
# @DATA_CONTRACT Input[target, subject, body, context?] -> Output[bool]
# @INVARIANT Concrete providers preserve boolean send contract and swallow transport exceptions into False.
#
# @INVARIANT: Providers must be stateless and resilient to network failures.
# @INVARIANT: Sensitive credentials must be handled via encrypted config.
# @INVARIANT Providers must be stateless and resilient to network failures.
# @INVARIANT Sensitive credentials must be handled via encrypted config.
from abc import ABC, abstractmethod
from email.mime.multipart import MIMEMultipart
@@ -29,9 +29,9 @@ from ...core.logger import logger
# #region NotificationProvider [C:2] [TYPE Class]
# @BRIEF Abstract base class for all notification providers.
# @RELATION DEPENDED_ON_BY -> [SMTPProvider]
# @RELATION DEPENDED_ON_BY -> [TelegramProvider]
# @RELATION DEPENDED_ON_BY -> [SlackProvider]
# @RELATION CALLED_BY -> [SMTPProvider]
# @RELATION CALLED_BY -> [TelegramProvider]
# @RELATION CALLED_BY -> [SlackProvider]
class NotificationProvider(ABC):
@abstractmethod
async def send(

View File

@@ -1,7 +1,7 @@
# #region service [C:5] [TYPE Module] [SEMANTICS fastapi, notification, dispatch, policy, routing]
#
# @BRIEF Orchestrates notification routing based on user preferences and policy context.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [NotificationProvider]
# @RELATION DEPENDS_ON -> [SMTPProvider]
# @RELATION DEPENDS_ON -> [TelegramProvider]
@@ -10,11 +10,11 @@
# @RELATION DEPENDS_ON -> [ValidationPolicy]
# @RELATION DEPENDS_ON -> [UserDashboardPreference]
#
# @INVARIANT: NotificationService maintains singleton pattern for per-channel notifications
# @DATA_CONTRACT: NotificationChannelConfig -> NotificationRecipient
# @PRE: channel_config is loaded
# @POST: Notification dispatched via configured providers
# @SIDE_EFFECT: Sends notifications via configured providers
# @INVARIANT NotificationService maintains singleton pattern for per-channel notifications
# @DATA_CONTRACT NotificationChannelConfig -> NotificationRecipient
# @PRE channel_config is loaded
# @POST Notification dispatched via configured providers
# @SIDE_EFFECT Sends notifications via configured providers
from fastapi import BackgroundTasks
from sqlalchemy.orm import Session
@@ -37,15 +37,15 @@ from .providers import (
# @RELATION DEPENDS_ON -> [ValidationRecord]
# @RELATION DEPENDS_ON -> [ValidationPolicy]
# @RELATION DEPENDS_ON -> [UserDashboardPreference]
# @PRE: Service receives a live DB session and configuration manager with notification payload settings.
# @POST: Service can resolve targets and dispatch provider sends without mutating validation records.
# @SIDE_EFFECT: Reads notification configuration, queries user preferences, and dispatches provider I/O.
# @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]
# @PRE Service receives a live DB session and configuration manager with notification payload settings.
# @POST Service can resolve targets and dispatch provider sends without mutating validation records.
# @SIDE_EFFECT Reads notification configuration, queries user preferences, and dispatches provider I/O.
# @DATA_CONTRACT Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]
class NotificationService:
# region NotificationService_init [TYPE Function]
# @PURPOSE: Bind DB and configuration collaborators used for provider initialization and routing.
# @RELATION: [BINDS_TO] ->[NotificationService]
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
# @RELATION BINDS_TO ->[NotificationService]
# @RELATION DEPENDS_ON ->[ValidationPolicy]
def __init__(self, db: Session, config_manager: ConfigManager):
self.db = db
self.config_manager = config_manager
@@ -56,9 +56,9 @@ class NotificationService:
# region _initialize_providers [TYPE Function]
# @PURPOSE: Materialize configured notification channel adapters once per service lifetime.
# @RELATION: [DEPENDS_ON] ->[SMTPProvider]
# @RELATION: [DEPENDS_ON] ->[TelegramProvider]
# @RELATION: [DEPENDS_ON] ->[SlackProvider]
# @RELATION DEPENDS_ON ->[SMTPProvider]
# @RELATION DEPENDS_ON ->[TelegramProvider]
# @RELATION DEPENDS_ON ->[SlackProvider]
def _initialize_providers(self):
if self._initialized:
return
@@ -81,14 +81,14 @@ class NotificationService:
# region dispatch_report [TYPE Function]
# @PURPOSE: Route one validation record to resolved owners and configured custom channels.
# @RELATION: [CALLS] ->[_initialize_providers]
# @RELATION: [CALLS] ->[_should_notify]
# @RELATION: [CALLS] ->[_resolve_targets]
# @RELATION: [CALLS] ->[_build_body]
# @PRE: record is persisted and providers can be initialized from configuration payload.
# @POST: Eligible notification sends are scheduled in background or awaited inline.
# @SIDE_EFFECT: Schedules or performs outbound provider sends and emits notification logs.
# @DATA_CONTRACT: Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]
# @RELATION CALLS ->[EXT:method:_initialize_providers]
# @RELATION CALLS ->[EXT:method:_should_notify]
# @RELATION CALLS ->[EXT:method:_resolve_targets]
# @RELATION CALLS ->[EXT:method:_build_body]
# @PRE record is persisted and providers can be initialized from configuration payload.
# @POST Eligible notification sends are scheduled in background or awaited inline.
# @SIDE_EFFECT Schedules or performs outbound provider sends and emits notification logs.
# @DATA_CONTRACT Input[ValidationRecord, Optional[ValidationPolicy], Optional[BackgroundTasks]] -> Output[None]
async def dispatch_report(
self,
record: ValidationRecord,
@@ -140,8 +140,8 @@ class NotificationService:
# region _should_notify [TYPE Function]
# @PURPOSE: Evaluate record status against effective alert policy.
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
# @RELATION DEPENDS_ON ->[ValidationRecord]
# @RELATION DEPENDS_ON ->[ValidationPolicy]
def _should_notify(
self, record: ValidationRecord, policy: ValidationPolicy | None
) -> bool:
@@ -157,9 +157,9 @@ class NotificationService:
# region _resolve_targets [TYPE Function]
# @PURPOSE: Resolve owner and policy-defined delivery targets for one validation record.
# @RELATION: [CALLS] ->[_find_dashboard_owners]
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
# @RELATION: [DEPENDS_ON] ->[ValidationPolicy]
# @RELATION CALLS ->[EXT:method:_find_dashboard_owners]
# @RELATION DEPENDS_ON ->[ValidationRecord]
# @RELATION DEPENDS_ON ->[ValidationPolicy]
def _resolve_targets(
self, record: ValidationRecord, policy: ValidationPolicy | None
) -> list[tuple]:
@@ -193,8 +193,8 @@ class NotificationService:
# region _find_dashboard_owners [TYPE Function]
# @PURPOSE: Load candidate dashboard owners from persisted profile preferences.
# @RELATION: [DEPENDS_ON] ->[ValidationRecord]
# @RELATION: [DEPENDS_ON] ->[UserDashboardPreference]
# @RELATION DEPENDS_ON ->[ValidationRecord]
# @RELATION DEPENDS_ON ->[UserDashboardPreference]
def _find_dashboard_owners(
self, record: ValidationRecord
) -> list[UserDashboardPreference]:
@@ -214,7 +214,7 @@ class NotificationService:
# region _build_body [TYPE Function]
# @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:
return (
f"Dashboard ID: {record.dashboard_id}\n"

View File

@@ -4,7 +4,7 @@
# @LAYER Domain
# @RELATION DEPENDS_ON -> [AuthRepository]
# @RELATION DEPENDS_ON -> [UserDashboardPreference]
# @RELATION DEPENDS_ON -> [profile_utils]
# @RELATION DEPENDS_ON -> [profile[EXT:internal:_utils]]
# @RELATION DEPENDS_ON -> [EncryptionManager]
# @RATIONALE Extracted from ProfileService to satisfy INV_7. Preference CRUD is the core profile
# operation with DB persistence, token encryption, and cross-field validation — a
@@ -31,7 +31,7 @@ from ..schemas.profile import (
ProfilePreferenceUpdateRequest,
)
from .llm_provider import EncryptionManager
from .profile_utils import (
from .profile[EXT:internal:_utils] import (
ProfileAuthorizationError,
ProfileValidationError,
build_default_preference,
@@ -336,7 +336,7 @@ class ProfilePreferenceService:
# #endregion _to_preference_payload
# #region _build_default_preference [C:1] [TYPE Function]
# @BRIEF Delegate to profile_utils.build_default_preference.
# @BRIEF Delegate to profile[EXT:internal:_utils].build_default_preference.
def _build_default_preference(self, user_id: str) -> ProfilePreference:
return build_default_preference(user_id)
# #endregion _build_default_preference

View File

@@ -2,33 +2,33 @@
#
# @BRIEF Composite facade orchestrating profile preference persistence, Superset account lookup,
# security badges, and deterministic actor matching by delegating to focused sub-services.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [UserDashboardPreference]
# @RELATION DEPENDS_ON -> [ProfilePreferenceResponse]
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [AuthRepositoryModule]
# @RELATION DEPENDS_ON -> [User]
# @RELATION DEPENDS_ON -> [sqlalchemy.orm.Session]
# @RELATION DEPENDS_ON -> [EXT:Library:sqlalchemy.orm.Session]
# @RELATION DEPENDS_ON -> [ProfilePreferenceService]
# @RELATION DEPENDS_ON -> [SupersetLookupService]
# @RELATION DEPENDS_ON -> [SecurityBadgeService]
# @RELATION DEPENDS_ON -> [profile_utils]
# @RELATION DEPENDS_ON -> [profile[EXT:internal:_utils]]
#
# @INVARIANT: Profile ID needs to be unique per-user session
# @INVARIANT Profile ID needs to be unique per-user session
#
# @TEST_CONTRACT: ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse
# @TEST_FIXTURE: valid_profile_update -> {"user_id":"u-1","superset_username":"John_Doe","show_only_my_dashboards":true}
# @TEST_EDGE: enable_without_username -> toggle=true with empty username returns validation error
# @TEST_EDGE: cross_user_mutation -> attempt to update another user preference returns forbidden
# @TEST_EDGE: lookup_env_not_found -> unknown environment_id returns not found
# @TEST_INVARIANT: normalization_consistency -> VERIFIED_BY: [valid_profile_update, enable_without_username]
# @DATA_CONTRACT: Profile_id -> ProfileInfo; session_id -> valid UUID
# @PRE: Session is active and valid
# @POST: Profile with updated fields populated and
# @SIDE_EFFECT: Database read/write operations
# @TEST_CONTRACT ProfilePreferenceUpdateRequest -> ProfilePreferenceResponse
# @TEST_FIXTURE valid_profile_update -> {"user_id":"u-1","superset_username":"John_Doe","show_only_my_dashboards":true}
# @TEST_EDGE enable_without_username -> toggle=true with empty username returns validation error
# @TEST_EDGE cross_user_mutation -> attempt to update another user preference returns forbidden
# @TEST_EDGE lookup_env_not_found -> unknown environment_id returns not found
# @TEST_INVARIANT normalization_consistency -> VERIFIED_BY: [valid_profile_update, enable_without_username]
# @DATA_CONTRACT Profile_id -> ProfileInfo; session_id -> valid UUID
# @PRE Session is active and valid
# @POST Profile with updated fields populated and
# @SIDE_EFFECT Database read/write operations
# @RATIONALE Decomposed from monolithic 770-line ProfileService into three focused services
# (ProfilePreferenceService, SupersetLookupService, SecurityBadgeService) plus pure
# utility functions (profile_utils) to satisfy INV_7. This module is a thin facade
# utility functions (profile[EXT:internal:_utils]) to satisfy INV_7. This module is a thin facade
# that preserves the public API contract for all existing callers.
# @REJECTED Keeping all three domains in a single ProfileService class was rejected — it violated
# INV_7 (770 lines vs 150 max), mixed I/O patterns (DB writes + HTTP calls), and created
@@ -48,7 +48,7 @@ from ..schemas.profile import (
SupersetAccountLookupResponse,
)
from .profile_preference_service import ProfilePreferenceService
from .profile_utils import (
from .profile[EXT:internal:_utils] import (
EnvironmentNotFoundError,
ProfileAuthorizationError,
ProfileValidationError,
@@ -77,12 +77,12 @@ __all__ = [
# @RELATION DEPENDS_ON -> [ProfilePreferenceService]
# @RELATION DEPENDS_ON -> [SupersetLookupService]
# @RELATION DEPENDS_ON -> [SecurityBadgeService]
# @RELATION DEPENDS_ON -> [profile_utils]
# @PRE: Caller provides authenticated User context for external service methods.
# @POST: Delegates to sub-services and returns normalized profile/lookup responses.
# @SIDE_EFFECT: Writes preference records and encrypted tokens; performs external account lookups when requested.
# @DATA_CONTRACT: Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool]
# @INVARIANT: Profile data integrity maintained, cache consistency with database state
# @RELATION DEPENDS_ON -> [profile[EXT:internal:_utils]]
# @PRE Caller provides authenticated User context for external service methods.
# @POST Delegates to sub-services and returns normalized profile/lookup responses.
# @SIDE_EFFECT Writes preference records and encrypted tokens; performs external account lookups when requested.
# @DATA_CONTRACT Input[User,ProfilePreferenceUpdateRequest|SupersetAccountLookupRequest] -> Output[ProfilePreferenceResponse|SupersetAccountLookupResponse|bool]
# @INVARIANT Profile data integrity maintained, cache consistency with database state
# @RATIONALE Thin coordinator — all sub-10-line delegating methods. Actual business logic lives
# in the injected sub-services.
class ProfileService:
@@ -90,8 +90,8 @@ class ProfileService:
# region init [TYPE Function]
# @BRIEF Initialize facade and create sub-services.
# @PRE: db session is active and config_manager supports get_environments().
# @POST: All sub-services are initialized and ready.
# @PRE db session is active and config_manager supports get_environments().
# @POST All sub-services are initialized and ready.
def __init__(self, db: Session, config_manager: Any, plugin_loader: Any = None):
self.preference_service = ProfilePreferenceService(db, config_manager, plugin_loader)
self.lookup_service = SupersetLookupService(config_manager)
@@ -143,8 +143,8 @@ class ProfileService:
# region matches_dashboard_actor [TYPE Function]
# @BRIEF Apply trim+case-insensitive actor match across owners OR modified_by.
# @PRE: bound_username can be empty; owners may contain mixed payload.
# @POST: Returns True when normalized username matches owners or modified_by.
# @PRE bound_username can be empty; owners may contain mixed payload.
# @POST Returns True when normalized username matches owners or modified_by.
def matches_dashboard_actor(
self,
bound_username: str | None,

View File

@@ -1,4 +1,4 @@
# #region profile_utils [C:2] [TYPE Module] [SEMANTICS string,normalization,utility,sanitize]
# #region profile[EXT:internal:_utils] [C:2] [TYPE Module] [SEMANTICS string,normalization,utility,sanitize]
# @BRIEF Pure utility helpers for profile data sanitization, normalization, and secret masking.
# Also contains shared exception classes to avoid circular imports between profile sub-modules.
# @LAYER Domain
@@ -15,7 +15,7 @@ from typing import Any
# #region ProfileValidationError [C:2] [TYPE Class]
# @RELATION INHERITS -> Exception
# @RELATION INHERITS -> [EXT:Python:Exception]
# @BRIEF Domain validation error for profile preference update requests.
class ProfileValidationError(Exception):
def __init__(self, errors: Sequence[str]):
@@ -25,7 +25,7 @@ class ProfileValidationError(Exception):
# #region EnvironmentNotFoundError [C:2] [TYPE Class]
# @RELATION INHERITS -> Exception
# @RELATION INHERITS -> [EXT:Python:Exception]
# @BRIEF Raised when environment_id from lookup request is unknown in app configuration.
class EnvironmentNotFoundError(Exception):
pass
@@ -33,7 +33,7 @@ class EnvironmentNotFoundError(Exception):
# #region ProfileAuthorizationError [C:2] [TYPE Class]
# @RELATION INHERITS -> Exception
# @RELATION INHERITS -> [EXT:Python:Exception]
# @BRIEF Raised when caller attempts cross-user preference mutation.
class ProfileAuthorizationError(Exception):
pass
@@ -122,7 +122,8 @@ def mask_secret_value(secret: str | None) -> str | None:
# @BRIEF Validate username/toggle constraints for preference mutation.
# @RELATION CALLS -> [sanitize_username]
# @RELATION CALLS -> [sanitize_text]
# @RELATION DEPENDS_ON -> [SUPPORTED_DENSITIES, SUPPORTED_START_PAGES]
# @RELATION DEPENDS_ON -> [EXT:profile[EXT:internal:_utils]:SUPPORTED_DENSITIES]
# @RELATION DEPENDS_ON -> [EXT:profile[EXT:internal:_utils]:SUPPORTED_START_PAGES]
# @POST Returns validation errors list; empty list means valid.
def validate_update_payload(
superset_username: str | None,
@@ -241,4 +242,4 @@ def normalize_owner_tokens(owners: Iterable[Any] | None) -> list[str]:
normalized.append(token)
return normalized
# #endregion normalize_owner_tokens
# #endregion profile_utils
# #endregion profile[EXT:internal:_utils]

View File

@@ -27,8 +27,8 @@ ROUTES_DIR = Path(__file__).resolve().parent.parent / "api" / "routes"
# #region _iter_route_files [C:4] [TYPE Function]
# @BRIEF Iterates API route files that may contain RBAC declarations.
# @PRE: ROUTES_DIR points to backend/src/api/routes.
# @POST: Yields Python files excluding test and cache directories.
# @PRE ROUTES_DIR points to backend/src/api/routes.
# @POST Yields Python files excluding test and cache directories.
def _iter_route_files() -> Iterable[Path]:
with belief_scope("rbac_permission_catalog._iter_route_files"):
if not ROUTES_DIR.exists():
@@ -46,8 +46,8 @@ def _iter_route_files() -> Iterable[Path]:
# #region _discover_route_permissions [C:4] [TYPE Function]
# @BRIEF Extracts explicit has_permission declarations from API route source code.
# @PRE: Route files are readable UTF-8 text files.
# @POST: Returns unique set of (resource, action) pairs declared in route guards.
# @PRE Route files are readable UTF-8 text files.
# @POST Returns unique set of (resource, action) pairs declared in route guards.
def _discover_route_permissions() -> set[tuple[str, str]]:
with belief_scope("rbac_permission_catalog._discover_route_permissions"):
discovered: set[tuple[str, str]] = set()
@@ -72,8 +72,8 @@ def _discover_route_permissions() -> set[tuple[str, str]]:
# #region _discover_route_permissions_cached [C:4] [TYPE Function]
# @BRIEF Cache route permission discovery because route source files are static during normal runtime.
# @PRE: None.
# @POST: Returns stable discovered route permission pairs without repeated filesystem scans.
# @PRE None.
# @POST Returns stable discovered route permission pairs without repeated filesystem scans.
@lru_cache(maxsize=1)
def _discover_route_permissions_cached() -> tuple[tuple[str, str], ...]:
with belief_scope("rbac_permission_catalog._discover_route_permissions_cached"):
@@ -83,8 +83,8 @@ def _discover_route_permissions_cached() -> tuple[tuple[str, str], ...]:
# #region _discover_plugin_execute_permissions [C:4] [TYPE Function]
# @BRIEF Derives dynamic task permissions of form plugin:{plugin_id}:EXECUTE from plugin registry.
# @PRE: plugin_loader is optional and may expose get_all_plugin_configs.
# @POST: Returns unique plugin EXECUTE permissions if loader is available.
# @PRE plugin_loader is optional and may expose get_all_plugin_configs.
# @POST Returns unique plugin EXECUTE permissions if loader is available.
def _discover_plugin_execute_permissions(plugin_loader=None) -> set[tuple[str, str]]:
with belief_scope("rbac_permission_catalog._discover_plugin_execute_permissions"):
discovered: set[tuple[str, str]] = set()
@@ -110,8 +110,8 @@ def _discover_plugin_execute_permissions(plugin_loader=None) -> set[tuple[str, s
# #region _discover_plugin_execute_permissions_cached [C:4] [TYPE Function]
# @BRIEF Cache dynamic plugin EXECUTE permission pairs by normalized plugin id tuple.
# @PRE: plugin_ids is a deterministic tuple of plugin ids.
# @POST: Returns stable permission tuple without repeated plugin catalog expansion.
# @PRE plugin_ids is a deterministic tuple of plugin ids.
# @POST Returns stable permission tuple without repeated plugin catalog expansion.
@lru_cache(maxsize=8)
def _discover_plugin_execute_permissions_cached(
plugin_ids: tuple[str, ...],
@@ -123,8 +123,8 @@ def _discover_plugin_execute_permissions_cached(
# #region discover_declared_permissions [C:4] [TYPE Function]
# @BRIEF Builds canonical RBAC permission catalog from routes and plugin registry.
# @PRE: plugin_loader may be provided for dynamic task plugin permission discovery.
# @POST: Returns union of route-declared and dynamic plugin EXECUTE permissions.
# @PRE plugin_loader may be provided for dynamic task plugin permission discovery.
# @POST Returns union of route-declared and dynamic plugin EXECUTE permissions.
def discover_declared_permissions(plugin_loader=None) -> set[tuple[str, str]]:
with belief_scope("rbac_permission_catalog.discover_declared_permissions"):
permissions = set(_discover_route_permissions_cached())
@@ -144,10 +144,10 @@ def discover_declared_permissions(plugin_loader=None) -> set[tuple[str, str]]:
# #region sync_permission_catalog [C:4] [TYPE Function]
# @BRIEF Persists missing RBAC permission pairs into auth database.
# @PRE: db is a valid SQLAlchemy session bound to auth database.
# @PRE: declared_permissions is an iterable of (resource, action) tuples.
# @POST: Missing permissions are inserted; existing permissions remain untouched.
# @SIDE_EFFECT: Commits auth database transaction when new permissions are added.
# @PRE db is a valid SQLAlchemy session bound to auth database.
# @PRE declared_permissions is an iterable of (resource, action) tuples.
# @POST Missing permissions are inserted; existing permissions remain untouched.
# @SIDE_EFFECT Commits auth database transaction when new permissions are added.
def sync_permission_catalog(
db: Session,
declared_permissions: Iterable[tuple[str, str]],

View File

@@ -1,9 +1,9 @@
# region test_report_normalizer [TYPE Module]
# @SEMANTICS: tests, reports, normalizer, fallback
# @PURPOSE: Validate unknown task type fallback and partial payload normalization behavior.
# @RELATION: TESTS ->[normalize_report:Function]
# @LAYER: Domain
# @INVARIANT: Unknown plugin types are mapped to canonical unknown task type.
# @RELATION BINDS_TO ->[EXT:frontend:normalize_report]
# @LAYER Domain
# @INVARIANT Unknown plugin types are mapped to canonical unknown task type.
from datetime import datetime
@@ -12,7 +12,7 @@ from src.services.reports.normalizer import normalize_task_report
# region test_unknown_type_maps_to_unknown_profile [TYPE Function]
# @RELATION: BINDS_TO -> test_report_normalizer
# @RELATION BINDS_TO -> test_report_normalizer
# @PURPOSE: Ensure unknown plugin IDs map to unknown profile with populated summary and error context.
def test_unknown_type_maps_to_unknown_profile():
task = Task(
@@ -36,7 +36,7 @@ def test_unknown_type_maps_to_unknown_profile():
# region test_partial_payload_keeps_report_visible_with_placeholders [TYPE Function]
# @RELATION: BINDS_TO -> test_report_normalizer
# @RELATION BINDS_TO -> test_report_normalizer
# @PURPOSE: Ensure missing result payload still yields visible report details with result placeholder.
def test_partial_payload_keeps_report_visible_with_placeholders():
task = Task(
@@ -60,7 +60,7 @@ def test_partial_payload_keeps_report_visible_with_placeholders():
# region test_clean_release_plugin_maps_to_clean_release_task_type [TYPE Function]
# @RELATION: BINDS_TO -> test_report_normalizer
# @RELATION BINDS_TO -> test_report_normalizer
# @PURPOSE: Ensure clean-release plugin ID maps to clean_release task profile and summary passthrough.
def test_clean_release_plugin_maps_to_clean_release_task_type():
task = Task(

View File

@@ -1,7 +1,7 @@
# region test_report_service [TYPE Module]
# @PURPOSE: Unit tests for ReportsService list/detail operations
# @RELATION: TESTS ->[ReportsService:Class]
# @LAYER: Domain
# @RELATION BINDS_TO ->[ReportsService]
# @LAYER Domain
from pathlib import Path
import sys
@@ -13,7 +13,7 @@ from unittest.mock import MagicMock
# region _make_task [TYPE Function]
# @RELATION: BINDS_TO -> test_report_service
# @RELATION BINDS_TO -> test_report_service
def _make_task(task_id="task-1", plugin_id="superset-backup", status_value="SUCCESS",
started_at=None, finished_at=None, result=None, params=None, logs=None):
"""Create a mock Task object matching the Task model interface."""

View File

@@ -1,5 +1,5 @@
# region __tests__/test_report_type_profiles [TYPE Module]
# @RELATION: VERIFIES -> ../type_profiles.py
# @RELATION BINDS_TO -> ../type_profiles.py
# @PURPOSE: Contract testing for task type profiles and resolution logic.
# endregion __tests__/test_report_type_profiles
@@ -7,10 +7,10 @@ from src.models.report import TaskType
from src.services.reports.type_profiles import get_type_profile, resolve_task_type
# @TEST_CONTRACT: ResolveTaskType -> Invariants
# @TEST_INVARIANT: fallback_to_unknown
# @TEST_CONTRACT ResolveTaskType -> Invariants
# @TEST_INVARIANT fallback_to_unknown
# region test_resolve_task_type_fallbacks [TYPE Function]
# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles
# @RELATION BINDS_TO -> __tests__/test_report_type_profiles
# @PURPOSE: Verify resolve_task_type_fallbacks returns correct fallback type when primary is missing.
def test_resolve_task_type_fallbacks():
"""Verify missing/unmapped plugin_id returns TaskType.UNKNOWN."""
@@ -19,11 +19,11 @@ def test_resolve_task_type_fallbacks():
assert resolve_task_type(" ") == TaskType.UNKNOWN
assert resolve_task_type("invalid_plugin") == TaskType.UNKNOWN
# @TEST_FIXTURE: valid_plugin
# @TEST_FIXTURE valid_plugin
# endregion test_resolve_task_type_fallbacks
# region test_resolve_task_type_valid [TYPE Function]
# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles
# @RELATION BINDS_TO -> __tests__/test_report_type_profiles
# @PURPOSE: Verify resolve_task_type_valid returns the correct type when valid input is provided.
def test_resolve_task_type_valid():
"""Verify known plugin IDs map correctly."""
@@ -32,11 +32,11 @@ def test_resolve_task_type_valid():
assert resolve_task_type("superset-backup") == TaskType.BACKUP
assert resolve_task_type("documentation") == TaskType.DOCUMENTATION
# @TEST_FIXTURE: valid_profile
# @TEST_FIXTURE valid_profile
# endregion test_resolve_task_type_valid
# region test_get_type_profile_valid [TYPE Function]
# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles
# @RELATION BINDS_TO -> __tests__/test_report_type_profiles
# @PURPOSE: Verify get_type_profile_valid returns the correct profile for a valid task type.
def test_get_type_profile_valid():
"""Verify known task types return correct profile metadata."""
@@ -45,12 +45,12 @@ def test_get_type_profile_valid():
assert profile["visual_variant"] == "migration"
assert profile["fallback"] is False
# @TEST_INVARIANT: always_returns_dict
# @TEST_EDGE: missing_profile
# @TEST_INVARIANT always_returns_dict
# @TEST_EDGE missing_profile
# endregion test_get_type_profile_valid
# region test_get_type_profile_fallback [TYPE Function]
# @RELATION: BINDS_TO -> __tests__/test_report_type_profiles
# @RELATION BINDS_TO -> __tests__/test_report_type_profiles
# @PURPOSE: Verify get_type_profile_fallback returns default profile when type is unknown.
def test_get_type_profile_fallback():
"""Verify unknown task type returns fallback profile."""

View File

@@ -1,14 +1,14 @@
# #region normalizer [C:5] [TYPE Module] [SEMANTICS pydantic, report, task, normalize, status]
# @BRIEF Convert task manager task objects into canonical unified TaskReport entities with deterministic fallback behavior.
# @LAYER: Domain
# @RELATION DEPENDS_ON -> [backend.src.core.task_manager.models.Task:Function]
# @RELATION DEPENDS_ON -> [backend.src.models.report:Function]
# @RELATION DEPENDS_ON -> [backend.src.services.reports.type_profiles:Function]
# @INVARIANT: Normalizer instance maintains consistent field order
# @DATA_CONTRACT: ReportRow -> NormalizerInput; session_id -> valid UUID
# @PRE: session is active and valid
# @POST: Returns Normalizer output with normalized fields
# @SIDE_EFFECT: Read-only database operations
# @LAYER Domain
# @RELATION DEPENDS_ON -> [EXT:frontend:TaskModel]
# @RELATION DEPENDS_ON -> [EXT:frontend:ReportModel]
# @RELATION DEPENDS_ON -> [EXT:frontend:TypeProfiles]
# @INVARIANT Normalizer instance maintains consistent field order
# @DATA_CONTRACT ReportRow -> NormalizerInput; session_id -> valid UUID
# @PRE session is active and valid
# @POST Returns Normalizer output with normalized fields
# @SIDE_EFFECT Read-only database operations
from datetime import datetime
from typing import Any
@@ -21,8 +21,8 @@ from .type_profiles import get_type_profile, resolve_task_type
# #region status_to_report_status [TYPE Function]
# @BRIEF Normalize internal task status to canonical report status.
# @PRE: status may be known or unknown string/enum value.
# @POST: Always returns one of canonical ReportStatus values.
# @PRE status may be known or unknown string/enum value.
# @POST Always returns one of canonical ReportStatus values.
def status_to_report_status(status: Any) -> ReportStatus:
with belief_scope("status_to_report_status"):
raw = str(status.value if isinstance(status, TaskStatus) else status).upper()
@@ -38,8 +38,8 @@ def status_to_report_status(status: Any) -> ReportStatus:
# #region build_summary [TYPE Function]
# @BRIEF Build deterministic user-facing summary from task payload and status.
# @PRE: report_status is canonical; plugin_id may be unknown.
# @POST: Returns non-empty summary text.
# @PRE report_status is canonical; plugin_id may be unknown.
# @POST Returns non-empty summary text.
def build_summary(task: Task, report_status: ReportStatus) -> str:
with belief_scope("build_summary"):
result = task.result
@@ -60,8 +60,8 @@ def build_summary(task: Task, report_status: ReportStatus) -> str:
# #region extract_error_context [TYPE Function]
# @BRIEF Extract normalized error context and next actions for failed/partial reports.
# @PRE: task is a valid Task object.
# @POST: Returns ErrorContext for failed/partial when context exists; otherwise None.
# @PRE task is a valid Task object.
# @POST Returns ErrorContext for failed/partial when context exists; otherwise None.
def extract_error_context(task: Task, report_status: ReportStatus) -> ErrorContext | None:
with belief_scope("extract_error_context"):
if report_status not in {ReportStatus.FAILED, ReportStatus.PARTIAL}:
@@ -101,10 +101,10 @@ def extract_error_context(task: Task, report_status: ReportStatus) -> ErrorConte
# #region normalize_task_report [TYPE Function]
# @BRIEF Convert one Task to canonical TaskReport envelope.
# @PRE: task has valid id and plugin_id fields.
# @POST: Returns TaskReport with required fields and deterministic fallback behavior.
# @PRE task has valid id and plugin_id fields.
# @POST Returns TaskReport with required fields and deterministic fallback behavior.
#
# @TEST_CONTRACT: NormalizeTaskReport ->
# @TEST_CONTRACT NormalizeTaskReport ->
# {
# required_fields: {task: Task},
# invariants: [
@@ -113,10 +113,10 @@ def extract_error_context(task: Task, report_status: ReportStatus) -> ErrorConte
# "Extracts ErrorContext for FAILED/PARTIAL tasks"
# ]
# }
# @TEST_FIXTURE: valid_task -> {"task": "MockTask(id='1', plugin_id='superset-migration', status=TaskStatus.SUCCESS)"}
# @TEST_EDGE: task_with_error -> {"task": "MockTask(status=TaskStatus.FAILED, logs=[LogEntry(level='ERROR', message='Failed')])"}
# @TEST_EDGE: unknown_plugin_type -> {"task": "MockTask(plugin_id='unknown-plugin', status=TaskStatus.PENDING)"}
# @TEST_INVARIANT: deterministic_normalization -> verifies: [valid_task, task_with_error, unknown_plugin_type]
# @TEST_FIXTURE valid_task -> {"task": "MockTask(id='1', plugin_id='superset-migration', status=TaskStatus.SUCCESS)"}
# @TEST_EDGE task_with_error -> {"task": "MockTask(status=TaskStatus.FAILED, logs=[LogEntry(level='ERROR', message='Failed')])"}
# @TEST_EDGE unknown_plugin_type -> {"task": "MockTask(plugin_id='unknown-plugin', status=TaskStatus.PENDING)"}
# @TEST_INVARIANT deterministic_normalization -> verifies: [valid_task, task_with_error, unknown_plugin_type]
def normalize_task_report(task: Task) -> TaskReport:
with belief_scope("normalize_task_report"):
task_type = resolve_task_type(task.plugin_id)

View File

@@ -1,6 +1,6 @@
# #region report_service [C:5] [TYPE Module] [SEMANTICS report, task, filter, paginate, aggregate]
# @BRIEF Aggregate, normalize, filter, and paginate task reports for unified list/detail API use cases.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DEPENDS_ON -> [TaskReport]
# @RELATION DEPENDS_ON -> [ReportQuery]
@@ -8,11 +8,11 @@
# @RELATION DEPENDS_ON -> [ReportDetailView]
# @RELATION DEPENDS_ON -> [normalize_task_report]
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
# @INVARIANT: ReportService maintains consistent report structure
# @DATA_CONTRACT: ReportQuery -> ReportRow; session_id -> valid UUID
# @PRE: session is active and valid
# @POST: Returns Report with generated summary
# @SIDE_EFFECT: Read-only database operations; logs report generation
# @INVARIANT ReportService maintains consistent report structure
# @DATA_CONTRACT ReportQuery -> ReportRow; session_id -> valid UUID
# @PRE session is active and valid
# @POST Returns Report with generated summary
# @SIDE_EFFECT Read-only database operations; logs report generation
from datetime import UTC, datetime
@@ -32,16 +32,16 @@ from .normalizer import normalize_task_report
# #region ReportsService [C:5] [TYPE Class]
# @BRIEF Service layer for list/detail report retrieval and normalization.
# @PRE: TaskManager dependency is initialized.
# @POST: Provides deterministic list/detail report responses.
# @PRE TaskManager dependency is initialized.
# @POST Provides deterministic list/detail report responses.
# @RELATION DEPENDS_ON -> [TaskManager]
# @RELATION DEPENDS_ON -> [CleanReleaseRepository]
# @RELATION CALLS -> [normalize_task_report]
# @SIDE_EFFECT: Reads task history and optional clean-release repository state without mutating source records.
# @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]
# @INVARIANT: Service methods are read-only over task history source.
# @SIDE_EFFECT Reads task history and optional clean-release repository state without mutating source records.
# @DATA_CONTRACT Input[TaskManager, Optional[CleanReleaseRepository], ReportQuery|report_id] -> Output[ReportCollection|ReportDetailView|None]
# @INVARIANT Service methods are read-only over task history source.
#
# @TEST_CONTRACT: ReportsServiceModel ->
# @TEST_CONTRACT ReportsServiceModel ->
# {
# required_fields: {task_manager: TaskManager},
# invariants: [
@@ -49,22 +49,22 @@ from .normalizer import normalize_task_report
# "get_report_detail returns a valid ReportDetailView or None"
# ]
# }
# @TEST_FIXTURE: valid_service -> {"task_manager": "MockTaskManager()"}
# @TEST_EDGE: empty_task_list -> returns empty ReportCollection
# @TEST_EDGE: report_not_found -> get_report_detail returns None
# @TEST_INVARIANT: consistent_pagination -> verifies: [valid_service]
# @TEST_FIXTURE valid_service -> {"task_manager": "MockTaskManager()"}
# @TEST_EDGE empty_task_list -> returns empty ReportCollection
# @TEST_EDGE report_not_found -> get_report_detail returns None
# @TEST_INVARIANT consistent_pagination -> verifies: [valid_service]
class ReportsService:
# region init [TYPE Function]
# @PURPOSE: Initialize service with TaskManager dependency.
# @PRE: task_manager is a live TaskManager instance.
# @POST: self.task_manager is assigned and ready for read operations.
# @INVARIANT: Constructor performs no task mutations.
# @RELATION: [BINDS_TO] ->[ReportsService]
# @RELATION: [DEPENDS_ON] ->[TaskManager]
# @RELATION: [DEPENDS_ON] ->[CleanReleaseRepository]
# @SIDE_EFFECT: Stores collaborator references for later read-only report projections.
# @DATA_CONTRACT: Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService]
# @PARAM: task_manager (TaskManager) - Task manager providing source task history.
# @PRE task_manager is a live TaskManager instance.
# @POST self.task_manager is assigned and ready for read operations.
# @INVARIANT Constructor performs no task mutations.
# @RELATION BINDS_TO ->[ReportsService]
# @RELATION DEPENDS_ON ->[TaskManager]
# @RELATION DEPENDS_ON ->[CleanReleaseRepository]
# @SIDE_EFFECT Stores collaborator references for later read-only report projections.
# @DATA_CONTRACT Input[TaskManager, Optional[CleanReleaseRepository]] -> Output[ReportsService]
# @PARAM task_manager (TaskManager) - Task manager providing source task history.
def __init__(
self,
task_manager: TaskManager,
@@ -78,10 +78,10 @@ class ReportsService:
# region _load_normalized_reports [TYPE Function]
# @PURPOSE: Build normalized reports from all available tasks.
# @PRE: Task manager returns iterable task history records.
# @POST: Returns normalized report list preserving source cardinality.
# @INVARIANT: Every returned item is a TaskReport.
# @RETURN: List[TaskReport] - Reports sorted later by list logic.
# @PRE Task manager returns iterable task history records.
# @POST Returns normalized report list preserving source cardinality.
# @INVARIANT Every returned item is a TaskReport.
# @RETURN List[TaskReport] - Reports sorted later by list logic.
def _load_normalized_reports(self) -> list[TaskReport]:
with belief_scope("_load_normalized_reports"):
tasks = self.task_manager.get_all_tasks()
@@ -92,11 +92,11 @@ class ReportsService:
# region _to_utc_datetime [TYPE Function]
# @PURPOSE: Normalize naive/aware datetime values to UTC-aware datetime for safe comparisons.
# @PRE: value is either datetime or None.
# @POST: Returns UTC-aware datetime or None.
# @INVARIANT: Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.
# @PARAM: value (Optional[datetime]) - Source datetime value.
# @RETURN: Optional[datetime] - UTC-aware datetime or None.
# @PRE value is either datetime or None.
# @POST Returns UTC-aware datetime or None.
# @INVARIANT Naive datetimes are interpreted as UTC to preserve deterministic ordering/filtering.
# @PARAM value (Optional[EXT:Python:datetime]) - Source datetime value.
# @RETURN Optional[EXT:Python:datetime] - UTC-aware datetime or None.
def _to_utc_datetime(self, value: datetime | None) -> datetime | None:
with belief_scope("_to_utc_datetime"):
if value is None:
@@ -109,11 +109,11 @@ class ReportsService:
# region _datetime_sort_key [TYPE Function]
# @PURPOSE: Produce stable numeric sort key for report timestamps.
# @PRE: report contains updated_at datetime.
# @POST: Returns float timestamp suitable for deterministic sorting.
# @INVARIANT: Mixed naive/aware datetimes never raise TypeError.
# @PARAM: report (TaskReport) - Report item.
# @RETURN: float - UTC timestamp key.
# @PRE report contains updated_at datetime.
# @POST Returns float timestamp suitable for deterministic sorting.
# @INVARIANT Mixed naive/aware datetimes never raise TypeError.
# @PARAM report (TaskReport) - Report item.
# @RETURN float - UTC timestamp key.
def _datetime_sort_key(self, report: TaskReport) -> float:
with belief_scope("_datetime_sort_key"):
updated = self._to_utc_datetime(report.updated_at)
@@ -125,12 +125,12 @@ class ReportsService:
# region _matches_query [TYPE Function]
# @PURPOSE: Apply query filtering to a report.
# @PRE: report and query are normalized schema instances.
# @POST: Returns True iff report satisfies all active query filters.
# @INVARIANT: Filter evaluation is side-effect free.
# @PARAM: report (TaskReport) - Candidate report.
# @PARAM: query (ReportQuery) - Applied query.
# @RETURN: bool - True if report matches all filters.
# @PRE report and query are normalized schema instances.
# @POST Returns True iff report satisfies all active query filters.
# @INVARIANT Filter evaluation is side-effect free.
# @PARAM report (TaskReport) - Candidate report.
# @PARAM query (ReportQuery) - Applied query.
# @RETURN bool - True if report matches all filters.
def _matches_query(self, report: TaskReport, query: ReportQuery) -> bool:
with belief_scope("_matches_query"):
if query.task_types and report.task_type not in query.task_types:
@@ -164,12 +164,12 @@ class ReportsService:
# region _sort_reports [TYPE Function]
# @PURPOSE: Sort reports deterministically according to query settings.
# @PRE: reports contains only TaskReport items.
# @POST: Returns reports ordered by selected sort field and order.
# @INVARIANT: Sorting criteria are deterministic for equal input.
# @PARAM: reports (List[TaskReport]) - Filtered reports.
# @PARAM: query (ReportQuery) - Sort config.
# @RETURN: List[TaskReport] - Sorted reports.
# @PRE reports contains only TaskReport items.
# @POST Returns reports ordered by selected sort field and order.
# @INVARIANT Sorting criteria are deterministic for equal input.
# @PARAM reports (List[TaskReport]) - Filtered reports.
# @PARAM query (ReportQuery) - Sort config.
# @RETURN List[TaskReport] - Sorted reports.
def _sort_reports(
self, reports: list[TaskReport], query: ReportQuery
) -> list[TaskReport]:
@@ -189,10 +189,10 @@ class ReportsService:
# region list_reports [TYPE Function]
# @PURPOSE: Return filtered, sorted, paginated report collection.
# @PRE: query has passed schema validation.
# @POST: Returns {items,total,page,page_size,has_next,applied_filters}.
# @PARAM: query (ReportQuery) - List filters and pagination.
# @RETURN: ReportCollection - Paginated unified reports payload.
# @PRE query has passed schema validation.
# @POST Returns {items,total,page,page_size,has_next,applied_filters}.
# @PARAM query (ReportQuery) - List filters and pagination.
# @RETURN ReportCollection - Paginated unified reports payload.
def list_reports(self, query: ReportQuery) -> ReportCollection:
with belief_scope("list_reports"):
reports = self._load_normalized_reports()
@@ -220,10 +220,10 @@ class ReportsService:
# region get_report_detail [TYPE Function]
# @PURPOSE: Return one normalized report with timeline/diagnostics/next actions.
# @PRE: report_id exists in normalized report set.
# @POST: Returns normalized detail envelope with diagnostics and next actions where applicable.
# @PARAM: report_id (str) - Stable report identifier.
# @RETURN: Optional[ReportDetailView] - Detailed report or None if not found.
# @PRE report_id exists in normalized report set.
# @POST Returns normalized detail envelope with diagnostics and next actions where applicable.
# @PARAM report_id (str) - Stable report identifier.
# @RETURN Optional[ReportDetailView] - Detailed report or None if not found.
def get_report_detail(self, report_id: str) -> ReportDetailView | None:
with belief_scope("get_report_detail"):
reports = self._load_normalized_reports()

View File

@@ -69,19 +69,19 @@ TASK_TYPE_PROFILES: dict[TaskType, dict[str, Any]] = {
# #region resolve_task_type [TYPE Function]
# @BRIEF Resolve canonical task type from plugin/task identifier with guaranteed fallback.
# @PRE: plugin_id may be None or unknown.
# @POST: Always returns one of TaskType enum values.
# @PRE plugin_id may be None or unknown.
# @POST Always returns one of TaskType enum values.
#
# @TEST_CONTRACT: ResolveTaskType ->
# @TEST_CONTRACT ResolveTaskType ->
# {
# required_fields: {plugin_id: str},
# invariants: ["returns TaskType.UNKNOWN for missing/unmapped plugin_id"]
# }
# @TEST_FIXTURE: valid_plugin -> {"plugin_id": "superset-migration"}
# @TEST_EDGE: empty_plugin -> {"plugin_id": ""}
# @TEST_EDGE: none_plugin -> {"plugin_id": None}
# @TEST_EDGE: unknown_plugin -> {"plugin_id": "invalid-plugin"}
# @TEST_INVARIANT: fallback_to_unknown -> verifies: [empty_plugin, none_plugin, unknown_plugin]
# @TEST_FIXTURE valid_plugin -> {"plugin_id": "superset-migration"}
# @TEST_EDGE empty_plugin -> {"plugin_id": ""}
# @TEST_EDGE none_plugin -> {"plugin_id": None}
# @TEST_EDGE unknown_plugin -> {"plugin_id": "invalid-plugin"}
# @TEST_INVARIANT fallback_to_unknown -> verifies: [empty_plugin, none_plugin, unknown_plugin]
def resolve_task_type(plugin_id: str | None) -> TaskType:
with belief_scope("resolve_task_type"):
normalized = (plugin_id or "").strip()
@@ -95,17 +95,17 @@ def resolve_task_type(plugin_id: str | None) -> TaskType:
# #region get_type_profile [TYPE Function]
# @BRIEF Return deterministic profile metadata for a task type.
# @PRE: task_type may be known or unknown.
# @POST: Returns a profile dict and never raises for unknown types.
# @PRE task_type may be known or unknown.
# @POST Returns a profile dict and never raises for unknown types.
#
# @TEST_CONTRACT: GetTypeProfile ->
# @TEST_CONTRACT GetTypeProfile ->
# {
# required_fields: {task_type: TaskType},
# invariants: ["returns a valid metadata dictionary even for UNKNOWN"]
# }
# @TEST_FIXTURE: valid_profile -> {"task_type": "migration"}
# @TEST_EDGE: missing_profile -> {"task_type": "some_new_type"}
# @TEST_INVARIANT: always_returns_dict -> verifies: [valid_profile, missing_profile]
# @TEST_FIXTURE valid_profile -> {"task_type": "migration"}
# @TEST_EDGE missing_profile -> {"task_type": "some_new_type"}
# @TEST_INVARIANT always_returns_dict -> verifies: [valid_profile, missing_profile]
def get_type_profile(task_type: TaskType) -> dict[str, Any]:
with belief_scope("get_type_profile"):
return TASK_TYPE_PROFILES.get(task_type, TASK_TYPE_PROFILES[TaskType.UNKNOWN])

View File

@@ -1,13 +1,13 @@
# #region ResourceServiceModule [C:5] [TYPE Module] [SEMANTICS resource, git, task, status, superset]
# @BRIEF Shared service for fetching resource data with Git status and task status
# @LAYER: Service
# @LAYER Service
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [TaskManagerPackage]
# @RELATION DEPENDS_ON -> [TaskManagerModels]
# @RELATION DEPENDS_ON -> [GitService]
# @INVARIANT: All resources include metadata about their current state
# @SIDE_EFFECT: Queries multiple backends for status
# @DATA_CONTRACT: ResourceQuery -> ResourceStatusSummary
# @INVARIANT All resources include metadata about their current state
# @SIDE_EFFECT Queries multiple backends for status
# @DATA_CONTRACT ResourceQuery -> ResourceStatusSummary
import asyncio
from datetime import UTC, datetime
@@ -27,8 +27,8 @@ class ResourceService:
# region ResourceService_init [TYPE Function]
# @PURPOSE: Initialize the resource service with dependencies
# @PRE: None
# @POST: ResourceService is ready to fetch resources
# @PRE None
# @POST ResourceService is ready to fetch resources
def __init__(self):
with belief_scope("ResourceService.__init__"):
self.git_service = GitService()
@@ -37,14 +37,14 @@ class ResourceService:
# region get_dashboards_with_status [TYPE Function]
# @PURPOSE: Fetch dashboards from environment with Git status and last task status
# @PRE: env is a valid Environment object
# @POST: Returns list of dashboards with enhanced metadata
# @PARAM: env (Environment) - The environment to fetch from
# @PARAM: tasks (List[Task]) - List of tasks to check for status
# @RETURN: List[Dict] - Dashboards with git_status and last_task fields
# @RELATION: CALLS -> [SupersetClientGetDashboardsSummary]
# @RELATION: CALLS ->[_get_git_status_for_dashboard]
# @RELATION: CALLS ->[_get_last_llm_task_for_dashboard]
# @PRE env is a valid Environment object
# @POST Returns list of dashboards with enhanced metadata
# @PARAM env (Environment) - The environment to fetch from
# @PARAM tasks (List[Task]) - List of tasks to check for status
# @RETURN List[Dict] - Dashboards with git_status and last_task fields
# @RELATION CALLS -> [SupersetClientGetDashboardsSummary]
# @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]
# @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]
async def get_dashboards_with_status(
self,
env: Any,
@@ -86,16 +86,16 @@ class ResourceService:
# region get_dashboards_page_with_status [TYPE Function]
# @PURPOSE: Fetch one dashboard page from environment and enrich only that page with status metadata.
# @PRE: env is valid; page >= 1; page_size > 0.
# @POST: Returns page items plus total counters without scanning all pages locally.
# @PARAM: env (Environment) - Source environment.
# @PARAM: tasks (Optional[List[Task]]) - Tasks for latest LLM status.
# @PARAM: page (int) - 1-based page number.
# @PARAM: page_size (int) - Page size.
# @RETURN: Dict[str, Any] - {"dashboards": List[Dict], "total": int, "total_pages": int}
# @RELATION: CALLS -> [SupersetClientGetDashboardsSummaryPage]
# @RELATION: CALLS ->[_get_git_status_for_dashboard]
# @RELATION: CALLS ->[_get_last_llm_task_for_dashboard]
# @PRE env is valid; page >= 1; page_size > 0.
# @POST Returns page items plus total counters without scanning all pages locally.
# @PARAM env (Environment) - Source environment.
# @PARAM tasks (Optional[List[Task]]) - Tasks for latest LLM status.
# @PARAM page (int) - 1-based page number.
# @PARAM page_size (int) - Page size.
# @RETURN Dict[str, Any] - {"dashboards": List[Dict], "total": int, "total_pages": int}
# @RELATION CALLS -> [SupersetClientGetDashboardsSummaryPage]
# @RELATION CALLS ->[EXT:method:_get_git_status_for_dashboard]
# @RELATION CALLS ->[EXT:method:_get_last_llm_task_for_dashboard]
async def get_dashboards_page_with_status(
self,
env: Any,
@@ -152,15 +152,15 @@ class ResourceService:
# region _get_last_llm_task_for_dashboard [TYPE Function]
# @PURPOSE: Get most recent LLM validation task for a dashboard in an environment
# @PRE: dashboard_id is a valid integer identifier
# @POST: Returns the newest llm_dashboard_validation task summary or None
# @PARAM: dashboard_id (int) - The dashboard ID
# @PARAM: env_id (Optional[str]) - Environment ID to match task params
# @PARAM: tasks (Optional[List[Task]]) - List of tasks to search
# @RETURN: Optional[Dict] - Task summary with task_id and status
# @RELATION: CALLS ->[_normalize_datetime_for_compare]
# @RELATION: CALLS ->[_normalize_validation_status]
# @RELATION: CALLS ->[_normalize_task_status]
# @PRE dashboard_id is a valid integer identifier
# @POST Returns the newest llm_dashboard_validation task summary or None
# @PARAM dashboard_id (int) - The dashboard ID
# @PARAM env_id (Optional[str]) - Environment ID to match task params
# @PARAM tasks (Optional[List[Task]]) - List of tasks to search
# @RETURN Optional[Dict] - Task summary with task_id and status
# @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]
# @RELATION CALLS ->[EXT:method:_normalize_validation_status]
# @RELATION CALLS ->[EXT:method:_normalize_task_status]
def _get_last_llm_task_for_dashboard(
self,
dashboard_id: int,
@@ -235,11 +235,11 @@ class ResourceService:
# region _normalize_task_status [TYPE Function]
# @PURPOSE: Normalize task status to stable uppercase values for UI/API projections
# @PRE: raw_status can be enum or string
# @POST: Returns uppercase status without enum class prefix
# @PARAM: raw_status (Any) - Raw task status object/value
# @RETURN: str - Normalized status token
# @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]
# @PRE raw_status can be enum or string
# @POST Returns uppercase status without enum class prefix
# @PARAM raw_status (Any) - Raw task status object/value
# @RETURN str - Normalized status token
# @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]
def _normalize_task_status(self, raw_status: Any) -> str:
if raw_status is None:
return ""
@@ -252,11 +252,11 @@ class ResourceService:
# region _normalize_validation_status [TYPE Function]
# @PURPOSE: Normalize LLM validation status to PASS/FAIL/WARN/UNKNOWN
# @PRE: raw_status can be any scalar type
# @POST: Returns normalized validation status token or None
# @PARAM: raw_status (Any) - Raw validation status from task result
# @RETURN: Optional[str] - PASS|FAIL|WARN|UNKNOWN
# @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]
# @PRE raw_status can be any scalar type
# @POST Returns normalized validation status token or None
# @PARAM raw_status (Any) - Raw validation status from task result
# @RETURN Optional[str] - PASS|FAIL|WARN|UNKNOWN
# @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]
def _normalize_validation_status(self, raw_status: Any) -> str | None:
if raw_status is None:
return None
@@ -268,12 +268,12 @@ class ResourceService:
# region _normalize_datetime_for_compare [TYPE Function]
# @PURPOSE: Normalize datetime values to UTC-aware values for safe comparisons.
# @PRE: value may be datetime or any scalar.
# @POST: Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.
# @PARAM: value (Any) - Candidate datetime-like value.
# @RETURN: datetime - UTC-aware comparable datetime.
# @RELATION: USED_BY ->[_get_last_llm_task_for_dashboard]
# @RELATION: USED_BY ->[_get_last_task_for_resource]
# @PRE value may be datetime or any scalar.
# @POST Returns UTC-aware datetime; non-datetime values map to minimal UTC datetime.
# @PARAM value (Any) - Candidate datetime-like value.
# @RETURN datetime - UTC-aware comparable datetime.
# @RELATION CALLED_BY -> [EXT:method:_get_last_llm_task_for_dashboard]
# @RELATION CALLED_BY -> [EXT:method:_get_last_task_for_resource]
def _normalize_datetime_for_compare(self, value: Any) -> datetime:
if isinstance(value, datetime):
if value.tzinfo is None:
@@ -284,14 +284,14 @@ class ResourceService:
# region get_datasets_with_status [TYPE Function]
# @PURPOSE: Fetch datasets from environment with mapping progress and last task status
# @PRE: env is a valid Environment object
# @POST: Returns list of datasets with enhanced metadata
# @PARAM: env (Environment) - The environment to fetch from
# @PARAM: tasks (List[Task]) - List of tasks to check for status
# @RETURN: List[Dict] - Datasets with mapped_fields, last_task and linked_dashboard_count fields
# @RELATION: CALLS -> [SupersetClientGetDatasetsSummary]
# @RELATION: CALLS -> [SupersetClientGetDatasetLinkedDashboardCount]
# @RELATION: CALLS ->[_get_last_task_for_resource]
# @PRE env is a valid Environment object
# @POST Returns list of datasets with enhanced metadata
# @PARAM env (Environment) - The environment to fetch from
# @PARAM tasks (List[Task]) - List of tasks to check for status
# @RETURN List[Dict] - Datasets with mapped_fields, last_task and linked_dashboard_count fields
# @RELATION CALLS -> [SupersetClientGetDatasetsSummary]
# @RELATION CALLS -> [SupersetClientGetDatasetLinkedDashboardCount]
# @RELATION CALLS ->[EXT:method:_get_last_task_for_resource]
# @RATIONALE linked_dashboard_count was missing — get_datasets_summary() only returns id/table_name/schema/database
# StatsBar showed linked_count=0 because the field was never populated in list endpoint.
# Fix: fetch /dataset/{id}/related_objects for each dataset concurrently (semaphore=3, timeout=10s).
@@ -319,7 +319,7 @@ class ResourceService:
)
except (asyncio.TimeoutError, Exception):
logger.warning(
f"[get_datasets_with_status][Warning] "
f"[EXT:method:get_datasets_with_status][Warning] "
f"Failed to fetch linked dashboard count for dataset {ds_id}"
)
return 0
@@ -355,12 +355,12 @@ class ResourceService:
# region get_activity_summary [TYPE Function]
# @PURPOSE: Get summary of active and recent tasks for the activity indicator
# @PRE: tasks is a list of Task objects
# @POST: Returns summary with active_count and recent_tasks
# @PARAM: tasks (List[Task]) - List of tasks to summarize
# @RETURN: Dict - Activity summary
# @RELATION: CALLS ->[_extract_resource_name_from_task]
# @RELATION: CALLS ->[_extract_resource_type_from_task]
# @PRE tasks is a list of Task objects
# @POST Returns summary with active_count and recent_tasks
# @PARAM tasks (List[Task]) - List of tasks to summarize
# @RETURN Dict - Activity summary
# @RELATION CALLS ->[EXT:method:_extract_resource_name_from_task]
# @RELATION CALLS ->[EXT:method:_extract_resource_type_from_task]
def get_activity_summary(self, tasks: list[Task]) -> dict[str, Any]:
with belief_scope("get_activity_summary"):
# Count active (RUNNING, WAITING_INPUT) tasks
@@ -396,11 +396,11 @@ class ResourceService:
# region _get_git_status_for_dashboard [TYPE Function]
# @PURPOSE: Get Git sync status for a dashboard
# @PRE: dashboard_id is a valid integer
# @POST: Returns git status or None if no repo exists
# @PARAM: dashboard_id (int) - The dashboard ID
# @RETURN: Optional[Dict] - Git status with branch and sync_status
# @RELATION: CALLS ->[get_repo]
# @PRE dashboard_id is a valid integer
# @POST Returns git status or None if no repo exists
# @PARAM dashboard_id (int) - The dashboard ID
# @RETURN Optional[Dict] - Git status with branch and sync_status
# @RELATION CALLS ->[EXT:method:get_repo]
def _get_git_status_for_dashboard(self, dashboard_id: int) -> dict[str, Any] | None:
try:
repo = self.git_service.get_repo(dashboard_id)
@@ -455,12 +455,12 @@ class ResourceService:
# region _get_last_task_for_resource [TYPE Function]
# @PURPOSE: Get the most recent task for a specific resource
# @PRE: resource_id is a valid string
# @POST: Returns task summary or None if no tasks found
# @PARAM: resource_id (str) - The resource identifier (e.g., "dashboard-123")
# @PARAM: tasks (Optional[List[Task]]) - List of tasks to search
# @RETURN: Optional[Dict] - Task summary with task_id and status
# @RELATION: CALLS ->[_normalize_datetime_for_compare]
# @PRE resource_id is a valid string
# @POST Returns task summary or None if no tasks found
# @PARAM resource_id (str) - The resource identifier (e.g., "dashboard-123")
# @PARAM tasks (Optional[List[Task]]) - List of tasks to search
# @RETURN Optional[Dict] - Task summary with task_id and status
# @RELATION CALLS ->[EXT:method:_normalize_datetime_for_compare]
def _get_last_task_for_resource(
self,
resource_id: str,
@@ -493,11 +493,11 @@ class ResourceService:
# region _extract_resource_name_from_task [TYPE Function]
# @PURPOSE: Extract resource name from task params
# @PRE: task is a valid Task object
# @POST: Returns resource name or task ID
# @PARAM: task (Task) - The task to extract from
# @RETURN: str - Resource name or fallback
# @RELATION: USED_BY ->[get_activity_summary]
# @PRE task is a valid Task object
# @POST Returns resource name or task ID
# @PARAM task (Task) - The task to extract from
# @RETURN str - Resource name or fallback
# @RELATION CALLED_BY -> [EXT:method:get_activity_summary]
def _extract_resource_name_from_task(self, task: Task) -> str:
params = task.params or {}
return params.get('resource_name', f"Task {task.id}")
@@ -505,11 +505,11 @@ class ResourceService:
# region _extract_resource_type_from_task [TYPE Function]
# @PURPOSE: Extract resource type from task params
# @PRE: task is a valid Task object
# @POST: Returns resource type or 'unknown'
# @PARAM: task (Task) - The task to extract from
# @RETURN: str - Resource type
# @RELATION: USED_BY ->[get_activity_summary]
# @PRE task is a valid Task object
# @POST Returns resource type or 'unknown'
# @PARAM task (Task) - The task to extract from
# @RETURN str - Resource type
# @RELATION CALLED_BY -> [EXT:method:get_activity_summary]
def _extract_resource_type_from_task(self, task: Task) -> str:
params = task.params or {}
return params.get('resource_type', 'unknown')

View File

@@ -4,7 +4,7 @@
# @LAYER Domain
# @RELATION DEPENDS_ON -> [User]
# @RELATION DEPENDS_ON -> [discover_declared_permissions]
# @RELATION DEPENDS_ON -> [profile_utils]
# @RELATION DEPENDS_ON -> [profile[EXT:internal:_utils]]
# @RATIONALE Extracted from ProfileService to satisfy INV_7. Security badge construction
# is a distinct concern with its own dependencies (discover_declared_permissions,
# plugin_loader) and can be tested independently.
@@ -20,7 +20,7 @@ from typing import Any
from ..core.logger import belief_scope, logger
from ..models.auth import User
from ..schemas.profile import ProfilePermissionState, ProfileSecuritySummary
from .profile_utils import sanitize_text
from .profile[EXT:internal:_utils] import sanitize_text
from .rbac_permission_catalog import discover_declared_permissions

View File

@@ -4,7 +4,7 @@
# Phase 2: In Jinja spans, extract "schema.table" from string values
# Phase 3: In SQL spans, regex pattern + sqlparse filter to reject string literal false positives
# @LAYER Service
# @RELATION DEPENDS_ON -> [re, sqlparse]
# @RELATION DEPENDS_ON -> [[EXT:internal:re_sqlparse]]
# @INVARIANT Only exact schema.table matches (case-insensitive); unqualified references are NOT matched.
# @INVARIANT Returns a set[str] of fully-qualified table names (lowercased for case-insensitive matching).

View File

@@ -3,7 +3,7 @@
# @LAYER Domain
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [SupersetAccountLookupAdapter]
# @RELATION DEPENDS_ON -> [profile_utils]
# @RELATION DEPENDS_ON -> [profile[EXT:internal:_utils]]
# @RATIONALE Extracted from ProfileService to satisfy INV_7. Superset account lookup has distinct
# I/O patterns (external HTTP calls, environment resolution) that isolate cleanly from
# preference persistence and security badge logic.
@@ -25,7 +25,7 @@ from ..schemas.profile import (
SupersetAccountLookupRequest,
SupersetAccountLookupResponse,
)
from .profile_utils import EnvironmentNotFoundError
from .profile[EXT:internal:_utils] import EnvironmentNotFoundError
# #region SupersetLookupService [C:4] [TYPE Class] [SEMANTICS superset,lookup,environment,degradation]