Skip to content

fix(coachhelm): unify player/team trend across Brief, Players tab, Team Stats - #929

Closed
njrini99-code wants to merge 3 commits into
mainfrom
fix/coachhelm-trend-coherence
Closed

fix(coachhelm): unify player/team trend across Brief, Players tab, Team Stats#929
njrini99-code wants to merge 3 commits into
mainfrom
fix/coachhelm-trend-coherence

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Problem

Part of #914. FOUR CoachHelm surfaces each computed "improving / steady / declining" independently and disagreed for the same team/player (verified live):

  • Brief category cards (team-category-insights.ts) — "Team putting trending up — 4 of 7 players improving" (per-round category metric, 5-vs-5 window, 0.5 threshold).
  • Players tab roster table (development/page.tsx) — 6 Declining + 1 Steady, 0 improving (read golf_player_stats_cache.trend_direction, a DB-trigger value with its OWN 5-vs-5 window and 1.0-stroke threshold).
  • Team Stats trajectory tile (FairwayTeamStats.tsx) — 2 improving / 0 steady / 5 declining.
  • Team Stats player cards (same file) — 1 Improving.

Same underlying rounds, three independently-reimplemented thresholds/windows/sources — a player could read "declining" on the roster table and "improving" on Team Stats for the identical rounds.

Two more symptoms of the same root cause:

  • Brief: "7 active players · 8 need attention" (8 > 7) — totalAttention summed each category's attentionCount, double-counting any player flagged in more than one category.
  • Players tab: header said "7 players to look at" but the list showed only 5 — a silent top-5 cap with no indication it was capped.
  • Brief hero/category contradiction: "Putting needs the most work (20/100)" sat directly above a green "Trending up" chip on the putting category — rating (how good right now) and trend (which way it's moving) were conflated into one label.

Fix

  • New src/lib/coachhelm/trend.ts — canonical classifyTrendDelta + computeSeriesTrend: one 5-vs-5 recent/previous window, one threshold-comparison definition, a hasSignal flag so "not enough history" never gets fabricated into a zero-delta "Steady". Unit tested.
  • New src/lib/golf/scoring-trend.tscomputeScoringTrendFromRounds, the shared "player's overall score trend" wrapper (18-hole normalization + the canonical classifier), now consumed by both stats/team/page.tsx and development/page.tsx so the same rounds always read the same verdict.
  • team-category-insights.ts now calls computeSeriesTrend instead of a private reimplementation (behavior preserved exactly — same window, same 0.5 threshold, same 1-previous-round floor).
  • development/page.tsx no longer reads trend_direction for the roster table's Trend column; it fetches each player's recent rounds (same per-player sampler team-category-insights.ts already uses, avoiding the "most-active player starves everyone else" bug) and classifies via the canonical function — so it agrees with Team Stats for the same rounds.
  • FairwayTeamStats.tsx's classifyScoringTrend now delegates the improving/declining/steady decision to classifyTrendDelta (same 0.3 threshold, now sourced canonically instead of a second hardcoded copy).
  • FairwayBrief.tsx:
    • "N need attention" is now the union of flagged player ids across categories (bounded by roster size), not a sum.
    • The hero-vs-category contradiction is resolved by blending rating + trend in the category status label — a below-mid-line category that's improving now reads "Weak, improving" / "<label> is weak but improving" instead of a bare "Trending up" that reads as disagreeing with the hero. The 20/100 rating itself is untouched — rating and trend stay separate numbers, just labeled so they don't contradict.
  • PlayersGridView.tsx — the "N players to look at" header count and its capped top-5 list now agree: a "+N more players need a look — showing the top 5 by priority" caption appears whenever the list is truncated.

Gates

  • npx tsc --noEmit -p tsconfig.json — clean
  • npx eslint on all changed files — clean
  • npx vitest run on the new/touched test files — 264 passed
  • Broader sweep (src/components/fairway, src/app/golf/actions/__tests__, src/lib/golf) — 288 files / 3492 tests passed, 4 pre-existing skips

Caveats

  • development/page.tsx's new rounds fetch adds one paginated query (golf_rounds, player_id/total_score/holes_played/round_date, batched via fetchAllRowsResult, capped to each player's last 10 rounds in a 365-day window) — same pattern already used in team-category-insights.ts, but it is additional DB load on that route.
  • Category-level trend thresholds (putting 0.5 putts, score 0.3 strokes, etc.) are intentionally still per-metric — a single absolute threshold doesn't make sense across differing units (percentages vs putts vs strokes). "Canonical" here means one shared classification function + window semantics, not one universal numeric threshold across all metrics. Each threshold now lives in exactly one place instead of being copy-pasted.
  • Did not touch the SQL trigger that still writes golf_player_stats_cache.trend_direction — that column is simply no longer read by the Players tab display. It may still be read elsewhere (patterns/predictions); a follow-up could migrate those too if this PR's approach is confirmed correct.

