diff --git a/README.md b/README.md index e3da7b2..4fea918 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 7.1 and Phase 7.2 are closed green:** latest dbt quality evidence is TRUSTED at 14/14, the Federal Funds Rate consumer gate is COMPLIANT at 5/5 and 100%, all five SkyCommand boundary contracts remain compatible, and the final Phase 7.2 local validation passed 64 tests. **Phase 7.3 is now in progress:** real non-passing contract outcomes become durable Studio-owned incidents with acknowledgement, ownership, resolution, recurrence, and lifecycle history. +**Current status:** Phases 0 through 6 are complete. **Phase 7.1 through Phase 7.3 are closed green:** dbt quality evidence is TRUSTED at 14/14, the Federal Funds Rate consumer gate is COMPLIANT at 5/5 and 100%, durable incident reconciliation correctly creates no synthetic failure for clean evidence, and the final Phase 7.3 local validation passed 68 tests. **Phase 7.4 is now in progress:** source-controlled reliability objectives and idempotent evidence observations turn point-in-time quality gates into measurable historical SLO posture. --- @@ -196,6 +196,34 @@ Open `/quality/incidents`. A clean 5/5 contract intentionally creates **zero** i --- +## Phase 7.4 quick start + +Phase 7.4 adds an observation-backed quality SLO without pretending local captures are continuous uptime. The Federal Funds Rate quality contract now owns a 30-day, 99% minimum compliant-observation target. Re-run the idempotent metadata bootstrap so `quality_slo_observation` exists: + +```powershell +docker compose -f .\infra\docker-compose.yml up -d studio-postgres +uv run python .\scripts\bootstrap_metadata.py +``` + +Capture the latest quality evidence. This also reconciles durable incidents first, then writes **at most one** reliability observation for the current dbt invocation: + +```powershell +Invoke-RestMethod ` + -Method Post ` + "http://localhost:8100/api/v1/quality/reliability/capture" | + ConvertTo-Json -Depth 12 +``` + +With the current clean Federal Funds Rate evidence, the first capture should report `observation_created=true`; repeating the same capture should return `false` because the dbt invocation is already represented. Inspect the rolling posture with: + +```powershell +Invoke-RestMethod ` + "http://localhost:8100/api/v1/quality/reliability/summary" | + ConvertTo-Json -Depth 12 +``` + +The first clean proof should show `MEETING`, 100% observed compliance, a 99% target, one observation, zero blocked observations, and a compliant streak of one. Open **Quality & Lineage → Reliability** for the same evidence in the workbench. + ## Validation contract Run the complete local validation suite before every SkyData Studio development promotion: @@ -306,6 +334,8 @@ Orchestration Quality & Lineage ├─ Data Quality ├─ Contracts + ├─ Quality Incidents + ├─ Reliability └─ Lineage Analytics Delivery diff --git a/apps/api/skydata_studio/api/routes/platform.py b/apps/api/skydata_studio/api/routes/platform.py index 47e50ba..23796f6 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.3 — Durable Quality Incidents and Remediation Lifecycle", + current_phase="Phase 7.4 — Quality SLO and Reliability History", 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 c008c14..72a27e6 100644 --- a/apps/api/skydata_studio/api/routes/quality.py +++ b/apps/api/skydata_studio/api/routes/quality.py @@ -9,6 +9,8 @@ QualityIncidentRead, QualityIncidentReconcileResult, QualityIncidentSummary, + QualityReliabilityCaptureResult, + QualityReliabilitySummary, ) from skydata_studio.services.dbt_quality import dbt_quality_summary from skydata_studio.services.quality_contracts import quality_contract_summary @@ -20,6 +22,10 @@ reconcile_quality_incidents, resolve_quality_incident, ) +from skydata_studio.services.quality_reliability import ( + capture_quality_reliability_observation, + quality_reliability_summary, +) router = APIRouter() @@ -35,7 +41,7 @@ def _incident_storage_unavailable() -> HTTPException: return HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=( - "SkyData Studio quality incident storage is unavailable. Start studio-postgres " + "SkyData Studio quality operational storage is unavailable. Start studio-postgres " "and run uv run python scripts/bootstrap_metadata.py." ), ) @@ -80,7 +86,14 @@ def incident_summary(session: SessionDependency) -> QualityIncidentSummary: @router.post("/incidents/reconcile", response_model=QualityIncidentReconcileResult) def incident_reconcile(session: SessionDependency) -> QualityIncidentReconcileResult: try: - return reconcile_quality_incidents(session) + contract = quality_contract_summary() + result = reconcile_quality_incidents(session, contract=contract) + observation_created = capture_quality_reliability_observation( + session, contract=contract, incidents=result.summary + ) + return result.model_copy( + update={"reliability_observation_created": observation_created} + ) except SQLAlchemyError as error: session.rollback() raise _incident_storage_unavailable() from error @@ -91,6 +104,44 @@ def incident_reconcile(session: SessionDependency) -> QualityIncidentReconcileRe ) from error +@router.get("/reliability/summary", response_model=QualityReliabilitySummary) +def reliability_summary(session: SessionDependency) -> QualityReliabilitySummary: + try: + return quality_reliability_summary(session) + except SQLAlchemyError as error: + raise _incident_storage_unavailable() from error + except (OSError, ValueError, TypeError) as error: + raise _artifact_unavailable( + "SkyData Studio could not evaluate quality reliability history. " + "Run .\\scripts\\dbt.ps1 build, reconcile quality evidence, and retry." + ) from error + + +@router.post("/reliability/capture", response_model=QualityReliabilityCaptureResult) +def reliability_capture(session: SessionDependency) -> QualityReliabilityCaptureResult: + try: + contract = quality_contract_summary() + incident_result = reconcile_quality_incidents(session, contract=contract) + observation_created = capture_quality_reliability_observation( + session, contract=contract, incidents=incident_result.summary + ) + return QualityReliabilityCaptureResult( + observation_created=observation_created, + incident_created_count=incident_result.created_count, + incident_reopened_count=incident_result.reopened_count, + incident_resolved_count=incident_result.resolved_count, + summary=quality_reliability_summary(session, contract=contract), + ) + except SQLAlchemyError as error: + session.rollback() + raise _incident_storage_unavailable() from error + except (OSError, ValueError, TypeError) as error: + session.rollback() + raise _artifact_unavailable( + "SkyData Studio could not capture the latest quality reliability observation." + ) from error + + @router.post("/incidents/{incident_id}/acknowledge", response_model=QualityIncidentRead) def incident_acknowledge( incident_id: str, diff --git a/apps/api/skydata_studio/models/__init__.py b/apps/api/skydata_studio/models/__init__.py index be51af0..7ad3c2c 100644 --- a/apps/api/skydata_studio/models/__init__.py +++ b/apps/api/skydata_studio/models/__init__.py @@ -18,7 +18,11 @@ PipelineStepRun, PipelineVersion, ) -from skydata_studio.models.quality import QualityIncident, QualityIncidentEvent +from skydata_studio.models.quality import ( + QualityIncident, + QualityIncidentEvent, + QualitySloObservation, +) __all__ = [ "MetadataAsset", @@ -39,4 +43,5 @@ "PipelineVersion", "QualityIncident", "QualityIncidentEvent", + "QualitySloObservation", ] diff --git a/apps/api/skydata_studio/models/quality.py b/apps/api/skydata_studio/models/quality.py index ecbba48..890d2f1 100644 --- a/apps/api/skydata_studio/models/quality.py +++ b/apps/api/skydata_studio/models/quality.py @@ -3,7 +3,7 @@ from datetime import UTC, datetime from uuid import uuid4 -from sqlalchemy import DateTime, ForeignKey, Integer, String, Text +from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from skydata_studio.db.base import Base @@ -73,3 +73,24 @@ class QualityIncidentEvent(Base): ) incident: Mapped[QualityIncident] = relationship(back_populates="events") + + +class QualitySloObservation(Base): + __tablename__ = "quality_slo_observation" + + id: Mapped[str] = mapped_column(String(36), primary_key=True, default=_uuid) + observation_key: Mapped[str] = mapped_column(String(420), unique=True, index=True) + contract_code: Mapped[str] = mapped_column(String(160), index=True) + contract_version: Mapped[str] = mapped_column(String(40)) + evidence_invocation_id: Mapped[str | None] = mapped_column(String(160), index=True) + evidence_generated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + contract_status: Mapped[str] = mapped_column(String(40), index=True) + artifact_status: Mapped[str] = mapped_column(String(40), index=True) + evidence_trust_posture: Mapped[str] = mapped_column(String(40), index=True) + pass_rate: Mapped[float] = mapped_column(Float, nullable=False) + active_incident_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + blocking_active_incident_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + warning_active_incident_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + captured_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), default=_utc_now, nullable=False, index=True + ) diff --git a/apps/api/skydata_studio/schemas/quality.py b/apps/api/skydata_studio/schemas/quality.py index 6fdbdf7..f3762c0 100644 --- a/apps/api/skydata_studio/schemas/quality.py +++ b/apps/api/skydata_studio/schemas/quality.py @@ -54,6 +54,11 @@ class DbtQualitySummary(BaseModel): checks: list[DbtQualityCheckSummary] +class QualitySloDefinition(BaseModel): + window_days: int = Field(default=30, ge=1, le=365) + minimum_compliance_rate: float = Field(default=0.99, ge=0.0, le=1.0) + + class QualityContractRuleDefinition(BaseModel): code: str label: str @@ -72,6 +77,7 @@ class QualityContractDefinition(BaseModel): layer: Literal["STAGING", "INTERMEDIATE", "MART"] enforcement_mode: Literal["BLOCK", "ADVISORY"] minimum_pass_rate: float = Field(ge=0.0, le=1.0) + slo: QualitySloDefinition = Field(default_factory=QualitySloDefinition) rules: list[QualityContractRuleDefinition] @@ -110,6 +116,10 @@ class QualityContractSummary(BaseModel): blocking_rule_count: int = Field(ge=0) missing_rule_count: int = Field(ge=0) source_path: str + evidence_invocation_id: str | None = None + evidence_generated_at: str | None = None + slo_window_days: int = Field(default=30, ge=1, le=365) + slo_minimum_compliance_rate: float = Field(default=0.99, ge=0.0, le=1.0) rules: list[QualityContractRuleEvaluation] @@ -186,4 +196,57 @@ class QualityIncidentReconcileResult(BaseModel): created_count: int = Field(ge=0) reopened_count: int = Field(ge=0) resolved_count: int = Field(ge=0) + reliability_observation_created: bool = False summary: QualityIncidentSummary + + +QualityReliabilityStatus = Literal["MEETING", "AT_RISK", "BREACHED", "PENDING"] + + +class QualityReliabilityObservationRead(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + contract_code: str + contract_version: str + evidence_invocation_id: str | None = None + evidence_generated_at: datetime | None = None + contract_status: Literal["COMPLIANT", "DEGRADED", "BLOCKED", "PENDING"] + artifact_status: Literal["READY", "PENDING", "MISSING"] + evidence_trust_posture: Literal["TRUSTED", "DEGRADED", "BLOCKED", "PENDING"] + pass_rate: float = Field(ge=0.0, le=1.0) + active_incident_count: int = Field(ge=0) + blocking_active_incident_count: int = Field(ge=0) + warning_active_incident_count: int = Field(ge=0) + captured_at: datetime + + +class QualityReliabilitySummary(BaseModel): + project_name: str = "skydata_studio" + phase: str = "Phase 7.4 — Quality SLO and Reliability History" + contract_code: str + contract_version: str + current_contract_status: Literal["COMPLIANT", "DEGRADED", "BLOCKED", "PENDING"] + reliability_status: QualityReliabilityStatus + window_days: int = Field(ge=1, le=365) + minimum_compliance_rate: float = Field(ge=0.0, le=1.0) + observed_compliance_rate: float = Field(ge=0.0, le=1.0) + observation_count: int = Field(ge=0) + compliant_count: int = Field(ge=0) + degraded_count: int = Field(ge=0) + blocked_count: int = Field(ge=0) + pending_count: int = Field(ge=0) + current_compliant_streak: int = Field(ge=0) + window_start: datetime + window_end: datetime + observations: list[QualityReliabilityObservationRead] + + +class QualityReliabilityCaptureResult(BaseModel): + project_name: str = "skydata_studio" + phase: str = "Phase 7.4 — Quality SLO and Reliability History" + observation_created: bool + incident_created_count: int = Field(ge=0) + incident_reopened_count: int = Field(ge=0) + incident_resolved_count: int = Field(ge=0) + summary: QualityReliabilitySummary diff --git a/apps/api/skydata_studio/services/quality_contracts.py b/apps/api/skydata_studio/services/quality_contracts.py index a0e3ecd..ac07929 100644 --- a/apps/api/skydata_studio/services/quality_contracts.py +++ b/apps/api/skydata_studio/services/quality_contracts.py @@ -141,6 +141,10 @@ def evaluate_quality_contract( blocking_rule_count=blocking, missing_rule_count=missing, source_path=source_path, + evidence_invocation_id=evidence.invocation_id, + evidence_generated_at=evidence.generated_at, + slo_window_days=contract.slo.window_days, + slo_minimum_compliance_rate=contract.slo.minimum_compliance_rate, rules=rules, ) diff --git a/apps/api/skydata_studio/services/quality_reliability.py b/apps/api/skydata_studio/services/quality_reliability.py new file mode 100644 index 0000000..3ee9272 --- /dev/null +++ b/apps/api/skydata_studio/services/quality_reliability.py @@ -0,0 +1,141 @@ +from __future__ import annotations + +from datetime import UTC, datetime, timedelta + +from sqlalchemy import select +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session + +from skydata_studio.models.quality import QualitySloObservation +from skydata_studio.schemas.quality import ( + QualityContractSummary, + QualityIncidentSummary, + QualityReliabilityObservationRead, + QualityReliabilityStatus, + QualityReliabilitySummary, +) +from skydata_studio.services.quality_contracts import quality_contract_summary + + +def _utc_now() -> datetime: + return datetime.now(UTC) + + +def _parse_generated_at(value: str | None) -> datetime | None: + if not value: + return None + normalized = value[:-1] + "+00:00" if value.endswith("Z") else value + parsed = datetime.fromisoformat(normalized) + if parsed.tzinfo is None: + return parsed.replace(tzinfo=UTC) + return parsed.astimezone(UTC) + + +def _observation_key(contract: QualityContractSummary) -> str: + evidence_key = ( + contract.evidence_invocation_id + or contract.evidence_generated_at + or f"{contract.artifact_status}:{contract.contract_status}" + ) + return f"{contract.contract_code}:{contract.contract_version}:{evidence_key}" + + +def capture_quality_reliability_observation( + session: Session, + *, + contract: QualityContractSummary, + incidents: QualityIncidentSummary, +) -> bool: + key = _observation_key(contract) + existing = session.scalar( + select(QualitySloObservation).where(QualitySloObservation.observation_key == key) + ) + if existing is not None: + return False + + session.add( + QualitySloObservation( + observation_key=key, + contract_code=contract.contract_code, + contract_version=contract.contract_version, + evidence_invocation_id=contract.evidence_invocation_id, + evidence_generated_at=_parse_generated_at(contract.evidence_generated_at), + contract_status=contract.contract_status, + artifact_status=contract.artifact_status, + evidence_trust_posture=contract.evidence_trust_posture, + pass_rate=contract.pass_rate, + active_incident_count=incidents.active_count, + blocking_active_incident_count=incidents.blocking_active_count, + warning_active_incident_count=incidents.warning_active_count, + ) + ) + try: + session.commit() + except IntegrityError: + session.rollback() + return False + return True + + +def quality_reliability_summary( + session: Session, + *, + contract: QualityContractSummary | None = None, + now: datetime | None = None, +) -> QualityReliabilitySummary: + current = contract or quality_contract_summary() + window_end = now or _utc_now() + if window_end.tzinfo is None: + window_end = window_end.replace(tzinfo=UTC) + window_start = window_end - timedelta(days=current.slo_window_days) + + statement = ( + select(QualitySloObservation) + .where(QualitySloObservation.contract_code == current.contract_code) + .where(QualitySloObservation.captured_at >= window_start) + .order_by(QualitySloObservation.captured_at.asc()) + ) + rows = list(session.scalars(statement).all()) + observations = [QualityReliabilityObservationRead.model_validate(row) for row in rows] + + total = len(observations) + compliant = sum(item.contract_status == "COMPLIANT" for item in observations) + degraded = sum(item.contract_status == "DEGRADED" for item in observations) + blocked = sum(item.contract_status == "BLOCKED" for item in observations) + pending = sum(item.contract_status == "PENDING" for item in observations) + observed_rate = compliant / total if total else 0.0 + + streak = 0 + for item in reversed(observations): + if item.contract_status != "COMPLIANT": + break + streak += 1 + + status: QualityReliabilityStatus + if not observations: + status = "PENDING" + elif observed_rate < current.slo_minimum_compliance_rate: + status = "BREACHED" + elif current.contract_status != "COMPLIANT": + status = "AT_RISK" + else: + status = "MEETING" + + return QualityReliabilitySummary( + contract_code=current.contract_code, + contract_version=current.contract_version, + current_contract_status=current.contract_status, + reliability_status=status, + window_days=current.slo_window_days, + minimum_compliance_rate=current.slo_minimum_compliance_rate, + observed_compliance_rate=observed_rate, + observation_count=total, + compliant_count=compliant, + degraded_count=degraded, + blocked_count=blocked, + pending_count=pending, + current_compliant_streak=streak, + window_start=window_start, + window_end=window_end, + observations=observations, + ) diff --git a/apps/web/src/App.jsx b/apps/web/src/App.jsx index bc9a5b5..fbe4bd3 100644 --- a/apps/web/src/App.jsx +++ b/apps/web/src/App.jsx @@ -16,6 +16,7 @@ import SemanticLayer from './pages/SemanticLayer.jsx'; import DataQuality from './pages/DataQuality.jsx'; import DataContracts from './pages/DataContracts.jsx'; import QualityIncidents from './pages/QualityIncidents.jsx'; +import QualityReliability from './pages/QualityReliability.jsx'; import PlaceholderPage from './pages/PlaceholderPage.jsx'; const PAGE_META = { @@ -32,6 +33,7 @@ const PAGE_META = { '/quality/checks': ['Data Quality', 'Tests, coverage, and trust evidence'], '/quality/contracts': ['Contracts', 'Quality gates and consumer compatibility'], '/quality/incidents': ['Quality Incidents', 'Durable failures, ownership, and remediation history'], + '/quality/reliability': ['Reliability', 'Quality SLO, observation history, and consumer-readiness posture'], '/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'], @@ -76,6 +78,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> @@ -95,6 +98,7 @@ function App() { '/quality/checks', '/quality/contracts', '/quality/incidents', + '/quality/reliability', ].includes(path)) .map(([path, [title, subtitle]]) => ( Development - Phase 7.3 durable quality incidents + Phase 7.4 quality SLO + reliability diff --git a/apps/web/src/pages/QualityReliability.jsx b/apps/web/src/pages/QualityReliability.jsx new file mode 100644 index 0000000..9d530b8 --- /dev/null +++ b/apps/web/src/pages/QualityReliability.jsx @@ -0,0 +1,193 @@ +import { useCallback, useEffect, useState } from 'react'; +import StatusPill from '../components/StatusPill.jsx'; +import { getJson, postJson } from '../services/api.js'; + +const EMPTY = { + contract_code: 'FED_FUNDS_RATE_DAILY_QUALITY', + contract_version: '1.1.0', + current_contract_status: 'PENDING', + reliability_status: 'PENDING', + window_days: 30, + minimum_compliance_rate: 0.99, + observed_compliance_rate: 0, + observation_count: 0, + compliant_count: 0, + degraded_count: 0, + blocked_count: 0, + pending_count: 0, + current_compliant_streak: 0, + window_start: null, + window_end: null, + observations: [], +}; + +function toneForStatus(status) { + if (['MEETING', 'COMPLIANT'].includes(status)) return 'READY'; + if (['AT_RISK', 'DEGRADED', 'PENDING'].includes(status)) return 'WARNING'; + if (['BREACHED', 'BLOCKED'].includes(status)) return 'BLOCKED'; + return 'UNKNOWN'; +} + +function percent(value) { + return `${Math.round(Number(value || 0) * 100)}%`; +} + +function dateLabel(value) { + if (!value) return '—'; + const date = new Date(value); + return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); +} + +function QualityReliability() { + const [summary, setSummary] = useState(EMPTY); + const [loading, setLoading] = useState(true); + const [capturing, setCapturing] = useState(false); + const [error, setError] = useState(''); + const [message, setMessage] = useState(''); + + const load = useCallback(async () => { + setLoading(true); + setError(''); + try { + setSummary(await getJson('/api/v1/quality/reliability/summary')); + } catch (requestError) { + setError(requestError.message); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + const controller = new AbortController(); + getJson('/api/v1/quality/reliability/summary', { signal: controller.signal }) + .then(setSummary) + .catch((requestError) => { + if (requestError.name !== 'AbortError') setError(requestError.message); + }) + .finally(() => { + if (!controller.signal.aborted) setLoading(false); + }); + return () => controller.abort(); + }, []); + + async function capture() { + setCapturing(true); + setError(''); + setMessage(''); + try { + const result = await postJson('/api/v1/quality/reliability/capture'); + setSummary(result.summary); + setMessage( + result.observation_created + ? 'Captured one new reliability observation from the latest dbt invocation.' + : 'Latest dbt invocation was already captured; reliability history was left unchanged.', + ); + } catch (requestError) { + setError(requestError.message); + } finally { + setCapturing(false); + } + } + + return ( +
+
+
+ PHASE 7.4 · QUALITY SLO + RELIABILITY HISTORY +

