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