Skip to content

fix(baseball): #379 Phase 4b engine reader migration + (player,date)-scoped precedence (supersedes #852) - #854

Merged
njrini99-code merged 2 commits into
batch/bbh-finish-0714from
task/379-engine-4b-v2
Jul 15, 2026
Merged

fix(baseball): #379 Phase 4b engine reader migration + (player,date)-scoped precedence (supersedes #852)#854
njrini99-code merged 2 commits into
batch/bbh-finish-0714from
task/379-engine-4b-v2

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Supersedes #852 (same chunk rebased onto the current batch tip + the review's critical fix; fresh branch to avoid a force-push).

What this is

The full #379 Phase 4b chunk (engine-stat-rows.ts consolidated read; outcome-sweep/action-baseline/engine-run migrated; #811 clock residual closed) PLUS the adversarial-review fix: legacy game-row exclusion is now scoped to (player_id, session_date) against canonical game dates — a transitional player's un-box-scored history survives (was: any single canonical row dropped ALL the player's legacy games). Same-day heuristic limitation documented in-code (legacy table has no game_id).

Review trail

  • Full chunk reviewed adversarially (verdict: fix-first, one critical) — the fix here is that mustFix, verbatim.
  • Existing precedence-test fixtures realigned to genuine same-day overlap (dates only, zero assertion changes) — they previously passed only under the buggy player-scoped drop.
  • New tests: mixed-coverage (3 legacy-only + 2 canonical → 5 rows) and same-day collision.

Gates