Quality Reliability

+

+ Turn contract evaluations into idempotent historical observations, then measure whether + consumer-ready data is meeting its source-controlled reliability objective over time. +

+
+
+ + +
+
+ + {error ?
!{error}
: null} + {message ?
{message}
: null} + +
+
SLO Posture{summary.reliability_status}{summary.contract_code}
+
Window Compliance{percent(summary.observed_compliance_rate)}Target {percent(summary.minimum_compliance_rate)}
+
Observations{summary.observation_count}{summary.window_days}-day rolling window
+
Blocking{summary.blocked_count}{summary.degraded_count} degraded · {summary.pending_count} pending
+
Clean Streak{summary.current_compliant_streak}Consecutive compliant observations
+
+ +
+
+
RELIABILITY OBJECTIVE

Consumer readiness measured from durable evidence

+ +
+
+
Contract Version{summary.contract_version}Policy changes remain source-controlled
+
Current Gate{summary.current_contract_status}Latest quality contract state
+
Window{summary.window_days} days{dateLabel(summary.window_start)} → {dateLabel(summary.window_end)}
+
Target{percent(summary.minimum_compliance_rate)}Minimum compliant observation rate
+
+
+ +
+
+
OBSERVATION HISTORY

One row per dbt evidence invocation

+ {summary.observation_count} observations +
+
+ + + + + + {summary.observations.slice().reverse().map((observation) => ( + + + + + + + + + + ))} + {!summary.observations.length ? ( + + + + ) : null} + +
CapturedInvocationContractTrustPass RateActive IncidentsBlocking
{dateLabel(observation.captured_at)}{observation.evidence_invocation_id || 'No invocation id'}{dateLabel(observation.evidence_generated_at)}{observation.evidence_trust_posture}{percent(observation.pass_rate)}{observation.active_incident_count}{observation.blocking_active_incident_count}
+ No reliability observations yet. + Capture the latest evidence once; repeated capture of the same dbt invocation is intentionally idempotent. +
+
+
+ +
+
+
ACCEPTANCE CONTRACT

