← back to Wallco Ai

docs/ops/dw-stale-urls-trim-B-2026-05-19.md

87 lines

# dw-stale-urls trim-B — HALTED at safety check

**Date:** 2026-05-19
**Operator:** Claude (parking-lot dw-stale-urls substep B)
**Outcome:** NO DELETE — orphan count outside safety band, awaiting Steve.

## Result

- Bad rows in `image_hashes` (status != 'success'): **16,713**
- Distinct referenced URLs across all 177 tables with `image_url`: **651,962**
- **Orphan bad rows (URL not referenced anywhere): 0**
- Safety band per task spec: 9,120 – 13,680 (±20% of 11,400 estimate)
- 0 is **100% below** the lower bound → STOP, per hard constraint.

## SQL run

```sql
-- Discover tables with image_url column (177 found, includes shopify_products + 176 *_catalog / catalog-adjacent tables)
SELECT table_name
FROM information_schema.columns
WHERE column_name='image_url' AND table_schema='public'
ORDER BY table_name;

-- Orphan count
WITH all_referenced_urls AS (
  SELECT image_url AS url FROM _tsd_seed WHERE image_url IS NOT NULL
  UNION ALL SELECT image_url FROM all_designs WHERE image_url IS NOT NULL
  -- … 175 more UNION ALL legs, one per table …
  UNION ALL SELECT image_url FROM zoffany_catalog WHERE image_url IS NOT NULL
)
SELECT count(*)
FROM image_hashes ih
WHERE ih.status != 'success'
  AND NOT EXISTS (SELECT 1 FROM all_referenced_urls a WHERE a.url = ih.url);
-- → 0
```

## Sanity-check trace

To verify the orphan query was not silently failing, picked one bad URL and grepped every catalog table individually:

```
URL: /_assets/07d0a465f98edc0af823532f8bdd8abc/Images/ImageNotFound.jpg
  HIT: legal_scan_cache = 1
  HIT: vc_public_eligible = 1
  HIT: vendor_catalog = 1
  HIT: versa_catalog = 1
```

Conclusion: the join is correct. Every bad URL in `image_hashes` IS referenced by at least one row somewhere in the 177 tables — most often vendor catalog tables that legitimately store the (now-broken) vendor URL.

## Status breakdown of the 16,713 bad rows

| status        | rows  |
|---------------|------:|
| http_404      | 16132 |
| http_401      |   178 |
| http_403      |   143 |
| decode_error  |   142 |
| fetch_error   |    50 |
| http_502      |    24 |
| too_small     |    13 |
| http_524      |     8 |
| http_521      |     8 |
| invalid_url   |     7 |
| timeout       |     5 |
| http_523      |     2 |
| http_522      |     1 |

## Why the estimate was wrong

The 11,400 estimate assumed bad URLs would be old historical entries no longer tied to any catalog row. In reality, `image_hashes` is checked against the *current* catalog mirrors — when a vendor URL 404s, the row stays in both `image_hashes` (negative cache) AND the catalog tables (the catalog still records what vendor URL was scraped, even after it broke). So the negative-cache rows are not orphans — they're the catalog's own broken-link entries.

## Recommendation for Steve

Trim-B as defined ("pure orphans, nowhere in any catalog") deletes nothing. If the goal is "remove negative-cache rows that no longer matter," the policy needs reframing — e.g.:

- **Option 1 — delete by age:** drop `status != 'success'` rows older than N days (`fetched_at < now() - interval 'N days'`), regardless of catalog references. Counts as of today: 16,713 total — would need date histogram to size.
- **Option 2 — delete by status:** drop only `http_404` (16,132 rows, 96% of all bad rows). These are permanently broken vendor URLs; keeping the negative cache only matters if you want to re-probe them. If re-probing is off, they're noise.
- **Option 3 — leave it alone:** 16,713 rows × ~64 B/row ≈ 1 MB. Not worth the operational risk.

Awaiting Steve's call before any DELETE. No backup taken, no temp table staged, `image_hashes` untouched.

## Time elapsed

~3 min from task start to halt (mostly the spot-check pass + writing this report).