fix(baseball,golf): post-deploy error sweep — roster TDZ cycle, postgame/signals schema drift, OPS derivation, shot-analytics noise#804
Conversation
…ema drift, OPS derivation, shot-analytics noise
Root-caused every error in the 17:40-17:48 UTC post-deploy window (Helm
Bridge incident export + prod Postgres logs + live information_schema):
- Baseball roster client TDZ crash ('Cannot access X before
initialization', /baseball/dashboard/roster + prefetch from
/command-center): RosterMemberActions spreads POSITIONS into a
module-scope options array while importing it from RosterFairway, which
circularly imports RosterMemberActions — a runtime cycle whose eval
order the bundler flipped in today's build (no baseball source changed).
POSITIONS moves to dependency-free roster-constants.ts; the value cycle
is gone (the remaining madge edge is an erased import type).
- generatePostgameReview 'Could not save the postgame review':
baseball_postgame_reviews/_review_items pre-existed 20260624000093's
CREATE IF NOT EXISTS under an older schema, so the intended V10 columns
never landed — PostgREST rejects the upsert at its schema cache.
20260711180000 reconciles both tables additively (house pattern from
20260710020000), incl. the onConflict arbiters and the legacy
body NOT NULL relaxation. Both tables have 0 rows.
- runOperationalSignalDetection 'Could not save operational signals',
two stacked faults: (1) the live baseball_signals category CHECK still
carries the old table's 6-value vocabulary while the rule engine emits
'operations'/'practice' (23514 on every upsert) — 20260711180100 swaps
it for the full 15-value BaseballSignalCategory union; (2) the
cold-streak fact loader selected ops/obp/slg off baseball_player_stats,
which stores counting stats only (42703) — it now selects the raw
counting columns and derives OBP/SLG/OPS with stats-center's
finalizeBatting formulas.
- getPlayerShotAnalytics 'No rounds found in the selected period' logged
at error severity: an expected brand-new-player empty state hitting the
soft-failure observer. The envelope now carries the stable code
'no_rounds_in_period', classified under EXPECTED_EMPTY_STATE_CODES
('info', skipSentry) per the observer's own convention.
Gates: tsc clean, eslint clean on touched files, 1,047 baseball tests +
postgame + observe-action-result suites green, madge confirms the roster
value-cycle is broken. Migrations reviewed (replay-safe on both the
fresh shadow DB and drifted prod; golf surface untouched).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Updates to Preview Branch (fix/prod-error-sweep-0711) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (9)
Summary by CodeRabbit
WalkthroughThe PR centralizes roster positions, recalculates baseball OPS from counting stats, adds a golf no-rounds result code, and reconciles baseball database columns and constraints. ChangesRoster constants
Baseball OPS calculation
Golf empty-state classification
Baseball schema reconciliation
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.44.1)ast-grep could not parse rule config: /ast-grep-rules/../git/.coderabbit/ast-grep/no-explicit-any.yml Comment |
Greptile SummaryThis PR fixes several post-deploy failures across BaseballHelm and GolfHelm. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (2): Last reviewed commit: "test(baseball): avoid deprecated-table l..." | Re-trigger Greptile |
| function computeOPS(r: HittingCountingRow): number | null { | ||
| const ab = r.at_bats ?? 0; | ||
| const h = r.hits ?? 0; | ||
| const doubles = r.doubles ?? 0; | ||
| const triples = r.triples ?? 0; | ||
| const hr = r.home_runs ?? 0; | ||
| const bb = r.walks ?? 0; | ||
| const hbp = r.hit_by_pitch ?? 0; | ||
| const sf = r.sacrifice_flies ?? 0; | ||
|
|
||
| const singles = Math.max(0, h - doubles - triples - hr); | ||
| const totalBases = singles + 2 * doubles + 3 * triples + 4 * hr; | ||
| const slg = ab > 0 ? totalBases / ab : null; | ||
| const obpDen = ab + bb + hbp + sf; | ||
| const obp = obpDen > 0 ? (h + bb + hbp) / obpDen : null; | ||
| if (obp === null || slg === null) return null; | ||
| return obp + slg; | ||
| } |
There was a problem hiding this comment.
The changed helper no longer supports the old explicit-ops column shortcut, but the existing product-trust test still checks for that branch. The runtime query now uses counting columns correctly, but this source-level contract test can fail until it is updated to assert the new derived-OPS behavior.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/baseball/actions/operational-signals.ts
Line: 480-497
Comment:
**Stale OPS Contract Test**
The changed helper no longer supports the old explicit-`ops` column shortcut, but the existing product-trust test still checks for that branch. The runtime query now uses counting columns correctly, but this source-level contract test can fail until it is updated to assert the new derived-OPS behavior.
How can I resolve this? If you propose a fix, please make it concise.…k contract test Greptile round on #804: computeOPS now rounds to 3 decimals so borderline 0.150-drop comparisons agree with the OPS coaches see in stats-center, and the product-trust contract test asserts the derived-from-counting-stats invariant instead of the removed explicit-ops branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV
The stat-layer manifest scan matches raw table-name strings, so the comment added in 2a95743 tripped it. Reworded — no behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV
PR Summary by QodoFix roster TDZ cycle, reconcile postgame/signals schema drift, derive OPS, reduce shot-analytics noise
AI Description
Diagram
High-Level Assessment
Files changed (9)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
98 rules 1. OPS rounding mismatch
|
| const singles = Math.max(0, h - doubles - triples - hr); | ||
| const totalBases = singles + 2 * doubles + 3 * triples + 4 * hr; | ||
| const slg = ab > 0 ? totalBases / ab : null; | ||
| const obpDen = ab + bb + hbp + sf; | ||
| const obp = obpDen > 0 ? (h + bb + hbp) / obpDen : null; | ||
| if (obp === null || slg === null) return null; | ||
| // Round like stats-center's round3 so borderline cold-streak threshold | ||
| // comparisons agree with the OPS coaches see rendered. | ||
| return Number((obp + slg).toFixed(3)); |
There was a problem hiding this comment.
1. Ops rounding mismatch 🐞 Bug ≡ Correctness
computeOPS() rounds only the final (OBP+SLG) sum, but stats-center’s finalizeBatting() rounds OBP and SLG to 3 decimals before summing and rounding OPS again; this can shift derived OPS by 0.001 in edge cases. That inconsistency can change the computed OPS drop used by the cold-streak rule, making signal firing/evidence slightly disagree with coaches’ rendered stats-center OPS.
Agent Prompt
### Issue description
`computeOPS()` in `operational-signals.ts` rounds only the final `obp + slg` value. The canonical stats-center logic (`finalizeBatting`) rounds SLG and OBP individually to 3 decimals, then rounds OPS from their sum. This can yield a 0.001 discrepancy in OPS and therefore in the cold-streak drop calculation.
### Issue Context
Cold-streak evaluation compares `drop = seasonOPS - recentOPS` against a threshold; any inconsistency in OPS derivation/rounding can cause borderline differences and mismatches vs what coaches see in stats-center.
### Fix Focus Areas
- src/app/baseball/actions/operational-signals.ts[453-499]
### Suggested change
Update `computeOPS` to mirror stats-center:
- introduce a local `round3(n) => parseFloat(n.toFixed(3))`
- compute `slg` as `round3(totalBases / ab)` (when `ab > 0`)
- compute `obp` as `round3((h + bb + hbp) / obpDen)` (when `obpDen > 0`)
- return `round3(obp + slg)`
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…#805) Prod schema moved under the two reconcile migrations merged in #804 (applied 2026-07-11); regenerate so the committed types match the live database again. Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV Co-authored-by: Claude <noreply@anthropic.com>
…ected (#807) * fix(golf): classify remaining CoachHelm no-rounds empty states as expected Sentry alerted at 23:11 UTC on /golf/dashboard/coachhelm: a rounds-less player loading the page fires FOUR sibling soft failures, and #804 only gave getPlayerShotAnalytics a stable code. getPlayerProfile, getPlayerTrendAnalysis, getPlayerShotContext, and the what-if scenario path in coachhelm-data.ts now carry codes on their empty-state returns (no_completed_rounds / insufficient_rounds / no_rounds_in_period), all registered under EXPECTED_EMPTY_STATE_CODES — 'info', skipSentry, same convention as #804. A brand-new player browsing CoachHelm is not a production error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV * docs(admin): make the global-code contract explicit on EXPECTED_EMPTY_STATE_CODES Greptile on #807: classification is by code globally, so a future real failure reusing one of these codes would silently skip Sentry. Spell that contract out where the set is defined. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV * fix(golf): distinguish unscored completed rounds from no rounds; test new empty-state codes getPlayerProfile's round query filters total_score IS NOT NULL, so an empty result was ambiguous: the globally-silenced no_completed_rounds code could also fire when completed rounds exist but all lack scores — a data-quality failure that must stay visible per the EXPECTED_EMPTY_STATE_CODES contract. The empty branch now runs a head-count of completed rounds and only attaches the code when the player truly has none. Also adds regression coverage for the three newly registered codes (no_rounds_in_period, no_completed_rounds, insufficient_rounds) asserting the info/skipSentry classification path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV * fix(golf): guard insufficient_rounds against unscored completed rounds Same contract violation as the getPlayerProfile fix in aeda380, in the two insufficient_rounds sites: getPlayerTrendAnalysis and the what-if scenario path both filter score_to_par IS NOT NULL, so a player with 3+ completed rounds whose scores never populated would be silenced as an expected empty state. Both branches now head-count completed rounds and only attach the globally-silenced code when the player truly has fewer than 3; otherwise they return an uncoded data-quality error that stays visible to Sentry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV * fix(golf): surface count-query failures instead of silencing as empty states Qodo caught that the three fallback head-count queries ignored the Supabase error: a failed count leaves count null, which fell through to the globally-silenced empty-state code — hiding a genuine backend failure from Sentry. All three guards now return an uncoded failure when the count query errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Root-causes every error in today's 17:40–17:48 UTC post-deploy window (Helm Bridge incident export, cross-checked against prod Postgres logs and live
information_schema). Four independent fixes:Cannot access 'X' before initializationon/baseball/dashboard/roster, and via prefetch on/command-center) —RosterMemberActionsspreadsPOSITIONSinto a module-scope options array while importing it fromRosterFairway, which circularly importsRosterMemberActions. Today's build flipped the bundler's module evaluation order (no baseball source changed in the deploy) and the cycle bit.POSITIONSmoves to a dependency-freeroster-constants.ts; madge confirms the value-level cycle is gone (the one remaining edge is an erasedimport type).generatePostgameReviewsave failure —baseball_postgame_reviews/_review_itemspre-existed migration20260624000093'sCREATE TABLE IF NOT EXISTSunder an older schema, so the intended V10 columns never landed in prod; PostgREST rejects the upsert at its schema cache. Migration20260711180000reconciles both tables additively (house pattern from20260710020000), including theonConflictarbiters and the legacybodyNOT NULL relaxation. Both tables have 0 rows in prod.runOperationalSignalDetectionsave failure — two stacked faults confirmed in prod Postgres logs: the livebaseball_signalscategory CHECK still carries the old table's 6-value vocabulary while the rule engine emitsoperations/practice(23514 on every upsert) — migration20260711180100swaps in the full 15-valueBaseballSignalCategoryunion; and the cold-streak fact loader selectedops/obp/slgoffbaseball_player_stats, which stores counting stats only (42703) — it now selects raw counting columns and derives OBP/SLG/OPS withstats-center'sfinalizeBattingformulas.getPlayerShotAnalytics's "No rounds found in the selected period" is an expected brand-new-player empty state, but the soft-failure observer logged it at error severity. The envelope now carries the stable codeno_rounds_in_period, classified underEXPECTED_EMPTY_STATE_CODES(info, skipSentry) per the observer's own convention.Note: the two migrations must be applied to prod (
supabase db pushor MCP) for fixes 2 and 3 to take effect — the code for those paths is already live and correct.Partner-readable summary
Coaches opening the Baseball roster page could hit a hard crash, and two AI features — Postgame Reviews and the Signal Inbox — silently failed to save anything they generated because the production database was missing columns the features expect. All three are fixed, and a harmless "player has no rounds yet" case no longer shows up as a production error.
Type of change
Area
baseball · golf · coachhelm
Risk level
Git Activity Timeline note
Fixes the Baseball roster crash and unblocks Postgame Review and Signal Inbox saves that were silently failing against the production database.
Checklist
npm run lintandnpm run typecheckpass locally (no new ratchet regressions)npm test) — 1,047 baseball tests + postgame + observe-action-result suites greenIF NOT EXISTS/ definition-guardedADD CONSTRAINT), reviewed for shared golf-prod safety — no destructiveDELETE/DROP TABLE/data rewritesis_baseball_team_staffpolicies and grants unchangedScreenshots / notes
Migration replay-safety was reviewed against both worlds: on the CI shadow DB (where
20260624000093creates the correct schema fresh) every statement no-ops; on drifted prod the columns/constraints land on empty tables. Known pre-existing gap (not addressed here): prod's postgame RLS lacks the fresh-schema's player-visible SELECT policy, so player-facing sharing stays dark until a follow-up policy reconcile — staff-facing generation works.🤖 Generated with Claude Code
https://claude.ai/code/session_019WJ1Gzjj9MT3UsxhMoMzSV
Generated by Claude Code