semantics

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

View File

@@ -1,12 +1,12 @@
# #region MigrationArchiveParserModule [C:5] [TYPE Module] [SEMANTICS migration, transform, superset, migration-archive-parser]
# @BRIEF Parse Superset export ZIP archives into normalized object catalogs for diffing.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [LoggerModule]
# @INVARIANT: Parsing is read-only and never mutates archive files.
# @PRE: Archive file path is valid and readable
# @POST: Parsed migration archive returned
# @SIDE_EFFECT: Reads archive file (read-only)
# @DATA_CONTRACT: ArchivePath -> ParsedMigration
# @INVARIANT Parsing is read-only and never mutates archive files.
# @PRE Archive file path is valid and readable
# @POST Parsed migration archive returned
# @SIDE_EFFECT Reads archive file (read-only)
# @DATA_CONTRACT ArchivePath -> ParsedMigration
import json
from pathlib import Path
import tempfile
@@ -20,16 +20,16 @@ from ..logger import belief_scope, logger
# #region MigrationArchiveParser [TYPE Class]
# @BRIEF Extract normalized dashboards/charts/datasets metadata from ZIP archives.
# @RELATION CONTAINS -> [extract_objects_from_zip]
# @RELATION CONTAINS -> [_collect_yaml_objects]
# @RELATION CONTAINS -> [_normalize_object_payload]
# @RELATION DEPENDS_ON -> [extract_objects_from_zip]
# @RELATION DEPENDS_ON -> [_collect_yaml_objects]
# @RELATION DEPENDS_ON -> [_normalize_object_payload]
class MigrationArchiveParser:
# #region extract_objects_from_zip [TYPE Function]
# @PURPOSE: Extract object catalogs from Superset archive.
# @RELATION: DEPENDS_ON -> [_collect_yaml_objects]
# @PRE: zip_path points to a valid readable ZIP.
# @POST: Returns object lists grouped by resource type.
# @RETURN: Dict[str, List[Dict[str, Any]]]
# @RELATION DEPENDS_ON -> [_collect_yaml_objects]
# @PRE zip_path points to a valid readable ZIP.
# @POST Returns object lists grouped by resource type.
# @RETURN Dict[str, List[Dict[str, Any]]]
def extract_objects_from_zip(
self, zip_path: str
) -> dict[str, list[dict[str, Any]]]:
@@ -52,9 +52,9 @@ class MigrationArchiveParser:
# #endregion extract_objects_from_zip
# #region _collect_yaml_objects [TYPE Function]
# @PURPOSE: Read and normalize YAML manifests for one object type.
# @RELATION: DEPENDS_ON -> [_normalize_object_payload]
# @PRE: object_type is one of dashboards/charts/datasets.
# @POST: Returns only valid normalized objects.
# @RELATION DEPENDS_ON -> [_normalize_object_payload]
# @PRE object_type is one of dashboards/charts/datasets.
# @POST Returns only valid normalized objects.
def _collect_yaml_objects(
self, root_dir: Path, object_type: str
) -> list[dict[str, Any]]:
@@ -79,8 +79,8 @@ class MigrationArchiveParser:
# #endregion _collect_yaml_objects
# #region _normalize_object_payload [TYPE Function]
# @PURPOSE: Convert raw YAML payload to stable diff signature shape.
# @PRE: payload is parsed YAML mapping.
# @POST: Returns normalized descriptor with `uuid`, `title`, and `signature`.
# @PRE payload is parsed YAML mapping.
# @POST Returns normalized descriptor with `uuid`, `title`, and `signature`.
def _normalize_object_payload(
self, payload: dict[str, Any], object_type: str
) -> dict[str, Any] | None:

View File

