← back to Dw Fleet Registry
build-registry.mjs
207 lines
#!/usr/bin/env node
// Canonical DW fleet registry generator.
// Reconciles the two DW website fleets into ONE source-of-truth map:
// Fleet 1 — local "sister site" microsites in ~/Projects (standalone Node, per-dir repo)
// Fleet 2 — dw-domain-fleet manifest domains (shared multi-tenant server, per-site JSON)
// Output: dw-fleet-registry.json (one row per DOMAIN, deduped, overlaps flagged)
//
// Read-only scan. Re-runnable: same inputs -> same registry (plus a fresh timestamp).
// Decision provenance: DTD verdict 2026-06-01 (Claude+Codex => A, consolidate the
// control plane only; serving methods stay pluggable).
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const PROJECTS = path.join(os.homedir(), 'Projects');
const MANIFEST_DIR = path.join(PROJECTS, 'dw-domain-fleet', 'sites');
// A ~/Projects dir is a Fleet-1 customer site if its name reads like a DW property
// AND it carries a real site marker — and it isn't tooling/scraper/admin scaffolding.
const DW_NAME = /(wallpaper|wallcover|wallco|silk|cork|grasscloth|linen|raffia|glitter|flocked|mohair|suede|chinoiserie|damask|toile|mural|hospitality|healthcare|novasuede|philipperomano|romano|architectural|setdecorator|brandstem|jute|mica|mylar|metallic|goldleaf|silverleaf|glassbeaded|embroidered|handcrafted|screenprinted|blockprinted|textile|string|pastel|saloon|recycled|restoration|museum|vintage|aged)/i;
const EXCLUDE = /^(dw-|_)|scraper|viewer|admin|monitoring|launcher|boardroom|war-room|universe|design-bridge|theme-toggle|settlement-audit|product-manager|pairs-well|hero-admin|collections|nextjs/i;
const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };
const exists = (p) => { try { fs.accessSync(p); return true; } catch { return false; } };
// Slugs whose real domain isn't slug+".com" (dir name != domain).
const DOMAIN_OVERRIDES = { 'wallco-ai': 'wallco.ai' };
// Dirs the DW_NAME regex matches but that are NOT customer sites (false positives).
const NOT_A_SITE = new Set(['animate-museum-posts']);
const inferDomain = (slug) => DOMAIN_OVERRIDES[slug] || (slug.includes('.') ? slug.toLowerCase() : slug.toLowerCase() + '.com');
// ── Fleet 1: local microsites ──────────────────────────────────────────────
function scanLocal() {
const out = [];
let dirs = [];
try { dirs = fs.readdirSync(PROJECTS, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name); }
catch { return out; }
for (const name of dirs) {
if (EXCLUDE.test(name) || NOT_A_SITE.has(name) || !DW_NAME.test(name)) continue;
const dir = path.join(PROJECTS, name);
const hasPublicIndex = exists(path.join(dir, 'public', 'index.html'));
const hasServer = exists(path.join(dir, 'server.js'));
const cfg = readJson(path.join(dir, 'site.config.json'));
// require a real site marker so we don't pick up stray dirs
if (!hasPublicIndex && !hasServer && !cfg) continue;
const domain = (cfg && cfg.domain) ? String(cfg.domain).toLowerCase() : inferDomain(name);
out.push({
domain,
slug: name,
siteName: (cfg && (cfg.siteName || cfg.heroHeadline)) || name,
servingMode: 'standalone-node',
repoPath: dir,
hasGit: exists(path.join(dir, '.git')),
hasPublicIndex,
hasServer,
hasSiteConfig: !!cfg,
domainInferred: !(cfg && cfg.domain),
port: (cfg && cfg.port) || null,
theme: (cfg && cfg.theme) || null,
});
}
return out;
}
// ── Fleet 2: dw-domain-fleet manifest ──────────────────────────────────────
function scanManifest() {
const out = [];
let files = [];
try { files = fs.readdirSync(MANIFEST_DIR).filter(f => f.endsWith('.json')); }
catch { return out; }
for (const f of files) {
const cfg = readJson(path.join(MANIFEST_DIR, f));
if (!cfg) continue;
const slug = cfg.slug || f.replace(/\.json$/, '');
const domain = (cfg.domain ? String(cfg.domain) : inferDomain(slug)).toLowerCase();
out.push({
domain,
slug,
siteName: cfg.siteName || slug,
servingMode: 'shared-multitenant',
configPath: path.join(MANIFEST_DIR, f),
port: cfg.port || null,
siteEmail: cfg.siteEmail || null,
hasLocalDir: exists(path.join(PROJECTS, slug)),
theme: cfg.theme || null,
});
}
return out;
}
// ── Reconcile by domain ─────────────────────────────────────────────────────
function reconcile(local, manifest) {
const byDomain = new Map();
const put = (row, fleet) => {
const key = row.domain;
if (!byDomain.has(key)) {
byDomain.set(key, { domain: key, fleets: [], local: null, manifest: null });
}
const e = byDomain.get(key);
if (!e.fleets.includes(fleet)) e.fleets.push(fleet);
if (fleet === 'local-microsite') e.local = row;
else e.manifest = row;
};
local.forEach(r => put(r, 'local-microsite'));
manifest.forEach(r => put(r, 'domain-fleet-manifest'));
const rows = [...byDomain.values()].map(e => {
const overlap = e.fleets.length > 1;
const primary = e.local || e.manifest;
return {
domain: e.domain,
slug: primary.slug,
siteName: primary.siteName,
fleets: e.fleets,
overlap, // appears in BOTH systems — needs a canonical-owner decision
servingModes: [e.local && 'standalone-node', e.manifest && 'shared-multitenant'].filter(Boolean),
canonicalOwner: overlap ? 'UNRESOLVED' : (e.local ? 'local-microsite' : 'domain-fleet-manifest'),
local: e.local,
manifest: e.manifest,
};
}).sort((a, b) => a.domain.localeCompare(b.domain));
return rows;
}
// ── Near-duplicate clustering ───────────────────────────────────────────────
// Exact-domain overlap is rare across the two fleets, but NEAR-duplicates are
// common (grassclothwallcovering.com vs grassclothwallpaper.com). Strip the
// DW suffix tokens to a "concept key" and group domains that share it — these
// are the real consolidation candidates Steve cares about.
function conceptKey(slug) {
return String(slug).toLowerCase()
.replace(/\.(com|net|org)$/, '')
.replace(/(wallcoverings?|wallcover|wallpapers?|walls?|wallco|coverings?)/g, '')
.replace(/[^a-z0-9]/g, '');
}
function nearDuplicates(rows) {
const byConcept = new Map();
for (const r of rows) {
const k = conceptKey(r.slug);
if (!k) continue;
if (!byConcept.has(k)) byConcept.set(k, []);
byConcept.get(k).push(r.domain);
}
const clusters = {};
for (const [k, doms] of byConcept) {
if (doms.length > 1) clusters[k] = doms.sort();
}
return clusters;
}
// Near-dup canonical owners — DTD verdict C (2026-06-01, Claude+Codex unanimous):
// consumer niches canonicalize to the "wallpaper" domain, trade/contract niches to
// "wallcovering". Maps concept -> canonical domain. raffia already 301s in prod.
const CANONICAL = {
silk: 'silkwallpaper.com',
carmel: 'carmelwallpaper.com',
flocked: 'flockedwallpaper.com',
hospitality: 'hospitalitywallcoverings.com',
hollywood: 'hollywoodwallcoverings.com',
raffia: 'raffiawallcoverings.com',
};
const local = scanLocal();
const manifest = scanManifest();
const rows = reconcile(local, manifest);
const overlaps = rows.filter(r => r.overlap);
const nearDup = nearDuplicates(rows);
// Annotate each near-dup member with its cluster's canonical owner + redirect target.
for (const r of rows) {
const k = conceptKey(r.slug);
if (nearDup[k] && CANONICAL[k]) {
r.clusterCanonical = CANONICAL[k];
r.isCanonical = (r.domain === CANONICAL[k]);
r.shouldRedirectTo = r.isCanonical ? null : CANONICAL[k];
}
}
const registry = {
$schema: 'dw-fleet-registry/v1',
generatedAt: new Date().toISOString(),
provenance: 'DTD verdict 2026-06-01 — consolidate to one canonical registry (control plane only; serving methods pluggable).',
summary: {
totalDomains: rows.length,
localMicrosites: local.length,
manifestDomains: manifest.length,
overlappingDomains: overlaps.length,
unresolvedOwners: overlaps.length,
nearDuplicateClusters: Object.keys(nearDup).length,
},
overlaps: overlaps.map(o => o.domain),
nearDuplicates: nearDup,
canonicalResolutions: CANONICAL, // DTD verdict C — concept -> canonical domain
sites: rows,
};
const outPath = path.join(path.dirname(new URL(import.meta.url).pathname), 'dw-fleet-registry.json');
fs.writeFileSync(outPath, JSON.stringify(registry, null, 2) + '\n');
console.log(`Wrote ${outPath}`);
console.log(` total domains : ${registry.summary.totalDomains}`);
console.log(` local sites : ${registry.summary.localMicrosites}`);
console.log(` manifest : ${registry.summary.manifestDomains}`);
console.log(` OVERLAPS : ${registry.summary.overlappingDomains}${overlaps.length ? ' -> ' + overlaps.map(o=>o.domain).join(', ') : ''}`);
console.log(` near-dup sets : ${registry.summary.nearDuplicateClusters}`);
for (const [k, doms] of Object.entries(nearDup)) console.log(` · ${k}: ${doms.join(' <--> ')}`);