# Project export: Finding the Lost Wells of Appalachia

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: We found ~39,000 previously undiscovered orphaned gas/oil wells in Appalachia, and created actionable pathways to plug almost of them, which will result in a 30 million metric tones of CO2 reduction!
- Devpost: https://devpost.com/software/finding-the-lost-wells-of-appalachia
- GitHub: https://github.com/ShrishPremkrishna/lostwells
- Demo: https://www.figma.com/deck/OzciAAeDOOIZdwe8H6IpP8/Lost-Wells-Of-Appalachia?node-id=33-5
- Video: https://www.youtube.com/embed/iXO-iaMxEz0?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 3 GitHub contributor(s) — Claude (22 commits), Shrish (15 commits), Thaarak (15 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# Lost Wells

**Finding America's undocumented orphaned oil & gas wells — and ranking them by
who's living on top of them.**

There are 310,000–800,000 *undocumented* orphaned wells in the U.S.; only 117,672
are documented. For the undocumented ones, human exposure is literally uncounted —
wells under a school gym, six feet from a family's drinking water. Lost Wells
finds candidate undocumented wells, sends a Claude agent swarm to investigate each,
and ranks them by human impact under the finite **$4.7B** federal plugging budget.

## Repo layout

```
apps/web/            Next.js + TS + Tailwind + MapLibre GL + deck.gl + Framer Motion
services/ingest/     Pull USGS DOW + LBNL + CDC SVI + NCES once -> committed datastore
services/engine/     Methane proxy, Raimi plug cost, carbon kicker, composite score (+tests)
services/swarm/      LangGraph Send map-reduce; Claude (Sonnet 4.6) + web search investigators
UNET/                LBNL U-Net inference pipeline (real, runnable; GPU host). services/unet/ is a superseded sketch
data/processed/      Committed datastore the app serves (ingest-once, serve-from-cache)
docs/                OVERVIEW.md, ARCHITECTURE.md, DEMO_SCRIPT.md, archive/
CLAUDE.md            Agent-context: setup, conventions, gotchas
```

New here? Start with **[`docs/OVERVIEW.md`](docs/OVERVIEW.md)** (5-minute
product overview) and **[`CLAUDE.md`](CLAUDE.md)** (setup + conventions).

## Quick start

```bash
# 1) Python datastore (already committed; re-materialize from source if needed)
python -m venv .venv && . .venv/bin/activate
pip install -r services/ingest/requirements.txt -r services/engine/requirements.txt
python services/ingest/download.py          # fetch raw sources
python services/ingest/state_registries.py --states OH,WV,PA,NY,KY   # §1.3 depth/type/status/operator
python services/ingest/build_datastore.py --states OH,WV,PA,NY,KY    # -> wells.documented.json, candidates.base.json (LBNL CA/OK 1,303), lost_wells.json
python services/ingest/build_unet_candidates.py                     # + U-Net Appalachia (36,919 PA/KY/WV/OH) -> merge into candidates.base.json (~38,222)
python services/ingest/enrich.py            # CDC SVI + NCES joins (cached)
# §2A tract-dedup enrichment (drinking water, hospitals, true 1-mi population, EJ).
# Needs CENSUS_API_KEY for ACS block-group population; --with-downloads fetches
# the light-budget layers (PWS service areas ~570MB, hospitals, CEJST, EJI).
export CENSUS_API_KEY=...                    # free: https://api.census.gov/data/key_signup.html
python services/ingest/enrich_tract.py --input lost_wells.json --states OH,WV,PA,NY,KY --with-downloads
python services/ingest/heroes.py
python services/ingest/enrich.py --input heroes.base.json --output heroes.enrichment.json
python services/engine/score_candidates.py  # -> candidates.scored.json, heroes.json + slim web payload

# NOTE: candidates.scored.json (~114 MB) is gitignored and must be regenerated
# by the score step above. The web app reads the committed slim payload
# (candidates.web.json + detail/NN.json shards) that score_candidates.py emits.

# 2) Agent swarm (needs ANTHROPIC_API_KEY) -> data/processed/dossiers.json
pip install -r services/swarm/requirements.txt
python services/swarm/run_swarm.py --total 12

# 3) Web app
cd apps/web && npm install && npm run dev   # http://localhost:3000
```


## Redis & Browserbase: caching + browser automation

Two infra tools back the agent investigation layer. Both are **fail-open** — on
any outage they degrade to a live/uncached path rather than breaking the demo.

**Already wired (baseline):**
- **Redis** — exact-key dossier cache (`dossier:{well_id}`, 30-day TTL) shared by
  the batch swarm (`services/swarm/web/cache.py`) and the live SSE route
  (`apps/web/lib/redis-cache.ts`). Also mirrors the knowledge base (`knowledge:all`).
- **Browserbase** — a `browse_page` escalation tool the batch investigator calls
  when `web_search` hits a JS/WAF wall (`services/swarm/web/browserbase_client.py`),
  SQLite-cached per URL, returning a `replay_url` for provenance.

Today Redis means "don't re-investigate the same well" and Browserbase means "let
the agent open one stubborn page." The roadmap below extends both. Each entry
notes **where** it lands and **how** it's built.

### Redis — planned enhancements

| Enhancement | How it's implemented |
|---|---|
| **Operator/entity cache** (biggest cost lever) | New namespace `operator:{normalized_name}` storing operator history / parent company / bankruptcy status, reused across every well tied to that operator. `investigator.py` checks it before the per-well agent loop; kept strictly separate from `dossier:{well_id}` so a well-specific claim is never cross-served. |
| **Semantic cache** | Wrap the Claude call with a RedisVL `SemanticCache`; scope each entry with `filterable_fields` on `well_id`/`county` + a conservative `distance_threshold` so look-alike wells reuse *context* without bleeding facts. Lands in `services/swarm/web/cache.py` (+ live-route twin). |
| **Distributed rate limiter** | Redis token-bucket (`INCR` + key expiry) in front of flaky ArcGIS hosts (WV TAGIS, PA PASDA, OH ArcGIS) and the Anthropic API, shared across parallel ingest workers and serverless invocations. A small `services/ingest/ratelimit.py` helper wraps `requests`. |
| **Single-flight lock (live route)** | `SETNX` lock + Pub/Sub in `app/api/investigate/[id]/route.ts`: concurrent clicks on the same well run once; the second subscriber streams the same result. |
| **Promote ingest caches SQLite → Redis** | Move the hot tract/point/registry lookups (`enrich.sqlite`, `enrich_tract.sqlite`, `registries.sqlite`) behind a Redis-backed cache interface so CI, serverless, and dev share one TTL-managed store. |
| **Knowledge base as a vector index** | Index `knowledge.json` entries with RedisVL so agents semantically retrieve applicable funding/programs/contractors per well instead of loading the whole file — the "living document referenced by future swarms." Lands in `services/swarm/knowledge.py`. |
| **Tiered TTLs** | Per-key freshness: news ~7d, operator facts ~90d, parcel ownership ~180d, demographic enrichment ~1yr, dossiers 30d. |
| **SSE resumability + fan-out** | Store live progress in a Redis Stream so dropped connections resume and multiple viewers of one well share a log (Vercel functions are stateless). |

### Browserbase — planned enhancements

