Skip to content

fix(baseball,golf): post-deploy error sweep — roster TDZ cycle, postgame/signals schema drift, OPS derivation, shot-analytics noise#804

Merged
njrini99-code merged 3 commits into
mainfrom
fix/prod-error-sweep-0711
Jul 11, 2026
Merged

fix(baseball,golf): post-deploy error sweep — roster TDZ cycle, postgame/signals schema drift, OPS derivation, shot-analytics noise#804
njrini99-code merged 3 commits into
mainfrom
fix/prod-error-sweep-0711

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

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:

  1. Baseball roster client TDZ crash (Cannot access 'X' before initialization on /baseball/dashboard/roster, and via prefetch on /command-center) — RosterMemberActions spreads POSITIONS into a module-scope options array while importing it from RosterFairway, which circularly imports RosterMemberActions. Today's build flipped the bundler's module evaluation order (no baseball source changed in the deploy) and the cycle bit. POSITIONS moves to a dependency-free roster-constants.ts; madge confirms the value-level cycle is gone (the one remaining edge is an erased import type).
  2. generatePostgameReview save failurebaseball_postgame_reviews / _review_items pre-existed migration 20260624000093's CREATE TABLE IF NOT EXISTS under an older schema, so the intended V10 columns never landed in prod; PostgREST rejects the upsert at its schema cache. Migration 20260711180000 reconciles both tables additively (house pattern from 20260710020000), including the onConflict arbiters and the legacy body NOT NULL relaxation. Both tables have 0 rows in prod.
  3. runOperationalSignalDetection save failure — two stacked faults confirmed in prod Postgres logs: 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) — migration 20260711180100 swaps in the full 15-value BaseballSignalCategory union; and the cold-streak fact loader selected ops/obp/slg off baseball_player_stats, which stores counting stats only (42703) — it now selects raw counting columns and derives OBP/SLG/OPS with stats-center's finalizeBatting formulas.
  4. Golf shot-analytics noisegetPlayerShotAnalytics'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 code no_rounds_in_period, classified under EXPECTED_EMPTY_STATE_CODES (info, skipSentry) per the observer's own convention.

