[object Object]

← 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

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 →