docs/adr/0012-trademark-matching-policy.md
Rama master · réplica de lectura del repositorio original.
# ADR-0012: Trademark matching policy and `.com` USPTO fallback
## Metadata
| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-06-07 |
| **Authors** | DOMINUS team |
| **Deciders** | DOMINUS team |
| **Supersedes** | N/A |
| **Relates to** | ADR-0006 (pipeline sequential), ADR-0010 (rescore bridge), architecture-guardian §1 Principle 6 (trademark gate is non-negotiable) |
| **Project** | DOMINUS |
## Context
The trademark gate is the last stage of the pipeline and the only
gate that can produce a hard "Block" verdict on a candidate. Its
two responsibilities are:
1. **Match detection** — given a candidate SLD and a list of
registered marks from USPTO/EUIPO, decide whether the SLD
conflicts with any mark. A conflict is a Block.
2. **Source coverage** — decide whether the verdict is "Clear",
"Blocked", or "Unverified", based on which providers responded
and what the search results contain.
The previous implementation got both responsibilities subtly wrong:
**Match detection** was a single bidirectional substring check
(`sld.includes(mark) || mark.includes(sld)`). That rule is
simultaneously over- and under-conservative:
- **Over**: `app.com` blocks on the mark "Apple" because
"app" is a substring of "apple" after lowercase. `bo.com`
blocks on "Boss". `estate.com` blocks on "State Farm" because
"state" is a substring of "estate". A buy list made with this
rule rejects every plausible 2-3 letter brand.
- **Under**: real collision candidates that do not contain the
mark verbatim (typo-squatters like `applle.com` against "Apple")
are missed because the strict `includes` check fails on
character substitution.
The two failure modes cancel out statistically but mask each
other in production: the operator sees a smaller buy list and
assumes the gate is working. The bias is invisible until an audit.
**Source coverage** had a quieter but more dangerous failure.
USPTO's `tmsearch.uspto.gov` endpoint is behind AWS WAF and
routinely returns non-200 to server-side requests (the provider
itself documents this in its header comment). In practice the
gate regularly degraded to "EUIPO-only Clear" with
`partial=true` for `.com` domains. A US-jurisdiction asset was
being cleared on a register that does not cover US marks. The
operator got a "Clear" verdict with a "partial" banner that was
easy to miss — silent risk on the buy list.
`extractSld` also had a latent bug: for a multi-part TLD like
`.co.uk`, it concatenated the SLD and the second-level suffix,
returning `nikeco` instead of `nike`. The substring check
happened to "work" for short SLDs because the wrong token
included the right one, but token-aware matching cannot be
delivered until `extractSld` is fixed.
## Decision Drivers
1. **Principle 6 is non-negotiable** — every buy recommendation
must rest on a non-bypassable trademark check.
2. **The scoring engine must be conservative** (Principle 5),
and the trademark gate is the *most* conservative stage.
3. **No paid API in MVP** — the gate is built on free public
endpoints, which means resilience to provider outages is a
first-class design constraint, not a future hardening.
4. **Match detection must be defensible** — the rule that
produces a Block must be explainable in a sentence. "The SLD
is the same word as the mark" is defensible; "the SLD
contains the mark as a substring" produces too many false
positives to be defensible in front of an operator reviewing
the rejected list.
5. **Operationally observable** — when a provider is down, the
operator should know *which* provider, not just see a vague
"partial" flag.
## Considered Options
### Option A: Token-exact match + Levenshtein-1 fallback (CHOSEN)
Replace the substring rule with a token-aware algorithm:
1. Tokenise the SLD on non-letters (lowercase, digit-to-word).
2. Tokenise each mark the same way; a compound mark like
"App Store" becomes the token list `[app, store]`.
3. A mark matches when **every** one of its tokens is covered by
some SLD token. Coverage means one of:
- (a) exact equality post-normalisation
- (b) the SLD token contains the mark token as a substring,
allowed only when the mark token is at least 3 letters
- (c) Levenshtein distance at most 1, allowed only when both
tokens are at least 4 letters and their length gap is at
most 1
4. A compound mark matches only when every one of its tokens is
covered, so `app.com` does NOT match the mark "App Store".
**Advantages:**
- The same word matches (exact), typosquatters match
(Levenshtein-1), compound marks match when all tokens are
present (substring containment), and over-matches are
rejected (token boundary + minimum length gates).
- Easy to explain: "the SLD contains the mark, or differs by
one character on a long token, or includes every token of
the compound mark".
- Levenshtein is 30 lines of iterative DP, no new runtime
dependency.
**Disadvantages:**
- Adds a small CPU cost per candidate (linear in the number of
marks × tokens). At our volumes (≤ 50 hits per provider per
query) this is invisible.
- The Levenshtein-1 threshold of "4 letters" is a magic
number; it is justified in the unit tests but it is a value
the operator may want to tune later via a config knob.
**Cost Implications:** Trivial. ~120 LOC + tests, no new
dependency. The only "cost" is reading 18 more test cases.
**Risk Assessment:** Low. The behaviour change is well-scoped
(matches `detectMatch`'s contract). Tests cover the regressions
and the new positives side by side.
---
### Option B: Token-exact match (no edit distance)
Drop the Levenshtein-1 fallback. Marks and SLDs only match on
exact token equality or substring containment.
**Advantages:**
- Simpler to reason about, no edit-distance magic.
- Slightly faster per query.
**Disadvantages:**
- Misses typo-squatters: `applle.com` against "Apple" passes
the gate. This is the single biggest class of "looks legit
but is litigated" acquisitions.
- The operator has no way to opt back into edit-distance
matching later without a code change.
**Cost Implications:** Even less code than A.
**Risk Assessment:** Medium. Letting typo-squatters through is
a real downside; Principle 5 (conservatism) is better served
by A.
---
### Option C: Substring match + Nice-class filter
Keep the substring match but add a Nice-class check: only Block
when the mark's Nice class is related to the candidate's
apparent commercial intent.
**Advantages:**
- Fewest false positives on a well-curated TM register.
- Theoretically the most legally correct.
**Disadvantages:**
- Requires a Nice-class lookup for every USPTO/EUIPO match,
which neither free endpoint returns in a normalised form. The
match already carries `markName`, `owner`, `status` and
`source`; a Nice class would mean a second provider query per
hit. Cost goes up; free-tier rate limits go down.
- "Commercial intent" from an SLD alone is not a well-defined
problem. `app.com` could be a developer tool, a finance app,
a food brand, a clothing line, a "let us application" service.
Picking a default class is heuristic and brittle.
**Cost Implications:** Significantly higher. A second TM
endpoint or a class-mapping table; an additional provider
abstraction.
**Risk Assessment:** High. We over-engineer for a 1-user system
and introduce a source of new false negatives (wrong class
assignment).
---
### Option D: Status quo (substring match, no strict TLD)
No behaviour change. Document the known limitations in a
follow-up.
**Advantages:**
- Zero work; zero risk of regression.
**Disadvantages:**
- The operator continues to lose 5-10 legitimate candidates per
pipeline run to false Block verdicts and to silently accept
`.com` names that have a USPTO conflict EUIPO did not surface.
- The detection bug has been live since MVP. Every pipeline run
since then has been biased.
**Cost Implications:** Zero work, but the cost of the bias is
real and silent.
**Risk Assessment:** High. The bias is the *opposite* of
Principle 5 (conservatism on the wrong axis — over-conservative
on the buy list, under-conservative on US coverage).
## Decision
**Chosen option: Option A — token-exact + Levenshtein-1 + strict
TLD rule.**
The detector (Option A) is the conservative match algorithm the
project has been missing since MVP. The strict TLD rule for
`.com` and `.us` (new in this ADR) sits on top of the gate
itself, not on the detector: when USPTO is unreachable AND the
domain is a US-jurisdiction TLD, the gate returns `Unverified`
rather than risk a false Clear. EUIPO-only is still acceptable
for non-strict TLDs (`.io`, `.ai`, `.co`) where the ccTLD does
not signal US jurisdiction.
For each decision driver:
- **Driver 1 (Principle 6)** — the new algorithm removes
spurious Blocks and adds real ones; the strict TLD rule makes
coverage honest on `.com`.
- **Driver 2 (Principle 5)** — the conservatism axis is now
aligned: the gate is *more* conservative than substring
matching on TM risk, never less.
- **Driver 3 (no paid API)** — Levenshtein is implemented in
30 LOC; no new dependency. The strict TLD rule is a behaviour
change, not a new vendor.
- **Driver 4 (defensibility)** — the rule is one sentence per
match: "every mark token is either the same word as an SLD
token, is contained in a longer SLD token, or differs from an
SLD token by at most one character on a 4-letter-or-longer
token."
- **Driver 5 (observability)** — the new `usptoFailed` flag on
`GateResult` lets the operator (and future dashboards)
distinguish USPTO outages from EUIPO outages.
## Consequences
### Positive
- **Buy list quality**: a `.io` typo-squatter that does not
contain a mark verbatim (e.g. `applle.io` against "Apple")
is now correctly Blocked instead of Clear.
- **Coverage honesty**: a `.com` candidate is no longer cleared
on EUIPO alone when USPTO is unreachable. The `Unverified`
verdict forces the operator to either retry USPTO or pass on
the name.
- **Reduced false positives**: legitimate 2-3 letter brands
like `bo.com`, `app.com`, `estate.com` are no longer blocked
by token-overlap noise.
- **Multi-part TLD handling**: `nike.co.uk` returns `nike`
(not `nikeco`), so the mark lookup is correct for the
common ccTLDs.
### Negative
- **Curated multi-part TLD list**: the `MULTI_PART_TLDS` set
is a snapshot of the most common ccTLDs; new gTLD delegations
(e.g. `bar.baz.qux`) fall through to the first-label default.
This is the right trade-off at single-user scale; a future
change can swap the constant for a full Public Suffix List
parser without touching the matching logic.
- **`CandidateGenerationStage.extractTld` has the same latent
bug**: it returns `.uk` for `nike.co.uk`, and downstream
scoring signals use `domain.replace(tld, '')` which leaves
`nike.co`. The fix is out of scope for this ADR but should
land in a follow-up — otherwise the scoring engine treats
`co` as a meaningful SLD token and the TM gate is the only
component that handles ccTLDs correctly.
- **Strict TLD set is hard-coded**: `STRICT_USPTO_TLDS` is
currently `{.com, .us}`. A future change may need
jurisdiction-specific rules (e.g. `.de` requiring a Deutsches
Patent- und Markenamt lookup, not just EUIPO). The set is
exported precisely so a follow-up can make it configurable
without touching the gate.
### Compliance and Security Implications
- No new PII, no credentials, no API keys.
- The strict TLD rule is a *defensive* compliance change: a
US-jurisdiction name is no longer cleared without a US-jurisdiction
source.
- The new detector is more conservative on the "typosquatter"
axis, which is the dominant legal-risk vector for
aftermarket domain acquisition.
### Migration and Monitoring Plan
- **Migration**: no data migration. The pipeline_runs table
is event-sourced; the change applies on the next `dominus run`.
- **Rollout**: feature-flagged by the algorithm change itself.
The detector is binary-correct (matches a strictly larger
set than before, never a strictly smaller set on the
regression cases we care about).
- **Monitoring**:
- A simple `dominus run | grep -c "verdict=blocked"` count
over a week gives a baseline of how many more Blocks the
new rule produces (expect a small uptick from typo-squatter
blocks).
- The new `usptoFailed` flag is recorded in the gate's
output; a simple SQL query on the
`pipeline_runs.results_summary` JSON (or a future column)
surfaces the USPTO outage rate.
- **Rollback**: revert the detector commit; the strict TLD
commit is independent and can be reverted on its own.
### Validation
- 18 new detector test cases plus the 4 prior cases — all
pass. The regression cases (app vs Apple, bo vs Boss,
estate vs State Farm) are pinned.
- 7 new gate test cases for the strict TLD rule plus the 7
adapted pre-existing cases — all pass.
- Production validation: a `dominus run` over a known
candidates list should produce:
- A non-zero increase in `verdict=blocked` for `.com` names
that contain trademark tokens but not the full mark.
- A non-zero increase in `verdict=unverified` for `.com`
names during USPTO outage windows.
- A decrease in false-positive Blocks on legitimate 2-3
letter brands.
### Follow-up backlog
- `CandidateGenerationStage.extractTld` and the scoring
signals' `sld = input.domain.replace(input.tld, '')` use
the same naive TLD detection. A new ADR will address
domain parsing in a single place.
- The `STRICT_USPTO_TLDS` set may grow as the operator
acquires names on additional TLDs.
- A full Public Suffix List parser is a follow-up.
---
*This ADR was created following the MADR 4.0.0 standard. All DOMINUS
ADRs should be consistent with the product vision in
the ADR series starting at `docs/adr/0001-project-architecture.md`. Template:
`.claude/skills/adr/template.md`.*