typecheck exit 0 · eslint --max-warnings 0 clean (all touched) · targeted engine suites 184/184 · full test:business 5193/5217 passed, 10 failures = the pre-existing stat-layer-contract (owned by #845/#847) + FairwayRecruitingPage ResizeObserver set, confirmed absent from this diff (git diff --stat vs base).

🤖 Generated with Claude Code

https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

Fable Integrator and others added 2 commits July 15, 2026 05:35
…weep/baseline/engine-run + deterministic import-quality clock (#379, #811 residual)

Reader migration (#379 Phase 4b, the highest-blast-radius chunk):
- NEW src/lib/baseball/coachhelm/engine-stat-rows.ts — the ONE consolidated
  per-session stat-row read for the engine. Prefers canonical
  baseball_box_score_batting/_pitching rows (normalized onto the loader shape
  via #851's normalizeBoxScoreBattingRow/normalizeBoxScorePitchingRow, with
  session_date joined from baseball_games and source-table provenance tags),
  reconciled over legacy baseball_player_stats rows per the #379 precedence
  rule: canonical rows replace a player's legacy GAME rows outright (never
  blended — the #827 seed writes the same games into both layers), legacy
  practice/other rows always survive (practice carve-out), and a player with
  zero canonical rows keeps full legacy history (fallback tier). Canonical-side
  read failures degrade all-or-nothing to the legacy pool; a legacy read
  failure remains the callers' hard error. All reads paginate past the
  PostgREST 1000-row cap with stable ordering.
- outcome-sweep.ts / action-baseline.ts / engine-run.ts all swap their direct
  baseball_player_stats reads for the shared helper, so baseline capture, the
  outcome sweep, and the engine run measure the SAME reconciled pool
  (apples-to-apples did-it-move). action-baseline's old single-page
  .limit(1000) read is replaced by the paginated shared read.

#811 residual (deterministic engine clock):
- BaseballV10EngineInputs gains an optional now (ISO); engine-run threads its
  nowIso through it; importQualityGenerator's 14-day recency window computes
  from the caller-supplied nowIso instead of raw Date.now() (default preserves
  real-time behavior for non-engine callers). New generators/v10.test.ts pins
  the window against a fixed 2020 clock; engine-run-helm-lifting.test.ts pins
  that runBaseballEngineCore passes its own nowIso end-to-end.

Provenance labels:
- generators/index.ts driver() last-resort fallback label no longer hardcodes
  the deprecated table (loaders now cite the real per-row table); v10.ts's
  practice-effectiveness cite stays deliberately (its feeder still reads
  legacy practice rows) with an explanatory comment.

Manifest (stat-layer contract kept green in both directions for this chunk):
- Removed migrated entries: outcome-sweep.ts, engine-run.ts,
  action-baseline.ts, generators/index.ts, ai-policy-enforcement.test.ts,
  signal-from-insight.test.ts (fixtures moved to canonical table names).
- Added: engine-stat-rows.ts + its test (the one allowed legacy-fallback read).
- Updated notes: loaders.ts, generators/v10.ts, effectiveness/engine.ts and
  operational-rule-engine.ts (both reviewed, deliberately deferred — their
  cites are honest while their feeders still read layer 1), plus the three
  engine test entries now pinning the fallback tier.

Tests: engine-stat-rows.test.ts pins the precedence rule directly;
action-baseline.test.ts + outcome-sweep-insight-resolve.test.ts gain
canonical-preferred, never-blended coverage alongside the existing
legacy-fallback pins.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa
…ot player alone (#379, #852 review fix)

The #379 exclusion rule dropped ALL of a player's legacy stat_type='game'
rows once they had ANY canonical box-score row, even for games with no
canonical counterpart — a mid-season box-score import start would silently
erase that player's earlier legacy-logged games from every engine caller
(engine-run, outcome-sweep, action-baseline), shrinking sample_n and
flipping confidence/verdicts. baseball_player_stats has no game_id, so we
now correlate on the resolved canonical game date (truncated to YYYY-MM-DD
on both sides) instead: a legacy game row is dropped only when that same
player has canonical coverage on that exact calendar day: a same-day
heuristic, not a guaranteed game-identity match, since there's no FK to
lean on (documented in the module comment as an accepted double-header
collision risk).

Existing precedence tests encoded the bug: their legacy-row fixture dates
never matched the canonical game dates, yet still asserted full drop —
only possible under the old player-scoped exclusion. Realigned those
fixture dates to same-day overlap (preserving each test's 100%-coverage
intent) and added a mixed-coverage case (3 legacy-only + 2 canonical -> 5
rows survive) plus a same-day-collision case (legacy row on a
canonically-covered date -> dropped).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: 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

@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: 06df8b12-2263-4ee4-8bc7-38f6d163b694

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-engine-4b-v2
  • 🛠️ 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.

@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 ↗︎.

@njrini99-code
njrini99-code merged commit 67f1c86 into batch/bbh-finish-0714 Jul 15, 2026
28 of 31 checks passed
@njrini99-code
njrini99-code deleted the task/379-engine-4b-v2 branch July 15, 2026 09:52
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR lands Phase 4b of #379: a single loadEngineStatRows function centralises all three engine callers' (engine-run, outcome-sweep, action-baseline) previously-duplicated baseball_player_stats reads. Legacy stat_type='game' rows are now superseded on a (player_id, session_date) basis rather than player-only, so a transitional player's un-box-scored history on dates with no canonical coverage survives. The #811 residual is also closed by threading the run's deterministic nowIso into importQualityGenerator.

  • engine-stat-rows.ts (new): Runs four parallel paginated reads, normalises canonical rows via the existing normalizeBoxScore*Row helpers, builds a Set<\"player_id|YYYY-MM-DD\"> to drop only legacy game rows whose date has canonical coverage for that player, and degrades ALL-OR-NOTHING to the legacy pool on any canonical-side read error.
  • engine-run.ts / outcome-sweep.ts / action-baseline.ts: Direct baseball_player_stats reads replaced with a single loadEngineStatRows call, keeping baseline/observed apples-to-apples.
  • v10.ts / engine.ts: importQualityGenerator now accepts an optional nowIso threaded from BaseballV10EngineInputs.now; 8 new unit tests pin the fix.

Confidence Score: 4/5

Safe to merge. The new consolidated read correctly applies the (player, date)-scoped precedence rule, degrades cleanly to legacy on canonical-side errors, and the three migrated callers are wired identically — keeping baseline/observed apples-to-apples.

The implementation is logically sound: the precedence Set is built from already player-filtered canonical rows, the ALL-OR-NOTHING degrade prevents partial blends, and the nowIso threading closes the last rolling-window clock leak. The only gap is the primary unit test file's no-op .eq()/.in() fake, which means team-scoping of canonical queries is not exercised by tests — a regression there would require a production incident to detect.

src/lib/baseball/tests/engine-stat-rows.test.ts — the fake client skips .eq()/.in() filtering; consider adding a cross-team isolation assertion before this module graduates out of the grandfathered list.

Important Files Changed

Filename Overview
src/lib/baseball/coachhelm/engine-stat-rows.ts New consolidated stat-row reader; precedence logic, degrade path, and pagination are correct. Minor: baseball_games loaded without player-scoping (all team games, unavoidable without extra round-trip). No blocking issues.
src/lib/baseball/tests/engine-stat-rows.test.ts Comprehensive behavioural coverage of the precedence rule, but the fake client's no-op .eq()/.in() means team-scoping of canonical queries is untested. All assertion math checks out.
src/lib/baseball/coachhelm/engine-run.ts Direct baseball_player_stats read replaced with loadEngineStatRows; nowIso threaded into BaseballV10EngineInputs.now for the #811 residual. Type cast cleaned up correctly.
src/lib/baseball/coachhelm/outcome-sweep.ts Old limit(4000)/fetchAllRowsResult direct read replaced cleanly with loadEngineStatRows; pool construction unchanged; STAT_SELECT constant removed.
src/lib/baseball/coachhelm/action-baseline.ts Single-player .limit(1000) read replaced with loadEngineStatRows; now paginates and applies the precedence rule for apples-to-apples baseline/observed comparison.
src/lib/coachhelm/baseball/generators/v10.ts importQualityGenerator extended with optional nowIso defaulting to new Date().toISOString(); Date.now() window replaced with Date.parse(nowIso). Backward-compatible.
src/lib/coachhelm/baseball/engine.ts BaseballV10EngineInputs gains optional now?: string; generateAllBaseballCandidates threads it into importQualityGenerator. Optional field keeps all existing callers unaffected.
src/lib/baseball/stat-layer-manifest.ts GRANDFATHERED_CONSUMERS correctly updated: engine-run, outcome-sweep, action-baseline removed as direct readers; engine-stat-rows.ts added as the new consolidated pending-migration entry.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    ER[engine-run.ts] --> LSR
    OS[outcome-sweep.ts] --> LSR
    AB[action-baseline.ts] --> LSR
    LSR["loadEngineStatRows(db, teamId, playerIds)"]
    LSR --> P1[baseball_player_stats]
    LSR --> P2[baseball_games]
    LSR --> P3[baseball_box_score_batting]
    LSR --> P4[baseball_box_score_pitching]
    P1 --> M{Promise.all}
    P2 --> M
    P3 --> M
    P4 --> M
    M -->|legacy read error| HE[return null + error]
    M -->|any canonical error| DEG[degrade to legacyRows only]
    M -->|zero canonical rows| LEG[legacy fallback unchanged]
    M -->|canonical rows present| NORM[normalizeBoxScore rows]
    NORM --> SET[Build canonicalPlayerDates Set]
    SET --> FILTER[Drop legacy game rows on covered dates]
    FILTER --> OUT[return normalized + retainedLegacy]
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
    ER[engine-run.ts] --> LSR
    OS[outcome-sweep.ts] --> LSR
    AB[action-baseline.ts] --> LSR
    LSR["loadEngineStatRows(db, teamId, playerIds)"]
    LSR --> P1[baseball_player_stats]
    LSR --> P2[baseball_games]
    LSR --> P3[baseball_box_score_batting]
    LSR --> P4[baseball_box_score_pitching]
    P1 --> M{Promise.all}
    P2 --> M
    P3 --> M
    P4 --> M
    M -->|legacy read error| HE[return null + error]
    M -->|any canonical error| DEG[degrade to legacyRows only]
    M -->|zero canonical rows| LEG[legacy fallback unchanged]
    M -->|canonical rows present| NORM[normalizeBoxScore rows]
    NORM --> SET[Build canonicalPlayerDates Set]
    SET --> FILTER[Drop legacy game rows on covered dates]
    FILTER --> OUT[return normalized + retainedLegacy]
Loading

Fix All in Claude Code

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

---

### Issue 1 of 1
src/lib/baseball/__tests__/engine-stat-rows.test.ts:182-201
**Test fake silently skips team-scoping on canonical reads**

The fake client's `.eq()` and `.in()` are no-ops (`eq: () => builder`, `in: () => builder`), so no test in this file verifies that the canonical queries carry `.eq('team_id', teamId)`. If that filter were accidentally removed from `engine-stat-rows.ts`, every test here would still pass while the production read would cross team boundaries.

The `action-baseline.test.ts` fake *does* filter on `eq`, and the schema + RLS enforce isolation in production — so there's no current data leakage — but the gap means a future refactor of the canonical read chain could silently drop the `team_id` predicate without a test failure. Adding a per-table row-count assertion after seeding rows for a second team (with a different `team_id`) would close the gap without restructuring the rest of the test suite.

Reviews (1): Last reviewed commit: "fix(baseball): scope engine-stat-rows pr..." | Re-trigger Greptile

Comment on lines +182 to +201
legacyGame('lg-1', 'p1', { session_date: '2026-01-01' }),
legacyGame('lg-2', 'p1', { session_date: '2026-01-08' }),
legacyGame('lg-3', 'p1', { session_date: '2026-01-15' }),
],
baseball_games: [
{ id: 'g1', game_date: '2026-04-01' },
{ id: 'g2', game_date: '2026-04-08' },
],
baseball_box_score_batting: [
boxBatting('bb-1', 'p1', 'g1'),
boxBatting('bb-2', 'p1', 'g2'),
],
});

const { data, error } = await loadEngineStatRows(client, TEAM, ['p1']);
expect(error).toBeNull();
// 3 legacy-only + 2 canonical = 5 rows; nothing here collides on date, so
// the player-scoped bug (dropping ALL legacy games once ANY canonical row
// exists) would have wrongly shrunk this to 2.
const ids = data!.map((r) => r.id).sort();

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 Test fake silently skips team-scoping on canonical reads

The fake client's .eq() and .in() are no-ops (eq: () => builder, in: () => builder), so no test in this file verifies that the canonical queries carry .eq('team_id', teamId). If that filter were accidentally removed from engine-stat-rows.ts, every test here would still pass while the production read would cross team boundaries.

The action-baseline.test.ts fake does filter on eq, and the schema + RLS enforce isolation in production — so there's no current data leakage — but the gap means a future refactor of the canonical read chain could silently drop the team_id predicate without a test failure. Adding a per-table row-count assertion after seeding rows for a second team (with a different team_id) would close the gap without restructuring the rest of the test suite.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/baseball/__tests__/engine-stat-rows.test.ts
Line: 182-201

Comment:
**Test fake silently skips team-scoping on canonical reads**

The fake client's `.eq()` and `.in()` are no-ops (`eq: () => builder`, `in: () => builder`), so no test in this file verifies that the canonical queries carry `.eq('team_id', teamId)`. If that filter were accidentally removed from `engine-stat-rows.ts`, every test here would still pass while the production read would cross team boundaries.

The `action-baseline.test.ts` fake *does* filter on `eq`, and the schema + RLS enforce isolation in production — so there's no current data leakage — but the gap means a future refactor of the canonical read chain could silently drop the `team_id` predicate without a test failure. Adding a per-table row-count assertion after seeding rows for a second team (with a different `team_id`) would close the gap without restructuring the rest of the test suite.

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

Fix in Claude Code

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