What Phase 7.4 proves

7.4
+
    +
  1. 01
    SLO policy is versionedThe Federal Funds Rate quality contract owns a 30-day, 99% readiness objective.
  2. +
  3. 02
    History is idempotentA dbt invocation contributes at most one observation even when reconciliation is repeated.
  4. +
  5. 03
    Reliability is evidence-backedWindow compliance, streaks, trust posture, and incident counts come from durable observations rather than invented uptime.
  6. +
+
+
+
BOUNDARY

Observed reliability, not synthetic uptime

◷
+

+ Phase 7.4 measures captured quality-evidence observations. It does not pretend irregular local development captures are continuous service uptime; scheduled capture and notification routing remain later operational integrations. +

+
Evidence identityIdempotent historySource-controlled target
+
+
+
+ ); +} + +export default QualityReliability; diff --git a/apps/web/src/pages/StudioOverview.jsx b/apps/web/src/pages/StudioOverview.jsx index fd776c5..85b3603 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.3 — Durable Quality Incidents and Remediation Lifecycle', + current_phase: 'Phase 7.4 — Quality SLO and Reliability History', 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.3 + AURORA FOUNDRY · PHASE 7.4

Shape trusted data into analytical products.

{summary.boundary}

@@ -61,7 +61,7 @@ function StudioOverview() {
Platform API{apiState}FastAPI contract service
-
Current Phase7.3durable quality incidents + remediation
+
Current Phase7.4quality SLO + reliability history
Pipeline ProofREADYCurated mart materialization proven
OrchestratorAirflow 3Batch and asset workflows
@@ -110,11 +110,11 @@ function StudioOverview() {
-
CURRENT IMPLEMENTATION

durable quality incidents and remediation lifecycle

PHASE 7.3
+
CURRENT IMPLEMENTATION

quality SLO and reliability history

PHASE 7.4
    -
  1. 01
    Evidence-driven incident creationPersist only real warning, blocking, or missing contract outcomes; passing evidence creates no fake incident.
  2. -
  3. 02
    Durable remediation lifecyclePreserve acknowledgement, operator ownership, resolution notes, timestamps, and event history in Studio PostgreSQL.
  4. -
  5. 03
    Automatic recovery and recurrenceReturning PASS evidence resolves active incidents; a later failure reopens the same durable record as another occurrence.
  6. +
  7. 01
    Source-controlled SLO policyThe quality contract owns a 30-day, 99% consumer-readiness target alongside its blocking rules.
  8. +
  9. 02
    Idempotent reliability observationsEach dbt invocation contributes at most one durable SLO observation, so repeated reconciliation cannot inflate history.
  10. +
  11. 03
    Observable reliability postureWindow compliance, current streak, blocked observations, and incident counts become a dedicated operational surface.
diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index 93353a5..8ed2123 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -1101,3 +1101,22 @@ a { color: inherit; } .incident-actions-row { justify-content: flex-start; } .incidents-metric-grid, .incident-lifecycle-grid, .incident-detail-grid, .incident-workflow-grid { grid-template-columns: 1fr; } } + +/* Phase 7.4 — Quality SLO and reliability history */ +.reliability-intro { align-items: end; } +.reliability-metric-grid { grid-template-columns: repeat(5, minmax(0, 1fr)); } +.reliability-objective-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.72rem; } +.reliability-objective-grid article { display: grid; gap: 0.34rem; padding: 0.82rem; border: 1px solid rgba(111,190,187,0.11); border-radius: 0.72rem; background: rgba(3,11,17,0.48); } +.reliability-objective-grid span { color: var(--subtle); font-size: 0.58rem; font-weight: 850; letter-spacing: 0.08em; text-transform: uppercase; } +.reliability-objective-grid strong { color: var(--mint); font-size: 0.88rem; } +.reliability-objective-grid small { color: var(--muted); font-size: 0.62rem; overflow-wrap: anywhere; } +.reliability-table td { vertical-align: top; } +.reliability-table td code { display: block; max-width: 230px; overflow: hidden; color: var(--mint); font-size: 0.62rem; text-overflow: ellipsis; white-space: nowrap; } +.reliability-table td small { display: block; margin-top: 0.22rem; color: var(--muted); font-size: 0.58rem; } +@media (max-width: 1180px) { + .reliability-metric-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .reliability-objective-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 720px) { + .reliability-metric-grid, .reliability-objective-grid { grid-template-columns: 1fr; } +} diff --git a/contracts/quality/README.md b/contracts/quality/README.md index 31e0194..680eabf 100644 --- a/contracts/quality/README.md +++ b/contracts/quality/README.md @@ -15,3 +15,7 @@ A quality contract declares the evidence a downstream consumer requires from the - 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`. + +## Reliability objective + +The governed Federal Funds Rate daily contract also carries a Phase 7.4 SLO policy: a 30-day observation window with a 99% minimum compliant-observation rate. Studio persists one SLO observation per dbt invocation, so repeated reconciliation of identical evidence cannot inflate reliability history. diff --git a/contracts/quality/fed_funds_rate_daily.v1.json b/contracts/quality/fed_funds_rate_daily.v1.json index 7e1f6d4..87e1520 100644 --- a/contracts/quality/fed_funds_rate_daily.v1.json +++ b/contracts/quality/fed_funds_rate_daily.v1.json @@ -1,12 +1,16 @@ { "code": "FED_FUNDS_RATE_DAILY_QUALITY", - "version": "1.0.0", + "version": "1.1.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, + "slo": { + "window_days": 30, + "minimum_compliance_rate": 0.99 + }, "rules": [ { "code": "OBSERVATION_DATE_REQUIRED", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 640dc34..a57aff4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -95,7 +95,7 @@ Studio quality-contract evaluator ◀── Phase 7.1 latest dbt evidence └── 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. Phase 7.3 adds Studio-owned operational incident memory downstream of this policy seam; historical SLO calculation remains later work. +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. Phase 7.3 adds Studio-owned operational incident memory downstream of this policy seam. Phase 7.4 adds a source-controlled 30-day/99% reliability objective plus one idempotent `quality_slo_observation` per dbt invocation, turning reconciled quality evidence into historical SLO posture without claiming irregular local captures are continuous uptime. ### Durable quality incident boundary @@ -210,3 +210,10 @@ Studio PostgreSQL: mart.fed_funds_rate_mart The materializer creates the target relation from registered Studio metadata when necessary and uses mapping business keys for matching. Existing rows with identical non-key values are counted as unchanged; changed values are updated; new business keys are inserted. Structured run evidence records read, transformed, inserted, updated, unchanged, rejected, published, and target-row counts. Replay reuse remains intentionally different from a forced physical rerun. `REUSE` returns the existing durable execution and performs no second mutation. `FORCE_NEW` creates a distinct run and re-executes the same `MERGE`, which provides the idempotency proof: unchanged source data must not duplicate target rows. The replay-key canonical input also includes the execution-engine version so a pre-materialization Phase 4.2 run cannot be reused after the Phase 4.3 boundary is enabled. + + +### Quality reliability observations + +`quality_slo_observation` is Studio-owned operational evidence, not copied dbt metadata. Each row is keyed by quality-contract identity plus dbt invocation identity and records contract status, artifact/trust posture, pass rate, and active incident counts at capture time. The unique observation key makes repeated reconciliation safe: the same dbt invocation cannot be counted twice for the same contract version. + +The first reliability SLO is defined beside the quality contract rather than in application code: `window_days=30` and `minimum_compliance_rate=0.99`. The `/quality/reliability` workbench reports `MEETING`, `AT_RISK`, `BREACHED`, or `PENDING` from captured observations. This is explicitly an observation-based reliability measure; scheduled capture cadence and notification routing remain later operational integrations. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1232007..23aef17 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -516,7 +516,7 @@ Closure evidence: ## Phase 7.3 — Durable Quality Incidents and Remediation Lifecycle -**Status:** In progress. +**Status:** Complete. Additive incident persistence, clean-evidence reconciliation, lifecycle proof, workbench proof, and local validation are green. Scope: @@ -529,11 +529,36 @@ Scope: - add a Quality Incidents workbench while explicitly creating no fake incident when the current contract remains clean; - keep historical SLO calculation, notification routing, and cross-product lineage impact outside this slice. -Acceptance proof: +Closure evidence: -- re-running `uv run python scripts/bootstrap_metadata.py` creates the additive incident and event tables in existing Studio PostgreSQL; -- reconciling the current COMPLIANT 5/5 Federal Funds Rate contract returns zero active incidents and creates no synthetic failure record; +- re-running `uv run python scripts/bootstrap_metadata.py` reported the Studio metadata schema ready against the existing PostgreSQL database; +- reconciling the live COMPLIANT 5/5 Federal Funds Rate contract returned `created_count=0`, `reopened_count=0`, `resolved_count=0`, zero active incidents, and no synthetic failure record; +- the Quality Incidents API and workbench both showed COMPLIANT/READY with 0 active, 0 blocking, 0 resolved, and an intentionally empty durable register; - backend lifecycle tests prove BLOCK → OPEN, operator acknowledgement, manual/PASS-driven resolution, and recurrence → REOPENED with occurrence history; -- the Quality Incidents workbench shows current contract state, active/blocking/resolved totals, the durable register, lifecycle history, and acknowledgement/resolution controls; -- local validation remains green with 68 pytest tests plus Ruff, mypy, npm audit, ESLint, and Vite build. +- a Ruff import-order finding in the new lifecycle test was auto-fixed, after which final validation passed Ruff, mypy across 53 source files, 68 pytest tests, npm audit with zero vulnerabilities, ESLint, and Vite production build. + + + +## Phase 7.4 — Quality SLO and Reliability History + +**Status:** In progress. + +Scope: + +- extend the source-controlled Federal Funds Rate quality contract with a 30-day reliability window and 99% minimum compliant-observation target; +- persist `quality_slo_observation` as Studio-owned operational history while leaving dbt test definitions and contract rules at their existing authorities; +- key observations by contract version plus dbt invocation identity so repeated reconciliation of identical evidence is idempotent; +- capture contract status, artifact/trust posture, pass rate, and active/blocking/warning incident totals for each observed evidence invocation; +- compute rolling `MEETING`, `AT_RISK`, `BREACHED`, or `PENDING` posture, compliance rate, outcome counts, and current compliant streak; +- expose `GET /api/v1/quality/reliability/summary` and `POST /api/v1/quality/reliability/capture`; +- make normal incident reconciliation capture reliability evidence automatically, while the dedicated capture endpoint remains a self-contained workbench action; +- add a Quality Reliability workbench and keep scheduled capture cadence, notification routing, and continuous-uptime claims outside this slice. + +Acceptance proof: +- re-running the idempotent metadata bootstrap creates `quality_slo_observation` in existing Studio PostgreSQL; +- the first capture of the current clean dbt invocation creates exactly one observation; repeating capture against the same invocation reports `observation_created=false` and leaves history unchanged; +- the live reliability summary reports `MEETING`, a 30-day window, 99% target, 100% observed compliance, 1 compliant observation, 0 blocked observations, and a clean streak of 1; +- the Reliability workbench presents the SLO target, current contract state, rolling posture, and invocation-level observation history; +- backend tests prove idempotent capture, clean SLO success, blocking-observation breach, and pending-before-first-capture behavior; +- local validation remains green with 72 pytest tests plus Ruff, mypy, npm audit, ESLint, and Vite build. diff --git a/docs/SkyData_RepoMap.md b/docs/SkyData_RepoMap.md index 09f603c..4472e9b 100644 --- a/docs/SkyData_RepoMap.md +++ b/docs/SkyData_RepoMap.md @@ -70,7 +70,8 @@ SkyDataStudio/ │ │ ├── pipeline_execution.py │ │ ├── pipeline_registry.py │ │ ├── quality_contracts.py -│ │ └── quality_incidents.py +│ │ ├── quality_incidents.py +│ │ └── quality_reliability.py │ └── web/ │ ├── eslint.config.js │ ├── index.html @@ -97,6 +98,7 @@ SkyDataStudio/ │ │ ├── Pipelines.jsx │ │ ├── PlaceholderPage.jsx │ │ ├── QualityIncidents.jsx +│ │ ├── QualityReliability.jsx │ │ ├── SchedulesBackfills.jsx │ │ ├── SemanticLayer.jsx │ │ ├── SourceMappings.jsx @@ -124,7 +126,8 @@ SkyDataStudio/ │ │ ├── 0002_source_target_mapping.sql │ │ ├── 0003_pipeline_definition.sql │ │ ├── 0004_pipeline_execution.sql -│ │ └── 0005_quality_incident.sql +│ │ ├── 0005_quality_incident.sql +│ │ └── 0006_quality_slo_observation.sql │ └── dbt/ │ └── Dockerfile ├── orchestration/ @@ -162,6 +165,7 @@ SkyDataStudio/ │ ├── test_quality_contracts.py │ ├── test_quality_gate_contracts.py │ ├── test_quality_incidents.py +│ ├── test_quality_reliability.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 5a17121..2090b8f 100644 --- a/docs/repository-map.txt +++ b/docs/repository-map.txt @@ -63,7 +63,8 @@ SkyDataStudio │ │ │ ├── pipeline_execution.py │ │ │ ├── pipeline_registry.py │ │ │ ├── quality_contracts.py -│ │ │ └── quality_incidents.py +│ │ │ ├── quality_incidents.py +│ │ │ └── quality_reliability.py │ │ ├── __init__.py │ │ └── main.py │ └── web @@ -84,6 +85,7 @@ SkyDataStudio │ │ │ ├── Pipelines.jsx │ │ │ ├── PlaceholderPage.jsx │ │ │ ├── QualityIncidents.jsx +│ │ │ ├── QualityReliability.jsx │ │ │ ├── SchedulesBackfills.jsx │ │ │ ├── SemanticLayer.jsx │ │ │ ├── SourceMappings.jsx @@ -118,7 +120,8 @@ SkyDataStudio │ │ ├── 0002_source_target_mapping.sql │ │ ├── 0003_pipeline_definition.sql │ │ ├── 0004_pipeline_execution.sql -│ │ └── 0005_quality_incident.sql +│ │ ├── 0005_quality_incident.sql +│ │ └── 0006_quality_slo_observation.sql │ ├── dbt │ │ └── Dockerfile │ └── docker-compose.yml @@ -158,6 +161,7 @@ SkyDataStudio │ ├── test_quality_contracts.py │ ├── test_quality_gate_contracts.py │ ├── test_quality_incidents.py +│ ├── test_quality_reliability.py │ ├── test_skycommand_client.py │ ├── test_skycommand_contracts.py │ └── test_validation_environment.py diff --git a/infra/db/migrations/0006_quality_slo_observation.sql b/infra/db/migrations/0006_quality_slo_observation.sql new file mode 100644 index 0000000..0d26671 --- /dev/null +++ b/infra/db/migrations/0006_quality_slo_observation.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS quality_slo_observation ( + id VARCHAR(36) PRIMARY KEY, + observation_key VARCHAR(420) NOT NULL UNIQUE, + contract_code VARCHAR(160) NOT NULL, + contract_version VARCHAR(40) NOT NULL, + evidence_invocation_id VARCHAR(160), + evidence_generated_at TIMESTAMPTZ, + contract_status VARCHAR(40) NOT NULL, + artifact_status VARCHAR(40) NOT NULL, + evidence_trust_posture VARCHAR(40) NOT NULL, + pass_rate DOUBLE PRECISION NOT NULL, + active_incident_count INTEGER NOT NULL DEFAULT 0, + blocking_active_incident_count INTEGER NOT NULL DEFAULT 0, + warning_active_incident_count INTEGER NOT NULL DEFAULT 0, + captured_at TIMESTAMPTZ NOT NULL +); + +CREATE INDEX IF NOT EXISTS ix_quality_slo_observation_contract_code + ON quality_slo_observation (contract_code); +CREATE INDEX IF NOT EXISTS ix_quality_slo_observation_evidence_invocation_id + ON quality_slo_observation (evidence_invocation_id); +CREATE INDEX IF NOT EXISTS ix_quality_slo_observation_contract_status + ON quality_slo_observation (contract_status); +CREATE INDEX IF NOT EXISTS ix_quality_slo_observation_artifact_status + ON quality_slo_observation (artifact_status); +CREATE INDEX IF NOT EXISTS ix_quality_slo_observation_evidence_trust_posture + ON quality_slo_observation (evidence_trust_posture); +CREATE INDEX IF NOT EXISTS ix_quality_slo_observation_captured_at + ON quality_slo_observation (captured_at); diff --git a/tests/test_platform.py b/tests/test_platform.py index c90c5f1..66a37a5 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.3 — Durable Quality Incidents and Remediation Lifecycle" + "Phase 7.4 — Quality SLO and Reliability History" ) assert {item["code"] for item in payload["capabilities"]} >= { "AIRFLOW", diff --git a/tests/test_quality_reliability.py b/tests/test_quality_reliability.py new file mode 100644 index 0000000..f6bfec6 --- /dev/null +++ b/tests/test_quality_reliability.py @@ -0,0 +1,133 @@ +from collections.abc import Generator +from datetime import UTC, datetime, timedelta + +import pytest +from skydata_studio.models.quality import QualitySloObservation +from skydata_studio.schemas.quality import QualityContractSummary, QualityIncidentSummary +from skydata_studio.services.quality_reliability import ( + capture_quality_reliability_observation, + quality_reliability_summary, +) +from sqlalchemy import create_engine +from sqlalchemy.orm import Session + + +@pytest.fixture +def session() -> Generator[Session, None, None]: + engine = create_engine("sqlite+pysqlite:///:memory:") + QualitySloObservation.__table__.create(engine) + with Session(engine) as db_session: + yield db_session + + +def _contract( + status: str = "COMPLIANT", + *, + invocation_id: str = "invocation-1", + pass_rate: float = 1.0, +) -> QualityContractSummary: + return QualityContractSummary.model_validate( + { + "contract_code": "FED_FUNDS_RATE_DAILY_QUALITY", + "contract_version": "1.1.0", + "contract_name": "Federal Funds Rate Daily Quality Contract", + "description": "Reliability proof contract.", + "target_name": "fct_fed_funds_rate_daily", + "layer": "MART", + "enforcement_mode": "BLOCK", + "artifact_status": "READY", + "evidence_trust_posture": "TRUSTED" if status == "COMPLIANT" else "BLOCKED", + "contract_status": status, + "minimum_pass_rate": 1.0, + "pass_rate": pass_rate, + "required_rule_count": 5, + "satisfied_rule_count": 5 if status == "COMPLIANT" else 4, + "warning_rule_count": 0, + "blocking_rule_count": 0 if status == "COMPLIANT" else 1, + "missing_rule_count": 0, + "source_path": "contracts/quality/fed_funds_rate_daily.v1.json", + "evidence_invocation_id": invocation_id, + "evidence_generated_at": "2026-08-12T18:36:30.823071Z", + "slo_window_days": 30, + "slo_minimum_compliance_rate": 0.99, + "rules": [], + } + ) + + +def _incidents(active: int = 0, blocking: int = 0) -> QualityIncidentSummary: + return QualityIncidentSummary.model_validate( + { + "contract_code": "FED_FUNDS_RATE_DAILY_QUALITY", + "contract_status": "COMPLIANT" if active == 0 else "BLOCKED", + "artifact_status": "READY", + "total_count": active, + "active_count": active, + "open_count": active, + "acknowledged_count": 0, + "resolved_count": 0, + "blocking_active_count": blocking, + "warning_active_count": max(active - blocking, 0), + "incidents": [], + } + ) + + +def test_reliability_capture_is_idempotent_per_dbt_invocation(session: Session) -> None: + contract = _contract() + + first = capture_quality_reliability_observation( + session, contract=contract, incidents=_incidents() + ) + second = capture_quality_reliability_observation( + session, contract=contract, incidents=_incidents() + ) + + assert first is True + assert second is False + assert len(session.query(QualitySloObservation).all()) == 1 + + +def test_reliability_meets_slo_for_clean_observation(session: Session) -> None: + contract = _contract() + capture_quality_reliability_observation(session, contract=contract, incidents=_incidents()) + + summary = quality_reliability_summary( + session, + contract=contract, + now=datetime.now(UTC) + timedelta(seconds=1), + ) + + assert summary.reliability_status == "MEETING" + assert summary.observation_count == 1 + assert summary.compliant_count == 1 + assert summary.observed_compliance_rate == 1.0 + assert summary.current_compliant_streak == 1 + + +def test_reliability_breaches_slo_when_blocking_observation_is_captured( + session: Session, +) -> None: + contract = _contract("BLOCKED", invocation_id="invocation-2", pass_rate=0.8) + capture_quality_reliability_observation( + session, contract=contract, incidents=_incidents(active=1, blocking=1) + ) + + summary = quality_reliability_summary( + session, + contract=contract, + now=datetime.now(UTC) + timedelta(seconds=1), + ) + + assert summary.reliability_status == "BREACHED" + assert summary.blocked_count == 1 + assert summary.observed_compliance_rate == 0.0 + assert summary.observations[0].blocking_active_incident_count == 1 + + +def test_reliability_is_pending_until_first_observation_exists(session: Session) -> None: + summary = quality_reliability_summary(session, contract=_contract()) + + assert summary.reliability_status == "PENDING" + assert summary.observation_count == 0 + assert summary.observed_compliance_rate == 0.0