La Socrata Ingester

repo: ~/Projects/la-socrata-ingester · 103 commits · 0 in last 24h, 1 in last 7d · █

Search the build

103 commits indexed

  1. 30db64a 2026-09-23 auto-data-snapshot: 2026-09-23T08:28:09 (1 data files) — data/canary-baseline.json
  2. ac0fc9c 2026-09-18 buildingpermits viewer: remove VIEWER_NO_AUTH bypass, require un/pw
  3. d0c1307 2026-09-15 auto-data-snapshot: 2026-09-15T08:12:49 (1 data files) — data/canary-baseline.json
  4. cbc32be 2026-09-12 fix(la-data): adaptive page-size backoff for oversized-geometry gateway 502s (TK-11500)
  5. 1879ff0 2026-09-12 chore: v0.2.4 (session close)
  6. 93a7ac5 2026-09-12 fix(la-data): tolerate assessor_parcels' audited page-1 400 (TK-10955)
  7. 28fcd49 2026-09-11 fix(la-data): don't cache a failed catalog fetch; report probe errors honestly
  8. 482ed58 2026-09-11 feat(la-data): verified auto-repoint for drifting NavigateLA layer ids
  9. 314f1dc 2026-09-11 fix(la-data): repoint 9 NavigateLA layers after a SECOND upstream +1 insertion
  10. f6042cd 2026-09-10 fix(la-data): scope allowFailure to the audited failure, not the source name
  11. 88e40a8 2026-09-10 fix(la-data): repoint 9 NavigateLA layers after upstream +1 re-index; pin layers by NAME
  12. f5caca4 2026-09-08 auto-data-snapshot: 2026-09-08T08:24:00 (1 data files) — data/canary-baseline.json
  13. f524e0b 2026-09-05 auto-data-snapshot: 2026-09-05T08:08:29 (1 data files) — data/canary-baseline.json
  14. feb3908 2026-09-01 auto-data-snapshot: 2026-09-01T08:28:16 (1 data files) — data/canary-baseline.json
  15. 79e59f8 2026-08-30 Record LA refresh exit-policy proof
  16. ece3ea5 2026-08-30 Treat audited zoning failure as aggregate degradation
  17. 9460ee0 2026-08-27 auto-data-snapshot: 2026-08-27T08:19:36 (1 data files) — data/canary-baseline.json
  18. 1d31e30 2026-08-26 auto-data-snapshot: 2026-08-26T09:01:00 (1 data files) — package-lock.json
  19. 6069fd2 2026-08-26 chore: v0.2.3 (session close — arcgis transient-retry fix)
  20. a8295e4 2026-08-26 arcgis: retry transient body-level errors (499/CONT_0001,429,5xx) so one flaky source can't poison the nightly batch exit code (TK-10851)
  21. 45c0d9f 2026-08-26 chore: v0.2.2 (session close)
  22. 559875d 2026-08-25 fix(la-data): repoint gis_parcels dead 2025 ArcGIS layer -> live 2026_gdb (400 Item-does-not-exist was failing the whole nightly run via pipefail). Verified 1-row smoke ingests clean.
  23. 93d1279 2026-08-25 auto-data-snapshot: 2026-08-25T08:16:28 (1 data files) — data/canary-baseline.json
  24. b16e3ff 2026-08-18 auto-data-snapshot: 2026-08-18T08:10:11 (1 data files) — data/canary-baseline.json
  25. e6f0356 2026-08-12 Viewer: CD/ZIP quick-jump control on the Map view
  26. 48e81f3 2026-08-12 Viewer: per-column filter inputs in the List header
  27. bcf4edb 2026-08-12 Viewer: saved views — named snapshots of the full view state
  28. d7a05ae 2026-08-12 Viewer: column show/hide menu, / search shortcut, density+columns in deep-link
  29. 0d560bd 2026-08-12 auto-data-snapshot: 2026-08-12T11:28:02 (1 data files) — package-lock.json
  30. 57d3d5a 2026-08-12 Viewer: loading spinner + empty-state + fetch-error overlay
  31. f56acfa 2026-08-12 chore: v0.2.1 (session close — rate-limit script finalize)
  32. d5d5fec 2026-08-12 Viewer: CSV export of current filtered view + fix blank Issued date column
  33. eb45025 2026-08-12 rate-limit: prompt for token interactively when none supplied (avoids <bracket> shell-redirect trap)
  34. d2328d9 2026-08-12 add rentv LinkedIn enricher via openclaw (dedup by firm, prioritized, resumable)
  35. f06221a 2026-08-12 chore: refactor (ERA_DAYS const, indent), minor version bump (session close)
  36. 952a377 2026-08-12 property.js: one command → property + crew + each member's LinkedIn/email/phone (--linkedin opt-in openclaw fill)
  37. 258fd0a 2026-08-12 add targeted openclaw LinkedIn tool (real-browser Google; site-less deal crews; precision-guarded)
  38. da8cbba 2026-08-12 yoloforever c1: disk-space preflight in run-refresh.sh (skip+CNCP note below 10G floor) — Cody FIX-FIRST, prevents ENOSPC PG crash at 04:30 on the 96%-full disk
  39. 298f4ef 2026-08-12 li-search: multi-engine rotation + bing/mojeek (built; PAUSED — $0 bulk LinkedIn search is bot-blocked, integrity risk)
  40. cefbc8e 2026-08-12 contacts: per-row try/catch so one bad URL can't crash a shard worker
  41. 71dd4ab 2026-08-12 li-search: multi-source (cslb + rentv_licensed_targets) through one rate budget
  42. 7ba14e7 2026-08-12 contacts: fix $2 type inference in linkedin_source CASE (::text cast)
  43. d5b07c9 2026-08-12 contacts: linkedin_source tier (site=authoritative vs search=candidate) + phase-2 site-less LinkedIn search
  44. 27b3127 2026-08-12 Add --testfire button to canary-alert (fires one real CNCP+George alert, synthetic, leaves armed state untouched)
  45. 90cf00f 2026-08-12 contacts: $0 own-site miner for linkedin/email/phone (NANP-validated, TOS-safe)
  46. 15c6ec1 2026-08-12 waf: period 10 (plan-allowed) — 20 req/10s = 120/min ceiling
  47. b8b0d82 2026-08-12 Add transition-only canary alert wrapper (CNCP card + George email on NEW problem, silent otherwise; --selftest+DRY_RUN verified); point canary plist at it — enable is gated
  48. cc6b19f 2026-08-12 Archive la-permits-live-refresh plist for one-copy rollback of the pending consolidation
  49. 5ef077c 2026-08-12 Add self-rotating refresh wrapper (bounds tmp/ingest.log to ~10MB, no extra cron job); point refresh plist at it — swap is gated
  50. 707ec14 2026-08-12 viewer: swap live ranking to permit_lead_score_v2 (0/1.58M penalized; teardown signal)
  51. 4c7b267 2026-08-12 Pre-stage lint-clean launchd plists for daily canary (08:00) + refresh (04:30) — install gated, awaiting Steve
  52. e9415b8 2026-08-12 rate-limit: finalize apply-ratelimit.sh — token-free DRY_RUN preview, dedup rule, verified JSON+zone
  53. c9ccae5 2026-08-12 Unified property drill-down: address -> property + trade structure + NAMED CREW + contacts
  54. b291216 2026-08-12 permit-crew c-fix (Cody gate): era-scope the crew + dates + entity decode + no-match flag
  55. 1cd9469 2026-08-12 Named build crew (scripts/permit-crew.js) — the linkage layer: subs on any build
  56. fa030eb 2026-08-12 Property drill-down (scripts/property.js): full permit/trade crew-structure per parcel
  57. 012231a 2026-08-12 enrich: domain-guessing pipeline (scripts/enrich-domain.js) — the real max-it unlock
  58. 14a832a 2026-08-12 enrich: don't lose DDG-rate-limited rows (mark ddg-blocked, 45s cooldown, throw on 403/429)
  59. daf3111 2026-08-12 enrich: --shard=i/N + --ollama/--model for max-it fan-out across Mac2+Mac1 Ollama
  60. 5fc3e57 2026-08-12 Contractor enrichment: deterministic directory blocklist (nulls LLM-accepted aggregators)
  61. c5f4234 2026-08-12 Contractor web-enrichment — $0 local (free DDG search + local Ollama verifier)
  62. 804a15e 2026-08-12 yoloforever c8 FIX (Cody): status brief market = permit volume + median (not outlier-inflated sum)
  63. e1a34c1 2026-08-12 yoloforever c8: consolidated platform status brief (scripts/status-brief.js)
  64. 1d08e30 2026-08-12 viewer: make filters+sort shareable in the URL (deep-link/bookmark any filtered view)
  65. d115714 2026-08-12 yoloforever c7 FIX (Cody): honest digest — verified-only headline + district table
  66. 02d9b82 2026-08-12 yoloforever c7: RE-news deals-digest markdown generator (scripts/deals-digest.js)
  67. 4aad896 2026-08-12 yoloforever c6 FIX (Cody): drop invalid per-ZIP ratio, add valid county supply/demand + fix trade map
  68. e534fb9 2026-08-12 yoloforever c6: contractor-market intelligence (scripts/contractor-intel.js)
  69. 00f8def 2026-08-12 yoloforever c5 FIX (Cody): gate teardown bonus on valuation>=$500k
  70. 48faabe 2026-08-12 yoloforever c5: permit_lead_score_v2 — parcel-signal teardown/redevelopment ranking
  71. 60f8851 2026-08-12 yoloforever c4 FIX (Cody): deals feed ranks by lead_score, dedups address, flags outliers
  72. 909b171 2026-08-12 yoloforever c4: newsworthy permit deals feed (scripts/deals-feed.js)
  73. 0377c1a 2026-08-11 yoloforever c3 FIX (Cody): green-able canary + real CSLB load + time-anchored bleed
  74. f794850 2026-08-11 yoloforever c3: close Cody-c1 canary gaps — CSLB tracking + rolling-window bleed detection
  75. b3681eb 2026-08-11 yoloforever c2 FIX (Cody): median_value + explicit scope label + CD-join diagnostic
  76. f57ce2c 2026-08-11 yoloforever c2: per-CD + per-ZIP permit-activity rollups (scripts/rollups.js)
  77. 3687729 2026-08-11 yoloforever c1 FIX (Cody gate): canary reads la_ingest_state for per-source staleness+errors
  78. 9a4905f 2026-08-11 yoloforever c1: LA-data freshness+rowcount canary (baseline-aware, read-only)
  79. 02a2dc3 2026-08-11 Add monetization-readiness checklist (viewer/MONETIZATION-READINESS.md)
  80. 3ff3d2f 2026-08-11 Add one-step rate-limit apply script (needs WAF-scoped token)
  81. 8d2dafc 2026-08-11 Compliance hygiene: add Terms of Use + Privacy Policy pages + footer links
  82. daffbc8 2026-08-11 chore: bump v0.1.0 → v0.1.1 (session close — compliance footer)
  83. cf20498 2026-08-11 Compliance: pull board.html (monetization deck) off public surface + add robots.txt
  84. 69f49ff 2026-08-11 Viewer: swap CARTO basemap -> OSM tiles (ToS/copyright) + OSM attribution
  85. 1efa08a 2026-08-11 viewer: mirror compliance footer onto table.html + board.html
  86. 6f64d55 2026-08-11 viewer: add source-attribution + as-is/no-endorsement compliance footer
  87. fa5796c 2026-08-11 Deploy: buildingpermits.agentabrams.com public via Cloudflare Tunnel (pm2 + DEPLOY.md)
  88. ff21d5c 2026-08-11 Viewer: List/Grid/Map views + color-ID + shareable ?view=&color= deep links
  89. 4862c20 2026-08-11 GIS land use via hosted LADCP FeatureServer (56,901 features, reliable)
  90. 0ccaea8 2026-08-11 GIS: parcel polygons via hosted FeatureServer (421,684 w/ geometry); no-order offset for gateway layers
  91. cc12805 2026-08-11 GIS layers (NavigateLA ArcGIS) + CSLB contractor loader
  92. bc01699 2026-08-11 auto-data-snapshot: 2026-08-11T09:10:52 (1 data files) — db/gis_schema.sql
  93. d9ab84f 2026-08-11 Promote merged card+lead-score viewer to index.html; preserve table as table.html
  94. 2efe81b 2026-08-11 Viewer: VIEWER_NO_AUTH env bypass for local screenshot/dev (auth on by default)
  95. f47742c 2026-08-11 Viewer: sortable+movable-column table + permit_lead_score ranking (default sort)
  96. bbdafa5 2026-08-11 LA Building Permits viewer frontend: faceted rail, search/sort/density, detail modal
  97. ed10986 2026-08-11 Add DTD-driven Opportunity Board (board.html)
  98. 00f0a40 2026-08-11 Add palisades_rebuilds + current_permits_all views + pool/master lead exports
  99. b9cec1a 2026-08-11 Add current_permit_leads view + CSV export (2,034 live residential leads, $370M)
  100. 0c2fe12 2026-08-11 Add --year=YYYY for resumable per-roll-year assessor loads
  101. b045794 2026-08-11 assessor --full now scans all roll years (matches docs); default stays latest year
  102. 4c680aa 2026-08-10 Fix: dedupe rows by conflict key within upsert batch (last-wins)
  103. 899b4fc 2026-08-10 LA Socrata/ArcGIS public-data ingester → realestate DB (TK-10450)

