# _CONTRACT/gates.yaml — GOLD_PERMITS single machine-readable gate spec
# Contract: 8020rei.gold.permits v2.2.1 (ADOPTED 2026-06-10) — see contracts/etl/GOLD_PERMITS_CONTRACT.md §7.
# This file is the SINGLE definition of every gate in contract §§2–6 [CON-02].
# Producer (DE/Santiago) and consumer (modeling/Ignacio) both execute THIS spec —
# producer inside the ETL pre-swap on Athena, consumer pre-train/list-build on DuckDB,
# then 0-diff every gate's measured value (contract §7.3). Changes to this file follow
# contract §9.2 sign-off and land in the what-changed block (§8.4).
#
# STATUS: v0.2.2 LOCAL AMENDMENT (2026-07-20; producer ack pending) — E4 v5.4
# package/golden-gate handoff, DET-03 four-field scoring API, and additive SUSPENDED status.
# ST-01, CLS-04, CLS-04a and DET-03 rule_version 1→2. Base v0.2.1 remains the
# bilaterally adopted contract; this amendment must be acknowledged before prod cutover.
# v0.2.1 ADOPTED (2026-06-10) — S1 re-spec to the already-signed contract §6 S1
# (D-8 + v2.2.1, locally hardened 2026-06-24 for read pruning): path =
# Permits_Master_Gold/<env>/<version>/FIPS=*/PERMIT_PERIOD=* replaces the dropped
# dated-path pattern _V<N>_<YYYYMMDD>; snapshot_date validated as constant-per-snapshot
# against execution metadata (no path date suffix to compare); promotion-by-gate audit
# explicit. S1 rule_version 1→2 [RUL-02]. Spec-lag correction, not a new promise — v0.2.0
# re-specced only CLS-07/L6 when D-8 landed and missed S1, leaving an ENFORCE gate that the
# rebuild's first snapshot would fail by construction (§9.3-2 unreachable). Sign-off
# carries from the v2.2.1 §9.3 bilateral signatures (the contract prose both parties signed
# is the normative text); lands in the first _AUDIT what-changed block (§8.4) for DE ack.
# v0.2.0 (2026-06-10): consumer-amended after the DE 12-point feedback round; adopted with
# contract v2.2.1 — both parties signed §9.3 — adoption criterion §9.3-2 input. Changes:
# CLS-07 added (scalar↔scope consistency, dry_run, contract §3.7 D-6) · L6 enum +=
# 'legacy_address' (transitional, contract §3.3; sunset = contract open item 8) ·
# contract_version → 2.2.1 (v2.2.1 = dataset-identity name correction, no gate changes).
# v0.1.0 (2026-06-09): initial 62-gate authoring.
# FAIL thresholds copied verbatim from contract v2.2.0 (numerically unchanged since v2.1.1); ALERT values the contract does not
# pin (and FRS-01's fail grace) are consumer-proposed and marked `threshold_status:
# provisional` — DE confirms or replaces at kickoff. Modes per §8.1 initial assignment.
# DE may amend at kickoff; every amendment = a row in the kickoff minutes.

meta:
  contract_id: 8020rei.gold.permits
  contract_version: "2.2.1"
  gates_yaml_version: "0.2.2"
  status: ADOPTED
  date: "2026-07-20"
  authors: [ignacio-consumer]
  signed_off: [ignacio-consumer, santiago-producer]  # base v0.2.1, 2026-06-10
  amendment_status: PENDING_PRODUCER_ACK
  amendment_signed_off: [ignacio-consumer]

  # Field semantics
  # id:           stable gate id; referenced by contract prose, report.json, quarantine dirs, alerts.
  # section:      contract clause implemented (two-way traceability, CC-2).
  # dimension:    primary quality dimension [DIM-01] ∈ {completeness, validity, consistency,
  #               referential_integrity, uniqueness, timeliness} (+ guard: volume|coverage|drift).
  # scope:        per_county | pooled | full_population | per_county_year | per_jurisdiction_year
  #               | snapshot | sample
  # mode:         dry_run | shadow | enforce  (contract §8.1; promotion = 2 consecutive
  #               passing snapshots; demotion = signed contract change, never silent).
  # owner:        producer | consumer | both — who answers when the gate is red.
  # rule_version: bumped on ANY logic change to the gate's check [RUL-02] — results are
  #               only comparable across runs at the same rule_version.
  # measured_as:  what the runner computes; compared against thresholds with direction ≤
  #               (value must be ≤ threshold) unless `direction: min` stated.
  # alert_threshold / fail_threshold: numeric promises from the contract — EXCEPT rows
  #               marked `threshold_status: provisional` (consumer-proposed, confirm at
  #               kickoff). fail only binds in enforce mode; alert binds in shadow+enforce.
  #               null = not defined yet.
  # today:        current measurement (contract "today" figures / finding-91 suite 2026-05-29/30)
  #               — the §8.1 calibration anchor. null = unmeasured (rebuild-only column).
  # implements:   executable ancestor — `suite:<id>` (finding-91 results-table check, A1…F1;
  #               prefixed to avoid collision with gate ids B1/B2) | <ticket-id> (GOLD-xx /
  #               SILVER-xx / CLF-xx) | finding-NN | to_build = no current implementation.
  # check:        canonical SQL (Athena/Trino dialect) over CONTRACT column names, or
  #               procedure: prose spec when not a single aggregate. Placeholders bound by
  #               the runner per meta.bindings below. NOTE: snapshot_date is an in-table
  #               COLUMN (§3.1), not a placeholder.

  bindings:
    # Every placeholder used anywhere in this file, with its binding source. Two runners
    # binding differently = the drift [CON-02] forbids; this block is normative.
    "{table}": "the candidate snapshot — producer: pre-swap Athena table; consumer: active snapshot S3 path (DuckDB)"
    "{fips}": "iterated over the snapshot's county set (5-digit zero-padded string)"
    "{coverage}": "the snapshot's _COVERAGE/coverage.parquet; columns (jurisdiction, year, bz_coverage_label, n_distinct_permit_uids, min_event_date, max_event_date) per contract §5"
    "{condo_allowlist}": "_CONTRACT/condo_allowlist.csv (header fa_propertyid), registered as a relation; NULL rows excluded by the check itself"
    "{zip_lo}/{zip_hi}": "per-county state USPS ZIP prefix range from _CONTRACT/state_zip_ranges.csv (to publish with this file; until then GEO-05 is unexecutable)"
    "{type_enum}/{action_enum}": "the 20-value type / 7-value action literal lists from classifier v5.4.0 (etl/permits/silver/classifier/taxonomy.json), pinned to classifier_version"
    "{fa_roof_cover_enum}": "locked FA ROOF_COVER value list — PENDING open item 4; CLS-06 unexecutable until bound"
    "{sentinel_denylist}": "sentinel street token list versioned with addr_norm_version (ADDR-03)"

  dialect_shims:
    # One semantic definition; runners apply ONLY these mechanical rewrites for DuckDB.
    # Anything beyond this list = a spec change, not a shim.
    - "regexp_like(x, p)  ->  regexp_matches(x, p)"
    - "CROSS JOIN UNNEST(arr) AS t(x)  ->  , UNNEST(arr) AS t(x)   (array of scalars)"
    - "CROSS JOIN UNNEST(arr_of_struct) AS t(f1, f2, ...)  ->  , UNNEST(arr_of_struct) AS u(e), then t.fN := u.e.fN   (Trino expands row fields to columns; DuckDB keeps the struct — runner maps field names mechanically)"
    - "date_add('day', n, d)  ->  d + INTERVAL (n) DAY"
    - "cardinality(arr)  ->  len(arr)"

  exemptions:
    condo_allowlist: "_CONTRACT/condo_allowlist.csv"   # L5 exemption set — seeded empty (header only) 2026-06-09; populate before first L5 run
    fa_roof_cover_vocabulary: "PENDING open item 4"     # CLS-06 crosswalk — modeling supplies

