← back to Homesonspec

collectors/chafin-communities/selftest-live.mts

84 lines

/**
 * Bounded live self-test — chafin-communities adapter.
 * Fetches up to 3 community pages, extracts records, reports field coverage.
 * Pass criterion: ≥6/10 core field coverage (price, beds, baths, lot, id, city,
 * state, status) across all homes found.
 *
 * Run:
 *   CHAFIN_COMMUNITY_LIMIT=3 node_modules/.bin/tsx selftest-live.mts
 */

process.env["CHAFIN_COMMUNITY_LIMIT"] = "3";
process.env["CHAFIN_HOME_LIMIT"] = "20";

import { chafinCommunitiesAdapter } from "./src/index.js";
import type { SourceRegistryEntry } from "@homesonspec/collectors-common";

const registry: SourceRegistryEntry = {
  key: "chafin-communities-site",
  collectionMethod: "CRAWL",
  rateLimitRpm: 10,
  mediaRights: "NONE",
};

const ctx = { mode: "live" as const, registry };

let pages = 0;
let communityRecords = 0;
let homeRecords = 0;
let errors = 0;

const cov = {
  price: 0, beds: 0, baths: 0, lot: 0, id: 0,
  city: 0, state: 0, status: 0,
};

console.log("[chafin-live] starting bounded crawl (limit=3 communities)...");

for await (const page of chafinCommunitiesAdapter.fetch(ctx)) {
  pages++;
  const out = chafinCommunitiesAdapter.extract(page);
  errors += out.errors.length;
  for (const err of out.errors) console.warn("  error:", err);
  for (const rec of out.records) {
    if (rec.entityType === "community") {
      communityRecords++;
    } else if (rec.entityType === "inventory_home") {
      homeRecords++;
      const f = rec.fields as Record<string, { value: unknown }>;
      if (f["price"]?.value != null) cov.price++;
      if (f["beds"]?.value != null) cov.beds++;
      if (f["bathsTotal"]?.value != null) cov.baths++;
      if (f["lotNumber"]?.value != null) cov.lot++;
      if (f["builderInventoryId"]?.value != null) cov.id++;
      if (f["city"]?.value != null) cov.city++;
      if (f["state"]?.value != null) cov.state++;
      if (f["constructionStatus"]?.value != null) cov.status++;
    }
  }
  console.log(
    `  page ${pages}: ${page.url} → ${out.records.filter((r) => r.entityType === "inventory_home").length} homes`,
  );
}

const n = homeRecords;
function pct(v: number) { return n ? `${Math.round(100 * v / n)}%` : "n/a"; }

console.log(`\n[chafin-live] RESULTS`);
console.log(`  pages=${pages}  communities=${communityRecords}  homes=${n}  errors=${errors}`);
console.log(`  price=${pct(cov.price)}  beds=${pct(cov.beds)}  baths=${pct(cov.baths)}`);
console.log(`  lot=${pct(cov.lot)}  id=${pct(cov.id)}  city=${pct(cov.city)}  state=${pct(cov.state)}  status=${pct(cov.status)}`);

// Pass criterion: at least some homes found + 6/8 fields at >50%
const fieldPcts = Object.values(cov).map((v) => (n ? v / n : 0));
const above50 = fieldPcts.filter((p) => p > 0.5).length;
if (n === 0) {
  console.error("\nFAIL: no homes found (site may be blocking; check manually)");
  process.exit(1);
} else if (above50 < 6) {
  console.error(`\nFAIL: only ${above50}/8 fields above 50% coverage (need 6)`);
  process.exit(1);
} else {
  console.log(`\nPASS: ${n} homes, ${above50}/8 fields >50% coverage`);
}