[object Object]

← back to Homesonspec

auto-save: 2026-08-02T17:46:59 (1 files) — scripts/audit-fields.mjs

3c8b733cb4cba7832dcfaf785d6f7ad4cdd37081 · 2026-08-02 17:47:05 -0700 · Steve Abrams

Files touched

Diff

commit 3c8b733cb4cba7832dcfaf785d6f7ad4cdd37081
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 2 17:47:05 2026 -0700

    auto-save: 2026-08-02T17:46:59 (1 files) — scripts/audit-fields.mjs
---
 scripts/audit-fields.mjs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 75 insertions(+)

diff --git a/scripts/audit-fields.mjs b/scripts/audit-fields.mjs
new file mode 100644
index 00000000..8115b9d7
--- /dev/null
+++ b/scripts/audit-fields.mjs
@@ -0,0 +1,75 @@
+// Offline spec-field auditor — enumerates the FULL field universe a builder
+// exposes in a raw snapshot and diffs it against the fields we currently
+// capture, so "capture every spec they offer" has a concrete work-list.
+//
+// $0, no network: reads a var/snapshots/<builder>/<hash>.html|json file that
+// the crawler already stored. Tries every known blob shape (raw JSON,
+// __NEXT_DATA__, `var model=`/`var allMIRs=`, RSC __next_f) and unions keys.
+//
+// Usage: node scripts/audit-fields.mjs <snapshot-file>
+//   → prints JSON { total, specCandidates: [...], sample: {k:v} }
+import fs from "node:fs";
+
+const CAPTURED = new Set([
+  "price", "originalprice", "beds", "bedrooms", "numberofbedrooms",
+  "bathstotal", "bathrooms", "numberofbathrooms", "sqft", "squarefeet",
+  "squarefootage", "stories", "numberofstories", "garage", "garages",
+  "garagespaces", "numberofgarages", "lat", "latitude", "lon", "longitude",
+  "street", "address", "address1", "city", "state", "zip", "zipcode",
+  "images", "image", "thumbnail", "status", "hometype", "lotnumber",
+  "estcompletiondate", "availabilitystatus",
+]);
+// UI/layout/plumbing keys that are not buyer-facing specs.
+const NOISE = /^(font|color|background|badge|brand|block|config|guid|itemid|isnull|isglobal|isdisabled|ispending|sortorder|sortingtype|htmlitems|totalitems|queryparam|querystring|modaltext|purchaseurl|url|text|name|thumbnail|image|images|multigenimage|configurationid|divisionid|jde|stagecode)/i;
+
+function collectKeys(obj, seen, depth = 0) {
+  if (!obj || typeof obj !== "object" || depth > 8) return;
+  for (const k of Object.keys(obj)) {
+    const v = obj[k];
+    if (v === null || typeof v !== "object") {
+      if (!seen.has(k)) seen.set(k, v);
+    }
+    collectKeys(v, seen, depth + 1);
+  }
+}
+
+function tryParse(s) {
+  try { return JSON.parse(s); } catch { return null; }
+}
+
+function extractBlobs(raw) {
+  const blobs = [];
+  // 1. whole file is JSON (a stored API response)
+  const whole = tryParse(raw);
+  if (whole) blobs.push(whole);
+  // 2. __NEXT_DATA__
+  const nd = raw.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
+  if (nd) { const o = tryParse(nd[1]); if (o) blobs.push(o); }
+  // 3. var model = {...}; / var allMIRs = "..."(double-encoded) / var X = {...}
+  for (const m of raw.matchAll(/var\s+\w+\s*=\s*(\{[\s\S]*?\}|"(?:\\.|[^"\\])*")\s*;/g)) {
+    let s = m[1];
+    if (s[0] === '"') { const inner = tryParse(s); if (typeof inner === "string") s = inner; }
+    const o = tryParse(s);
+    if (o && typeof o === "object") blobs.push(o);
+  }
+  // 4. RSC __next_f pushes: self.__next_f.push([1,"...json..."])
+  for (const m of raw.matchAll(/self\.__next_f\.push\(\[1,\s*"([\s\S]*?)"\]\)/g)) {
+    const unescaped = m[1].replace(/\\"/g, '"').replace(/\\n/g, "\n");
+    for (const jm of unescaped.matchAll(/\{[^{}]*"(?:display_price|min_price|home_status|squareFeet)"[^{}]*\}/g)) {
+      const o = tryParse(jm[0]); if (o) blobs.push(o);
+    }
+  }
+  return blobs;
+}
+
+const file = process.argv[2];
+if (!file || !fs.existsSync(file)) { console.log(JSON.stringify({ error: "no file", file })); process.exit(0); }
+const raw = fs.readFileSync(file, "utf8");
+const seen = new Map();
+for (const b of extractBlobs(raw)) collectKeys(b, seen);
+const specCandidates = [...seen.keys()]
+  .filter((k) => !CAPTURED.has(k.toLowerCase()) && !NOISE.test(k) && k.length > 1 && !/^\d+$/.test(k))
+  .sort();
+const sample = {};
+for (const k of specCandidates.slice(0, 12)) sample[k] = seen.get(k);
+console.log(JSON.stringify({ total: seen.size, specCount: specCandidates.length, specCandidates, sample }));

← 478e1c73 docs(specs): all-specs capture audit — method + dr-horton re  ·  back to Homesonspec  ·  feat(specs): offline field auditor + fleet pass-1 results (a d46a0ce9 →