# GOLD-Permits ETL audit — entry point

**Audience:** a future GOLD-Permits ETL-audit agent. Read this first; it indexes every canonical
source on "why roof permits go missing." **Do not copy long content — each link below is the
owning file; this page is the map + the numbers, not the prose.**

**Scope of the evidence:** BuildZoom permit feed → `Roofing_Master_Gold_V1` (Apache Hudi COW,
`s3://8020rei-gold-data-lake/`). Primary audit counties: FL-7 CallZeke
(`12053 12101 12103 12031 12095 12115 12086`), extended nationally to 10 counties / 8 states
(finding 99). Dates 2026-05-19 → 2026-06-02.

---

## Bottom line

Roof permits go missing by **two independent mechanisms, both quantified, both ours (not the
vendor).** **Mechanism A** — permits *are* written to Gold but are mis-attributed downstream: the
permit→property link (`FA_PROPERTYID`) is keyed on a corrupt `STREET4JOIN1` join key and is
wrong/empty in the **majority** of populated rows (Pinellas FA mislink **82.9%**; empty-FA
**13–21%**; `ISSUED_DATE` null **44–72%**), so a real roof permit becomes invisible to the
recency filter — this shipped **518 false-positive CallZeke leads (3.45% of 15K)** and corrupts
training labels (**~14.78% positive-label loss**, **~166K duplicate-positive inflation**). The Gold
table additionally **fails its own QA: 108/167 ground-truth checks fail** (finding 91) and is
**non-reproducible** (re-commits ≈ every 3 min). **Mechanism B** — permits present in **Bronze
that never reach Gold**: a Hudi `bulk_insert` called repeatedly under `mode('append')` in the
Gold-builder's per-partition loop silently sheds rows from the heaviest batches, dropping
**~398,500 Pinellas permits (~7.1%, 1-in-14)**; St-Petersburg alone loses **18.3%** (22,040 of
120,362; 99.7% proven present in Silver one step earlier). The full Bronze-vs-Gold anti-join
(every permit in Bronze not in Gold, all counties/types) is **not yet run; access is HOLD**. The
current agent map says IAM-blocked, while a 2026-06-09 audit verified Bronze S3 List+Get on sampled
prefixes. Reverify the exact all-county prefixes before calling it unblocked. **Santiago owns the ETL/DB.** A trustworthy
roofing retrain is gated on the fix tickets below; **do not retrain on current data.**

---

## Mechanism A — in-Gold mis-attribution

Permit *is* in Gold, but the wrong/empty property link, dup surrogate keys, or null/garbage dates
hide it from the recency filter and corrupt labels. Magnitudes are ground-truth (DuckDB-over-raw-parquet
== Athena Spark native Hudi reader, 0 diffs — finding 91 / [ENGINE_TRANSPARENCY.md](ENGINE_TRANSPARENCY.md)).

