# _CONTRACT/ars_gates.yaml — ARS single machine-readable gate spec
# Contract: 8020rei.ars.address_resolution v1.0 (DRAFT) — see knowledge/notes/etl_audit/ARS_CONTRACT.md §7.
# This file is the SINGLE definition of every executable gate in contract §§2–8 [CON-02].
# The runner is the ars-audit battery itself (scripts/ars/, run.py sync|errors|health) —
# consumer runs it as certification before pinning addr_norm_version; producer runs the
# SAME battery as its pre-deploy release suite. Checks here are API/battery PROCEDURES,
# not SQL (a service handoff has no table to scan).
#
# STATUS: v0.1.0 DRAFT (2026-06-09, consumer-authored) — adoption criterion §9.3-2 input.
# EVERY threshold is `threshold_status: provisional` by construction (v1.0, nothing
# producer-pinned before it); values derive from the 2026-06-04 baseline run
# (91/91 PASS · cache 42 / athena 33 / smarty 13 / null 3 · 12.15 s · 1 over-match ·
# 8 weak-DPV; artifacts in knowledge/notes/investigations/ars/artifacts/).

meta:
  contract_id: 8020rei.ars.address_resolution
  contract_version: "1.0"
  gates_yaml_version: "0.1.0"
  status: DRAFT
  date: "2026-06-09"
  authors: [ignacio-consumer]
  pending_sign_off: [ars-producer-tbd]   # named owner = adoption open item 1
  baseline: "2026-06-04 certification run — sync_compare.json + errors.json + health.json (knowledge/notes/investigations/ars/artifacts/)"

  # Field semantics — identical to silver_rem_gates.yaml v0.1.0:
  # id / title (short human label) / section / dimension [DIM-01] / scope / mode (§8.1)
  # / owner / measured_as / alert_threshold / fail_threshold (direction ≤ unless
  # stated) / threshold_status / today / implements (battery component | <artifact
  # filename> | to_build) / rule_version [RUL-02] / procedure.
  # reality: true = documented ground-truth-side reality (here, a Smarty confirmation
  # gap on edge forms — D-4) whose threshold is a tolerance, not a defect target.
  # scope values here: per_run (one value per certification run) | per_case (evaluated
  # over the 91 battery cases) | per_probe (the error-contract probe set).
  # SKIPPED rule: a gate whose component did not run reports SKIPPED with a visible
  # reason — never PASS, never 0. A certification with a SKIPPED ENFORCE gate is NOT
  # green. Detecting "did not run" requires run-keyed artifacts (R2, open item 4);
  # until keying lands, a certification is assessable ONLY if sync_compare.json,
  # errors.json and health.json carry the same run identity (interim — matching run
  # timestamps, verified and recorded in the cert); an artifact from a different run
  # = SKIPPED for every gate it implements. cmd_all's catch-and-continue path
  # (cli.py:102-108) must hard-fail or stamp the cert non-green (open item 4).

  bindings:
    "{battery}": "scripts/ars/ run.py sync — the 91-case suite at the stamped battery_version"
    "{probes}": "scripts/ars/ run.py errors + health — the §2 probe set"
    "{artifacts}": "the certification run's keyed artifact dir (sync_compare.json, errors.json, health.json — R2 keying = open item 4)"
    "{cap}": "documented sync batch cap (120 addresses; probe sends 130)"
    "{cert_max_age_days}": "30 — maximum certification age for CERT-01 (provisional, pin at kickoff)"

