[object Object]

← back to Homesonspec

auto-save: 2026-07-29T09:37:47 (1 files) — collectors/richmond-american/

63d56d74733350504654ba1e0b52ccc127ac3b82 · 2026-07-29 09:37:54 -0700 · Steve Abrams

Files touched

Diff

commit 63d56d74733350504654ba1e0b52ccc127ac3b82
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Jul 29 09:37:54 2026 -0700

    auto-save: 2026-07-29T09:37:47 (1 files) — collectors/richmond-american/
---
 collectors/richmond-american/RECON.md | 113 ++++++++++++++++++++++++++++++++++
 1 file changed, 113 insertions(+)

diff --git a/collectors/richmond-american/RECON.md b/collectors/richmond-american/RECON.md
new file mode 100644
index 00000000..7c50c741
--- /dev/null
+++ b/collectors/richmond-american/RECON.md
@@ -0,0 +1,113 @@
+# Richmond American Homes — RECON (2026-07-29, TK-10001)
+
+**Status: DEFERRED — architecture-fit blocker (NOT a robots/bot blocker, NOT bucket-D).**
+The inventory data is fully reachable, robots-ALLOWED, and extracts cleanly under an
+honest UA. It is NOT collectible by the current HTTP-only collector framework because
+the site's facts only materialize after a **real-browser render**. This adapter was
+intentionally NOT built; see the DTD verdict below. This file documents the exact
+verified path so a future browser-enabled shared fetcher can adopt it in ~1 hour.
+
+---
+
+## Why it's deferred (DTD 3/4 verdict B, 2026-07-29)
+
+`richmondamerican.com` is a **Blazor Server** SPA with **prerendering DISABLED**:
+
+- A plain HTTP `GET` of a per-home page returns a **242 KB empty app shell** —
+  no `<title>`, no JSON-LD, no price/beds/baths/sqft anywhere in the HTML.
+- Facts arrive only as **DOM diffs over the Blazor WebSocket** (`wss://www.richmondamerican.com/_blazor?id=…`)
+  after `/_blazor/negotiate`, rendered by `blazor.server.js` in a real browser.
+  (Verified: 27 WS frames / ~528 KB per home; 2 frames carry the fact payload.)
+
+The shared `@homesonspec/collectors-common` `LiveFetcher` is **HTTP-only** (`GET` /
+`postJson` via `fetch()`, no browser) and every one of the ~30 existing adapters uses
+it. A one-off Playwright render smuggled inside this adapter's `fetch()` would:
+break the invariant that a `RawPage` == an HTTP response, add a hidden non-workspace
+dependency no other adapter has, and fracture the shared pattern. The panel
+(Qwen + Grok + Kimi = B; Claude dissent = A; Codex abstained on a bad key) ruled the
+correct fix is a **shared browser-enabled fetcher**, which is out of the
+"touch only collectors/richmond-american/, no shared edits" scope of TK-10001.
+
+---
+
+## robots.txt — the data path IS allowed
+
+`https://www.richmondamerican.com/robots.txt` disallows (do NOT use):
+`/Community/LoadAll/`, `/Home/GenerateSiteMapXml/`, `/bin/`, `/Views/`,
+`/communityInfo*`, `/my-account/*`, `/account/*`, `*.config/*.dll` etc.
+
+The per-home detail URLs below are **ALLOWED** (no rule matches them). Verified with
+UA token `homesonspecbot` against the standard longest-match precedence.
+
+---
+
+## VERIFIED render path (sitemap → per-home URL → honest-UA Blazor render → DOM)
+
+1. **Sitemap index** (allowed): `https://www.richmondamerican.com/sitemap.xml`
+   → `sitemap1.xml … sitemap7.xml`.
+   - `sitemap4.xml` = **communities**  (`/{state}/{metro}/{city}/{community}/`)
+   - `sitemap5.xml` = **floor plans** (plan-level aggregate — SKIP, not per-home)
+   - **`sitemap6.xml` = the QMI per-home inventory** — **1,681** URLs of the form
+     `/{state}/{metro}/{city}/{community}/{plan}/{homeId}/`
+     where `homeId` is `NNNNNNNN-NNNN` (e.g. `24880000-0046`). This is the feed.
+
+2. **Fetch each per-home URL in a real browser with the honest UA**
+   `HomesOnSpecBot/0.1 (+homesonspec.com; site-owner-facts-only)` (or the
+   framework UA `HomesOnSpecBot/0.1 (+https://homesonspec.com/bot; contact: data@homesonspec.com)`).
+   `waitUntil: "networkidle"`, then `waitForFunction` until `document.body.innerText`
+   contains a `$NNN,NNN` price OR a "Model For Tour" / "no longer available" marker,
+   then `waitForTimeout(~1500ms)`. (~9 s per home total.)
+
+   **IMPORTANT — landing pages hit a CHEQ bot-wall, detail pages do NOT.**
+   `/quick-move-in-homes`, `/inventory`, `/homes` etc. are SPA entry points guarded by
+   CHEQ (`obs.starsbuildingweb.com/ct`) that redirects an automated client to `/404/`.
+   The **per-home detail URLs from sitemap6 render fine** under the honest UA — do NOT
+   drive collection through the QMI landing/search pages; go straight from sitemap6.
+
+3. **Extract from the rendered DOM** (`document.body.innerText`, regex — see below).
+   No JSON-LD, no `window.__` state, no embedded JSON in the DOM: text-scrape only.
+
+### Field extraction (regex over rendered innerText — verified 8/8 across FL/CO/AZ/CA/UT)
+
+| Field       | Source                                               |
+|-------------|------------------------------------------------------|
+| address     | `document.title` (`"{addr} \| Home For Sale in {community}"`) OR innerText `NN..., City, ST, ZIP` |
+| plan name   | `<h1>` (e.g. "Agate", "Dillon II")                   |
+| community   | tail of `<title>` after "in " / the community URL slug |
+| price       | `\$([0-9]{3},[0-9]{3})`  — **null for "Model For Tour" homes (they have no sale price — correct, keep null)** |
+| beds        | `([0-9]+)\s*Bed\b`                                    |
+| baths       | `([0-9](?:\.[0-9])?)\s*Bath\b`  (half-baths already folded, e.g. 2.5) |
+| sqft        | `([0-9],?[0-9]{3})\s*Approx\.\s*sq\.?\s*ft` (number sits ABOVE the "Approx. sq. ft." label) |
+| garage      | `([0-9]+)\s*Car Garage`                              |
+| move-in     | `Ready for move-in (now|<Month> <Year>) at <community>` → MOVE_IN_READY when "now", else UNDER_CONSTRUCTION + estCompletion |
+| homeId      | trailing `NNNNNNNN-NNNN` path segment (→ builderInventoryId) |
+| state/city  | parsed from the address tail (`, City, ST, ZIP`) → `normalizeStateCode` |
+| **lat/lon** | **NOT available** — Blazor renders an SVG map, no lat/lon in any request. Leave null; the city+state gate works off the parsed address. |
+
+### Emission (mirror meritage-homes exactly)
+- Emit **community FIRST** (canonicalHints `{builderSlug:"richmond-american", communityName}`),
+  then **inventory_home** (`{builderSlug, communityName, address, builderInventoryId:homeId, planName}`).
+- `homeType: "SINGLE_FAMILY"`, `images: []` (facts-only, no images).
+- Skip a home with no address or no community (log, don't stage — same as Meritage).
+- `RICHMOND_PAGE_LIMIT` (default 10) caps homes/run; optional `RICHMOND_STATE` filter
+  the sitemap6 URL list by `/{state}/` slug.
+
+---
+
+## Self-test proof (data quality — 8 homes, 6 states)
+
+Ran `/tmp/richmond-extract2.mjs` on 8 sitemap6 URLs spread across the catalog:
+
+- **8/8** homes returned distinct real street addresses + distinct sqft
+  (2070 / 2020 / 2510 / 2600 / 2170 / 2580 / 4518 / 2420) — genuinely per-home,
+  NOT floorplan- or community-aggregate.
+- **6/8** have a real price ($437,997 … $979,950); the 2 nulls are correctly
+  "Model For Tour" homes (no sale price by definition — a real distinction, not a miss).
+- **8/8** beds / baths / sqft / garage / move-in complete.
+- States seen: FL, CO, AZ, CA, UT.
+
+## What to do to ship it
+Add a browser-render mode to `collectors-common` (a `RenderFetcher` alongside
+`LiveFetcher`, same robots + rate-limit + honest-UA rails, but a Playwright page render
+returning rendered HTML as the RawPage body). Then this adapter is a thin sitemap6
+crawler + innerText-regex `extract()` — the fields above are already verified.

← 5c09c690 collectors: add Mattamy Homes — cracked the 'bucket-D' natio  ·  back to Homesonspec  ·  lennar refresh runner: source DB pw from gitignored .env (wa 9c5e6719 →