| Bug | Magnitude | Owner | Ticket | Finding / source |
|---|---|---|---|---|
| `FA_PROPERTYID` link keyed on corrupt `STREET4JOIN1` → points at wrong property when populated | Pinellas mislink **82.9%**; `STREET4JOIN1≠STREET` **78.4%** (3.92M rows); Smarty fix recovers ~4× (20%→83%) | gold_etl | **GOLD-17** | [finding 101](../findings/101_smarty_permit_fa_match_proof_2026-05-31.md), [finding 90](../findings/90_permit_join_fa_linkage_audit_2026-05-29.md), [montague walkthrough](fa_propertyid_montague_flujo_2026-05-29.md) |
| `FA_PROPERTYID` empty array `[]` → permit attaches to no property | **13–21%** (Pasco 21.2%, Hernando 22.0%, Pinellas 13.5%) | gold_etl | GOLD-19 | [dashboard C1](gold_qa_audit_dashboard_2026-05-29.md), [suite](qa_gold_audit_suite_2026-05-29.md) |
| Roof-REPLACEMENT positives attaching to **NO property** (net label loss) | **14.78%** pooled (335,739 / 2,272,119); Pinellas **35.3%** vs Orange 8.7% | gold_etl | **GOLD-18** | [finding 98](../findings/98_data_bug_catalog_2026-05-30.md) (E6) |
| Same physical permit duplicated under N `BUILDING_PERMIT_ID` (surrogate key is not a dedup) → double-counts positives | **1.1% (Duval) → 31.2% (Pasco)** of rows; ~166K dup positive rows; A2 dup-groups Pinellas 737,859 | gold_etl | **GOLD-01** | [finding 98](../findings/98_data_bug_catalog_2026-05-30.md) (A1), [results table A2](gold_qa_audit_results_table_2026-05-29.md) |
| `ISSUED_DATE` null on roof positives → recency unknowable; silent COALESCE substitutes wrong date | null **44–72%** (Pasco 66–72%, Pinellas 44.5–52%); Montague shipped on this | gold_etl | **GOLD-08** | [finding 90](../findings/90_permit_join_fa_linkage_audit_2026-05-29.md), [montague](fa_propertyid_montague_flujo_2026-05-29.md), [dashboard C2](gold_qa_audit_dashboard_2026-05-29.md) |
| Pinellas `COMPLETED_DATE` 1995–97 contamination → 22.3% ISSUED>COMPLETED, 31.3% APPLIED>ISSUED | Pinellas **380,369 rows (22.3%)**; 14,830 provably impossible; every other county ≤0.77% | gold_etl | **GOLD-14** | [finding 98](../findings/98_data_bug_catalog_2026-05-30.md) (C1) |
| Out-of-range date sentinels (1969-12-31 epoch, 1900/01, 2099/2100, AUX years 4270/9023) pass COALESCE | best-date <1990: Duval 267,870 / Hernando 59,066 / Pinellas 32,187; future Miami 147 | gold_etl | **GOLD-12** | [finding 98](../findings/98_data_bug_catalog_2026-05-30.md) (B7/C3/C4) |
| `OwnerOccupied` is a 100% deterministic INVERSE of `Absentee` (0 mismatch / 2.4M SFH) — zero independent occupancy signal for the OO-at-permit rule | all 7 counties; NULL-OO 14.6–28.0%; Company owners flagged OO='Y' | silver_etl | **SILVER-01** | [finding 98](../findings/98_data_bug_catalog_2026-05-30.md) (G1) |
| `silver_cohort` (coverage denominator) missing all 3 CallZeke delivery counties + 2 months staler than `silver_rem` | structural; cohort 2026-02 vs rem 2026-04; missing 12053/12101/12103 | silver_etl | **SILVER-02** | [finding 98](../findings/98_data_bug_catalog_2026-05-30.md) (G2) |
| `ROOFING:NA` (excluded from y=1) is LARGER than `ROOFING:REPLACEMENT` and hides reroofs; NA/REPL boundary non-deterministic for identical text | +2.1% positives recoverable (Hernando +12.2%, Pinellas +5.0%); 47,860 hidden | classifier | **CLF-01 / CLF-02** | [finding 98](../findings/98_data_bug_catalog_2026-05-30.md) (F1/F2) |

**Downstream blast radius of Mechanism A:**
- **518 of 15,000 shipped CallZeke v22 leads (3.45%)** mailed despite a confirmed recent reroof at
  their own address — the recency gate keyed on broken `FA_PROPERTYID` instead of address.
  [finding 92](../findings/92_callzeke_15k_blast_radius_2026-05-30.md).
- Old list **22.5% wrong (3,372)**, driven by **Pinellas 83.7% already-re-roofed**; honest
  addressable lift **5.5×, not 10.9×** (Simpson/base-rate, not a feature artifact).
  [finding 102](../findings/102_callzeke_list_audit_and_roof_confound_2026-05-31.md),
  [finding 103](../findings/103_roof_confound_decomposition/103_roof_confound_decomposition.md).
