← 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('  <-->  ')}`);