@@ -1,15 +1,15 @@
# #region MigrationDryRunOrchestratorModule [C:5] [TYPE Module] [SEMANTICS sqlalchemy, migration, orchestration, diff, migration-dry-run-service]
# @BRIEF Compute pre-flight migration diff and risk scoring without apply.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION DEPENDS_ON -> [MigrationEngine]
# @RELATION DEPENDS_ON -> [MigrationArchiveParser]
# @RELATION DEPENDS_ON -> [RiskAssessorModule]
# @INVARIANT: Dry run is informative only and must not mutate target environment.
# @PRE: Source and target environments configured
# @POST: Dry-run diff returned without mutation
# @SIDE_EFFECT: Reads source environment (read-only)
# @DATA_CONTRACT: EnvironmentConfig -> MigrationDiffReport
# @INVARIANT Dry run is informative only and must not mutate target environment.
# @PRE Source and target environments configured
# @POST Dry-run diff returned without mutation
# @SIDE_EFFECT Reads source environment (read-only)
# @DATA_CONTRACT EnvironmentConfig -> MigrationDiffReport
from datetime import UTC, datetime
import json
from typing import Any
@@ -28,32 +28,32 @@ from .risk_assessor import build_risks, score_risks
# #region MigrationDryRunService [TYPE Class]
# @BRIEF Build deterministic diff/risk payload for migration pre-flight.
# @RELATION CONTAINS -> [__init__]
# @RELATION CONTAINS -> [run]
# @RELATION CONTAINS -> [_load_db_mapping]
# @RELATION CONTAINS -> [_accumulate_objects]
# @RELATION CONTAINS -> [_index_by_uuid]
# @RELATION CONTAINS -> [_build_object_diff]
# @RELATION CONTAINS -> [_build_target_signatures]
# @RELATION CONTAINS -> [_build_risks]
# @RELATION DEPENDS_ON -> [__init__]
# @RELATION DEPENDS_ON -> [run]
# @RELATION DEPENDS_ON -> [_load_db_mapping]
# @RELATION DEPENDS_ON -> [_accumulate_objects]
# @RELATION DEPENDS_ON -> [_index_by_uuid]
# @RELATION DEPENDS_ON -> [_build_object_diff]
# @RELATION DEPENDS_ON -> [_build_target_signatures]
# @RELATION DEPENDS_ON -> [_build_risks]
class MigrationDryRunService:
# #region __init__ [TYPE Function]
# @PURPOSE: Wire parser dependency for archive object extraction.
# @PRE: parser can be omitted to use default implementation.
# @POST: Service is ready to calculate dry-run payload.
# @PRE parser can be omitted to use default implementation.
# @POST Service is ready to calculate dry-run payload.
def __init__(self, parser: MigrationArchiveParser | None = None):
self.parser = parser or MigrationArchiveParser()
# #endregion __init__
# #region run [TYPE Function]
# @PURPOSE: Execute full dry-run computation for selected dashboards.
# @RELATION: DEPENDS_ON -> [_load_db_mapping]
# @RELATION: DEPENDS_ON -> [_accumulate_objects]
# @RELATION: DEPENDS_ON -> [_build_target_signatures]
# @RELATION: DEPENDS_ON -> [_build_object_diff]
# @RELATION: DEPENDS_ON -> [_build_risks]
# @PRE: source/target clients are authenticated and selection validated by caller.
# @POST: Returns JSON-serializable pre-flight payload with summary, diff and risk.
# @SIDE_EFFECT: Reads source export archives and target metadata via network.
# @RELATION DEPENDS_ON -> [_load_db_mapping]
# @RELATION DEPENDS_ON -> [_accumulate_objects]
# @RELATION DEPENDS_ON -> [_build_target_signatures]
# @RELATION DEPENDS_ON -> [_build_object_diff]
# @RELATION DEPENDS_ON -> [_build_risks]
# @PRE source/target clients are authenticated and selection validated by caller.
# @POST Returns JSON-serializable pre-flight payload with summary, diff and risk.
# @SIDE_EFFECT Reads source export archives and target metadata via network.
def run(
self,
selection: DashboardSelection,
@@ -193,7 +193,7 @@ class MigrationDryRunService:
# #endregion _index_by_uuid
# #region _build_object_diff [TYPE Function]
# @PURPOSE: Compute create/update/delete buckets by UUID+signature.
# @RELATION: DEPENDS_ON -> [_index_by_uuid]
# @RELATION DEPENDS_ON -> [_index_by_uuid]
def _build_object_diff(
self, source_objects: list[dict[str, Any]], target_objects: list[dict[str, Any]]
) -> dict[str, list[dict[str, Any]]]:

View File

@@ -1,28 +1,28 @@
# #region RiskAssessorModule [C:5] [TYPE Module] [SEMANTICS tenacity, migration, dry-run]
# @BRIEF Compute deterministic migration risk items and aggregate score for dry-run reporting.
# @LAYER: Domain
# @LAYER Domain
# @RELATION DEPENDS_ON -> [SupersetClient]
# @RELATION CONTAINS -> [index_by_uuid]
# @RELATION CONTAINS -> [extract_owner_identifiers]
# @RELATION CONTAINS -> [build_risks]
# @RELATION CONTAINS -> [score_risks]
# @INVARIANT: Risk scoring must remain bounded to [0,100] and preserve severity-to-weight mapping.
# @PRE: Risk assessor functions receive normalized migration object collections from dry-run orchestration.
# @POST: Risk scoring output preserves item list and provides bounded score with derived level.
# @SIDE_EFFECT: Emits diagnostic logs and performs read-only metadata requests via Superset client.
# @DATA_CONTRACT: Module[build_risks, score_risks]
# @TEST_CONTRACT: [source_objects,target_objects,diff,target_client] -> [List[RiskItem]]
# @TEST_SCENARIO: [overwrite_update_objects] -> [medium overwrite_existing risk is emitted for each update diff item]
# @TEST_SCENARIO: [missing_datasource_dataset] -> [high missing_datasource risk is emitted]
# @TEST_SCENARIO: [owner_mismatch_dashboard] -> [low owner_mismatch risk is emitted]
# @TEST_EDGE: [missing_field] -> [object without uuid is ignored by indexer]
# @TEST_EDGE: [invalid_type] -> [non-list owners input normalizes to empty identifiers]
# @TEST_EDGE: [external_fail] -> [target_client get_databases exception propagates to caller]
# @TEST_INVARIANT: [score_upper_bound_100] -> VERIFIED_BY: [severity_weight_aggregation]
# @UX_STATE: [Idle] -> [N/A backend domain module]
# @UX_FEEDBACK: [N/A] -> [No direct UI side effects in this module]
# @UX_RECOVERY: [N/A] -> [Caller-level retry/recovery]
# @UX_REACTIVITY: [N/A] -> [Backend synchronous function contracts]
# @RELATION DEPENDS_ON -> [index_by_uuid]
# @RELATION DEPENDS_ON -> [extract_owner_identifiers]
# @RELATION DEPENDS_ON -> [build_risks]
# @RELATION DEPENDS_ON -> [score_risks]
# @INVARIANT Risk scoring must remain bounded to [0,100] and preserve severity-to-weight mapping.
# @PRE Risk assessor functions receive normalized migration object collections from dry-run orchestration.
# @POST Risk scoring output preserves item list and provides bounded score with derived level.
# @SIDE_EFFECT Emits diagnostic logs and performs read-only metadata requests via Superset client.
# @DATA_CONTRACT Module[build_risks, score_risks]
# @TEST_CONTRACT [source_objects,target_objects,diff,target_client] -> [List[RiskItem]]
# @TEST_SCENARIO [overwrite_update_objects] -> [medium overwrite_existing risk is emitted for each update diff item]
# @TEST_SCENARIO [missing_datasource_dataset] -> [high missing_datasource risk is emitted]
# @TEST_SCENARIO [owner_mismatch_dashboard] -> [low owner_mismatch risk is emitted]
# @TEST_EDGE [missing_field] -> [object without uuid is ignored by indexer]
# @TEST_EDGE [invalid_type] -> [non-list owners input normalizes to empty identifiers]
# @TEST_EDGE [external_fail] -> [target_client get_databases exception propagates to caller]
# @TEST_INVARIANT [score_upper_bound_100] -> VERIFIED_BY: [severity_weight_aggregation]
# @UX_STATE [Idle] -> [N/A backend domain module]
# @UX_FEEDBACK [N/A] -> [No direct UI side effects in this module]
# @UX_RECOVERY [N/A] -> [Caller-level retry/recovery]
# @UX_REACTIVITY [N/A] -> [Backend synchronous function contracts]
from typing import Any
@@ -32,10 +32,10 @@ from ..superset_client import SupersetClient
# #region index_by_uuid [TYPE Function]
# @BRIEF Build UUID-index from normalized objects.
# @PRE: Input list items are dict-like payloads potentially containing "uuid".
# @POST: Returns mapping keyed by string uuid; only truthy uuid values are included.
# @SIDE_EFFECT: Emits reasoning/reflective logs only.
# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Dict[str, Any]]
# @PRE Input list items are dict-like payloads potentially containing "uuid".
# @POST Returns mapping keyed by string uuid; only truthy uuid values are included.
# @SIDE_EFFECT Emits reasoning/reflective logs only.
# @DATA_CONTRACT List[Dict[str, Any]] -> Dict[str, Dict[str, Any]]
def index_by_uuid(objects: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
with belief_scope("risk_assessor.index_by_uuid"):
logger.reason("Building UUID index", extra={"objects_count": len(objects)})
@@ -53,10 +53,10 @@ def index_by_uuid(objects: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
# #region extract_owner_identifiers [TYPE Function]
# @BRIEF Normalize owner payloads for stable comparison.
# @PRE: Owners may be list payload, scalar values, or None.
# @POST: Returns sorted unique owner identifiers as strings.
# @SIDE_EFFECT: Emits reasoning/reflective logs only.
# @DATA_CONTRACT: Any -> List[str]
# @PRE Owners may be list payload, scalar values, or None.
# @POST Returns sorted unique owner identifiers as strings.
# @SIDE_EFFECT Emits reasoning/reflective logs only.
# @DATA_CONTRACT Any -> List[str]
def extract_owner_identifiers(owners: Any) -> list[str]:
with belief_scope("risk_assessor.extract_owner_identifiers"):
logger.reason("Normalizing owner identifiers")
@@ -86,16 +86,16 @@ def extract_owner_identifiers(owners: Any) -> list[str]:
# @BRIEF Build risk list from computed diffs and target catalog state.
# @RELATION DEPENDS_ON -> [index_by_uuid]
# @RELATION DEPENDS_ON -> [extract_owner_identifiers]
# @PRE: source_objects/target_objects/diff contain dashboards/charts/datasets keys with expected list structures.
# @PRE: target_client is authenticated/usable for database list retrieval.
# @POST: Returns list of deterministic risk items derived from overwrite, missing datasource, reference, and owner mismatch checks.
# @SIDE_EFFECT: Calls target Superset API for databases metadata and emits logs.
# @DATA_CONTRACT: (
# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],
# @DATA_CONTRACT: Dict[str, List[Dict[str, Any]]],
# @DATA_CONTRACT: Dict[str, Dict[str, List[Dict[str, Any]]]],
# @DATA_CONTRACT: SupersetClient
# @DATA_CONTRACT: ) -> List[Dict[str, Any]]
# @PRE source_objects/target_objects/diff contain dashboards/charts/datasets keys with expected list structures.
# @PRE target_client is authenticated/usable for database list retrieval.
# @POST Returns list of deterministic risk items derived from overwrite, missing datasource, reference, and owner mismatch checks.
# @SIDE_EFFECT Calls target Superset API for databases metadata and emits logs.
# @DATA_CONTRACT (
# @DATA_CONTRACT Dict[str, List[Dict[str, Any]]],
# @DATA_CONTRACT Dict[str, List[Dict[str, Any]]],
# @DATA_CONTRACT Dict[str, Dict[str, List[Dict[str, Any]]]],
# @DATA_CONTRACT SupersetClient
# @DATA_CONTRACT ) -> List[Dict[str, Any]]
def build_risks(
source_objects: dict[str, list[dict[str, Any]]],
target_objects: dict[str, list[dict[str, Any]]],
@@ -177,10 +177,10 @@ def build_risks(
# #region score_risks [TYPE Function]
# @BRIEF Aggregate risk list into score and level.
# @PRE: risk_items contains optional severity fields expected in {high,medium,low} or defaults to low weight.
# @POST: Returns dict with score in [0,100], derived level, and original items.
# @SIDE_EFFECT: Emits reasoning/reflective logs only.
# @DATA_CONTRACT: List[Dict[str, Any]] -> Dict[str, Any]
# @PRE risk_items contains optional severity fields expected in {high,medium,low} or defaults to low weight.
# @POST Returns dict with score in [0,100], derived level, and original items.
# @SIDE_EFFECT Emits reasoning/reflective logs only.
# @DATA_CONTRACT List[Dict[str, Any]] -> Dict[str, Any]
def score_risks(risk_items: list[dict[str, Any]]) -> dict[str, Any]:
with belief_scope("risk_assessor.score_risks"):
logger.reason("Scoring risk items", extra={"risk_items_count": len(risk_items)})