Files
vyndr/docs/ARCHETYPE-AXES.md
T
builtbykev 7ac6aa73e3 Layer 2: multi-axis archetype classifier; the FLEX fallback is gone
A player is a blend across independent axes, not one label. Skubal is a STARTER
and a strikeout arm and a ground-ball arm and a control arm — four true things
at once, and single-label classification threw three of them away.

AXIS INDEPENDENCE WAS MEASURED, NOT ASSUMED. Correlations over the live store
(467 batters, 531 pitchers); anything |r| >= 0.70 is one underlying trait and
was collapsed so we never show one trait as two archetypes. Batter k% ~ whiff%
+0.89, hard-hit% ~ exit velo +0.88, chase% ~ swing% +0.87, chase% ~ bb% -0.72;
pitcher k% ~ whiff% +0.76, gb% ~ fb% -0.73 — all collapsed.

The survivors are genuinely orthogonal, and one result is worth stating: pitcher
velocity correlates +0.14 with K%, +0.07 with whiff% and +0.07 with GB%.
Velocity is NOT a proxy for missing bats — a hard thrower who misses no bats is
a real distinct type, so CANNON earns its own axis rather than being folded into
STRIKEOUT. Pitcher K% ~ GB% is -0.10, so PUNCHOUT and SINKER are independent,
which is exactly the multi-axis thesis.

Cut-lines are the measured p75 (distinctive) and p90 (elite), per role where the
tails differ even when the medians agree: reliever GB% p90 is 54.1 against a
starter's 48.9, both with a median of 42.5.

THE FALLBACK IS DELETED. classify() used to return FLEX (mlb) / SHIELD (wnba) /
CONNECTOR (nba) at weight 1.0 when nothing scored — "could not classify"
rendered as a fully-confident classification of a real archetype, with
descriptive education copy attached. 8 of 18 MLB players carried it, and FLEX
could never be earned because its only scoring input had zero writers. Every
sport now does what MMA already did: unclassified is absent.

Induced on real players. Skubal: STARTER, throws L, WHIFF + SEAM + PINPOINT, all
elite. Judge: BOMBER + GRINDER + WHIFF RISK — elite power, patient, strikes out,
three true things. Kwan: SURGEON + SNIPER + SLASH with NO power claimed (0.4
barrel% is absent, not "low power"). Josh Bell, who used to classify as DRIVER:
empty blend, "No standout profile — league-average across every measured axis."
Alan Roden, who was FLEX at weight 1.0 on 21 PA: every axis absent, "Not enough
plate appearances yet — no profile claimed."

Per-axis honest-absence holds: a velo-less pitcher keeps every other axis, and
NO DATA is distinguishable from LEAGUE-AVERAGE rather than collapsing into one
shrug. The full vector is stored for Layer 3; only the top three distinctive
traits surface.

Three existing tests asserted the fallback and were updated to assert absence.
One of them surfaced a real robustness gap: classify(sport, null) threw, because
an explicit null does not trigger a default parameter and every scorer
dereferences its argument. Guarded.

Every baseball name is accounted for in docs/ARCHETYPE-AXES.md — built, alias,
tier, or shelved with its unlock condition. Zero orphans; cross-sport names left
for their sport.

Tests 3601 passed / 293 suites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
2026-07-20 22:38:32 -04:00

4.5 KiB
Raw Blame History

MLB ARCHETYPE AXES — Layer 2

A player is a blend across independent axes, not one label. Skubal is a STARTER and a strikeout arm and a ground-ball arm and a control arm — four true things at once. Single-label classification is lossy, and its failure mode is the FLEX disease: when nothing matches, invent a bucket.

There is no fallback. Unremarkable on an axis → absent on that axis. A genuinely average player surfaces nothing, and says so.

Axis independence — measured, not assumed

Correlations over the live store (467 batters PA≥50, 531 pitchers IP≥10). |r| ≥ 0.70 = one underlying trait → collapsed, so one trait is never shown as two archetypes.

