Arm the S59 invariant by fixing its input; matchup sourcing = BUILDABLE
PART 1 -- PREMISE CORRECTION, then the real fix.
The order said the invariant's blocker was removed because "team is now
populated 416/416". It is not: what became 416/416 is home_team/away_team.
`team` (the PLAYER'S roster team) is still 0/416. Arming the guard off
home_team would compare the prop's game to itself -- always a match, a
permanent no-op that LOOKS armed. That would be worse than leaving it
disarmed, because it would read as a working guard.
The guard is also ALREADY fail-safe by construction (`if (knownTeam &&
gameTeams && ...)`), so Part 1's requirement was met in code all along.
What was missing was the data.
ROOT CAUSE: /sports/1/players returns currentTeam as { id, link } with NO
name, so searchPlayer's `hit.currentTeam?.name` was ALWAYS undefined and
every resolve returned team: null. The id is present on 1342/1342 and the
/teams list (already cached 24h) maps id -> name, so resolving it costs no
new request. Verified: Schwarber -> Philadelphia Phillies, Ohtani -> Los
Angeles Dodgers, Judge -> New York Yankees.
Five tests lock the fail-safe: drops only on a positive not-in-game;
abstains on unknown player team; abstains on unknown game participants;
and a row carrying only home_team/away_team does NOT satisfy the guard --
so the tautology can never be reintroduced.
PART 2 -- MATCHUP SOURCING: BUILDABLE. Measured on tonight's real board
against the free feeds, by VALUE not endpoint presence (the environment
trap: wired and null 634/634):
opposing starter 29/30 team-sides (home 14/15, away 15/15)
pitcher hand 1342/1342 (pitchHand.code)
batter hand 1342/1342 (batSide.code; L 416 / R 848 / S 78)
SHARED DEPENDENCY, and it is the finding: /sports/1/players -- a list we
ALREADY fetch and cache -- carries currentTeam.id, batSide AND pitchHand.
One join unlocks the invariant's input and two of the three matchup inputs
at once. The third (probable starter) comes from the schedule hydrate that
already exists.
So matchup is BUILDABLE and is the next order; SOURCE-LINEUPS-first is NOT
needed. Archetype-level reach on the opposing starter is available too
(the SP resolves to a player id, so the existing classifier applies) --
noted, not built.
Champion p_win, ranking, calibration and both accruing verdicts untouched.
Gates: 4,082 tests / 327 suites green; next build exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
@@ -304,11 +304,28 @@ async function searchPlayer(name, season = DEFAULT_SEASON, opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!hit) return null;
|
if (!hit) return null;
|
||||||
|
// TEAM NAME RESOLUTION (2026-08-01). `/sports/1/players` returns
|
||||||
|
// `currentTeam: { id, link }` with NO `name`, so `hit.currentTeam?.name` was
|
||||||
|
// ALWAYS undefined and every resolve returned team: null. Measured downstream:
|
||||||
|
// `team` was null on 416/416 stored grades, which silently disarmed the S59
|
||||||
|
// slate join invariant (it can only drop a prop when it KNOWS the player's
|
||||||
|
// team) — a guard that looked armed and could never fire.
|
||||||
|
//
|
||||||
|
// The id is present on 100% of the list, and the /teams list (already cached
|
||||||
|
// 24h) maps id -> name, so this costs no new request.
|
||||||
|
const teamId = teamConfirmed ? (hit.currentTeam?.id ?? null) : null;
|
||||||
|
let teamName = teamConfirmed ? (hit.currentTeam?.name ?? null) : null;
|
||||||
|
if (teamName == null && teamId != null) {
|
||||||
|
try {
|
||||||
|
const row = (await ensureTeams()).find((t) => t.id === teamId);
|
||||||
|
if (row && row.name) teamName = row.name;
|
||||||
|
} catch { /* honest-absent: an unresolved team stays null, never guessed */ }
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
id: hit.id,
|
id: hit.id,
|
||||||
fullName: hit.fullName ?? name,
|
fullName: hit.fullName ?? name,
|
||||||
team: teamConfirmed ? (hit.currentTeam?.name ?? null) : null,
|
team: teamName,
|
||||||
teamId: teamConfirmed ? (hit.currentTeam?.id ?? null) : null,
|
teamId,
|
||||||
position: hit.primaryPosition?.abbreviation ?? null,
|
position: hit.primaryPosition?.abbreviation ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S59 SLATE JOIN INVARIANT — re-armed 2026-08-01.
|
||||||
|
*
|
||||||
|
* The invariant drops a graded prop whose player's REAL team is not a
|
||||||
|
* participant in that game. It was written correctly and was silently INERT:
|
||||||
|
* `mlbStatsAdapter.searchPlayer` returned `team: null` for every player
|
||||||
|
* (`/sports/1/players` gives `currentTeam: { id, link }` with no `name`), so
|
||||||
|
* `team` was null on 416/416 stored grades and the guard could never fire.
|
||||||
|
*
|
||||||
|
* Two properties are locked here:
|
||||||
|
* 1. FAIL-SAFE — it drops ONLY on a positive not-in-game. Unknown team, or
|
||||||
|
* unknown game participants, ABSTAIN. Dropping a real prop on absent data
|
||||||
|
* would be a new fabrication, worse than the bad row it guards against.
|
||||||
|
* 2. It must key off the PLAYER'S team, never the prop's own game teams —
|
||||||
|
* comparing a game to itself always matches and would re-disarm it while
|
||||||
|
* looking armed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { buildPlayerStripsFromProps } = require('../../web/src/lib/slateAdapter');
|
||||||
|
|
||||||
|
const prop = (player, team) => ({ player, stat_type: 'hits', line: 1.5, side: 'O', team });
|
||||||
|
const GAME = { home: 'Philadelphia Phillies', away: 'New York Mets' };
|
||||||
|
const players = (strips) => strips.map((s) => s.player);
|
||||||
|
|
||||||
|
describe('join invariant — fail-safe by construction', () => {
|
||||||
|
it('DROPS a prop whose player is positively not in this game', () => {
|
||||||
|
const out = buildPlayerStripsFromProps(
|
||||||
|
[prop('In Game', 'Philadelphia Phillies'), prop('Wrong Game', 'Toronto Blue Jays')],
|
||||||
|
{}, {}, Date.now(), GAME,
|
||||||
|
);
|
||||||
|
expect(players(out)).toEqual(['In Game']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ABSTAINS when the player team is unknown — never drops on absent data', () => {
|
||||||
|
const out = buildPlayerStripsFromProps(
|
||||||
|
[prop('No Team', null), prop('Empty Team', '')],
|
||||||
|
{}, {}, Date.now(), GAME,
|
||||||
|
);
|
||||||
|
expect(players(out)).toEqual(['No Team', 'Empty Team']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ABSTAINS when the GAME participants are unknown', () => {
|
||||||
|
const rows = [prop('Anyone', 'Toronto Blue Jays')];
|
||||||
|
expect(players(buildPlayerStripsFromProps(rows, {}, {}, Date.now(), null))).toEqual(['Anyone']);
|
||||||
|
expect(players(buildPlayerStripsFromProps(rows, {}, {}, Date.now(), {}))).toEqual(['Anyone']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('matches on either participant, and on abbreviation or full name', () => {
|
||||||
|
const out = buildPlayerStripsFromProps(
|
||||||
|
[prop('Home Guy', 'Philadelphia Phillies'), prop('Away Guy', 'New York Mets')],
|
||||||
|
{}, {}, Date.now(), GAME,
|
||||||
|
);
|
||||||
|
expect(players(out)).toEqual(['Home Guy', 'Away Guy']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('the invariant’s input is the PLAYER team, not the prop’s own game', () => {
|
||||||
|
it('a grade carrying only home_team/away_team does NOT satisfy the guard', () => {
|
||||||
|
// home_team describes the game the prop came from, so using it as the
|
||||||
|
// player's team compares the game to itself: always a match, guard dead.
|
||||||
|
const row = { player: 'X', stat_type: 'hits', line: 1.5, side: 'O',
|
||||||
|
home_team: 'Toronto Blue Jays', away_team: 'Boston Red Sox' };
|
||||||
|
const out = buildPlayerStripsFromProps([row], {}, {}, Date.now(), GAME);
|
||||||
|
expect(players(out)).toEqual(['X']); // abstained — no player team known
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user