semantics
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
*,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user