Authors

Agents used

  • none detected

Skills used

  • /canary-baseline8
  • /claude8
  • /code8
  • /phone4
  • /sort3
  • /density3
  • /run-policy2
  • /query2
  • /layer-identity2
  • /status2
  • /era2
  • /hide2
  • /permits2
  • /property2
  • /trade2
  • /plumbing2
  • /linkedin2
  • /contractor2
  • /value2
  • /board2
  • /year2
  • /arcgis-adaptive-page1
  • /auto-repoint1
  • /zero-1
  • /view1
  • /color1
  • /columns1
  • /popover1
  • /req1
  • /mojeek1

Creative ideas + design notes

Commits with substantial prose (≥120 chars) — the rationale behind each move.

cbc32be · 2026-09-12 · fix(la-data): adaptive page-size backoff for oversized-geometry gateway 502s (TK-11500)
Zoning stalled at 75% (43,957 of 58,953 rows) because the NavigateLA gateway 502s
on a GEOMETRY page whose response exceeds ~30s to serialize. Root cause (pre-measured
by claude-run-10955, re-confirmed live 2026-09-12): the OBJECTID 42000-56000 band
carries ~22KB of geometry per feature, so a 2000-row page there is ~44MB. It is a
fixed OBJECTID band, not a fixed depth, so neither offset- nor oid-cursor paging
avoids it — the constraint is response SIZE.

