{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "data-quality-metrics",
  "title": "Data Quality Metrics",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/data-quality-metrics/data-quality-metrics.component.ts",
      "content": "import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, computed, signal } from '@angular/core'\nimport {\n  Activity,\n  AlertCircle,\n  AlertTriangle,\n  Check,\n  CheckCircle2,\n  Clock,\n  Copy,\n  Database,\n  Download,\n  FileCode2,\n  Filter,\n  GitBranch,\n  Layers,\n  LucideAngularModule,\n  RefreshCw,\n  Search,\n  ShieldCheck,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiProgressComponent } from '@/ui/progress/progress.component'\nimport { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport {\n  UiSheetCloseComponent,\n  UiSheetComponent,\n  UiSheetContentComponent,\n  UiSheetDescriptionComponent,\n  UiSheetFooterComponent,\n  UiSheetHeaderComponent,\n  UiSheetTitleComponent,\n} from '@/ui/sheet/sheet.component'\nimport {\n  UiTableBodyComponent,\n  UiTableCellComponent,\n  UiTableComponent,\n  UiTableHeadComponent,\n  UiTableHeaderComponent,\n  UiTableRowComponent,\n} from '@/ui/table/table.component'\n\nexport type AssertionStatus = 'Passing' | 'Warning' | 'Failed'\nexport type AssertionCategory = 'Completeness' | 'Uniqueness' | 'Validity' | 'Volume' | 'Schema'\n\nexport interface AnomalyRecord {\n  recordId: string\n  column: string\n  value: string\n  reason: string\n}\n\nexport interface AssertionItem {\n  id: string\n  assertionType: string\n  columnName: string\n  category: AssertionCategory\n  ruleDefinition: string\n  targetThreshold: string\n  observedValue: string\n  observedPercent: number\n  failedCount: number\n  totalEvaluated: number\n  status: AssertionStatus\n  statusVariant: 'success' | 'warning' | 'destructive'\n  executionTime: string\n  expectationConfig: Record<string, unknown>\n  remediationQuery: string\n  anomalies: AnomalyRecord[]\n}\n\nexport const defaultAssertions: AssertionItem[] = [\n  {\n    id: 'dq-assert-1',\n    assertionType: 'expect_column_values_to_not_be_null',\n    columnName: 'user_id',\n    category: 'Completeness',\n    ruleDefinition: 'Primary key non-null identity constraint verification across all partition shards.',\n    targetThreshold: '100.0%',\n    observedValue: '100.0%',\n    observedPercent: 100.0,\n    failedCount: 0,\n    totalEvaluated: 1480000,\n    status: 'Passing',\n    statusVariant: 'success',\n    executionTime: '142ms',\n    expectationConfig: {\n      expectation_type: 'expect_column_values_to_not_be_null',\n      kwargs: {\n        column: 'user_id',\n        mostly: 1.0,\n      },\n    },\n    remediationQuery: `-- Primary key check passed cleanly. No quarantine needed.\\nSELECT COUNT(*) FROM production_analytics.dim_users WHERE user_id IS NULL;`,\n    anomalies: [],\n  },\n  {\n    id: 'dq-assert-2',\n    assertionType: 'expect_column_values_to_be_unique',\n    columnName: 'email',\n    category: 'Uniqueness',\n    ruleDefinition: 'Global case-insensitive deduplication check across customer directory.',\n    targetThreshold: '100.0%',\n    observedValue: '100.0%',\n    observedPercent: 100.0,\n    failedCount: 0,\n    totalEvaluated: 1480000,\n    status: 'Passing',\n    statusVariant: 'success',\n    executionTime: '380ms',\n    expectationConfig: {\n      expectation_type: 'expect_column_values_to_be_unique',\n      kwargs: {\n        column: 'email',\n        mostly: 1.0,\n      },\n    },\n    remediationQuery: `-- Zero duplicate emails detected in active partition.\\nSELECT LOWER(email), COUNT(*) FROM production_analytics.dim_users GROUP BY 1 HAVING COUNT(*) > 1;`,\n    anomalies: [],\n  },\n  {\n    id: 'dq-assert-3',\n    assertionType: 'expect_column_values_to_match_regex',\n    columnName: 'phone_e164',\n    category: 'Validity',\n    ruleDefinition: 'International telecommunications ITU-T E.164 format pattern matching (^\\\\+[1-9]\\\\d{1,14}$).',\n    targetThreshold: '≥ 98.0%',\n    observedValue: '98.8%',\n    observedPercent: 98.8,\n    failedCount: 17760,\n    totalEvaluated: 1480000,\n    status: 'Warning',\n    statusVariant: 'warning',\n    executionTime: '890ms',\n    expectationConfig: {\n      expectation_type: 'expect_column_values_to_match_regex',\n      kwargs: {\n        column: 'phone_e164',\n        regex: '^\\\\+[1-9]\\\\d{1,14}$',\n        mostly: 0.98,\n      },\n    },\n    remediationQuery: `-- Quarantine 17,760 legacy unformatted phone numbers for libphonenumber normalization:\\nCREATE OR REPLACE TABLE staging.unformatted_phones AS\\nSELECT user_id, email, phone_e164, 'MALFORMED_E164' AS failure_reason\\nFROM production_analytics.dim_users\\nWHERE phone_e164 NOT RLIKE '^\\\\+[1-9]\\\\d{1,14}$';`,\n    anomalies: [\n      {\n        recordId: 'usr_9941a8',\n        column: 'phone_e164',\n        value: '+1-555-019',\n        reason: 'Invalid E.164 length (incomplete national number)',\n      },\n      {\n        recordId: 'usr_1082fc',\n        column: 'phone_e164',\n        value: '0784910293',\n        reason: 'Missing international country dial prefix (+)',\n      },\n      {\n        recordId: 'usr_7729de',\n        column: 'phone_e164',\n        value: '+44 (0)20 7946',\n        reason: 'Unstripped parentheses and whitespace delimiter',\n      },\n      {\n        recordId: 'usr_4019ab',\n        column: 'phone_e164',\n        value: 'NULL',\n        reason: 'Unexpected empty field on SMS-enrolled user',\n      },\n    ],\n  },\n  {\n    id: 'dq-assert-4',\n    assertionType: 'expect_table_row_count_to_be_between',\n    columnName: 'table: dim_users',\n    category: 'Volume',\n    ruleDefinition: 'Daily snapshot table row count volumetric boundary expectation between 1.0M and 2.0M rows.',\n    targetThreshold: '1.0M .. 2.0M',\n    observedValue: '1,480,000',\n    observedPercent: 100.0,\n    failedCount: 0,\n    totalEvaluated: 1480000,\n    status: 'Passing',\n    statusVariant: 'success',\n    executionTime: '95ms',\n    expectationConfig: {\n      expectation_type: 'expect_table_row_count_to_be_between',\n      kwargs: {\n        min_value: 1000000,\n        max_value: 2000000,\n      },\n    },\n    remediationQuery: `-- Row count volumetric validation passed within safety boundaries (1.48M in [1.0M, 2.0M]).\\nSELECT COUNT(*) AS total_rows FROM production_analytics.dim_users;`,\n    anomalies: [],\n  },\n  {\n    id: 'dq-assert-5',\n    assertionType: 'expect_column_values_to_be_between',\n    columnName: 'age',\n    category: 'Validity',\n    ruleDefinition: 'Demographic account age boundary distribution constraint between 18 and 120 years.',\n    targetThreshold: '100.0%',\n    observedValue: '100.0%',\n    observedPercent: 100.0,\n    failedCount: 0,\n    totalEvaluated: 1480000,\n    status: 'Passing',\n    statusVariant: 'success',\n    executionTime: '210ms',\n    expectationConfig: {\n      expectation_type: 'expect_column_values_to_be_between',\n      kwargs: {\n        column: 'age',\n        min_value: 18,\n        max_value: 120,\n        mostly: 1.0,\n      },\n    },\n    remediationQuery: `-- All verified user ages lie cleanly between min 18 and max 94 (100.0% compliant).\\nSELECT MIN(age), MAX(age), AVG(age) FROM production_analytics.dim_users;`,\n    anomalies: [],\n  },\n]\n\nconst PILL_BASE =\n  'focus-visible:ring-ring inline-flex items-center rounded-full border px-2.5 py-1 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none'\nconst PILL_IDLE = 'border-border bg-card text-muted-foreground hover:bg-accent hover:text-foreground'\nconst CAT_BASE =\n  'focus-visible:ring-ring inline-flex items-center rounded-md border px-2 py-1 font-mono text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none'\n\n/**\n * Angular port of the React DataQualityMetrics block: dataset header with quality score and\n * run / export actions, four assertion metric cards, a filterable assertions table and an\n * anomaly inspector sheet with the expectation config and remediation SQL.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-data-quality-metrics, [ui-data-quality-metrics]',\n  standalone: true,\n  host: { class: 'contents' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiProgressComponent,\n    UiSeparatorComponent,\n    UiSheetComponent,\n    UiSheetCloseComponent,\n    UiSheetContentComponent,\n    UiSheetDescriptionComponent,\n    UiSheetFooterComponent,\n    UiSheetHeaderComponent,\n    UiSheetTitleComponent,\n    UiTableComponent,\n    UiTableBodyComponent,\n    UiTableCellComponent,\n    UiTableHeadComponent,\n    UiTableHeaderComponent,\n    UiTableRowComponent,\n  ],\n  template: `\n    <div data-slot=\"data-quality-metrics\" [class]=\"rootClass\">\n      <!-- Header Section -->\n      <div ui-card class=\"border-border bg-card shadow-xs\">\n        <div ui-card-content class=\"p-5 sm:p-6\">\n          <div class=\"flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between\">\n            <!-- Left: Dataset and Suite Metadata -->\n            <div class=\"space-y-2\">\n              <div class=\"flex flex-wrap items-center gap-2.5\">\n                <div\n                  class=\"bg-muted text-foreground border-border flex size-9 shrink-0 items-center justify-center rounded-lg border shadow-xs\"\n                >\n                  <lucide-icon [img]=\"Database\" class=\"text-success size-4.5\" />\n                </div>\n                <div>\n                  <div class=\"flex flex-wrap items-center gap-2\">\n                    <h1 class=\"text-foreground font-mono text-lg font-bold tracking-tight break-all sm:text-xl\">\n                      {{ datasetTitle }}\n                    </h1>\n                    <span ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs\">{{ suiteName }}</span>\n                  </div>\n                  <div class=\"text-muted-foreground flex flex-wrap items-center gap-2 pt-1 text-xs\">\n                    <span class=\"flex items-center gap-1\">\n                      <lucide-icon [img]=\"Clock\" class=\"size-3.5\" />\n                      {{ currentLastRun() }}\n                    </span>\n                    <span class=\"opacity-40\">·</span>\n                    <span class=\"font-mono\">Snowflake DW</span>\n                    <span class=\"opacity-40\">·</span>\n                    <span class=\"font-mono\">dbt Core v1.8</span>\n                  </div>\n                </div>\n              </div>\n            </div>\n\n            <!-- Right: Overall Score Card & Action Buttons -->\n            <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center\">\n              <div\n                class=\"border-success/30 bg-success/10 flex flex-wrap items-center gap-3.5 rounded-xl border p-3.5 shadow-xs\"\n              >\n                <div class=\"bg-success/20 text-success flex size-11 shrink-0 items-center justify-center rounded-lg\">\n                  <lucide-icon [img]=\"CheckCircle2\" class=\"size-6\" />\n                </div>\n                <div class=\"min-w-0 space-y-0.5\">\n                  <div class=\"flex flex-wrap items-baseline gap-2\">\n                    <span class=\"text-success text-2xl font-bold tracking-tight tabular-nums sm:text-3xl\"\n                      >{{ qualityScore }}%</span\n                    >\n                    <span ui-badge wrap variant=\"success\" class=\"gap-1 px-2 text-xs font-semibold\">\n                      <lucide-icon [img]=\"ShieldCheck\" class=\"size-3\" />\n                      Quality Score\n                    </span>\n                  </div>\n                  <p class=\"text-muted-foreground text-xs\">Automated Sodacl / GX Engine</p>\n                </div>\n              </div>\n\n              <div class=\"flex flex-wrap gap-2.5 sm:flex-col\">\n                <button\n                  ui-button\n                  variant=\"default\"\n                  size=\"sm\"\n                  class=\"gap-2 shadow-xs\"\n                  [disabled]=\"isRunningSuite()\"\n                  (click)=\"handleRunSuite()\"\n                >\n                  <lucide-icon [img]=\"RefreshCw\" [class]=\"refreshIconClass()\" />\n                  <span>{{ isRunningSuite() ? 'Evaluating Suite...' : 'Run Assertions Suite' }}</span>\n                </button>\n\n                <button ui-button variant=\"outline\" size=\"sm\" class=\"gap-2 shadow-xs\" (click)=\"exportQualityReport()\">\n                  @if (exportSuccess()) {\n                    <lucide-icon [img]=\"Check\" class=\"text-success size-4\" />\n                  } @else {\n                    <lucide-icon [img]=\"Download\" class=\"size-4\" />\n                  }\n                  <span>{{ exportSuccess() ? 'Report Downloaded!' : 'Export Quality Report' }}</span>\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Sub-bar Contract Guarantee -->\n        <div\n          class=\"bg-muted/40 border-border text-muted-foreground flex flex-col gap-2 border-t px-5 py-2.5 text-xs sm:flex-row sm:items-center sm:justify-between\"\n        >\n          <div class=\"flex items-center gap-2 font-mono\">\n            <span class=\"bg-success size-2 rounded-full\"></span>\n            <span>Schema Contract: Locked v3.2 · Zero Breaking Schema Drift</span>\n          </div>\n          <div class=\"flex items-center gap-3\">\n            <span>Engine SLA: <strong class=\"text-foreground font-medium\">Strict Production</strong></span>\n            <ui-separator orientation=\"vertical\" class=\"hidden h-3 sm:block\" />\n            <span>48 assertions evaluated</span>\n          </div>\n        </div>\n      </div>\n\n      <!-- 4 Assertion Metric Cards -->\n      <div class=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n        <!-- Card 1: Passed Assertions -->\n        <div ui-card class=\"border-border bg-card hover:border-border shadow-xs transition-colors\">\n          <div ui-card-header class=\"p-4 pb-2\">\n            <div class=\"flex items-start justify-between gap-2\">\n              <p class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Passed Assertions</p>\n              <div\n                class=\"border-success/20 bg-success/10 text-success flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"CheckCircle2\" class=\"size-4\" />\n              </div>\n            </div>\n            <div class=\"flex items-baseline gap-2 pt-1\">\n              <span class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">47 / 48</span>\n              <span ui-badge wrap variant=\"success\" class=\"text-xs font-medium\">98% Passing</span>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-2 p-4 pt-1\">\n            <div ui-progress [value]=\"98\" class=\"h-1.5 w-full\"></div>\n            <p class=\"text-muted-foreground text-xs\">{{ passedAssertions }}</p>\n          </div>\n        </div>\n\n        <!-- Card 2: Failed / Warnings -->\n        <div ui-card class=\"border-border bg-card hover:border-border shadow-xs transition-colors\">\n          <div ui-card-header class=\"p-4 pb-2\">\n            <div class=\"flex items-start justify-between gap-2\">\n              <p class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Failed / Warnings</p>\n              <div\n                class=\"border-warning/20 bg-warning/10 text-warning flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"AlertTriangle\" class=\"size-4\" />\n              </div>\n            </div>\n            <div class=\"flex items-baseline gap-2 pt-1\">\n              <span class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">1 Warning</span>\n              <span ui-badge wrap variant=\"warning\" class=\"text-xs font-medium\">1.2% Non-conformant</span>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-2 p-4 pt-1\">\n            <div ui-progress [value]=\"1.2\" class=\"h-1.5 w-full\"></div>\n            <p class=\"text-muted-foreground text-xs\">{{ failedWarnings }}</p>\n          </div>\n        </div>\n\n        <!-- Card 3: Schema Drift Status -->\n        <div ui-card class=\"border-border bg-card hover:border-border shadow-xs transition-colors\">\n          <div ui-card-header class=\"p-4 pb-2\">\n            <div class=\"flex items-start justify-between gap-2\">\n              <p class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Schema Drift Status</p>\n              <div\n                class=\"border-info/20 bg-info/10 text-info flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"GitBranch\" class=\"size-4\" />\n              </div>\n            </div>\n            <div class=\"flex items-baseline gap-2 pt-1\">\n              <span class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">0 Breaking</span>\n              <span ui-badge wrap variant=\"outline\" class=\"text-success text-xs font-medium\">No Drift</span>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-2 p-4 pt-1\">\n            <div class=\"bg-muted/40 flex items-center justify-between gap-x-2 rounded-md px-2 py-1 text-xs\">\n              <span class=\"text-muted-foreground\">Columns Synced</span>\n              <span class=\"text-foreground font-mono font-medium\">14 / 14 matched</span>\n            </div>\n            <p class=\"text-muted-foreground text-xs\">{{ schemaDriftStatus }}</p>\n          </div>\n        </div>\n\n        <!-- Card 4: Total Rows Audited -->\n        <div ui-card class=\"border-border bg-card hover:border-border shadow-xs transition-colors\">\n          <div ui-card-header class=\"p-4 pb-2\">\n            <div class=\"flex items-start justify-between gap-2\">\n              <p class=\"text-muted-foreground text-xs font-medium tracking-wider uppercase\">Total Rows Audited</p>\n              <div\n                class=\"bg-muted text-foreground border-border flex size-7 items-center justify-center rounded-md border\"\n              >\n                <lucide-icon [img]=\"Layers\" class=\"size-4\" />\n              </div>\n            </div>\n            <div class=\"flex items-baseline gap-2 pt-1\">\n              <span class=\"text-foreground font-mono text-2xl font-bold tracking-tight tabular-nums\">1,480,000</span>\n              <span ui-badge wrap variant=\"outline\" class=\"font-mono text-xs font-normal\">1.48M Rows</span>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-2 p-4 pt-1\">\n            <div class=\"bg-muted/40 flex items-center justify-between gap-x-2 rounded-md px-2 py-1 text-xs\">\n              <span class=\"text-muted-foreground\">Partition Range</span>\n              <span class=\"text-foreground font-mono font-medium\">1.0M .. 2.0M</span>\n            </div>\n            <p class=\"text-muted-foreground text-xs\">{{ totalRowsAudited }}</p>\n          </div>\n        </div>\n      </div>\n\n      <!-- Data Quality Test Assertions Table Card -->\n      <div ui-card class=\"border-border bg-card shadow-xs\">\n        <div ui-card-header class=\"flex flex-col gap-4 pb-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div>\n            <div class=\"flex items-center gap-2\">\n              <div ui-card-title class=\"text-base font-semibold\">Data Quality Test Assertions</div>\n              <span ui-badge wrap variant=\"secondary\" class=\"font-mono text-xs\"\n                >{{ filteredAssertions().length }} of {{ assertionList().length }} Assertions</span\n              >\n            </div>\n            <div ui-card-description class=\"text-xs\">\n              Automated expectations, boundary validations, regex format rules, and volume anomaly checks.\n            </div>\n          </div>\n\n          <!-- Filter Pills by Status -->\n          <div class=\"flex flex-wrap items-center gap-1.5\">\n            <button type=\"button\" [class]=\"statusPillClass('all')\" (click)=\"selectedStatus.set('all')\">\n              All Assertions ({{ assertionList().length }})\n            </button>\n            <button type=\"button\" [class]=\"statusPillClass('passing')\" (click)=\"selectedStatus.set('passing')\">\n              Passing (4)\n            </button>\n            <button type=\"button\" [class]=\"statusPillClass('warning')\" (click)=\"selectedStatus.set('warning')\">\n              Warnings (1)\n            </button>\n          </div>\n        </div>\n\n        <!-- Filter & Search Toolbar -->\n        <div class=\"border-border/60 border-t p-4 pt-3 pb-3\">\n          <div class=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n            <div class=\"relative flex-1\">\n              <lucide-icon\n                [img]=\"Search\"\n                class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-3.5 -translate-y-1/2\"\n              />\n              <input\n                [value]=\"searchQuery()\"\n                (input)=\"onSearch($event)\"\n                type=\"text\"\n                placeholder=\"Search by assertion type, column name, or rule definition...\"\n                class=\"border-border bg-background placeholder:text-muted-foreground focus-visible:ring-ring h-8 w-full rounded-md border px-3 pl-8 text-xs focus-visible:ring-2 focus-visible:outline-none\"\n              />\n            </div>\n\n            <div class=\"flex flex-wrap items-center gap-1.5\">\n              <span class=\"text-muted-foreground mr-1 flex items-center gap-1 text-xs font-medium\">\n                <lucide-icon [img]=\"Filter\" class=\"size-3\" />\n                Category:\n              </span>\n              @for (cat of availableCategories(); track cat) {\n                <button type=\"button\" [class]=\"categoryClass(cat)\" (click)=\"selectedCategory.set(cat)\">\n                  {{ cat === 'all' ? 'All Categories' : cat }}\n                </button>\n              }\n            </div>\n          </div>\n        </div>\n\n        <!-- Table Container -->\n        <div ui-card-content class=\"p-0\">\n          <div class=\"border-border/60 overflow-x-auto border-t\">\n            <div ui-table>\n              <thead ui-table-header class=\"bg-muted/50\">\n                <tr ui-table-row>\n                  <th ui-table-head class=\"min-w-[260px] text-xs\">Assertion &amp; Target Column</th>\n                  <th ui-table-head class=\"min-w-[100px] text-xs\">Category</th>\n                  <th ui-table-head class=\"min-w-[120px] text-xs\">Target Threshold</th>\n                  <th ui-table-head class=\"min-w-[120px] text-xs\">Observed Value</th>\n                  <th ui-table-head class=\"min-w-[110px] text-xs\">Status</th>\n                  <th ui-table-head class=\"min-w-[130px] text-right text-xs\">Actions</th>\n                </tr>\n              </thead>\n              <tbody ui-table-body>\n                @for (assertion of filteredAssertions(); track assertion.id) {\n                  <tr ui-table-row class=\"hover:bg-muted/20 transition-colors\">\n                    <td ui-table-cell class=\"py-3\">\n                      <div class=\"space-y-1\">\n                        <div class=\"flex flex-wrap items-center gap-2\">\n                          <span ui-badge wrap variant=\"outline\" class=\"border-border font-mono text-xs font-semibold\">{{\n                            assertion.columnName\n                          }}</span>\n                          <span class=\"text-foreground font-mono text-xs font-medium\">{{\n                            assertion.assertionType\n                          }}</span>\n                        </div>\n                        <p class=\"text-muted-foreground max-w-sm text-xs leading-relaxed sm:max-w-md\">\n                          {{ assertion.ruleDefinition }}\n                        </p>\n                      </div>\n                    </td>\n\n                    <td ui-table-cell class=\"py-3\">\n                      <span ui-badge wrap variant=\"secondary\" class=\"font-normal\">{{ assertion.category }}</span>\n                    </td>\n\n                    <td ui-table-cell class=\"py-3\">\n                      <span class=\"text-muted-foreground font-mono text-xs font-medium\">{{\n                        assertion.targetThreshold\n                      }}</span>\n                    </td>\n\n                    <td ui-table-cell class=\"py-3\">\n                      <div class=\"space-y-0.5 font-mono text-xs\">\n                        <span [class]=\"observedClass(assertion, 'font-bold tabular-nums')\">{{\n                          assertion.observedValue\n                        }}</span>\n                        <span class=\"text-muted-foreground/70 block text-xs\">{{ assertion.executionTime }} eval</span>\n                      </div>\n                    </td>\n\n                    <td ui-table-cell class=\"py-3\">\n                      <span ui-badge wrap [variant]=\"assertion.statusVariant\" class=\"gap-1 text-xs font-medium\">\n                        @if (assertion.status === 'Passing') {\n                          <lucide-icon [img]=\"CheckCircle2\" class=\"size-3\" />\n                        } @else if (assertion.status === 'Warning') {\n                          <lucide-icon [img]=\"AlertTriangle\" class=\"size-3\" />\n                        } @else {\n                          <lucide-icon [img]=\"AlertCircle\" class=\"size-3\" />\n                        }\n                        {{ assertion.status }}\n                      </span>\n                    </td>\n\n                    <td ui-table-cell class=\"py-3 text-right\">\n                      <button\n                        ui-button\n                        variant=\"outline\"\n                        size=\"xs\"\n                        class=\"h-7 gap-1 text-xs font-medium shadow-xs\"\n                        (click)=\"handleInspectAnomalies(assertion)\"\n                      >\n                        <lucide-icon [img]=\"Activity\" class=\"size-3\" />\n                        <span>Inspect Anomalies</span>\n                      </button>\n                    </td>\n                  </tr>\n                }\n\n                @if (filteredAssertions().length === 0) {\n                  <tr ui-table-row>\n                    <td ui-table-cell colspan=\"6\" class=\"py-8 text-center\">\n                      <div class=\"flex flex-col items-center justify-center gap-1.5\">\n                        <lucide-icon [img]=\"ShieldCheck\" class=\"text-success size-7\" />\n                        <p class=\"text-foreground text-sm font-medium\">No assertions match your filter query</p>\n                        <p class=\"text-muted-foreground text-xs\">All assertions meet standard quality constraints.</p>\n                      </div>\n                    </td>\n                  </tr>\n                }\n              </tbody>\n            </div>\n          </div>\n\n          <!-- Table Footer -->\n          <div\n            class=\"border-border/60 bg-muted/20 text-muted-foreground flex flex-col gap-2 border-t px-4 py-2.5 text-xs sm:flex-row sm:items-center sm:justify-between\"\n          >\n            <div class=\"flex items-center gap-2\">\n              <span class=\"bg-success size-2 rounded-full\"></span>\n              <span>Great Expectations Assertions Runner: 100% evaluated</span>\n            </div>\n            <span class=\"font-mono text-xs\">Dataset: {{ datasetTitle }} · Last Batch: 1,480,000 rows</span>\n          </div>\n        </div>\n      </div>\n\n      <!-- Anomaly Inspector Drawer / Sheet -->\n      <div ui-sheet [open]=\"isDrawerOpen()\" (openChange)=\"isDrawerOpen.set($event)\">\n        <div ui-sheet-content class=\"w-full space-y-6 overflow-y-auto p-6 sm:max-w-xl md:max-w-2xl\">\n          @if (selectedAssertion(); as sel) {\n            <div ui-sheet-header class=\"space-y-2 p-0 text-left\">\n              <div class=\"flex items-center justify-between gap-3 pr-6\">\n                <div class=\"flex items-center gap-2\">\n                  <span ui-badge wrap [variant]=\"sel.statusVariant\" class=\"gap-1 font-mono text-xs font-semibold\">\n                    @if (sel.status === 'Passing') {\n                      <lucide-icon [img]=\"CheckCircle2\" class=\"size-3\" />\n                    } @else {\n                      <lucide-icon [img]=\"AlertTriangle\" class=\"size-3\" />\n                    }\n                    {{ sel.status.toUpperCase() }}\n                  </span>\n                  <span ui-badge wrap variant=\"outline\" class=\"font-mono text-xs font-semibold\">{{\n                    sel.category\n                  }}</span>\n                </div>\n\n                <span class=\"text-muted-foreground font-mono text-xs\">Latency: {{ sel.executionTime }}</span>\n              </div>\n\n              <h2 ui-sheet-title class=\"text-lg font-bold tracking-tight\">{{ sel.assertionType }}</h2>\n              <p ui-sheet-description class=\"text-foreground/90 font-mono text-xs\">\n                Target Column: <strong class=\"text-foreground font-semibold\">{{ sel.columnName }}</strong>\n              </p>\n            </div>\n\n            <div class=\"space-y-5 text-xs\">\n              <!-- Summary Metrics Cards Grid -->\n              <div class=\"grid grid-cols-2 gap-2.5 sm:grid-cols-4\">\n                <div class=\"bg-muted/40 border-border/80 rounded-lg border p-3\">\n                  <span class=\"text-muted-foreground text-xs\">Target Threshold</span>\n                  <p class=\"text-foreground mt-1 font-mono text-sm font-bold\">{{ sel.targetThreshold }}</p>\n                </div>\n                <div class=\"bg-muted/40 border-border/80 rounded-lg border p-3\">\n                  <span class=\"text-muted-foreground text-xs\">Observed Pass Rate</span>\n                  <p [class]=\"observedClass(sel, 'mt-1 font-mono text-sm font-bold tabular-nums')\">\n                    {{ sel.observedValue }}\n                  </p>\n                </div>\n                <div class=\"bg-muted/40 border-border/80 rounded-lg border p-3\">\n                  <span class=\"text-muted-foreground text-xs\">Audited Records</span>\n                  <p class=\"text-foreground mt-1 font-mono text-sm font-bold tabular-nums\">\n                    {{ formatNumber(sel.totalEvaluated) }}\n                  </p>\n                </div>\n                <div class=\"bg-muted/40 border-border/80 rounded-lg border p-3\">\n                  <span class=\"text-muted-foreground text-xs\">Failed Anomalies</span>\n                  <p [class]=\"failedClass(sel)\">{{ formatNumber(sel.failedCount) }}</p>\n                </div>\n              </div>\n\n              <!-- Expectation Configuration DSL Box -->\n              <div class=\"space-y-2\">\n                <div class=\"flex items-center justify-between gap-x-2\">\n                  <h3 class=\"text-muted-foreground text-xs font-semibold tracking-wider uppercase\">\n                    Great Expectations Specification\n                  </h3>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"xs\"\n                    class=\"h-6 gap-1 px-2 text-xs\"\n                    (click)=\"copyCodeSnippet('dsl', configJson(sel))\"\n                  >\n                    @if (copiedSnippetKey() === 'dsl') {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                    } @else {\n                      <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                    }\n                    <span>{{ copiedSnippetKey() === 'dsl' ? 'Copied' : 'Copy Config' }}</span>\n                  </button>\n                </div>\n                <div class=\"overflow-hidden rounded-md border bg-zinc-950 font-mono text-xs text-zinc-100 shadow-inner\">\n                  <div\n                    class=\"text-muted-foreground flex items-center gap-2 border-b border-zinc-800 bg-zinc-900/90 px-3 py-1.5 text-xs\"\n                  >\n                    <lucide-icon [img]=\"FileCode2\" class=\"size-3.5\" />\n                    <span>expectation_suite.json</span>\n                  </div>\n                  <pre class=\"overflow-x-auto p-3 text-xs leading-relaxed text-zinc-300 select-text\">{{\n                    configJson(sel)\n                  }}</pre>\n                </div>\n              </div>\n\n              <!-- Anomalous Samples Table or Clean Pass State -->\n              <div class=\"space-y-2\">\n                <div class=\"flex items-center justify-between gap-x-2\">\n                  <h3 class=\"text-muted-foreground text-xs font-semibold tracking-wider uppercase\">\n                    Anomalous Sample Records\n                  </h3>\n                  @if (sel.anomalies.length > 0) {\n                    <span class=\"text-muted-foreground font-mono text-xs\"\n                      >Showing {{ sel.anomalies.length }} failure samples</span\n                    >\n                  }\n                </div>\n\n                @if (sel.anomalies.length > 0) {\n                  <div class=\"border-border/80 overflow-hidden rounded-md border shadow-xs\">\n                    <div ui-table>\n                      <thead ui-table-header class=\"bg-muted/40\">\n                        <tr ui-table-row>\n                          <th ui-table-head class=\"text-xs\">Record ID</th>\n                          <th ui-table-head class=\"text-xs\">Column</th>\n                          <th ui-table-head class=\"text-xs\">Observed Value</th>\n                          <th ui-table-head class=\"text-xs\">Diagnostic Reason</th>\n                        </tr>\n                      </thead>\n                      <tbody ui-table-body>\n                        @for (anomaly of sel.anomalies; track anomaly.recordId) {\n                          <tr ui-table-row>\n                            <td ui-table-cell class=\"font-mono text-xs font-medium\">{{ anomaly.recordId }}</td>\n                            <td ui-table-cell class=\"font-mono text-xs\">{{ anomaly.column }}</td>\n                            <td ui-table-cell class=\"text-destructive font-mono text-xs font-semibold\">\n                              {{ anomaly.value }}\n                            </td>\n                            <td ui-table-cell class=\"text-muted-foreground text-xs\">{{ anomaly.reason }}</td>\n                          </tr>\n                        }\n                      </tbody>\n                    </div>\n                  </div>\n                } @else {\n                  <div\n                    class=\"bg-muted/20 border-border/80 flex flex-col items-center justify-center gap-1.5 rounded-md border p-6 text-center\"\n                  >\n                    <lucide-icon [img]=\"CheckCircle2\" class=\"text-success size-6\" />\n                    <p class=\"text-foreground text-xs font-semibold\">Zero Anomalies Detected</p>\n                    <p class=\"text-muted-foreground text-xs\">\n                      All {{ formatNumber(sel.totalEvaluated) }} audited records strictly satisfied the expectation\n                      rule.\n                    </p>\n                  </div>\n                }\n              </div>\n\n              <!-- Quarantine SQL Script -->\n              <div class=\"space-y-2\">\n                <div class=\"flex items-center justify-between gap-x-2\">\n                  <h3 class=\"text-muted-foreground text-xs font-semibold tracking-wider uppercase\">\n                    Quarantine &amp; Remediation SQL\n                  </h3>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"xs\"\n                    class=\"h-6 gap-1 px-2 text-xs\"\n                    (click)=\"copyCodeSnippet('sql', sel.remediationQuery)\"\n                  >\n                    @if (copiedSnippetKey() === 'sql') {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                    } @else {\n                      <lucide-icon [img]=\"Copy\" class=\"size-3\" />\n                    }\n                    <span>{{ copiedSnippetKey() === 'sql' ? 'Copied' : 'Copy SQL' }}</span>\n                  </button>\n                </div>\n                <div\n                  class=\"border-border bg-muted/60 rounded-md border p-3 font-mono text-xs leading-relaxed select-text\"\n                >\n                  <pre class=\"text-foreground overflow-x-auto whitespace-pre-wrap\">{{ sel.remediationQuery }}</pre>\n                </div>\n              </div>\n            </div>\n\n            <div ui-sheet-footer class=\"border-border/80 flex flex-row items-center justify-end gap-2 border-t pt-4\">\n              <button ui-button ui-sheet-close variant=\"outline\" size=\"sm\" class=\"text-xs\">Close</button>\n\n              @if (sel.anomalies.length > 0) {\n                <button\n                  ui-button\n                  variant=\"default\"\n                  size=\"sm\"\n                  class=\"gap-1.5 text-xs font-medium shadow-xs\"\n                  (click)=\"exportAnomalyCsv()\"\n                >\n                  @if (anomalyExportSuccess()) {\n                    <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n                  } @else {\n                    <lucide-icon [img]=\"Download\" class=\"size-3.5\" />\n                  }\n                  <span>{{ anomalyExportSuccess() ? 'Anomalies Exported!' : 'Export Anomaly CSV' }}</span>\n                </button>\n              }\n            </div>\n          }\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiDataQualityMetricsComponent implements OnChanges {\n  @Input() datasetTitle = 'production_analytics.dim_users'\n  @Input() suiteName = 'user_profile_integrity_v3'\n  @Input() qualityScore = 98.4\n  @Input() lastRun = 'Ran 14m ago · 48 assertions evaluated'\n  @Input() passedAssertions = '47 / 48 Passing · 98%'\n  @Input() failedWarnings = '1 Warning · Null phone numbers 1.2%'\n  @Input() schemaDriftStatus = '0 breaking schema changes'\n  @Input() totalRowsAudited = '1,480,000 rows'\n  @Input() set assertions(value: AssertionItem[] | null | undefined) {\n    this.assertionList.set(value ?? defaultAssertions)\n  }\n  @Input('class') className?: string\n\n  protected readonly Activity = Activity\n  protected readonly AlertCircle = AlertCircle\n  protected readonly AlertTriangle = AlertTriangle\n  protected readonly Check = Check\n  protected readonly CheckCircle2 = CheckCircle2\n  protected readonly Clock = Clock\n  protected readonly Copy = Copy\n  protected readonly Database = Database\n  protected readonly Download = Download\n  protected readonly FileCode2 = FileCode2\n  protected readonly Filter = Filter\n  protected readonly GitBranch = GitBranch\n  protected readonly Layers = Layers\n  protected readonly RefreshCw = RefreshCw\n  protected readonly Search = Search\n  protected readonly ShieldCheck = ShieldCheck\n\n  readonly assertionList = signal<AssertionItem[]>(defaultAssertions)\n  readonly searchQuery = signal('')\n  readonly selectedStatus = signal<string>('all')\n  readonly selectedCategory = signal<string>('all')\n  readonly isRunningSuite = signal(false)\n  readonly currentLastRun = signal(this.lastRun)\n  readonly selectedAssertion = signal<AssertionItem | null>(null)\n  readonly isDrawerOpen = signal(false)\n  readonly copiedSnippetKey = signal<string | null>(null)\n  readonly exportSuccess = signal(false)\n  readonly anomalyExportSuccess = signal(false)\n\n  readonly availableCategories = computed(() => {\n    const categories = new Set<string>()\n    this.assertionList().forEach((item) => categories.add(item.category))\n    return ['all', ...Array.from(categories)]\n  })\n\n  readonly filteredAssertions = computed(() => {\n    const query = this.searchQuery().trim().toLowerCase()\n    const statusFilter = this.selectedStatus()\n    const categoryFilter = this.selectedCategory()\n\n    return this.assertionList().filter((item) => {\n      const matchesStatus = statusFilter === 'all' || item.status.toLowerCase() === statusFilter.toLowerCase()\n      const matchesCategory = categoryFilter === 'all' || item.category.toLowerCase() === categoryFilter.toLowerCase()\n      const matchesQuery =\n        !query ||\n        item.assertionType.toLowerCase().includes(query) ||\n        item.columnName.toLowerCase().includes(query) ||\n        item.ruleDefinition.toLowerCase().includes(query) ||\n        item.category.toLowerCase().includes(query)\n\n      return matchesStatus && matchesCategory && matchesQuery\n    })\n  })\n\n  // React seeds currentLastRun from the lastRun prop once (useState initial value).\n  private lastRunSeeded = false\n  ngOnChanges(changes: SimpleChanges): void {\n    if (changes['lastRun'] && !this.lastRunSeeded) {\n      this.currentLastRun.set(this.lastRun)\n    }\n    this.lastRunSeeded = true\n  }\n\n  get rootClass(): string {\n    return cn('w-full space-y-6', this.className)\n  }\n\n  refreshIconClass(): string {\n    return cn('size-4', this.isRunningSuite() && 'animate-spin')\n  }\n\n  statusPillClass(status: 'all' | 'passing' | 'warning'): string {\n    const active = {\n      all: 'border-primary bg-primary text-primary-foreground shadow-xs',\n      passing: 'border-success bg-success bg-success text-white shadow-xs',\n      warning: 'border-warning bg-warning bg-warning text-white shadow-xs',\n    }[status]\n    return cn(PILL_BASE, this.selectedStatus() === status ? active : PILL_IDLE)\n  }\n\n  categoryClass(cat: string): string {\n    return cn(\n      CAT_BASE,\n      this.selectedCategory() === cat ? 'border-foreground bg-foreground text-background shadow-xs' : PILL_IDLE,\n    )\n  }\n\n  observedClass(assertion: AssertionItem, base: string): string {\n    return cn(base, assertion.status === 'Passing' ? 'text-success' : 'text-warning')\n  }\n\n  failedClass(assertion: AssertionItem): string {\n    return cn(\n      'mt-1 font-mono text-sm font-bold tabular-nums',\n      assertion.failedCount > 0 ? 'text-warning' : 'text-success',\n    )\n  }\n\n  formatNumber(value: number): string {\n    return value.toLocaleString()\n  }\n\n  configJson(assertion: AssertionItem): string {\n    return JSON.stringify(assertion.expectationConfig, null, 2)\n  }\n\n  onSearch(event: Event): void {\n    this.searchQuery.set((event.target as HTMLInputElement).value)\n  }\n\n  handleRunSuite(): void {\n    if (this.isRunningSuite()) return\n    this.isRunningSuite.set(true)\n\n    setTimeout(() => {\n      this.isRunningSuite.set(false)\n      this.currentLastRun.set('Ran just now · 48 assertions evaluated')\n    }, 750)\n  }\n\n  handleInspectAnomalies(assertion: AssertionItem): void {\n    this.selectedAssertion.set(assertion)\n    this.isDrawerOpen.set(true)\n  }\n\n  copyCodeSnippet(key: string, content: string): void {\n    if (typeof navigator !== 'undefined' && navigator.clipboard) {\n      void navigator.clipboard.writeText(content)\n      this.copiedSnippetKey.set(key)\n      setTimeout(() => {\n        if (this.copiedSnippetKey() === key) this.copiedSnippetKey.set(null)\n      }, 2000)\n    }\n  }\n\n  exportQualityReport(): void {\n    const report = {\n      dataset: this.datasetTitle,\n      suite: this.suiteName,\n      overallQualityScore: this.qualityScore,\n      evaluatedAt: new Date().toISOString(),\n      engine: 'Great Expectations v0.18 / SodaCL',\n      summary: {\n        passed: this.passedAssertions,\n        warnings: this.failedWarnings,\n        schemaDrift: this.schemaDriftStatus,\n        totalRows: this.totalRowsAudited,\n      },\n      assertions: this.assertionList(),\n    }\n\n    const jsonStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(report, null, 2))\n    if (typeof document !== 'undefined') {\n      const link = document.createElement('a')\n      link.setAttribute('href', jsonStr)\n      link.setAttribute('download', `dq-report-${this.suiteName}-${new Date().toISOString().slice(0, 10)}.json`)\n      document.body.appendChild(link)\n      link.click()\n      link.remove()\n\n      this.exportSuccess.set(true)\n      setTimeout(() => {\n        this.exportSuccess.set(false)\n      }, 2000)\n    }\n  }\n\n  exportAnomalyCsv(): void {\n    const selected = this.selectedAssertion()\n    if (!selected || selected.anomalies.length === 0) return\n\n    const headers = ['Record ID', 'Column', 'Observed Value', 'Failure Reason']\n    const rows = selected.anomalies.map((a) => [a.recordId, a.column, `\"${a.value}\"`, `\"${a.reason}\"`])\n    const csvContent = [headers.join(','), ...rows.map((r) => r.join(','))].join('\\n')\n\n    const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })\n    const url = URL.createObjectURL(blob)\n    const link = document.createElement('a')\n    link.setAttribute('href', url)\n    link.setAttribute('download', `anomalies-${selected.columnName}.csv`)\n    document.body.appendChild(link)\n    link.click()\n    link.remove()\n\n    this.anomalyExportSuccess.set(true)\n    setTimeout(() => {\n      this.anomalyExportSuccess.set(false)\n    }, 2000)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/data-quality-metrics/data-quality-metrics.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/data-quality-metrics/index.ts",
      "content": "export {\n  UiDataQualityMetricsComponent,\n  defaultAssertions,\n  type AnomalyRecord,\n  type AssertionCategory,\n  type AssertionItem,\n  type AssertionStatus,\n} from './data-quality-metrics.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/data-quality-metrics/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/progress.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/sheet.json",
    "https://uipkge.dev/r/angular/table.json"
  ],
  "description": "Great Expectations and Soda style automated data quality assertions, schema validation, and drift detector dashboard: top header with overall quality score and run triggers, 4 assertion metric cards, data quality assertions table with category filters, and an interactive anomaly inspector drawer with SQL quarantine remediation.",
  "categories": [
    "devops",
    "dashboard",
    "app",
    "data"
  ]
}