- **National, not FL-specific:** every headline Gold bug reproduces across 10 counties / 8 states
  (LA empty-FA 28.1%, Harris dup-PN 1.29M, etc.). [finding 99](../findings/99_gold_bugs_national_10county_2026-05-30.md).
- **The systemic fix already shipped:** `scripts/roofing/lib/data_gate.py` is a fail-CLOSED data
  contract — the pipeline now refuses to run on data that fails its audit.
  [finding 96](../findings/96_fail_closed_data_gate_2026-05-30.md).

**Architecture context (read before touching the ETL):**
- [buildzoom_etl_walkthrough_2026-05-19.md](buildzoom_etl_walkthrough_2026-05-19.md) — full
  Bronze→Silver→Gold architecture (4 Bronze tables, Hudi COW, the 3-condition address match, 13
  HIGH bugs). The Gold join is `Build_Zoom_Gold_ETL.ipynb` cell 11.
- [../Roofing/HOW_WE_MATCH_DATA.md](../Roofing/HOW_WE_MATCH_DATA.md) — how permits are matched to
  properties (the join we are auditing).

---

## Mechanism B — Bronze → Gold drop (permits that never reach Gold)

This is the user-reported "missing permits along the way" — and it **was quantified.** Permits
arrive in Bronze, survive Bronze→Silver (proven present in Silver), then **disappear at the
Silver→Gold write.**

**Quantified loss (Pinellas, 12103):**

| Metric | Value | Source |
|---|---|---|
| County-wide Gold loss (all permit types) | **~398,538 (~7.1–7.4%, 1-in-14)** | [HTML_2_etl_audit.md](../Roofing/false_negatives_pinellas/HTML_2_etl_audit.md), [HTML_2_exec_mckinsey.md](../Roofing/false_negatives_pinellas/HTML_2_exec_mckinsey.md) |
| St-Petersburg dropped (Bronze→Gold) | **22,040 of 120,362 = 18.3%** | HTML_2_etl_audit.md |
| …of those, confirmed present in Silver (lost at Gold write) | **21,968 = 99.7%** | HTML_2_etl_audit.md |
| St-Pete 2024 batch drop rate (heaviest year) | **28%** (vs 6.8% in 2025) | HTML_2_etl_audit.md |
| Pinellas Silver distinct permit-ids → Gold | 5,403,114 → 5,004,576 | HTML_2_etl_audit.md |
| St-Pete share of roofing misses | **~70%** | HTML_2_exec_mckinsey.md |
| Roof misses in the 519-property audit (visible tip) | 274 confirmed in Bronze, ~$2.76M | [ETL_LOSS_LIST.md](../Roofing/false_negatives_pinellas/ETL_LOSS_LIST.md) |
| Declared roof value behind bad leads (exec figure) | **$2.8M** (visible tip only) | HTML_2_exec_mckinsey.md |

**ROOT CAUSE — Hudi `bulk_insert` in an append loop.** The shared helper
`write_table_in_hudi_format` (`Gold-ETL/…py:149-174`) is called per-partition in a loop
(`Build_Zoom_Gold_ETL.ipynb:691-815`) with `operation='bulk_insert'`, `clean.automatic='false'`,
`.mode('append')`. `bulk_insert` does no record-index / upsert dedup and is designed as a
single one-shot load; cycling it under `append` over the heaviest file-group silently sheds rows —
no error, no warning. St-Pete is hardest-hit only because it is the largest/densest cluster
(120,362 permits, 65,590 in 2024, `PARCEL_NUMBER` 100% null), not because of any city-specific
rule. **Loss tracks batch size, not geography.** GOLD-14 (Pinellas date corruption) may compound
it in the heaviest 2024 batch.