Fixes #914

Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01MMdviLDsAg2YYJ8adsM6fg

Fable Integrator and others added 2 commits July 17, 2026 17:20
Four fixes in the post-round-to-insight pipeline that were letting
CoachHelm insights go stale silently:

1. RACE — round submit registered two independent after() callbacks
   (stats-cache refresh + postRoundTrigger). after() callbacks run
   concurrently, not in registration order, so the engine could read
   golf_player_stats_cache before the refresh finished writing it.
   Chained into a single after() that awaits the cache refresh THEN
   runs postRoundTrigger.

2. ROSTER-SWEEP SKIP — the nightly sweep skipped a player whenever ANY
   of their rounds was analyzed within a 12h window, even if their
   genuinely most-recent round was still unanalyzed (e.g. an older
   round got re-analyzed by the safety-net cron inside that window).
   Now skips a player only when their MOST RECENT completed round has
   coachhelm_analyzed_at set — sweeps whenever the latest round is
   unanalyzed, regardless of when some earlier round was touched.

3. SILENT POSTURE VISIBILITY — alert_posture='silent' maps to an
   infinite confidence threshold, silently blocking every insight for
   that player with no signal anywhere. Kept the behavior, surfaced it:
   (a) logServerEvent (info, skipSentry) once per run when the gate
   blocks; (b) the CoachHelm Players-tab roster row now shows an
   "Insights muted" indicator for players with silent posture (reads
   the same golf_coach_player_intent the Roster page already loads via
   loadCoachIntents — no new query pattern).

4. FAKE TIMESTAMP — the Brief's "updated" label rendered
   golf_rounds.round_date (a DATE column, no time component) through
   `new Date(dateOnly).toISOString()` then a viewer-timezone
   toLocaleString with hour/minute — fabricating a specific clock time
   ("Jun 8, 8:00 PM") the round never had, and for viewers west of UTC,
   silently shifting the calendar date itself back a day. Now formats
   as a DATE ONLY ("Jun 8"), anchoring both parse and format in UTC so
   every viewer sees the same calendar day.

Gates: tsc clean, eslint clean on all changed files, and the full
unit-test slate under the touched directories (55 files / 492 tests)
passes. Added regression tests for 1, 2 and 4 — each one verified to
FAIL against the pre-fix code and PASS against the fix.

Fixes #920

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMdviLDsAg2YYJ8adsM6fg
…am Stats

Part of #914. FOUR surfaces computed "improving/steady/declining"
independently and disagreed for the same player/roster:

- Brief category cards (team-category-insights.ts): per-round category
  metric, 5-vs-5 window, 0.5 threshold.
- Players tab roster table (development/page.tsx): read
  golf_player_stats_cache.trend_direction, a DB-trigger value with its
  OWN 5-vs-5 window and 1.0-stroke threshold.
- Team Stats trajectory tile + player cards (FairwayTeamStats.tsx /
  stats/team/page.tsx): live-computed 5-vs-5 window, 0.3-stroke threshold.

Same underlying rounds, three different thresholds/sources, so a player
could read "declining" on the roster table and "improving" on Team Stats.
The Brief also summed attentionCount across all 5 categories for its
"N need attention" figure, double-counting any player flagged in more
than one category (observed live: "8 need attention" on a 7-player team).

Fix:
- New src/lib/coachhelm/trend.ts: canonical classifyTrendDelta +
  computeSeriesTrend (one 5-vs-5 window, one threshold-comparison
  definition, a hasSignal flag so "not enough history" never reads as a
  fabricated zero-delta "Steady"). Unit tested.
- New src/lib/golf/scoring-trend.ts: computeScoringTrendFromRounds, the
  shared "player's overall score trend" wrapper (18-hole normalization +
  the canonical classifier) now consumed by BOTH stats/team/page.tsx and
  development/page.tsx so the same rounds always read the same verdict.
- team-category-insights.ts now calls computeSeriesTrend instead of a
  private reimplementation (behavior preserved exactly — same window,
  same 0.5 threshold, same 1-previous-round floor).
