Build the conditioning registry, and a probe so "proven" stops drifting

The order opens with "two proven clusters live". They are not proven -- the
proven set is empty -- and this is the fourth consecutive order to start from
a stronger claim than the measurements support. Correcting that in prose four
times has not worked, so this session adds scripts/proven-status.js, which
recomputes the answer from the ledger: hits LOSES (-0.096, CI excluding zero),
total_bases INCONCLUSIVE (+0.004), strikeouts INCONCLUSIVE (+0.259 at n=57).
It deliberately reports sample readiness separately from recorded verdicts, so
"n>=500" can never again be read as "passed".

A counting error worth recording. The first read of the top-volume archetype
said BOMBER x hits was 641 rows -- gate-ready. It is 287. model_snapshots
holds one row per prop PER SNAPSHOT CYCLE, so joining it to ledger_entries
counts each ledger row once per cycle it appeared in. Deduping on the ledger
row id gives the true figure, and my own status script had the same bug until
it was fixed. That is the difference between running the gate and being short
by 213.

So no archetype x stat combination reaches the gate. BOMBER x hits at 287 is
the closest; pitcher archetypes are untestable at 58 settled strikeout rows
across all of them, so the pitcher half of this order could not be run.

The registry is built: recordConditioning keys archetype x underlying-skill x
interaction x status with measured lift, and the skill tag is MANDATORY and
enforced -- untagged entries are refused, and PROVEN without sufficient
evidence is refused. validatedSkills() returns the coherent profile as it
stands, which is {} for every archetype, by design.

BOMBER x hits conditioning was tested across the order's categories and every
result is underpowered: arsenal (barrel x breaking share) incremental +0.043,
batted-ball (launch x pitcher GB) +0.001, contact quality -0.020 and -0.015,
K x K -0.063. Within BOMBER the counter still leads on hits, 0.218 to 0.160,
consistent with the closed pooled negative.

One bug fixed mid-run: fromStatcastRow maps percentage and raw fields only and
does not carry pitch_mix, so the arsenal category first reported n=0 for every
row -- it was measuring nothing rather than failing. Without catching it,
"arsenal doesn't matter" would have been recorded from a column that was never
populated.

On defense: I looked for a derivable proxy before calling it unsourceable, and
there isn't one. We ingest no fielding data at all, and opposing pitchers'
hits-allowed conflates pitching with defense, so it would validate the wrong
skill. It needs Savant's fielding endpoint -- free, same host as the five
feeds already ingested -- and it is not sourced here, because sourcing it to
test at n=282 would answer nothing.

Nothing proved, so nothing was recalibrated and nothing shipped.

4,221 tests green (335 suites); web build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
This commit is contained in:
Kev
2026-08-03 21:49:51 -04:00
parent 9538e11198
commit ac1361486e
6 changed files with 468 additions and 5 deletions
+74
View File
@@ -268,8 +268,81 @@ function summary(sport) {
};
}
/**
* ── THE CONDITIONING REGISTRY ────────────────────────────────────────────
*
* Keyed archetype x underlying-skill x conditioning-interaction x status, with
* the measured out-of-sample lift.
*
* WHY THE SKILL TAG IS THE POINT. A proven interaction is not just "this feature
* helps this stat" — it is evidence that ONE UNDERLYING SKILL is real and
* measurable for this archetype. `barrel x park` proving would validate
* POWER-SKILL; `stuff x lineup-K` proving would validate WHIFF-SKILL. Tagging by
* skill is what lets a later model derive ALL of a player's props from one
* profile instead of fitting each prop separately — so the tag is the coherent
* profile taking shape, not bookkeeping.
*
* It starts EMPTY of proven entries, deliberately. Nothing has beaten the
* counter out-of-sample, so nothing has a skill validated. Seeding it with
* hopeful rows would defeat the purpose exactly as seeding PROVEN features would.
*/
const SKILLS = Object.freeze({
POWER: 'power-skill — how far and how hard the ball leaves the bat',
CONTACT: 'contact-skill — whether bat meets ball at all',
SPEED: 'speed-skill — beating out contact, taking the extra base',
WHIFF: 'whiff-skill — a pitcher missing bats',
COMMAND: 'command-skill — locating and expanding the zone',
OPPORTUNITY: 'opportunity — plate appearances / batters faced, not skill at all',
});
const conditioning = [];
/**
* Record a conditioning result. `skill` MUST name the underlying skill the
* interaction validates — an untagged proven entry cannot contribute to a
* coherent profile, so it is refused.
*/
function recordConditioning({ sport, archetype, stat, interaction, skill, status, lift = null, evidence = null }) {
if (!Object.keys(SKILLS).includes(String(skill || '').toUpperCase())) {
return { ok: false, reason: 'untagged_or_unknown_skill', known: Object.keys(SKILLS) };
}
if (!Object.values(STATUS).includes(status)) return { ok: false, reason: 'bad_status' };
if (status === STATUS.PROVEN && !isSufficient(evidence)) {
return { ok: false, reason: 'insufficient_evidence_for_proven' };
}
const row = {
sport: String(sport || '').toLowerCase(),
archetype: String(archetype || '').toUpperCase(),
stat: String(stat || '').toLowerCase(),
interaction, skill: String(skill).toUpperCase(), status, lift, evidence,
};
conditioning.push(row);
return { ok: true, row };
}
/** The conditioning map for one archetype — what is proven, pending, dead. */
function conditioningFor(sport, archetype) {
const sp = String(sport || '').toLowerCase();
const ar = String(archetype || '').toUpperCase();
return conditioning.filter((c) => c.sport === sp && c.archetype === ar);
}
/**
* The SKILLS VALIDATED for an archetype — the coherent profile as it stands.
* Only PROVEN entries count: a candidate interaction validates nothing.
*/
function validatedSkills(sport, archetype) {
const out = {};
for (const c of conditioningFor(sport, archetype)) {
if (c.status !== STATUS.PROVEN) continue;
(out[c.skill] = out[c.skill] || []).push({ stat: c.stat, interaction: c.interaction, lift: c.lift });
}
return out;
}
/** Test-only: restore the declared statuses so suites cannot leak into each other. */
function __reset() {
conditioning.length = 0;
statVerdicts.clear();
byKey.clear();
for (const f of FEATURES) byKey.set(`${f.sport}|${f.key}`, { ...f, evidence: f.evidence || null, history: [] });
@@ -279,5 +352,6 @@ module.exports = {
STATUS, MIN_PROMOTION_N,
allFeatures, liveFeatures, candidateFeatures, statusOf, isLive,
recordStatVerdict, statusForStat, liveFeaturesForStat, candidateFeaturesForStat,
SKILLS, recordConditioning, conditioningFor, validatedSkills,
isSufficient, promote, demote, summary, __reset,
};