fix(coachhelm): close insight-staleness engine races (#920) - #925
fix(coachhelm): close insight-staleness engine races (#920)#925njrini99-code wants to merge 1 commit into
Conversation
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
|
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 reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Warning Review limit reached
Next review available in: 59 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (10)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
🤖 Mission Control — PR summary What it changes: Closes four races in the post-round → insight pipeline that let CoachHelm insights go stale silently (#920):
Risk / areas: golf CoachHelm engine, roster-sweep cron, Watch: CI: ✅ green so far — 34 checks passing, 4 pending, 0 failing; mergeable state BLOCKED on required review (no CI failure). Awaiting review. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
…opment page import conflict Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMdviLDsAg2YYJ8adsM6fg
Problem
Four independent bugs in the round-submit → CoachHelm insight pipeline were letting insights go silently stale or misleading:
submitGolfRoundComprehensive(src/app/golf/actions/golf.ts) registered TWO independentafter()callbacks — one for the stats-cache refresh (invalidateOnRoundComplete) and one for the CoachHelm trigger (postRoundTrigger).after()callbacks run concurrently, not in registration order, so the engine could readgolf_player_stats_cachebefore the cache refresh finished writing it, producing insights/predictions off stale data.src/app/api/cron/coachhelm-roster-sweep/route.tsskipped a player whenever ANY of their rounds hadcoachhelm_analyzed_atset within a 12h window — even if that was an OLDER round and their genuinely most-recent completed round was still unanalyzed (e.g. an earlier round got re-analyzed by the safety-net cron inside that same 12h). A player who played again shortly after such a re-analysis would be silently skipped by the nightly sweep.alert_posture='silent'(src/app/golf/actions/insights.ts) resolves the confidence gate toInfinity, silently blocking every insight for that player — with zero signal anywhere (log or UI) that this happened.FairwayBrief.tsx) renderedgolf_rounds.round_date(a DATE column with no time component) throughnew Date(dateOnly).toISOString()(parses as UTC midnight) then a viewer-timezonetoLocaleStringwithhour/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.Fix
after()callbacks into one:invalidateOnRoundCompleteis fully awaited beforepostRoundTriggerruns, inside a singleafter()registration.order('round_date', desc).order('created_at', desc).limit(1), fail-open on lookup error). Skip only when THAT round'scoachhelm_analyzed_atis set.logServerEvent('...alert_posture=silent...', {..., skipSentry: true}, 'info')call, once per run, right where the confidence threshold goes infinite. (b) Threaded asilentPostureByPlayermap (from the sameloadCoachIntents(coach.id)the Roster page already loads — no new query pattern) intoPlayersGridView's roster row (nameAddonslot onPlayerIdentity), showing an "Insights muted" badge for players with silent posture on the CoachHelm Players tab.team-category-insights.tsnow passes the plain'YYYY-MM-DD'round_datestraight through (no fabricated.toISOString());FairwayBrief.formatAnalyzedanchors both the parse (${dateOnly}T00:00:00Z) and the format (timeZone: 'UTC') in UTC and dropshour/minute— renders "Jun 8", never a clock time, and is immune to the viewer's local timezone.Tests
Added 3 regression test files, each verified to fail against the pre-fix code and pass against the fix (confirmed by manually reverting each fix locally, re-running, and restoring):
src/app/golf/actions/__tests__/golf-round-submit-after-chain.test.ts— drivessubmitGolfRoundComprehensivethrough a fake Supabase client + mocked RPC, and assertsinvalidateOnRoundCompletealways completes beforepostRoundTriggerruns (order array + real macrotask delay to make the race deterministic to reproduce under the old code).src/test/api/cron/coachhelm-roster-sweep.test.ts— three-player fixture reproducing the exact bug shape: a player whose latest round is unanalyzed but an EARLIER round was recently analyzed must NOT be skipped; a player whose only round is analyzed (regardless of age) must be skipped.src/components/fairway/pages/coachhelm/FairwayBrief.formatAnalyzed.test.ts— asserts date-only output, no time-of-day, and TZ-stubbed (vi.stubEnv('TZ', ...)) tests proving immunity to viewer timezone in both directions.Fix 3 (log line + UI badge) doesn't have a natural narrow unit-test seam in the existing layout (deeply embedded in
triggerPlayerInsightsAfterRoundImpl's engine call and a presentational roster-row prop) — verified viatsc/eslintand manual code read instead, per the task's "where the repo's test layout allows" scoping.Gates
npx tsc --noEmit -p tsconfig.json— cleannpx eslint <all changed files>— cleannpx vitest run --project=unitacrosssrc/app/golf/actions/__tests__,src/test/api/cron,src/components/fairway/pages/coachhelm— 55 files / 492 tests passedScope notes
vercel.jsoncron schedules (separate PR per task).Fixes #920
🤖 Generated with Claude Code