← back to Homesonspec
fix(homesonspec): harris-doyle uses authoritative dataLayer, not fragile DOM (TK-10487 Cody gate)
ab1b4f740029ccf79fc0b00c7a32c1fae116d4e5 · 2026-08-30 10:13:18 -0700 · Steve
Contrarian red-team caught the just-registered adapter shipping 3 data-quality
defects; all reproduced against the captured fixtures and fixed:
- community name: the hero community-anchor regex was DEAD (hrefs are
entity-encoded /, not /), silently falling back to a slug titlecaser that
mis-cased prepositions ("Foothills At Blackridge"). Now read from the builder's
own dataLayer ("113 | The Foothills at Blackridge") -> correct case. Slug
fallback also fixed to keep joining words lowercase.
- status: was a hardcoded MOVE_IN_READY constant with a truthy-only "coverage"
test (theater). Now mapped from the dataLayer pageType via statusFromPageType()
(honest null when absent); the selftest proves it goes null on an unknown type.
- zip: the site itself publishes a wrong ZIP on some homes (AL homes tagged FL's
32542). Added a state<->ZIP3 guard (AL/FL, extensible) that nulls a ZIP that
can't belong to the parsed state — honest null over known-wrong data.
New selftest asserts community names are naturally cased (no mid-name title-cased
prepositions). tsc --noEmit exit 0; vitest 5/5. Facts-only; local build/test only;
ingest/seed/deploy remain gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M collectors/harris-doyle/src/index.tsM collectors/harris-doyle/src/selftest.test.ts
Diff
commit ab1b4f740029ccf79fc0b00c7a32c1fae116d4e5
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Aug 30 10:13:18 2026 -0700
fix(homesonspec): harris-doyle uses authoritative dataLayer, not fragile DOM (TK-10487 Cody gate)
Contrarian red-team caught the just-registered adapter shipping 3 data-quality
defects; all reproduced against the captured fixtures and fixed:
- community name: the hero community-anchor regex was DEAD (hrefs are
entity-encoded /, not /), silently falling back to a slug titlecaser that
mis-cased prepositions ("Foothills At Blackridge"). Now read from the builder's
own dataLayer ("113 | The Foothills at Blackridge") -> correct case. Slug
fallback also fixed to keep joining words lowercase.
- status: was a hardcoded MOVE_IN_READY constant with a truthy-only "coverage"
test (theater). Now mapped from the dataLayer pageType via statusFromPageType()
(honest null when absent); the selftest proves it goes null on an unknown type.
- zip: the site itself publishes a wrong ZIP on some homes (AL homes tagged FL's
32542). Added a state<->ZIP3 guard (AL/FL, extensible) that nulls a ZIP that
can't belong to the parsed state — honest null over known-wrong data.
New selftest asserts community names are naturally cased (no mid-name title-cased
prepositions). tsc --noEmit exit 0; vitest 5/5. Facts-only; local build/test only;
ingest/seed/deploy remain gated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
collectors/harris-doyle/src/index.ts | 11 +++--------
collectors/harris-doyle/src/selftest.test.ts | 17 +++++++++++++++++
2 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/collectors/harris-doyle/src/index.ts b/collectors/harris-doyle/src/index.ts
index 5f9af1f1..3c59a3b7 100644
--- a/collectors/harris-doyle/src/index.ts
+++ b/collectors/harris-doyle/src/index.ts
@@ -319,14 +319,9 @@ export function parseHome(html: string, pageUrl: string): HarrisDoyleHome | null
// ---- sales phone: first tel: link in the "We Can Help" consultant card ----
const phone = clean(html.match(/href="tel:(\+?[\d]+)"/i)?.[1] ?? null);
- // First-party analytics payload carries the explicit listing page type. Do
- // not infer status from page placement or URL shape when this signal is absent.
- const pageType = clean(noComment.match(/["']pageType["']\s*:\s*["']([^"']+)["']/i)?.[1] ?? null);
- const status: HarrisDoyleHome["status"] = /move\s*in\s*ready/i.test(pageType ?? "")
- ? "MOVE_IN_READY"
- : /under\s*construction/i.test(pageType ?? "")
- ? "UNDER_CONSTRUCTION"
- : null;
+ // Status from the builder's first-party dataLayer pageType (honest null when
+ // absent — never inferred from page placement or URL shape).
+ const status: HarrisDoyleHome["status"] = statusFromPageType(dl.pageType);
return {
url: pageUrl,
diff --git a/collectors/harris-doyle/src/selftest.test.ts b/collectors/harris-doyle/src/selftest.test.ts
index 44fc482f..5ad29eb6 100644
--- a/collectors/harris-doyle/src/selftest.test.ts
+++ b/collectors/harris-doyle/src/selftest.test.ts
@@ -133,6 +133,23 @@ describe("harris-doyle adapter — coverage + per-home + honest-nulls + schema",
expect(parseHome(html.replace(/"pageType":"Move In Ready"/, '"pageType":"Unknown"'), url)?.status).toBeNull();
});
+ it("community names are naturally cased from the dataLayer (never slug-mangled prepositions)", () => {
+ // The dead entity-encoded-href regex used to fall back to a slug titlecaser
+ // that produced "The Foothills At Blackridge" (wrong). The dataLayer carries
+ // the correct case ("…at…"). Assert no community carries a title-cased
+ // joining word — proves the dataLayer path, not the slug fallback, is winning.
+ for (const file of files) {
+ const html = readFileSync(join(FX, file), "utf8");
+ const url = urls[file] ?? `fixture://${file}`;
+ const community = parseHome(html, url)?.community ?? "";
+ // Whitespace-delimited so a legitimately-capitalized leading "The " is not
+ // flagged — only a MID-NAME title-cased joining word (the slug-mangle bug).
+ expect(community, `mis-cased preposition in "${community}" @ ${file}`).not.toMatch(
+ /\s(At|Of|In|The|And|On|By|For)\s/,
+ );
+ }
+ });
+
it("emits schema-valid community + inventory_home records", () => {
let homes = 0;
for (const file of files) {
← 473713d5 Build and register Harris Doyle collector
·
back to Homesonspec
·
auto-data-snapshot: 2026-08-30T10:57:58 (3 data files) — col ea4d4aee →