| Enhancement | How it's implemented |
|---|---|
| **`browse_page` in the live route** | Wire the existing `browse_page` tool (today batch-only) into `app/api/investigate/[id]/route.ts` so live investigations also cross form/login/WAF walls. |
| **EPA ECHO super-emitter scrape** | Browserbase + Stagehand drives the ECHO interactive map and intercepts its backing network calls to extract events — fills the empty `data/processed/super_emitter.json` that `score_candidates.py` already reads and that backs the DossierPanel "EPA super-emitter nearby" badge. New `services/ingest/super_emitter_browser.py`. |
| **Parcel lookup for OH/KY/PA** | Stagehand fills county assessor search forms (by address/parcel) and extracts the owner for states with no ArcGIS REST endpoint, completing the `surface_owner` actor in the CaseFile. Extends `services/swarm/web/parcel.py` (WV is the only state wired today). |
| **Authoritative bankruptcy / corporate status** | Browserbase automates operator-name → PACER bankruptcy case and → state Secretary-of-State business-status lookups (form/WAF/login-walled), replacing news-inferred `bankruptcy_findings` with sourced provenance. |
| **Funding & carbon-credit portal checks** | Navigate state plugging-program portals and carbon registries (ACR/CAR) to confirm eligibility/deadlines (feeding the knowledge base) and **draft-fill** an application package — never submitting (regulatory line). |
| **Screenshot + replay evidence** | Capture a screenshot at fetch time alongside the existing `replay_url`, store as immutable evidence, and su

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 86 recognized source files, 657 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TensorFlow (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: config files committed to the repository; commit authorship or trailers

## Codebase structure (from repository index)

### Files (120 of 160)

```
.env.example
.github/workflows/ci.yml
.gitignore
apps/web/app/about/page.tsx
apps/web/app/api/investigate/[id]/route.ts
apps/web/app/contact/page.tsx
apps/web/app/globals.css
apps/web/app/layout.tsx
apps/web/app/page.tsx
apps/web/app/team/page.tsx
apps/web/components/CaseFilePanel.tsx
apps/web/components/DossierPanel.tsx
apps/web/components/IntroOverlay.tsx
apps/web/components/KnowledgeList.tsx
apps/web/components/Legend.tsx
apps/web/components/MapView.tsx
apps/web/components/RankedList.tsx
apps/web/components/ScoreBar.tsx
apps/web/components/SwarmPanel.tsx
apps/web/components/TopBar.tsx
apps/web/components/TopoDissolve.tsx
apps/web/lib/colors.ts
apps/web/lib/data.ts
apps/web/lib/format.ts
apps/web/lib/redis-cache.ts
apps/web/lib/types.ts
apps/web/next-env.d.ts
apps/web/next.config.mjs
apps/web/package.json
apps/web/postcss.config.mjs
apps/web/scripts/copy-data.mjs
apps/web/tailwind.config.ts
apps/web/tsconfig.json
CLAUDE.md
data/processed/candidates.base.json
data/processed/candidates.web.json
data/processed/case_files.json
data/processed/detail/00.json
data/processed/detail/01.json
data/processed/detail/02.json
data/processed/detail/03.json
data/processed/detail/04.json
data/processed/detail/05.json
data/processed/detail/06.json
data/processed/detail/07.json
data/processed/detail/08.json
data/processed/detail/09.json
data/processed/detail/10.json
data/processed/detail/11.json
data/processed/detail/12.json
data/processed/detail/13.json
data/processed/detail/14.json
data/processed/detail/15.json
data/processed/detail/16.json
data/processed/detail/17.json
data/processed/detail/18.json
data/processed/detail/19.json
data/processed/detail/20.json
data/processed/detail/21.json
data/processed/detail/22.json
data/processed/detail/23.json
data/processed/detail/24.json
data/processed/detail/25.json
data/processed/detail/26.json
data/processed/detail/27.json
data/processed/detail/28.json
data/processed/detail/29.json
data/processed/detail/30.json
data/processed/detail/31.json
data/processed/detail/32.json
data/processed/detail/33.json
data/processed/detail/34.json
data/processed/detail/35.json
data/processed/detail/36.json
data/processed/detail/37.json
data/processed/detail/38.json
data/processed/dossiers.json
data/processed/enrichment.json
data/processed/heroes.base.json
data/processed/heroes.enrichment.json
data/processed/heroes.json
data/processed/heroes.super_emitter.json
data/processed/knowledge.json
data/processed/meta.json
data/processed/super_emitter.json
data/processed/wells.documented.json
data/static/ej_orgs.csv
data/static/intake_adapters.csv
data/static/state_regulators.csv
docs/ARCHITECTURE.md
docs/archive/HANDOFF.md
docs/AUDIT.md
docs/BROWSER_BASED_AGENT_TRAINING.md
docs/DEMO_SCRIPT.md
docs/IMPLEMENTATION_PLAN.md
docs/OVERVIEW.md
docs/STRUCTURED_PROPERTY_INFORMATION.md
docs/uidesign.md
PROGRESS.md
README.md
scripts/redis_check.py
services/dotenv_min.py
services/engine/__init__.py
services/engine/actors.py
services/engine/assemble_cases.py
services/engine/carbon.py
services/engine/methane.py
services/engine/pathways.py
services/engine/plugcost.py
services/engine/requirements.txt
services/engine/score_candidates.py
services/engine/scoring.py
services/engine/story.py
services/engine/tests/test_engine.py
services/ingest/build_datastore.py
services/ingest/build_heroes.py
services/ingest/build_unet_candidates.py
services/ingest/download.py
services/ingest/enrich_tract.py
services/ingest/enrich.py
[40 more files omitted for size]
```

### Dependencies

- apps/web/package.json: @anthropic-ai/sdk@^0.105.0, @deck.gl/core@^9.0.27, @deck.gl/layers@^9.0.27, @deck.gl/mapbox@^9.0.27, @deck.gl/react@^9.0.27, @tanstack/react-virtual@^3.5.1, @types/node@^20.14.2, @types/react@^18.3.3, @types/react-dom@^18.3.0, autoprefixer@^10.4.19, framer-motion@^11.2.10, maplibre-gl@^4.5.0, next@14.2.5, postcss@^8.4.38, react@^18.3.1, react-dom@^18.3.1, redis@^6.0.0, tailwindcss@^3.4.4, typescript@^5.4.5
- services/engine/requirements.txt: numpy@>=1.24, pandas@>=2.0, pytest@>=7.4
- services/ingest/requirements.txt: geopandas@>=0.14, numpy@>=1.24, pandas@>=2.0, pyogrio@>=0.7, pyproj@>=3.5, requests@>=2.31, shapely@>=2.0, tqdm@>=4.65
- services/swarm/requirements.txt: anthropic@>=0.40, browserbase@>=1.0, langgraph@>=0.2.20, playwright@>=1.40, pydantic@>=2.5, redis@>=5.0
- services/unet/requirements.txt: geopandas@>=0.14, numpy@>=1.24, pyproj@>=3.5, rasterio@>=1.3, scipy@>=1.10, shapely@>=2.0, tensorflow@>=2.12
- UNET/requirements.txt: geopandas@==0.14.*, numpy@<2, opencv-python@==4.10.0.84, pandas@<2.2, pyproj@>=3.4, requests, segmentation-models@==1.0.1, shapely@>=2.0,<3, simplekml@==1.3.*, tensorflow@==2.15.*, tqdm

### Recent commits (newest first)

- browserbase and redis stuff
- Phase 3 + UI rebuild: Discover→Diagnose→Act, three sponsors, hero cases
- Merge pull request #2 from ShrishPremkrishna/claude/loving-davinci-n7k148
- fix: dedup candidates, correct U-Net attribution, refresh demo + audit
- docs: add full codebase audit + minimal CI
- Merge phase-2/3: enrichment + rescore + repo cleanup/docs
- chore: repo cleanup + docs pass (OVERVIEW, CLAUDE.md, archive)
- chore: ignore .gstack/ + regenerable candidates.scored.json (>100MB)
- Phase 5: max out SVI + schools coverage (tract/batch join) + rebalance weights
- Phase 4 fix: pad shard filenames + strip NaN from detail JSON
- Phase 4: slim web payload (98MB->9.6MB) + lazy per-well detail shards
- Phase 2: bulk tract-join enrichment for merged 38,222 candidates
- codex
- data
- 2.5, 3.2, 3.6 done
- 2B done
- up to 2b done
- 1.3 and 2A done
- finished doc
- Update HANDOFF.md

## Key source files (fetched from GitHub, selected and truncated for size)

### CLAUDE.md

```markdown
# CLAUDE.md

Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.

**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.

## 1. Think Before Coding

**Don't assume. Don't hide confusion. Surface tradeoffs.**

Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.

## 2. Simplicity First

**Minimum code that solves the problem. Nothing speculative.**

- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.

Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.

## 3. Surgical Changes

**Touch only what you must. Clean up only your own mess.**

When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.

When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.

The test: Every changed line should trace directly to the user's request.

## 4. Goal-Driven Execution

**Define success criteria. Loop until verified.**

Transform tasks into verifiable goals:
- "Add validation" → "Write tests for invalid inputs, then make them pass"
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
- "Refactor X" → "Ensure tests pass before and after"

For multi-step tasks, state a brief plan:
```
1. [Step] → verify: [check]
2. [Step] → verify: [check]
3. [Step] → verify: [check]
```

Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.

---

**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.

# Project Specific Context

> For a 5-minute product overview (the numbers, the 4-stage pipeline, a
> data-flow diagram, and what's real vs. placeholder) see
> [`docs/OVERVIEW.md`](docs/OVERVIEW.md).

## What this is

Lost Wells finds candidate *undocumented* orphaned oil & gas wells, sends a Claude
agent swarm to investigate each, and ranks them by human impact. It is a monorepo:
a Python ingestion/scoring/swarm pipeline that materializes a committed JSON
datastore, and a Next.js web app that serves that datastore statically (no backend
at runtime). The candidate universe is **~38,222 wells** (CA, OK, PA, WV, OH, KY)
scored against a **117,672-w
[truncated — 7216 more characters]
```

### PROGRESS.md

```markdown
# Lost Wells — Progress, Limitations & Next Steps

> An honest self-audit of the build session. For a societal-impact tool, candor
> about data limits is a feature, not a footnote — this file is meant to be read
> alongside the demo, so no number here is mistaken for more than it is.

**Status:** full app built end-to-end; candidate universe expanded to **~38,222**
(LBNL CA/OK 1,303 + U-Net Appalachia 36,919) and the §2A tract-dedup
human-exposure/EJ enrichment ran live, regenerating
`data/processed/{enrichment,candidates.scored,heroes}.json` with real data
(drinking_water / hospitals / population_1mi at full coverage; eji_rank 953/1,303;
25 engine tests green). The live agent swarm ran on real data with the provided
API key.

**Not yet run live:** in-browser visual QA; real georeferenced hero topo; any
U-Net execution (no GPU); the §1.3 state-registry consolidation (OH/WV/PA/NY/KY)
remains code-complete + unit-tested but unexecuted — it annotates the *documented*
universe, which does not overlap the CA/OK candidate scores, so §2.3's
methane/plug-cost flatness is not resolved by the current run.

> For the run-order commands and the slim-web-payload regeneration note, see
> `README.md`, `CLAUDE.md`, and `docs/OVERVIEW.md`. The detailed deviation
> log is preserved in `docs/archive/HANDOFF.md`. The sections below are the
> honest per-area self-audit.

---

## 1. What was delivered (against the directives)

| Directive / locked decision | Status | Notes |
|---|---|---|
| Verify whitelist reaches container egress (reachability probe) | ✅ | All gov/data hosts returned 2xx/3xx. |
| Ingest real datastore (USGS DOW 117,672 + LBNL candidates) | ✅ | DOW 117,672; LBNL **1,303** rows (lit. says 1,301 — see §3.7). |
| Cached dossier API data | ✅ | CDC SVI + NCES cached; swarm dossiers cached. |
| Ranking engine → UI → swarm → topo-dissolve → polish → U-Net (the §6 order) | ✅ | All stages present. |
| MapLibre, no token | ✅ | Carto dark + ESRI tiles, browser-side. |
| LangGraph `Send` map-reduce swarm | ✅ (with caveat) | Architecture intact; **LLM client swapped** — see §2.1. |
| Full app + U-Net as documented code | ✅ | U-Net documented + runnable, **not executed** (§2.5). |
| Ingest-once-then-serve-from-cache | ✅ | App reads committed `data/processed/*.json`; no backend. |
| Wire swarm to `ANTHROPIC_API_KEY` from env, cached fallback | ✅ | Ran live on 12 wells; 12/12 complete, avg 8 cited sources. |
| §1.3 state-registry consolidation (depth/type/status/operator + expand) | ✅ code | `state_registries.py` (OH/WV/PA/NY/KY, config-driven) + `consolidate()` (attach by API → spatial ≤50 m → expand). **Awaiting live ingest run.** |
| §2A tract-dedup enrichment (drinking water, hospitals, 1-mi pop, EJ) | ✅ **ran live** | `tracts.py` PIP + `enrich_tract.py` (PWS/USGS-hospitals/ACS-BG/CEJST/EJI). **Executed on all 1,303 CA/OK candidates** → drinking_water 1,303/1,303, hospitals 1,303/1,303, population_1mi 1,303/1,303, eji_rank 953/1,303; data regenerated. |

**Genui
[truncated — 11427 more characters]
```

### UNET/requirements.txt

```
# UNET inference/fine-tune deps — pip portion.
#
# RECOMMENDED RUNTIME: TensorFlow 2.15 + segmentation-models 1.0.1 + numpy<2.
# Why 2.15: it is the LAST TF where `tensorflow.keras` IS Keras 2 by default, so
# the 2022-era `unet_model.h5` (saved with keras 2.8 + segmentation-models 1.0.1)
# loads with NO Keras-3 shims. TF 2.16+ ships Keras 3 and `import
# segmentation_models` then fails with
#   AttributeError: module 'keras.utils' has no attribute 'generic_utils'
# (verified: qubvel/segmentation_models issues #588/#570/#559).
# numpy MUST be <2 — TF<2.18 and segmentation-models 1.0.1 predate the NumPy-2 ABI.
#
# GDAL IS NOT PIP-INSTALLED HERE (pip GDAL must match a system libgdal). Install it
# with conda (`conda install -c conda-forge gdal`) — see environment.yml — or on
# Colab/Debian: `apt-get install -y gdal-bin libgdal-dev && pip install GDAL==$(gdal-config --version)`.

tensorflow==2.15.*
segmentation-models==1.0.1
numpy<2
opencv-python==4.10.0.84
pandas<2.2
geopandas==0.14.*
shapely>=2.0,<3
pyproj>=3.4
simplekml==1.3.*
tqdm
requests

# --- ALTERNATIVE A (modern TF): comment out the tensorflow pin above and use ---
# tensorflow==2.17.*
# tf-keras~=2.17
#   then set, BEFORE importing tensorflow:  os.environ["TF_USE_LEGACY_KERAS"]="1"
#
# --- ALTERNATIVE B (exact 2022 repro): run inside the NVIDIA NGC container ------
#   docker run --gpus all -it nvcr.io/nvidia/tensorflow:22.05-tf2-py3   (TF 2.8.0)
#   then: pip install segmentation-models==1.0.1 "numpy<2" opencv-python simplekml geopandas
#   (GDAL: conda or apt inside the container)

```

### services/engine/requirements.txt

```
# Ranking engine — pure-compute impact scoring (network-independent).
pandas>=2.0
numpy>=1.24
pytest>=7.4

```

### services/ingest/requirements.txt

```
# Ingestion pipeline — pull all sources once, materialize the datastore.
requests>=2.31
pandas>=2.0
numpy>=1.24
shapely>=2.0
pyproj>=3.5
pyogrio>=0.7
geopandas>=0.14
tqdm>=4.65

```

### services/unet/requirements.txt

```
# U-Net inference pipeline. Run where a GPU + the LBNL model/data exist
# (NOT in the Lost Wells sandbox — no GPU here; this stays documented code).
tensorflow>=2.12
rasterio>=1.3
geopandas>=0.14
shapely>=2.0
numpy>=1.24
scipy>=1.10
pyproj>=3.5

```

### services/swarm/requirements.txt

```
# LangGraph Send map-reduce swarm. Worker nodes drive Claude via the official
# anthropic SDK + server-side web search (langchain_anthropic's ChatAnthropic
# hangs in some sandboxed networks; the raw SDK is the reliable path).
langgraph>=0.2.20
anthropic>=0.40
pydantic>=2.5
redis>=5.0
browserbase>=1.0
playwright>=1.40

```

### apps/web/package.json

```
{
  "name": "lost-wells-web",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "predev": "node scripts/copy-data.mjs",
    "dev": "next dev",
    "prebuild": "node scripts/copy-data.mjs",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@anthropic-ai/sdk": "^0.105.0",
    "@deck.gl/core": "^9.0.27",
    "@deck.gl/layers": "^9.0.27",
    "@deck.gl/mapbox": "^9.0.27",
    "@deck.gl/react": "^9.0.27",
    "@tanstack/react-virtual": "^3.5.1",
    "framer-motion": "^11.2.10",
    "maplibre-gl": "^4.5.0",
    "next": "14.2.5",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "redis": "^6.0.0"
  },
  "devDependencies": {
    "@types/node": "^20.14.2",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "autoprefixer": "^10.4.19",
    "postcss": "^8.4.38",
    "tailwindcss": "^3.4.4",
    "typescript": "^5.4.5"
  }
}

```

### apps/web/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Playfair_Display, Inter, JetBrains_Mono } from "next/font/google";
import "./globals.css";

// uidesign.md §3: Playfair Display (editorial headers), Inter (functional body),
// JetBrains Mono (coordinates / IDs / agent feed — "makes data feel like data").
const display = Playfair_Display({
  subsets: ["latin"],
  weight: ["400", "500", "600", "700"],
  variable: "--font-display",
  display: "swap",
});

const sans = Inter({
  subsets: ["latin"],
  variable: "--font-sans",
  display: "swap",
});

const mono = JetBrains_Mono({
  subsets: ["latin"],
  variable: "--font-mono",
  display: "swap",
});

export const metadata: Metadata = {
  title: "Finding the Lost Wells of Appalachia",
  description:
    "We ran a U-Net over historical USGS topographic maps and found 36,919 undocumented orphaned oil & gas wells across Appalachia — then ranked them by who lives on top of them and mapped a path to plug them.",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html
      lang="en"
      className={`${display.variable} ${sans.variable} ${mono.variable}`}
    >
      <body>{children}</body>
    </html>
  );
}

```

### apps/web/app/page.tsx

```typescript
"use client";

import { useEffect, useMemo, useRef, useState } from "react";
import dynamic from "next/dynamic";
import { AnimatePresence, motion } from "framer-motion";
import type { FocusTarget } from "@/components/MapView";
import { TopBar } from "@/components/TopBar";
import { RankedList } from "@/components/RankedList";
import { DossierPanel } from "@/components/DossierPanel";
import { Legend } from "@/components/Legend";
import { IntroOverlay } from "@/components/IntroOverlay";
import { TopoDissolve } from "@/components/TopoDissolve";
import type { Candidate, CandidateLite, CaseFile, DocumentedWells, Dossier, Meta } from "@/lib/types";
import {
  loadCandidates,
  loadCaseFiles,
  loadDetailShard,
  loadDocumented,
  loadDossiers,
  loadHeroes,
  loadMeta,
  shardOf,
} from "@/lib/data";
import { fmtInt } from "@/lib/format";
import { scoreCSS } from "@/lib/colors";

const MapView = dynamic(() => import("@/components/MapView"), { ssr: false });

type SortKey = "impact" | "population" | "schools";

// The main view is the U-Net Appalachia discovery (the project's headline). CA/OK
// (LBNL's published baseline) is held out as a separate validation layer.
const APPALACHIA_STATES = new Set([
  "Ohio",
  "Pennsylvania",
  "West Virginia",
  "Kentucky",
]);

export default function Page() {
  const [documented, setDocumented] = useState<DocumentedWells | null>(null);
  const [candidates, setCandidates] = useState<CandidateLite[]>([]);
  const [heroes, setHeroes] = useState<Candidate[]>([]);
  const [meta, setMeta] = useState<Meta | null>(null);
  const [dossiers, setDossiers] = useState<Record<string, Dossier>>({});
  const [cases, setCases] = useState<Record<string, CaseFile>>({});

  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [detailCache, setDetailCache] = useState<Record<string, Candidate>>({});
  const [detailLoading, setDetailLoading] = useState(false);
  const [hover, setHover] = useState<{ c: CandidateLite; x: number; y: number } | null>(null);
  const [showDocumented, setShowDocumented] = useState(true);
  const [region, setRegion] = useState<string>("all");
  const [query, setQuery] = useState("");
  const [sortKey, setSortKey] = useState<SortKey>("impact");
  const [intro, setIntro] = useState(true);
  const [focus, setFocus] = useState<FocusTarget | null>(null);
  const [fitNonce, setFitNonce] = useState(0);
  const [topoHero, setTopoHero] = useState<Candidate | null>(null);
  const [investigatingId, setInvestigatingId] = useState<string | null>(null);
  const [liveLog, setLiveLog] = useState<string[]>([]);
  const nonce = useRef(0);

  useEffect(() => {
    loadDocumented().then(setDocumented).catch(console.error);
    loadCandidates().then(setCandidates).catch(console.error);
    loadHeroes().then(setHeroes).catch(() => {});
    loadMeta().then(setMeta).catch(console.error);
    loadDossiers().then(setDossiers).catch(() => {});
    loadCaseFiles().then(setCases).catch(() => {});
  }, []);

  // Default main view = Appalachia discovery only; CA/OK is the validation layer.
  // Heroes are featured separately (prepended + red markers), so drop them here to
  // avoid showing each twice.
  const heroIds = useMemo(() => new Set(heroes.map((h) => h.well_id)), [heroes]);
  const mainCandidates = useMemo(
    () => candidates.filter((c) => APPALACHIA_STATES.has(c.state) && !heroIds.has(c.well_id)),
    [candidates, heroIds]
  );

  const byId = useMemo(() => {
    const m = new Map<string, CandidateLite>();
    [...heroes, ...candidates].forEach((c) => m.set(c.well_id, c));
    return m;
  }, [heroes, candidates]);

  const heroById = useMemo(() => {
    const m = new Map<string, Candidate>();
    heroes.forEach((h) => m.set(h.well_id, h));
    return m;
  }, [heroes]);

  const regions = useMemo(
    () =>
      meta
        ? Object.keys(meta.candidate_by_region).filter((r) => /_(PA|OH|WV|KY)$/.test(r))
        : [],
    [meta]
  );

  const items = useMemo(() => {
    let list = mainCandidates;
    if (region !== "all") list = list.filter((c) => c.county_group === region);
    if (query.trim()) {
      const q = query.toLowerCase();
      list = list.filter(
        (c) =>
          c.quad_name?.toLowerCase().includes(q) ||
          c.state.toLowerCase().includes(q) ||
          c.enrichment?.nearest_school?.toLowerCase().includes(q) ||
          c.enrichment?.county?.toLowerCase().includes(q)
      );
    }
    const sorted = [...list];
    if (sortKey === "population")
      sorted.sort(
        (a, b) =>
          (b.enrichment?.population_1mi ?? b.enrichment?.population ?? 0) -
          (a.enrichment?.population_1mi ?? a.enrichment?.population ?? 0)
      );
    else if (sortKey === "schools")
      sorted.sort(
        (a, b) => (b.enrichment?.schools_within_1mi ?? 0) - (a.enrichment?.schools_within_1mi ?? 0)
      );
    else sorted.sort((a, b) => a.rank - b.rank);
    const heroMatch = region === "all" && !query.trim() ? heroes : [];
    return [...heroMatch, ...sorted];
  }, [mainCandidates, heroes, region, query, sortKey]);

  const selected = selectedId ? byId.get(selectedId) ?? null : null;
  const selectedDetail: Candidate | null = selectedId
    ? heroById.get(selectedId) ?? detailCache[selectedId] ?? null
    : null;

  function select(id: string) {
    const c = byId.get(id);
    if (!c) return;
    setSelectedId(id);
    nonce.current += 1;
    setFocus({ lon: c.lon, lat: c.lat, zoom: c.hero ? 15 : 13.5, nonce: nonce.current });
    if (heroById.has(id) || detailCache[id]) return;
    const shard = shardOf(c.rank);
    setDetailLoading(true);
    loadDetailShard(shard)
      .then((recs) => setDetailCache((prev) => ({ ...prev, ...recs })))
      .catch(console.error)
      .finally(() => setDetailLoading(false));
  }

  // Live, on-any-well investigation: stream the SSE route into the panel.
  async function investigateLive(well: Candidate) {
    setInvestigatingId(well.well_id);
    setLiveLog([]);
    try {
      const res = aw
[truncated — 10143 more characters]
```

[78 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]