**Ruled out** (HTML_2_etl_audit.md): join-miss on `STATE=split(PERMIT_JURISDICTION,'_')[0]`
(99.7% of dropped permits still resolve to FIPS 12103); Gold groupBy collapsing dups (Gold is
exactly 1 row per permit); FA/property left-joins (all `how='left'`, a miss empties FA but keeps
the row — that empty-FA path is Mechanism A / GOLD-18, a separate defect).

**A distinct sub-case — PROVIDER GAP (not ETL drop):** case `PG-2026-06-01` found a
2025 re-roof permit absent from Bronze entirely; BuildZoom never received it and the
newest Bronze permit for the audited parcel was from 2021. **Gold history for many shipped
parcels stops at 2021.** Also a parcel-key trap: Bronze `PARCEL_NUMBER` is a collision key
shared across neighboring houses — key on Bronze `property_id`, never `PARCEL_NUMBER`.
The record-level case evidence remains repository-only;
[false_positives_pinellas.md](../lessons/false_positives_pinellas.md),
[etl_data_loss.md](../lessons/etl_data_loss.md).

> **The two mechanisms are distinct.** A: permit IS in Bronze AND Gold, but mis-linked/dated.
> B-ETL-drop: permit IS in Bronze, NOT in Gold (our write loses it). B-provider-gap: permit is
> NOT in Bronze (vendor never delivered it). The 519 Pinellas false-positives split across all
> three — 274 (≥53%) proven Bronze-but-not-Gold (recoverable, our bug); the rest provider-gap or
> unverified.

### ⏸ HOLD next step — the full Bronze-vs-Gold anti-join

The definitive deliverable — **list every permit in Bronze that is absent from Gold, all counties,
all permit types** — is **NOT YET RUN.** Access evidence conflicts and must be reverified:

- Bronze (`8020rei-bronze-data-lake`) Glue + S3 returned **AccessDenied** for the automation user
  when this investigation began. (Note: HTML_2_etl_audit.md
  records a partial Bronze grant on 2026-06-01 and a Silver grant on 2026-06-02 used for the St-Pete
  membership proof.) **UPDATE 2026-06-09:** Bronze **S3** List+Get now verified working for
  `ignacio-claudecode` across arbitrary FIPS (MA/RI pulls in
  [usps_bounce_unit_audit_2026-06-09.md](usps_bounce_unit_audit_2026-06-09.md)) — the S3 half of
  the S3 half appeared available; **Glue catalog access remained untested**. The current agent map
  still marks the full anti-join IAM-blocked, so the sampled grant is not treated as current
  all-prefix authorization.
- **Owner:** **Santiago owns the ETL / DB.** Route the IAM request and the Gold-builder fix
  (single counted union write OR `upsert` with a proper recordkey, replacing the looped
  `bulk_insert`/`append`) through him.
- **After re-verifying access, run query A** in [HTML_2_etl_audit.md](../Roofing/false_negatives_pinellas/HTML_2_etl_audit.md)
  (per-jurisdiction Bronze→Gold survival) across all 7 counties + all permit types to get the
  national drop count. The 274 roof misses are a tiny visible slice.

---

## Files in this folder (`notes/etl_audit/`)

