From 81f38c3cab24d108f296eff50f6704603a1a6705 Mon Sep 17 00:00:00 2001 From: Paul Sattaur Date: Wed, 12 Aug 2026 11:52:49 -0400 Subject: [PATCH 1/2] =?UTF-8?q?Phase=207.1=20=E2=80=94=20dbt=20Quality=20E?= =?UTF-8?q?vidence=20+=20Trust=20Posture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 15 +- apps/api/skydata_studio/api/router.py | 2 + .../api/skydata_studio/api/routes/platform.py | 8 +- apps/api/skydata_studio/api/routes/quality.py | 20 ++ apps/api/skydata_studio/schemas/quality.py | 49 ++++ .../skydata_studio/services/dbt_quality.py | 251 ++++++++++++++++++ apps/web/src/App.jsx | 3 + apps/web/src/components/Sidebar.jsx | 2 +- apps/web/src/pages/DataQuality.jsx | 197 ++++++++++++++ apps/web/src/pages/StudioOverview.jsx | 16 +- apps/web/src/styles.css | 33 +++ docs/ARCHITECTURE.md | 19 ++ docs/ROADMAP.md | 33 ++- docs/SkyData_RepoMap.md | 7 +- docs/repository-map.txt | 8 +- tests/test_platform.py | 2 +- tests/test_quality.py | 163 ++++++++++++ 17 files changed, 805 insertions(+), 23 deletions(-) create mode 100644 apps/api/skydata_studio/api/routes/quality.py create mode 100644 apps/api/skydata_studio/schemas/quality.py create mode 100644 apps/api/skydata_studio/services/dbt_quality.py create mode 100644 apps/web/src/pages/DataQuality.jsx create mode 100644 tests/test_quality.py diff --git a/README.md b/README.md index 03017cd..290220e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SkyData Studio is the post-ingestion data engineering application in the Sky ecosystem. It begins where SkyCommand's ingestion responsibility ends and prepares trusted data for analytical consumption by SkyWeb Analytics, Power BI, and future client-facing applications. -**Current status:** Phases 0 through 5 are complete, **Phase 6.1 is complete** with a Docker-isolated dbt/Postgres runtime and the proven three-layer Federal Funds Rate model chain, and **Phase 6.2 is complete** with an artifact-backed Data Models catalogue reporting 3/3 models ready, 1 source, and 14 data tests. **Phase 6.3 is now in progress:** the governed mart becomes SkyData Studio's first dbt semantic model, with a stable entity, reusable dimensions, and four governed metrics projected from dbt artifacts. +**Current status:** Phases 0 through 6 are complete. **Phase 6.3 closed green** with one governed semantic model, four metrics, one DAY-grain MetricFlow time spine, and 18/18 dbt build nodes passing. **Phase 7.1 is now in progress:** dbt test definitions and latest run outcomes become an artifact-backed Data Quality trust surface. --- @@ -157,6 +157,10 @@ Unblock-File .\scripts\dbt.ps1 # only when Windows marks the trusted local help Phase 6.1 materializes the proven `stg_fed_funds_rate → int_fed_funds_rate_changes → fct_fed_funds_rate_daily` chain. Open `/workspace/transformations` for physical relation readiness and row-count proof. Phase 6.2 reads the generated `target/manifest.json` and `target/run_results.json` artifacts through `GET /api/v1/transformations/dbt/models`; open `/workspace/models` to inspect model layers, materializations, tests, columns, tags, and direct dependencies. Phase 6.3 adds model-embedded semantic definitions to the governed mart, a dedicated DAY-grain MetricFlow time spine, and projects semantic evidence through `GET /api/v1/transformations/dbt/semantic`; open `/delivery/semantic` to inspect the semantic model, primary entity, dimensions, and governed metrics. Generated dbt artifacts remain ignored by Git and are refreshed by the next dbt build. +## Phase 7.1 quick start + +Phase 7.1 keeps dbt as the test-definition and execution authority, then joins `manifest.json` test metadata with `run_results.json` outcomes through `GET /api/v1/quality/dbt/summary`. Open `/quality/checks` to inspect the latest trust posture, layer coverage, test dimensions, severity, failures, and runtime evidence. This first quality slice is observational only; persistent incidents, blocking policies, reconciliation rules, and SLOs remain later Phase 7 boundaries. + --- ## Validation contract @@ -297,8 +301,8 @@ The public roadmap is intentionally compact so GitHub visitors can see progress | Phase 3 | ✅ Complete | Studio-owned metadata registry, governance, source-to-target mappings, target schemas, and lineage dependencies | | Phase 4 | ✅ Complete | Versioned ETL/ELT pipeline workbench, replay-safe local execution, structured run evidence, and idempotent curated-table materialization | | Phase 5 | ✅ Complete | Apache Airflow 3 durable batch orchestration, REST API v2 integration, DAG/run/task observability, schedules, backfills, and ingestion-complete triggers | -| Phase 6 | 🔄 In Progress | dbt transformation and modelling foundation with tested staging, intermediate, mart, and semantic layers | -| Phase 7 | ⏳ Planned | Data quality, reconciliation, blocking policies, incidents, SLOs, and observability | +| Phase 6 | ✅ Complete | dbt transformation and modelling foundation with tested staging, intermediate, mart, and semantic layers | +| Phase 7 | 🔄 In Progress | Data quality, reconciliation, blocking policies, incidents, SLOs, and observability | | Phase 8 | ⏳ Planned | Asset/field/model/report lineage and downstream impact analysis | | Phase 9 | ⏳ Planned | Curated analytical marts, governed metrics, semantic delivery, and SkyWeb consumer contracts | | Phase 10 | ⏳ Planned | Power BI semantic models, refresh strategy, reporting inventory, and governed executive delivery | @@ -407,7 +411,8 @@ The repository currently includes: - a completed Phase 5.4 native Airflow asset-event bridge from terminal successful SkyCommand FRED/DFF ingestion evidence, including duplicate-signal reuse; - a completed Phase 6.1 Dockerized dbt/Postgres runtime plus the first governed staging → intermediate → mart model chain with 14 green data tests; - a completed Phase 6.2 artifact-backed dbt model catalogue surface for logical model, column, test, build, and dependency evidence; -- a Phase 6.3 model-embedded dbt semantic definition surface with one governed semantic model, reusable dimensions, and four governed metrics; +- a completed Phase 6.3 model-embedded dbt semantic definition surface with one governed semantic model, one stable entity, four dimensions, four governed metrics, and a DAY-grain MetricFlow time spine; +- a Phase 7.1 artifact-backed Data Quality surface that joins dbt test definitions to latest run outcomes and computes a consumer-facing trust posture; - backend/frontend validation, repository map, and compact handoff tooling. -The active implementation target is **Phase 6.3 — dbt Semantic Model and Governed Metric Foundation**. Phase 6.1 is complete: the isolated runtime, PostgreSQL connection, three-layer DFF model chain, 26,335-row grain, 14 data tests, Transformations API, and workbench readiness proof are green. Phase 6.2 is also complete: dbt's generated artifacts now drive a 3/3-ready logical Data Models catalogue without duplicating dbt-owned metadata in Studio PostgreSQL. Phase 6.3 extends that same single-authority pattern into semantic metadata: one mart-owned semantic model, one stable primary entity, reusable dimensions, four governed metrics, and one dedicated DAY-grain MetricFlow time spine surfaced through artifact evidence. +The active implementation target is **Phase 7.1 — dbt Quality Evidence and Trust Posture Foundation**. Phase 6 is complete: the isolated runtime, physical model chain, artifact-backed catalogue, semantic model, governed metrics, time spine, and 18/18 dbt build proof are green. Phase 7.1 now turns the same dbt artifacts into quality evidence without creating another test metadata authority inside Studio. diff --git a/apps/api/skydata_studio/api/router.py b/apps/api/skydata_studio/api/router.py index 1ddb2ac..112d2c5 100644 --- a/apps/api/skydata_studio/api/router.py +++ b/apps/api/skydata_studio/api/router.py @@ -9,6 +9,7 @@ pipeline_runs, pipelines, platform, + quality, skycommand, ) @@ -20,6 +21,7 @@ tags=["airflow-integration"], ) api_router.include_router(platform.router, prefix="/platform", tags=["platform"]) +api_router.include_router(quality.router, prefix="/quality", tags=["data-quality"]) api_router.include_router(dbt.router, prefix="/transformations/dbt", tags=["dbt-transformations"]) api_router.include_router(contracts.router, prefix="/contracts", tags=["contracts"]) api_router.include_router(metadata.router, prefix="/metadata", tags=["metadata-registry"]) diff --git a/apps/api/skydata_studio/api/routes/platform.py b/apps/api/skydata_studio/api/routes/platform.py index 8786090..b76ad43 100644 --- a/apps/api/skydata_studio/api/routes/platform.py +++ b/apps/api/skydata_studio/api/routes/platform.py @@ -50,7 +50,7 @@ code="QUALITY_LINEAGE", name="Quality and Lineage", description="Explains trust, failures, dependencies, ownership, and downstream impact.", - status="PLANNED", + status="FOUNDATION", phase=7, ), Capability( @@ -69,8 +69,8 @@ RoadmapPhase(number=3, name="Data Catalogue and Engineering Metadata", status="COMPLETE"), RoadmapPhase(number=4, name="ETL/ELT Pipeline Workbench", status="COMPLETE"), RoadmapPhase(number=5, name="Apache Airflow Integration", status="COMPLETE"), - RoadmapPhase(number=6, name="dbt Transformation and Modelling", status="IN_PROGRESS"), - RoadmapPhase(number=7, name="Data Quality and Observability", status="PLANNED"), + RoadmapPhase(number=6, name="dbt Transformation and Modelling", status="COMPLETE"), + RoadmapPhase(number=7, name="Data Quality and Observability", status="IN_PROGRESS"), RoadmapPhase(number=8, name="Lineage and Impact Analysis", status="PLANNED"), RoadmapPhase(number=9, name="Analytical Marts and Semantic Delivery", status="PLANNED"), RoadmapPhase(number=10, name="Power BI Integration", status="PLANNED"), @@ -83,7 +83,7 @@ def platform_summary() -> PlatformSummary: product="SkyData Studio", subtitle="Data Engineering Workbench", theme="Aurora Foundry", - current_phase="Phase 6.3 — dbt Semantic Model and Governed Metric Foundation", + current_phase="Phase 7.1 — dbt Quality Evidence and Trust Posture Foundation", boundary=( "SkyData Studio starts after SkyCommand ingestion and publishes governed " "analytical products for SkyWeb Analytics, Power BI, and future consumers." diff --git a/apps/api/skydata_studio/api/routes/quality.py b/apps/api/skydata_studio/api/routes/quality.py new file mode 100644 index 0000000..f224aa3 --- /dev/null +++ b/apps/api/skydata_studio/api/routes/quality.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter, HTTPException, status + +from skydata_studio.schemas.quality import DbtQualitySummary +from skydata_studio.services.dbt_quality import dbt_quality_summary + +router = APIRouter() + + +@router.get("/dbt/summary", response_model=DbtQualitySummary) +def dbt_quality() -> DbtQualitySummary: + try: + return dbt_quality_summary() + except (OSError, ValueError, TypeError) as error: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=( + "SkyData Studio could not read dbt quality artifacts. " + "Run .\\scripts\\dbt.ps1 build and refresh Data Quality." + ), + ) from error diff --git a/apps/api/skydata_studio/schemas/quality.py b/apps/api/skydata_studio/schemas/quality.py new file mode 100644 index 0000000..89815b7 --- /dev/null +++ b/apps/api/skydata_studio/schemas/quality.py @@ -0,0 +1,49 @@ +from typing import Literal + +from pydantic import BaseModel, Field + + +class DbtQualityCheckSummary(BaseModel): + unique_id: str + name: str + test_kind: Literal["GENERIC", "SINGULAR"] + quality_dimension: Literal[ + "COMPLETENESS", + "UNIQUENESS", + "VALIDITY", + "REFERENTIAL_INTEGRITY", + "BUSINESS_RULE", + "OTHER", + ] + target_name: str + target_resource_type: Literal["MODEL", "SOURCE", "UNKNOWN"] + layer: Literal["SOURCE", "STAGING", "INTERMEDIATE", "MART", "UNKNOWN"] + column_name: str | None = None + severity: Literal["ERROR", "WARN"] + status: Literal["PASS", "WARN", "FAIL", "ERROR", "SKIP", "UNKNOWN"] + failures: int | None = Field(default=None, ge=0) + execution_time_ms: float | None = Field(default=None, ge=0) + message: str | None = None + path: str + + +class DbtQualitySummary(BaseModel): + project_name: str = "skydata_studio" + phase: str = "Phase 7.1 — dbt Quality Evidence and Trust Posture Foundation" + artifact_status: Literal["READY", "PENDING", "MISSING"] + trust_posture: Literal["TRUSTED", "DEGRADED", "BLOCKED", "PENDING"] + generated_at: str | None = None + dbt_version: str | None = None + invocation_id: str | None = None + invocation_command: str | None = None + elapsed_time_ms: float | None = Field(default=None, ge=0) + test_count: int = Field(ge=0) + passed_count: int = Field(ge=0) + warning_count: int = Field(ge=0) + failed_count: int = Field(ge=0) + error_count: int = Field(ge=0) + skipped_count: int = Field(ge=0) + unknown_count: int = Field(ge=0) + source_test_count: int = Field(ge=0) + model_test_count: int = Field(ge=0) + checks: list[DbtQualityCheckSummary] diff --git a/apps/api/skydata_studio/services/dbt_quality.py b/apps/api/skydata_studio/services/dbt_quality.py new file mode 100644 index 0000000..249e116 --- /dev/null +++ b/apps/api/skydata_studio/services/dbt_quality.py @@ -0,0 +1,251 @@ +import json +from pathlib import Path +from typing import Any, Literal, cast + +from skydata_studio.schemas.quality import DbtQualityCheckSummary, DbtQualitySummary + + +def _default_dbt_target_dir() -> Path: + repository_root = Path(__file__).resolve().parents[4] + return repository_root / "transformations" / "dbt" / "skydata_studio" / "target" + + +def _read_json(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"Expected an object in dbt artifact {path}.") + return cast(dict[str, Any], payload) + + +def _model_layer(node: dict[str, Any]) -> Literal["STAGING", "INTERMEDIATE", "MART"]: + schema = str(node.get("schema") or "").lower() + original_path = str(node.get("original_file_path") or "").lower() + if schema == "dbt_staging" or "/staging/" in f"/{original_path}": + return "STAGING" + if schema == "dbt_intermediate" or "/intermediate/" in f"/{original_path}": + return "INTERMEDIATE" + return "MART" + + +def _quality_dimension(test_node: dict[str, Any]) -> Literal[ + "COMPLETENESS", + "UNIQUENESS", + "VALIDITY", + "REFERENTIAL_INTEGRITY", + "BUSINESS_RULE", + "OTHER", +]: + metadata = test_node.get("test_metadata") + test_name = "" + if isinstance(metadata, dict): + test_name = str(metadata.get("name") or "").lower() + + if test_name == "not_null": + return "COMPLETENESS" + if test_name == "unique": + return "UNIQUENESS" + if test_name in {"accepted_values", "expression_is_true"}: + return "VALIDITY" + if test_name == "relationships": + return "REFERENTIAL_INTEGRITY" + if not metadata: + return "BUSINESS_RULE" + return "OTHER" + + +def _target_dependency_id(test_node: dict[str, Any]) -> str | None: + attached_node = test_node.get("attached_node") + if isinstance(attached_node, str) and attached_node: + return attached_node + + depends_on = test_node.get("depends_on") + dependency_ids = depends_on.get("nodes", []) if isinstance(depends_on, dict) else [] + for dependency_id in dependency_ids: + if isinstance(dependency_id, str) and dependency_id.startswith(("model.", "source.")): + return dependency_id + return None + + +def _target_summary( + dependency_id: str | None, + nodes: dict[str, Any], + sources: dict[str, Any], +) -> tuple[str, Literal["MODEL", "SOURCE", "UNKNOWN"], str]: + if dependency_id and dependency_id in nodes: + node = nodes[dependency_id] + if isinstance(node, dict): + return str(node.get("name") or dependency_id), "MODEL", _model_layer(node) + if dependency_id and dependency_id in sources: + source = sources[dependency_id] + if isinstance(source, dict): + return str(source.get("name") or dependency_id), "SOURCE", "SOURCE" + return dependency_id or "unknown_target", "UNKNOWN", "UNKNOWN" + + +def _run_result_index(run_results: dict[str, Any]) -> dict[str, dict[str, Any]]: + results = run_results.get("results") + if not isinstance(results, list): + return {} + return { + unique_id: result + for result in results + if isinstance(result, dict) + and isinstance((unique_id := result.get("unique_id")), str) + } + + +def _status(result: dict[str, Any] | None) -> Literal[ + "PASS", "WARN", "FAIL", "ERROR", "SKIP", "UNKNOWN" +]: + raw = str(result.get("status") if result else "").lower() + mapping = { + "pass": "PASS", + "success": "PASS", + "warn": "WARN", + "fail": "FAIL", + "error": "ERROR", + "runtime error": "ERROR", + "skipped": "SKIP", + "skip": "SKIP", + } + return cast(Any, mapping.get(raw, "UNKNOWN")) + + +def _severity(test_node: dict[str, Any]) -> Literal["ERROR", "WARN"]: + config = test_node.get("config") + severity = str(config.get("severity") if isinstance(config, dict) else "ERROR").upper() + return "WARN" if severity == "WARN" else "ERROR" + + +def dbt_quality_summary(target_dir: Path | None = None) -> DbtQualitySummary: + target = target_dir or _default_dbt_target_dir() + manifest_path = target / "manifest.json" + if not manifest_path.exists(): + return DbtQualitySummary( + artifact_status="MISSING", + trust_posture="PENDING", + test_count=0, + passed_count=0, + warning_count=0, + failed_count=0, + error_count=0, + skipped_count=0, + unknown_count=0, + source_test_count=0, + model_test_count=0, + checks=[], + ) + + manifest = _read_json(manifest_path) + run_results_path = target / "run_results.json" + run_results = _read_json(run_results_path) if run_results_path.exists() else {} + result_index = _run_result_index(run_results) + + raw_nodes = manifest.get("nodes") + raw_sources = manifest.get("sources") + nodes = cast(dict[str, Any], raw_nodes) if isinstance(raw_nodes, dict) else {} + sources = cast(dict[str, Any], raw_sources) if isinstance(raw_sources, dict) else {} + + test_nodes = { + unique_id: node + for unique_id, node in nodes.items() + if isinstance(node, dict) + and node.get("resource_type") == "test" + and node.get("package_name") == "skydata_studio" + and bool((node.get("config") or {}).get("enabled", True)) + } + + checks: list[DbtQualityCheckSummary] = [] + for unique_id, test_node in sorted( + test_nodes.items(), key=lambda item: str(item[1].get("name") or item[0]) + ): + dependency_id = _target_dependency_id(test_node) + target_name, target_type, layer = _target_summary(dependency_id, nodes, sources) + metadata = test_node.get("test_metadata") + result = result_index.get(unique_id) + execution_time = result.get("execution_time") if result else None + failures = result.get("failures") if result else None + message = result.get("message") if result else None + + checks.append( + DbtQualityCheckSummary( + unique_id=unique_id, + name=str(test_node.get("name") or unique_id), + test_kind="GENERIC" if isinstance(metadata, dict) else "SINGULAR", + quality_dimension=_quality_dimension(test_node), + target_name=target_name, + target_resource_type=target_type, + layer=cast(Any, layer), + column_name=( + str(test_node.get("column_name")) if test_node.get("column_name") else None + ), + severity=_severity(test_node), + status=_status(result), + failures=(int(failures) if isinstance(failures, int) and failures >= 0 else None), + execution_time_ms=( + round(float(execution_time) * 1000, 2) + if isinstance(execution_time, (int, float)) and execution_time >= 0 + else None + ), + message=(str(message) if isinstance(message, str) and message else None), + path=str(test_node.get("original_file_path") or test_node.get("path") or ""), + ) + ) + + counts = { + status: sum(check.status == status for check in checks) + for status in ("PASS", "WARN", "FAIL", "ERROR", "SKIP", "UNKNOWN") + } + if counts["FAIL"] or counts["ERROR"]: + posture = "BLOCKED" + elif counts["WARN"] or counts["SKIP"]: + posture = "DEGRADED" + elif counts["UNKNOWN"]: + posture = "PENDING" + else: + posture = "TRUSTED" + + manifest_metadata = manifest.get("metadata") + manifest_metadata = manifest_metadata if isinstance(manifest_metadata, dict) else {} + run_metadata = run_results.get("metadata") + run_metadata = run_metadata if isinstance(run_metadata, dict) else {} + args = run_results.get("args") + args = args if isinstance(args, dict) else {} + elapsed_time = run_results.get("elapsed_time") + + return DbtQualitySummary( + artifact_status="READY" if run_results_path.exists() else "PENDING", + trust_posture=cast(Any, posture), + generated_at=( + str(run_metadata.get("generated_at") or manifest_metadata.get("generated_at")) + if run_metadata.get("generated_at") or manifest_metadata.get("generated_at") + else None + ), + dbt_version=( + str(run_metadata.get("dbt_version") or manifest_metadata.get("dbt_version")) + if run_metadata.get("dbt_version") or manifest_metadata.get("dbt_version") + else None + ), + invocation_id=( + str(run_metadata.get("invocation_id")) if run_metadata.get("invocation_id") else None + ), + invocation_command=( + str(args.get("invocation_command")) if args.get("invocation_command") else None + ), + elapsed_time_ms=( + round(float(elapsed_time) * 1000, 2) + if isinstance(elapsed_time, (int, float)) and elapsed_time >= 0 + else None + ), + test_count=len(checks), + passed_count=counts["PASS"], + warning_count=counts["WARN"], + failed_count=counts["FAIL"], + error_count=counts["ERROR"], + skipped_count=counts["SKIP"], + unknown_count=counts["UNKNOWN"], + source_test_count=sum(check.target_resource_type == "SOURCE" for check in checks), + model_test_count=sum(check.target_resource_type == "MODEL" for check in checks), + checks=checks, + ) diff --git a/apps/web/src/App.jsx b/apps/web/src/App.jsx index 8f47062..ad460ae 100644 --- a/apps/web/src/App.jsx +++ b/apps/web/src/App.jsx @@ -13,6 +13,7 @@ import SchedulesBackfills from './pages/SchedulesBackfills.jsx'; import Transformations from './pages/Transformations.jsx'; import DataModels from './pages/DataModels.jsx'; import SemanticLayer from './pages/SemanticLayer.jsx'; +import DataQuality from './pages/DataQuality.jsx'; import PlaceholderPage from './pages/PlaceholderPage.jsx'; const PAGE_META = { @@ -69,6 +70,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> @@ -85,6 +87,7 @@ function App() { '/orchestration/runs', '/orchestration/backfills', '/delivery/semantic', + '/quality/checks', ].includes(path)) .map(([path, [title, subtitle]]) => ( Development - Phase 6.3 semantic model + metrics + Phase 7.1 dbt quality evidence diff --git a/apps/web/src/pages/DataQuality.jsx b/apps/web/src/pages/DataQuality.jsx new file mode 100644 index 0000000..125d4a8 --- /dev/null +++ b/apps/web/src/pages/DataQuality.jsx @@ -0,0 +1,197 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import StatusPill from '../components/StatusPill.jsx'; +import { getJson } from '../services/api.js'; + +const EMPTY = { + artifact_status: 'MISSING', + trust_posture: 'PENDING', + generated_at: null, + dbt_version: null, + invocation_id: null, + invocation_command: null, + elapsed_time_ms: null, + test_count: 0, + passed_count: 0, + warning_count: 0, + failed_count: 0, + error_count: 0, + skipped_count: 0, + unknown_count: 0, + source_test_count: 0, + model_test_count: 0, + checks: [], +}; + +function generatedLabel(value) { + if (!value) return 'Awaiting dbt test evidence'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString(); +} + +function titleCase(value) { + return String(value || '—').toLowerCase().replaceAll('_', ' ').replace(/\b\w/g, (letter) => letter.toUpperCase()); +} + +function toneForStatus(status) { + if (status === 'PASS' || status === 'TRUSTED') return 'READY'; + if (status === 'WARN' || status === 'DEGRADED' || status === 'PENDING' || status === 'SKIP') return 'WARNING'; + if (status === 'FAIL' || status === 'ERROR' || status === 'BLOCKED') return 'BLOCKED'; + return 'UNKNOWN'; +} + +function DataQuality() { + const [summary, setSummary] = useState(EMPTY); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [dimension, setDimension] = useState('ALL'); + + const load = useCallback(async () => { + setLoading(true); + setError(''); + try { + setSummary(await getJson('/api/v1/quality/dbt/summary')); + } catch (requestError) { + setError(requestError.message); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + const controller = new AbortController(); + getJson('/api/v1/quality/dbt/summary', { signal: controller.signal }) + .then(setSummary) + .catch((requestError) => { + if (requestError.name !== 'AbortError') setError(requestError.message); + }) + .finally(() => { + if (!controller.signal.aborted) setLoading(false); + }); + return () => controller.abort(); + }, []); + + const dimensions = useMemo( + () => ['ALL', ...new Set(summary.checks.map((check) => check.quality_dimension))], + [summary.checks], + ); + const filteredChecks = dimension === 'ALL' + ? summary.checks + : summary.checks.filter((check) => check.quality_dimension === dimension); + + const layerCoverage = useMemo(() => { + const layers = ['SOURCE', 'STAGING', 'INTERMEDIATE', 'MART']; + return layers.map((layer) => { + const checks = summary.checks.filter((check) => check.layer === layer); + return { + layer, + total: checks.length, + passed: checks.filter((check) => check.status === 'PASS').length, + }; + }); + }, [summary.checks]); + + return ( +
+
+
+ PHASE 7.1 · DBT QUALITY EVIDENCE + TRUST POSTURE +

