Data-Backfill Review Queue
Shipped: 2026-08-07 (entirely-api ccf261f + migration 035, entirely-portal
b06aadc, deployed). The human gate between the extraction fleet
(document-extraction-pipeline) and the projects table — the
no-batch-mutations rule embodied as a feature.
Shape
services/backfill_service.pycomputes candidates: for each project FIELD that is missing or a junk placeholder (Unknown/N-A/0/NaaN/…), propose the consensus value across extracted documents. Fields: permit_number, jurisdiction, state, property_owner, permit_issue_date, inspection_date (cert packages only — latest stated visit date). Disagreements surface as a conflict flag + alternatives, never hidden.- Routes
/portal/backfill/{candidates,apply,reject}(up_owner/up_admin). Accepts write throughsupabase_write_service.update_projectwith callerbackfill_review:<user>— raw_data, transforms, and event logging behave normally. Decisions land inbackfill_reviews(migration 035); a rejected (project, field, value) triple never resurfaces. - Portal page
/backfill-review(“Data Backfill”, Monitoring section for owner/admin): per-project cards, current → proposed with agreement chips, conflict badges, per-row accept/dismiss, card-level apply, accept-all (non-conflict rows only).
Volume at launch
3,317 proposals across 1,382 projects with extraction only ~25% through the project-folder corpus (1,172 inspection dates, 779 jurisdictions, 655 owners, 553 permit numbers, 158 permit issue dates; 568 conflicts). Grows as the drip and later the archive phase complete. If reviewing one-by-one proves too heavy at full scale, a bulk-accept of high-agreement non-conflict rows is the obvious next step — but that stays Dustin’s explicit call.
Notes
- Candidates are computed on request (~7.5k-row scan, ~1-2s); cache if it ever bothers anyone.
create_tokenfor testing: auth rides theportal_tokencookie, not the Authorization header.
Bulk accept 2026-08-07 (Dustin-authorized)
“Bulk-accept the non-conflict rows with 2+ docs agreeing” — 1,145 fields
across 586 projects applied via /opt/scripts/bulk-backfill-accept.py
(dry-run → apply INSIDE the api container; host lacks arq). decided_by =
dustin:bulk-2026-08-07; all writes through apply_backfill/update_project.
Verified: 1,145 accepted rows in backfill_reviews; inspection-date coverage
683 → 1,128 projects. Remaining queue 2,172 (568 conflicts + 1,604
single-doc) for manual review — single-doc rows may gain corroboration as
extraction completes; re-run the script (with explicit authorization) after
Phase B.
Bulk accept round 2 — 2026-08-07 (“all rows including conflicts”)
Premise check first: conflicts are never value-vs-missing (junk is filtered pre-candidate) — they are real disagreements. Classified before writing: 1,619 clean single-doc + 311 conflict-with-clear-majority applied (984 projects, 0 failures, decided_by dustin:bulk-all-2026-08-07); 239 exact TIES withheld — writing those would be a coin flip. En route discovered B2021-127 on 111 projects = template boilerplate; cross-project frequency guard added to backfill_service (entirely-api e2cf028, >3 projects ⇒ filtered) — it was never written anywhere (conflict flag caught it). Remaining queue = 239 tie fields / 196 projects (list also at /home/claude/bulk_ties_remaining.json) — genuine judgment calls for the portal UI.