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.
+
+ No reliability observations yet.
+ Capture the latest evidence once; repeated capture of the same dbt invocation is intentionally idempotent.
+
+
+ ) : null}
+
+
+
+
+
+
+
+
ACCEPTANCE CONTRACT
What Phase 7.4 proves
7.4
+
+
01
SLO policy is versionedThe Federal Funds Rate quality contract owns a 30-day, 99% readiness objective.
+
02
History is idempotentA dbt invocation contributes at most one observation even when reconciliation is repeated.
+
03
Reliability is evidence-backedWindow compliance, streaks, trust posture, and incident counts come from durable observations rather than invented uptime.
+
+
+
+
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.
+
@@ -61,7 +61,7 @@ function StudioOverview() {
Platform API{apiState}FastAPI contract service
- Current Phase7.3durable quality incidents + remediation
+ Current Phase7.4quality SLO + reliability historyPipeline ProofREADYCurated mart materialization provenOrchestratorAirflow 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
-
01
Evidence-driven incident creationPersist only real warning, blocking, or missing contract outcomes; passing evidence creates no fake incident.
-
02
Durable remediation lifecyclePreserve acknowledgement, operator ownership, resolution notes, timestamps, and event history in Studio PostgreSQL.
-
03
Automatic recovery and recurrenceReturning PASS evidence resolves active incidents; a later failure reopens the same durable record as another occurrence.
+
01
Source-controlled SLO policyThe quality contract owns a 30-day, 99% consumer-readiness target alongside its blocking rules.
+
02
Idempotent reliability observationsEach dbt invocation contributes at most one durable SLO observation, so repeated reconciliation cannot inflate history.
+
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