docs/adr/0015-psl-parser-adoption.md
Rama master · réplica de lectura del repositorio original.
# ADR-0015: Adopt full Public Suffix List via `psl` npm Package
## Metadata
| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-06-08 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | ADR-0013 (domain parsing — the curated `MULTI_PART_TLDS` set was always intended as an interim solution) |
| **Relates to** | ADR-0013, ADR-0012 |
| **Project** | DOMINUS |
## Context
ADR-0013 consolidated domain parsing into a single `parseDomain` function in `src/utils/domain.ts` backed by a hand-curated `MULTI_PART_TLDS` set of 29 multi-part country-code suffixes (`co.uk`, `com.au`, etc.). The ADR explicitly noted this as an interim measure and marked full Public Suffix List (PSL) adoption as a follow-up in its "Negative Consequences" section.
The curated set has three gaps that grow over time:
1. **Missing ccTLD patterns**: several common multi-part ccTLDs are absent (`ne.kz`, `or.th`, `go.kr`, `ac.nz`, `co.ve`, `com.ec`, `org.mx`, to name a few). The original selection was based on the domain aftermarket categories the author expected to encounter during MVP, but closeout CSV imports are source-agnostic and regularly include names with unlisted suffixes.
2. **Missing new gTLD hybrids**: the new gTLD programme has delegated suffixes with second-level registration policies (e.g., `uk.com`, `eu.com`, `gb.net`). These are not in the curated set, causing incorrect SLD extraction (e.g., `example.uk.com` would be parsed as SLD `exampleuk` over TLD `.com` instead of SLD `example` over TLD `.uk.com`).
3. **Subdomain misclassification**: the curated approach falls through to `parts[0]`-as-SLD for any 3+ label domain whose last-two labels are not in the set — even when the extra labels are ordinary subdomains of a single-label TLD. For `sub.domain.com`, the current parser returns `{ sld: 'sub', tld: '.com' }` when it should return `{ sld: 'domain', tld: '.com' }`. This has not yet surfaced as a visible bug because most pipeline candidates arrive as bare `sld.tld` pairs, but it is silently wrong.
The PSL is maintained by Mozilla, updated continuously, covers ~10,000+ entries, and is the de facto standard for browser-grade domain parsing. The `psl` npm package wraps it with a simple `parse()` API.
### Previous ADR trajectory
- ADR-0012 adopted the curated set for the trademark gate, noting the limitation.
- ADR-0013 consolidated the set into a single exported constant and explicitly made full PSL adoption the follow-up.
- ADR-0015 closes that follow-up.
## Decision Drivers
1. **Correctness** — SLD/TLD extraction must be correct for any domain a closeout CSV or keyword combinator could produce. A curated set that silently misparses unlisted suffixes will eventually produce bad scoring signals and false trademark matches.
2. **Maintenance burden** — the curated set requires manual updates when new TLDs or second-level registration policies are introduced. With ~1,500+ TLDs and thousands of PSL entries, manual curation is not sustainable at single-developer scale.
3. **Library maturity** — the PSL is a low-risk, zero-cost dependency. The `psl` package has 2M+ weekly downloads, stable API, and is dependency-free. Adopting it eliminates the correctness gap without introducing operational risk.
4. **Conservatism principle** (Principle 5) — the scoring engine must be conservative. Incorrect SLD extraction inflates the apparent SLD (e.g., `exampleuk` instead of `example`), which distorts intrinsic signals (length, pronounceability). Full PSL adoption makes the parser *more* conservative, not less.
## Considered Options
### Option A: Adopt `psl` npm Package
Replace the curated `MULTI_PART_TLDS` set with `psl.parse()` for all SLD/TLD extraction. The `ParsedDomain` interface and `parseDomain`/`extractTld`/`extractSld` exports remain unchanged — only the implementation changes.
The `psl` package:
- Bundles the full Mozilla Public Suffix List
- Exports `parse(domain)` → `{ tld, sld, domain, subdomain, error }`
- Zero runtime dependencies, 30KB unpacked
- Handles all edge cases (Punycode, subdomain stripping, unknown TLDs)
**Advantages:**
- Correct for every domain that has a known TLD or PSL entry — no gaps
- Zero maintenance — updates ship with `npm update` when Mozilla publishes revisions
- Eliminates the subdomain misclassification bug (`sub.domain.com` → correct SLD)
- Proven library: 2M+ weekly downloads, backed by Mozilla's PSL
- Same API used by the trademark gate, so both systems agree on SLD extraction
**Disadvantages:**
- Adds one dependency (30KB, zero sub-dependencies)
- Increases parse time marginally for the first call (PSL is loaded into memory) — negligible for DOMINUS volumes
- `psl.parse()` returns `tld` without a leading dot, requiring a `.` prefix to maintain the existing `ParsedDomain` interface
**Cost Implications:** Zero monetary cost. ~30 minutes to implement (install, replace implementation, update tests, remove dead code in trademark module).
**Risk Assessment:** Low. The PSL is a stable, widely-adopted standard. The `psl` package has a single responsibility and no open CVEs. Rollback is trivial (revert commit, restore `MULTI_PART_TLDS`).
---
### Option B: Expand the Curated Set
Keep the current `parseDomain` logic but grow the `MULTI_PART_TLDS` set to cover more suffixes — either by manually adding entries as they're encountered or by importing a PSL-derived list at build time.
**Advantages:**
- No new dependency
- Full control over which suffixes are recognised
- Current code path unchanged — no interface breakage
**Disadvantages:**
- Manual expansion is reactive and error-prone — closeout CSVs from different sources will keep surfacing unlisted suffixes
- The subdomain misclassification bug cannot be fixed without fundamentally changing the algorithm (the fallthrough to `parts[0]` is inherent to the approach)
- Build-time PSL import would require a code-generation step, adding more complexity than just using the library at runtime
- Ongoing maintenance burden falls entirely on the single developer
**Cost Implications:** Zero monetary cost. Ongoing developer time cost for each suffix addition.
**Risk Assessment:** Medium-high. The subdomain bug will eventually cause incorrect scores. Missing suffixes will produce incorrect SLDs that feed into trademark matching and scoring simultaneously — two wrong answers instead of one.
---
### Option C: Write a Minimal PSL Matcher In-House
Implement a Trie-based matcher using the PSL raw data file (`public_suffix_list.dat`), bundled as a resource in the repo.
**Advantages:**
- No external dependency
- Full control over the matching algorithm
- Educational value
**Disadvantages:**
- Significant development and testing effort (estimated 2-3 days for a correct implementation covering all edge cases)
- Ongoing maintenance — must re-download the PSL data file on each release
- Risk of subtle bugs in edge cases (wildcard rules, exception rules, Punycode)
- Reinventing a well-solved wheel — the `psl` package already does this correctly
- No benefit over Option A for the effort invested
**Cost Implications:** Zero monetary cost. ~2-3 days of developer time for initial implementation plus recurring maintenance.
**Risk Assessment:** High. A custom matcher is likely to have edge-case bugs that take months to surface. The `psl` package has been battle-tested by millions of users.
## Decision
**Chosen option: Option A — Adopt `psl` npm Package**
The rationale is straightforward:
1. **Correctness gap is real and growing.** The curated set covers 29 patterns. The PSL covers ~10,000+. Missing even one common suffix (e.g., `uk.com`, which has seen significant domain aftermarket activity) produces wrong SLD extraction in both the scoring engine and the trademark gate — two subsystems whose correctness is critical.
2. **Zero-cost, zero-maintenance.** Adding a 30KB package with no dependencies and 2M+ weekly downloads is a strict improvement over maintaining a hand-curated set. The PSL is updated by Mozilla as new TLDs and registration policies emerge — DOMINUS gets those updates free with `npm update`.
3. **Fixes the subdomain bug.** Option A is the only option that correctly handles `sub.domain.com` → `{ sld: 'domain', tld: '.com' }`. Neither Option B (curated expansion) nor Option C (in-house matcher) addresses the algorithmic fallthrough issue without fundamental redesign. Fixing this bug improves scoring accuracy for any candidate that arrives with subdomain-like formatting — rare in closeout lists but not impossible.
4. **Trademark gate alignment.** Both `domain.ts` and `match-detector.ts` currently duplicate the `MULTI_PART_TLDS` logic. Adopting `psl` in `domain.ts` and having `match-detector.ts` import `extractSld` from `domain.ts` eliminates the duplication entirely — a clean architecture win.
Option B (curated expansion) is rejected because it cannot fix the subdomain bug and creates ongoing maintenance drag. Option C (in-house matcher) is rejected because it duplicates an existing, proven library at a high development cost with higher risk of bugs.
## Consequences
### Positive
- Correct SLD/TLD extraction for every domain with a known PSL entry — eliminates the correctness gap entirely
- Zero-maintenance PSL updates ship with routine `npm update`
- Subdomain misclassification bug (`sub.domain.com` → `{ sld: 'domain', tld: '.com' }`) is fixed
- Duplicated SLD extraction logic in `match-detector.ts` is eliminated — trademark module uses `extractSld` from `domain.ts`
- No API or interface changes anywhere in the codebase — purely an implementation swap
### Negative
- One additional dependency (balanced against the elimination of ~35 lines of curated set + duplicate trademark logic)
- `psl.parse()` returns TLD without leading dot, requiring a `'.' + parsed.tld` transformation in `parseDomain` — minor, consistent with the existing interface
- Test expectations for the "unknown multi-part suffix fallthrough" case change (now correctly handled by PSL)
### Compliance and Security Implications
- PSL is a Mozilla standard with no compliance requirements
- No security implications — the `psl` package does not make network calls or evaluate untrusted input beyond string parsing
- The `psl` package has no known CVEs (checked at time of writing)
### Migration and Monitoring Plan
- **Installation**: `npm install psl` and `npm install -D @types/psl`
- **Code changes**: Replace `MULTI_PART_TLDS` usage in `parseDomain` with `psl.parse()`; remove duplicate `extractSld` from `match-detector.ts` in favour of the `domain.ts` export; update tests to reflect PSL-correct behavior for 3+ label domains
- **Rollback**: Revert the commit — `MULTI_PART_TLDS` and the duplicate trademark logic remain in git history
- **Validation**: Existing `parseDomain` tests must pass (with two updated expectations); trademark gate tests must pass with the imported `extractSld`
### Validation
- All 354 existing tests pass (post-update)
- `parseDomain` test suite confirms: vanilla gTLDs, multi-part ccTLDs, subdomain-only prefixes, empty input, and malformed input all produce the correct result
- Match-detector tests confirm `extractSld` from `domain.ts` produces identical results to the previous trademark-specific implementation for known suffixes
---
*This ADR was created following the MADR 4.0.0 standard. All DOMINUS ADRs should be consistent with the ADR series starting at `docs/adr/0001-project-architecture.md`. Template: `.claude/skills/adr/template.md`.*