Fix: arcgisPages now halves resultRecordCount on a gateway size-failure and retries
the SAME cursor position (down to a floor, LA_ADAPTIVE_MIN_PAGE default 50), then
grows back x2 on success so the rest of the layer still pages fast (AIMD). General to
both offset and oid modes. fetchArcgis gains an httpTries knob so the descent fails
fast while shrinking and spends the full retry budget only at the floor — a page that
502s even at the floor is a real outage and is rethrown (fail-loud; the aggregate
run's allowFailure exemption still catches a genuinely stuck source). Only geometry
pages shrink; a non-geometry transient is a real upstream outage and is never masked.

Proven three ways: negative test test/arcgis-adaptive-page.test.mjs (red on the pre-fix
baseline, green with the fix; covers offset backoff, all-band completion, non-geometry
fail-loud, oid mode); full existing suite unchanged; and a bounded read-only live run
against the real OBJECTID>42000 band — real 502 at 2000 caught, halved to 1000,
completed 2200 rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkjvkxHU1B2a7gfVQFKzxX
1879ff0 · 2026-09-12 · chore: v0.2.4 (session close)
Version bump only — reflects the TK-10955 assessor_parcels allowFailure fix
(93a7ac5) committed earlier this session. No lint issues found (no linter
configured in this repo); reviewed the diff manually, no refactor needed —
it mirrors the existing gis_zoning allowFailure pattern exactly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MffyeUFB1BstaK2xS7ufYd
93a7ac5 · 2026-09-12 · fix(la-data): tolerate assessor_parcels' audited page-1 400 (TK-10955)
FeatureServer has twice returned the same generic HTTP 400 ("Cannot perform
query. Invalid query parameters.") on page 1 before any row is fetched
(2026-09-10, 2026-09-12), always ~55.3s after the request to within 50ms
across both occurrences -- consistent with an upstream execution-time
ceiling under load, not a bad query. Manual replay of the byte-identical
runtime query succeeded live both times. Table is upsert-only (conflict on
ain+roll_year, never deleted) and the OID-cursor rescans the full current
roll year every run, so a skipped day just leaves yesterday's 2.4M-row
snapshot in place one extra day -- no data loss, no corruption.

Added an allowFailure exemption scoped to the exact audited message,
mirroring gis_zoning's existing precedent exactly: a genuinely different
assessor_parcels error still fails the aggregate run loud, and a direct
(non-aggregate) run stays strict. Extended test/run-policy.test.mjs with a
positive control, a negative control, and a direct-run-stays-strict check.

Verified: 4/4 suites pass (18 checks); live smoke test (--max=200) confirms
normal ingestion is unaffected; full 'cli.js all' aggregate re-run today
exits 0 via the real classifyRunResults against real SOURCES (gis_zoning's
known 502 tolerated, assessor_parcels succeeded outright this run).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MffyeUFB1BstaK2xS7ufYd
28fcd49 · 2026-09-11 · fix(la-data): don't cache a failed catalog fetch; report probe errors honestly
Two defects surfaced while running the shipped resolver past a second model. Codex
was spend-capped (HTTP 429) and Kimi's key is expired, so the lens was a local 8B
model whose output was mostly a restatement of the questions — but two of its
prompts were worth answering properly, and one was a real bug.

REAL BUG — fetchCatalog memoised FAILURES. `Promise.resolve(...).catch(() => null)`
cached a null for the life of the process, so one transient blip while resolving the
FIRST drifted source silently downgraded every later source in the same run to the
window-scan path. Not a correctness hole (the window path also demands full proof)
but needlessly narrower. Failures are now forgotten so the next source retries.
Proven by check 11 plus a negative control showing the old shape called the catalog
once where the new one calls it twice.

MESSAGE HONESTY — probeLayer swallowed every error into `null`, which is
indistinguishable from "that layer isn't there". It still fails loud either way
(never ingests), but the error text could mislead a human into reading a flaky
upstream as "the layer genuinely moved away". Probe errors are now counted and the
failure message says "(N probe(s) ERRORED — absence here is not proof of absence)".

Analysed and NOT changed: "what if the catalog and the per-layer plane are BOTH
stale?" A consistently-old upstream is self-consistent — our pinned (new) id
mismatches, the old catalog names the old id, the old per-layer plane re-proves it,
and we repoint to the old id, which in that world IS our layer. Correct. The
dangerous case is SKEW between the two planes, which is exactly what the re-proof
step catches and what check 3 covers. The 'unverifiable keeps a pin but cannot MOVE'
asymmetry is likewise deliberate, not a false-green: it never lets weak evidence
move us onto a new layer, it only declines to break a pin that already matches.

5/5 suites (11 auto-repoint checks). Live re-verified: 9/9 clean on the pinned ids,
9/9 auto-repoint from stale ids. TK-10955. $0 local.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CocwcUw6gFt3tr4CZJmra6
482ed58 · 2026-09-11 · feat(la-data): verified auto-repoint for drifting NavigateLA layer ids
Steve's call (2026-09-11, DTD option B/D). NavigateLA layer ids are positional, so
LA inserting one layer renumbers everything below it. That happened on 09-05 and
again on 09-11 ("Special Event Permits" at position 52, 452 -> 453 layers). The
fail-loud-only posture ratified on 09-10 was right at the time but, at that
recurrence rate, means a roughly weekly outage where GIS layers sit stale until a
human repoints. This reversal is deliberate and the new fact is the recurrence.

New policy: the pinned id is a HINT. On a name mismatch the adapter FINDS the layer
by exact name and PROVES it before reading a single row; it still fails loud on
anything it cannot prove.

  resolveLayerIdentity(src) — two discovery paths, proof required either way:
    1. service catalog (one cached GET) — covers any drift distance
    2. bounded +/-N window scan of per-layer endpoints — the fallback for a STALE
       catalog, which was MEASURED on 09-11: it served a 452-layer body (old index)
       while per-layer endpoints already served 453, so resolving off the catalog
       ALONE would have returned the WRONG layer. A catalog hit is never trusted on
       its own; it is always re-proved on /MapServer/<id>?f=json, the plane /query
       follows.

Proof required to MOVE: exact normalized name + type 'Feature Layer' + a field
fingerprint (the source's oidField present, and at least one nameField when it
declares any). That closes codex's stated risk — a replacement dataset reusing the
exact name — and the documented layer-73 trap (same name, Group Layer). Measured
9/9 live before enforcing, so it cannot false-alarm today.

Fails loud, never guesses: a rename, zero matches, MORE THAN ONE proved match (the
window is scanned whole, so ambiguity is detected rather than masked), drift beyond
the window with no catalog, or right-name-wrong-schema on the pinned id (which
never triggers a search — a replaced dataset needs a human). Deliberate asymmetry:
metadata with no field list keeps a WORKING pin but is never enough to MOVE.

LIVE PROOF, not just mocks: replaying the pre-fix ids against the real MapServer
auto-repoints 9/9 to the correct layer via the catalog; with the catalog forced
unavailable, the window scan recovers 419->420 and correctly fails loud on a drift
of 419. Had this shipped a day earlier, today's 04:30 run would have self-healed.

test/auto-repoint.test.mjs — 10 checks, 8 of them NEGATIVE controls (Group Layer,
wrong schema, ambiguity, rename, schema drift, kill switch, unpinned passthrough).
Check 9 earned its keep immediately: it caught that LA_NO_AUTO_REPOINT and
LA_REPOINT_WINDOW were module-load consts, so the kill switch silently did nothing.
Both now read at call time.

Also closes a coverage regression the contrarian red-team found in 314f1dc: the old
check 6 caught two sources sharing one layer id only as a side effect of asserting
exact ids, and dropping it traded a static catch for a runtime-only one. Asserted
directly now (check 7b), with a negative control.

4/4 suites pass. TK-10955. $0 local (free public ArcGIS GETs).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CocwcUw6gFt3tr4CZJmra6
314f1dc · 2026-09-11 · fix(la-data): repoint 9 NavigateLA layers after a SECOND upstream +1 insertion
LA inserted the Feature Layer "Special Event Permits" at position 52 of the
NavigateLA MapServer (layer count 452 -> 453), shifting every layer at id >= 52
by +1 — the same mechanism as 2026-09-05, six days later.

This time the expectName guard from 88e40a8 caught it: the 2026-09-11 04:30 run
failed all 8 pinned NavigateLA sources loud at preflight and ingested NOTHING,
so there was no corruption to clean up. la_gis_features was verified to still
hold exactly the correct upstream counts. The exit=1 was a TRUE POSITIVE.

Repointed +1 and verified 9/9 on BOTH planes that matter — exact upstream layer
NAME and historical ROW COUNT: zoning 72->73, council_districts 419->420,
neighborhood_councils 440->441, hpoz 76->77, community_plan_areas 415->416,
fault_zones 120->121, liquefaction 125->126, flood 255->256, fire_vhfhsz
359->360. Live: 9/9 preflight pass, 9/9 negative control (the now-stale ids are
all rejected). Re-ingest of the 8 affected sources: 8/8 exit 0 with exactly the
historical counts, content spot-checked semantically (real council members, real
community plan areas, real HPOZ districts) — not just matching counts.

test/layer-identity.test.mjs check 6 rewritten. It asserted specific numeric ids,
which is a restatement of config: it proves nothing about correctness and turns
every legitimate repoint into a test edit. It now asserts the durable invariants —
every NavigateLA-served source is ARMED (pins an expectName, or the guard is a
no-op for it) and no two sources expect the same upstream name. Both are proven
to go red under injected faults.

Documented the trap that nearly shaped the fix wrongly: /MapServer?f=json is
cacheable and was observed serving a STALE body (452 layers, old index) while the
per-layer endpoints already served the new one. Resolving a layer by name off the
catalog alone would have returned the WRONG id. The authoritative plane is
/MapServer/<id>?f=json, because that is what /query follows.

TK-10955. $0 local (free public ArcGIS GETs + local Postgres).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CocwcUw6gFt3tr4CZJmra6
f6042cd · 2026-09-10 · fix(la-data): scope allowFailure to the audited failure, not the source name
Contrarian review (TK-10955) found that the identity guard shipped in 88e40a8 was
silently neutered for the one source that carries an exemption. classifyRunResults()
tolerated ANY failure on a source whose allowFailure.scope matched, keyed purely on the
source NAME and never on the cause. So a future NavigateLA re-index that drifted zoning's
layer 72 would throw "ArcGIS layer identity drift" -> land in the tolerated bucket ->
exit 0, and the cron exit code (the one alarm that actually caught the 2026-09-05
incident) would never fire for the exact failure class the guard exists to make loud.

- allowFailure gains an optional `match` array; a failure is tolerated only if its error
  text contains one of the audited signatures. Omitting `match` keeps prior behaviour.
- gis_zoning pins match to its audited gateway degradation in BOTH error shapes:
  the HTTP-level form ("HTTP 502 after 5 tries") and the ArcGIS body-level form
  ("ArcGIS error: {"code":502,...}").
- test/run-policy.test.mjs: the audited 502 still exits 0; identity drift exits 1;
  asserted against the live SOURCES config, not just a mock.

This is closer to the ratified 2026-08-30 DTD intent ("exit 1 only for unexpected
failures") than the previous blanket-tolerate: a genuine 400 on gis_zoning was also
being swallowed and now correctly exits 1.

TK-10955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LHiEXMR2FCqRqUmoK4kQY
88e40a8 · 2026-09-10 · fix(la-data): repoint 9 NavigateLA layers after upstream +1 re-index; pin layers by NAME
On 2026-09-05 LA inserted a layer into the NavigateLA MapServer and every layer id
shifted +1. Sources pinned bare numeric ids, so all 9 GIS layers silently re-pointed
at a different dataset. Only 2 failed loudly (gis_fault_zones hit a Group Layer,
gis_flood hit a layer with no FLD_AR_ID) — those 400s are what has been failing the
nightly run's exit code since 09-05. The other 7 landed on queryable layers and
ingested a stranger's rows under our labels for 6 days with no error at all
(council_district 15->3, neighborhood_council 99->26, hpoz 35->7843,
community_plan_area 36->7, liquefaction 474->14181, fire_vhfhsz 14->247).

- repoint 71->72, 75->76, 119->120, 124->125, 254->255, 358->359, 414->415,
  418->419, 439->440; verified read-only that all 6 countable layers return their
  exact historical row counts and that 120/255/72 carry the fields our mappers need.
- add assertLayerIdentity(): every gisSrc now pins the expected upstream layer NAME
  and the adapter verifies it before paging, so the next re-index fails LOUD instead
  of silently ingesting the wrong layer. An ArcGIS layer id is a positional index,
  not an identifier — this is the second id-drift incident (cf. 559875d).
- test/layer-identity.test.mjs: zero-network/zero-DB proof, incl. that all 9 sources
  are pinned to the post-re-index ids.

Does not touch the 2026-08-30 allowFailure exit policy (ece3ea5): run-policy and
arcgis-retry suites still pass.

TK-10955

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LHiEXMR2FCqRqUmoK4kQY
a8295e4 · 2026-08-26 · arcgis: retry transient body-level errors (499/CONT_0001,429,5xx) so one flaky source can't poison the nightly batch exit code (TK-10851)
ArcGIS tunnels errors as HTTP 200 with an {error:{code}} body, so http.js's
status-based retry never saw them and cli.js:76-77 failed the whole 'all' run's
exit code on a single transient 499. Add payload-aware fetchArcgis retry with
backoff; bare 499 (Token Required) stays fail-fast, only 499+CONT_0001 retries.
+ zero-dep regression test (transient-retry / 400-fail-fast / auth-499-fail-fast).
e6f0356 · 2026-08-12 · Viewer: CD/ZIP quick-jump control on the Map view
- On-map 'Jump to' control (top-left, clear of the zoom control) with CD + ZIP
  selects. Picking one sets the filter and the existing fitBounds flies the map
  to that area; clearing removes it. Options carry live counts and are drawn from
  a stable unfiltered facet list (allFacets, fetched once at boot) so every CD/ZIP
  is always offered even when a filter is active. Composes with deep-links + saved
  views via the shared state.filters.

Author: Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48e81f3 · 2026-08-12 · Viewer: per-column filter inputs in the List header
- A sticky filter row under the column headers: each filterable column gets an
  input wired to the matching server filter param (address->q contains, zip/cd/
  work_type/building_type/status/era->exact with facet-fed <datalist> autocomplete,
  issued->since date, project_value->min_value). Reuses state.filters + qs(), so it
  composes with facets, deep-links, and saved views for free.
- Focus + caret preserved across the reload-triggered re-render; address input
  stays in sync with the top search box; inputs only appear for visible columns.

Author: Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bcf4edb · 2026-08-12 · Viewer: saved views — named snapshots of the full view state
- 📑 Views popover: name + save the current filters/sort/view/color/columns/density
  as a preset, apply any saved view in one click, delete with ✕. Persists to
  localStorage. Since syncURL() already serializes the whole view state to the
  querystring, a saved view is just a named snapshot of that query.
- Refactored boot into reusable hydrateFromQuery(strict) + syncControls(), shared
  by page load (persisted-pref fallback) and saved-view apply (exact snapshot).

Author: Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d7a05ae · 2026-08-12 · Viewer: column show/hide menu, / search shortcut, density+columns in deep-link
- ▦ Columns popover (List view): checkbox per column to show/hide, show-all +
  reset actions, always keeps >=1 column visible; persists to localStorage.
- Keyboard: '/' focuses the search box, Esc closes the modal/popover; placeholder
  hints the shortcut.
- Deep-link now also encodes density (d=) and the visible column set+order
  (cols=) so a shared URL restores the exact table layout. Verified round-trip.

Author: Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57d3d5a · 2026-08-12 · Viewer: loading spinner + empty-state + fetch-error overlay
- Loading spinner overlays the results area during every fresh fetch (valuable:
  the unfiltered lead_score sort over 1.58M rows takes a couple seconds).
- Empty state when a filter set returns 0 rows: clear message + a Clear-all-filters
  button (wired to reset) instead of a blank table.
- Network-error state shows the failure inline instead of a silent blank view.

Author: Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d5d5fec · 2026-08-12 · Viewer: CSV export of current filtered view + fix blank Issued date column
- ⬇ CSV button exports the current filtered+sorted permits as CSV (client-side,
  pages /api/permits at 200/req up to a 5,000-row cap, UTF-8 BOM, quoted escaping,
  filter-named file). Surfaces an explicit notice when the filtered total exceeds
  the cap (no silent truncation).
- Fix real bug: API sends the date as issue_date but List/Grid column key is
  'issued' -> the Issued date rendered blank everywhere. Normalize issued from
  issue_date on load (List, Grid, and CSV). Verified: column now shows the date.

Author: Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c9ccae5 · 2026-08-12 · Unified property drill-down: address -> property + trade structure + NAMED CREW + contacts
Folded getCrew() into property.js (permit-crew.js now exports it, CLI guarded). One
command: node scripts/property.js "<address>" -> parcel + permit/trade structure +
timeline + full named crew (GC/subs/architect/engineer w/ license, phone, website).
--no-crew skips the LADBS scrape. $0 (cached LADBS GET + local CSLB join).
b291216 · 2026-08-12 · permit-crew c-fix (Cody gate): era-scope the crew + dates + entity decode + no-match flag
Cody: merging 2018+ permits presented stale-era tradespeople as current crew. Now
anchors to the latest building permit, includes only ~18mo era (Woodlake correctly
drops 2 older permits), shows each permit date, flags GC-appears-as-sub cross-role,
distinguishes 'no CSLB match' from not-tried, decodes HTML entities. DTD final: SHIP.
1cd9469 · 2026-08-12 · Named build crew (scripts/permit-crew.js) — the linkage layer: subs on any build
Breakthrough (la-research-agent): LADBS PcisPermitDetail is a PUBLIC single GET with
id1/id2/id3 (permit# split on -), not permitNumber. Chain: property -> all permits on
parcel -> GET each detail -> Contractor/Architect/Engineer + CSLB license -> match to
enriched cslb_raw (website/phone). GC on building permit + SUBS on electrical/plumbing/
mech permits = full named crew. Proven: 7143 Tampa named 7-firm crew w/ phones. $0.
Cached in permit_crew. DTD-decided, la-research-discovered, Cody-gated next.
fa030eb · 2026-08-12 · Property drill-down (scripts/property.js): full permit/trade crew-structure per parcel
Assembles every permit on a parcel grouped by trade (GC building + electrical/plumbing/
HVAC sub-permits = the crew) + parcel data + timeline. Answers 'who worked on this build'
as trade structure; contractor names are the LADBS permit-detail scrape layer (next).
012231a · 2026-08-12 · enrich: domain-guessing pipeline (scripts/enrich-domain.js) — the real max-it unlock
DDG rate-limits by IP so parallel search self-blocks. Domain-guessing hits each
contractor's OWN candidate domains (different hosts = no shared rate limit) + local
model verifies the page belongs to them. Test: 5/10 real sites, $0, fully parallel.
14a832a · 2026-08-12 · enrich: don't lose DDG-rate-limited rows (mark ddg-blocked, 45s cooldown, throw on 403/429)
Max-it fan-out over-pressured DuckDuckGo (403s). Bug: errors stamped enriched_at =
rate-limited contractors marked 'no site' (data loss). Now: 403/429 throws ddg-blocked,
worker cools 45s, degraded rows reset for clean retry. Gentler concurrency.
c5f4234 · 2026-08-12 · Contractor web-enrichment — $0 local (free DDG search + local Ollama verifier)
NO paid API (Steve: Exa $2400 = hard no). Free DuckDuckGo HTML search finds candidate
URLs; local qwen2.5 verifies + extracts official website/email/linkedin (rejects
directories), matched to the CSLB name/city/phone. Writes website/email/linkedin/
enrich_* fields on cslb_raw. Resumable (enriched_at stamp), prioritized by hottest
recent-permit ZIPs. Test: 4/5 real sites found, $0.
804a15e · 2026-08-12 · yoloforever c8 FIX (Cody): status brief market = permit volume + median (not outlier-inflated sum)
Cody c8: the Market section re-imported the sum(valuation) outlier problem killed in
c2/c7 ($493M incl. the $375M unverified permit, unflagged). Now ranks hottest district
by permit VOLUME (outlier-immune) + shows MEDIAN value. Consistent with the digest.
e1a34c1 · 2026-08-12 · yoloforever c8: consolidated platform status brief (scripts/status-brief.js)
One dated internal brief tying the 8 cycles together: data footprint (16.6M rows),
canary verdict (reads its own heartbeat), top-5 deals, hottest market + contractor
supply. Reads each component's own output (truthful); missing artifacts show as gaps.
1d08e30 · 2026-08-12 · viewer: make filters+sort shareable in the URL (deep-link/bookmark any filtered view)
- syncURL() mirrors filters + sort + view + color into the URL on every render (history.replaceState, no history spam)
- boot IIFE now restores era/work_type/building_type/status/cd/zip/q/min_value/since + sort/dir from the URL (previously only view+color)
- verified: applying facets writes ?work_type=Bldg-New&cd=5&sort=...; cold-loading that URL restores active facets, filtered count (9,224), sort, and rows
d115714 · 2026-08-12 · yoloforever c7 FIX (Cody): honest digest — verified-only headline + district table
Cody c7 kill-shot: district table silently summed the flagged $375M into CD9's total
(#1) while the deals list flagged it — $700M headline was 2x inflated. Now: headline
splits verified ($278M) vs flagged/unverified ($422M); district table sorts by verified
value + marks outlier rows (CD11 Palisades now correctly #1; CD9's single-outlier row
demoted). Added stale-feed guard. Publishable.
02d9b82 · 2026-08-12 · yoloforever c7: RE-news deals-digest markdown generator (scripts/deals-digest.js)
Reads the latest deals-feed JSON (single source of truth) and renders publishable
markdown: top deals w/ council member + parcel value + map links + ⚠verify flags,
by-council-district table, source disclaimer. Presentation layer only.
4aad896 · 2026-08-12 · yoloforever c6 FIX (Cody): drop invalid per-ZIP ratio, add valid county supply/demand + fix trade map
Cody c6: permits_per_contractor by ZIP was a residential-ness artifact (wealthy ZIPs
have few contractor BUSINESS addresses, not undersupply) — a caveat can't rescue a
'90272 underserved' headline that's false. Dropped it. Section 2 now county-level
supply vs permit demand by trade (Gen Building 2.7, Electrical 8.4, Plumbing 9.4
permits/contractor/yr — no geo proxy). Trade-code map fixed (norm dash-stripped).
e534fb9 · 2026-08-12 · yoloforever c6: contractor-market intelligence (scripts/contractor-intel.js)
First use of the 243k CSLB dataset. Joins active LA-County contractors (by trade
classification) to permit activity: supply by trade (23,250 General Building etc.)
+ per-ZIP demand-vs-local-supply (Palisades 90272 = 33.5 permits/contractor, fire-
rebuild straining local supply). Honesty caveat labeled: contractor ZIP=business
address, supply proxy not work-location. INTERNAL (not public site).
00f8def · 2026-08-12 · yoloforever c5 FIX (Cody): gate teardown bonus on valuation>=$500k
Cody c5: a $250k permit on a $6.9M parcel jumping to 98 rewarded parcel wealth, not
a teardown (that's a garage, not a rebuild). Added valuation>=$500k to the teardown
CASE. Now: $250k/$6.9M example 98->90 (tier only); real $2.16M rebuild keeps +->100.
192 true teardowns flagged; 0 penalized. v2 defensible.
48faabe · 2026-08-12 · yoloforever c5: permit_lead_score_v2 — parcel-signal teardown/redevelopment ranking
NEW function (live permit_lead_score untouched). v2 = v1 + up to 15pts parcel signal
(cap 100, no rescale): teardown (new build on valuable old parcel) + assessed tier.
Validated: 0 permits penalized (no-parcel = v1), 20,045 lifted, max +15. Surfaces
high-end redevelopment v1 misses (e.g. $250k permit on $6.9M 1922 parcel: 83->98).
60f8851 · 2026-08-12 · yoloforever c4 FIX (Cody): deals feed ranks by lead_score, dedups address, flags outliers
Cody c4 (verified): raw-max-valuation surfaced LAX 3x + a $375M 'addition'. Now:
(1) DISTINCT ON (address,zip) dedups same-project permits; (2) ORDER BY lead_score
then valuation = newsworthiness not brute dollars (ground-up dev now leads, airport
gone); (3) valuation_outlier flag (>$100M or >150x parcel assessed) tags data
artifacts for editorial verify. Now publishable as RE-news leads.
909b171 · 2026-08-12 · yoloforever c4: newsworthy permit deals feed (scripts/deals-feed.js)
Surfaces top high-value permits (last 90d, >=$1M, issued) as human-readable
headlines + structured JSON feed for RENTV/RE-news synergy. Enriched w/ parcel
value + council-member name. Fixed date formatting + 'built 0'. New local surface.
0377c1a · 2026-08-11 · yoloforever c3 FIX (Cody): green-able canary + real CSLB load + time-anchored bleed
Cody c3 (all verified): (1) permanently-FAIL canary = anti-canary -> demote KNOWN_BROKEN
sources (gis_zoning, documented) to WARNING so verdict can be OK/WARN; a NEW error still
FAILs. (2) bleed floor was run-count anchored (walks up, misses slow decline) -> time-anchored
to ~30d. (3) CSLB state was a planted timestamp that lies -> scripts/cslb-load.sh writes
real state on actual load. Verdict now WARN (was perpetual FAIL); green = signal again.
f794850 · 2026-08-11 · yoloforever c3: close Cody-c1 canary gaps — CSLB tracking + rolling-window bleed detection
(1) CSLB now recorded in la_ingest_state (cslb_master) so the source-staleness check
covers it (was the untracked blind spot). (2) Rolling history (canary-history.jsonl)
+ BLEED status: flags a slow net decline vs earliest-observed count that the ratchet
floor misses. Verified: no false-bleed on stable data; verdict still correctly FAILs
on gis_zoning's real 502.
b3681eb · 2026-08-11 · yoloforever c2 FIX (Cody): median_value + explicit scope label + CD-join diagnostic
Cody: total_value is right-skewed vanity without a central figure. Added median
(reveals CD11 $8.05B total vs $9k median = a few mega-projects). Scope label now
states building-only/current-dataset (excludes 1.17M historical+trade). Join
diagnostic prints parsed/matched (15/15) so a blank member column can't ship silently.
f57ce2c · 2026-08-11 · yoloforever c2: per-CD + per-ZIP permit-activity rollups (scripts/rollups.js)
Aggregates permits x lead_score joined to gis council-district NAMES; permits,
issued, new-builds, last-90d, total/avg value, avg lead score per CD + top-60 ZIPs.
CSV exports. New local analytics surface (not the live viewer). $0 local.
3687729 · 2026-08-11 · yoloforever c1 FIX (Cody gate): canary reads la_ingest_state for per-source staleness+errors
Count-only was false comfort (idempotent upserts keep counts flat while a feed
goes stale/errors). Now checks all 21 sources' last_run age + last_status — verdict
FAIL correctly surfaces gis_zoning stuck in HTTP 502. Covers the 5 count-only tables.
9a4905f · 2026-08-11 · yoloforever c1: LA-data freshness+rowcount canary (baseline-aware, read-only)
Flags row-count regressions vs a ratcheting baseline + stale feeds (max-age per
table). Report-only; CNCP/George alerting is a gated follow-up. Also: parcel tails
now 100% complete (12,099,614 = full 12.1M, all 5 roll years, OID-cursor fix).
cf20498 · 2026-08-11 · Compliance: pull board.html (monetization deck) off public surface + add robots.txt
vp-compliance flagged the public no-auth board.html resale/lead-gen deck as HIGH
(self-documented commercial + owner-targeting intent on a free-viewer surface).
Moved to viewer/private/ (preserved, not served). robots.txt disallows /api/ + deck.
69f49ff · 2026-08-11 · Viewer: swap CARTO basemap -> OSM tiles (ToS/copyright) + OSM attribution
CARTO CDN basemap is not licensed for public non-customer use; OSM standard
tiles are attribution-only. Disclaimer/sources footer already present.
1efa08a · 2026-08-11 · viewer: mirror compliance footer onto table.html + board.html
All three viewer entry points now carry source attribution, as-is warranty
disclaimer, and no-endorsement notice. board.html uses a self-contained
inline-styled footer (different flex layout, no --muted var).
6f64d55 · 2026-08-11 · viewer: add source-attribution + as-is/no-endorsement compliance footer
Data reuse verified: LA County eGIS Terms grant copy/publish/distribute +
commercial use; LA City permits are untagged-public open data. Footer keeps
any external viewer compliant (attribution, as-is warranty disclaimer, no
County/City endorsement).
ff21d5c · 2026-08-11 · Viewer: List/Grid/Map views + color-ID + shareable ?view=&color= deep links
Map view (Leaflet) plots the current filtered set color-coded by lead score/
status/value/work-type; grid = color-bordered cards; list = the sortable table.
One colorFor() model drives all three. ?view=map&color=project_value deep-links.
4862c20 · 2026-08-11 · GIS land use via hosted LADCP FeatureServer (56,901 features, reliable)
Replaces the flaky NavigateLA gateway for land use. Zoning stays partial (75%,
gateway-capped); complete LA City zoning is fragmented across ~15 per-community
Zone_Builder_Database services (documented follow-up).
0ccaea8 · 2026-08-11 · GIS: parcel polygons via hosted FeatureServer (421,684 w/ geometry); no-order offset for gateway layers
NavigateLA MapServer gateway 502s/500s on bulk geometry past ~offset 44k;
parcels repointed to hosted Assessor_Parcels_Land_2025 FeatureServer (reliable).
zoning partial (43,957), landuse blocked — need hosted-FeatureServer equivalents.
cc12805 · 2026-08-11 · GIS layers (NavigateLA ArcGIS) + CSLB contractor loader
ArcGIS adapter: OBJECTID-cursor pagination + geometry support. GIS schema
(la_parcel_geom, la_gis_features). Loaded zoning/land-use/council districts/
neighborhood councils/HPOZ/community plan areas + fire/flood/fault/liquefaction
hazard layers. CSLB free-portal ASP.NET session downloader -> 243,555 CA
contractors in cslb_raw. Assessor switched to OID-cursor (fixes deep-paging 400s).
d9ab84f · 2026-08-11 · Promote merged card+lead-score viewer to index.html; preserve table as table.html
- index.html: card viewer (facet rail, search/sort/density, detail modal) + permit_lead_score Rank badge on every card, 'Rank (high->low)' sort, 'Lead score N/100' in modal; header nav Cards/Table/Board
- table.html: previous sortable/movable-column table viewer, preserved + reachable from nav
- cards.html: removed (promoted into index.html)
Steve-directed (TK-10450). Verified in Chrome: / = cards w/ ranks, /table.html = table, /board.html = DTD board
f47742c · 2026-08-11 · Viewer: sortable+movable-column table + permit_lead_score ranking (default sort)
Every column server-sortable (whitelisted sort=<col>&dir); drag-to-reorder columns
persisted to localStorage; lead_score (0-100) ranking as default sort. Row-density
slider, faceted filter rail with live counts, detail modal w/ parcel enrichment +
Google Maps + raw JSON. On port 9814.
bbdafa5 · 2026-08-11 · LA Building Permits viewer frontend: faceted rail, search/sort/density, detail modal
- public/index.html: professional teal/neutral data-tool UI over /api/permits + /api/facets
- Left collapsible facet rail (Era/Work Type/Building Type/Status/CD/ZIP) with live counts, click-to-filter, active teal badge; active-facet options stay switchable (refetch minus own filter)
- Top bar: debounced address search, live count, sort (newest/value/oldest/address), density slider -> --cols; sort+density persist in localStorage
- Cards: address, work/building badges, formatted value, issue date + status pill, CD/era, assessed value/year built/sqft when present
- Detail modal (draggable+resizable, persisted): full permit + parcel enrichment, Google Maps link, clickable filter values, raw-JSON toggle
- Load-more pagination (no infinite scroll); stale-response seq guards on facets+permits
- server.js: existing viewer server (was untracked)

File tree

60 files tracked. Click any to browse the source at HEAD.

Other build journals

← La Permits School  ·  all 4 projects  ·  Labj Cre Banks →

Export

commits.csv · feed.atom · project.json · commits.json

rendered in 2ms