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..7d6dd18
--- /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
+
+QualityStatus = Literal["PASS", "WARN", "FAIL", "ERROR", "SKIP", "UNKNOWN"]
+
+
+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) -> QualityStatus:
+ raw = str(result.get("status") if result else "").lower()
+ mapping: dict[str, QualityStatus] = {
+ "pass": "PASS",
+ "success": "PASS",
+ "warn": "WARN",
+ "fail": "FAIL",
+ "error": "ERROR",
+ "runtime error": "ERROR",
+ "skipped": "SKIP",
+ "skip": "SKIP",
+ }
+ return 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() {
+ 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. +
+| Check | Dimension | Target | Layer | Column | Severity | Result | Runtime |
|---|---|---|---|---|---|---|---|
| {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. | |||||||
+ 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. +
+{summary.boundary}