← 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
A scripts/audit-fields.mjs
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 →