gates:

  # ────────────────────────────── API SURFACE (§2/§3.1) ──────────────────────────────

  - id: API-01
    rule_version: 1
    title: response schema complete per result
    section: "§3.1"
    dimension: validity
    scope: per_case
    mode: dry_run   # the key-set ASSERTION is unbuilt (ars_fields() defaults missing
                    # keys to '' — a dropped key certifies green undetected); promote
                    # to shadow when the open-item-4 assertion lands, enforce after 2
                    # green certifications
    owner: producer
    measured_as: battery results missing any contracted field (the 20-key §3.1 shape —
      scored five + delivery_line_2 + summary + source + dpv_match_code/dpv_footnotes/
      rdi/precision/lat/lon/county_name/datatree_id/notes/input_address/plus4_code/
      full_zipcode)
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "0 missing across 91 results (2026-06-04 — verified post-hoc from
      sync_compare.json; NOT asserted by the battery yet)"
    implements: to_build
    procedure: per result dict in the sync response, assert key-set ⊇ the §3.1 manifest;
      count results with missing keys. Assertion = open item 4.

  - id: API-02
    rule_version: 1
    title: error contract conforms (5 probes)
    section: "§2"
    dimension: validity
    scope: per_probe
    mode: enforce
    owner: producer
    measured_as: failing probes among — 422_oversized_sync_batch (130 > {cap}) ·
      422_invalid_shape · 404_missing_job · 404_missing_property ·
      auth_missing_key (401/403)
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "5/5 conform (errors.json 2026-06-04; auth returned 403)"
    implements: errors.json
    procedure: run.py errors — probes/contract.py probe set verbatim; count pass=false.

  - id: API-03
    rule_version: 1
    title: batch-count integrity (one result per input, same order)
    section: "§2 /v1/resolve/sync"
    dimension: completeness
    scope: per_run
    mode: dry_run   # the count ASSERTION is unbuilt — cli.py:78 pads short responses
                    # with empty dicts; promote to shadow when the open-item-4
                    # assertion lands, enforce after 2 green certifications (sibling
                    # doctrine: infrastructure-needing gates park in dry_run, nothing
                    # implicit)
    owner: producer
    measured_as: abs(len(results) − len(request addresses)) — a short or padded response
      is a contract breach, never silently tolerated (the consumer battery today PADS
      short responses with empty dicts at cli.py:78 — replacing that with this
      assertion is open item 4)
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "0 (91 in / 91 out on 2026-06-04) — but unasserted; padding would mask it"
    implements: to_build
    procedure: assert count equality immediately after resolve_sync, before row build;
      order asserted via input_address echo per result.

  - id: API-04
    rule_version: 1
    title: resolution source enum
    section: "§3.1 D-5"
    dimension: validity
    scope: per_case
    mode: dry_run   # the enum ASSERTION is unbuilt (scoring.py buckets ANY source via
                    # setdefault — an out-of-enum value renders as a new bucket, never
                    # fails); promote to shadow when the open-item-4 assertion lands,
                    # enforce after 2 green certifications
    owner: producer
    measured_as: results with source outside the enum — source ∈ {athena, cache, smarty}
      ∪ {JSON null} (no-match rows carry LITERAL JSON null, not the string 'null';
      'null' is display-only bucketing — the checker must accept None)
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "0 violations; mix cache 42 / athena 33 / smarty 13 / JSON-null 3 (verified
      post-hoc; NOT asserted by the battery yet)"
    implements: to_build
    procedure: enum check over results[].source accepting None; per-source pass
      breakdown reported with ACC-01 (D-5 transparency). Assertion = open item 4.

  - id: API-05
    rule_version: 1
    title: health liveness
    section: "§2 /health"
    dimension: timeliness
    scope: per_run
    mode: enforce
    owner: producer
    measured_as: health probe failures — HTTP ≠ 200 OR body.status ≠ 'ok' (latency
      reported informationally; today 3.873 s)
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "200 / ok / 3.873 s — service 'ars-api-poc' (2026-06-04)"
    implements: health.json
    procedure: run.py health; assert status code + body.status.

  - id: API-06
    rule_version: 1
    title: service version exposed
    section: "§2 /health / §6 R1"
    dimension: completeness
    scope: per_run
    mode: dry_run    # the field DOES NOT EXIST today — producer feature, adoption
                     # blocker §9.3-3; promote to shadow when the field ships, enforce
                     # after 2 consecutive green certifications per §8.1
    owner: producer
    measured_as: health body missing a version/build identifier — without it, "certified
      at ars_version X" (REP-01, CERT-01) is unverifiable
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "MISSING — health body carries only {status, service, ts} (2026-06-04)"
    implements: to_build
    procedure: assert a non-empty version field in the health body; record it as the
      run's ars_version pin.

  # ────────────────────────────── ACCURACY (§3.2/§4 — the battery) ──────────────────────────────

  - id: ACC-01
    rule_version: 1
    title: five-field certification pass (vs Smarty ground truth)
    section: "§3.2/§4 D-1/D-2/D-3"
    dimension: consistency
    scope: per_case
    mode: enforce
    owner: both    # producer's standardization quality, consumer's scoring rule
    measured_as: battery cases NOT scoring PASS under the fair 5-field rule (NM-agreed
      D-2 and unit-only D-3 applied). Per-field agreement (5 × n) and the per-source
      breakdown (D-5) ship with the verdict as diagnostics.
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "91/91 PASS; per-field 91/91 ×5; all four sources 100% (2026-06-04)"
    implements: battery-sync
    procedure: run.py sync; count score_row verdicts ≠ PASS. scoring.py is the single
      source of truth; any scoring change = scoring_version bump (R1).

  - id: ACC-02
    rule_version: 1
    title: over-match count (ARS over-reach)
    section: "§4 D-4"
    dimension: validity
    scope: per_case
    mode: shadow
    owner: producer
    measured_as: expect-no-match cases (nonexistent/garbage/foreign/street_only, 4 in
      battery_version 1) that ARS resolved match-mailable where Smarty did NOT confirm
      (overmatch_kind = ars_only). A DEFECT class, not a reality — tolerated at the
      known baseline under the §8.4 waiver; every occurrence alerts.
    alert_threshold: 0
    fail_threshold: 1
    threshold_status: provisional — fail tightens to 0 once case 27 is fixed (waiver §8.4)
    today: "1 — case 27 'nonexistent' (99999 Nowhere Fake St resolved mailable); no owner/disposition yet (open item 7)"
    implements: battery-sync
    procedure: summarize().overmatch.ars_only_n from the certification run.

  - id: ACC-03
    rule_version: 1
    title: weak-DPV pass count
    section: "§4 D-4"
    dimension: validity
    scope: per_case
    mode: shadow
    owner: consumer    # a Smarty-side confirmation gap on edge forms, tracked not fixed
    reality: true
    measured_as: PASS cases where Smarty did not DPV-confirm and the input was NOT
      expect-no-match (weak_dpv — rural route, fractional, grid, territory, no-ZIP edge
      forms; the five fields agree but green means same-string, not deliverable)
    alert_threshold: 8
    fail_threshold: 12
    threshold_status: provisional — baseline exactly 8; alert fires only above it
    today: "8 (zip_missing_empire, rural_route, dl_half, fips_sanjuan_pr, dl_grid_utah, dl_dir_suffix, dl_us_highway, dl_county_road)"
    implements: battery-sync
    procedure: len(summarize().weak.weak_dpv).

  # ────────────────────────────── SLA (§5) ──────────────────────────────

  - id: SLA-01
    rule_version: 1
    title: sync batch latency
    section: "§5"
    dimension: timeliness
    scope: per_run
    mode: shadow
    owner: producer
    measured_as: round-trip seconds for the 91-address sync batch
    alert_threshold: 20
    fail_threshold: 30
    threshold_status: provisional — no SLA existed (POC); 12.15 s baseline; pin at kickoff
    today: "12.15 s (2026-06-04)"
    implements: battery-sync
    procedure: ars_latency_s from the certification artifact.

  # ────────────────────────────── REPRODUCIBILITY (§6) ──────────────────────────────

  - id: REP-01
    rule_version: 1
    title: version pins + keyed artifacts
    section: "§6 R1/R2"
    dimension: consistency
    scope: per_run
    mode: dry_run    # battery work (open item 4) + API-06 producer feature; promote to
                     # shadow when stamps land, enforce after 2 green certifications
    owner: consumer
    measured_as: violations among — certification artifact missing any of
      {scoring_version, battery_version, ars_version} · artifact path not keyed by run
      timestamp (overwrite = violation) · scorecard page stamped with render date
      instead of the collection timestamp
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "ALL THREE failing — no version stamps anywhere (unit-only rule shipped
      unstamped, the RUL-02 case); sync_compare.json overwritten per run; page restamps
      date.today() on rebuild"
    implements: to_build
    procedure: artifact-shape assertion before publishing a certification.

  - id: REP-02
    rule_version: 1
    title: re-score determinism
    section: "§6 R3"
    dimension: consistency
    scope: per_run
    mode: dry_run   # the DIFF is unbuilt — cmd_html only rebuilds (recomputes
                    # summarize() and ignores the stored summary), nothing compares
                    # recomputed vs stored verdicts; promote to shadow when the
                    # open-item-4 diff lands, enforce after 2 green certifications
    owner: consumer
    measured_as: verdict differences between the stored run summary and a rebuild
      (run.py html) re-scored from the same stored sync_compare.json raw values —
      guards exactly the unit-only-retrofit event (a scoring change between run and
      rebuild rendering silently different verdicts)
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional
    today: "0 by construction while run and rebuild share the same scoring.py — but no
      code DIFFS recomputed vs stored verdicts; the comparison is open item 4"
    implements: to_build
    procedure: in cmd_html, compare summarize(raw_rows) against the stored summary
      (pass count, weak sets, per-field agreement); persist + alert on delta.

  # ────────────────────────────── CONSUMPTION + DRIFT ──────────────────────────────

  - id: CERT-01
    rule_version: 1
    title: certification gate for D-1 consumption (fail-closed)
    section: "§7 / GOLD_PERMITS D-1"
    dimension: timeliness
    scope: per_run
    mode: enforce
    owner: consumer
    measured_as: violations of — addr_norm_version may be pinned to ars-<ver> ONLY with
      a green certification newer than {cert_max_age_days} days at that same
      ars_version. GREEN = all ENFORCE gates EVALUATED AND PASS — a SKIPPED enforce
      gate makes the cert NOT green (meta SKIPPED rule). The load-bearing gate;
      checked by the consumer data gate wherever ARS output enters a build (permits
      standardization, list hygiene).
    alert_threshold: 0
    fail_threshold: 0
    threshold_status: provisional — 30-day validity window, pin at kickoff
    mode_note: NOT WIRED today — enforce is intentional and binds AT adoption. The
      consumer data-gate wiring is adoption criterion §9.3-7 (the enforce set must
      actually refuse); registered as a §8.4 deviation until then. Keyed-artifact leg
      depends on REP-01 (open item 4); ars_version leg on API-06 (open item 3).
    today: "not wired — nothing requires a green/current cert before consumption (the
      gap analysis' structural root 3 — no fail-closed gate on this handoff; standard
      GATE-03 fail-closed class); ars_version leg blocked on API-06"
    implements: to_build
    procedure: data-gate check — latest keyed certification artifact exists, is green
      (no skipped enforce gates), is fresh, and (once API-06 lands) matches the pinned
      service version.

  - id: DRIFT-01
    rule_version: 1
    title: certification drift vs the last 5 keyed runs
    section: "§8.3 / standard §11.2"
    dimension: consistency   # guard: drift
    scope: per_run
    mode: dry_run    # needs REP-01's keyed history; promote to shadow at 5 runs
    owner: consumer
    measured_as: relative change of pass count, per-field agreement, by-source mix,
      over-match/weak-DPV counts, and latency vs the trailing 5 certifications —
      "valid but moved" warns, never blocks
    alert_threshold: null
    fail_threshold: null
    threshold_status: provisional — per-metric thresholds pinned after 5 keyed runs
    today: "unmeasurable — single overwritten artifact (REP-01)"
    implements: to_build
    procedure: trailing-window comparison over keyed certification artifacts.