- development/page.tsx no longer reads golf_player_stats_cache.trend_direction
  for the roster table; it fetches each player's recent rounds (same
  per-player sampler team-category-insights.ts uses) and classifies via
  the canonical function, so it agrees with Team Stats for the same rounds.
- FairwayTeamStats.tsx's classifyScoringTrend now delegates the
  improving/declining/steady decision to classifyTrendDelta.
- FairwayBrief.tsx: "N need attention" is now the union of flagged player
  ids across categories (bounded by roster size), not a sum. The hero
  vs. category-card contradiction ("Putting needs the most work (20/100)"
  next to a green "Trending up" chip) is resolved by blending rating +
  trend in the category status label — a below-mid-line category that's
  improving now reads "Weak, improving" / "<label> is weak but improving"
  instead of a bare "Trending up" that reads as disagreeing with the hero.
  The 20/100 rating itself is untouched — rating and trend stay separate
  numbers, just labeled so they don't read as contradictory.
- PlayersGridView.tsx: the "N players to look at" header count and its
  capped top-5 list now agree — a "+N more players need a look — showing
  the top 5 by priority" caption appears whenever the list is truncated.

Gates: npx tsc --noEmit -p tsconfig.json (clean); npx eslint on all
changed files (clean); npx vitest run on the new/touched test files
(264 passed) plus a broader sweep of src/components/fairway,
src/app/golf/actions/__tests__, src/lib/golf (288 files / 3492 tests
passed, 4 pre-existing skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMdviLDsAg2YYJ8adsM6fg
@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

@cursor

cursor Bot commented Jul 17, 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.

@vercel

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

Request Review

@supabase

supabase Bot commented Jul 17, 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 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@njrini99-code, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4ec383ce-2aaf-4297-8fb0-7d450047333b

📥 Commits

Reviewing files that changed from the base of the PR and between 6d25e44 and dece8f5.

📒 Files selected for processing (16)
  • src/app/api/cron/coachhelm-roster-sweep/route.ts
  • src/app/golf/(dashboard)/dashboard/development/page.tsx
  • src/app/golf/(dashboard)/dashboard/stats/team/page.tsx
  • src/app/golf/actions/__tests__/golf-round-submit-after-chain.test.ts
  • src/app/golf/actions/golf.ts
  • src/app/golf/actions/insights.ts
  • src/app/golf/actions/team-category-insights.ts
  • src/components/fairway/pages/coachhelm/FairwayBrief.formatAnalyzed.test.ts
  • src/components/fairway/pages/coachhelm/FairwayBrief.tsx
  • src/components/fairway/pages/coachhelm/FairwayTeamStats.tsx
  • src/components/fairway/pages/coachhelm/PlayersGridView.tsx
  • src/lib/coachhelm/trend.test.ts
  • src/lib/coachhelm/trend.ts
  • src/lib/golf/scoring-trend.test.ts
  • src/lib/golf/scoring-trend.ts
  • src/test/api/cron/coachhelm-roster-sweep.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/coachhelm-trend-coherence
  • 🛠️ 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.

@njrini99-code

Copy link
Copy Markdown
Owner Author

🤖 Mission Control — PR summary

What it changes: Part of #914. Four surfaces computed "improving/steady/declining" independently and disagreed for the same player/roster:

  • Brief category cards: per-round metric, 5-vs-5 window, 0.5 threshold.
  • Players-tab roster table: read golf_player_stats_cache.trend_direction (a DB-trigger value with its own 5-vs-5 window + 1.0 threshold).
  • Team Stats trajectory tile / player cards: live 5-vs-5, 0.3 threshold.

Same rounds, three thresholds/sources, so a player could read "declining" on the roster table and "improving" on Team Stats. The Brief also summed attentionCount across all 5 categories, double-counting players flagged in more than one ("8 need attention" on a 7-player team). Fix introduces one shared trend module consumed by all surfaces.

Risk / areas: golf CoachHelm trend logic across Brief, Players tab, Team Stats.

Watch: every surface now adopts the unified threshold — displayed trends will shift for some players (expected); the de-duped attention count; parity of the new module vs the retired DB-trigger value.

CI: ✅ green so far — 34 checks passing, 4 pending, 0 failing; mergeable state BLOCKED on required review (no CI failure). Awaiting review.

…opment page import conflict

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

Copy link
Copy Markdown
Owner Author

Superseded — landed on main inside merge train #938 (commit 6ecede6). Branch kept.

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.

[QA] CoachHelm tells four different stories about the same team — cross-surface number coherence

1 participant