Note: the two migrations must be applied to prod (supabase db push or 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

  • Bug fix
  • Feature / new behavior
  • Security / RLS / auth
  • Database migration
  • CI / tooling / chore
  • Docs only

Area

baseball · golf · coachhelm

Risk level

  • Low
  • Medium
  • High — includes migrations (additive-only, existence-guarded, both target tables empty; reviewed for shared golf-prod safety, golf surface untouched)

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 lint and npm run typecheck pass locally (no new ratchet regressions)
  • Unit/contract tests pass (npm test) — 1,047 baseball tests + postgame + observe-action-result suites green
  • Migrations are additive + idempotent (IF NOT EXISTS / definition-guarded ADD CONSTRAINT), reviewed for shared golf-prod safety — no destructive DELETE/DROP TABLE/data rewrites
  • RLS: no new tables; existing is_baseball_team_staff policies and grants unchanged
  • No secrets in the diff
  • UI changes use design-system primitives — n/a (no UI changes)
  • Partner-readable summary and timeline note included

Screenshots / notes

Migration replay-safety was reviewed against both worlds: on the CI shadow DB (where 20260624000093 creates 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

…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
@vercel

vercel Bot commented Jul 11, 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 11, 2026 6:27pm

Request Review

@supabase

supabase Bot commented Jul 11, 2026

Copy link
Copy Markdown

Updates to Preview Branch (fix/prod-error-sweep-0711) ↗︎

Deployments Status Updated
Database Sat, 11 Jul 2026 18:27:58 UTC
Services Sat, 11 Jul 2026 18:27:58 UTC
APIs Sat, 11 Jul 2026 18:27:58 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Sat, 11 Jul 2026 18:27:59 UTC
Migrations Sat, 11 Jul 2026 18:27:59 UTC
Seeding Sat, 11 Jul 2026 18:27:59 UTC
Edge Functions ⚠️ Sat, 11 Jul 2026 18:27:59 UTC

⚠️ Warning — Only Functions declared in config.toml will be automatically deployed to branches: [functions.my-slug]


View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 58205f72-17e8-4901-80cf-5781559b8a9b

📥 Commits

Reviewing files that changed from the base of the PR and between 760b626 and e797110.

📒 Files selected for processing (9)
  • src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/RosterMemberActions.tsx
  • src/app/baseball/(dashboard)/dashboard/roster/roster-constants.ts
  • src/app/baseball/actions/operational-signals.ts
  • src/app/golf/actions/shot-analytics.ts
  • src/contracts/baseball/product-trust/honest-empty-state.test.ts
  • src/lib/admin/observe-action-result.ts
  • supabase/migrations/20260711180000_baseball_postgame_shape_reconcile.sql
  • supabase/migrations/20260711180100_baseball_signals_category_check_reconcile.sql

Summary by CodeRabbit

  • Bug Fixes

    • Prevented roster pages from crashing during position filter loading by centralizing position codes.
    • Improved cold-streak OPS calculations using formula-based hitting stats with safer null handling and consistent rounding.
    • Made golf “no rounds found in the selected period” handling return a stable error code and reduce diagnostic noise.
  • Database

    • Reconciled baseball postgame review schemas, defaults, constraints, and uniqueness rules.
    • Expanded supported baseball signal categories to match app behavior.
  • Tests

    • Updated OPS validation to cover formula-based calculations and empty-stat cases.

Walkthrough

The PR centralizes roster positions, recalculates baseball OPS from counting stats, adds a golf no-rounds result code, and reconciles baseball database columns and constraints.

Changes

Roster constants

Layer / File(s) Summary
Centralize roster positions
src/app/baseball/(dashboard)/dashboard/roster/roster-constants.ts, src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx, src/app/baseball/(dashboard)/dashboard/roster/RosterMemberActions.tsx
POSITIONS is exported from a dependency-free module and imported by both roster components to avoid the previous import-cycle evaluation issue.

Baseball OPS calculation

Layer / File(s) Summary
Derive OPS from counting stats
src/app/baseball/actions/operational-signals.ts, src/contracts/baseball/product-trust/honest-empty-state.test.ts
OPS now derives OBP and SLG from raw counting statistics, handles zero denominators with null, rounds to three decimals, and updates game/season queries and assertions accordingly.

Golf empty-state classification

Layer / File(s) Summary
Propagate no-rounds result code
src/app/golf/actions/shot-analytics.ts, src/lib/admin/observe-action-result.ts
The no-rounds response includes no_rounds_in_period, the exported failure type permits optional codes, and observation classifies the code as an expected empty state.

Baseball schema reconciliation

Layer / File(s) Summary
Reconcile postgame review schema
supabase/migrations/20260711180000_baseball_postgame_shape_reconcile.sql
The migration conditionally adds expected postgame review columns, checks, unique constraints, foreign keys, defaults, and nullable legacy body behavior.
Reconcile signal categories
supabase/migrations/20260711180100_baseball_signals_category_check_reconcile.sql
The baseball signals category check constraint is replaced with the expanded application category set.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • njrini99-code/helmv3#630: Earlier Fairway roster work also touched the position filter data source in RosterFairway.tsx.

Suggested labels: database

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/prod-error-sweep-0711
  • 🛠️ helm safety pass
  • 🛠️ dashboard ux pass
  • 🛠️ rls test pass

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 @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes several post-deploy failures across BaseballHelm and GolfHelm. The main changes are:

  • Moves roster position constants into a dependency-free module.
  • Derives baseball OPS from counting stats instead of missing rate columns.
  • Adds schema reconciliations for postgame reviews and operational signals.
  • Marks the golf no-rounds shot analytics state as expected.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
src/app/baseball/actions/operational-signals.ts Updates cold-streak OPS loading to use counting stats and three-decimal derived OPS.
src/contracts/baseball/product-trust/honest-empty-state.test.ts Updates the OPS contract assertions to match the derived counting-stat path.
src/app/baseball/(dashboard)/dashboard/roster/roster-constants.ts Adds a dependency-free roster position constant module.
supabase/migrations/20260711180000_baseball_postgame_shape_reconcile.sql Adds guarded schema reconciliation for baseball postgame review tables.
supabase/migrations/20260711180100_baseball_signals_category_check_reconcile.sql Aligns the baseball signal category check with the current category vocabulary.
src/app/golf/actions/shot-analytics.ts Returns a stable expected-empty-state code when a player has no rounds.
src/lib/admin/observe-action-result.ts Classifies the golf no-rounds shot analytics code as an expected empty state.

Reviews (2): Last reviewed commit: "test(baseball): avoid deprecated-table l..." | Re-trigger Greptile

Comment thread src/app/baseball/actions/operational-signals.ts Outdated
Comment on lines +480 to 497
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;
}

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 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.

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.

Fix in Claude Code

claude added 2 commits July 11, 2026 18:18
…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
@njrini99-code njrini99-code marked this pull request as ready for review July 11, 2026 18:31
@njrini99-code njrini99-code merged commit 81ba62f into main Jul 11, 2026
40 of 41 checks passed
@njrini99-code njrini99-code deleted the fix/prod-error-sweep-0711 branch July 11, 2026 18:31
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix roster TDZ cycle, reconcile postgame/signals schema drift, derive OPS, reduce shot-analytics noise

🐞 Bug fix ⚙️ Configuration changes 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Break baseball roster circular import by moving POSITIONS into a dependency-free module.
• Add additive Supabase migrations to reconcile prod postgame and signals schemas/constraints.
• Derive OPS from counting stats and mark golf empty-state as expected telemetry.
Diagram

graph TD
  RosterFairway["RosterFairway.tsx"] --> RosterActions["RosterMemberActions.tsx"] --> RosterConsts["roster-constants.ts"]
  RosterFairway --> RosterConsts
  OpSignals["operational-signals.ts"] --> BaseballDB[("Baseball tables")]
  Migrations["Supabase migrations"] --> BaseballDB
  ShotAnalytics["shot-analytics.ts"] --> Observe["observe-action-result.ts"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Import shared batting finalization helpers (single source of truth)
  • ➕ Avoids duplicating OBP/SLG/OPS math and rounding behavior in multiple locations
  • ➕ Reduces risk of formula drift between read models and rule engine thresholds
  • ➖ May introduce undesirable dependencies (or circulars) between action-layer code and read-model modules
  • ➖ Could increase bundle/runtime surface if helpers pull in extra logic
2. Compute OPS in the database (view/generated column)
  • ➕ Centralizes derived stat computation and keeps queries simple
  • ➕ Prevents future code from selecting non-existent columns by defining a stable projection
  • ➖ Harder to keep in lockstep with app-level definitions across environments
  • ➖ More operational overhead to manage schema changes and backfills (if needed later)
3. Add automated schema-drift detection for CREATE TABLE IF NOT EXISTS migrations
  • ➕ Prevents the same drift class (table pre-exists → migration silently no-ops) from reaching prod again
  • ➕ Can be enforced in CI via information_schema assertions or post-migrate verification
  • ➖ Additional CI complexity and possible false positives across shared prod DB consumers
  • ➖ Doesn't replace the need for targeted reconcile migrations when drift is already present

Recommendation: Keep the PR’s approach: breaking the value-level import cycle via a dependency-free constants module and reconciling prod drift with additive, idempotent migrations is the safest production fix. Consider a follow-up to centralize the batting-rate formulas (or expose them via a dedicated helper) and add lightweight schema verification to catch future CREATE-IF-NOT-EXISTS drift early.

Files changed (9) +340 / -37

Bug fix (5) +78 / -33
RosterFairway.tsxImport POSITIONS from dependency-free module to avoid TDZ import cycle +4/-1

Import POSITIONS from dependency-free module to avoid TDZ import cycle

• Removes the in-module POSITIONS export and imports it from a new roster-constants module. Adds an explicit comment documenting the prior runtime cycle/TDZ crash mechanism.

src/app/baseball/(dashboard)/dashboard/roster/RosterFairway.tsx

RosterMemberActions.tsxSwitch POSITIONS import to roster-constants to break circular dependency +1/-1

Switch POSITIONS import to roster-constants to break circular dependency

• Updates POSITIONS import source from RosterFairway to roster-constants. This prevents module-scope option initialization from participating in a runtime circular import.

src/app/baseball/(dashboard)/dashboard/roster/RosterMemberActions.tsx

operational-signals.tsDerive OPS from counting stats and stop selecting non-existent rate columns +53/-28

Derive OPS from counting stats and stop selecting non-existent rate columns

• Replaces computeOPS logic to derive OBP/SLG/OPS from counting columns, returning null when denominators are zero and rounding to 3 decimals. Updates baseball_player_stats selects to fetch counting stats instead of ops/obp/slg, preventing runtime query errors in prod.

src/app/baseball/actions/operational-signals.ts

shot-analytics.tsReturn stable empty-state code for no-rounds shot analytics +17/-3

Return stable empty-state code for no-rounds shot analytics

• Extends the action’s failure envelope to optionally include a code. Emits code no_rounds_in_period for the expected brand-new-player empty state so observers can downgrade severity.

src/app/golf/actions/shot-analytics.ts

observe-action-result.tsClassify no_rounds_in_period as an expected empty state (info, skip Sentry) +3/-0

Classify no_rounds_in_period as an expected empty state (info, skip Sentry)

• Adds no_rounds_in_period to EXPECTED_EMPTY_STATE_CODES. This routes the soft-failure observer to log at info severity and avoids error/Sentry noise for routine empty states.

src/lib/admin/observe-action-result.ts

Refactor (1) +13 / -0
roster-constants.tsAdd dependency-free roster constants module +13/-0

Add dependency-free roster constants module

• Introduces roster-constants.ts exporting POSITIONS with extensive context about the production TDZ crash. Designed to be safe for module-scope consumption without reintroducing cycles.

src/app/baseball/(dashboard)/dashboard/roster/roster-constants.ts

Tests (1) +9 / -4
honest-empty-state.test.tsUpdate contract test for new OPS derivation behavior +9/-4

Update contract test for new OPS derivation behavior

• Adjusts the computeOPS contract assertions to match the new counting-stat-based OBP/SLG derivation and null-on-zero-denominator behavior. Keeps the "never fabricate 0" guarantee enforced.

src/contracts/baseball/product-trust/honest-empty-state.test.ts

Other (2) +240 / -0
20260711180000_baseball_postgame_shape_reconcile.sqlAdditive reconcile for baseball_postgame_reviews and baseball_postgame_review_items +198/-0

Additive reconcile for baseball_postgame_reviews and baseball_postgame_review_items

• Adds missing columns and CHECK/UNIQUE constraints to match the app-expected V10 postgame schema when prod tables pre-exist under an older shape. Uses existence/definition guards for idempotent replay and relaxes legacy body NOT NULL to prevent future upsert failures.

supabase/migrations/20260711180000_baseball_postgame_shape_reconcile.sql

20260711180100_baseball_signals_category_check_reconcile.sqlReconcile baseball_signals category CHECK constraint to full vocabulary +42/-0

Reconcile baseball_signals category CHECK constraint to full vocabulary

• Drops the legacy category CHECK constraint (if present) and adds a new named CHECK matching the full BaseballSignalCategory union. Unblocks upserts that emit categories not allowed by the drifted prod constraint.

supabase/migrations/20260711180100_baseball_signals_category_check_reconcile.sql

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 98 rules

Grey Divider


Remediation recommended

1. OPS rounding mismatch 🐞 Bug ≡ Correctness
Description
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.
Code

src/app/baseball/actions/operational-signals.ts[R490-498]

+  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));
Relevance

⭐⭐⭐ High

PR #804 accepted aligning computeOPS rounding with stats-center to avoid borderline threshold flips.

PR-#804

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The current computeOPS rounds only the final sum, while stats-center rounds the component rates
first and then rounds OPS; the cold-streak rule uses these OPS values to compute a drop threshold,
so rounding differences can alter borderline comparisons/evidence text.

src/app/baseball/actions/operational-signals.ts[453-499]
src/lib/baseball/read-models/stats-center.ts[388-416]
src/lib/baseball/operational-rule-engine.ts[843-886]
PR-#804

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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


Grey Divider

Qodo Logo

Comment on lines +490 to +498
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));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

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

njrini99-code added a commit that referenced this pull request Jul 11, 2026
…#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>
@coderabbitai coderabbitai Bot added the database Schema, migrations, indexes, SQL label Jul 11, 2026
njrini99-code added a commit that referenced this pull request Jul 12, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

database Schema, migrations, indexes, SQL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants