[object Object]

← 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

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 &#x2f;, 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 →