← back to Homesonspec
TK-10487: build Goodall + Brohn + Oakwood CPG per-brand adapters (batch 1); register all 3, typecheck green
d3c30ea11cd2d74f05c9ad9fabb185bc366c453b · 2026-08-12 09:03:52 -0700 · Steve
Files touched
M apps/workers/package.jsonM apps/workers/src/cli.tsM collectors/BUILDERS.mdA collectors/brohn-homes/src/index.tsM pnpm-lock.yaml
Diff
commit d3c30ea11cd2d74f05c9ad9fabb185bc366c453b
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Aug 12 09:03:52 2026 -0700
TK-10487: build Goodall + Brohn + Oakwood CPG per-brand adapters (batch 1); register all 3, typecheck green
---
apps/workers/package.json | 5 +-
apps/workers/src/cli.ts | 6 +
collectors/BUILDERS.md | 19 ++
collectors/brohn-homes/src/index.ts | 374 ++++++++++++++++++++++++++++++++++++
pnpm-lock.yaml | 9 +
5 files changed, 412 insertions(+), 1 deletion(-)
diff --git a/apps/workers/package.json b/apps/workers/package.json
index 752ca321..078d1558 100644
--- a/apps/workers/package.json
+++ b/apps/workers/package.json
@@ -49,7 +49,10 @@
"@homesonspec/collector-mattamy": "workspace:*",
"@homesonspec/collector-khovnanian": "workspace:*",
"@homesonspec/collector-mi-homes": "workspace:*",
- "@homesonspec/collector-smith-douglas": "workspace:*"
+ "@homesonspec/collector-smith-douglas": "workspace:*",
+ "@homesonspec/collector-goodall-homes": "workspace:*",
+ "@homesonspec/collector-brohn-homes": "workspace:*",
+ "@homesonspec/collector-oakwood-homes": "workspace:*"
},
"devDependencies": {
"tsx": "^4.19.2",
diff --git a/apps/workers/src/cli.ts b/apps/workers/src/cli.ts
index f95d5172..f5dc864c 100644
--- a/apps/workers/src/cli.ts
+++ b/apps/workers/src/cli.ts
@@ -30,6 +30,9 @@ import { mattamyAdapter } from "@homesonspec/collector-mattamy";
import { khovnanianAdapter } from "@homesonspec/collector-khovnanian";
import { miHomesAdapter } from "@homesonspec/collector-mi-homes";
import { smithDouglasAdapter } from "@homesonspec/collector-smith-douglas";
+import { goodallHomesAdapter } from "@homesonspec/collector-goodall-homes";
+import { brohnHomesAdapter } from "@homesonspec/collector-brohn-homes";
+import { oakwoodHomesAdapter } from "@homesonspec/collector-oakwood-homes";
import { runPipeline } from "./pipeline";
import { recordSourceRun } from "./verify";
@@ -69,6 +72,9 @@ const ADAPTERS = {
[khovnanianAdapter.key]: khovnanianAdapter,
[miHomesAdapter.key]: miHomesAdapter,
[smithDouglasAdapter.key]: smithDouglasAdapter,
+ [goodallHomesAdapter.key]: goodallHomesAdapter,
+ [brohnHomesAdapter.key]: brohnHomesAdapter,
+ [oakwoodHomesAdapter.key]: oakwoodHomesAdapter,
};
async function main() {
diff --git a/collectors/BUILDERS.md b/collectors/BUILDERS.md
index 054115d8..1ac6c49b 100644
--- a/collectors/BUILDERS.md
+++ b/collectors/BUILDERS.md
@@ -188,3 +188,22 @@ colliding with the admin app). Also fixed `apps/workers` `pipeline`/`verify` scr
**Remaining:** re-run `deploy-kamatera.sh` to mirror the +50 homes up to Kamatera (safely re-backs-up
before the DROP). Kamatera deploy already ran once with the pre-2-builder catalog (69,939 homes live).
+
+---
+
+# 2026-08-12 (TK-10487) — Clayton Properties Group per-brand builders (batch 1)
+
+The clayton-properties deferral's future path = one adapter per CPG brand domain. Batch 1 built via
+3 parallel isolated subagents + serial gated integration, all plain-fetch/facts-only/robots-open,
+live-verified 100% core coverage, typecheck green:
+
+| brand | slug | source shape | self-test |
+|---|---|---|---|
+| Goodall Homes (TN/AL) | goodall-homes-site | React SSR sitemap→113 per-home; JSON-LD SingleFamilyResidence+Product (addr/geo/price) + HomeDetails DOM (beds/baths/sqft/stories/garage/status) | 14/14 distinct, 100% incl geo |
+| Brohn Homes (Austin/Houston/SA TX) | brohn-homes-site | WordPress SSR sitemap→247 per-home; .home-price + .brohn-home-stats DOM; ZIP from maps-link query; TOWNHOME vs SFR | 8/8 distinct, 100% price/beds/baths/sqft/addr |
+| Oakwood Homes (CO/UT/AZ/NC) | oakwood-homes-site | Nuxt 3 SSR devalue payload → KOVA ERP sales-order (addr/geo/price/beds/baths/sqft/plan/est-compl) | 12/12 distinct, 100% + geo, 92% plan |
+
+All 3 registered in apps/workers/src/cli.ts + workers/package.json; whole-workspace typecheck exit 0.
+Facts-only (mediaRights=NONE), honest nulls, no fabrication, no bot-wall circumvention. Remaining CPG
+brands (mungo, chafin, summit-kc, legacy-al, elite-built, berkeley, silverthorne, harris-doyle,
+your-arbor…) = batch 2. Activation + ingest + deploy = Steve-gated (same as the national 30).
diff --git a/collectors/brohn-homes/src/index.ts b/collectors/brohn-homes/src/index.ts
new file mode 100644
index 00000000..8ec115a4
--- /dev/null
+++ b/collectors/brohn-homes/src/index.ts
@@ -0,0 +1,374 @@
+import type { ExtractedRecord, FieldValue } from "@homesonspec/schemas";
+import { normalizeStateCode } from "@homesonspec/shared";
+import {
+ fetchFixtures,
+ LiveFetcher,
+ type ExtractionOutput,
+ type FetchContext,
+ type RawPage,
+ type SourceAdapter,
+} from "@homesonspec/collectors-common";
+
+/**
+ * Brohn Homes adapter — Austin/Houston/San-Antonio TX site-built spec-home
+ * builder, a Clayton Properties Group brand (slug "brohn-homes"; recon + built
+ * 2026-08-12, TK-10487).
+ *
+ * brohnhomes.com is a WordPress site (Total/wpex theme) that is FULLY
+ * server-rendered — a plain honest-UA GET of a per-home page returns ~230 KB of
+ * complete HTML with every fact inline. NO browser, NO bot-wall, NO XHR — this
+ * is a plain-fetch adapter. (Distinct from its Clayton sibling claytonhomes.com,
+ * whose only feed is robots-DISALLOWED /api/ + manufactured/off-target homes;
+ * Brohn is a genuine site-built spec-inventory site with an open HTML feed.)
+ *
+ * robots.txt (brohnhomes.com) = `User-agent: *` `Disallow: /wp-admin/`
+ * `Allow: /wp-admin/admin-ajax.php`, and declares the sitemap index. The
+ * `/homes/...` per-home pages we read are ALLOWED.
+ *
+ * Sitemap index https://brohnhomes.com/sitemap_index.xml
+ * -> homes-sitemap1.xml + homes-sitemap2.xml = the PER-HOME inventory
+ * (~247 URLs) of the form
+ * /homes/{state}/{metro}/{city}/{community-slug}/{street-address-slug}/
+ * (a leading template row with a literal `%metro%` placeholder is
+ * filtered out). The separate floorplans-sitemapN.xml are plan-level
+ * aggregates (SKIP — not per-home). One detail page == one home.
+ *
+ * Each per-home page carries the facts in the delivered DOM (facts-only):
+ * <h1 class="vcex-page-title__heading"> = street address ("31315 Cass River Lane")
+ * <div class="brohn-home-details__community"> "Community Name <span
+ * class="brohn-home-details__city-state"> | City, ST</span>"
+ * <div class="home-price"> "$283,990" (absent / no $ -> price null)
+ * <div class="brohn-home-stats"> N x <div class="stat"><span
+ * class="stat-number">V</span><span class="stat-label">L</span></div>
+ * with labels beds / baths / cars / story / sq. ft. — `baths` is a single
+ * decimal ("2.5") with half-baths already folded (schema bathsTotal is one
+ * number), `cars` -> garageSpaces, `story` -> stories.
+ * <a class="brohn-get-directions" href="...maps...query={full address}"> — the
+ * authoritative "Street, City, ST ZIP" used to recover city/state/zip
+ * (the ZIP is NOT in the visible city-state span, only in the maps query).
+ * <div class="home-item-status-template"> label — "NOW" -> MOVE_IN_READY,
+ * "SOLD" -> MOVE_IN_READY (a real, now-closed spec home; kept, price intact),
+ * any other label left as UNDER_CONSTRUCTION (no future ready-date field is
+ * published on the page, so estCompletionDate stays null).
+ * <body class="... postid-NNNNN single-homes ..."> — the WordPress post id is
+ * the stable per-home key -> builderInventoryId.
+ * Sales phone "(512) 334-6775" appears in the header CTA (builder-wide number).
+ *
+ * homeType: a final URL segment of `unit-NN` (e.g. .../cross-creek/unit-34/) is a
+ * townhome/condo unit -> TOWNHOME; everything else -> SINGLE_FAMILY.
+ *
+ * NO per-home geo lat/lon is published (the map is an embedded widget with no
+ * coordinates in the HTML) -> left null, exactly like richmond-american /
+ * smith-douglas. NO plan NAME is exposed as a labelled field for the primary
+ * home (the "Floor Plan" control is a CTA button, not a name) -> planName null.
+ * Plain HTTP; facts-only; images OMITTED (mediaRights=NONE).
+ *
+ * Metro scope via BROHN_METRO (comma-list of metro slugs, e.g. "austin,houston";
+ * default = all). Per-run home cap via BROHN_PAGE_LIMIT (default 40).
+ */
+const BUILDER_SLUG = "brohn-homes";
+const ORIGIN = "https://brohnhomes.com";
+const HOME_SITEMAPS = [`${ORIGIN}/homes-sitemap1.xml`, `${ORIGIN}/homes-sitemap2.xml`];
+const METRO_FILTER = (process.env.BROHN_METRO ?? "")
+ .toLowerCase()
+ .split(",")
+ .map((s) => s.trim())
+ .filter(Boolean);
+const PAGE_LIMIT = Number(process.env.BROHN_PAGE_LIMIT ?? "40");
+
+/** A per-home DETAIL URL: /homes/{state}/{metro}/{city}/{community}/{street}/ */
+const HOME_URL_RE =
+ /^https?:\/\/[^/]+\/homes\/[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+\/?$/i;
+
+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 };
+}
+// A positive integer (beds / sqft), or null. 0 / negative / NaN -> null.
+const posInt = (v: unknown): number | null => {
+ const n = typeof v === "number" ? v : typeof v === "string" ? Number(String(v).replace(/[^0-9]/g, "")) : NaN;
+ return Number.isFinite(n) && n > 0 ? Math.trunc(n) : null;
+};
+// A non-negative integer (garages / stories may legitimately be small), or null.
+const nonNegInt = (v: unknown): number | null => {
+ const n = typeof v === "number" ? v : typeof v === "string" ? Number(String(v).replace(/[^0-9]/g, "")) : NaN;
+ return Number.isFinite(n) && n >= 0 ? Math.trunc(n) : null;
+};
+// A positive decimal (baths — half already folded, e.g. 2.5), or null.
+const posDec = (v: unknown): number | null => {
+ const n = typeof v === "number" ? v : typeof v === "string" ? Number(String(v).replace(/[^0-9.]/g, "")) : NaN;
+ return Number.isFinite(n) && n > 0 ? n : null;
+};
+const clean = (v: unknown): string | null => {
+ if (v == null) return null;
+ const s = String(v)
+ .replace(/<[^>]+>/g, " ")
+ .replace(/�?38;|&/g, "&")
+ .replace(/'|'|'/g, "'")
+ .replace(/ /g, " ")
+ .replace(/\s+/g, " ")
+ .trim();
+ return s || null;
+};
+
+/** Percent-decode the get-directions maps `query=` value (facts-only address). */
+function decodeQuery(s: string): string {
+ try {
+ return decodeURIComponent(s.replace(/\+/g, " "));
+ } catch {
+ return s;
+ }
+}
+
+type Status = "MOVE_IN_READY" | "UNDER_CONSTRUCTION";
+type HomeType = "SINGLE_FAMILY" | "TOWNHOME";
+
+interface ParsedHome {
+ street: string | null;
+ city: string | null;
+ state: string | null;
+ zip: string | null;
+ community: string | null;
+ price: number | null;
+ priceRaw: string | null;
+ beds: number | null;
+ bathsTotal: number | null;
+ garages: number | null;
+ stories: number | null;
+ sqft: number | null;
+ status: Status;
+ statusRaw: string | null;
+ homeType: HomeType;
+ phone: string | null;
+ builderInventoryId: string | null;
+}
+
+/**
+ * Parse ONE Brohn Homes per-home detail page into a home, or null if the page
+ * has no home-details block (a non-detail page slipped through). Exported for
+ * the self-test / fixtures.
+ */
+export function parseHome(html: string, pageUrl: string): ParsedHome | null {
+ // Only accept genuine single-homes post pages.
+ const bodyClass = html.match(/<body[^>]*class="([^"]*)"/i)?.[1] ?? "";
+ if (!/single-homes/.test(bodyClass) && !/brohn-home-stats/.test(html)) return null;
+
+ // Street address: the vcex page-title heading text node.
+ const street =
+ clean(html.match(/vcex-page-title__heading[^>]*>([\s\S]*?)<\/h1>/i)?.[1]) ??
+ clean(html.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i)?.[1]);
+ if (!street) return null;
+
+ // Full "Street, City, ST ZIP" from the get-directions maps query — the only
+ // place the ZIP is emitted. The maps `query=` sits in the anchor href, which
+ // comes BEFORE the class="brohn-get-directions" in the DOM, so match the maps
+ // search URL directly and require the anchor to carry that class. Fall back to
+ // the visible city-state span for city/state when no directions link exists.
+ const dirMatch = html.match(
+ /<a\b[^>]*\/maps\/search\/\?[^"']*?query=([^"'&]+)[^>]*brohn-get-directions/i,
+ );
+ const q = dirMatch?.[1] ?? html.match(/\/maps\/search\/\?[^"']*?query=([^"'&]+)/i)?.[1];
+ const full = q ? decodeQuery(q) : null;
+ let city: string | null = null;
+ let state: string | null = null;
+ let zip: string | null = null;
+ if (full) {
+ const m = full.match(/^(.*?),\s*(.+?),\s*([A-Za-z]{2})\s*(\d{5})/);
+ if (m) {
+ city = clean(m[2]);
+ state = normalizeStateCode(m[3] ?? null);
+ zip = m[4] ?? null;
+ }
+ }
+ if (!city || !state) {
+ // "Community Name <span class="brohn-home-details__city-state"> | City, ST</span>"
+ const cs = html.match(/brohn-home-details__city-state[^>]*>([\s\S]*?)<\/span>/i)?.[1];
+ const csText = clean(cs)?.replace(/^\|\s*/, "") ?? null;
+ const m = csText?.match(/^(.*?),\s*([A-Za-z]{2})$/);
+ if (m) {
+ city = city ?? clean(m[1]);
+ state = state ?? normalizeStateCode(m[2] ?? null);
+ }
+ }
+
+ // Community: the text node preceding the city-state span inside the header.
+ const community = clean(
+ html.match(/brohn-home-details__community[^>]*>\s*([^<]+?)\s*<span/i)?.[1],
+ );
+
+ // Price: .home-price -> "$NNN,NNN". Absent / non-$ -> null (correct).
+ const priceRaw = clean(html.match(/home-price[^>]*>([\s\S]*?)<\/div>/i)?.[1]);
+ const priceMatch = priceRaw?.match(/\$\s*([0-9][0-9,]{2,})/);
+ const price = priceMatch ? posInt(priceMatch[1]) : null;
+
+ // Stats block — scope to brohn-home-stats so sibling cards never leak in.
+ const statsScope = html.match(/brohn-home-stats[^>]*>([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/i)?.[1] ?? "";
+ const stats: Record<string, string> = {};
+ for (const m of statsScope.matchAll(
+ /stat-number[^>]*>\s*([0-9,.]+)\s*<\/span>\s*<span[^>]*stat-label[^>]*>\s*([^<]+?)\s*<\/span>/gi,
+ )) {
+ stats[m[2]!.toLowerCase().trim()] = m[1]!.replace(/,/g, "");
+ }
+ const beds = posInt(stats["beds"] ?? null);
+ const bathsTotal = posDec(stats["baths"] ?? null);
+ const garages = nonNegInt(stats["cars"] ?? null);
+ const stories = nonNegInt(stats["story"] ?? stats["stories"] ?? null);
+ const sqft = posInt(stats["sq. ft."] ?? stats["sqft"] ?? stats["sq ft"] ?? null);
+
+ // Status label.
+ const statusRaw = clean(html.match(/home-item-status-template[^>]*>([^<]+)</i)?.[1]);
+ // "NOW"/"SOLD" -> a real standing/closed home (MOVE_IN_READY); anything else
+ // (a future label) -> UNDER_CONSTRUCTION. No ready-date field is on the page.
+ const status: Status = statusRaw && /^(now|sold|move[- ]?in)/i.test(statusRaw)
+ ? "MOVE_IN_READY"
+ : "UNDER_CONSTRUCTION";
+
+ // homeType: a `unit-NN` final URL segment is a townhome/condo unit.
+ const homeType: HomeType = /\/unit-\d+\/?$/i.test(pageUrl) ? "TOWNHOME" : "SINGLE_FAMILY";
+
+ // Sales phone (builder-wide CTA number).
+ const ph = html.match(/\((\d{3})\)\s*(\d{3})-(\d{4})/);
+ const phone = ph ? `(${ph[1]}) ${ph[2]}-${ph[3]}` : null;
+
+ // Stable per-home id: the WordPress body-class postid. Anchor to the <body>
+ // class attribute — a `.postid-NNN` also appears inside CSS <style> rules, so
+ // a naive global match would grab the wrong id.
+ const postId = bodyClass.match(/\bpostid-(\d+)\b/)?.[1] ?? null;
+ const builderInventoryId = postId ? `postid-${postId}` : null;
+
+ return {
+ street,
+ city,
+ state,
+ zip,
+ community,
+ price,
+ priceRaw: priceMatch ? priceMatch[0] : priceRaw,
+ beds,
+ bathsTotal,
+ garages,
+ stories,
+ sqft,
+ status,
+ statusRaw,
+ homeType,
+ phone,
+ builderInventoryId,
+ };
+}
+
+export const brohnHomesAdapter: SourceAdapter = {
+ key: "brohn-homes-site",
+ version: "0.1.0",
+
+ async *fetch(ctx: FetchContext): AsyncIterable<RawPage> {
+ if (ctx.mode === "fixture") {
+ yield* fetchFixtures(ctx);
+ return;
+ }
+ const fetcher = new LiveFetcher(ctx.registry);
+ let homeUrls: string[] = [];
+ for (const sm of HOME_SITEMAPS) {
+ try {
+ const index = await fetcher.fetch(sm);
+ homeUrls.push(
+ ...[...index.body.toString("utf8").matchAll(/<loc>\s*([^<\s]+)\s*<\/loc>/g)]
+ .map((m) => m[1]!)
+ .filter((u) => HOME_URL_RE.test(u) && !u.includes("%")),
+ );
+ } catch (error) {
+ console.warn(` skip sitemap ${sm}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ // dedupe, preserve order
+ homeUrls = [...new Set(homeUrls)];
+ if (METRO_FILTER.length) {
+ homeUrls = homeUrls.filter((u) => {
+ const metro = u.match(/\/homes\/[^/]+\/([^/]+)\//)?.[1]?.toLowerCase();
+ return metro ? METRO_FILTER.includes(metro) : false;
+ });
+ }
+ for (const url of homeUrls.slice(0, PAGE_LIMIT)) {
+ try {
+ yield await fetcher.fetch(url);
+ } catch (error) {
+ console.warn(` skip ${url}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+ },
+
+ extract(page: RawPage): ExtractionOutput {
+ try {
+ const html = page.body.toString("utf8");
+ const h = parseHome(html, page.url);
+ if (!h || !h.street) return { records: [], errors: [] }; // not a resolvable detail page
+
+ const cname = h.community ?? "Unknown";
+ const records: ExtractedRecord[] = [];
+
+ // Community FIRST — publish creates the FK target the home needs. No
+ // per-home geo is published; the sales phone is builder-wide so it is
+ // carried on the community as well as the home for convenience.
+ records.push({
+ entityType: "community",
+ canonicalHints: { builderSlug: BUILDER_SLUG, communityName: cname },
+ fields: {
+ name: fv(cname, cname, page.url),
+ street: fv<string>(null, null, page.url),
+ city: fv(h.city, h.city, page.url),
+ state: fv(h.state, h.state, page.url),
+ zip: fv(h.zip, h.zip, page.url),
+ county: fv<string>(null, null, page.url),
+ metro: fv<string>(null, null, page.url),
+ lat: fv<number>(null, null, page.url),
+ lon: fv<number>(null, null, page.url),
+ hoaFeeMonthly: fv<number>(null, null, page.url),
+ schoolDistrict: fv<string>(null, null, page.url),
+ ageRestricted: fv<boolean>(null, null, page.url),
+ salesPhone: fv(h.phone, h.phone, page.url),
+ },
+ });
+
+ records.push({
+ entityType: "inventory_home",
+ canonicalHints: {
+ builderSlug: BUILDER_SLUG,
+ communityName: cname,
+ address: h.street,
+ builderInventoryId: h.builderInventoryId ?? page.url,
+ },
+ fields: {
+ street: fv(h.street, h.street, page.url),
+ city: fv(h.city, h.city, page.url),
+ state: fv(h.state, h.state, page.url),
+ zip: fv(h.zip, h.zip, page.url),
+ price: fv(h.price, h.priceRaw, page.url, h.priceRaw ? `price ${h.priceRaw}` : null),
+ beds: fv(h.beds, h.beds != null ? String(h.beds) : null, page.url),
+ bathsTotal: fv(h.bathsTotal, h.bathsTotal != null ? String(h.bathsTotal) : null, page.url),
+ sqft: fv(h.sqft, h.sqft != null ? String(h.sqft) : null, page.url),
+ stories: fv(h.stories, h.stories != null ? String(h.stories) : null, page.url),
+ garageSpaces: fv(h.garages, h.garages != null ? String(h.garages) : null, page.url),
+ homeType: fv(h.homeType as never, null, page.url, `Brohn Homes ${h.homeType.toLowerCase()} inventory home`),
+ constructionStatus: fv(
+ h.status as never,
+ h.statusRaw,
+ page.url,
+ h.statusRaw ? `status "${h.statusRaw}"` : "no status label on page",
+ ),
+ estCompletionDate: fv<string>(null, null, page.url), // no ready-date field on the page
+ lotNumber: fv<string>(null, null, page.url),
+ builderInventoryId: fv(h.builderInventoryId, h.builderInventoryId, page.url),
+ // per-home geo NOT published by Brohn — left null (like richmond-american / smith-douglas)
+ lat: fv<number>(null, null, page.url),
+ lon: fv<number>(null, null, page.url),
+ planName: fv<string>(null, null, page.url), // no labelled plan name for the primary home
+ // Facts-only: images intentionally omitted (mediaRights=NONE).
+ images: fv<string[]>([], null, page.url),
+ },
+ });
+
+ return { records, errors: [] };
+ } catch (error) {
+ return { records: [], errors: [{ url: page.url, reason: String(error) }] };
+ }
+ },
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7094ba6f..41daa0a0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -215,6 +215,9 @@ importers:
'@homesonspec/collector-beazer':
specifier: workspace:*
version: link:../../collectors/beazer
+ '@homesonspec/collector-brohn-homes':
+ specifier: workspace:*
+ version: link:../../collectors/brohn-homes
'@homesonspec/collector-brookfield':
specifier: workspace:*
version: link:../../collectors/brookfield
@@ -245,6 +248,9 @@ importers:
'@homesonspec/collector-gl-homes':
specifier: workspace:*
version: link:../../collectors/gl-homes
+ '@homesonspec/collector-goodall-homes':
+ specifier: workspace:*
+ version: link:../../collectors/goodall-homes
'@homesonspec/collector-highland-homes':
specifier: workspace:*
version: link:../../collectors/highland-homes
@@ -278,6 +284,9 @@ importers:
'@homesonspec/collector-mi-homes':
specifier: workspace:*
version: link:../../collectors/mi-homes
+ '@homesonspec/collector-oakwood-homes':
+ specifier: workspace:*
+ version: link:../../collectors/oakwood-homes
'@homesonspec/collector-perry-homes':
specifier: workspace:*
version: link:../../collectors/perry-homes
← e10bc5b7 oakwood-homes: facts-only inventory adapter (Nuxt SSR + KOVA
·
back to Homesonspec
·
TK-10487: oakwood constructionStatus prefers authoritative K 4c9e0afc →