Skip to content

International Rating-System Audit — JP / US / EU (adversarial)

Status: Active · Last reviewed: 2026-08-11

Owner-commissioned adversarial audit (2026-08-11): three parallel agents each researched a target market's real rating ecosystem, then attacked the shipped code by attempting to write that market's RatingRegionConfig + tierCredentials entry, documenting everything inexpressible, mislabeled, or silently dishonest. Draft attack configs live in the session scratchpad; this doc is the distilled verdict + fix roadmap.

Verdict

The rating ENGINE is genuinely portable; the display/calibration layer is KR-only in three structural ways; the market-selection plumbing is 0% wired. No target market ships today. The registry pattern itself survived the audit — every agent independently concluded the shape is right and the wiring/field-vocabulary is what fails.

One live KR-facing bug was found and fixed immediately (76b75349): session cards carried pre-recalibration NTRP bands (마스터 5.0–6.5) contradicting the wizard's ratified table (4.0–4.5) — two NTRP ladders in one app. skill-band.ts now derives from TIER_RATING_TABLE (drift structurally impossible, DRIFT PIN test added); the dead stale ranking.tierDesc/tierNtrp/tierUtr keys were deleted.

What the markets actually run

MarketDominant scaleDirectionDiscipline splitCredential cultureElite segregation
KR(none official) NTRP folklore + division laddersascendingdoubles-primary culturedivision podiums (신인부→오픈부, 개나리→국화, KASTA groups)YES — formal (선출 sanctions, amateur-only groups)
JPWTN (JTA adopting); 草トー per-organizer 級 labels; JOP for competitiveWTN: 40→1 (reversed)WTN splits S/Dnon-standardized per-platform levels (テニスベア Lv.1–9 etc.)partial, per-organizer, unenforced
USUSTA NTRP (ONE number, not discipline-split) + UTR (S/D split)ascendingNTRP: none; UTR: S/DNTRP-banded leagues/events; mixed = PAIR-SUM cap (7.0 = sum≤7.0)none — college/HS play is a normal self-rate input
DEDTB LK 1–25REVERSED (1=best)NONE — one LK fed by all formatsyour LK IS the credential (orders Medenspiele lineups)none
FRFFT classement (NC, 40…30/2…15/1…0, séries)ordinal, NON-NUMERICsingles-primaryyour classement IS the credentialnone
UKWTN (LTA fully adopted)40→1 reversedS/D splitWTN IS the credentialnone

Convergent findings (all three audits independently)

  1. BLOCKER — the registry is decorative: zero market plumbing. Every getRatingRegionConfig() / getMarketConventions() call site passes no argument (grep-verified). Nothing threads a market from profile (nationalityCode exists, never read for this) or club (adm1Slug is a different concept). TierInfoSheet bypasses the registry entirely (literal 'doubles'/'singles' against the module table). A perfect JP/DE config entry would be inert code.
  2. BLOCKER — TierRatingBand hardcodes the NTRP+UTR vocabulary. Fields are ntrpMin/ntrpMax/utrMin/utrMax; formatNtrpBand/formatUtrBand print literal "NTRP "/"UTR "; primaryScale/secondaryScale on the config are DEAD parameters (only a shape test reads them). Inexpressible: WTN (reversed 40→1), LK (reversed 25→1), FFT classement (non-numeric ordinal). Storing them in the NTRP slots would be a fabricated scale.
  3. BLOCKER — discipline-axis assumptions don't universalize. US NTRP is ONE number (the dual-column table would fabricate a "Doubles NTRP"); DE LK has NO discipline split; US mixed is a pair-sum GATE on two players' ratings, not a per-player track (inexpressible in player_ratings' one-row-per-player shape). The INTERNAL three-track ELO ruling stays correct — the mismatch is between internal tracks and each market's EXTERNAL anchor axes, which the config must be able to declare per market.
  4. MAJOR — EliteBackground is KR-pipeline-shaped and DB-frozen. The 7-value CHECK constraint is global (a market variance needs a migration, breaking the config-only promise). Content-wise: maps well to JP (실업단 ≈ pro_team), badly to US ('college' collapses D1..juco — a range wider than KR's whole ladder; no segregation culture) and EU (no analog at all — ex-pros just have low LK; eliteLadder: [] correctly expresses "no ladder" but the enum/write-path can't hold EU realities).
  5. MAJOR — the credential-ROW concept itself is KR-shaped. In DE/FR/UK your rating IS the credential (no division ladder exists); tierCredentials rows would need to switch semantic mode per market ("podium evidence" vs "rating range"), and a missing region subtree currently degrades SILENTLY (row just disappears). JP's 草トー labels are per-platform, non-standardized — citing them as if universal would be a specificity lie.
  6. MAJOR — SKILL_DISPLAY_MODE is a global constant (tier.config.ts), not per-region config, despite being the canonical per-market display decision.

What genuinely held up

  • Internal ELO engine + five earned tiers + promotion math: region-agnostic, no change needed for any market.
  • Three-track independence (internal): sound everywhere; the ruling stands.
  • The registry PATTERN (Record<string, RatingRegionConfig> + KR fallback) and eliteLadder: [].
  • market.config.ts JP/DE rows are content-correct (unreachable, but correct).
  • Anchors are BETTER abroad than at home: DTB LK and FFT classement are public per-player numbers; WTN/UTR have APIs; USTA NTRP is queryable. KR's claim→corroborate design is the hard case — every target market has an easier verification story once player_external_ratings (planned, not yet built) exists.

Fix roadmap (proposed S7 — owner decisions marked ◆)

  1. S7a Scale-generic bands. Replace NTRP/UTR-named fields with per-region scale descriptors: { scale: { id, name, direction: 'asc'|'desc', kind: 'decimal'|'ordinal', levels? }, bands: Record<EloTier, {min,max}| {levelRange}> }; formatters read the descriptor (kills the dead primaryScale params). Ordinal support covers FFT. ◆ approve shape.
  2. S7b Market plumbing. One resolver (club's country → rating region; profile fallback) threaded to getRatingRegionConfig(...)/getMarketConventions(...) call sites + TierInfoSheet de-hardcoded + SKILL_DISPLAY_MODE into the region config. Mechanical once the resolver rule is decided ◆ (club-country vs user-country precedence).
  3. S7c External-anchor axes per market. Config declares how external scales map onto internal tracks (external: [{scale, disciplines: 'per-discipline'|'unified'|'pair-sum'}]) so US-NTRP (unified) and UTR (split) coexist honestly; UI stops fabricating discipline-split labels where the market has none.
  4. S7d Elite background v2. Either widen the CHECK to a per-region-validated TEXT + region config owns the value set, or keep the 7 rungs as a universal coarse scale with per-region display mapping ◆. US 'college' sub-granularity (D1/D3) decision rides here.
  5. S7e Credential-row modes. tierCredentials entries become {mode: 'podium'|'rating-range', rows}; missing region falls back to rating-range mode derived from bands (kills the silent-undefined).
  6. S7f player_external_ratings (already in the overhaul blueprint §4) — unblocks the superior US/EU verification stories.

None of this blocks KR. The app-not-live clean-break window applies to S7a/S7c/S7d schema shapes — cheaper now than after launch.

Markdown remains the source of truth. Run yarn docs:check before handoff.