← back to Homesonspec
scripts/build-loop.sh
107 lines
#!/usr/bin/env bash
# ALL-NIGHT autonomous "keep building" loop for HomesOnSpec inventory.
# Layer 1 (this script): deterministic ingest + public-records enrichment across
# the working adapters × top builder states, op-40 each, repeated in freshness
# sweeps until a STOP file appears or MAX_SWEEPS is hit. Facts-only, robots-
# enforced (LiveFetcher), LOCAL DB only — the public Kamatera go-live stays gated.
# Idempotent: re-sweeps of unchanged data are cheap no-ops (contentHash dedup).
# bash-3.2 safe (macOS system bash) — no associative arrays.
# Stop anytime: touch ~/Projects/homesonspec/tmp/BUILD-LOOP-STOP
set -uo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT/apps/workers" || exit 1
# Load the repo .env (holds the correct DATABASE_URL per host: Kamatera-local on prod, Mac
# socket on dev), then honor any already-injected DATABASE_URL, else default to the Mac socket.
[ -f "$ROOT/.env" ] && { set -a; . "$ROOT/.env"; set +a; }
export DATABASE_URL="${DATABASE_URL:-postgresql://macstudio3@localhost/homesonspec?host=/tmp}"
mkdir -p "$ROOT/tmp"
STOP="$ROOT/tmp/BUILD-LOOP-STOP"
MAX_SWEEPS="${MAX_SWEEPS:-6}"
SWEEP_SLEEP="${SWEEP_SLEEP:-9000}" # ~2.5h between freshness sweeps (polite)
LOG() { echo "[$(date '+%m-%d %H:%M:%S')] $*"; }
PSQL() { psql "$DATABASE_URL" -tAc "$1" 2>/dev/null; }
CLI() { npx tsx src/cli.ts --adapter="$1" 2>&1 | grep -E '"published"|"pages"|Error|error|inactive|blocked|disallow' | head -3; }
# NATIONWIDE — WEST COAST FIRST (Steve 2026-07-28), then the rest by volume.
# Adapters filter by state; a state a builder doesn't serve is a cheap 0-published no-op.
# CAP = communities/homes per (adapter,state) per sweep — deep enough to actually fill CA.
CAP="${CAP:-120}"
STATES="CA OR WA NV AZ ID HI TX FL NC GA SC TN CO OH PA VA UT IN MD MI MN MO AL OK KY LA NJ NM NY WI AR KS NE IA MS WV CT DE NH ME RI"
fullname() { case "$1" in
AL) echo "Alabama";; AK) echo "Alaska";; AZ) echo "Arizona";; AR) echo "Arkansas";;
CA) echo "California";; CO) echo "Colorado";; CT) echo "Connecticut";; DE) echo "Delaware";;
FL) echo "Florida";; GA) echo "Georgia";; HI) echo "Hawaii";; ID) echo "Idaho";;
IL) echo "Illinois";; IN) echo "Indiana";; IA) echo "Iowa";; KS) echo "Kansas";;
KY) echo "Kentucky";; LA) echo "Louisiana";; ME) echo "Maine";; MD) echo "Maryland";;
MA) echo "Massachusetts";; MI) echo "Michigan";; MN) echo "Minnesota";; MS) echo "Mississippi";;
MO) echo "Missouri";; MT) echo "Montana";; NE) echo "Nebraska";; NV) echo "Nevada";;
NH) echo "New Hampshire";; NJ) echo "New Jersey";; NM) echo "New Mexico";; NY) echo "New York";;
NC) echo "North Carolina";; ND) echo "North Dakota";; OH) echo "Ohio";; OK) echo "Oklahoma";;
OR) echo "Oregon";; PA) echo "Pennsylvania";; RI) echo "Rhode Island";; SC) echo "South Carolina";;
SD) echo "South Dakota";; TN) echo "Tennessee";; TX) echo "Texas";; UT) echo "Utah";;
VT) echo "Vermont";; VA) echo "Virginia";; WA) echo "Washington";; WV) echo "West Virginia";;
WI) echo "Wisconsin";; WY) echo "Wyoming";;
*) echo "$1";; esac; }
lc() { echo "$1" | tr '[:upper:]' '[:lower:]'; }
slug() { fullname "$1" | tr '[:upper:] ' '[:lower:]-'; } # "North Carolina" -> north-carolina
# max-it: one worker per adapter, sweeping ALL states. The 4 run CONCURRENTLY —
# different domains, so each stays polite within its own per-source rate limit while
# 4x the inventory lands per wall-clock second. $0 (no LLM anywhere in ingestion).
sweep_adapter() {
local kind="$1"
for S in $STATES; do
[ -f "$STOP" ] && break
case "$kind" in
kb) KBHOME_STATE="$S" CLI kb-home-site | sed "s/^/ kb $S: /" ;;
drh) DRHORTON_STATE="$(slug "$S")" DRHORTON_PAGE_LIMIT=$CAP CLI dr-horton-site | sed "s/^/ drh $S: /" ;;
plt) PULTE_STATE="$(fullname "$S")" PULTE_PAGE_LIMIT=$CAP CLI pultegroup-site | sed "s/^/ plt $S: /" ;;
tri) TRIPOINTE_STATE="$(lc "$S")" TRIPOINTE_PAGE_LIMIT=$CAP CLI tri-pointe-site | sed "s/^/ tri $S: /" ;;
len) LENNAR_STATE="$S" LENNAR_PAGE_LIMIT=$CAP CLI lennar-site | sed "s/^/ len $S: /" ;;
esac
done
}
# national adapters (single feed, no per-state param) — run ONCE per sweep, not per-state,
# so we don't hammer the same national endpoint 40x. Toll Brothers publishes ~all in one run.
sweep_national() { CLI toll-brothers-site | sed "s/^/ tol NATIONAL: /"; }
# wait until no other adapter run is in flight (avoid same-source races)
while pgrep -f "cli.ts --adapter" 2>/dev/null | grep -qv "^$$\$"; do sleep 20; done
for k in dr-horton-site kb-home-site pultegroup-site tri-pointe-site lennar-site toll-brothers-site; do
PSQL "update \"SourceRegistry\" set active=true, health='HEALTHY', \"consecutiveFailures\"=0 where key='$k';" >/dev/null
done
sweep=0
while [ ! -f "$STOP" ] && [ "$sweep" -lt "$MAX_SWEEPS" ]; do
sweep=$((sweep+1))
s0=$(PSQL "select count(*) from \"InventoryHome\" where status='PUBLISHED';")
b0=$(PSQL "select count(distinct \"builderId\") from \"InventoryHome\" where status='PUBLISHED';")
LOG "===== SWEEP $sweep/$MAX_SWEEPS start: $s0 homes / $b0 builders ====="
LOG "fanning out 5 state adapters + Toll(national) in parallel across ${CAP}-deep states (West Coast first)"
sweep_adapter kb &
sweep_adapter drh &
sweep_adapter plt &
sweep_adapter tri &
sweep_adapter len &
# Toll = communities-only (0 spec homes until wave-2 QMI); its data changes on the order of
# weeks, so refresh ~daily (every 8th ~2.5h sweep) not every sweep (DTD 4/5 throttle 2026-07-28).
if [ $((sweep % 8)) -eq 1 ]; then sweep_national & fi
wait
# drain enrichment queue (bigger batches for speed)
for i in $(seq 1 60); do
rem=$(PSQL "select count(*) from \"Community\" where lat is not null and lon is not null and amenities is null;")
[ "${rem:-0}" -eq 0 ] && { LOG "enrich: queue drained"; break; }
LOG "enrich pass $i: $rem left"
NEARBY_LIMIT=80 npx tsx scripts/enrich-nearby.ts >/dev/null 2>&1
done
s1=$(PSQL "select count(*) from \"InventoryHome\" where status='PUBLISHED';")
b1=$(PSQL "select count(distinct \"builderId\") from \"InventoryHome\" where status='PUBLISHED';")
enr=$(PSQL "select count(*) from \"Community\" where amenities is not null;")
LOG "===== SWEEP $sweep done: $s1 homes / $b1 builders (+$((s1-s0))) · $enr communities enriched ====="
[ -f "$STOP" ] && break
[ "$sweep" -lt "$MAX_SWEEPS" ] && { LOG "sleeping ${SWEEP_SLEEP}s"; sleep "$SWEEP_SLEEP"; }
done
LOG "BUILD LOOP EXIT (sweeps=$sweep)"