| File | What |
|---|---|
| **README.md** (this file) | Entry point — the whole "permits go missing" picture, both mechanisms, links + numbers |
| [GOLD_PERMITS_CONTRACT.md](../../../contracts/etl/GOLD_PERMITS_CONTRACT.md) | **Data contract v2.2.1 (ADOPTED 2026-06-10)** for the DE rebuild: scalar `fa_propertyid`, address standardization, classification, full-history backfill, in-row OO auditability, required columns and governed gates. The adopted gate baseline is v0.2.1; `gates.yaml` v0.2.2 is a later local amendment awaiting producer acknowledgement. Adoption does not prove that a conforming production snapshot has shipped. Published on Atlas → `/contracts`. |
| [ARS_CONTRACT.md](../../../contracts/etl/ARS_CONTRACT.md) | **Service data contract v1.0 (DRAFT)** for ARS (`ars-api-poc`) — the GOLD-D-1 standardizer: 5-field Smarty-equivalence promise (NM-agreed + unit-only doctrines), 91-case certification battery, probed error contract (5/5), SLA, CERT-01 fail-closed consumption gate (no green/current cert → no `addr_norm_version` pin). Executable half `contracts/etl/gates/ars_gates.yaml` (14 gates, 4 enforce / 3 shadow / 7 dry_run). Published on Atlas → `/contracts`. |
| [SILVER_REM_CONTRACT.md](../../../contracts/etl/SILVER_REM_CONTRACT.md) | **Data contract v1.0 (DRAFT)** for FA Silver `Real_Estate_Master_V1` — contracts the EXISTING table the models train on: 28-check battery (finding 104) promoted to gates with measured-baseline tolerances (kills the zero-tolerance/slider inversion), SILVER-01/02 declared, YearBuilt-immutable hard gate (G1), distress-encoding contract, cohort alignment, PII classification, waivers, deprecation. Executable half `contracts/etl/gates/silver_rem_gates.yaml` (37 gates, 12/21/4, every check verified to reproduce the 2026-04 baseline). Published on Atlas → `/contracts`. |
| [AUDIT_UPDATE_PLAN.md](AUDIT_UPDATE_PLAN.md) | Audit update plan, re-pegged to **contract v2.1** (2026-06-09): **§0 contract-conformance audit CC-1…CC-7 (runnable now — the audit now audits the contract itself)**, §2 shared-gates.yaml execution + 0-diff (replaces the v2.0 "mirror" shape), §2b v2.1 lifecycle checks (BUNDLE/MODE/WCH/QUA/PTB/XENG), schema gate, checks to retire, breaking-consumer sweep. §§1–3 still blocked on sign-off |
| [QA_STANDARD_GAP_ANALYSIS_2026-06-09.md](QA_STANDARD_GAP_ANALYSIS_2026-06-09.md) | **Gap analysis of the 3 audits (ARS / Gold_Permits / Silver AE) + contract v2.1 vs the DE Data Quality Audit Standard** — 4 finders + 4 adversarial verifiers; 3 structural roots (missing contracts, one-shot execution, no producer gate); P0: gates.yaml unauthored, no ARS/Silver-REM contracts; prioritized roadmap. No-Grafana decision recorded |
| [ENGINE_TRANSPARENCY.md](ENGINE_TRANSPARENCY.md) | Which engine produced the QA numbers (DuckDB = dashboard, == Athena Spark; Athena SQL blocked for automation user) + how to replicate |
| [data_bug_catalog_2026-05-30.json](data_bug_catalog_2026-05-30.json) | Machine-readable: 53 raw → 28 consolidated bugs, every prevalence + evidence query. Narrative = [finding 98](../findings/98_data_bug_catalog_2026-05-30.md) |
| [data_bug_tickets_2026-05-30.md](data_bug_tickets_2026-05-30.md) / `.csv` / `.json` | 52 fix tickets (8 BLOCK / 18 HIGH / 18 MED / 8 LOW), retrain-gate list, critical path, owner rollup. CSV = Jira/Linear import |
| [buildzoom_etl_walkthrough_2026-05-19.md](buildzoom_etl_walkthrough_2026-05-19.md) | Bronze→Silver→Gold architecture, Hudi layers, the 3-condition match, 13 HIGH bugs |
| [gold_qa_audit_dashboard_2026-05-29.md](gold_qa_audit_dashboard_2026-05-29.md) | 19-check / 56-pair QA dashboard (Pasco/Hernando/Pinellas) + full 54-column data dictionary |
| [qa_gold_audit_suite_2026-05-29.md](qa_gold_audit_suite_2026-05-29.md) | Cardinality matrix + STREET4JOIN1 root-cause writeup; key-ID verdict (use `PropertyID(gold)`, never `FA_PROPERTYID`/`APN`) |
| [gold_qa_audit_results_table_2026-05-29.md](gold_qa_audit_results_table_2026-05-29.md) | The 19-row QA results table (A1…F1) with per-county counts + thresholds |
| [fa_propertyid_montague_flujo_2026-05-29.md](fa_propertyid_montague_flujo_2026-05-29.md) | Repository-only worked trace of how one corrupt ZIP orphans a permit |
| [usps_bounce_unit_audit_2026-06-09.md](usps_bounce_unit_audit_2026-06-09.md) | **Direct-mail bounce audit (MA/RI 65-row sample): missing unit numbers are a FA vendor gap, not an ETL drop** — 0/30 bounced rows had a unit in Bronze at two vintages; Silver == Bronze; 4/30 recoverable from APN `U:NN`. Side discovery: Bronze S3 read now works for `ignacio-claudecode` |
| `usps_bounce_unit_audit_2026-06-09.py` / `…results_2026-06-09.csv` | The DuckDB-over-S3 audit script + per-row results (65 rows × Bronze 2026-04 / Bronze 2025-09 / Silver 2026-04 + verdicts) |
| `gold_qa_audit_dictionary_2026-05-29.xlsx` | Column dictionary (spreadsheet form) |