gates:

  # ────────────────────────────── SCHEMA (contract §3, all) ──────────────────────────────

  - id: SCH-01
    rule_version: 1
    title: contracted columns present with contracted types
    section: "§3.1–3.8"
    dimension: validity
    scope: snapshot
    mode: enforce
    owner: producer
    measured_as: count of missing/renamed/retyped columns vs contract §3 (incl. fips VARCHAR(5),
      zip_code VARCHAR(5), fa_propertyid scalar BIGINT — no array column ships, permit_scope
      list<struct<type,action,details struct<roof_material>>>)
    alert_threshold: 0
    fail_threshold: 0
    today: null   # rebuild-only; today's table is the 54-col v1 schema
    implements: to_build   # consumer half = data_gate.py schema gate (AUDIT_UPDATE_PLAN §4.5)
    procedure: information_schema/DESCRIBE diff against the contract §3 column manifest.

  - id: SCH-02
    rule_version: 1
    title: reproducibility pins NOT NULL
    section: "§3.1 / §6 S2"
    dimension: completeness
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows with any NULL pin
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}'
        AND (etl_version IS NULL OR classifier_version IS NULL
             OR addr_norm_version IS NULL OR fa_match_vintage IS NULL
             OR snapshot_date IS NULL)

  - id: SCH-03
    rule_version: 1
    title: fips zero-padded VARCHAR(5)
    section: "§3.2"
    dimension: validity
    scope: full_population
    mode: enforce
    owner: producer
    measured_as: rows where fips fails ^[0-9]{5}$
    alert_threshold: 0
    fail_threshold: 0
    today: "fails by construction today — fips is INTEGER (contract: 'fix at source')"
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips IS NULL OR NOT regexp_like(fips, '^[0-9]{5}$')

  # ────────────────────────────── GRAIN & IDENTITY (§2) ──────────────────────────────

  - id: G1
    rule_version: 2
    title: one row per safe permit document (guarded natural-key dedup)
    section: "§2 G1"
    dimension: uniqueness
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: unflagged natural-key groups (permit_jurisdiction, canonicalized permit_number)
      with >1 row, PLUS fallback-key dup groups for the permit_number-IS-NULL population
      (one BZ source id must not land in two Gold rows). Repeated natural keys are allowed
      only when every row is explicitly flagged permit_group_status='ambiguous_reused_number'
      and uses a finer/source-row permit_uid fallback. Canonicalization = upper(trim())
      over the G1a-sentinel-cleaned value; the full normalization rule rides in etl_version.
    alert_threshold: 0
    fail_threshold: 0
    today: "local 2026-06-10 BuildZoom slice: 123,346 same-property merge candidates; 550 probable multi-property documents; 159 ambiguous reused-number groups (worst NC_Brunswick|1000 = 194 rows / 160 properties)"
    implements: [suite:A2, GOLD-01]
    check: |
      SELECT
        (SELECT COUNT(*) FROM (
           SELECT permit_jurisdiction, upper(trim(permit_number)) AS pn
           FROM {table}
           WHERE fips = '{fips}' AND permit_number IS NOT NULL
             AND coalesce(permit_group_status, '') <> 'ambiguous_reused_number'
           GROUP BY 1, 2
           HAVING COUNT(*) > 1
         ) g)
        +
        (SELECT COUNT(*) FROM (
           SELECT t.src_id
           FROM {table} p
           CROSS JOIN UNNEST(p.source_permit_ids) AS t(src_id)
           WHERE p.fips = '{fips}' AND p.permit_number IS NULL
           GROUP BY 1
           HAVING COUNT(*) > 1
         ) f)

  - id: G1a
    rule_version: 1
    title: permit_number sentinels nulled; sentinels never in dedup
    section: "§2 G1a"
    dimension: validity
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows where permit_number is a surviving sentinel ('0', '', whitespace, 'NULL', 'NONE')
    alert_threshold: 0
    fail_threshold: 0
    today: "Hernando 9,479 rows with PN='0' (must NOT collapse into one permit)"
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}'
        AND trim(coalesce(permit_number, 'x')) IN ('0', '', 'NULL', 'NONE')

  - id: G2
    rule_version: 1
    title: permit_uid NOT NULL + UNIQUE (full table)
    section: "§2 G2 / §3.1"
    dimension: uniqueness
    scope: full_population   # full-table PK promise — per-batch/per-county uniqueness is the
                             # standard's named blind spot (§1.2); no fips filter on purpose
    mode: enforce
    owner: producer
    measured_as: sum of two counts (each must be 0; sum 0 ⇔ both 0) — NULL permit_uid rows +
      permit_uid values duplicated ANYWHERE in the table (cross-county dups included);
      report.json additionally carries the per-county breakdown of both halves
    alert_threshold: 0
    fail_threshold: 0
    today: "uid does not exist today; surrogate BUILDING_PERMIT_ID is 1:1 per row (suite:A1) but not a dedup"
    implements: suite:A1
    check: |
      SELECT (SELECT COUNT(*) FROM {table} WHERE permit_uid IS NULL)
           + (SELECT COUNT(*) FROM (
                SELECT permit_uid FROM {table}
                GROUP BY 1 HAVING COUNT(*) > 1) d)

  - id: G3
    rule_version: 2
    title: lineage kept (source ids, line count, group status, first/last_seen)
    section: "§2 G3"
    dimension: completeness
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows with NULL/empty source_permit_ids, missing source_line_count,
      missing permit_group_key/status, invalid permit_group_status, NULL first_seen_date/last_seen_date,
      OR last_seen_date < first_seen_date (ordering leg)
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}'
        AND (source_permit_ids IS NULL
             OR cardinality(source_permit_ids) < 1
             OR source_line_count IS NULL
             OR source_line_count < 1
             OR permit_group_key IS NULL
             OR permit_group_status NOT IN ('single_source',
                                            'safe_same_property_merge',
                                            'multi_property_document',
                                            'ambiguous_reused_number')
             OR first_seen_date IS NULL OR last_seen_date IS NULL
             OR last_seen_date < first_seen_date)

  - id: G4
    rule_version: 2
    title: deterministic attribute merge (sample re-derivation)
    section: "§2 G4"
    dimension: consistency
    scope: sample
    mode: shadow
    owner: producer
    measured_as: sampled collapsed rows (N≥200/county with cardinality(source_permit_ids)>1
      and permit_group_status <> 'ambiguous_reused_number') where re-derived merge
      (earliest non-null stage dates; latest non-null latest-status date; sorted distinct
      project_type union; per-source-row classifier union == permit_scope) differs from shipped values
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    procedure: re-run merge rule on the Bronze rows named by source_permit_ids; diff vs shipped row.
      Requires Bronze read (B3) + vendored classifier at pinned classifier_version.
    mode_note: deterministic in principle, but the check needs Bronze + classifier re-run
      infrastructure — promote to enforce after 2 green snapshots per §8.1.

  # ────────────────────────────── ADDRESS & GEO (§3.2) — IDs minted (CC-3) ──────────────────────────────

  - id: ADDR-01
    rule_version: 1
    title: standardized situs fields populated
    section: "§3.2 D-1"
    dimension: completeness
    scope: per_county
    mode: dry_run
    owner: producer
    measured_as: rate of rows with NULL situs_street_std among NON-catchall rows
      (catchall rows excluded from numerator AND denominator)
    alert_threshold: 0.02
    fail_threshold: 0.05
    threshold_status: provisional — D-1 mandates standardization but pins no rate; calibrate
      post-rebuild (open item 2 family), then promote.
    today: null
    implements: to_build
    check: |
      SELECT CAST(SUM(CASE WHEN situs_street_std IS NULL THEN 1 ELSE 0 END) AS DOUBLE)
             / COUNT(*)
      FROM {table}
      WHERE fips = '{fips}' AND NOT coalesce(is_catchall_address, FALSE)

  - id: ADDR-02
    rule_version: 1
    title: catchall rows ship unlinked
    section: "§3.2"
    dimension: consistency
    scope: full_population
    mode: enforce
    owner: producer
    measured_as: rows with is_catchall_address = TRUE AND fa_propertyid IS NOT NULL
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE is_catchall_address AND fa_propertyid IS NOT NULL

  - id: ADDR-03
    rule_version: 1
    title: sentinel street tokens nulled at ingest
    section: "§3.2"
    dimension: validity
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows where situs_street_std matches the sentinel denylist
      (full list versioned with addr_norm_version — bound via {sentinel_denylist},
      never hardcoded, so the canonical SQL can't go stale vs the versioned list)
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}'
        AND upper(situs_street_std) IN ({sentinel_denylist})

  - id: GEO-01
    rule_version: 1
    title: tract_fips prefix equals fips
    section: "§3.2"
    dimension: referential_integrity
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: share of non-null tract_fips whose first 5 chars ≠ fips
    alert_threshold: 0.005
    fail_threshold: 0.01      # contract: prefix = fips ≥99%
    threshold_status: provisional alert — contract pins only the fail value (≥99%); alert
      is a consumer-proposed early-warning level, confirm at kickoff.
    today: null
    implements: to_build
    check: |
      SELECT CAST(SUM(CASE WHEN substr(tract_fips, 1, 5) <> fips THEN 1 ELSE 0 END) AS DOUBLE)
             / NULLIF(COUNT(tract_fips), 0)
      FROM {table} WHERE fips = '{fips}' AND tract_fips IS NOT NULL

  - id: GEO-02
    rule_version: 1
    title: tract_fips null rate
    section: "§3.2"
    dimension: completeness
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: NULL tract_fips rate
    alert_threshold: 0.003
    fail_threshold: 0.005     # contract: null ≤0.5%/county
    threshold_status: provisional alert — contract pins only the fail value; confirm at kickoff.
    today: null
    implements: to_build
    check: |
      SELECT CAST(SUM(CASE WHEN tract_fips IS NULL THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
      FROM {table} WHERE fips = '{fips}'

  - id: GEO-03
    rule_version: 1
    title: latitude/longitude NOT NULL
    section: "§3.2"
    dimension: completeness
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: rows with NULL lat or lon
    alert_threshold: 0
    fail_threshold: 0
    mode_note: CONFLICT with §3.2 letter (lat/lon NOT NULL is unconditional) — the value is
      vendor-dependent, not producer-controlled in a rebuild, so shadow per §8.1 spirit.
      Raise at kickoff per §9.2 — either amend §3.2 to a thresholded null-rate gate
      (mirror GEO-02) or commit to enforce after baseline; record the resolution in the
      kickoff minutes + what-changed.
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}' AND (latitude IS NULL OR longitude IS NULL)

  - id: GEO-04
    rule_version: 1
    title: zip_code is 5-digit string, leading zeros preserved
    section: "§3.2"
    dimension: validity
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: non-null zip_code rows failing ^[0-9]{5}$
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}' AND zip_code IS NOT NULL
        AND NOT regexp_like(zip_code, '^[0-9]{5}$')

  - id: GEO-05
    rule_version: 1
    title: zip belongs to the county's state range
    section: "§3.2 (extension — pending contract amendment; confirm at kickoff)"
    dimension: referential_integrity
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: share of non-null zips outside the state's USPS prefix range
    alert_threshold: 0
    fail_threshold: 0.001
    threshold_status: provisional — requirement AND thresholds consumer-proposed from suite
      check E3 (the standard's A-Virginia-ZIP-in-Arizona class); not yet a contract clause.
    today: "FL counties 0.00–0.02% (suite:E3 2026-05-29; national run 41–1,415 rows/county)"
    implements: suite:E3
    check: |
      -- {zip_lo}/{zip_hi} bound from the state-range table per county
      SELECT CAST(SUM(CASE WHEN zip_code NOT BETWEEN '{zip_lo}' AND '{zip_hi}'
                           THEN 1 ELSE 0 END) AS DOUBLE) / NULLIF(COUNT(zip_code), 0)
      FROM {table} WHERE fips = '{fips}' AND zip_code IS NOT NULL

  - id: GEO-06
    rule_version: 1
    title: row fips == county_fips column
    section: "§3.2"
    dimension: consistency
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows where fips <> county_fips
    alert_threshold: 0
    fail_threshold: 0
    today: "0 across all counties (suite:E1 2026-05-29)"
    implements: suite:E1
    check: |
      SELECT COUNT(*) FROM {table} WHERE fips = '{fips}' AND fips <> county_fips

  - id: GEO-07
    rule_version: 1
    title: physical partition keys match row fips and permit_period
    section: "§3.2 / §6 S1"
    dimension: consistency
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows whose partition path FIPS ≠ row fips or PERMIT_PERIOD ≠ row permit_period
    alert_threshold: 0
    fail_threshold: 0
    today: "0 across all counties (suite:E2 2026-05-29)"
    implements: suite:E2
    procedure: compare partition directory keys vs row fips and row permit_period (engine-specific path read).

  # ────────────────────────────── PROPERTY LINK (§3.3) ──────────────────────────────

  - id: L1
    rule_version: 1
    title: NULL fa_propertyid rate per county
    section: "§3.3 L1"
    dimension: completeness
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: NULL fa_propertyid share, ALL rows (catchall included — consistent with the
      today-baseline basis; the 0.05/0.08 thresholds bind to THIS value). The companion
      without-catchall rate is reported alongside for calibration only.
    alert_threshold: 0.05
    fail_threshold: 0.08
    today: "13–23% (suite:C1 2026-05-30: Pasco 21.2%, Hernando 22.0%, Pinellas 13.5%)"
    implements: suite:C1
    check: |
      SELECT CAST(SUM(CASE WHEN fa_propertyid IS NULL THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
      FROM {table} WHERE fips = '{fips}'
    check_companion: |
      -- calibration-only companion (NOT threshold-bound): same rate excluding catchall rows
      SELECT CAST(SUM(CASE WHEN fa_propertyid IS NULL THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
      FROM {table} WHERE fips = '{fips}' AND NOT coalesce(is_catchall_address, FALSE)

  - id: L2
    rule_version: 1
    title: mislink rate on stratified sample
    section: "§3.3 L2"
    dimension: referential_integrity
    scope: sample
    mode: shadow
    owner: producer
    measured_as: share of a 200-row/county stratified sample where normalized situs_street_std
      ≠ normalized fa_situs_address_matched
    alert_threshold: 0.01
    fail_threshold: 0.02
    threshold_status: provisional alert — contract pins only ≤2% fail; confirm at kickoff.
    today: "Pinellas 82.9% mislink (finding 101); STREET4JOIN1≠STREET 78.4% (suite:F1)"
    implements: [suite:F1, finding-101]
    procedure: stratify by fa_match_method × permit decade; normalize both sides with the
      pinned addr_norm_version normalizer; report per-stratum + pooled. Sample seed pinned
      in report.json for reproducibility.

  - id: L3
    rule_version: 1
    title: roof-REPLACEMENT rows with NULL FA (label loss)
    section: "§3.3 L3"
    dimension: completeness
    scope: pooled
    mode: shadow
    owner: producer
    measured_as: NULL fa_propertyid share among ROWS (row grain — deduped, a row with two
      matching scope entries counts once) whose permit_scope contains (ROOFING, REPLACEMENT)
    alert_threshold: 0.03
    fail_threshold: 0.05
    threshold_status: provisional alert — contract pins only ≤5% pooled fail; confirm at kickoff.
    today: "14.78% pooled (335,739 / 2,272,119; Pinellas 35.3%) — finding 98 (check E6) / GOLD-18"
    implements: [GOLD-18, finding-98]   # E6 in the finding-98 catalog
    check: |
      SELECT CAST(SUM(CASE WHEN fa_propertyid IS NULL THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
      FROM (
        SELECT DISTINCT p.permit_uid, p.fa_propertyid
        FROM {table} p
        CROSS JOIN UNNEST(p.permit_scope) AS s(type, action, details)
        WHERE s.type = 'ROOFING' AND s.action = 'REPLACEMENT'
      ) roof_repl

  - id: L4
    rule_version: 1
    title: cross-county link = 0
    section: "§3.3 L4"
    dimension: referential_integrity
    scope: full_population
    mode: enforce
    owner: producer
    measured_as: matched rows whose FA property's county ∉ the jurisdiction's allowed county set
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    procedure: join fa_propertyid → FA Real_Estate_Master (at fa_match_vintage) → county;
      compare vs jurisdiction registry allowed set. Every snapshot, full population.

  - id: L5
    rule_version: 1
    title: fanout caps
    section: "§3.3 L5"
    dimension: uniqueness
    scope: full_population
    mode: enforce
    owner: producer
    measured_as: sum of the two contracted caps — (a) fa_propertyid values with >50 distinct
      addresses (check below) + (b) BZ property ids mapping to >25 parcels (procedure —
      BZ property id is not a Gold column). Condo allowlist exempt on (a).
    alert_threshold: 0
    fail_threshold: 0
    today: "suite:B1 worst FA id → 3,998 addresses (Pinellas); suite:B4 16.91% Pinellas (2026-05-29)"
    implements: [suite:B1, suite:B2, suite:B4]
    check: |
      SELECT COUNT(*) FROM (
        SELECT fa_propertyid
        FROM {table}
        WHERE fa_propertyid IS NOT NULL
          AND fa_propertyid NOT IN (SELECT fa_propertyid FROM {condo_allowlist}
                                    WHERE fa_propertyid IS NOT NULL)
        GROUP BY 1
        HAVING COUNT(DISTINCT situs_street_std) > 50
      ) fanout
    procedure: parcel-cap half (≤25 parcels per BZ property id) — computed from the matcher
      artifact / Bronze link table at fa_match_vintage (BZ property id is not a Gold
      column); implements to_build, requires Bronze read or producer manifest (B3 means).
      Measured value = address-fanout count + parcel-fanout count.

  - id: L6
    rule_version: 1
    title: match metadata valid
    section: "§3.3"
    dimension: validity
    scope: full_population
    mode: enforce
    owner: producer
    measured_as: rows violating — fa_match_method ∉ {smarty_address, ars_address, apn,
      legacy_address, none} (legacy_address = transitional per contract §3.3, sunset open
      item 8); matched row with NULL/out-of-range fa_match_confidence; method='none' with
      non-null fa_propertyid
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fa_match_method IS NULL
         OR fa_match_method NOT IN ('smarty_address', 'ars_address', 'apn', 'legacy_address', 'none')
         OR (fa_propertyid IS NOT NULL
             AND (fa_match_confidence IS NULL
                  OR fa_match_confidence < 0 OR fa_match_confidence > 1))
         OR (fa_match_method = 'none' AND fa_propertyid IS NOT NULL)

  - id: PTB-01
    rule_version: 1
    title: tie-break determinism (P-TB)
    section: "§3.3 P-TB / §6 S2"
    dimension: consistency
    scope: sample
    mode: shadow
    owner: producer
    measured_as: sampled rows with fa_alternates_count > 0 where the shipped fa_propertyid
      violates the P-TB order (confidence desc → pinned-engine method > apn → lowest id)
      against the candidate set in _AUDIT/quarantine/
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build   # consumer half = AUDIT_UPDATE_PLAN §2b PTB
    procedure: read candidate sets from _AUDIT/quarantine/ (§8.3, capped 10/county);
      verify shipped winner = P-TB-ordered head. Adoption blocker is the RULE being
      documented (§9.3-3); this gate verifies the rule is APPLIED.
    mode_note: deterministic + producer-controlled once the P-TB rule is signed at kickoff
      (DE may replace the default order) AND candidate-set publication in _AUDIT/quarantine
      is proven — shadow until both land, then promote to enforce per §8.1.

  # ────────────────────────────── DATES & PERIODS (§3.4) — IDs minted (CC-3) ──────────────────────────────

  - id: DATE-01
    rule_version: 1
    title: date sentinel survivors = 0
    section: "§3.4"
    dimension: validity
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: ROWS (distinct permit_uid — a row with two sentinel dates counts once)
      where any of the six stage dates is a surviving sentinel
      (1969-12-31, ≤1900-01-01, ≥2090-01-01, AUX years 4270/9023, > snapshot_date+90d)
    alert_threshold: 0
    fail_threshold: 0
    today: "best-date <1990 — Duval 267,870 / Hernando 59,066 / Pinellas 32,187; future Miami 147 (GOLD-12, suite:D3); observed 2066-08-23 passes naive ≥2090"
    implements: [suite:D3, GOLD-12]
    check: |
      SELECT COUNT(DISTINCT t.permit_uid) FROM {table} t
      CROSS JOIN UNNEST(ARRAY[t.applied_date, t.issued_date, t.completed_date,
                              t.cancelled_date, t.initial_status_date, t.latest_status_date])
                 AS d(stage_date)
      WHERE t.fips = '{fips}' AND d.stage_date IS NOT NULL
        AND (d.stage_date = DATE '1969-12-31'
             OR d.stage_date <= DATE '1900-01-01'
             OR d.stage_date >= DATE '2090-01-01'
             OR d.stage_date > date_add('day', 90, t.snapshot_date))

  - id: DATE-02
    rule_version: 1
    title: event_date null rate
    section: "§3.4"
    dimension: completeness
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: NULL event_date share
    alert_threshold: 0.01
    fail_threshold: 0.02
    threshold_status: provisional alert — contract pins only ≤2% fail; confirm at kickoff.
    today: "ISSUED_DATE null 44–72% today (suite:C2: Pasco 72.1%, Pinellas 52.5%) — event_date
      min-of-six makes most recoverable; post-rebuild baseline unmeasured"
    implements: suite:C2
    check: |
      SELECT CAST(SUM(CASE WHEN event_date IS NULL THEN 1 ELSE 0 END) AS DOUBLE) / COUNT(*)
      FROM {table} WHERE fips = '{fips}'

  - id: DATE-03
    rule_version: 1
    title: stage-date ordering violations
    section: "§3.4"
    dimension: consistency
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: rows with applied_date > issued_date OR issued_date > completed_date.
      Denominator = ALL rows in the county (rows with NULL pairs contribute 0 to the
      numerator) — the basis on which the Pinellas 22.3% calibration figure was computed;
      producer and consumer MUST use this same denominator (0-diff).
    alert_threshold: 0.005
    fail_threshold: 0.01
    threshold_status: provisional alert — contract pins only ≤1% fail; confirm at kickoff.
    today: "Pinellas 22.3% (380,369 rows; 14,830 provably impossible — GOLD-14, suite:D1/D2); every other county ≤0.77%"
    implements: [suite:D1, suite:D2, GOLD-14]
    check: |
      SELECT CAST(SUM(CASE WHEN (applied_date > issued_date)
                             OR (issued_date > completed_date) THEN 1 ELSE 0 END) AS DOUBLE)
             / COUNT(*)
      FROM {table} WHERE fips = '{fips}'

  - id: DATE-04
    rule_version: 1
    title: event_date == MIN of six stage dates (frozen rule re-derivation)
    section: "§3.4"
    dimension: consistency
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows where event_date ≠ least eligible stage date (eligible = non-null AND
      ≤ snapshot_date+90d, the frozen rule's cap) OR event_date_source names a stage whose
      value ≠ event_date
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      -- eligible(x) := CASE WHEN x IS NULL OR x > date_add('day', 90, snapshot_date)
      --                THEN DATE '9999-12-31' ELSE x END  (cap applied, matching §3.4)
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}' AND event_date IS NOT NULL
        AND (event_date <> least(
               CASE WHEN applied_date        IS NULL OR applied_date        > date_add('day', 90, snapshot_date) THEN DATE '9999-12-31' ELSE applied_date        END,
               CASE WHEN issued_date         IS NULL OR issued_date         > date_add('day', 90, snapshot_date) THEN DATE '9999-12-31' ELSE issued_date         END,
               CASE WHEN completed_date      IS NULL OR completed_date      > date_add('day', 90, snapshot_date) THEN DATE '9999-12-31' ELSE completed_date      END,
               CASE WHEN cancelled_date      IS NULL OR cancelled_date      > date_add('day', 90, snapshot_date) THEN DATE '9999-12-31' ELSE cancelled_date      END,
               CASE WHEN initial_status_date IS NULL OR initial_status_date > date_add('day', 90, snapshot_date) THEN DATE '9999-12-31' ELSE initial_status_date END,
               CASE WHEN latest_status_date  IS NULL OR latest_status_date  > date_add('day', 90, snapshot_date) THEN DATE '9999-12-31' ELSE latest_status_date  END)
          OR (CASE event_date_source
                WHEN 'applied_date'        THEN applied_date
                WHEN 'issued_date'         THEN issued_date
                WHEN 'completed_date'      THEN completed_date
                WHEN 'cancelled_date'      THEN cancelled_date
                WHEN 'initial_status_date' THEN initial_status_date
                WHEN 'latest_status_date'  THEN latest_status_date
              END) <> event_date)

  - id: DATE-05
    rule_version: 1
    title: event_date within [1990-01-01, snapshot+90d]
    section: "§3.4"
    dimension: validity
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: non-null event_date rows outside the contracted range
    alert_threshold: 0
    fail_threshold: 0
    mode_note: lower bound is NOT producer-deterministic — the §3.4 sentinel floor nulls
      only ≤1900-01-01, so 1901–1989 placeholder/genuine dates survive (GOLD-12 names
      1900/1901/1957 placeholders; best-date <1990 today — Duval 267,870 / Hernando
      59,066 / Pinellas 32,187). Enforce-from-day-one would block every ship. Shadow
      until the <1990 handling rule is pinned at kickoff, then promote per §8.1.
    today: "best-date <1990: Duval 267,870 / Hernando 59,066 / Pinellas 32,187 (GOLD-12)"
    implements: GOLD-12
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}' AND event_date IS NOT NULL
        AND (event_date < DATE '1990-01-01'
             OR event_date > date_add('day', 90, snapshot_date))

  - id: DATE-06
    rule_version: 1
    title: period arithmetic property check
    section: "§3.4"
    dimension: consistency
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows violating — permit_period ≠ YYYY-MM(event_date); t0_period ≠
      permit_period − 1 month; oo_anchor_period ≠ the LATEST month whose month-end ≤
      event_date − 30d (full equality re-derivation — both too-late AND too-early anchors
      are violations)
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    procedure: three equality re-derivations over (event_date, permit_period, t0_period,
      oo_anchor_period); the anchor predicate is month_end(oo_anchor_period) ≤
      event_date−30d AND month_end(oo_anchor_period + 1 month) > event_date−30d.
      Runner renders per-dialect date arithmetic from this single spec. 100% of rows
      with non-null event_date.

  - id: DATE-07
    rule_version: 1
    title: partition_year == year(event_date)
    section: "§3.4"
    dimension: consistency
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: mismatch share
    alert_threshold: 0.005
    fail_threshold: 0.01
    threshold_status: provisional alert — contract pins only <1% fail; confirm at kickoff.
    today: null
    implements: to_build
    mode_note: contract allows the alternative — declared physical-layout artifact banned as
      temporal key (§3.4); if DE declares that, this gate retires to documentation.
    check: |
      SELECT CAST(SUM(CASE WHEN partition_year <> year(event_date) THEN 1 ELSE 0 END) AS DOUBLE)
             / NULLIF(COUNT(event_date), 0)
      FROM {table} WHERE fips = '{fips}' AND event_date IS NOT NULL

  # ────────────────────────────── OWNER-OCCUPANCY (§3.5) — IDs minted (CC-3) ──────────────────────────────

  - id: OO-01
    rule_version: 1
    title: oo_at_permit re-derivable from in-row inputs
    section: "§3.5 D-4"
    dimension: consistency
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows where (oo_flag_at_anchor = 'Y' OR canonicalized mailing == canonicalized
      situs) ≠ shipped oo_at_permit. The stored anchor addresses are ALREADY the
      standardized forms the rule compared (§3.5) — no external normalizer re-run; the
      re-derivation uses only cheap canonicalization upper(trim()), NULL-safe.
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build   # consumer half = AUDIT_UPDATE_PLAN §2 OO row; rule = CLAUDE.md #8
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}' AND oo_at_permit IS NOT NULL
        AND oo_at_permit <> (coalesce(oo_flag_at_anchor, '') = 'Y'
                             OR (oo_mailing_addr_at_anchor IS NOT NULL
                                 AND oo_situs_addr_at_anchor IS NOT NULL
                                 AND upper(trim(oo_mailing_addr_at_anchor))
                                     = upper(trim(oo_situs_addr_at_anchor))))

  - id: OO-02
    rule_version: 1
    title: oo NULL semantics (NULL ⇔ no silver coverage at anchor)
    section: "§3.5"
    dimension: completeness
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: rows where oo_at_permit IS NULL XOR oo_source_vintage IS NULL
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}'
        AND ((oo_at_permit IS NULL AND oo_source_vintage IS NOT NULL)
          OR (oo_at_permit IS NOT NULL AND oo_source_vintage IS NULL))

  # ────────────────────────────── STATUS (§3.6) — IDs minted (CC-3) ──────────────────────────────

  - id: ST-01
    rule_version: 2
    title: status_class enum violations = 0
    section: "§3.6"
    dimension: validity
    scope: full_population
    mode: enforce
    owner: producer
    measured_as: rows with status_class ∉ {ACTIVE, COMPLETE, CANCELLED, EXPIRED, PENDING, SUSPENDED, UNKNOWN}
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE status_class NOT IN ('ACTIVE','COMPLETE','CANCELLED','EXPIRED','PENDING','SUSPENDED','UNKNOWN')

  - id: ST-02
    rule_version: 1
    title: cancellation consistency
    section: "§3.6"
    dimension: consistency
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: rows with latest_status ∈ {complete, final} AND cancelled_date NOT NULL
    alert_threshold: 0
    fail_threshold: 0
    today: "Pinellas 335,739 (GOLD-16); suite:D4 final-without-issue Pasco 59.8% / Pinellas 40.1%"
    implements: [GOLD-16, suite:D4]
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE fips = '{fips}'
        AND lower(latest_status) IN ('complete', 'final')
        AND cancelled_date IS NOT NULL

  - id: ST-03
    rule_version: 1
    title: status_events loss vs Bronze
    section: "§3.6"
    dimension: completeness
    scope: per_county_year
    mode: shadow
    owner: producer
    measured_as: per (county, year) — 1 − (status events in Gold status_events /
      Bronze Permit_Statuses_V1 events for the same permits)
    alert_threshold: 0.003
    fail_threshold: 0.005
    threshold_status: provisional alert — contract pins only ≤0.5% fail; confirm at kickoff.
    today: null
    implements: to_build   # needs Bronze read (B3) — verified for consumer 2026-06-09
    procedure: anti-join Bronze Permit_Statuses_V1 events against unioned Gold status_events
      per collapsed permit_uid; deterministic order also asserted (G4 family).

  # ────────────────────────────── CLASSIFICATION (§3.7) — IDs minted (CC-3) ──────────────────────────────

  - id: CLS-01
    rule_version: 1
    title: permit_scope enum violations = 0
    section: "§3.7 D-3"
    dimension: validity
    scope: full_population
    mode: enforce
    owner: both     # classifier is consumer-owned code vendored into the ETL
    measured_as: scope entries with NULL type/action, type ∉ 20-value enum, OR action ∉
      7-value enum (neither axis is nullable per §3.7)
    alert_threshold: 0
    fail_threshold: 0
    today: "v5.4.0 taxonomy packaged at etl/permits/silver/classifier/taxonomy.json"
    implements: to_build
    check: |
      -- Trino row-expansion UNNEST (array(row) → one alias per field); DuckDB via shim
      SELECT COUNT(*) FROM {table} p
      CROSS JOIN UNNEST(p.permit_scope) AS s(type, action, details)
      WHERE s.type IS NULL OR s.action IS NULL
         OR s.type NOT IN ({type_enum}) OR s.action NOT IN ({action_enum})

  - id: CLS-02
    rule_version: 1
    title: empty scope = 0
    section: "§3.7"
    dimension: completeness
    scope: full_population
    mode: enforce
    owner: both
    measured_as: rows with NULL or empty permit_scope (no-signal must be (UNKNOWN, NA),
      never NULL, never empty)
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table}
      WHERE permit_scope IS NULL OR cardinality(permit_scope) = 0

  - id: CLS-03
    rule_version: 1
    title: UNKNOWN share
    section: "§3.7"
    dimension: validity
    scope: per_county
    mode: shadow
    owner: both
    measured_as: share of rows whose ONLY scope entry is (UNKNOWN, NA) — both axes checked
    alert_threshold: 0.15
    fail_threshold: null    # alert-only by contract ("alert >15%")
    today: null
    implements: to_build
    check: |
      -- element_at + struct field access: identical 1-based semantics in Trino and DuckDB
      SELECT CAST(SUM(CASE WHEN cardinality(permit_scope) = 1
                            AND permit_scope[1].type = 'UNKNOWN'
                            AND permit_scope[1].action = 'NA' THEN 1 ELSE 0 END) AS DOUBLE)
             / COUNT(*)
      FROM {table} WHERE fips = '{fips}'

  - id: CLS-04
    rule_version: 2
    title: golden-set regression (365-row, exact match per pinned classifier_version)
    section: "§3.7"
    dimension: consistency
    scope: sample
    mode: shadow
    owner: consumer
    measured_as: golden-set rows where classifying the v5.4 Gold-carried inputs
      (description, type, subtype, project_name — §3.8) ≠ the independently audited
      permit_scope label; project_type and business_name are not read by v5.4
    alert_threshold: 0
    fail_threshold: 0
    today: "v5.4 runner committed; 365-row independently reviewed type/action artifact pending"
    implements: etl/permits/silver/classifier/golden.py
    procedure: run python -m classifier.golden against the artifact pinned in the v5.4
      manifest; exact full-scope match, fail closed on missing/unreviewed labels or hash drift.

  - id: CLS-04a
    rule_version: 2
    title: golden-set per-cell coverage ≥25 per (type, action)
    section: "§3.7"
    dimension: completeness
    scope: snapshot
    mode: dry_run    # §8.1 explicit dry_run list
    owner: consumer
    measured_as: (type, action) cells with <25 golden rows
    alert_threshold: 0
    fail_threshold: null
    today: "not yet extended — contract states fidelity certified for ROOFING only until then"
    implements: etl/permits/silver/classifier/golden.py
    procedure: count independently reviewed expected scope entries per non-empty cell;
      report and fail the release on every cell below 25.

  - id: CLS-05
    rule_version: 1
    title: input-hygiene canary (opposite-trade tokens)
    section: "§3.7"
    dimension: validity
    scope: per_county
    mode: shadow
    owner: producer
    measured_as: rows where project_type says Roofing while description matches
      opposite-trade tokens, as share of the GOLD-25 baseline
    alert_threshold: 0.05
    fail_threshold: 0.10    # contract: ≤10% of GOLD-25 baseline
    threshold_status: provisional alert — contract pins only the ≤10% fail; confirm at kickoff.
    today: "GOLD-25 baseline in data_bug_catalog_2026-05-30.json"
    implements: GOLD-25
    procedure: token list versioned with classifier_version; measured value = current
      count / GOLD-25 baseline count per county.

  - id: CLS-06
    rule_version: 1
    title: details.roof_material ∈ FA ROOF_COVER vocabulary
    section: "§3.7 D-3"
    dimension: validity
    scope: full_population
    mode: dry_run   # crosswalk = open item 4; promote to SHADOW when the locked FA
                    # ROOF_COVER enum lands, then to enforce after 2 green snapshots (§8.1
                    # promotion path — no direct dry_run → enforce)
    owner: consumer
    measured_as: non-null details.roof_material values outside the locked FA ROOF_COVER enum
    alert_threshold: 0
    fail_threshold: 0
    today: "slot ships now even if 100% NULL (§3.7); vocabulary mapping pending (open item 4)"
    implements: to_build
    check: |
      -- Trino row-expansion UNNEST; DuckDB via shim
      SELECT COUNT(*) FROM {table} p
      CROSS JOIN UNNEST(p.permit_scope) AS s(type, action, details)
      WHERE s.details.roof_material IS NOT NULL
        AND s.details.roof_material NOT IN ({fa_roof_cover_enum})

  - id: CLS-07
    rule_version: 1
    title: scalar permit_type/permit_action == re-derivation from permit_scope
    section: "§3.7 D-6"
    dimension: consistency
    scope: full_population
    mode: dry_run   # new gate enters in dry_run per §9.1; promote per §8.1 once the
                    # scalar_derivation version is pinned and the rebuild ships the columns
    owner: both     # dominance orders signed by modeling 2026-06-10; derivation
                    # implemented by DE as a pure versioned function (scalar_derivation
                    # inside etl_version)
    measured_as: rows where the shipped (permit_type, permit_action) pair differs from
      re-derivation from permit_scope under the pinned scalar_derivation version —
      type = highest-dominance type present (SOLAR > POOL > ROOFING > FIRE > GENERATOR >
      HVAC > ELECTRICAL > PLUMBING > WINDOWS > DOORS > GARAGE > SHED > DECK > FENCE >
      SIGN > FOUNDATION > SITEWORK > BUILDING > OTHER > UNKNOWN); action =
      highest-dominance action among the scope items of that type (DEMOLITION > NEW >
      REPLACEMENT > REPAIR > ADDITION > ALTERATION > NA); type-first per §3.7 D-6
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      procedure: per row, re-derive (type, action) from permit_scope — type = the
      minimum-rank type present under the type dominance table; action = the
      minimum-rank action among scope items whose type equals the dominant type, under
      the action dominance table; count rows where the shipped scalar pair differs.
      Rank tables ship as _CONTRACT/scalar_dominance.csv (columns: axis, value, rank),
      pinned to the scalar_derivation version inside etl_version.

  # ────────────────────────────── PERMIT DETAILS (§3.8) — IDs minted (CC-3) ──────────────────────────────

  - id: DET-01
    rule_version: 1
    title: negative job_value/fees shipped = 0
    section: "§3.8"
    dimension: validity
    scope: full_population
    mode: enforce
    owner: producer
    measured_as: rows with job_value < 0 OR fees < 0 (must be nulled at ingest, cells not rows)
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table} WHERE job_value < 0 OR fees < 0

  - id: DET-02
    rule_version: 1
    title: job_value absurdity cap applied
    section: "§3.8"
    dimension: validity
    scope: full_population
    mode: dry_run    # cap uncalibrated — §8.1 explicit dry_run list (open item 2)
    owner: producer
    measured_as: rows with job_value > cap (default 100000000)
    alert_threshold: 0
    fail_threshold: null
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {table} WHERE job_value > 100000000

  - id: DET-03
    rule_version: 2
    title: classifier input columns present
    section: "§3.8"
    dimension: completeness
    scope: snapshot
    mode: enforce
    owner: producer
    measured_as: missing columns among {description, type, subtype, project_name}
      for the v5.4 golden-set rerun; project_type and business_name are deliberately absent
      from the scoring API (CLS-04 depends on Gold-only inputs)
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    procedure: column-presence subset of SCH-01, kept separate for CLS-04 traceability.

  # ────────────────────────────── COMPLETENESS Bronze→Gold (§4) ──────────────────────────────

  - id: B1
    rule_version: 1
    title: Bronze→Gold no-loss reconciliation
    section: "§4 B1"
    dimension: completeness   # guard: volume
    scope: per_county_year
    mode: shadow
    owner: producer
    measured_as: per (county, year(event_date)) — 1 − (distinct physical permits in Gold /
      distinct physical permits in Bronze); EVERY cell in report.json
    alert_threshold: 0.003
    fail_threshold: 0.005
    threshold_status: provisional alert — contract pins only ≤0.5% fail; confirm at kickoff.
    today: "Pinellas −398.5K ≈ 7.1% county-wide; St-Pete 18.3% (99.7% provably in Silver);
      root cause Hudi bulk_insert under append (README Mechanism B)"
    implements: to_build   # query A in HTML_2_etl_audit.md; consumer runnable DuckDB-over-S3
    procedure: distinct natural-key anti-join Bronze permits vs Gold per cell; consumer
      replication independent via Bronze S3 (verified 2026-06-09) or producer id manifest (B3).

  - id: B2
    rule_version: 1
    title: reconciliation runs inside the ETL, pre-swap
    section: "§4 B2"
    dimension: timeliness   # guard: volume/process
    scope: snapshot
    mode: enforce
    owner: producer
    measured_as: report.json contains a B1 entry for every (county, year) cell of the snapshot;
      missing cells counted
    alert_threshold: 0
    fail_threshold: 0
    mode_note: process-presence gate, fully producer-controlled — report.json must carry a
      B1 entry per (county, year) cell from day one (same family as S3 bundle
      completeness). The B1 VALUES ramp per §8.1; the reconciliation RUNNING does not.
    today: null
    implements: to_build
    procedure: bundle-completeness check over _AUDIT/report.json (consumer BUNDLE family).

  # B3 (consumer verification means) is governance, not a data gate → audited as CC-2/CC-6
  # in AUDIT_UPDATE_PLAN §0; Bronze S3 read for ignacio-claudecode verified 2026-06-09.

  # ────────────────────────────── COVERAGE (§5) — IDs minted (CC-3) ──────────────────────────────

  - id: COV-01
    rule_version: 1
    title: coverage manifest present, full registry × year grid
    section: "§5"
    dimension: completeness   # guard: coverage / negative evidence
    scope: snapshot
    mode: enforce
    owner: producer
    measured_as: missing (jurisdiction × year ∈ [1990, now]) rows in _COVERAGE/coverage.parquet
      vs the FULL jurisdiction registry (not just observed-in-data)
    alert_threshold: 0
    fail_threshold: 0
    today: "no manifest exists today (provider-gap case PG-2026-06-01 — Gold history stops 2021 unseen)"
    implements: to_build   # consumer ancestor: build_coverage_decisions.py pattern
    procedure: registry cross-join years, anti-join manifest; bz_coverage_label ∈
      {Yes, Some, None, not_ingested} enum-checked; n_distinct_permit_uids + min/max event_date present.

  - id: COV-02
    rule_version: 1
    title: populated cell without coverage row = 0 (fail-closed)
    section: "§5"
    dimension: completeness   # guard: coverage
    scope: snapshot
    mode: enforce
    owner: producer
    measured_as: (jurisdiction, year) cells observed in permit rows with no coverage row
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM (
        SELECT permit_jurisdiction, year(event_date) AS y
        FROM {table} WHERE event_date IS NOT NULL
        GROUP BY 1, 2
      ) seen
      LEFT JOIN {coverage} c
        ON c.jurisdiction = seen.permit_jurisdiction AND c.year = seen.y
      WHERE c.jurisdiction IS NULL

  - id: COV-03
    rule_version: 1
    title: coverage contradictions (label=None with n>0)
    section: "§5"
    dimension: consistency   # guard: coverage
    scope: snapshot
    mode: shadow
    owner: producer
    measured_as: manifest rows with bz_coverage_label = 'None' AND n_distinct_permit_uids > 0
    alert_threshold: 0
    fail_threshold: null    # contract: "reported as contradictions"
    today: null
    implements: to_build
    check: |
      SELECT COUNT(*) FROM {coverage}
      WHERE bz_coverage_label = 'None' AND n_distinct_permit_uids > 0

  # ────────────────────────────── FRESHNESS (§5) — IDs minted (CC-3) ──────────────────────────────

  - id: FRS-01
    rule_version: 1
    title: monthly snapshot SLA (lands by day 20)
    section: "§5"
    dimension: timeliness
    scope: snapshot
    mode: shadow
    owner: producer
    measured_as: days late vs day-20 (vendor drop 11th–15th)
    alert_threshold: 0
    fail_threshold: 5
    threshold_status: provisional fail — contract pins day-20 only (no grace); the 5-day
      fail grace is consumer-proposed, confirm at kickoff.
    today: null
    implements: to_build
    procedure: snapshot_date day-of-month vs SLA; one value per snapshot in report.json.

  - id: FRS-02
    rule_version: 1
    title: per-jurisdiction max_event_date + late-arrival share
    section: "§5"
    dimension: timeliness
    scope: per_jurisdiction_year
    mode: dry_run    # metric that SETS the CallZeke recency blind window — reporting first
    owner: producer
    measured_as: share of permits new in snapshot N with event_date ≤ snapshot N−1
    alert_threshold: null
    fail_threshold: null
    today: null
    implements: to_build
    procedure: requires previous snapshot's permit_uid set (what-changed volume block).

  - id: FRS-03
    rule_version: 1
    title: staleness flags
    section: "§5"
    dimension: timeliness
    scope: per_jurisdiction_year
    mode: dry_run    # cutoffs uncalibrated — §8.1 explicit dry_run list
    owner: producer
    measured_as: jurisdictions flagged — max_event_date < snapshot−365d OR rolling-12m volume
      < 50% of 36m baseline
    alert_threshold: null
    fail_threshold: null
    today: null
    implements: to_build
    procedure: flags must be PRESENT in report.json (presence is the gate; values informational
      until thresholds pinned).

  - id: FRS-04
    rule_version: 1
    title: snapshot-over-snapshot jurisdiction volume drop
    section: "§5"
    dimension: timeliness   # guard: volume/drift
    scope: per_jurisdiction_year
    mode: shadow
    owner: producer
    measured_as: jurisdictions with row volume < 50% of previous snapshot
    alert_threshold: 0
    fail_threshold: null    # contract: ALERT on >50% drop
    today: null
    implements: to_build
    procedure: diff vs previous published snapshot (what-changed volume block).

  - id: FRS-05
    rule_version: 1
    title: county collapse to zero = fail-closed
    section: "§5"
    dimension: completeness   # guard: volume
    scope: per_county
    mode: enforce
    owner: producer
    measured_as: counties going from >0 rows (previous snapshot) to 0 rows
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build
    procedure: row-count diff vs previous snapshot per county.

  # ────────────────────────────── SNAPSHOT & REPRO (§6) ──────────────────────────────

  - id: S1
    rule_version: 2   # v0.2.1: re-spec to §6 S1 as signed (D-8 + v2.2.1) — the v0.2.0 file
                      # still encoded the dropped dated-path scheme and would have failed
                      # the rebuild's first snapshot by construction.
    title: immutable versioned path + snapshot_date coherence + promotion-by-gate
    section: "§6 S1"
    dimension: validity
    scope: snapshot
    mode: enforce
    owner: producer
    measured_as: path matches Permits_Master_Gold/<env>/<version>/FIPS=<fips>/PERMIT_PERIOD=<yyyy-mm>/
      (env/version parameters per D-8 — no dated suffix; permit-period read partition is
      derived from frozen event_date, not mutable raw INITIAL_STATUS_DATE); published <version> never overwritten in place;
      rows where snapshot_date ≠ the snapshot's single metadata-declared snapshot_date;
      prod promotion occurred only after ENFORCE gates passed
    alert_threshold: 0
    fail_threshold: 0
    today: "today: re-commits ~every 3 min on a mutable path (G2 note)"
    implements: to_build
    procedure: path-pattern check + snapshot_date constant-per-snapshot check against
      execution metadata (snapshot_date is an in-table column §3.1; it is NOT derivable
      from the path — no date suffix exists in the D-8 scheme) + immutability check (no
      re-commit into an already-published <version>) + verdict-precedes-swap audit on the
      dev/stage→prod promotion (gate is the single publisher [GATE-01]).

  - id: S2
    rule_version: 1
    title: deterministic rebuild (byte-identical modulo Hudi metadata)
    section: "§6 S2"
    dimension: consistency
    scope: sample
    mode: shadow
    owner: producer
    measured_as: row-set hash diff of a re-run sample partition (same Bronze + same 4 pins)
    alert_threshold: 0
    fail_threshold: 0
    mode_note: threshold IS pinned (0-diff) so dry_run's uncalibrated-threshold rationale
      does not apply — shadow per §8.1, like G4/PTB-01 the blocker is execution
      infrastructure (DE-exposed re-run + signed P-TB); until exposed, the evaluated
      artifact is the determinism attestation in report.json (procedure); promote to
      enforce after 2 green snapshots.
    today: "non-reproducible today (re-commits every ~3 min)"
    implements: to_build
    procedure: producer re-runs one county partition with identical inputs+pins; order-insensitive
      row hash compared. Until exposed, report.json must attest determinism (consumer accepts attestation).

  - id: S3
    rule_version: 1
    title: _AUDIT bundle complete
    section: "§6 S3 / §8.3 / §8.4"
    dimension: completeness
    scope: snapshot
    mode: enforce
    owner: producer
    measured_as: missing bundle parts — report.json (every gate id in THIS file, with measured
      value + threshold + pass/fail + mode, per county) · quarantine/<gate_id>/ samples (≤10
      per gate × county) for every non-pass gate · what_changed.json · gates.yaml version echo
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build   # consumer half = AUDIT_UPDATE_PLAN §2b BUNDLE/QUA
    procedure: bundle manifest check; red gate with empty quarantine dir = bundle defect.

  - id: S3a
    rule_version: 1
    title: undeclared pin change = fail-closed (what-changed)
    section: "§8.4 R-7"
    dimension: consistency
    scope: snapshot
    mode: enforce
    owner: producer
    measured_as: pins (etl_version, classifier_version, addr_norm_version, fa_match_vintage,
      gates.yaml version) that changed vs previous snapshot with no what_changed.json entry
    alert_threshold: 0
    fail_threshold: 0
    today: null
    implements: to_build   # consumer half = AUDIT_UPDATE_PLAN §2b WCH
    procedure: diff distinct pin values vs previous snapshot; cross-check what_changed.json.

  - id: S4a
    rule_version: 1
    title: history depth never shrinks (forward-only backfill detector)
    section: "§6 S4 / D-4"
    dimension: completeness
    scope: per_county
    mode: dry_run    # threshold (training-window start) confirmed at kickoff with S4 policy
    owner: producer
    measured_as: counties whose min(event_date) > training-window start (2021-01-01,
      confirm at kickoff) OR whose min(event_date) moved LATER vs the previous snapshot
      (a forward-only re-load silently shrinking history)
    alert_threshold: 0
    fail_threshold: null
    today: null
    implements: to_build
    procedure: per-county min(event_date) vs training-window start + vs previous snapshot's
      value (from what_changed volume block).

  # S4's one-shot half (the FIRST rebuild's what_changed.json must declare the full break vs
  # Roofing_Master_Gold_V1) is a REBUILD-EVENT governance rule audited via CC-5/CC-7
  # (AUDIT_UPDATE_PLAN §0), not a recurring gates.yaml row. B3 (consumer verification means)
  # is likewise governance — audited as CC-2/CC-6.