Data Quality

+

+ Turn dbt test artifacts into a readable trust surface: what was checked, where it ran, + which quality dimension it protects, and whether the latest build is safe to consume. +

+
+ +
+ + {error ?
!{error}
: null} + +
+
Trust Posture{summary.trust_posture}{generatedLabel(summary.generated_at)}
+
Checks{summary.test_count}{summary.source_test_count} source · {summary.model_test_count} model
+
Passed{summary.passed_count}Latest dbt build evidence
+
Blocking{summary.failed_count + summary.error_count}{summary.warning_count} warnings · {summary.skipped_count} skipped
+
dbt Core{summary.dbt_version || '—'}{summary.elapsed_time_ms == null ? 'Runtime evidence pending' : `${summary.elapsed_time_ms.toFixed(0)} ms build`}
+
+ +
+
+
QUALITY COVERAGE

Trust by engineering layer

+ +
+
+ {layerCoverage.map((item) => ( +
+ {item.layer} + {item.passed}/{item.total} + {item.total ? 'latest checks passing' : 'no checks declared'} +
+ ))} +
+
+ +
+
+
TEST INVENTORY

Artifact-backed dbt quality checks

+
+ {dimensions.map((item) => ( + + ))} +
+
+
+ + + + {filteredChecks.map((check) => ( + + + + + + + + + + + ))} + {!filteredChecks.length ? : null} + +
CheckDimensionTargetLayerColumnSeverityResultRuntime
{check.name}{check.test_kind} · {check.path}{titleCase(check.quality_dimension)}{check.target_name}{check.target_resource_type}{check.layer}{check.column_name || '—'}{check.severity}{check.execution_time_ms == null ? '—' : `${check.execution_time_ms.toFixed(2)} ms`}
No quality checks match this view.
+
+
+ +
+
+
ACCEPTANCE CONTRACT

