diff --git a/README.md b/README.md
index 290220e..8292560 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 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.
+**Current status:** Phases 0 through 6 are complete. **Phase 7.1 closed green** with 14/14 dbt checks passing, a TRUSTED latest-run posture, layer-aware quality evidence, and 60 local tests passing. **Phase 7.2 is now in progress:** source-controlled quality contracts turn latest dbt evidence into explicit consumer gates while the Contracts workbench keeps SkyCommand boundary compatibility visible.
---
@@ -161,6 +161,18 @@ Phase 6.1 materializes the proven `stg_fed_funds_rate → int_fed_funds_rate_cha
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.
+## Phase 7.2 quick start
+
+Phase 7.2 adds the first source-controlled consumer quality gate under `contracts/quality/fed_funds_rate_daily.v1.json`. The policy does not copy dbt test definitions; instead, stable selectors such as target model, quality dimension, test kind, and column are evaluated against the latest Phase 7.1 dbt evidence.
+
+Core endpoint:
+
+```text
+GET /api/v1/quality/contracts/summary
+```
+
+Open `/quality/contracts` to inspect the mart gate and the existing SkyCommand consumer compatibility boundary together. The Federal Funds Rate proof requires five passing MART checks and a 100% pass rate. A missing or non-passing required rule blocks the contract; missing latest run evidence leaves it `PENDING`. Durable incident lifecycle remains a later Phase 7 slice.
+
---
## Validation contract
diff --git a/apps/api/skydata_studio/api/routes/platform.py b/apps/api/skydata_studio/api/routes/platform.py
index b76ad43..53f03ed 100644
--- a/apps/api/skydata_studio/api/routes/platform.py
+++ b/apps/api/skydata_studio/api/routes/platform.py
@@ -83,7 +83,7 @@ def platform_summary() -> PlatformSummary:
product="SkyData Studio",
subtitle="Data Engineering Workbench",
theme="Aurora Foundry",
- current_phase="Phase 7.1 — dbt Quality Evidence and Trust Posture Foundation",
+ current_phase="Phase 7.2 — Quality Contract Gate and Consumer Compatibility Workbench",
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
index f224aa3..a20a94c 100644
--- a/apps/api/skydata_studio/api/routes/quality.py
+++ b/apps/api/skydata_studio/api/routes/quality.py
@@ -1,20 +1,37 @@
from fastapi import APIRouter, HTTPException, status
-from skydata_studio.schemas.quality import DbtQualitySummary
+from skydata_studio.schemas.quality import DbtQualitySummary, QualityContractSummary
from skydata_studio.services.dbt_quality import dbt_quality_summary
+from skydata_studio.services.quality_contracts import quality_contract_summary
router = APIRouter()
+def _artifact_unavailable(message: str) -> HTTPException:
+ return HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail=message,
+ )
+
+
@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."
- ),
+ raise _artifact_unavailable(
+ "SkyData Studio could not read dbt quality artifacts. "
+ "Run .\\scripts\\dbt.ps1 build and refresh Data Quality."
+ ) from error
+
+
+@router.get("/contracts/summary", response_model=QualityContractSummary)
+def quality_contract() -> QualityContractSummary:
+ try:
+ return quality_contract_summary()
+ except (OSError, ValueError, TypeError) as error:
+ raise _artifact_unavailable(
+ "SkyData Studio could not evaluate the quality contract. "
+ "Confirm the source-controlled contract exists, run .\\scripts\\dbt.ps1 build, "
+ "and refresh Contracts."
) from error
diff --git a/apps/api/skydata_studio/schemas/quality.py b/apps/api/skydata_studio/schemas/quality.py
index 89815b7..14ae49b 100644
--- a/apps/api/skydata_studio/schemas/quality.py
+++ b/apps/api/skydata_studio/schemas/quality.py
@@ -2,25 +2,29 @@
from pydantic import BaseModel, Field
+QualityDimension = Literal[
+ "COMPLETENESS",
+ "UNIQUENESS",
+ "VALIDITY",
+ "REFERENTIAL_INTEGRITY",
+ "BUSINESS_RULE",
+ "OTHER",
+]
+QualityLayer = Literal["SOURCE", "STAGING", "INTERMEDIATE", "MART", "UNKNOWN"]
+QualityStatus = Literal["PASS", "WARN", "FAIL", "ERROR", "SKIP", "UNKNOWN"]
+
class DbtQualityCheckSummary(BaseModel):
unique_id: str
name: str
test_kind: Literal["GENERIC", "SINGULAR"]
- quality_dimension: Literal[
- "COMPLETENESS",
- "UNIQUENESS",
- "VALIDITY",
- "REFERENTIAL_INTEGRITY",
- "BUSINESS_RULE",
- "OTHER",
- ]
+ quality_dimension: QualityDimension
target_name: str
target_resource_type: Literal["MODEL", "SOURCE", "UNKNOWN"]
- layer: Literal["SOURCE", "STAGING", "INTERMEDIATE", "MART", "UNKNOWN"]
+ layer: QualityLayer
column_name: str | None = None
severity: Literal["ERROR", "WARN"]
- status: Literal["PASS", "WARN", "FAIL", "ERROR", "SKIP", "UNKNOWN"]
+ status: QualityStatus
failures: int | None = Field(default=None, ge=0)
execution_time_ms: float | None = Field(default=None, ge=0)
message: str | None = None
@@ -47,3 +51,62 @@ class DbtQualitySummary(BaseModel):
source_test_count: int = Field(ge=0)
model_test_count: int = Field(ge=0)
checks: list[DbtQualityCheckSummary]
+
+
+class QualityContractRuleDefinition(BaseModel):
+ code: str
+ label: str
+ quality_dimension: QualityDimension
+ test_kind: Literal["GENERIC", "SINGULAR"]
+ column_name: str | None = None
+ required_status: Literal["PASS"] = "PASS"
+
+
+class QualityContractDefinition(BaseModel):
+ code: str
+ version: str
+ name: str
+ description: str
+ target_name: str
+ layer: Literal["STAGING", "INTERMEDIATE", "MART"]
+ enforcement_mode: Literal["BLOCK", "ADVISORY"]
+ minimum_pass_rate: float = Field(ge=0.0, le=1.0)
+ rules: list[QualityContractRuleDefinition]
+
+
+class QualityContractRuleEvaluation(BaseModel):
+ code: str
+ label: str
+ quality_dimension: QualityDimension
+ test_kind: Literal["GENERIC", "SINGULAR"]
+ column_name: str | None = None
+ required_status: Literal["PASS"] = "PASS"
+ outcome: Literal["PASS", "WARN", "BLOCK", "MISSING", "PENDING"]
+ matched_check_name: str | None = None
+ matched_status: QualityStatus | None = None
+ matched_severity: Literal["ERROR", "WARN"] | None = None
+ message: str
+
+
+class QualityContractSummary(BaseModel):
+ project_name: str = "skydata_studio"
+ phase: str = "Phase 7.2 — Quality Contract Gate and Consumer Compatibility Workbench"
+ contract_code: str
+ contract_version: str
+ contract_name: str
+ description: str
+ target_name: str
+ layer: Literal["STAGING", "INTERMEDIATE", "MART"]
+ enforcement_mode: Literal["BLOCK", "ADVISORY"]
+ artifact_status: Literal["READY", "PENDING", "MISSING"]
+ evidence_trust_posture: Literal["TRUSTED", "DEGRADED", "BLOCKED", "PENDING"]
+ contract_status: Literal["COMPLIANT", "DEGRADED", "BLOCKED", "PENDING"]
+ minimum_pass_rate: float = Field(ge=0.0, le=1.0)
+ pass_rate: float = Field(ge=0.0, le=1.0)
+ required_rule_count: int = Field(ge=0)
+ satisfied_rule_count: int = Field(ge=0)
+ warning_rule_count: int = Field(ge=0)
+ blocking_rule_count: int = Field(ge=0)
+ missing_rule_count: int = Field(ge=0)
+ source_path: str
+ rules: list[QualityContractRuleEvaluation]
diff --git a/apps/api/skydata_studio/services/quality_contracts.py b/apps/api/skydata_studio/services/quality_contracts.py
new file mode 100644
index 0000000..a0e3ecd
--- /dev/null
+++ b/apps/api/skydata_studio/services/quality_contracts.py
@@ -0,0 +1,160 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any, Literal, cast
+
+from skydata_studio.schemas.quality import (
+ DbtQualityCheckSummary,
+ DbtQualitySummary,
+ QualityContractDefinition,
+ QualityContractRuleDefinition,
+ QualityContractRuleEvaluation,
+ QualityContractSummary,
+)
+from skydata_studio.services.dbt_quality import dbt_quality_summary
+
+
+def _repository_root() -> Path:
+ return Path(__file__).resolve().parents[4]
+
+
+def _default_contract_path() -> Path:
+ return _repository_root() / "contracts" / "quality" / "fed_funds_rate_daily.v1.json"
+
+
+def _read_contract(path: Path) -> QualityContractDefinition:
+ with path.open(encoding="utf-8") as handle:
+ payload = json.load(handle)
+ if not isinstance(payload, dict):
+ raise ValueError(f"Expected an object in quality contract {path}.")
+ return QualityContractDefinition.model_validate(cast(dict[str, Any], payload))
+
+
+def _matches_rule(
+ check: DbtQualityCheckSummary,
+ *,
+ contract: QualityContractDefinition,
+ rule: QualityContractRuleDefinition,
+) -> bool:
+ return (
+ check.target_name == contract.target_name
+ and check.layer == contract.layer
+ and check.quality_dimension == rule.quality_dimension
+ and check.test_kind == rule.test_kind
+ and check.column_name == rule.column_name
+ )
+
+
+def _rule_evaluation(
+ *,
+ contract: QualityContractDefinition,
+ rule: QualityContractRuleDefinition,
+ evidence: DbtQualitySummary,
+) -> QualityContractRuleEvaluation:
+ if evidence.artifact_status != "READY":
+ return QualityContractRuleEvaluation(
+ **rule.model_dump(),
+ outcome="PENDING",
+ message="Latest dbt run-result evidence is not ready yet.",
+ )
+
+ matches = [
+ check
+ for check in evidence.checks
+ if _matches_rule(check, contract=contract, rule=rule)
+ ]
+ if not matches:
+ return QualityContractRuleEvaluation(
+ **rule.model_dump(),
+ outcome="MISSING",
+ message="No latest dbt quality check satisfies this contract selector.",
+ )
+
+ check = matches[0]
+ if check.status == "PASS":
+ outcome: Literal["PASS", "WARN", "BLOCK"] = "PASS"
+ message = "Required dbt quality evidence is present and passing."
+ elif check.status == "WARN":
+ outcome = "WARN"
+ message = "Required evidence is present but the latest dbt result is warning."
+ else:
+ outcome = "BLOCK"
+ message = f"Required evidence is present but the latest dbt result is {check.status}."
+
+ if len(matches) > 1:
+ outcome = "BLOCK"
+ message = "More than one dbt check matched this contract selector; tighten the rule."
+
+ return QualityContractRuleEvaluation(
+ **rule.model_dump(),
+ outcome=outcome,
+ matched_check_name=check.name,
+ matched_status=check.status,
+ matched_severity=check.severity,
+ message=message,
+ )
+
+
+def evaluate_quality_contract(
+ contract: QualityContractDefinition,
+ evidence: DbtQualitySummary,
+ *,
+ source_path: str,
+) -> QualityContractSummary:
+ rules = [
+ _rule_evaluation(contract=contract, rule=rule, evidence=evidence)
+ for rule in contract.rules
+ ]
+ required = len(rules)
+ satisfied = sum(rule.outcome == "PASS" for rule in rules)
+ warnings = sum(rule.outcome == "WARN" for rule in rules)
+ missing = sum(rule.outcome == "MISSING" for rule in rules)
+ blocking = sum(rule.outcome in {"BLOCK", "MISSING"} for rule in rules)
+ pass_rate = satisfied / required if required else 1.0
+
+ if evidence.artifact_status != "READY":
+ contract_status: Literal["COMPLIANT", "DEGRADED", "BLOCKED", "PENDING"] = "PENDING"
+ elif blocking or pass_rate < contract.minimum_pass_rate:
+ contract_status = "BLOCKED" if contract.enforcement_mode == "BLOCK" else "DEGRADED"
+ elif warnings:
+ contract_status = "DEGRADED"
+ else:
+ contract_status = "COMPLIANT"
+
+ return QualityContractSummary(
+ contract_code=contract.code,
+ contract_version=contract.version,
+ contract_name=contract.name,
+ description=contract.description,
+ target_name=contract.target_name,
+ layer=contract.layer,
+ enforcement_mode=contract.enforcement_mode,
+ artifact_status=evidence.artifact_status,
+ evidence_trust_posture=evidence.trust_posture,
+ contract_status=contract_status,
+ minimum_pass_rate=contract.minimum_pass_rate,
+ pass_rate=pass_rate,
+ required_rule_count=required,
+ satisfied_rule_count=satisfied,
+ warning_rule_count=warnings,
+ blocking_rule_count=blocking,
+ missing_rule_count=missing,
+ source_path=source_path,
+ rules=rules,
+ )
+
+
+def quality_contract_summary(
+ *,
+ contract_path: Path | None = None,
+ target_dir: Path | None = None,
+) -> QualityContractSummary:
+ path = contract_path or _default_contract_path()
+ contract = _read_contract(path)
+ evidence = dbt_quality_summary(target_dir)
+ try:
+ source_path = path.relative_to(_repository_root()).as_posix()
+ except ValueError:
+ source_path = path.as_posix()
+ return evaluate_quality_contract(contract, evidence, source_path=source_path)
diff --git a/apps/web/src/App.jsx b/apps/web/src/App.jsx
index ad460ae..404d8cd 100644
--- a/apps/web/src/App.jsx
+++ b/apps/web/src/App.jsx
@@ -14,6 +14,7 @@ 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 DataContracts from './pages/DataContracts.jsx';
import PlaceholderPage from './pages/PlaceholderPage.jsx';
const PAGE_META = {
@@ -28,7 +29,7 @@ const PAGE_META = {
'/orchestration/runs': ['Pipeline Runs', 'Durable batch execution evidence'],
'/orchestration/backfills': ['Schedules & Backfills', 'Time, asset, and replay controls'],
'/quality/checks': ['Data Quality', 'Tests, incidents, policies, and trust evidence'],
- '/quality/contracts': ['Contracts', 'Schema and consumer compatibility'],
+ '/quality/contracts': ['Contracts', 'Quality gates and consumer compatibility'],
'/quality/lineage': ['Lineage', 'Upstream, downstream, and change impact'],
'/delivery/marts': ['Analytical Marts', 'Curated consumer-ready facts and dimensions'],
'/delivery/semantic': ['Semantic Layer', 'Governed metrics and business definitions'],
@@ -71,6 +72,7 @@ function App() {
} />
} />
} />
+ } />
} />
} />
} />
@@ -88,6 +90,7 @@ function App() {
'/orchestration/backfills',
'/delivery/semantic',
'/quality/checks',
+ '/quality/contracts',
].includes(path))
.map(([path, [title, subtitle]]) => (
Development
- Phase 7.1 dbt quality evidence
+ Phase 7.2 quality contract gates
diff --git a/apps/web/src/pages/DataContracts.jsx b/apps/web/src/pages/DataContracts.jsx
new file mode 100644
index 0000000..4322491
--- /dev/null
+++ b/apps/web/src/pages/DataContracts.jsx
@@ -0,0 +1,201 @@
+import { useCallback, useEffect, useState } from 'react';
+import StatusPill from '../components/StatusPill.jsx';
+import { getJson } from '../services/api.js';
+
+const EMPTY_QUALITY_CONTRACT = {
+ contract_code: 'FED_FUNDS_RATE_DAILY_QUALITY',
+ contract_version: '—',
+ contract_name: 'Federal Funds Rate Daily Quality Contract',
+ description: 'Awaiting source-controlled quality contract evidence.',
+ target_name: 'fct_fed_funds_rate_daily',
+ layer: 'MART',
+ enforcement_mode: 'BLOCK',
+ artifact_status: 'MISSING',
+ evidence_trust_posture: 'PENDING',
+ contract_status: 'PENDING',
+ minimum_pass_rate: 1,
+ pass_rate: 0,
+ required_rule_count: 0,
+ satisfied_rule_count: 0,
+ warning_rule_count: 0,
+ blocking_rule_count: 0,
+ missing_rule_count: 0,
+ source_path: 'contracts/quality/fed_funds_rate_daily.v1.json',
+ rules: [],
+};
+
+const EMPTY_COMPATIBILITY = {
+ mode: 'PREVIEW',
+ status: 'DEGRADED',
+ compatible: 0,
+ incompatible: 0,
+ missing: 0,
+ items: [],
+};
+
+function tone(status) {
+ if (['COMPLIANT', 'COMPATIBLE', 'PASS', 'TRUSTED', 'READY'].includes(status)) return 'READY';
+ if (['DEGRADED', 'WARN', 'PENDING'].includes(status)) return 'WARNING';
+ if (['BLOCKED', 'BLOCK', 'MISSING', 'INCOMPATIBLE', 'FAIL', 'ERROR'].includes(status)) return 'BLOCKED';
+ return 'UNKNOWN';
+}
+
+function outcomeLabel(rule) {
+ if (rule.outcome === 'PASS') return 'Required evidence satisfied';
+ if (rule.outcome === 'WARN') return 'Evidence is warning';
+ if (rule.outcome === 'MISSING') return 'Required check is missing';
+ if (rule.outcome === 'BLOCK') return 'Latest evidence blocks consumption';
+ return 'Waiting for latest dbt evidence';
+}
+
+function DataContracts() {
+ const [qualityContract, setQualityContract] = useState(EMPTY_QUALITY_CONTRACT);
+ const [compatibility, setCompatibility] = useState(EMPTY_COMPATIBILITY);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState('');
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setError('');
+ try {
+ const [qualityPayload, compatibilityPayload] = await Promise.all([
+ getJson('/api/v1/quality/contracts/summary'),
+ getJson('/api/v1/integrations/skycommand/contracts/compatibility'),
+ ]);
+ setQualityContract(qualityPayload);
+ setCompatibility(compatibilityPayload);
+ } catch (requestError) {
+ setError(requestError.message);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ Promise.all([
+ getJson('/api/v1/quality/contracts/summary', { signal: controller.signal }),
+ getJson('/api/v1/integrations/skycommand/contracts/compatibility', { signal: controller.signal }),
+ ])
+ .then(([qualityPayload, compatibilityPayload]) => {
+ setQualityContract(qualityPayload);
+ setCompatibility(compatibilityPayload);
+ })
+ .catch((requestError) => {
+ if (requestError.name !== 'AbortError') setError(requestError.message);
+ })
+ .finally(() => {
+ if (!controller.signal.aborted) setLoading(false);
+ });
+ return () => controller.abort();
+ }, []);
+
+ const compatibilityTotal = compatibility.compatible + compatibility.incompatible + compatibility.missing;
+
+ return (
+
+
+
+
PHASE 7.2 · QUALITY CONTRACT GATE + CONSUMER COMPATIBILITY
+
Data Contracts
+
+ Keep the rules that decide whether data is safe to consume in source control, then
+ evaluate them against dbt evidence while preserving the existing versioned SkyCommand
+ consumer boundary.
+
+
+ load()} disabled={loading}>
+ {loading ? 'Refreshing…' : 'Refresh Contracts'}
+
+
+
+ {error ?
! {error}
: null}
+
+
+ Quality Gate {qualityContract.contract_status} {qualityContract.satisfied_rule_count}/{qualityContract.required_rule_count} required rules satisfied
+ Pass Rate {Math.round(qualityContract.pass_rate * 100)}% Minimum {Math.round(qualityContract.minimum_pass_rate * 100)}%
+ Consumer Contracts {compatibility.compatible}/{compatibilityTotal || 0} {compatibility.mode} compatibility evidence
+ Blocking Rules {qualityContract.blocking_rule_count} {qualityContract.missing_rule_count} missing · {qualityContract.warning_rule_count} warnings
+ Evidence {qualityContract.artifact_status} {qualityContract.evidence_trust_posture} dbt posture
+
+
+
+
+
QUALITY CONTRACT
{qualityContract.contract_name}
+
+
+
+
Contract {qualityContract.contract_code} v{qualityContract.contract_version}
+
Target {qualityContract.target_name} {qualityContract.layer} engineering layer
+
Enforcement {qualityContract.enforcement_mode} 100% evidence required for this proof
+
Source {qualityContract.source_path} Versioned with the repository
+
+ {qualityContract.description}
+
+
+
+
+
REQUIRED EVIDENCE
Five rules turn latest dbt tests into a consumer gate
+
{qualityContract.required_rule_count} rules
+
+
+ {qualityContract.rules.map((rule) => (
+
+
+
+
Selector {rule.test_kind}{rule.column_name ? ` · ${rule.column_name}` : ''}
+
Required {rule.required_status}
+
Matched Check {rule.matched_check_name || '—'}
+
Latest Result {rule.matched_status || '—'}{rule.matched_severity ? ` · ${rule.matched_severity}` : ''}
+
+ {outcomeLabel(rule)}
+
+ ))}
+ {!qualityContract.rules.length ?
No quality contract rules are available yet.
: null}
+
+
+
+
+
+
SKYCOMMAND CONSUMER BOUNDARY
Versioned read-only contracts remain compatible
+
+
+
+ {compatibility.items.map((item) => (
+
+
+ Expected {item.expected_version}
+ Observed {item.observed_version || '—'}
+ {item.message}
+
+ ))}
+ {!compatibility.items.length ?
Consumer compatibility evidence is unavailable.
: null}
+
+
+
+
+
+ ACCEPTANCE CONTRACT
What Phase 7.2 proves 7.2
+
+ 01 Policy is source-controlled The consumer gate is versioned independently from dbt-generated test identifiers.
+ 02 Evidence decides readiness Required selectors must resolve to passing latest-run dbt checks before the mart is compliant.
+ 03 Boundaries stay visible SkyCommand consumer compatibility and Studio quality policy share one contract workbench without sharing ownership.
+
+
+
+ BOUNDARY
Gate now; incidents next ▤
+
+ Phase 7.2 evaluates policy but does not persist incident lifecycle. Durable incidents,
+ acknowledgement, remediation ownership, and historical SLO evidence remain later slices.
+
+ Code as contract Artifact evidence No duplicated dbt authority
+
+
+
+ );
+}
+
+export default DataContracts;
diff --git a/apps/web/src/pages/StudioOverview.jsx b/apps/web/src/pages/StudioOverview.jsx
index b599316..18ef598 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 7.1 — dbt Quality Evidence and Trust Posture Foundation',
+ current_phase: 'Phase 7.2 — Quality Contract Gate and Consumer Compatibility Workbench',
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 7.1
+
AURORA FOUNDRY · PHASE 7.2
Shape trusted data into analytical products.
{summary.boundary}
@@ -61,7 +61,7 @@ function StudioOverview() {
Platform API {apiState} FastAPI contract service
- Current Phase 7.1 dbt quality evidence + trust posture
+ Current Phase 7.2 quality contract gates + compatibility
Pipeline Proof READY Curated mart materialization proven
Orchestrator Airflow 3 Batch and asset workflows
@@ -110,11 +110,11 @@ function StudioOverview() {
- CURRENT IMPLEMENTATION
dbt quality evidence and trust posture foundation PHASE 7.1
+ CURRENT IMPLEMENTATION
quality contract gate and consumer compatibility workbench PHASE 7.2
- 01 Artifact-backed quality evidence Join dbt test definitions from manifest.json to latest outcomes from run_results.json.
- 02 Trust posture Roll pass, warning, failure, error, skip, and unknown states into one readable consumption signal.
- 03 Layer-aware coverage Keep source, staging, intermediate, and mart tests traceable without creating another test metadata store.
+ 01 Source-controlled quality gate Declare required evidence selectors beside the repository instead of copying dbt metadata into another store.
+ 02 Consumer readiness Evaluate the latest dbt quality evidence against a versioned mart contract before presenting it as compliant.
+ 03 Compatibility in one place Keep SkyCommand consumer contracts and Studio-owned quality policy visible on the same workbench.
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css
index 404a0c7..49f763b 100644
--- a/apps/web/src/styles.css
+++ b/apps/web/src/styles.css
@@ -1009,3 +1009,41 @@ a { color: inherit; }
.quality-filter-row { justify-content: flex-start; }
.quality-metric-grid, .quality-layer-grid { grid-template-columns: 1fr; }
}
+
+/* Phase 7.2 — quality contract gate and consumer compatibility */
+.contracts-intro { display: flex; align-items: flex-end; justify-content: space-between; gap: 1.4rem; }
+.contracts-metric-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); }
+.contract-definition-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
+.contract-definition-grid article { display: grid; gap: 5px; padding: 13px; border: 1px solid rgba(80,231,207,0.14); border-radius: 11px; background: rgba(3,14,22,0.58); }
+.contract-definition-grid span, .contract-rule-grid dt { color: var(--muted); font-size: 0.62rem; font-weight: 850; letter-spacing: 0.09em; text-transform: uppercase; }
+.contract-definition-grid strong { color: var(--mint); font-size: 0.78rem; overflow-wrap: anywhere; }
+.contract-definition-grid small { color: var(--subtle); font-size: 0.64rem; }
+.contract-description { margin: 1rem 0 0; color: var(--soft); font-size: 0.72rem; line-height: 1.55; }
+.contract-rule-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
+.contract-rule-grid > article { display: grid; gap: 12px; padding: 15px; border: 1px solid rgba(154,131,255,0.18); border-radius: 12px; background: linear-gradient(145deg, rgba(5,17,25,0.88), rgba(17,16,38,0.56)); }
+.contract-rule-grid header, .consumer-contract-grid header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
+.contract-rule-grid header > div { display: grid; gap: 4px; }
+.contract-rule-grid header span { color: var(--subtle); font-size: 0.58rem; font-weight: 850; letter-spacing: 0.08em; }
+.contract-rule-grid header strong { color: var(--text); font-size: 0.86rem; }
+.contract-rule-grid dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 0; }
+.contract-rule-grid dl > div { display: grid; gap: 4px; padding: 9px 10px; border-radius: 8px; background: rgba(2,13,21,0.55); }
+.contract-rule-grid dd { margin: 0; color: var(--soft); font-size: 0.68rem; overflow-wrap: anywhere; }
+.contract-rule-grid p { margin: 0; color: var(--muted); font-size: 0.68rem; line-height: 1.5; }
+.consumer-contract-grid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
+.consumer-contract-grid > article { display: grid; gap: 7px; padding: 13px; border: 1px solid rgba(111,190,187,0.13); border-radius: 11px; background: rgba(3,14,22,0.58); }
+.consumer-contract-grid header strong { color: var(--text); font-size: 0.72rem; overflow-wrap: anywhere; }
+.consumer-contract-grid span, .consumer-contract-grid small { color: var(--muted); font-size: 0.62rem; overflow-wrap: anywhere; }
+.consumer-contract-grid p { margin: 0; color: var(--subtle); font-size: 0.64rem; line-height: 1.5; }
+.status-compliant, .status-compatible { color: var(--mint); background: rgba(66,217,198,0.08); }
+.status-block, .status-incompatible { color: #ff9e9e; background: rgba(255,99,99,0.08); }
+
+@media (max-width: 1180px) {
+ .contracts-metric-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+ .contract-definition-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+ .consumer-contract-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+}
+
+@media (max-width: 820px) {
+ .contracts-intro { align-items: stretch; flex-direction: column; }
+ .contracts-metric-grid, .contract-definition-grid, .contract-rule-grid, .consumer-contract-grid { grid-template-columns: 1fr; }
+}
diff --git a/contracts/quality/README.md b/contracts/quality/README.md
new file mode 100644
index 0000000..31e0194
--- /dev/null
+++ b/contracts/quality/README.md
@@ -0,0 +1,17 @@
+# Studio Quality Contracts
+
+Phase 7.2 introduces source-controlled quality gates without copying dbt test definitions into a second metadata store.
+
+A quality contract declares the evidence a downstream consumer requires from the latest dbt build. Rule selectors reference stable evidence attributes such as target model, quality dimension, test kind, and column rather than dbt-generated test unique IDs.
+
+## First proof contract
+
+`fed_funds_rate_daily.v1.json` protects the governed `fct_fed_funds_rate_daily` mart with five required checks:
+
+- observation date completeness;
+- rate completeness;
+- observation date uniqueness;
+- rate-direction validity;
+- the singular Federal Funds Rate reasonableness rule.
+
+The contract uses `BLOCK` enforcement and requires a 100% pass rate. The contract definition is versioned in source control; latest execution evidence still comes from dbt `manifest.json` plus `run_results.json`.
diff --git a/contracts/quality/fed_funds_rate_daily.v1.json b/contracts/quality/fed_funds_rate_daily.v1.json
new file mode 100644
index 0000000..7e1f6d4
--- /dev/null
+++ b/contracts/quality/fed_funds_rate_daily.v1.json
@@ -0,0 +1,52 @@
+{
+ "code": "FED_FUNDS_RATE_DAILY_QUALITY",
+ "version": "1.0.0",
+ "name": "Federal Funds Rate Daily Quality Contract",
+ "description": "Source-controlled consumer gate for the governed Federal Funds Rate daily mart.",
+ "target_name": "fct_fed_funds_rate_daily",
+ "layer": "MART",
+ "enforcement_mode": "BLOCK",
+ "minimum_pass_rate": 1.0,
+ "rules": [
+ {
+ "code": "OBSERVATION_DATE_REQUIRED",
+ "label": "Observation date is present",
+ "quality_dimension": "COMPLETENESS",
+ "test_kind": "GENERIC",
+ "column_name": "observation_date",
+ "required_status": "PASS"
+ },
+ {
+ "code": "RATE_REQUIRED",
+ "label": "Rate is present",
+ "quality_dimension": "COMPLETENESS",
+ "test_kind": "GENERIC",
+ "column_name": "rate",
+ "required_status": "PASS"
+ },
+ {
+ "code": "OBSERVATION_DATE_UNIQUE",
+ "label": "Observation date remains unique",
+ "quality_dimension": "UNIQUENESS",
+ "test_kind": "GENERIC",
+ "column_name": "observation_date",
+ "required_status": "PASS"
+ },
+ {
+ "code": "RATE_DIRECTION_VALID",
+ "label": "Rate direction stays in the governed vocabulary",
+ "quality_dimension": "VALIDITY",
+ "test_kind": "GENERIC",
+ "column_name": "rate_direction",
+ "required_status": "PASS"
+ },
+ {
+ "code": "RATE_REASONABLE",
+ "label": "Federal Funds Rate remains within the business-rule range",
+ "quality_dimension": "BUSINESS_RULE",
+ "test_kind": "SINGULAR",
+ "column_name": null,
+ "required_status": "PASS"
+ }
+ ]
+}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index d877125..493a108 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -79,7 +79,23 @@ dbt build ─────── run_results.json
└── 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.
+Phase 7.1 is intentionally observational and non-persistent. Phase 7.2 adds policy without adding a second test authority: a small source-controlled contract selects the dbt evidence that a downstream consumer requires.
+
+### Quality contract gate boundary
+
+```text
+contracts/quality/*.json
+ │ stable selectors
+ ▼
+Studio quality-contract evaluator ◀── Phase 7.1 latest dbt evidence
+ │
+ ├── COMPLIANT
+ ├── DEGRADED
+ ├── BLOCKED
+ └── PENDING
+```
+
+The contract is versioned with application code and references target model, quality dimension, test kind, and column instead of dbt-generated test unique IDs. dbt still owns test definitions and execution. SkyCommand still owns its separate ingestion consumer contracts. The `/quality/contracts` workbench presents both boundaries together for visibility without collapsing their ownership. Durable incidents, acknowledgements, remediation ownership, reconciliation state, and SLO history require Studio-owned runtime evidence and remain later Phase 7 slices.
## Product blueprint and mapping boundary
diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md
index dd88028..afc8556 100644
--- a/docs/ROADMAP.md
+++ b/docs/ROADMAP.md
@@ -470,7 +470,7 @@ Closure evidence:
## Phase 7.1 — dbt Quality Evidence and Trust Posture Foundation
-**Status:** In progress.
+**Status:** Complete. Artifact projection, trust posture, workbench proof, strict typing, and local validation are green.
Scope:
@@ -483,10 +483,32 @@ Scope:
- 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.
+Closure evidence:
+
+- 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;
+- the Data Quality workbench shows TRUSTED with 14 checks, 14 passes, 0 blocking results, and SOURCE 3/3, STAGING 3/3, INTERMEDIATE 3/3, MART 5/5 coverage;
+- generic and singular test rows preserve quality dimension, target, layer, column, severity, runtime, path, and result evidence;
+- strict mypy typing for normalized quality status is green after replacing an `Any` return with a typed `QualityStatus` lookup;
+- Ruff import organization, mypy, 60 pytest tests, npm audit, ESLint, and Vite production build all pass in the final local validation.
+
+## Phase 7.2 — Quality Contract Gate and Consumer Compatibility Workbench
+
+**Status:** In progress.
+
+Scope:
+
+- add a source-controlled quality-contract boundary under `contracts/quality` rather than persisting duplicate dbt test metadata;
+- define the first governed mart contract for `fct_fed_funds_rate_daily` with stable selectors for completeness, uniqueness, validity, and business-rule evidence;
+- require a 100% pass rate across five consumer-critical MART rules using `BLOCK` enforcement;
+- evaluate contract selectors against the latest Phase 7.1 dbt quality evidence and classify the gate as `COMPLIANT`, `DEGRADED`, `BLOCKED`, or `PENDING`;
+- expose the evaluation through `GET /api/v1/quality/contracts/summary`;
+- replace the Contracts placeholder with a workbench that shows both Studio-owned quality policy and existing SkyCommand consumer contract compatibility;
+- keep durable incidents, acknowledgement/remediation ownership, historical SLOs, and reconciliation state outside this slice.
+
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.
+- the quality-contract API reports the Federal Funds Rate contract as `COMPLIANT`, with 5/5 required rules satisfied and a 100% pass rate against the current 14/14 dbt quality evidence;
+- the five rules resolve to MART observation-date completeness, rate completeness, observation-date uniqueness, rate-direction validity, and the singular rate-reasonableness assertion;
+- the Contracts workbench displays the quality gate plus all five SkyCommand consumer boundary contracts without merging ownership between the two systems;
+- missing latest dbt evidence returns `PENDING`, while a missing or non-passing required selector blocks the contract under `BLOCK` enforcement;
+- local validation remains green with 64 pytest tests plus Ruff, mypy, npm audit, ESLint, and Vite build.
diff --git a/docs/SkyData_RepoMap.md b/docs/SkyData_RepoMap.md
index f33e8a0..eb6407f 100644
--- a/docs/SkyData_RepoMap.md
+++ b/docs/SkyData_RepoMap.md
@@ -67,7 +67,8 @@ SkyDataStudio/
│ │ ├── dbt_transformations.py
│ │ ├── metadata_registry.py
│ │ ├── pipeline_execution.py
-│ │ └── pipeline_registry.py
+│ │ ├── pipeline_registry.py
+│ │ └── quality_contracts.py
│ └── web/
│ ├── eslint.config.js
│ ├── index.html
@@ -86,6 +87,7 @@ SkyDataStudio/
│ ├── pages/
│ │ ├── Airflow.jsx
│ │ ├── DataAssets.jsx
+│ │ ├── DataContracts.jsx
│ │ ├── DataModels.jsx
│ │ ├── DataQuality.jsx
│ │ ├── MetadataRegistry.jsx
@@ -100,6 +102,9 @@ SkyDataStudio/
│ └── services/
│ └── api.js
├── contracts/
+│ ├── quality/
+│ │ ├── fed_funds_rate_daily.v1.json
+│ │ └── README.md
│ └── skycommand/
│ └── README.md
├── docs/
@@ -151,6 +156,7 @@ SkyDataStudio/
│ ├── test_pipeline_registry.py
│ ├── test_platform.py
│ ├── test_quality_contracts.py
+│ ├── test_quality_gate_contracts.py
│ ├── test_quality.py
│ ├── test_skycommand_client.py
│ ├── test_skycommand_contracts.py
diff --git a/docs/repository-map.txt b/docs/repository-map.txt
index 1ce8809..04c68da 100644
--- a/docs/repository-map.txt
+++ b/docs/repository-map.txt
@@ -60,7 +60,8 @@ SkyDataStudio
│ │ │ ├── dbt_transformations.py
│ │ │ ├── metadata_registry.py
│ │ │ ├── pipeline_execution.py
-│ │ │ └── pipeline_registry.py
+│ │ │ ├── pipeline_registry.py
+│ │ │ └── quality_contracts.py
│ │ ├── __init__.py
│ │ └── main.py
│ └── web
@@ -73,6 +74,7 @@ SkyDataStudio
│ │ ├── pages
│ │ │ ├── Airflow.jsx
│ │ │ ├── DataAssets.jsx
+│ │ │ ├── DataContracts.jsx
│ │ │ ├── DataModels.jsx
│ │ │ ├── DataQuality.jsx
│ │ │ ├── MetadataRegistry.jsx
@@ -95,6 +97,9 @@ SkyDataStudio
│ ├── package.json
│ └── vite.config.js
├── contracts
+│ ├── quality
+│ │ ├── fed_funds_rate_daily.v1.json
+│ │ └── README.md
│ └── skycommand
│ └── README.md
├── docs
@@ -147,6 +152,7 @@ SkyDataStudio
│ ├── test_platform.py
│ ├── test_quality.py
│ ├── test_quality_contracts.py
+│ ├── test_quality_gate_contracts.py
│ ├── test_skycommand_client.py
│ ├── test_skycommand_contracts.py
│ └── test_validation_environment.py
diff --git a/tests/test_platform.py b/tests/test_platform.py
index 95035e2..8035d9b 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 7.1 — dbt Quality Evidence and Trust Posture Foundation"
+ "Phase 7.2 — Quality Contract Gate and Consumer Compatibility Workbench"
)
assert {item["code"] for item in payload["capabilities"]} >= {
"AIRFLOW",
diff --git a/tests/test_quality_gate_contracts.py b/tests/test_quality_gate_contracts.py
new file mode 100644
index 0000000..0200562
--- /dev/null
+++ b/tests/test_quality_gate_contracts.py
@@ -0,0 +1,189 @@
+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 (
+ DbtQualityCheckSummary,
+ DbtQualitySummary,
+ QualityContractDefinition,
+ QualityContractRuleDefinition,
+ QualityContractSummary,
+)
+from skydata_studio.services.quality_contracts import evaluate_quality_contract
+
+client = TestClient(app)
+
+
+def _contract() -> QualityContractDefinition:
+ return QualityContractDefinition(
+ code="FED_FUNDS_RATE_DAILY_QUALITY",
+ version="1.0.0",
+ name="Federal Funds Rate Daily Quality Contract",
+ description="Proof contract.",
+ target_name="fct_fed_funds_rate_daily",
+ layer="MART",
+ enforcement_mode="BLOCK",
+ minimum_pass_rate=1.0,
+ rules=[
+ QualityContractRuleDefinition(
+ code="RATE_REQUIRED",
+ label="Rate is present",
+ quality_dimension="COMPLETENESS",
+ test_kind="GENERIC",
+ column_name="rate",
+ ),
+ QualityContractRuleDefinition(
+ code="RATE_REASONABLE",
+ label="Rate remains reasonable",
+ quality_dimension="BUSINESS_RULE",
+ test_kind="SINGULAR",
+ ),
+ ],
+ )
+
+
+def _check(
+ *,
+ name: str,
+ dimension: str,
+ test_kind: str,
+ column_name: str | None,
+ status: str = "PASS",
+) -> DbtQualityCheckSummary:
+ return DbtQualityCheckSummary.model_validate(
+ {
+ "unique_id": f"test.skydata_studio.{name}",
+ "name": name,
+ "test_kind": test_kind,
+ "quality_dimension": dimension,
+ "target_name": "fct_fed_funds_rate_daily",
+ "target_resource_type": "MODEL",
+ "layer": "MART",
+ "column_name": column_name,
+ "severity": "ERROR",
+ "status": status,
+ "failures": 0 if status == "PASS" else 1,
+ "execution_time_ms": 75.0,
+ "message": None,
+ "path": "models/marts/schema.yml",
+ }
+ )
+
+
+def _evidence(checks: list[DbtQualityCheckSummary]) -> DbtQualitySummary:
+ return DbtQualitySummary(
+ artifact_status="READY",
+ trust_posture="TRUSTED",
+ test_count=len(checks),
+ passed_count=sum(check.status == "PASS" for check in checks),
+ warning_count=sum(check.status == "WARN" for check in checks),
+ failed_count=sum(check.status == "FAIL" for check in checks),
+ error_count=sum(check.status == "ERROR" for check in checks),
+ skipped_count=sum(check.status == "SKIP" for check in checks),
+ unknown_count=sum(check.status == "UNKNOWN" for check in checks),
+ source_test_count=0,
+ model_test_count=len(checks),
+ checks=checks,
+ )
+
+
+def test_quality_contract_is_compliant_when_required_evidence_passes() -> None:
+ evidence = _evidence(
+ [
+ _check(
+ name="not_null_daily_rate",
+ dimension="COMPLETENESS",
+ test_kind="GENERIC",
+ column_name="rate",
+ ),
+ _check(
+ name="assert_rate_reasonable",
+ dimension="BUSINESS_RULE",
+ test_kind="SINGULAR",
+ column_name=None,
+ ),
+ ]
+ )
+
+ summary = evaluate_quality_contract(_contract(), evidence, source_path="proof.json")
+
+ assert summary.contract_status == "COMPLIANT"
+ assert summary.satisfied_rule_count == 2
+ assert summary.blocking_rule_count == 0
+ assert summary.pass_rate == 1.0
+ assert all(rule.outcome == "PASS" for rule in summary.rules)
+
+
+def test_quality_contract_blocks_when_required_rule_is_missing() -> None:
+ evidence = _evidence(
+ [
+ _check(
+ name="not_null_daily_rate",
+ dimension="COMPLETENESS",
+ test_kind="GENERIC",
+ column_name="rate",
+ )
+ ]
+ )
+
+ summary = evaluate_quality_contract(_contract(), evidence, source_path="proof.json")
+
+ assert summary.contract_status == "BLOCKED"
+ assert summary.missing_rule_count == 1
+ assert summary.blocking_rule_count == 1
+ assert summary.pass_rate == 0.5
+
+
+def test_quality_contract_is_pending_without_latest_run_evidence() -> None:
+ evidence = DbtQualitySummary(
+ artifact_status="PENDING",
+ trust_posture="PENDING",
+ test_count=2,
+ passed_count=0,
+ warning_count=0,
+ failed_count=0,
+ error_count=0,
+ skipped_count=0,
+ unknown_count=2,
+ source_test_count=0,
+ model_test_count=2,
+ checks=[],
+ )
+
+ summary = evaluate_quality_contract(_contract(), evidence, source_path="proof.json")
+
+ assert summary.contract_status == "PENDING"
+ assert all(rule.outcome == "PENDING" for rule in summary.rules)
+
+
+def test_quality_contract_endpoint_projects_evaluation(monkeypatch: pytest.MonkeyPatch) -> None:
+ expected = QualityContractSummary(
+ contract_code="FED_FUNDS_RATE_DAILY_QUALITY",
+ contract_version="1.0.0",
+ contract_name="Federal Funds Rate Daily Quality Contract",
+ description="Proof contract.",
+ target_name="fct_fed_funds_rate_daily",
+ layer="MART",
+ enforcement_mode="BLOCK",
+ artifact_status="READY",
+ evidence_trust_posture="TRUSTED",
+ contract_status="COMPLIANT",
+ minimum_pass_rate=1.0,
+ pass_rate=1.0,
+ required_rule_count=5,
+ satisfied_rule_count=5,
+ warning_rule_count=0,
+ blocking_rule_count=0,
+ missing_rule_count=0,
+ source_path="contracts/quality/fed_funds_rate_daily.v1.json",
+ rules=[],
+ )
+ monkeypatch.setattr(quality_route, "quality_contract_summary", lambda: expected)
+
+ response = client.get("/api/v1/quality/contracts/summary")
+
+ assert response.status_code == 200
+ payload = response.json()
+ assert payload["phase"].startswith("Phase 7.2")
+ assert payload["contract_status"] == "COMPLIANT"
+ assert payload["required_rule_count"] == 5