**Companion folders:**
- `notes/findings/` — findings 90, 91, 92, 96, 98, 99, 101, 102, 103 (the living research log; 98 supersedes 90/91/92's scattered defect counts).
- `notes/Roofing/false_negatives_pinellas/` — Mechanism B evidence + CSVs (`etl_dropped_bronze_not_gold_274.csv`, `pinellas_false_negatives_519.csv`).
- `notes/lessons/` — post-mortems (`etl_data_loss.md`, `false_positives_pinellas.md`, `bronze_parcel_key_trap_2113.md`) + `assets/`.

---

## How to run the QA battery

All read-only. DuckDB over raw S3 parquet produced the shipped dashboard numbers; Athena Spark is
the commit-pinned ground-truth cross-check (== DuckDB to the digit). Full provenance +
copy-paste in [ENGINE_TRANSPARENCY.md](ENGINE_TRANSPARENCY.md).

```bash
# 1) DuckDB — the engine that produced the dashboard numbers (live S3, no cache)
uv run python scripts/roofing/qa_gold_audit.py --live
#    -> data/sandbox/qa_gold_audit_results.json
uv run python scripts/roofing/qa_gold_examples.py --live   # worst-case example rows

# 2) Athena Spark — ground-truth cross-check, all FL-7, commit-pinned (~$0.11, auto-terminates)
#    REQUIRES Ignacio's OK first (billable AWS) — see CLAUDE.md REGLA DE ORO.
uv run python scripts/roofing/qa_gold_audit_athena.py
#    -> data/sandbox/qa_gold_audit_athena_fl7.json

# 3) Athena SQL — attempts the dashboard's literal queries; records the per-query block
#    (expect 0/19 for the automation user: results-bucket + Glue catalog denied)
uv run python scripts/roofing/qa_gold_audit_athena_sql.py

# 4) Build the HTML dashboard from the JSON results
uv run python scripts/roofing/build_gold_qa_dashboard.py
uv run python scripts/roofing/build_gold_qa_results_html.py

# 5) National 10-county battery (finding 99) — same checks, frozen per-county Gold commits
#    (see finding 99 for the runner)

# 6) Fail-closed data gate (finding 96) — the contract every stage now calls at entry
#    scripts/roofing/lib/data_gate.py  (raises GateFailure on contract violation)
```

Per-ticket `Detect:` SQL/Python one-liners (exact reproduction of each bug's magnitude) live in
[data_bug_tickets_2026-05-30.md](data_bug_tickets_2026-05-30.md) and the catalog JSON.

> **AWS cost discipline (CLAUDE.md REGLA DE ORO):** any billable AWS call (Athena Spark
> session/calc) needs an explicit one-line OK from Ignacio first; run locally (DuckDB) when the
> workload fits. Always `TerminateSession` and report DPU-ms cost at the end.

---

## Open / pending

| Item | Status | Owner / blocker |
|---|---|---|
| **Consumer-audit update to adopted contract v2.2.1 expectations** (new columns, scalar `fa_propertyid`, OO re-derivation check, coverage artifact, lifecycle checks) | **PREPARED; contract adoption closed 2026-06-10** — implementation checklist in [AUDIT_UPDATE_PLAN.md](AUDIT_UPDATE_PLAN.md); §0 contract-conformance checks and gate authoring are available now. Remaining work is execution evidence and the rebuild kickoff, not contract sign-off. | consumer (Ignacio); blocker: Santiago kickoff / first conforming snapshot |
| **`_CONTRACT/gates.yaml` authoring** (single machine-readable gate spec) + stable IDs for the gate families in the contract markdown | **DONE** — the v0.2.1 baseline was adopted with contract v2.2.1; current `gates.yaml` v0.2.2 is a local amendment (63 gates) awaiting producer acknowledgement. Silver REM (37) and ARS (14) gate files remain draft-contract companions. | consumer drafted; producer ack still required for the local amendment |
| **ARS contract** (was the last contract-less handoff; load-bearing via gold-permits D-1) | **DRAFT authored 2026-06-09** ([ARS_CONTRACT.md](../../../contracts/etl/ARS_CONTRACT.md)) — pending kickoff sign-off (§9.3; producer owner TBD, /health version field, battery assertions per open item 4) | Ignacio ↔ ARS owner (DE) |
| **Full Bronze-vs-Gold anti-join** (every Bronze permit absent from Gold, all counties + types) | **HOLD 2026-08-19, not yet run** — current agent map says IAM-blocked; a 2026-06-09 audit proved S3 List+Get on sampled prefixes, not current all-prefix access. | Reverify exact Bronze prefixes before the DuckDB anti-join. **Santiago owns ETL/DB** — fix still routes through him. |
| Fix the Gold-builder write (replace looped `bulk_insert`/`append` with one counted union write OR `upsert` w/ proper recordkey) | Diagnosed, **not implemented** | gold_etl / Santiago |
| Backfill + re-score after the Gold fix (rebuild Gold, re-score every shipped list) | Pending the fix | gold_etl |
| **Retrain gate** — ALL must pass before a trustworthy roofing retrain: `CLF-01 · CLF-02 · GOLD-01 · GOLD-08 · GOLD-12 · GOLD-14 · GOLD-17 · GOLD-18 · JOIN-02 · SILVER-01 · SILVER-02` | **Open** — do NOT retrain on current data | per ticket owners; see [data_bug_tickets_2026-05-30.md](data_bug_tickets_2026-05-30.md) |
| Provider-gap vs ETL-drop split for the full 519 Pinellas false-negatives | Partial (274/519 proven Bronze-but-not-Gold) | needs full Bronze read |
| Pasco + Hernando list-wide false-positive count | **Unmeasured** — neither county publishes a bulk permit file | external records request |
| `bulk_insert` causation — controlled rerun reproduction | Strongly evidenced, **not yet reproduced** in a controlled rerun | gold_etl / Santiago |
| Add a count-check gate (alert when Gold row count < upstream Silver count) | Recommended, not built | gold_etl |
| Smarty-standardize gold permit addresses then match to (already-standardized) FA — validated on Pinellas 50K | Validated locally, **full-county/national pending S3 gold (AWS)** | finding 101 |

**Hard rule (CLAUDE.md #8, PERMANENT):** the owner-occupied-at-permit filter
(`oo_at_permit`) stays ON for roofing labels/rows/CallZeke. Do NOT propose relaxing it.
Note SILVER-01: `OwnerOccupied` is degenerate (= inverse of `Absentee`), so the OR-clause needs
an independent occupancy source — that is a data fix, not a rule relaxation.
