From 548ede79c93a3b2ef1a8a8d3b20689b52415b638 Mon Sep 17 00:00:00 2001 From: Paul Sattaur Date: Wed, 12 Aug 2026 13:56:25 -0400 Subject: [PATCH 1/2] =?UTF-8?q?Phase=207.2=20=E2=80=94=20Quality=20Contrac?= =?UTF-8?q?t=20Gate=20+=20Consumer=20Compatibility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +- .../api/skydata_studio/api/routes/platform.py | 2 +- apps/api/skydata_studio/api/routes/quality.py | 31 ++- apps/api/skydata_studio/schemas/quality.py | 83 +++++++- .../services/quality_contracts.py | 160 +++++++++++++++ apps/web/src/App.jsx | 5 +- apps/web/src/components/Sidebar.jsx | 2 +- apps/web/src/pages/DataContracts.jsx | 188 +++++++++++++++++ apps/web/src/pages/StudioOverview.jsx | 14 +- apps/web/src/styles.css | 38 ++++ contracts/quality/README.md | 17 ++ .../quality/fed_funds_rate_daily.v1.json | 52 +++++ docs/ARCHITECTURE.md | 18 +- docs/ROADMAP.md | 34 +++- docs/SkyData_RepoMap.md | 8 +- docs/repository-map.txt | 8 +- tests/test_platform.py | 2 +- tests/test_quality_gate_contracts.py | 189 ++++++++++++++++++ 18 files changed, 827 insertions(+), 38 deletions(-) create mode 100644 apps/api/skydata_studio/services/quality_contracts.py create mode 100644 apps/web/src/pages/DataContracts.jsx create mode 100644 contracts/quality/README.md create mode 100644 contracts/quality/fed_funds_rate_daily.v1.json create mode 100644 tests/test_quality_gate_contracts.py 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..121a820 --- /dev/null +++ b/apps/web/src/pages/DataContracts.jsx @@ -0,0 +1,188 @@ +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 (signal) => { + setLoading(true); + setError(''); + try { + const [qualityPayload, compatibilityPayload] = await Promise.all([ + getJson('/api/v1/quality/contracts/summary', { signal }), + getJson('/api/v1/integrations/skycommand/contracts/compatibility', { signal }), + ]); + setQualityContract(qualityPayload); + setCompatibility(compatibilityPayload); + } catch (requestError) { + if (requestError.name !== 'AbortError') setError(requestError.message); + } finally { + if (!signal?.aborted) setLoading(false); + } + }, []); + + useEffect(() => { + const controller = new AbortController(); + load(controller.signal); + return () => controller.abort(); + }, [load]); + + 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. +

+
+ +
+ + {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) => ( +
+
+
{rule.quality_dimension.replaceAll('_', ' ')}{rule.label}
+ +
+
+
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) => ( +
+
{item.code}
+ 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
+
    +
  1. 01
    Policy is source-controlledThe consumer gate is versioned independently from dbt-generated test identifiers.
  2. +
  3. 02
    Evidence decides readinessRequired selectors must resolve to passing latest-run dbt checks before the mart is compliant.
  4. +
  5. 03
    Boundaries stay visibleSkyCommand consumer compatibility and Studio quality policy share one contract workbench without sharing ownership.
  6. +
+
+
+
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 contractArtifact evidenceNo 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 Phase7.1dbt quality evidence + trust posture
+
Current Phase7.2quality contract gates + compatibility
Pipeline ProofREADYCurated mart materialization proven
OrchestratorAirflow 3Batch 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
    -
  1. 01
    Artifact-backed quality evidenceJoin dbt test definitions from manifest.json to latest outcomes from run_results.json.
  2. -
  3. 02
    Trust postureRoll pass, warning, failure, error, skip, and unknown states into one readable consumption signal.
  4. -
  5. 03
    Layer-aware coverageKeep source, staging, intermediate, and mart tests traceable without creating another test metadata store.
  6. +
  7. 01
    Source-controlled quality gateDeclare required evidence selectors beside the repository instead of copying dbt metadata into another store.
  8. +
  9. 02
    Consumer readinessEvaluate the latest dbt quality evidence against a versioned mart contract before presenting it as compliant.
  10. +
  11. 03
    Compatibility in one placeKeep 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 From 6fc7e55bed5ac2068be52d3b227694f9d22f5cc5 Mon Sep 17 00:00:00 2001 From: Paul Sattaur Date: Wed, 12 Aug 2026 15:01:59 -0400 Subject: [PATCH 2/2] Phase 7.2.1 - React Effect Lint Fix --- apps/web/src/pages/DataContracts.jsx | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/web/src/pages/DataContracts.jsx b/apps/web/src/pages/DataContracts.jsx index 121a820..4322491 100644 --- a/apps/web/src/pages/DataContracts.jsx +++ b/apps/web/src/pages/DataContracts.jsx @@ -54,28 +54,41 @@ function DataContracts() { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - const load = useCallback(async (signal) => { + const load = useCallback(async () => { setLoading(true); setError(''); try { const [qualityPayload, compatibilityPayload] = await Promise.all([ - getJson('/api/v1/quality/contracts/summary', { signal }), - getJson('/api/v1/integrations/skycommand/contracts/compatibility', { signal }), + getJson('/api/v1/quality/contracts/summary'), + getJson('/api/v1/integrations/skycommand/contracts/compatibility'), ]); setQualityContract(qualityPayload); setCompatibility(compatibilityPayload); } catch (requestError) { - if (requestError.name !== 'AbortError') setError(requestError.message); + setError(requestError.message); } finally { - if (!signal?.aborted) setLoading(false); + setLoading(false); } }, []); useEffect(() => { const controller = new AbortController(); - load(controller.signal); + 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(); - }, [load]); + }, []); const compatibilityTotal = compatibility.compatible + compatibility.incompatible + compatibility.missing;