What Phase 7.1 proves

7.1
+
    +
  1. 01
    One test-evidence seamManifest definitions and run-results outcomes are joined without duplicating dbt metadata.
  2. +
  3. 02
    Readable trust posturePass, warning, failure, error, skip, and unknown states roll into one consumer-facing posture.
  4. +
  5. 03
    Layer-aware coverageSource, staging, intermediate, and mart checks stay traceable to their owning resource.
  6. +
+
+
+
BOUNDARY

Evidence first; incidents later

✓
+

+ Phase 7.1 is observational. dbt remains the test-definition and execution authority; + Studio projects the latest evidence. Persistent incidents, blocking policies, SLOs, + reconciliation rules, and acknowledgements belong to later Phase 7 slices. +

+
One authorityLatest-run evidenceNo fake incidents
+
+
+
+ ); +} + +export default DataQuality; diff --git a/apps/web/src/pages/StudioOverview.jsx b/apps/web/src/pages/StudioOverview.jsx index 60c6b0f..b599316 100644 --- a/apps/web/src/pages/StudioOverview.jsx +++ b/apps/web/src/pages/StudioOverview.jsx @@ -5,7 +5,7 @@ const FALLBACK = { product: 'SkyData Studio', subtitle: 'Data Engineering Workbench', theme: 'Aurora Foundry', - current_phase: 'Phase 6.3 — dbt Semantic Model and Governed Metric Foundation', + current_phase: 'Phase 7.1 — dbt Quality Evidence and Trust Posture Foundation', boundary: 'SkyData Studio starts after SkyCommand ingestion and publishes governed analytical products for downstream consumers.', capabilities: [], }; @@ -41,7 +41,7 @@ function StudioOverview() {
- AURORA FOUNDRY · PHASE 6.3 + AURORA FOUNDRY · PHASE 7.1

Shape trusted data into analytical products.

{summary.boundary}

@@ -61,7 +61,7 @@ function StudioOverview() {
Platform API{apiState}FastAPI contract service
-
Current Phase6.3governed semantic model + metrics
+
Current Phase7.1dbt quality evidence + trust posture
Pipeline ProofREADYCurated mart materialization proven
OrchestratorAirflow 3Batch and asset workflows
@@ -95,7 +95,7 @@ function StudioOverview() { { code: 'PIPELINE_WORKBENCH', name: 'ETL/ELT Pipeline Workbench', description: 'Versioned post-ingestion pipelines and structured run evidence.', status: 'READY', phase: 4 }, { code: 'AIRFLOW', name: 'Apache Airflow Orchestration', description: 'DAGs, assets, schedules, retries, and backfills.', status: 'FOUNDATION', phase: 5 }, { code: 'DBT', name: 'dbt Transformation Layer', description: 'Tested staging, intermediate, mart, and semantic models.', status: 'FOUNDATION', phase: 6 }, - { code: 'QUALITY_LINEAGE', name: 'Quality and Lineage', description: 'Trust evidence, dependencies, incidents, and impact analysis.', status: 'PLANNED', phase: 7 }, + { code: 'QUALITY_LINEAGE', name: 'Quality and Lineage', description: 'Trust evidence, dependencies, incidents, and impact analysis.', status: 'FOUNDATION', phase: 7 }, { code: 'ANALYTICS_DELIVERY', name: 'Analytics Delivery', description: 'Governed products for SkyWeb Analytics and Power BI.', status: 'PLANNED', phase: 9 }, ]).map((capability) => (
@@ -110,11 +110,11 @@ function StudioOverview() {
-
CURRENT IMPLEMENTATION

dbt semantic model and governed metric foundation

PHASE 6.3
+
CURRENT IMPLEMENTATION

dbt quality evidence and trust posture foundation

PHASE 7.1
    -
  1. 01
    Semantic model contractAnnotate the governed mart with one primary entity and reusable time/categorical dimensions.
  2. -
  3. 02
    Governed metricsDefine average, minimum, maximum, and observation-count metrics beside the dbt model that owns them.
  4. -
  5. 03
    Artifact-backed semantic workbenchProject parsed semantic-model and metric evidence without copying dbt-owned definitions into Studio storage.
  6. +
  7. 01
    Artifact-backed quality evidenceJoin dbt test definitions from manifest.json to latest outcomes from run_results.json.
  8. +
  9. 02
    Trust postureRoll pass, warning, failure, error, skip, and unknown states into one readable consumption signal.
  10. +
  11. 03
    Layer-aware coverageKeep source, staging, intermediate, and mart tests traceable without creating another test metadata store.
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 6bc84d5..404a0c7 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -976,3 +976,36 @@ a { color: inherit; } grid-template-columns: 1fr; } } + +/* Phase 7.1 — dbt quality evidence and trust posture */ +.quality-intro { display: flex; align-items: flex-end; justify-content: space-between; gap: 1.4rem; } +.quality-metric-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); } +.quality-layer-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; } +.quality-layer-grid article { display: grid; gap: 5px; padding: 14px; border: 1px solid rgba(80,231,207,0.14); border-radius: 11px; background: rgba(3,14,22,0.58); } +.quality-layer-grid span { color: var(--muted); font-size: 0.66rem; font-weight: 850; letter-spacing: 0.1em; } +.quality-layer-grid strong { color: var(--mint); font-size: 1.18rem; } +.quality-layer-grid small { color: var(--subtle); } +.quality-table-heading { align-items: flex-end; } +.quality-filter-row { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 6px; } +.quality-filter { border: 1px solid rgba(111,190,187,0.18); border-radius: 999px; padding: 0.42rem 0.62rem; color: var(--muted); background: rgba(4,17,25,0.7); font-size: 0.62rem; font-weight: 800; cursor: pointer; } +.quality-filter:hover, .quality-filter.active { border-color: rgba(80,231,207,0.5); color: var(--mint); background: rgba(66,217,198,0.08); } +.quality-table { width: 100%; min-width: 1120px; border-collapse: collapse; } +.quality-table th { padding: 0.7rem 0.72rem; color: var(--subtle); font-size: 0.62rem; letter-spacing: 0.08em; text-align: left; text-transform: uppercase; border-bottom: 1px solid rgba(111,190,187,0.15); } +.quality-table td { padding: 0.75rem 0.72rem; color: var(--soft); font-size: 0.72rem; vertical-align: top; border-bottom: 1px solid rgba(111,190,187,0.08); } +.quality-table td:first-child { min-width: 280px; } +.quality-table td strong, .quality-table td small { display: block; } +.quality-table td strong { color: var(--text); } +.quality-table td small { max-width: 310px; margin-top: 4px; overflow: hidden; color: var(--muted); font-size: 0.62rem; text-overflow: ellipsis; white-space: nowrap; } +.quality-layer-pill { display: inline-flex; border: 1px solid rgba(154,131,255,0.25); border-radius: 999px; padding: 0.22rem 0.42rem; color: #d8d0ff; background: rgba(154,131,255,0.07); font-size: 0.58rem; font-weight: 850; letter-spacing: 0.06em; } +.quality-empty { padding: 2rem !important; color: var(--muted) !important; text-align: center; } + +@media (max-width: 1180px) { + .quality-metric-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .quality-layer-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 820px) { + .quality-intro, .quality-table-heading { align-items: stretch; flex-direction: column; } + .quality-filter-row { justify-content: flex-start; } + .quality-metric-grid, .quality-layer-grid { grid-template-columns: 1fr; } +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9796211..d877125 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -62,6 +62,25 @@ SkyCommand assets enter the registry as `RAW` assets. Studio-created products ad Phase 6.3 keeps semantic authorship beside the dbt model that owns the analytical grain and supplies MetricFlow with a dedicated DAY-grain calendar time spine. The first contract flows `dbt_mart.fct_fed_funds_rate_daily → fed_funds_rate_daily semantic model → governed metrics → dbt artifacts → Studio Semantic Layer workbench`. SkyData Studio reads generated evidence; it does not copy dbt semantic definitions into Studio PostgreSQL. This phase establishes portable definitions and observability only, leaving hosted semantic-query execution and downstream BI integrations as later delivery boundaries. +### dbt quality evidence boundary + +Phase 7.1 treats dbt test artifacts as the first downstream quality-evidence seam. Test definitions remain in dbt project YAML/SQL and execution remains inside the Dockerized dbt runtime. SkyData Studio joins `manifest.json` definitions to `run_results.json` outcomes and projects the latest trust posture through its API and Data Quality workbench. + +```text +dbt test definitions + │ manifest.json + ├───────────────┐ + │ ▼ +dbt build ─────── run_results.json + │ + ▼ + Studio quality evidence + │ + └── TRUSTED / DEGRADED / BLOCKED / PENDING +``` + +This first quality slice is intentionally observational and non-persistent. Durable incidents, acknowledgements, blocking policies, reconciliation state, and SLO history require Studio-owned runtime evidence and are introduced only in later Phase 7 slices. + ## Product blueprint and mapping boundary Phase 3.2 adds a design-time contract between registered source assets and intended targets. A mapping is metadata, not execution: it records how a pipeline *should* move and shape data before Phase 4 introduces a runnable pipeline engine. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index c989a2d..dd88028 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -437,7 +437,7 @@ Closure evidence: ## Phase 6.3 — dbt Semantic Model and Governed Metric Foundation -**Status:** In progress. +**Status:** Complete. Semantic parsing, MetricFlow time-spine support, artifact projection, validation, and the workbench proof are green. Scope: @@ -459,3 +459,34 @@ Acceptance proof: - the Semantic Layer workbench displays the governed model, primary entity, dimensions, and all four metrics from the generated artifact evidence; - `dbt_mart.fct_fed_funds_rate_daily` retains 26,335 rows, `dbt_mart.time_spine_daily` exists at DAY granularity, and the business-model Data Models workbench remains 3/3 ready because semantic utility models are excluded from that catalogue; - local validation and GitHub checks remain green. + +Closure evidence: + +- `./scripts/dbt.ps1 build` discovers 4 models, 14 data tests, 1 source, 4 metrics, and 1 semantic model, then completes with `PASS=18`, `WARN=0`, `ERROR=0`, and `SKIP=0`; +- the semantic endpoint reports `artifact_status=READY`, 1 semantic model, 4 governed metrics, 1 primary entity, and 4 dimensions; +- the Data Models workbench remains 3/3 READY while the dedicated `time_spine_daily` semantic utility is excluded from the business-model catalogue; +- the Semantic Layer workbench renders the governed model, entity, dimensions, and all four metric definitions from dbt artifacts; +- local validation completes with 57 pytest tests, clean Ruff/mypy, zero npm vulnerabilities, clean ESLint, and a successful Vite production build. + +## Phase 7.1 — dbt Quality Evidence and Trust Posture Foundation + +**Status:** In progress. + +Scope: + +- keep dbt as the single authority for test definitions and execution outcomes; +- join test metadata from `target/manifest.json` with latest statuses, failures, messages, and timings from `target/run_results.json`; +- classify checks into completeness, uniqueness, validity, referential-integrity, business-rule, and other quality dimensions; +- resolve every check to its owning source/model, engineering layer, column when applicable, severity, and latest result; +- compute one latest-run trust posture: `TRUSTED`, `DEGRADED`, `BLOCKED`, or `PENDING`; +- expose the evidence through `GET /api/v1/quality/dbt/summary`; +- replace the Data Quality placeholder with an artifact-backed quality workbench and layer coverage view; +- keep Phase 7.1 observational: durable incidents, blocking policies, reconciliation rules, acknowledgements, and SLOs remain later Phase 7 slices. + +Acceptance proof: + +- the quality API reports `artifact_status=READY`, `trust_posture=TRUSTED`, 14 checks, 14 passes, 0 warnings, 0 failures, 0 errors, and the expected 3 source / 11 model split after the proven dbt build; +- the workbench displays SOURCE 3/3, STAGING 3/3, INTERMEDIATE 3/3, and MART 5/5 latest checks passing; +- generic and singular tests preserve their quality dimension, target, layer, severity, runtime, and failure evidence; +- missing run-results evidence produces `PENDING` rather than pretending the tests are green; +- local validation and GitHub checks remain green. diff --git a/docs/SkyData_RepoMap.md b/docs/SkyData_RepoMap.md index 7eeef1a..f33e8a0 100644 --- a/docs/SkyData_RepoMap.md +++ b/docs/SkyData_RepoMap.md @@ -25,6 +25,7 @@ SkyDataStudio/ │ │ │ ├── pipeline_runs.py │ │ │ ├── pipelines.py │ │ │ ├── platform.py +│ │ │ ├── quality.py │ │ │ └── skycommand.py │ │ ├── core/ │ │ │ ├── __init__.py @@ -55,12 +56,14 @@ SkyDataStudio/ │ │ │ ├── execution.py │ │ │ ├── metadata.py │ │ │ ├── pipelines.py -│ │ │ └── platform.py +│ │ │ ├── platform.py +│ │ │ └── quality.py │ │ └── services/ │ │ ├── __init__.py │ │ ├── asset_detail.py │ │ ├── asset_workspace.py │ │ ├── contract_compatibility.py +│ │ ├── dbt_quality.py │ │ ├── dbt_transformations.py │ │ ├── metadata_registry.py │ │ ├── pipeline_execution.py @@ -84,6 +87,7 @@ SkyDataStudio/ │ │ ├── Airflow.jsx │ │ ├── DataAssets.jsx │ │ ├── DataModels.jsx +│ │ ├── DataQuality.jsx │ │ ├── MetadataRegistry.jsx │ │ ├── PipelineRuns.jsx │ │ ├── Pipelines.jsx @@ -147,6 +151,7 @@ SkyDataStudio/ │ ├── test_pipeline_registry.py │ ├── test_platform.py │ ├── test_quality_contracts.py +│ ├── test_quality.py │ ├── test_skycommand_client.py │ ├── test_skycommand_contracts.py │ └── test_validation_environment.py diff --git a/docs/repository-map.txt b/docs/repository-map.txt index a82bba2..1ce8809 100644 --- a/docs/repository-map.txt +++ b/docs/repository-map.txt @@ -16,6 +16,7 @@ SkyDataStudio │ │ │ │ ├── pipeline_runs.py │ │ │ │ ├── pipelines.py │ │ │ │ ├── platform.py +│ │ │ │ ├── quality.py │ │ │ │ └── skycommand.py │ │ │ ├── __init__.py │ │ │ └── router.py @@ -48,12 +49,14 @@ SkyDataStudio │ │ │ ├── execution.py │ │ │ ├── metadata.py │ │ │ ├── pipelines.py -│ │ │ └── platform.py +│ │ │ ├── platform.py +│ │ │ └── quality.py │ │ ├── services │ │ │ ├── __init__.py │ │ │ ├── asset_detail.py │ │ │ ├── asset_workspace.py │ │ │ ├── contract_compatibility.py +│ │ │ ├── dbt_quality.py │ │ │ ├── dbt_transformations.py │ │ │ ├── metadata_registry.py │ │ │ ├── pipeline_execution.py @@ -71,6 +74,7 @@ SkyDataStudio │ │ │ ├── Airflow.jsx │ │ │ ├── DataAssets.jsx │ │ │ ├── DataModels.jsx +│ │ │ ├── DataQuality.jsx │ │ │ ├── MetadataRegistry.jsx │ │ │ ├── PipelineRuns.jsx │ │ │ ├── Pipelines.jsx @@ -141,6 +145,7 @@ SkyDataStudio │ ├── test_metadata_registry.py │ ├── test_pipeline_registry.py │ ├── test_platform.py +│ ├── test_quality.py │ ├── test_quality_contracts.py │ ├── test_skycommand_client.py │ ├── test_skycommand_contracts.py @@ -157,7 +162,6 @@ SkyDataStudio │ │ │ ├── int_fed_funds_rate_changes.sql │ │ │ └── schema.yml │ │ ├── marts -│ │ │ ├── .gitkeep │ │ │ ├── fct_fed_funds_rate_daily.sql │ │ │ ├── schema.yml │ │ │ └── time_spine_daily.sql diff --git a/tests/test_platform.py b/tests/test_platform.py index 68eab5a..95035e2 100644 --- a/tests/test_platform.py +++ b/tests/test_platform.py @@ -13,7 +13,7 @@ def test_platform_summary_exposes_product_boundary() -> None: assert payload["theme"] == "Aurora Foundry" assert "SkyCommand" in payload["boundary"] assert payload["current_phase"] == ( - "Phase 6.3 — dbt Semantic Model and Governed Metric Foundation" + "Phase 7.1 — dbt Quality Evidence and Trust Posture Foundation" ) assert {item["code"] for item in payload["capabilities"]} >= { "AIRFLOW", diff --git a/tests/test_quality.py b/tests/test_quality.py new file mode 100644 index 0000000..b2c3a8b --- /dev/null +++ b/tests/test_quality.py @@ -0,0 +1,163 @@ +import json +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient +from skydata_studio.api.routes import quality as quality_route +from skydata_studio.main import app +from skydata_studio.schemas.quality import DbtQualitySummary +from skydata_studio.services.dbt_quality import dbt_quality_summary + +client = TestClient(app) + + +def _write_manifest(target_dir: Path) -> None: + manifest = { + "metadata": {"generated_at": "2026-08-11T18:36:32Z", "dbt_version": "1.12.0"}, + "sources": { + "source.skydata_studio.studio_curated.fed_funds_rate": { + "resource_type": "source", + "package_name": "skydata_studio", + "name": "fed_funds_rate", + } + }, + "nodes": { + "model.skydata_studio.fct_fed_funds_rate_daily": { + "resource_type": "model", + "package_name": "skydata_studio", + "name": "fct_fed_funds_rate_daily", + "schema": "dbt_mart", + "original_file_path": "models/marts/fct_fed_funds_rate_daily.sql", + "config": {"enabled": True}, + }, + "test.skydata_studio.source_not_null_rate": { + "resource_type": "test", + "package_name": "skydata_studio", + "name": "source_not_null_rate", + "column_name": "rate", + "test_metadata": {"name": "not_null"}, + "attached_node": "source.skydata_studio.studio_curated.fed_funds_rate", + "depends_on": {"nodes": ["source.skydata_studio.studio_curated.fed_funds_rate"]}, + "config": {"enabled": True, "severity": "ERROR"}, + "original_file_path": "models/staging/sources.yml", + }, + "test.skydata_studio.unique_daily": { + "resource_type": "test", + "package_name": "skydata_studio", + "name": "unique_daily", + "column_name": "observation_date", + "test_metadata": {"name": "unique"}, + "attached_node": "model.skydata_studio.fct_fed_funds_rate_daily", + "depends_on": {"nodes": ["model.skydata_studio.fct_fed_funds_rate_daily"]}, + "config": {"enabled": True, "severity": "ERROR"}, + "original_file_path": "models/marts/schema.yml", + }, + "test.skydata_studio.assert_rate_reasonable": { + "resource_type": "test", + "package_name": "skydata_studio", + "name": "assert_rate_reasonable", + "test_metadata": None, + "depends_on": {"nodes": ["model.skydata_studio.fct_fed_funds_rate_daily"]}, + "config": {"enabled": True, "severity": "ERROR"}, + "original_file_path": "tests/assert_rate_reasonable.sql", + }, + }, + } + (target_dir / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8") + + +def test_dbt_quality_summary_projects_test_evidence_and_trust_posture(tmp_path: Path) -> None: + target_dir = tmp_path / "target" + target_dir.mkdir() + _write_manifest(target_dir) + run_results = { + "metadata": { + "generated_at": "2026-08-11T18:36:33Z", + "dbt_version": "1.12.0", + "invocation_id": "quality-proof", + }, + "elapsed_time": 0.91, + "args": {"invocation_command": "dbt build --profiles-dir ."}, + "results": [ + { + "unique_id": "test.skydata_studio.source_not_null_rate", + "status": "pass", + "failures": 0, + "execution_time": 0.08, + }, + { + "unique_id": "test.skydata_studio.unique_daily", + "status": "pass", + "failures": 0, + "execution_time": 0.09, + }, + { + "unique_id": "test.skydata_studio.assert_rate_reasonable", + "status": "pass", + "failures": 0, + "execution_time": 0.07, + }, + ], + } + (target_dir / "run_results.json").write_text(json.dumps(run_results), encoding="utf-8") + + summary = dbt_quality_summary(target_dir) + + assert summary.artifact_status == "READY" + assert summary.trust_posture == "TRUSTED" + assert summary.test_count == 3 + assert summary.passed_count == 3 + assert summary.source_test_count == 1 + assert summary.model_test_count == 2 + assert {check.quality_dimension for check in summary.checks} == { + "COMPLETENESS", + "UNIQUENESS", + "BUSINESS_RULE", + } + assert summary.checks[0].execution_time_ms is not None + + +def test_dbt_quality_summary_is_pending_without_run_results(tmp_path: Path) -> None: + target_dir = tmp_path / "target" + target_dir.mkdir() + _write_manifest(target_dir) + + summary = dbt_quality_summary(target_dir) + + assert summary.artifact_status == "PENDING" + assert summary.trust_posture == "PENDING" + assert summary.test_count == 3 + assert summary.unknown_count == 3 + assert all(check.status == "UNKNOWN" for check in summary.checks) + + +def test_quality_endpoint_projects_dbt_quality_summary(monkeypatch: pytest.MonkeyPatch) -> None: + expected = DbtQualitySummary( + artifact_status="READY", + trust_posture="TRUSTED", + generated_at="2026-08-11T18:36:33Z", + dbt_version="1.12.0", + invocation_id="quality-proof", + invocation_command="dbt build --profiles-dir .", + elapsed_time_ms=910.0, + test_count=14, + passed_count=14, + warning_count=0, + failed_count=0, + error_count=0, + skipped_count=0, + unknown_count=0, + source_test_count=3, + model_test_count=11, + checks=[], + ) + + monkeypatch.setattr(quality_route, "dbt_quality_summary", lambda: expected) + + response = client.get("/api/v1/quality/dbt/summary") + + assert response.status_code == 200 + payload = response.json() + assert payload["phase"].startswith("Phase 7.1") + assert payload["trust_posture"] == "TRUSTED" + assert payload["passed_count"] == 14 From 7ec80e6502e7df9454b8bc7c4c84c88d8e590063 Mon Sep 17 00:00:00 2001 From: Paul Sattaur Date: Wed, 12 Aug 2026 12:21:19 -0400 Subject: [PATCH 2/2] Phase 7.1.1 - Mypy Quality Status Fix --- apps/api/skydata_studio/services/dbt_quality.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/api/skydata_studio/services/dbt_quality.py b/apps/api/skydata_studio/services/dbt_quality.py index 249e116..7d6dd18 100644 --- a/apps/api/skydata_studio/services/dbt_quality.py +++ b/apps/api/skydata_studio/services/dbt_quality.py @@ -4,6 +4,8 @@ from skydata_studio.schemas.quality import DbtQualityCheckSummary, DbtQualitySummary +QualityStatus = Literal["PASS", "WARN", "FAIL", "ERROR", "SKIP", "UNKNOWN"] + def _default_dbt_target_dir() -> Path: repository_root = Path(__file__).resolve().parents[4] @@ -95,11 +97,9 @@ def _run_result_index(run_results: dict[str, Any]) -> dict[str, dict[str, Any]]: } -def _status(result: dict[str, Any] | None) -> Literal[ - "PASS", "WARN", "FAIL", "ERROR", "SKIP", "UNKNOWN" -]: +def _status(result: dict[str, Any] | None) -> QualityStatus: raw = str(result.get("status") if result else "").lower() - mapping = { + mapping: dict[str, QualityStatus] = { "pass": "PASS", "success": "PASS", "warn": "WARN", @@ -109,7 +109,7 @@ def _status(result: dict[str, Any] | None) -> Literal[ "skipped": "SKIP", "skip": "SKIP", } - return cast(Any, mapping.get(raw, "UNKNOWN")) + return mapping.get(raw, "UNKNOWN") def _severity(test_node: dict[str, Any]) -> Literal["ERROR", "WARN"]: