# Fact-check + refresh pass — loopers/
Date: 2026-06-09 (previous pass: [FACTCHECK_2026-05-12.md](FACTCHECK_2026-05-12.md))

Two parts: (1) verify every claim the site makes against the data behind it and against external sources, (2) bring the data current — the shipped snapshot was 26 days stale (2026-05-14).

## Part 1 — claims audit

### WRONG — "850 Bryant: the jail" (map legend + marker)

The cohort's bookings are at COUNTY JAIL 1 (intake), COUNTY JAIL 2, and COUNTY JAIL 3. CJ#1/#2 are at **425 7th Street**; the Hall of Justice jail at 850 Bryant (old CJ#4) **closed September 5, 2020**. CJ#3 is at 1 Moreland Drive, San Bruno — off the map entirely (~36% of cohort bookings are housed there, though intake still runs through 425 7th St).

**Fixed** (two passes): first the marker moved to 425 7th St; then the map was upgraded to show **all three jails** — CJ#1 and CJ#2 at 425 7th St and CJ#3 at 1 Moreland Dr, San Bruno — sized by how many cohort members are held in each right now, with every booking's arc routed to the facility that booking was actually held in. The map frame was widened to include San Bruno; the southbound spoke bundle makes the CJ#3 transfer pattern visible.
Sources: [Hall of Justice (Wikipedia)](https://en.wikipedia.org/wiki/Hall_of_Justice_(San_Francisco)), [SF Sheriff — County Jail #3](https://sfsheriff.com/services/jail-services/jails/county-jail-3), [SF County Jails (Wikipedia)](https://en.wikipedia.org/wiki/San_Francisco_County_Jails).

### NOW SUPPORTED — "After each booking they're released on Pretrial Diversion within a few days"

Both halves of this claim are now backed by data (the first half took two tries):
1. **"on Pretrial Diversion"** — initially flagged unsupported because the jail feed has no release-mechanism field. **Recovered from the SF Superior Court Register of Actions** (`register_of_actions.parquet`), which records the dated release ORDER per case. Across the 255 cohort cases with a release/hold record, the mechanism breakdown is **110 own-recognizance, 97 Pretrial Diversion Project supervision, 33 electronic monitoring (240 released) vs 40 county-jail commitments + 43 OR-denied**. So most cohort cases do end in pretrial release, and Pretrial Diversion specifically is the mechanism in 97 of them — the claim is real, just from court data, not the jail feed. (`roa_custody.json`, `build-roa-custody.py`.)
2. **"within a few days"** — naively computed over all booked→released pairs the median looks like 10 days, but that's **polluted by the feed outage** (Part 3): gap-spanning stints smear release dates onto catch-up days. On the continuously observed period (Apr 21 – Jun 9): 92 completed Looper stints, **median 4 days**, 50% ≤3d, 76% ≤7d (median holds at 4d even counting the 33 still-open bookings). The small ROA-datable sample agrees (median 4–5d), an independent cross-check.

**Final hero copy**: "released within days (on their own recognizance or to Pretrial Diversion supervision, per the court record) and they cycle back: four in five observed returns come within a month of release, three in ten within a week." The footer carries the mechanism breakdown; each row's expansion lists that person's actual court release orders with dates.

### WRONG (STALE DATES) — "138-day window", "Dec 25, 2025 to May 11, 2026", "SF Superior Court 2022 to present"

- "138 days" was hardcoded in the hero, two board headers, the footer, and the OG image — wrong even for the May data (141 days) and drifting daily. **Fixed**: window length and dates are now computed from the data everywhere (footer fills from `loopers.json`, OG image computes day count).
- "SF Superior Court 2022 to present" — the court pull ends **2026-04-26**. **Fixed**: now labeled "April 2022 – April 2026 pull".

### WRONG (STALE CLASSIFICATION) — Outcomes tab measured idle time as of 2026-05-12 against court data ending 2026-04-26

Concrete miss: **MOISES SANCHEZ** was classified "recovered" (idle 2.6y in court data) while the jail feed shows him **booked into SF County Jail on 2026-06-09** — the day of this pass. The court feed simply lags arrests by weeks-to-months and misses unprosecuted bookings entirely.

**Fixed**: `build-outcomes.py` now also matches each defendant against the sfoil jail feed (strict containment: every token of the court name must appear in the jail name) and classifies anyone booked in the last 180 days as looping. Result: **80 → 153 looping, 7 → 2 tapered, 8 → 5 recovered, 161 → 96 uncertain**. The 88 jail-feed-only flips were eyeballed: dominated by distinctive names (VIENGPHET SASONE, DENAYAHA DUREE, TRAVALIN MOORE, IGNACIO DZUL, BABOR SAYED…) — these are real re-bookings the court pull missed, not name collisions. Caveat: 2-token name matching can still collide on very common names; same risk class as the rest of the pipeline's name joins.

Notably this kills the previous pass's headline "7 tapered": CARLOS HERNANDEZ (193 lifetime cases) and LEMUEL BROWN (119) were both booked into jail within the last 6 months. They had not tapered; the court feed had.

### BROKEN — footer factcheck link 404'd

`FACTCHECK_2026-05-12.md` sat outside `public/`, so both the bun server and Vercel (outputDirectory: public) returned 404. **Fixed**: factcheck files copied into `public/`, footer links to both passes.

### FRAGILE/IMPRECISE (fixed in passing)

- Leaderboard prose indexed `yearly[3]` for "peaking in 2025" — now computes the peak year. Criterion text now states the real rule (≥3 prosecuted arrests **and ≥2 homeless-coded charge categories** in a calendar year) and notes 2022 is a partial year (pull starts April 2022).
- Outcomes "258 defendants" was hardcoded — now `${OUTCOMES.rows.length}`, with the as-of date shown.
- Attorneys tab now states its data is the **63-Looper / 724-case snapshot of May 14, 2026** (court records for the 43 members added since haven't been pulled).
- "looping" blurb said "Active 59-cohort" (stale since the cohort hit 63, let alone 106).

### VERIFIED TRUE

| claim | result |
|---|---|
| "The names are already public" | SF Sheriff publishes the jail roster; sfoil mirrors it |
| Attorneys tab "724 cohort cases" | 724 case_detail files in data/crimportal |
| Leaderboard "top defendant has 193 cases spanning 27 years" | CARLOS HERNANDEZ is genuinely the max (first-row-equals-max was coincidence; now irrelevant since he's also top by recent arrests) |
| "cohort visible in court data grew every year" (1→11→28→46) | with partial-2022 caveat now disclosed |
| Drug charges are user-side, not dealing | 628 controlled-substance/paraphernalia charge literals, only 47 (7.5%) mention sale/transport/purchase |
| No Looper matches a court-confirmed decedent | 0 token-matches against the 42 Path-1/1b decedents (re-run on the 106 cohort) |

## Part 2 — refresh (2026-05-14 → 2026-06-09)

| layer | before | after |
|---|---|---|
| sfoil mirror (jail.sfoil.com) | 3,984 inmates, last_seen 2026-05-14 | **4,955 inmates**, last_seen 2026-06-09; 0 failed detail fetches |
| da_arrests.parquet (DataSF czsm-3ei3) | through 2026-04-26 | **through 2026-06-07** (+2,109 rows) |
| SFPD incidents (wg3w-h783) | 9,830 rows | re-pulled for all 305 looper incident numbers |
| arrest locations geocoded | 156 dockets | **262 dockets** (50% of 524) |
| Looper cohort (canonical 10-tag, ≥2 true arrests, ≥3 tags) | 63 (35 in / 28 out) | **106 (39 in / 67 out)** — 0 dropped, 43 added |
| window | Dec 25 – May 14 (141d) | **Dec 25 – Jun 9 (167d)** |

Cohort growth is mostly a visibility artifact, not a crime wave: the qualifying window is "rolling 12 months" but the sfoil feed only exists since 2025-12-25, so every extra week of feed surfaces people who were always cycling. Expect the count to keep rising until the feed is a year old.

### Consistency checks on the new build (all pass)

- totals {106, 39 in, 67 out} match recomputation; `in_custody_now` ⟺ last booking has null release for all 106
- `n_bookings` = bookings array length for all 106; every member ≥2 bookings and ≥3 derived tags
- sparkline total (214) = in-window bookings (214); no future-dated bookings
- age present for all 106 (21–60, median 37)
- 42 pre-window (datecharged before Dec 25) bookings remain visible only in row expansions — disclosed in footer

## Part 3 — audit of the source itself (jail.sfoil.com, our own mirror)

### FINDING — the scraper was down ~Jan 6 – Apr 21, 2026

100 of the window's 167 days show **zero bookings jail-wide** — almost all of Jan 1 – Apr 20 — with catch-up polls on Mar 9 (507 "releases" stamped in one day), Mar 12 (566 bookings), Mar 22, Apr 10–13, then continuous operation from Apr 21. This is not a loss in our local pipeline: the May 14 backup snapshot and the original May 11 recon dump show the **identical** Jan–Apr profile, and the June 9 re-pull is a strict superset of both. The gap is in the upstream DB. Cross-checked against the Sheriff's official booking counts (DataSF [36n6-w97s](https://data.sfgov.org/Public-Safety/Sheriff-Jail-Bookings-by-Ethnicity/36n6-w97s)):

| month | official SFSD bookings | mirror (booked_date) |
|---|---|---|
| 2026-01 | 1,291 | 184 |
| 2026-02 | 1,142 | 0 |
| 2026-03 | 1,380 | 741 (mostly catch-up stamps) |
| 2026-04 | 1,310 | 933 |
| 2026-05 | 1,493 | **1,497 (≈100% capture)** |

Implications:
- **~2,400 booking events from Jan–Apr are missing or date-smeared.** Anyone booked *and* released inside the gap never entered the DB — unrecoverable from the SFSD lookup, which only shows current custody. (If the Railway Postgres or raw scrape logs have anything the API isn't surfacing, it could still be backfilled.)
- **The 106-Looper cohort is an undercount** — people who cycled entirely within the gap are invisible.
- `booked_date`/`released_date` are scrape-observation timestamps: trustworthy when the feed is live (since Apr 21, 92% of booked_dates are within 24h of `datecharged`), smeared across the gap. The app's use of `datecharged` for ribbon/sparkline positions was the right call; duration stats must be computed on the clean segment only (the hero numbers now are).
- When live, the mirror is near-perfect: May = 1,497 observed vs 1,493 official.

**Disclosed** in the site footer. **Action item for the sfoil side**: the scraper needs uptime monitoring/alerting — four silent months cost roughly a third of the dataset.

### RECOVERED — partial recovery of the gap from court records (2026-06-09, follow-up)

The gap is partially reconstructable from data already on disk, because the DA's arrests-presented feed (DataSF czsm-3ei3) never went down and sfoil dockets join it directly (`docketnumber` = `court_number` = court `case_number` minus "CRI"):

- **3,055** DA-presented arrests fall in the gap window (Jan 6 – Apr 20); only **845** are visible in sfoil. Of the 2,210 invisible ones, **756 recovered with defendant names** — 723 via the court-cases join, 33 more via crimportal rap-sheet docket pairs. February alone: 240+ named arrests recovered.
- **16 of the recovered arrests belong to 13 current cohort members** (e.g. ROBERT MICKLES arrested 2026-02-10 on 11350/11364, MICHELLE GREGOR 2026-02-25 on 459/487/11364) — real mid-window cycles the ribbons can't show.
- **Combining recovered arrests with existing sfoil bookings, 9 additional people qualify as Loopers** (marked **"court-recovered"**), via name-matched DA arrests + their sfoil bookings. 2 medium-confidence common-name matches left out; 4 rejected (same-custody / surname mismatch).
- **A second pass adds 56 more (marked "court-reconstructed"), taking the cohort to 171 (106 feed + 9 + 56).** These are sfoil people who already show a Looper-pattern booking, plus a gap arrest proven *on their own docket* — identity certain, no name-matching. Three own-docket evidence types, each filtered to fall **outside** observed custody intervals **and ≥7 days before any booking start** (the buffer matters — booked_dates can be stamped late by catch-up scrapes, so an event just before a booking is likely the same custody event): **DA-recorded arrest dates** on their dockets (czsm-3ei3 — the DA's official record of when that case's arrest happened, with booked charge codes that can also complete missing tags), **in-custody court proceedings** (OR-denied / pre-arraignment release consideration / in-custody referral / PSA / release order — these only happen to someone in custody), and **bench-warrant returns** (strong; minority could be voluntary surrenders).
- This pass went through three self-audit rounds, each of which removed false positives: (1) arraignment-alone is NOT custody proof (people are arraigned from the street) — initial "arraignment-backed" candidates were re-vetted against custody-only markers; (2) DA-arrest-date evidence within 7 days of a booking start is the same arrest with a smeared stamp (e.g. KRYSTEN HONEA: DA arrest Mar 21, "booking" Mar 22 — a catch-up stamp day) — **14 candidates dropped on the buffer rule**; (3) two earlier rejects (VANSICKLE, WU) legitimately returned once DA-recorded arrest dates well outside their bookings surfaced. 7 of the 56 qualify via tag-completion (their gap arrest's charge codes supply the missing tag).
- All supplemental evidence is consumed data-driven from `gap_recovery_2026-06-09.json` (`supplemental_evidence`, with `recovery_type`) by `rebuild_loopers_cohort.py` and `build-data.ts`. Their gap re-arrests also render as × marks on the ribbon.
- Caveats: names attach to only 34% of invisible arrests (court scrape cutoff ~Mar 26), DA-presented misses cite-and-release, and charge → tag mapping uses statute codes rather than booking literals. All recovery output: `data/sfoil/gap_recovery_2026-06-09.json`.

### RECOVERED — further inference from on-disk data (no new scraping)

**Booking-number ledger.** SFSD booking numbers are serial (`2026-00666015`). The 2026 sequence spans 660000–667349 (7,350 numbers); the mirror holds 3,713. Of the 3,637 missing numbers, 2,629 bracket between March-stamped neighbors and 862 between April-stamped — i.e. they fall in the outage.

**Reconstructed gap ribbon (built — `build-gap-reconstruction.py` → `gap_reconstruction.json`).** Two on-disk signals refill the blackout, drawn in a distinct hatched/× style so they're never mistaken for feed data:
1. *Booking-number interpolation* — re-dates the scraper's catch-up-stamped bookings to their true date (a "Mar 9" booking at seq ≈ 660,240 is really Jan 6). **21 cohort bookings re-positioned.**
2. *Court-proceeding booking detector* — bench-warrant returns (re-arrests) + arraignments dated in the gap, clustered per person. **74 court-sourced events across 59 Loopers (49 re-arrests + 25 arraignments)** the feed never saw — e.g. CHARLIE LEE BLAND re-arrested Feb 9 and Mar 13 inside the dark window. Membership unchanged; this restores *visible cycling* during the outage.

- 0 records with released_date earlier than booked_date (4,955 inmates, ~7k bookings)
- 1 sfno appearing under two inmate ids (negligible)
- 0 failed detail fetches on full re-pull
- Seed day Dec 25 (1,190 bookings stamped) correctly represents "everyone in custody at feed start" — the app already marks pre-window bookings with a "+N" chip

### Not refreshed (labeled as snapshots in the UI instead)

- **crimportal court pulls** (rap sheets, lifetime chips, attorneys, 724 case details) — browser-session pull, still the May 14 63-member snapshot. The 43 new members have no lifetime chip / attorney coverage yet.
- **leaderboard_v2** (≥6 prosecuted arrests) — court window still April 2022 – April 2026; outcomes idle-time now measured as of 2026-06-09 against it **plus** the live jail feed.
