[object Object]

← back to CelebritySignatures

wear/TM: partial-register completeness guard + fix retired-host runbook (TK-10286)

d3943773f1d424c3308fd3ec95efcfb764efe8fb · 2026-08-10 08:32:17 -0700 · Steve Abrams

Cody-gated hardening of the /wear USPTO trademark gate:
- build-tm-index.mjs: require >= TM_MIN_MARKS (default 500k, --min-marks/env override)
  parsed live marks before writing complete:true. Previously complete=(parsedReal>0),
  so an interrupted ingest (1 of ~91 register parts) would flip complete:true and,
  because build-tm-clearance screen() returns CLEAR on absence-of-match, false-CLEAR
  every name the missing parts would have flagged MARK_FOUND — a write that lands
  BEFORE any go-live approval. Guard keeps a truncated register partial -> store empty.
- WEAR-LEGAL-NOTE.md: replace retired bulkdata.uspto.gov --url runbook (dead host)
  with the ODP --file/--odp path; document the completeness floor.
- WEAR-READY-FOR-KEY.md: note the floor + that a daily-only screen never reaches complete.
Verified: seed-only stays complete:false (no regression); GET /api/wear/signatures
count:0; checkout of an otherwise-eligible sig (Jefferson Q11812) still 403s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit d3943773f1d424c3308fd3ec95efcfb764efe8fb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 08:32:17 2026 -0700

    wear/TM: partial-register completeness guard + fix retired-host runbook (TK-10286)
    
    Cody-gated hardening of the /wear USPTO trademark gate:
    - build-tm-index.mjs: require >= TM_MIN_MARKS (default 500k, --min-marks/env override)
      parsed live marks before writing complete:true. Previously complete=(parsedReal>0),
      so an interrupted ingest (1 of ~91 register parts) would flip complete:true and,
      because build-tm-clearance screen() returns CLEAR on absence-of-match, false-CLEAR
      every name the missing parts would have flagged MARK_FOUND — a write that lands
      BEFORE any go-live approval. Guard keeps a truncated register partial -> store empty.
    - WEAR-LEGAL-NOTE.md: replace retired bulkdata.uspto.gov --url runbook (dead host)
      with the ODP --file/--odp path; document the completeness floor.
    - WEAR-READY-FOR-KEY.md: note the floor + that a daily-only screen never reaches complete.
    Verified: seed-only stays complete:false (no regression); GET /api/wear/signatures
    count:0; checkout of an otherwise-eligible sig (Jefferson Q11812) still 403s.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 WEAR-LEGAL-NOTE.md         | 15 +++++++++++++--
 WEAR-READY-FOR-KEY.md      |  9 +++++++--
 data/tm-index.json         |  4 +++-
 scripts/build-tm-index.mjs | 32 ++++++++++++++++++++++++++++----
 4 files changed, 51 insertions(+), 9 deletions(-)

diff --git a/WEAR-LEGAL-NOTE.md b/WEAR-LEGAL-NOTE.md
index b00dca7..64f5b30 100644
--- a/WEAR-LEGAL-NOTE.md
+++ b/WEAR-LEGAL-NOTE.md
@@ -32,8 +32,19 @@ before go-live.
 - **Publishing `/wear` for sale / linking it in nav** — Steve's call.
 
 ## Build the real screen (before go-live)
+> NOTE (2026-08-10): the legacy host `bulkdata.uspto.gov` is **RETIRED**. Trademark bulk
+> data now lives on the USPTO Open Data Portal (`data.uspto.gov`), behind an AWS WAF +
+> an ID.me-linked API key. Use `--file` (a file you downloaded in a real browser) or
+> `--odp` (the keyed BSD download); the old `--url bulkdata.uspto.gov` path dead-ends.
+> See `WEAR-READY-FOR-KEY.md` for the full keyed runbook.
 ```
-node scripts/fetch-tm-register.mjs --url https://bulkdata.uspto.gov/data/trademark/dailyxml/applications/<file>.zip
-node scripts/build-tm-index.mjs        # complete flips true once real register data is parsed
+# (A) $0, no key — ingest a register file downloaded in a real browser / openclaw:
+node scripts/fetch-tm-register.mjs --file /path/to/downloaded-trtdxf.zip
+# (B) keyed — pull the full annual register once USPTO_ODP_API_KEY (ID.me-linked) is set:
+node scripts/fetch-tm-register.mjs --odp <fileName>
+node scripts/build-tm-index.mjs        # complete flips true ONLY once a plausibly-FULL register is parsed (>= completeness floor)
 node scripts/build-tm-clearance.mjs    # re-screens the catalog; cleared names appear in /wear
 ```
+A **partial** register never counts as complete: `build-tm-index.mjs` requires at least
+`TM_MIN_MARKS` (default 500,000) parsed live marks before it writes `complete:true`, so an
+interrupted/truncated ingest keeps the store safely empty rather than false-clearing names.
diff --git a/WEAR-READY-FOR-KEY.md b/WEAR-READY-FOR-KEY.md
index 2ff6194..4ab339a 100644
--- a/WEAR-READY-FOR-KEY.md
+++ b/WEAR-READY-FOR-KEY.md
@@ -20,10 +20,15 @@ API, and even the web file download — goes through this same key (USPTO's June
    # pull the full annual register (product trtyrap ≈ 91 parts) with the key:
    for f in $(seq -w 1 91); do \
      node scripts/fetch-tm-register.mjs --odp "apc18840407-20251231-$f.zip"; done
-   node scripts/build-tm-index.mjs        # complete flips true (real register parsed)
+   node scripts/build-tm-index.mjs        # complete flips true ONLY if >= TM_MIN_MARKS (default 500k) real marks parsed — so a truncated loop stays complete:false
    node scripts/build-tm-clearance.mjs    # re-screens the catalog
    ```
-   (Or just the daily product `trtdxfap` for a lighter, current-window screen.)
+   Run the **whole** loop — `build-tm-index.mjs` refuses to declare the register
+   complete on a partial ingest (fewer than ~500k parsed marks), which keeps the store
+   safely empty if the loop is interrupted. The daily product `trtdxfap` is a
+   current-window file (not the full register), so a daily-only screen will **not**
+   reach `complete:true` by design — nothing sells off a daily-only screen. Use the
+   full annual register for go-live.
 4. **Restart the app** (pm2 restart celebrity-signatures) and open `/wear` — it flips from
    empty to a populated, trademark-cleared store automatically. Nothing else to change.
 
diff --git a/data/tm-index.json b/data/tm-index.json
index 580927b..1cf4c31 100644
--- a/data/tm-index.json
+++ b/data/tm-index.json
@@ -1,7 +1,9 @@
 {
-  "generatedAt": "2026-08-06T16:22:25.930Z",
+  "generatedAt": "2026-08-10T15:31:23.660Z",
   "complete": false,
   "source": "seed-sample",
+  "parsedRealMarks": 0,
+  "minCompleteMarks": 5,
   "liveMarkCount": 12,
   "class25Count": 12,
   "marks": [
diff --git a/scripts/build-tm-index.mjs b/scripts/build-tm-index.mjs
index 736934d..7c037f3 100644
--- a/scripts/build-tm-index.mjs
+++ b/scripts/build-tm-index.mjs
@@ -31,6 +31,20 @@ const ROOT = fileURLToPath(new URL('..', import.meta.url));
 const DATA = join(ROOT, 'data');
 const CACHE = join(ROOT, 'tmp_tm_cache');
 
+// Completeness floor (TK-10286, Cody-gated 2026-08-10). A PARTIAL register must
+// never masquerade as complete: build-tm-clearance's screen() returns CLEAR on the
+// ABSENCE of a match, so declaring complete:true on a truncated ingest (e.g. the
+// go-live loop interrupted after 1 of ~91 parts) would false-CLEAR every name the
+// missing parts would have flagged as MARK_FOUND — and that write lands on disk
+// BEFORE any Steve go-live approval. The full USPTO annual register holds millions
+// of live marks; require a real floor of parsed marks before trusting it as a
+// complete screen. Override for legit edge cases via --min-marks N or TM_MIN_MARKS.
+// (A daily-only product is a current-window file, not the full register, so it
+// intentionally never reaches this floor — nothing sells off a daily-only screen.)
+const _argv = process.argv.slice(2);
+const _argGet = (flag) => { const i = _argv.indexOf(flag); return i >= 0 ? _argv[i + 1] : null; };
+const MIN_COMPLETE_MARKS = Number(_argGet('--min-marks') || process.env.TM_MIN_MARKS || 500000);
+
 // Status codes that indicate a LIVE mark (registered / pending-live). USPTO uses
 // 6xx (registered) and 7xx (registered/renewed/sections); dead marks are 6xx-abandoned
 // or explicit cancellation/abandonment dates. We treat presence of a live status-code
@@ -93,16 +107,26 @@ async function main() {
     else byNorm.set(key, { normalized: key, wordmark: m.wordmark, class25: !!m.class25, status: 'LIVE' });
   }
 
+  // complete ONLY when a real register of plausibly-full size was parsed — NOT on a
+  // truncated ingest (see MIN_COMPLETE_MARKS above). parsedReal excludes the seed.
+  const complete = parsedReal >= MIN_COMPLETE_MARKS;
+  if (parsedReal > 0 && !complete) {
+    console.warn(`  WARNING: parsed ${parsedReal} real marks — BELOW the ${MIN_COMPLETE_MARKS} completeness floor.`);
+    console.warn('  Treating the register as PARTIAL (complete=false); the store stays gated/empty by design.');
+    console.warn('  Ingest the full annual register (all parts) or override with --min-marks / TM_MIN_MARKS.');
+  }
   const index = {
     generatedAt: new Date().toISOString(),
-    complete: parsedReal > 0,                 // true only once real register data was parsed
-    source: parsedReal > 0 ? 'uspto-bulk+seed' : 'seed-sample',
+    complete,                                 // true only once a plausibly-COMPLETE register was parsed
+    source: complete ? 'uspto-bulk+seed' : (parsedReal > 0 ? 'uspto-bulk-partial+seed' : 'seed-sample'),
+    parsedRealMarks: parsedReal,
+    minCompleteMarks: MIN_COMPLETE_MARKS,
     liveMarkCount: byNorm.size,
     class25Count: [...byNorm.values()].filter(m => m.class25).length,
     marks: [...byNorm.values()],
   };
   await writeFile(join(DATA, 'tm-index.json'), JSON.stringify(index, null, 2));
-  console.log(`tm-index.json: ${index.liveMarkCount} live marks (${index.class25Count} apparel), complete=${index.complete}, source=${index.source}`);
-  if (!index.complete) console.log('  NOTE: seed-only. Run scripts/fetch-tm-register.mjs --url <...> then rebuild for a full screen.');
+  console.log(`tm-index.json: ${index.liveMarkCount} live marks (${index.class25Count} apparel), parsedReal=${parsedReal}, complete=${index.complete}, source=${index.source}`);
+  if (!index.complete) console.log('  NOTE: not a complete register. Ingest the full annual register via scripts/fetch-tm-register.mjs (--file or --odp), then rebuild for a full screen.');
 }
 main().catch(e => { console.error(e); process.exit(1); });

← 15b76e1 ios: fastlane push_listing = screenshots-only (skip_metadata  ·  back to CelebritySignatures  ·  chore: lint (node --check ✓, no linter configured) + v1.0.1 a858b81 →