Spec: ./spec.md Last synced with code: 2026-08-19
src/app/application/marts/quality/
├── __init__.py # public surface (audit_all, summarize, …)
├── check.py # MartCheck ABC + _finding() helper (62 LOC)
├── context.py # MartAuditContext, MartColumn, SourceTable (96 LOC)
├── auditor.py # catalog gathering + orchestration (232 LOC)
└── checks/
├── __init__.py # build_default_mart_checks() — flat list
├── row_count_drift.py # mart_hidden_despite_rows (95 LOC)
├── source_coverage.py # mart_source_coverage (66 LOC)
├── row_filter.py # mart_amount_filter_before_aggregation (92 LOC)
└── numeric_typing.py # mart_amount_column_is_text (97 LOC)
src/app/infrastructure/celery/tasks/mart_audit_tasks.py # openarg.audit_marts
810 LOC total in the application layer.
The spec's sibling module (013) has an ABC that caps a detector at one
finding. A mart legitimately yields several problems at once — three columns
typed TEXT is three findings, not one — so MartCheck.run() returns a
list. Everything downstream of the finding (Finding, Severity, Mode,
persist_findings, resolve_missing) is reused unchanged.
MartAuditContext is likewise deliberately not ResourceContext: that one
describes a downloaded file (raw_bytes, zip_member_names, http_status) and
roughly none of it applies to a materialized view.
collect_contexts(engine) runs five catalog queries once, then fans the
result out to every mart — so a sweep over 71 marts costs one pass, not one per
mart per check.
| Query | Source | Feeds |
|---|---|---|
_MART_ROWS_SQL |
mart_definitions |
registration, resolved SQL, serving_blocked |
_MART_STATS_SQL |
pg_class.reltuples for mart.* |
approx_row_count (-1 → None) |
_COLUMNS_SQL |
pg_attribute + format_type() |
real Postgres types per column |
_SOURCE_STATS_SQL |
pg_class.reltuples for raw.*/public.* |
source row estimates |
_TRAFFIC_SQL |
query_analytics, 30 d, served_table LIKE 'mart.%' |
hits_30d, success_rate_30d |
Two derived values are parsed out of the resolved SQL with regexes rather than re-resolving macros (which would need the engine, the live-version tables and a column introspection pass just to learn what a mart was built from):
_SOURCE_REF_RE→raw."name"/public."name"references →source_tables_COVERAGE_RE→ the/* macro_coverage: kept N of M */marker thatsql_macros._build_unionwrites at line 328 →candidate_table_count/kept_table_count
persist_findings upserts on
(resource_id, detector_name, detector_version, mode, input_hash). The task
builds input_hash as "<detector>:<discriminator>", where
finding_discriminator() picks, in order:
payload["finding_key"]— set by a check that emits several findings,payload["column"]— the per-column checks,- the mart id — the floor for checks emitting exactly one.
This is not cosmetic. On the auditor's first real run,
mart_hidden_despite_rows emitted both its signals for the same mart without
distinct keys: the WARN about a failed refresh overwrote the CRITICAL about
52 million unreachable rows, while the summary went on counting both. That is
why _finding() takes a key= and why FR-008/FR-009 exist.
| Where | What |
|---|---|
celery/app.py:168 |
task route → ingest queue |
celery/app.py:392 |
beat: crontab(hour=3, minute=45) — after refresh-via-b-marts-daily at 03:00, so it audits the state users will be served |
marts/mart.py:231-256 |
YAML loader reads serving.blocked / serving.blocked_reason; raises MartParseError when blocked without a reason |
mart_tasks.py::_upsert_mart_definition |
persists both fields on every build — that is what makes the block survive DROP + CREATE |
pg_sandbox_adapter.py:212 |
_blocked_mart_error() — execution-time enforcement (FR-016) |
| 8 discovery sites | NOT COALESCE(serving_blocked, FALSE) — see spec [DEBT-022-003] for the 2 that lack it |
| Rev | What | Why |
|---|---|---|
| 0054 | mart_definitions.serving_blocked BOOLEAN NOT NULL DEFAULT FALSE + serving_blocked_reason TEXT |
The only gate before it was last_row_count > 0, which covers empty. There was no way to say "has rows, but they are wrong". |
| 0055 | Extends the ck_ingestion_findings_mode CHECK with 'mart_audit' |
The mode vocabulary from 0033 enumerates the four ingestion phases. Folding mart audits into state_invariant would work at the cost of making the two indistinguishable in every ops query. |
openarg.audit_marts(persist: bool = True) -> dict
queue: ingest · soft_time_limit 600 · time_limit 900
returns {
marts_audited, marts_with_findings, findings,
by_severity: {critical: N, warn: N, …},
affected_marts: [mart_id, …],
persisted, resolved, persist
}
Any critical finding logs at WARNING on purpose (SC-006).
| File | Covers |
|---|---|
test_mart_quality_checks.py |
the four checks in isolation |
test_mart_rowcount_drift.py |
hidden-despite-rows, both signals |
test_mart_row_filters.py |
WHERE-before-GROUP-BY detection |
test_mart_audit_finding_keys.py |
FR-008/FR-009 — the collision that ate a CRITICAL |
test_mart_serving_block.py |
YAML load, persistence across rebuild, discovery filter |
test_sandbox_blocked_mart_execution.py |
FR-016 — execution-time enforcement |
- Subclass
MartCheckinchecks/, setname/version/severity. - Narrow with
applicable_to()— a check that cannot say anything about a mart must skip it, not emit a null finding. - Return a list. Set
key=on_finding()if more than one is possible. - Write the
remediationin the payload as an instruction, not a diagnosis. - Instantiate in
build_default_mart_checks(), in reading order. - Bump
versionwhen the logic changes: it is part of the idempotency tuple, so a bumped version re-opens findings under the new rule andresolve_missingcloses the old ones.
Carried from spec §9 — the two worth acting on first:
- [DEBT-022-001] nothing reads the findings. The sweep has run nightly
since 2026-07-27 and the four
build_failedmarts in production on 2026-08-19 were found by a manual audit. - [DEBT-022-002] a
builtmart that resolves to zero rows produces no finding at all, which is the state ofpresupuesto_consolidadoandpobreza_indec_aglomeradosin production today.