Skip to content

fix(baseball): migrate player-today/passport/snapshot-cards off legacy stat layer (#379) - #845

Merged
njrini99-code merged 2 commits into
batch/bbh-finish-0714from
task/379-player-reads
Jul 15, 2026
Merged

fix(baseball): migrate player-today/passport/snapshot-cards off legacy stat layer (#379)#845
njrini99-code merged 2 commits into
batch/bbh-finish-0714from
task/379-player-reads

Conversation

@njrini99-code

@njrini99-code njrini99-code commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the #379 reconciliation design's chunk "Migrate player-today.ts, player-snapshot-cards.ts, player-passport.ts", building on the already-merged Phase 0 seed fix (#827) and shared adapter module (#828).

Per-file changes

src/lib/baseball/read-models/player-today.ts

  • recentStats ("last few captured stat sessions") now reads baseball_box_score_batting/_pitching (game ids for this player) joined to baseball_games, instead of the deprecated baseball_player_stats table.
  • Box-score rows carry no CSV-import provenance columns (they're staff-entered via the box-score save flow, not imported) — trust/provenance are now honestly null instead of a fabricated import stamp.
  • Manifest entry deleted (file no longer references either deprecated table).

src/lib/baseball/read-models/player-passport.ts

src/lib/baseball/read-models/player-snapshot-cards.ts

  • Exit-velocity fields (avgExitVelocity/maxExitVelocity) now derive from baseball_batted_ball_events via elite-stat-events.ts's own exported buildHitterMetrics aggregator (same official/scrimmage context default, same current-row superseded_by_run_id filter) — closes the file's former "typed but un-migrated" comment.
  • Deliberately partial: this file still reads baseball_player_aggregates for (a) the Hitting/Pitching season-average legacy-fallback tier and (b) the game/scrimmage/practice "Performance" card. Neither has a canonical replacement today — stats-center.ts exposes official-vs-all splits, not a standalone scrimmage split, and neither canonical layer has a practice-session shape yet (the design doc's own open question). Forcing a full cutover here would either regress real teams or require new scope outside this chunk. Manifest note updated to document this precisely; entry not deleted.

src/lib/baseball/read-models/legacy-stat-adapters.ts + its test (closes the #828 residual the verifier flagged)

  • adaptLegacyPlayerStats never read legacy.avg_pitch_velocity/max_pitch_velocity. Wired through as a per-field fallback: an explicit event-grain reading still wins outright when supplied, otherwise falls back to the legacy scalar (a real, previously-captured measurement — not a fabrication). Exit velocity has no legacy column at all and stays event-only/null-safe, unchanged.
  • Test file extended with fallback, precedence (event wins when both present), and no-data cases.

src/lib/baseball/stat-layer-manifest.ts — entries updated per above.

Also touched (not in the original chunk file list, but directly affected):

  • src/contracts/baseball/product-trust/player-today-honest-loop.test.ts — its "recent stats never fabricate provenance" tests asserted against baseball_player_stats fixture rows; replaced with box-score/baseball_games fixtures pinning the same honesty invariant (trust/provenance always null).
  • src/contracts/baseball/access/player-today-self-scope.test.ts — fixture table list swapped to the box-score tables so the self-scope contract keeps exercising the real (non-mocked) getPlayerToday DB path.
  • src/app/baseball/(player-dashboard)/player/today/__tests__/page.test.tsx — reviewed, no change needed (getPlayerToday/getPlayerPassport are fully mocked at the module boundary there).

Gate evidence

  • npm run typecheck — clean.
  • npx eslint --max-warnings 0 <8 touched files> — clean, no output.
  • npx vitest run across all affected/new test files (legacy-stat-adapters, roster-aggregates-merge, player-snapshot-cards, player-passport-innings, player-today-honest-loop, player-today-self-scope, stat-layer-contract, player/today page.test.tsx) — 31 test files / 249 tests, all passing.
  • stat-layer-contract.test.ts (the BaseballHelm: consolidate legacy flat stats, box-score stats, and elite stat-event imports #381 CI guard) passes cleanly against the updated manifest — verified both directions (no new offenders, no stale entries for the two deleted files).

Base-branch residuals fixed in passing (manifest only)

The #381 stat-layer contract test was already red on batch/bbh-finish-0714 before this PR, in both scan directions. Since this PR edits the manifest anyway, both bookkeeping residuals are corrected here (no production code touched for either):

  • Stale entry deleted: src/app/baseball/actions/insights.tsfix(baseball): migrate discover.ts/insights.ts actions onto withBaseballAction (#394) #819 (merged tonight) migrated it off the deprecated tables but did not delete its GRANDFATHERED_CONSUMERS entry in the same commit, tripping the contract test's stale-entry direction.
  • Missing entry added: src/app/baseball/actions/__tests__/practice-effectiveness.test.tstest(baseball): add practice + practice-effectiveness action coverage #825 (merged tonight) landed this test with a fake-supabase fixture seeding baseball_player_stats, without a manifest entry, tripping the offender direction. Entry added mirroring the grandfathered production file it tests (practice-effectiveness.ts, which stays excluded from this chunk per the design's open practice-shape question).

Two other base-branch offenders (player-today-self-scope.test.ts, player-today-honest-loop.test.ts, both from #826) are resolved naturally by this PR's fixture migration — they no longer reference a deprecated table at all.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

@cursor

cursor Bot commented Jul 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
helmv3 Ignored Ignored Preview Jul 15, 2026 9:48am

Request Review

@supabase

supabase Bot commented Jul 15, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project qmnssrrolpinvwjjnufo because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (3)
  • main
  • develop
  • release/*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eff99b4b-fb43-46b3-81d7-7903e12e22b1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/379-player-reads
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task/379-player-reads

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

Migrates three BaseballHelm read models (player-today, player-passport, player-snapshot-cards) off the deprecated flat baseball_player_stats / baseball_player_aggregates layer onto the canonical box-score and batted-ball-event tables, and closes a legacy-stat-adapters residual by wiring the pre-existing avg_pitch_velocity / max_pitch_velocity legacy scalars as honest fallbacks.

  • player-today / player-passport: recentStats / recentActivity now read baseball_box_score_batting/_pitching joined to baseball_games; import-run N+1 lookup removed; trust/provenance are correctly null (no CSV import lineage on box-score rows). A near-identical fetchRecentBoxScoreActivity helper is defined in both files — worth consolidating into the shared adapter module from feat(baseball): shared legacy-flat stat adapter module (#379) #828 to prevent drift.
  • player-snapshot-cards: Exit velocity migrated to baseball_batted_ball_events via buildHitterMetrics; context pre-filtering and superseded_by_run_id guard match the elite-stat-events canonical path. baseball_player_aggregates reads remain for the season-average legacy-fallback tier and the practice-session Performance card (correctly documented as an open design question).
  • legacy-stat-adapters: Pitch-velocity legacy-scalar fallback wired correctly with explicit event-wins precedence; four new tests cover all cases.
  • stat-layer-manifest: Two fully-migrated entries removed, player-snapshot-cards note updated, insights.ts entry correctly removed (confirmed: file no longer reads legacy tables), practice-effectiveness.test.ts backfilled.

Confidence Score: 4/5

Safe to merge. The migration is mechanically correct and the honesty invariants (trust/provenance always null for box-score rows) are re-proved by updated contract tests.

The core data-source swap is clean: new queries are properly tenant-scoped, error paths degrade gracefully, and the stat-layer-contract CI guard passes. Two observations hold the score below 5: (1) fetchRecentBoxScoreActivity is duplicated verbatim across player-today.ts and player-passport.ts with a subtle interface difference (game_type field) and an inconsistent client cast — future changes to the query shape will need to be applied in two places; (2) capturedSessions on the passport silently changes semantics for two-way players (game count vs. stat-row count), which may confuse coaches comparing before/after numbers.

player-passport.ts — duplicated helper and the two-way-player capturedSessions count shift are both concentrated here.

Important Files Changed

Filename Overview
src/lib/baseball/read-models/player-today.ts Migrates recentStats off baseball_player_stats onto baseball_box_score_batting/pitching joined to baseball_games via a new fetchRecentBoxScoreActivity helper; error propagation updated correctly; statType now surfaces game_type values rather than stat_type values.
src/lib/baseball/read-models/player-passport.ts Migrates recent-activity card off baseball_player_stats; removes the import-run review-state N+1 lookup; capturedSessions now counts distinct games (not stat rows), and lastSessionTrust/Provenance are honestly null. Defines a near-duplicate of player-today.ts's fetchRecentBoxScoreActivity with an unnecessary (supabase as any) cast.
src/lib/baseball/read-models/player-snapshot-cards.ts Exit-velocity fields migrated off baseball_player_stats onto baseball_batted_ball_events via buildHitterMetrics; maxExitVelocity correctly derived from evLaPoints; context pre-filtering and superseded_by_run_id guard mirror the elite-stat-events canonical path.
src/lib/baseball/read-models/legacy-stat-adapters.ts Wires avg_pitch_velocity/max_pitch_velocity legacy scalars as a per-field fallback behind explicit event-grain readings; exit velocity correctly stays event-only; module doc and interface JSDoc updated to match.
src/lib/baseball/read-models/tests/legacy-stat-adapters.test.ts Adds fallback, event-wins-over-legacy, and no-data cases for pitch-velocity fields; corrects a misleading test description for the null exit-velocity case.
src/lib/baseball/stat-layer-manifest.ts Removes three fully-migrated entries, updates player-snapshot-cards entry with precise partial-migration note, adds missing practice-effectiveness.test.ts entry — all consistent with the verified passing stat-layer-contract.test.ts run.
src/contracts/baseball/product-trust/player-today-honest-loop.test.ts Replaces flat-stat-table fixtures with box-score fixtures; adds scrimmage-only and failing-read test cases; honesty invariant (trust/provenance always null) is re-proved against the new source.
src/contracts/baseball/access/player-today-self-scope.test.ts Table list in the fixture swapped from baseball_player_stats to the three box-score tables; no other contract logic changed; self-scope access contract preserved.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[getPlayerToday / getPlayerPassport / getPlayerSnapshotCards] --> B{Read source}
    B -- recentStats / recentActivity --> C[fetchRecentBoxScoreActivity]
    C --> D[baseball_box_score_batting\n.select game_id]
    C --> E[baseball_box_score_pitching\n.select game_id]
    D & E --> F[Set dedup game IDs]
    F --> G[baseball_games\n.select id game_date game_type opponent_name\n.order game_date desc .limit N]
    G --> H[PlayerTodayStat / recentActivity\ntrust: null provenance: null]
    B -- exit velocity --> I[baseball_batted_ball_events\ndata_context in official_game scrimmage\nsuperseded_by_run_id IS NULL\nbatter_id = playerId]
    I --> J[buildHitterMetrics\navg_exit_velocity metric\nevLaPoints for max]
    J --> K[avgExitVelocity / maxExitVelocity]
    B -- pitch velocity --> L[adaptLegacyPlayerStats]
    L --> M{event input?}
    M -- yes --> N[event.avgPitchVelocity wins]
    M -- no --> O[legacy.avg_pitch_velocity fallback]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[getPlayerToday / getPlayerPassport / getPlayerSnapshotCards] --> B{Read source}
    B -- recentStats / recentActivity --> C[fetchRecentBoxScoreActivity]
    C --> D[baseball_box_score_batting\n.select game_id]
    C --> E[baseball_box_score_pitching\n.select game_id]
    D & E --> F[Set dedup game IDs]
    F --> G[baseball_games\n.select id game_date game_type opponent_name\n.order game_date desc .limit N]
    G --> H[PlayerTodayStat / recentActivity\ntrust: null provenance: null]
    B -- exit velocity --> I[baseball_batted_ball_events\ndata_context in official_game scrimmage\nsuperseded_by_run_id IS NULL\nbatter_id = playerId]
    I --> J[buildHitterMetrics\navg_exit_velocity metric\nevLaPoints for max]
    J --> K[avgExitVelocity / maxExitVelocity]
    B -- pitch velocity --> L[adaptLegacyPlayerStats]
    L --> M{event input?}
    M -- yes --> N[event.avgPitchVelocity wins]
    M -- no --> O[legacy.avg_pitch_velocity fallback]
Loading

Comments Outside Diff (1)

  1. src/lib/baseball/read-models/player-snapshot-cards.ts, line 562-571 (link)

    P2 maxExitVelocity derived from evLaPoints rather than a dedicated metric

    buildHitterMetrics exposes avg_exit_velocity as a named metric (computed via avgOf) but has no max_exit_velocity metric key. Extracting the max from evLaPoints is correct today, but if buildHitterMetrics ever adds a max_exit_velocity metric in elite-stat-events.ts (the natural follow-on to the existing avg_exit_velocity metric), this file will silently use a different derivation than the Stats Lab. A brief comment noting "no max_exit_velocity metric exists in buildHitterMetrics — derived from evLaPoints instead" would make the intentionality explicit for future readers.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/baseball/read-models/player-snapshot-cards.ts
    Line: 562-571
    
    Comment:
    **`maxExitVelocity` derived from `evLaPoints` rather than a dedicated metric**
    
    `buildHitterMetrics` exposes `avg_exit_velocity` as a named metric (computed via `avgOf`) but has no `max_exit_velocity` metric key. Extracting the max from `evLaPoints` is correct today, but if `buildHitterMetrics` ever adds a `max_exit_velocity` metric in `elite-stat-events.ts` (the natural follow-on to the existing `avg_exit_velocity` metric), this file will silently use a different derivation than the Stats Lab. A brief comment noting "no max_exit_velocity metric exists in buildHitterMetrics — derived from evLaPoints instead" would make the intentionality explicit for future readers.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
src/lib/baseball/read-models/player-passport.ts:354-395
**Duplicated `fetchRecentBoxScoreActivity` — extract to shared utility**

`player-today.ts` defines an identically-named `fetchRecentBoxScoreActivity` with the same three-step logic (batting ids → pitching ids → Set dedup → games fetch). The two copies will drift: `player-today.ts` already returns `game_type` in its interface while this version doesn't, and the typed-vs-any client inconsistency is a result of the same copy-paste. Additionally, because `baseball_box_score_batting`, `baseball_box_score_pitching`, and `baseball_games` are all in `database.ts` (evidenced by `player-today.ts` using the typed client cleanly with no cast), the `(supabase as any)` cast here is unnecessary.

Consider extracting to `src/lib/baseball/read-models/box-score-activity.ts` (or adding to the shared adapter module from #828) so both read models share one implementation. If a follow-up PR is the right venue, a `// TODO #NNN` note on both copies would keep the duplication explicit.

### Issue 2 of 3
src/lib/baseball/read-models/player-passport.ts:471-485
**`capturedSessions` count semantics change for two-way players**

The old code counted rows in `baseball_player_stats` (which could have separate batting and pitching rows per game for a two-way player). The new code counts distinct games via `Set` dedup in `fetchRecentBoxScoreActivity`. A pitcher who also bats now counts each game once instead of twice, so `capturedSessions` will be lower than before for those players. The change is semantically correct (a game is one activity, not two), but the passport's "captured sessions" label may have been calibrated to the old per-row count. Worth verifying that coaches viewing a two-way player's passport don't interpret the lower count as missing data.

### Issue 3 of 3
src/lib/baseball/read-models/player-snapshot-cards.ts:562-571
**`maxExitVelocity` derived from `evLaPoints` rather than a dedicated metric**

`buildHitterMetrics` exposes `avg_exit_velocity` as a named metric (computed via `avgOf`) but has no `max_exit_velocity` metric key. Extracting the max from `evLaPoints` is correct today, but if `buildHitterMetrics` ever adds a `max_exit_velocity` metric in `elite-stat-events.ts` (the natural follow-on to the existing `avg_exit_velocity` metric), this file will silently use a different derivation than the Stats Lab. A brief comment noting "no max_exit_velocity metric exists in buildHitterMetrics — derived from evLaPoints instead" would make the intentionality explicit for future readers.

Reviews (1): Last reviewed commit: "fix(baseball): migrate player-today/pass..." | Re-trigger Greptile

Comment on lines +471 to +485
if (battingRes.error || pitchingRes.error) {
return { data: [], error: 'Recent activity could not be loaded.' };
}

const gameIds = [
...new Set([
...((battingRes.data ?? []) as Array<{ game_id: string }>).map((r) => r.game_id),
...((pitchingRes.data ?? []) as Array<{ game_id: string }>).map((r) => r.game_id),
]),
];
if (gameIds.length === 0) return { data: [], error: null };

const { data: games, error: gamesErr } = await db
.from('baseball_games')
.select('id, game_date, opponent_name')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 capturedSessions count semantics change for two-way players

The old code counted rows in baseball_player_stats (which could have separate batting and pitching rows per game for a two-way player). The new code counts distinct games via Set dedup in fetchRecentBoxScoreActivity. A pitcher who also bats now counts each game once instead of twice, so capturedSessions will be lower than before for those players. The change is semantically correct (a game is one activity, not two), but the passport's "captured sessions" label may have been calibrated to the old per-row count. Worth verifying that coaches viewing a two-way player's passport don't interpret the lower count as missing data.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/baseball/read-models/player-passport.ts
Line: 471-485

Comment:
**`capturedSessions` count semantics change for two-way players**

The old code counted rows in `baseball_player_stats` (which could have separate batting and pitching rows per game for a two-way player). The new code counts distinct games via `Set` dedup in `fetchRecentBoxScoreActivity`. A pitcher who also bats now counts each game once instead of twice, so `capturedSessions` will be lower than before for those players. The change is semantically correct (a game is one activity, not two), but the passport's "captured sessions" label may have been calibrated to the old per-row count. Worth verifying that coaches viewing a two-way player's passport don't interpret the lower count as missing data.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

Fable Integrator and others added 2 commits July 15, 2026 05:22
…y stat layer (#379)

Migrates the three remaining Phase-1 read-model consumers of the deprecated
flat/aggregate stat tables onto the canonical layers, per the #379
reconciliation design:

- player-today.ts: "recent activity" now reads baseball_box_score_batting/
  _pitching (joined to baseball_games) instead of baseball_player_stats.
  Box-score rows carry no CSV-import provenance, so trust/provenance are
  honestly null rather than a fabricated stamp. Manifest entry deleted.
- player-passport.ts: the "recent activity" counts card (capturedSessions/
  lastSessionDate/lastSessionSource) migrates the same way. The #434
  thirds-aware IP summation (summarizePitchingSeason, merged as #815) is
  untouched — it already read box-score data and needed no change.
  Manifest entry deleted.
- player-snapshot-cards.ts: exit-velocity fields now derive from
  baseball_batted_ball_events via elite-stat-events.ts's own
  buildHitterMetrics aggregator (closes the file's former "typed but
  un-migrated" comment) instead of baseball_player_stats.exit_velocity.
  Still reads baseball_player_aggregates for the Hitting/Pitching
  legacy-fallback tier and the game/scrimmage/practice "Performance" card —
  no canonical replacement exists yet for a standalone scrimmage split or a
  practice-session shape (see stats-migration-plan.md's open question).
  Manifest note updated to reflect this partial, deliberate scope; entry
  not deleted since the file still references the deprecated table.

Also closes the #828 residual: adaptLegacyPlayerStats never read the legacy
row's avg_pitch_velocity/max_pitch_velocity columns. Wired through as a
per-field fallback under an explicit event-grain reading (pitch velocity has
a legitimate legacy scalar; exit velocity has no legacy equivalent and stays
event-only/null-safe). Adapter tests extended to cover the fallback,
precedence, and no-data cases.

Updated src/contracts/baseball/product-trust/player-today-honest-loop.test.ts
and src/contracts/baseball/access/player-today-self-scope.test.ts fixtures
to match the box-score source (not in the original chunk file list, but
directly affected — these tests exercise getPlayerToday's DB path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa
…ce in #379 read paths (#845 review fix)

The #379 migration of player-today.ts/player-passport.ts/player-snapshot-
cards.ts onto the canonical box-score/event layer regressed legacy-only
players — real history captured before the box-score/event-grain pipelines
existed — from "shows real data" to a silent, honest-LOOKING empty state.
None of the three read paths ever consulted the deprecated flat table as a
fallback, so a genuine data-migration artifact was indistinguishable from a
player who truly has no activity.

- player-today.ts: fetchRecentBoxScoreActivity now falls back to
  baseball_player_stats ONLY when this player has zero box-score rows,
  restoring the pre-#379 stamped-provenance trust/provenance build. Every
  PlayerTodayStat entry now carries `sourceLayer` ('box-score' |
  'legacy-fallback') so the UI can label an old number honestly.
- player-passport.ts: fetchRecentActivity (renamed from
  fetchRecentBoxScoreActivity) applies the same fallback to
  recentActivity.capturedSessions/lastSessionDate/lastSessionSource/
  lastSessionTrust/lastSessionProvenance and the compact-mode 'Captured
  stats' completeness signal. recentActivity now carries `sourceLayer` too.
- player-snapshot-cards.ts: avgExitVelocity/maxExitVelocity fall back to the
  deprecated baseball_player_stats.exit_velocity column ONLY when this player
  has zero baseball_batted_ball_events rows. Extracted the precedence logic
  into a new pure, exported resolveExitVelocityFields() so it's directly
  unit-testable (the DB-bound getPlayerSnapshotCards itself stays
  integration-only, per this file's existing test strategy).

All three mirror the box-score > legacy-fallback > no-data precedence
legacy-stat-adapters.ts already enforces for aggregate rows; the two sources
are never blended for the same player.

Manifest: re-added grandfathered entries for player-today.ts and
player-passport.ts (both now reference baseball_player_stats again, as an
intentional fallback-only read), updated player-snapshot-cards.ts's note,
and added entries for the two test files whose fixtures now reference the
deprecated table.

Tests: extended player-today-honest-loop.test.ts with a legacy-fallback
describe block (shows real data, real stamped provenance, box-score
precedence over legacy), added
player-passport-recent-activity.test.ts (no prior DB-path coverage existed
for getPlayerPassport), and added resolveExitVelocityFields unit tests to
player-snapshot-cards.test.ts. 574 test files / 5206 tests green across
src/lib/baseball + src/contracts/baseball + src/app/baseball.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa
@njrini99-code

Copy link
Copy Markdown
Owner Author

Review fix applied (rebased onto batch/bbh-finish-0714)

Restored the box-score > legacy-fallback > no-data precedence in all three read paths this PR migrated, per the review's mustFix list.

1-4. player-today.ts / player-passport.ts — recentStats / recentActivity legacy fallback
fetchRecentBoxScoreActivity (player-today.ts) and fetchRecentActivity (player-passport.ts, renamed from its own fetchRecentBoxScoreActivity) now fall back to baseball_player_stats only when the player has zero box-score rows — restoring the pre-#379 stamped-provenance trust/provenance build (buildStampedSourceTrust/buildImportProvenance, batched baseball_import_runs review-state lookup) instead of the honest-but-wrong empty list/count. Every PlayerTodayStat entry and recentActivity now carry a sourceLayer: 'box-score' | 'legacy-fallback' | 'no-data' tag (mirrors legacy-stat-adapters.ts's SourceLayer) so the UI can label an old number honestly. The passport's compact-mode "Captured stats" completeness signal now keys off recentActivity.capturedSessions (which includes the fallback) instead of the box-score-only count. Box-score rows still never touch the legacy table — the two sources are never blended for the same player.

5. player-snapshot-cards.ts — exit-velocity legacy fallback
Added a baseball_player_stats.exit_velocity query, consulted only when this player has zero baseball_batted_ball_events rows. Extracted the precedence rule into a new pure, exported resolveExitVelocityFields() (next to inferRole/collapseBand) so it's directly unit-tested even though the DB-bound getPlayerSnapshotCards itself stays integration-only, matching this file's existing test strategy ("exercised via RLS/integration, not here").

6. .github/workflows/ci.yml Dependabot exclusion
Not applicable to this PR after rebase — the baseball-auth-smoke job doesn't exist in ci.yml on batch/bbh-finish-0714 (already resolved upstream via #849's merge), and I don't own that file. Verified via grep that .github/branch-protection.md/docs/CI_RUNBOOK.md already describe the corrected Dependabot-skip behavior.

Manifest housekeeping: re-added GRANDFATHERED_CONSUMERS entries for player-today.ts and player-passport.ts (both now reference the deprecated table again, as an intentional fallback-only read — not a regression), updated player-snapshot-cards.ts's note to describe the restored fallback, and added entries for the two test files whose fixtures now legitimately reference baseball_player_stats. stat-layer-contract.test.ts (#381) passes clean in both scan directions.

Tests extended:

  • player-today-honest-loop.test.ts — new describe block: a legacy-only player (zero box-score rows, real baseball_player_stats rows) gets real recentStats, real stamped trust/provenance when the row was imported, and box-score rows still win outright when both exist.
  • player-passport-recent-activity.test.tsnew file (no prior DB-path test coverage existed for getPlayerPassport); same four cases (legacy-only shows real data, stamped provenance, box-score precedence, honest no-data).
  • player-snapshot-cards.test.ts — unit tests for resolveExitVelocityFields covering legacy-only, event-wins, event-present-but-no-metric, no-data, and non-finite-value filtering.

Gates

  • npm run typecheck — clean.
  • npx eslint --max-warnings 0 on all 7 touched files — clean.
  • npx vitest run on the directly affected suites — 35 files / 297 tests green.
  • Full sweep: npx vitest run src/lib/baseball src/contracts/baseball src/app/baseball — 574 files / 5206 tests green.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

@njrini99-code
njrini99-code merged commit 3ad29c4 into batch/bbh-finish-0714 Jul 15, 2026
27 of 31 checks passed
@njrini99-code
njrini99-code deleted the task/379-player-reads branch July 15, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant