← back to Homesonspec
KB Home: national single-run adapter (all-states extract, per-record state) — fixes page-dedup coverage gap + kills 42x redundant 5.8MB fetch. DTD 5/5, root-cause verified
c7715e27b02d7b38e4949facf686da442bfa7371 · 2026-07-28 11:44:54 -0700 · Steve Abrams
Files touched
M collectors/kb-home/src/index.tsM scripts/build-loop.sh
Diff
commit c7715e27b02d7b38e4949facf686da442bfa7371
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 11:44:54 2026 -0700
KB Home: national single-run adapter (all-states extract, per-record state) — fixes page-dedup coverage gap + kills 42x redundant 5.8MB fetch. DTD 5/5, root-cause verified
---
collectors/kb-home/src/index.ts | 13 +++++++++----
scripts/build-loop.sh | 10 +++++++---
2 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/collectors/kb-home/src/index.ts b/collectors/kb-home/src/index.ts
index 757c5900..29985e16 100644
--- a/collectors/kb-home/src/index.ts
+++ b/collectors/kb-home/src/index.ts
@@ -18,7 +18,12 @@ import {
const FEED_URL = "https://www.kbhome.com/move-in-ready";
const ORIGIN = "https://www.kbhome.com";
const BUILDER_SLUG = "kb-home";
-const STATE = (process.env.KBHOME_STATE ?? "CA").toUpperCase();
+// KB is a SINGLE national feed (one fetch = every home). Per-state iteration is dead-on-arrival:
+// page-level contentHash dedup skips the identical 5.8MB feed for every state after the first, so
+// only one state ever gets extracted per feed-change. Default = national extraction (all states);
+// an explicit KBHOME_STATE still filters (back-compat). Each home is tagged by its own r.State.
+const STATE_FILTER = process.env.KBHOME_STATE?.toUpperCase();
+const ALL_STATES = !STATE_FILTER || STATE_FILTER === "ALL";
function fv<T>(value: T | null, raw: string | null, sourceUrl: string, evidenceText?: string | null): FieldValue<T> {
return { value, raw, evidenceText: evidenceText ?? raw, sourceUrl, confidence: value === null ? 0 : 1 };
@@ -78,7 +83,7 @@ export const kbHomeAdapter: SourceAdapter = {
if (!Array.isArray(all) || all.length === 0) {
return { records: [], errors: [{ url: page.url, reason: "allMIRs not found/empty" }] };
}
- const rows = all.filter((r) => str(r.State)?.toUpperCase() === STATE);
+ const rows = ALL_STATES ? all : all.filter((r) => str(r.State)?.toUpperCase() === STATE_FILTER);
const records: ExtractedRecord[] = [];
// Emit each unique community first (publish needs the FK target before homes).
@@ -95,7 +100,7 @@ export const kbHomeAdapter: SourceAdapter = {
name: fv(communityName, communityName, page.url),
street: fv<string>(null, null, page.url),
city: fv(str(r.City), str(r.City), page.url),
- state: fv(STATE, str(r.State), page.url),
+ state: fv(str(r.State)?.toUpperCase() ?? null, str(r.State), page.url),
zip: fv(str(r.ZipCode), str(r.ZipCode), page.url),
county: fv<string>(null, null, page.url),
metro: fv<string>(null, null, page.url),
@@ -129,7 +134,7 @@ export const kbHomeAdapter: SourceAdapter = {
fields: {
street: fv(address, address, page.url),
city: fv(str(r.City), str(r.City), page.url),
- state: fv(STATE, str(r.State), page.url),
+ state: fv(str(r.State)?.toUpperCase() ?? null, str(r.State), page.url),
zip: fv(str(r.ZipCode), str(r.ZipCode), page.url),
price: fv(num(r.Price), str(r.Price), page.url, r.Price ? `price ${String(r.Price)}` : null),
beds: fv(num(r.Bedrooms), str(r.Bedrooms), page.url),
diff --git a/scripts/build-loop.sh b/scripts/build-loop.sh
index 925db5bb..3c66bd1b 100644
--- a/scripts/build-loop.sh
+++ b/scripts/build-loop.sh
@@ -60,7 +60,6 @@ sweep_adapter() {
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: /" ;;
@@ -75,6 +74,11 @@ sweep_adapter() {
# 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: /"; }
+# KB Home = single national feed (one fetch = every home across ~9 states). Run ONCE per sweep in
+# ALL-states mode — per-state iteration was a no-op after the first state (page-dedup on the identical
+# feed) AND fired 42x redundant 5.8MB GETs/sweep. [Cody/DTD-gated 2026-07-28]
+sweep_kb() { KBHOME_STATE=ALL CLI kb-home-site | sed "s/^/ kb NATIONAL: /"; }
+
# metro-iterating adapters (footprint = METROS, not states) — scoped to served metros only
# (Cody WAF-lesson from the start). Ashton Woods publishes per-metro quick-move-in homes.
sweep_metros() {
@@ -103,8 +107,8 @@ while [ ! -f "$STOP" ] && [ "$sweep" -lt "$MAX_SWEEPS" ]; do
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 &
+ LOG "fanning out 4 state adapters + KB(national) + AW(metros) + Toll(throttled) across ${CAP}-deep states (West Coast first)"
+ sweep_kb &
sweep_adapter drh &
sweep_adapter plt &
sweep_adapter tri &
← 53fe5d85 build-loop: add 3 missing AW metros (charleston/jacksonville
·
back to Homesonspec
·
KB Home: key community seen-set on name+state (Cody: cross-s 1f76088e →