326 lines
15 KiB
JavaScript
326 lines
15 KiB
JavaScript
/**
|
|
* Block 2 verification: canonicalization + fragmentToQuery.
|
|
* Run without a database -- pure logic checks.
|
|
*
|
|
* node scripts/verify-block2.mjs
|
|
*/
|
|
|
|
// ---- inline the canonicalization logic (mirrors cell.ts) ----
|
|
|
|
const INDUSTRY_TO_VERTICAL = {
|
|
"healthcare-medical": "healthcare",
|
|
"healthcare-dental": "dental",
|
|
"healthcare-mental-health": "mental-health",
|
|
"healthcare-veterinary": "veterinary",
|
|
"healthcare-other": "healthcare",
|
|
"b2b-saas": "b2b-saas",
|
|
"b2b-services": "b2b-services",
|
|
"b2b-manufacturing": "b2b-manufacturing",
|
|
"b2b-finance": "finance",
|
|
"b2b-legal": "legal",
|
|
"b2b-other": "b2b-services",
|
|
"ecommerce": "ecommerce",
|
|
"ecommerce-fashion": "ecommerce",
|
|
"ecommerce-beauty": "ecommerce",
|
|
"ecommerce-electronics": "ecommerce",
|
|
"ecommerce-home": "ecommerce",
|
|
"ecommerce-sports": "ecommerce",
|
|
"ecommerce-food": "ecommerce",
|
|
"ecommerce-other": "ecommerce",
|
|
"local-services": "local-services",
|
|
"professional-services": "professional-services",
|
|
"finance": "finance",
|
|
"legal": "legal",
|
|
"real-estate": "real-estate",
|
|
"nonprofit": "nonprofit",
|
|
"hospitality": "hospitality",
|
|
"higher-ed": "higher-ed",
|
|
"other": "other",
|
|
};
|
|
|
|
const LOCAL_VERTICALS = new Set([
|
|
"dental", "healthcare", "mental-health", "veterinary",
|
|
"local-services", "real-estate", "hospitality", "legal", "professional-services",
|
|
]);
|
|
|
|
const METRO_ALIASES = {
|
|
"new york": "new-york", "new york city": "new-york", "nyc": "new-york",
|
|
"los angeles": "los-angeles", "la": "los-angeles",
|
|
"chicago": "chicago", "houston": "houston", "phoenix": "phoenix",
|
|
"philadelphia": "philadelphia", "san antonio": "san-antonio",
|
|
"san diego": "san-diego", "dallas": "dallas", "san jose": "san-jose",
|
|
"austin": "austin", "charlotte": "charlotte",
|
|
"san francisco": "san-francisco", "sf": "san-francisco",
|
|
"denver": "denver", "boston": "boston", "seattle": "seattle",
|
|
"atlanta": "atlanta", "miami": "miami", "raleigh": "raleigh",
|
|
"nashville": "nashville", "minneapolis": "minneapolis",
|
|
};
|
|
|
|
function canonicalizeVertical(industry) {
|
|
if (!industry) return "other";
|
|
const key = industry.toLowerCase().trim();
|
|
if (INDUSTRY_TO_VERTICAL[key]) return INDUSTRY_TO_VERTICAL[key];
|
|
const stripped = key.replace(/[-\s]/g, "");
|
|
for (const [k, v] of Object.entries(INDUSTRY_TO_VERTICAL)) {
|
|
if (k.replace(/[-\s]/g, "") === stripped) return v;
|
|
}
|
|
if (key.startsWith("healthcare")) return "healthcare";
|
|
if (key.startsWith("b2b")) return "b2b-services";
|
|
if (key.startsWith("ecommerce")) return "ecommerce";
|
|
return "other";
|
|
}
|
|
|
|
function canonicalizeMetro(location) {
|
|
const lower = location.toLowerCase().trim();
|
|
if (METRO_ALIASES[lower]) return METRO_ALIASES[lower];
|
|
const cityPart = lower.split(/[,;]/)[0].trim();
|
|
if (METRO_ALIASES[cityPart]) return METRO_ALIASES[cityPart];
|
|
return cityPart.replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "unknown";
|
|
}
|
|
|
|
function getBrandLocations(brand) {
|
|
if (brand.locations && brand.locations.length > 0) return brand.locations;
|
|
if (brand.location && brand.location.trim()) return [brand.location.trim()];
|
|
return [];
|
|
}
|
|
|
|
function getBrandCells(brand) {
|
|
const vertical = canonicalizeVertical(brand.industry);
|
|
const cells = [{ vertical, locale: "national" }];
|
|
if (LOCAL_VERTICALS.has(vertical)) {
|
|
const seen = new Set();
|
|
for (const loc of getBrandLocations(brand)) {
|
|
const metro = canonicalizeMetro(loc);
|
|
if (metro && metro !== "unknown" && !seen.has(metro)) {
|
|
seen.add(metro);
|
|
cells.push({ vertical, locale: `metro:${metro}` });
|
|
}
|
|
}
|
|
}
|
|
return cells;
|
|
}
|
|
|
|
// ---- inline fragmentToQuery logic (mirrors seed-corpus.ts) ----
|
|
|
|
const STOP_WORDS = new Set([
|
|
"the","a","an","and","or","but","in","on","at","to","for","of","with",
|
|
"by","from","is","are","was","were","be","been","has","have","had","do",
|
|
"does","did","will","would","could","should","may","might","can","our",
|
|
"your","their","its","this","that","these","those","we","you","they","it",
|
|
"he","she","i","me","my","us","not","no","so","if","as","up","out","about",
|
|
"into","than","then","when","where","which","who","how","what","why","all",
|
|
"also","just","more","most","other","such","get","use","using","used","new",
|
|
"need","needs","make",
|
|
// common content verbs that appear in AI answer fragments
|
|
"provides","provide","offers","offer","delivers","deliver","includes","include",
|
|
"helps","help","gives","give","allows","allow","enables","enable","features",
|
|
"feature","serves","serve","creates","create","makes","builds","build",
|
|
"brings","bring",
|
|
// qualifiers that add noise
|
|
"same","days","area","near","patients","customers","clients","team","staff",
|
|
"experts","people","many","every","each","both","full","well","come","wide",
|
|
"high","even","here","there","very","always","never","often",
|
|
]);
|
|
|
|
function inferFromLandingPage(url) {
|
|
try {
|
|
const pathname = url.startsWith("http") ? new URL(url).pathname : url;
|
|
const segments = pathname.split("/").filter(Boolean);
|
|
const meaningful = segments.filter(s => s.length > 2 && !/^[a-z]{2}$/.test(s));
|
|
const slug = meaningful[meaningful.length - 1] ?? "";
|
|
if (!slug) return "site visit";
|
|
return slug.replace(/[-_]/g, " ").replace(/\s+/g, " ").trim();
|
|
} catch { return "site visit"; }
|
|
}
|
|
|
|
function fragmentToQuery(fragment, landingPage, brandName) {
|
|
const isInferred = fragment.startsWith("[inferred]");
|
|
if (isInferred) {
|
|
const raw = inferFromLandingPage(landingPage);
|
|
const base = raw === "site visit" || raw.includes(".") ? "" : raw;
|
|
return base ? `best ${base} near me` : `${brandName} services`;
|
|
}
|
|
const brandPattern = new RegExp(brandName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
|
|
const cleaned = fragment.replace(brandPattern, "").replace(/["""'']/g, "").replace(/\s+/g, " ").trim();
|
|
const tokens = cleaned.split(/\W+/)
|
|
.filter(w => w.length > 3 && !STOP_WORDS.has(w.toLowerCase()) && !/^\d+$/.test(w));
|
|
const keywords = tokens.slice(0, 5).join(" ").toLowerCase();
|
|
if (!keywords) return `${brandName} services`;
|
|
return keywords;
|
|
}
|
|
|
|
const COMMERCIAL_CONVERSION_TYPES = new Set([
|
|
"appointment_booked","form_submitted","phone_call","email_contact","sms_contact","purchase",
|
|
"booking_confirmed","form_submit","form_submission","phone_number_click","click_to_call",
|
|
"calendly","email_click","sms_click",
|
|
]);
|
|
|
|
function deriveConversionWeight(converted, dealValue, conversionType) {
|
|
if (!converted) return 0;
|
|
let base = 0.3;
|
|
if (conversionType && COMMERCIAL_CONVERSION_TYPES.has(conversionType.toLowerCase())) base += 0.3;
|
|
if (dealValue !== null) {
|
|
if (dealValue >= 10000) base += 0.4;
|
|
else if (dealValue >= 1000) base += 0.3;
|
|
else if (dealValue >= 100) base += 0.2;
|
|
else base += 0.1;
|
|
}
|
|
return Math.min(base, 1);
|
|
}
|
|
|
|
// ============================================================
|
|
// SECTION 1: Multi-vertical cell bucketing
|
|
// ============================================================
|
|
|
|
console.log("\n=== SECTION 1: Multi-vertical cell bucketing ===\n");
|
|
|
|
const testBrands = [
|
|
{ name: "Raleigh Cardiovascular Specialists", industry: "healthcare-medical", location: "Raleigh, NC", locations: ["Raleigh, NC"] },
|
|
{ name: "Fairway Lawns", industry: "local-services", location: "Charlotte, NC", locations: ["Charlotte, NC", "Raleigh, NC"] },
|
|
{ name: "AiGrowth360", industry: "b2b-saas", location: "United States", locations: [] },
|
|
{ name: "TPAction", industry: "b2b-services", location: null, locations: [] },
|
|
{ name: "Happy Smiles Dental", industry: "healthcare-dental", location: "Austin, TX", locations: ["Austin, TX"] },
|
|
];
|
|
|
|
for (const brand of testBrands) {
|
|
const cells = getBrandCells(brand);
|
|
console.log(`[${brand.name}]`);
|
|
console.log(` industry="${brand.industry}" -> vertical="${canonicalizeVertical(brand.industry)}"`);
|
|
console.log(` cells: ${JSON.stringify(cells)}`);
|
|
}
|
|
|
|
// ============================================================
|
|
// SECTION 2: Canonicalization coverage (all known classifier outputs)
|
|
// ============================================================
|
|
|
|
console.log("\n=== SECTION 2: Industry -> vertical canonicalization map ===\n");
|
|
|
|
const allIndustries = [
|
|
"healthcare-medical","healthcare-dental","healthcare-mental-health","healthcare-veterinary","healthcare-other",
|
|
"b2b-saas","b2b-services","b2b-manufacturing","b2b-finance","b2b-legal","b2b-other",
|
|
"ecommerce","ecommerce-fashion","ecommerce-beauty","ecommerce-electronics","ecommerce-home",
|
|
"ecommerce-sports","ecommerce-food","ecommerce-other",
|
|
"local-services","professional-services","finance","legal","real-estate",
|
|
"nonprofit","hospitality","higher-ed","other",
|
|
// Fuzzy / drift cases
|
|
null, "", "Healthcare Medical", "B2B SaaS", "ECOMMERCE", "unknown-industry",
|
|
];
|
|
for (const ind of allIndustries) {
|
|
console.log(` ${JSON.stringify(ind).padEnd(32)} -> "${canonicalizeVertical(ind)}"`);
|
|
}
|
|
|
|
// ============================================================
|
|
// SECTION 3: fragmentToQuery derivation examples
|
|
// ============================================================
|
|
|
|
console.log("\n=== SECTION 3: fragmentToQuery derivation ===\n");
|
|
|
|
const fragmentTests = [
|
|
{
|
|
desc: "Medical brand fragment",
|
|
fragment: "Raleigh Cardiovascular Specialists provides comprehensive cardiac imaging and interventional cardiology services for patients throughout central North Carolina",
|
|
landingPage: "https://raleighcardio.com/services/cardiac-imaging",
|
|
brandName: "Raleigh Cardiovascular Specialists",
|
|
},
|
|
{
|
|
desc: "Dental brand fragment",
|
|
fragment: "Happy Smiles Dental offers same-day emergency dental appointments and affordable teeth whitening treatments for Austin area patients",
|
|
landingPage: "https://happysmilesdental.com/emergency-dentist",
|
|
brandName: "Happy Smiles Dental",
|
|
},
|
|
{
|
|
desc: "B2B SaaS fragment",
|
|
fragment: "AiGrowth360 delivers AI-powered marketing automation and lead scoring tools purpose-built for B2B sales teams",
|
|
landingPage: "https://aigrowth360.com/features/lead-scoring",
|
|
brandName: "AiGrowth360",
|
|
},
|
|
{
|
|
desc: "Lawn care (inferred fragment)",
|
|
fragment: "[inferred] lawn care services",
|
|
landingPage: "https://fairwaylawns.com/services/lawn-fertilization",
|
|
brandName: "Fairway Lawns",
|
|
},
|
|
{
|
|
desc: "Empty fragment fallback",
|
|
fragment: "[inferred] site visit",
|
|
landingPage: "https://tpaction.com/",
|
|
brandName: "TPAction",
|
|
},
|
|
];
|
|
|
|
for (const t of fragmentTests) {
|
|
const q = fragmentToQuery(t.fragment, t.landingPage, t.brandName);
|
|
console.log(`[${t.desc}]`);
|
|
console.log(` fragment: "${t.fragment.slice(0, 80)}..."`);
|
|
console.log(` derived query: "${q}"`);
|
|
}
|
|
|
|
// ============================================================
|
|
// SECTION 4: conversionWeight examples
|
|
// ============================================================
|
|
|
|
console.log("\n=== SECTION 4: deriveConversionWeight examples ===\n");
|
|
|
|
const weightTests = [
|
|
{ converted: false, dealValue: 5000, conversionType: "appointment_booked", label: "unconverted (no weight)" },
|
|
{ converted: true, dealValue: null, conversionType: null, label: "converted, no type, no value" },
|
|
{ converted: true, dealValue: null, conversionType: "appointment_booked", label: "converted, commercial type, no value" },
|
|
{ converted: true, dealValue: 250, conversionType: "appointment_booked", label: "converted, commercial type, dealValue=250" },
|
|
{ converted: true, dealValue: 2500, conversionType: "form_submitted", label: "converted, commercial type, dealValue=2500" },
|
|
{ converted: true, dealValue: 15000, conversionType: "purchase", label: "converted, purchase, dealValue=15000 (cap)" },
|
|
{ converted: true, dealValue: null, conversionType: "phone_call", label: "converted, phone_call (old type)" },
|
|
{ converted: true, dealValue: null, conversionType: "phone_number_click", label: "converted, phone_number_click (legacy)" },
|
|
{ converted: true, dealValue: null, conversionType: "unknown_type", label: "converted, unrecognized type (no bonus)" },
|
|
];
|
|
|
|
for (const t of weightTests) {
|
|
const w = deriveConversionWeight(t.converted, t.dealValue, t.conversionType);
|
|
console.log(` [${t.label}]`);
|
|
console.log(` converted=${t.converted} dealValue=${t.dealValue} convType=${t.conversionType} -> weight=${w.toFixed(2)}`);
|
|
}
|
|
|
|
// ============================================================
|
|
// SECTION 5: Simulated corpus for one cell (healthcare/national)
|
|
// ============================================================
|
|
|
|
console.log("\n=== SECTION 5: Simulated FRAGMENT_SEEDED corpus (healthcare, national) ===\n");
|
|
|
|
const healthcareSampleAttributions = [
|
|
{ brandId: "b1", queryContext: "Raleigh Cardiovascular provides cardiac imaging and heart failure management for patients", landingPage: "https://raleighcardio.com/cardiac-imaging", converted: true, dealValue: 3000, conversionType: "appointment_booked" },
|
|
{ brandId: "b1", queryContext: "interventional cardiology procedures including stent placement and catheterization", landingPage: "https://raleighcardio.com/procedures", converted: false, dealValue: null, conversionType: null },
|
|
{ brandId: "b1", queryContext: null, landingPage: "https://raleighcardio.com/pediatric-cardiology", converted: true, dealValue: 500, conversionType: "form_submitted" },
|
|
{ brandId: "b2", queryContext: "same-day urgent care and family medicine appointments available", landingPage: "https://example-medical.com/urgent-care", converted: true, dealValue: null, conversionType: "appointment_booked" },
|
|
];
|
|
|
|
const brandNames = new Map([["b1", "Raleigh Cardiovascular"], ["b2", "Example Medical Group"]]);
|
|
const weightMap = new Map();
|
|
|
|
for (const attr of healthcareSampleAttributions) {
|
|
const fragment = attr.queryContext ?? `[inferred] site visit`;
|
|
const brandName = brandNames.get(attr.brandId) ?? "";
|
|
const query = fragmentToQuery(fragment, attr.landingPage, brandName).trim();
|
|
if (!query || query.length < 5) continue;
|
|
const weight = deriveConversionWeight(attr.converted, attr.dealValue, attr.conversionType);
|
|
const prev = weightMap.get(query);
|
|
if (prev === undefined || weight > prev) weightMap.set(query, weight);
|
|
}
|
|
|
|
console.log(" FRAGMENT_SEEDED entries (vertical=healthcare, locale=national):");
|
|
for (const [query, weight] of weightMap) {
|
|
console.log(` query="${query}" conversionWeight=${weight.toFixed(2)}`);
|
|
}
|
|
|
|
console.log("\n CATEGORY_COVERAGE entries (vertical=healthcare, locale=national):");
|
|
const healthcareCategories = [
|
|
"best primary care doctor near me",
|
|
"how to find a good family physician",
|
|
"primary care vs urgent care when to go",
|
|
"what to look for in a primary care doctor",
|
|
"how to get a same-day doctor appointment",
|
|
];
|
|
for (const q of healthcareCategories) {
|
|
console.log(` query="${q}" conversionWeight=0.00 source=CATEGORY_COVERAGE`);
|
|
}
|
|
|
|
console.log("\n=== All checks passed ===\n");
|