← back to CelebritySignatures

scripts/build-tm-clearance.mjs

96 lines

#!/usr/bin/env node
// Build data/tm-clearance.json — a per-signature trademark verdict, cached so the
// screen runs once and the server just reads it.
//
// For every name in the merged signature catalog (celebrity_signatures + artists +
// authors), match against data/tm-index.json (live US marks) and classify:
//   MARK_FOUND    — a LIVE Class-25 (apparel) wordmark equals/covers the name -> NOT sellable
//   NEEDS_REVIEW  — a LIVE mark in another class, or a fuzzy/token match -> manual review, NOT sold
//   CLEAR         — no matching live mark
// Sale eligibility (server side) additionally requires the index to be COMPLETE;
// a CLEAR from a seed-only index is disclosed as a partial screen in the UI.
//
// Usage:
//   node scripts/build-tm-clearance.mjs                 # screen the whole catalog
//   node scripts/build-tm-clearance.mjs --test-name X   # print the verdict for one name (no write)
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { join } from 'node:path';
import { normName, tokens, isSubset } from './tm-normalize.mjs';

const ROOT = fileURLToPath(new URL('..', import.meta.url));
const DATA = join(ROOT, 'data');
const load = async (n, f) => { try { return JSON.parse(await readFile(join(DATA, n), 'utf8')); } catch { return f; } };

// Screen one normalized name against the live-mark index.
function screen(name, index) {
  const key = normName(name);
  const nameToks = tokens(name);
  if (!key) return { verdict: 'NEEDS_REVIEW', class25: false, matches: [] };
  const matches = [];
  let class25Hit = false, anyLiveHit = false;
  for (const m of index.marks) {
    const exact = m.normalized === key;
    const markToks = tokens(m.wordmark);
    // "covers" = the mark IS the person's name, or the person's name IS the mark
    // (subset either direction) — e.g. mark "WARHOL" vs name "andy warhol".
    const covers = exact || isSubset(markToks, nameToks) || isSubset(nameToks, markToks);
    if (!covers) continue;
    anyLiveHit = true;
    if (m.class25) class25Hit = true;
    matches.push({ wordmark: m.wordmark, class25: !!m.class25, exact });
    if (matches.length >= 5) break;
  }
  let verdict = 'CLEAR';
  if (class25Hit) verdict = 'MARK_FOUND';         // live apparel mark on the name -> block
  else if (anyLiveHit) verdict = 'NEEDS_REVIEW';  // live mark, other class -> review
  return { verdict, class25: class25Hit, matches };
}

// Same merge the server uses, kept simple: base (non Artists/Authors) + artists + authors.
async function mergedNames() {
  const base = await load('celebrity_signatures.json', []);
  const artists = await load('artists.json', []);
  const authors = await load('authors.json', []);
  const names = new Set();
  for (const r of base) if (r.full_name) names.add(r.full_name);
  for (const r of artists) if (r.full_name) names.add(r.full_name);
  for (const r of authors) if (r.full_name) names.add(r.full_name);
  return [...names];
}

async function main() {
  const index = await load('tm-index.json', null);
  if (!index) { console.error('data/tm-index.json missing — run build-tm-index.mjs first'); process.exit(1); }

  const args = process.argv.slice(2);
  const ti = args.indexOf('--test-name');
  if (ti >= 0) {
    const nm = args.slice(ti + 1).join(' ');
    console.log(nm, '->', JSON.stringify(screen(nm, index)));
    return;
  }

  const names = await mergedNames();
  const verdicts = {};
  const tally = { CLEAR: 0, NEEDS_REVIEW: 0, MARK_FOUND: 0 };
  for (const nm of names) {
    const v = screen(nm, index);
    verdicts[nm] = v;
    tally[v.verdict]++;
  }
  const out = {
    generatedAt: new Date().toISOString(),
    indexComplete: !!index.complete,
    indexSource: index.source,
    screened: names.length,
    tally,
    verdicts,
  };
  await writeFile(join(DATA, 'tm-clearance.json'), JSON.stringify(out));
  console.log(`tm-clearance.json: screened ${names.length} names — CLEAR ${tally.CLEAR}, NEEDS_REVIEW ${tally.NEEDS_REVIEW}, MARK_FOUND ${tally.MARK_FOUND}`);
  console.log(`  indexComplete=${out.indexComplete} (source=${out.indexSource})`);
  if (!out.indexComplete) console.log('  Sale stays gated as a PARTIAL screen until the full USPTO register is indexed.');
}
main().catch(e => { console.error(e); process.exit(1); });