Pair r Decision
batter k% ~ whiff% +0.89 collapsed → one SWING-AND-MISS axis
batter hard-hit% ~ avg exit velo +0.88 folded into POWER as intensity
batter chase% ~ swing% +0.87 collapsed → one AGGRESSION axis
batter chase% ~ bb% 0.72 same axis inverted (discipline)
pitcher k% ~ whiff% +0.76 collapsed → one STRIKEOUT axis
pitcher gb% ~ fb% 0.73 collapsed → one signed tilt axis
batter barrel% ~ hard-hit% +0.70 at the line; barrel leads
pitcher velo ~ k% +0.14 INDEPENDENT — velo earns its own axis
pitcher velo ~ whiff% +0.07 independent
pitcher velo ~ gb% +0.07 independent
pitcher k% ~ gb% 0.10 INDEPENDENT — PUNCHOUT ⊥ SINKER
batter barrel ~ launch +0.23 independent
batter k% ~ chase% +0.05 independent

The velocity result matters: velo is not a proxy for missing bats. A hard thrower who misses no bats is a real, distinct type.

Cut-lines

p75 = distinctive · p90 = elite, from the measured distribution — per role where the tails differ even when medians agree (reliever GB% p90 = 54.1 vs starter 48.9, both median 42.5). Sample floors: PA ≥ 50 · IP ≥ 10.

Every baseball name accounted for

Name Status Axis / reason
WORKHORSE TIER role=starter + high IP; the old hardcoded default is gone
PUNCHOUT / WHIFF BUILT strikeout (p75 / p90)
SINKER / SEAM BUILT ground_ball
FLY BALL / ELEVATOR BUILT fly_ball
SURGEON ARM / PINPOINT BUILT control (low BB%)
NIBBLER / SCATTERGUN BUILT wild (high BB%)
BAIT / TRAPDOOR BUILT chase
HITTABLE / BATTING PRACTICE BUILT contact_allowed
CANNON / HOWITZER BUILT velocity — unlocked by the 53%→99% velo fix
SIDEARM / SUBMARINE BUILT slot (low arm angle)
STARTER / RELIEVER / CLOSER / SETUP BUILT role, from real usage
SLUGGER / BOMBER BUILT power (barrel%)
TECHNICIAN / SURGEON BUILT contact (low K%)
WHIFF RISK / WINDMILL BUILT swing_miss
GRINDER / SNIPER BUILT patience
FREE SWINGER / HACKER BUILT aggression
TOPSPIN / LOFT BUILT launch
SLASH / BARREL FINDER BUILT line_drive
HAMMER ALIAS of chase/strikeout — best-pitch whiff r=0.50 with overall; not independent enough for its own axis
BRUSH ALIAS of contact (TECHNICIAN)
DRIVER ALIAS of power — RBI is lineup context, not a player trait
FLEX RETIRED it was the fallback, never earned: utility had zero writers
MIRROR SHELVED needs switch-hitter axis; bats='S' now joined — unlock when a platoon-split feed lands
GHOST / BURNER / LEG SHELVED speed: SB is statsapi, not yet joined into the aggregate store
CATALYST SHELVED table-setter: needs lineup slot (not ingested)
HYBRID BUILT-ADJACENT two-way = both role profiles exist (position TWP)
ALPHA ALIAS of strikeout+control combined; the blend expresses it
FASTBREAK · ENFORCER · CHAINMOVER · CONDUCTOR · ARCHITECT · TORCH · LINK · ARTILLERY · BELL COW · etc. OTHER SPORT NBA/WNBA/NFL names — correctly not built here

Zero orphans.

Output shape

{
  role, roleDetail, roleLabel, sufficient, sample, bats, throws,
  vector: { axisKey: {tier,label,value,strength} | null },  // Layer 3 reads ALL
  blend:  [ {label, axis, tier, value, strength} ],          // top ≤3 to surface
  absent: [axisKey],   // NO DATA — distinct from unremarkable (vector null)
  note                  // honest copy when the blend is empty
}

absent vs a null vector entry is a real distinction: absent means we could not measure it, null means we measured it and he is ordinary.