/** * Builds data/knowledge-base.json from the catalog's `replaces` lists. * * Each competitor string named by a product becomes one entry mapped to every * product that claims to displace it. Scores start at neutral defaults and are * refined automatically as real assessments are saved. * * Re-running is safe: existing entries keep their edits, observations and pins, * and only gain newly-discovered product mappings. */ import { readFileSync, writeFileSync, existsSync } from "node:fs"; const CATALOG = "data/catalog.json"; const KB = "data/knowledge-base.json"; const MULTIWORD_VENDORS = [ "Red Hat", "Palo Alto", "Check Point", "Micro Focus", "Trend Micro", "Arctic Wolf", "Red Canary", "Ping Identity", "One Identity", "Salt Security", "Noname Security", "Traceable AI", "Aqua Security", "Airlock Digital", "Abnormal Security", "Menlo Security", "Recorded Future", "Scale Computing", "Wind River", "Software AG", "Cato Networks", "Zero Networks", "Digital Guardian", "Snow Software", "Cardinal Commerce", "Duo Security", "Crunchy Data", "Morpheus Data", "Sumo Logic", "Prisma Cloud", "Amazon Web Services", "Google Cloud", "Data Theorem", ]; const splitVendor = (label) => { const clean = label.replace(/\s+/g, " ").trim(); for (const v of MULTIWORD_VENDORS) { if (clean.toLowerCase().startsWith(v.toLowerCase() + " ")) return { vendor: v, product: clean.slice(v.length).trim() }; if (clean.toLowerCase() === v.toLowerCase()) return { vendor: v, product: "" }; } const parts = clean.split(" "); return parts.length === 1 ? { vendor: clean, product: "" } : { vendor: parts[0], product: parts.slice(1).join(" ") }; }; const NON_COMPETITIVE = [ /\(status quo\)/i, /\(internal alternative\)/i, /status quo/i, /\bDIY\b/i, /\bscripts?\b/i, /spreadsheets/i, /\bmanual\b/i, /^custom /i, /^native /i, /^internal /i, /^self-managed/i, /^unsupported/i, /^physical /i, /^consulting/i, /^fragmented/i, /^serialized/i, /^shared /i, /^point[- ]tool/i, /^point IAM/i, /^no\b/i, /^none\b/i, /^password-only/i, /^display statements/i, /^unencrypted/i, /^green[- ]screen/i, /^cron\b/i, /rewrite|redevelopment/i, /^migration to /i, /^rehost/i, /^low-code platforms/i, /^multiple /i, /^various /i, /^hand-written/i, /^ISPF /i, /^unsupported community/i, ]; const isCompetitive = (label) => !NON_COMPETITIVE.some((re) => re.test(label)); const kbId = (label) => label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72) || "entry"; const catalog = JSON.parse(readFileSync(CATALOG, "utf8")); const existing = existsSync(KB) ? JSON.parse(readFileSync(KB, "utf8")) : { version: 1, entries: [] }; const byId = new Map(existing.entries.map((e) => [e.id, e])); const now = new Date().toISOString(); let created = 0, enriched = 0; for (const p of catalog.products) { for (const raw of p.replaces ?? []) { const label = raw.replace(/\s+/g, " ").trim(); if (!label) continue; const id = kbId(label); let e = byId.get(id); if (!e) { const { vendor, product } = splitVendor(label); e = { id, label, vendor, product, aliases: [], domain: p.domain, productIds: [], capability: 4, switchEase: 3, timeToRoiMonths: 12, savingsPct: 50, migrationCost: 0, unitLabel: "", risks: "", notes: "", competitive: isCompetitive(label), pinned: false, origin: "catalog", observations: [], createdAt: now, updatedAt: now, }; byId.set(id, e); created++; } // Only add mappings; never strip a mapping a human curated. if (!e.productIds.includes(p.id)) { e.productIds.push(p.id); if (e.origin === "catalog") enriched++; } if (!e.domain) e.domain = p.domain; } } const entries = [...byId.values()].sort((a, b) => a.label.localeCompare(b.label)); writeFileSync(KB, JSON.stringify({ version: 1, updatedAt: now, entries }, null, 2) + "\n"); // Backfill the flag for entries written before it existed. for (const e of entries) if (e.competitive === undefined) e.competitive = isCompetitive(e.label); const mapped = entries.filter((e) => e.productIds.length > 0).length; const rivals = entries.filter((e) => e.competitive).length; console.log(`knowledge base: ${entries.length} entries (${created} new), ${mapped} mapped, ${rivals} true competitors, ${enriched} mappings added`);