Skip to content

fix(baseball): distinct insight_type for hit/HR milestones (#478)#642

Merged
njrini99-code merged 1 commit into
mainfrom
fix/baseball-478-auto
Jul 3, 2026
Merged

fix(baseball): distinct insight_type for hit/HR milestones (#478)#642
njrini99-code merged 1 commit into
mainfrom
fix/baseball-478-auto

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

What & Why

Fixes #478.

Legacy analyzePlayer in src/app/baseball/actions/insights.ts emitted separate hit-milestone and HR-milestone insights, but both used insight_type: 'development_milestone'. Reconciliation keys insights by `${playerId}::${insight_type}` (lines 142-143), so when a player crossed both thresholds in one run (e.g. 50 hits and 10 HR), only one milestone survived — the other was dropped or blocked the first from refreshing. Coaches could miss an achievement entirely.

Fix

  • Hit-milestone branch now uses insight_type: 'milestone_hits'.
  • HR-milestone branch now uses insight_type: 'milestone_hr'.
  • Added both literals to the BaseballInsightType union in src/lib/types/index.ts so types stay correct.

Distinct types make the reconcile key naturally unique per milestone kind, so each is inserted/refreshed independently.

No DB migration required: baseball_coach_insights.insight_type is plain text with no CHECK constraint (verified in 20260527000000_prod_public_baseline.sql). The substring-tolerant templateForInsightType in practice-intelligence.ts still resolves safely (default template).

Acceptance criteria

  • Use distinct insight_type values (milestone_hits, milestone_hr).
  • Player with both milestones at once generates two persistable insights (different reconcile keys → two inserts).
  • Re-running generation updates each milestone independently without overwriting the other (each keyed row refreshes in place).

Scope

Minimal: 2 files, no auth changes, no GRANT, no destructive writes, no migration.

🤖 Generated with Claude Code

…ersist (#478)

Legacy analyzePlayer emitted both the hit-milestone and HR-milestone
insights with insight_type 'development_milestone'. Reconciliation keys
insights by `${playerId}::${insight_type}`, so when a player crossed
both thresholds in one run only one milestone survived; the other was
dropped or blocked the first from refreshing.

Give the two branches distinct insight_type values (milestone_hits /
milestone_hr) and add them to the BaseballInsightType union. Both now
reconcile and refresh independently. No DB migration needed —
baseball_coach_insights.insight_type is plain text with no CHECK
constraint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VUauUXsm9aXegShmpJi5n
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
helmv3 Ready Ready Preview, Comment Jul 1, 2026 4:58pm

Request Review

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix baseball milestone insights overwriting each other during reconciliation

🐞 Bug fix 🕐 10-20 Minutes

Grey Divider

AI Description

• Give hit and HR milestone insights distinct insight_type values to avoid key collisions.
• Ensure reconciliation persists/refreshes both milestones when earned in the same run.
• Extend BaseballInsightType union to keep new insight types type-safe.
Diagram

graph TD
  T["types/index.ts: BaseballInsightType"] --> A["insights.ts: analyzePlayer()"] --> B["Insight payload"] --> C["Reconcile key\nplayerId::insight_type"] --> D[("baseball_coach_insights")]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Change reconciliation key (include milestone kind/threshold)
  • ➕ Avoids needing new insight_type literals for every milestone variant
  • ➕ Keeps insight_type semantics broader if desired
  • ➖ Larger behavioral change to a core dedupe mechanism
  • ➖ May require backfill/migration logic depending on how keys are stored/compared
  • ➖ Higher risk of unintended duplicates across other insight categories
2. Add an explicit `insight_subtype`/`milestone_kind` column
  • ➕ Models milestone kind explicitly and keeps insight_type stable
  • ➕ Supports future milestone categories without proliferating insight_type values
  • ➖ Requires schema migration and likely code changes across read/write paths
  • ➖ More review surface area than necessary for this bug

Recommendation: Proceed with the PR’s approach: introducing distinct insight_type values is the smallest, safest fix that naturally resolves the reconciliation collision without changing dedupe semantics globally or requiring a DB migration. The alternatives are viable but disproportionate for the scope of this bug.

Files changed (2) +11 / -2

Enhancement (1) +4 / -0
index.tsAdd milestone_hits and milestone_hr to BaseballInsightType +4/-0

Add milestone_hits and milestone_hr to BaseballInsightType

• Extends the 'BaseballInsightType' union with the new milestone-specific insight type literals so the new values are type-safe across the codebase.

src/lib/types/index.ts

Bug fix (1) +7 / -2
insights.tsUse distinct insight_type values for hit vs HR milestones +7/-2

Use distinct insight_type values for hit vs HR milestones

• Changes the hit-milestone insight to 'insight_type: 'milestone_hits'' and the HR-milestone insight to 'insight_type: 'milestone_hr''. This prevents reconciliation collisions when both milestones are earned in the same generation run.

src/app/baseball/actions/insights.ts

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 93 rules

Grey Divider


Remediation recommended

1. Legacy milestone state stranded 🐞 Bug ≡ Correctness
Description
By changing milestone insights from insight_type 'development_milestone' to
'milestone_hits'/'milestone_hr', any existing persisted milestone rows will no longer match the
reconciliation key ${playerId}::${insight_type} and will never be refreshed or have their
dismissed/resolved status respected for the new types. The next run can therefore re-surface
milestones a coach previously dismissed/resolved (as “new” insight types) while leaving the legacy
rows behind.
Code

src/app/baseball/actions/insights.ts[R303-306]

+      // Distinct from the HR milestone so both survive reconciliation, which
+      // keys insights by `${playerId}::${insight_type}`. A player crossing both
+      // a hit and an HR milestone in one run now persists two independent rows.
+      insight_type: 'milestone_hits',
Relevance

⭐⭐ Medium

Team fixed “don’t resurface triaged insights” before, but rejected similar legacy-data
backfill/purge concerns elsewhere.

PR-#575
PR-#330
PR-#584

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new milestone insights now emit different insight_type values, but reconciliation still keys
uniqueness and refresh behavior solely on ${playerId}::${insight_type} and never renames/deletes
existing rows, so legacy 'development_milestone' entries cannot match the new keys and their
status/refresh behavior won’t transfer.

src/app/baseball/actions/insights.ts[136-170]
src/app/baseball/actions/insights.ts[295-334]
src/lib/types/index.ts[101-121]

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

## Issue description
Milestone insights used to persist as `insight_type='development_milestone'`. This PR changes new emissions to `milestone_hits` / `milestone_hr`, but reconciliation is keyed by `${playerId}::${insight_type}` and does not delete/rename legacy rows. As a result, legacy milestone rows become orphaned from refresh logic and coach decisions (dismissed/resolved) won’t carry forward to the new milestone types.

## Issue Context
- Reconciliation uses `keyOf(player_id, insight_type)` and only inserts when no matching key exists; it also only respects dismissed/resolved for matching keys.
- After this PR, a team/coach with existing `development_milestone` rows will start inserting `milestone_hits` / `milestone_hr` rows instead of updating or respecting the prior row’s status.

## Fix Focus Areas
- src/app/baseball/actions/insights.ts[136-170]
- src/app/baseball/actions/insights.ts[295-334]

## Suggested fix approach
1. Add a backward-compat mapping step when building `existingByKey`:
  - If `row.insight_type === 'development_milestone'`, inspect `row.metadata` (or title) to infer whether it represents hits vs HR (e.g., `metadata.total_hits` -> `milestone_hits`, `metadata.total_hr` -> `milestone_hr`).
  - Register the row under the inferred new key as well, so updates land on the existing row and dismissed/resolved status remains effective.
2. Optionally, perform an in-action normalization update for active rows:
  - `UPDATE baseball_coach_insights SET insight_type='milestone_hits' ... WHERE insight_type='development_milestone' AND metadata ? 'total_hits'` (and similarly for HR), scoped to `{team_id, coach_id}`.
  - This prevents long-lived legacy rows and ensures future code paths only see the new types.
3. Keep a safe fallback for any `development_milestone` rows that can’t be classified (leave them as-is).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +303 to +306
// Distinct from the HR milestone so both survive reconciliation, which
// keys insights by `${playerId}::${insight_type}`. A player crossing both
// a hit and an HR milestone in one run now persists two independent rows.
insight_type: 'milestone_hits',

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. Legacy milestone state stranded 🐞 Bug ≡ Correctness

By changing milestone insights from insight_type 'development_milestone' to
'milestone_hits'/'milestone_hr', any existing persisted milestone rows will no longer match the
reconciliation key ${playerId}::${insight_type} and will never be refreshed or have their
dismissed/resolved status respected for the new types. The next run can therefore re-surface
milestones a coach previously dismissed/resolved (as “new” insight types) while leaving the legacy
rows behind.
Agent Prompt
## Issue description
Milestone insights used to persist as `insight_type='development_milestone'`. This PR changes new emissions to `milestone_hits` / `milestone_hr`, but reconciliation is keyed by `${playerId}::${insight_type}` and does not delete/rename legacy rows. As a result, legacy milestone rows become orphaned from refresh logic and coach decisions (dismissed/resolved) won’t carry forward to the new milestone types.

## Issue Context
- Reconciliation uses `keyOf(player_id, insight_type)` and only inserts when no matching key exists; it also only respects dismissed/resolved for matching keys.
- After this PR, a team/coach with existing `development_milestone` rows will start inserting `milestone_hits` / `milestone_hr` rows instead of updating or respecting the prior row’s status.

## Fix Focus Areas
- src/app/baseball/actions/insights.ts[136-170]
- src/app/baseball/actions/insights.ts[295-334]

## Suggested fix approach
1. Add a backward-compat mapping step when building `existingByKey`:
   - If `row.insight_type === 'development_milestone'`, inspect `row.metadata` (or title) to infer whether it represents hits vs HR (e.g., `metadata.total_hits` -> `milestone_hits`, `metadata.total_hr` -> `milestone_hr`).
   - Register the row under the inferred new key as well, so updates land on the existing row and dismissed/resolved status remains effective.
2. Optionally, perform an in-action normalization update for active rows:
   - `UPDATE baseball_coach_insights SET insight_type='milestone_hits' ... WHERE insight_type='development_milestone' AND metadata ? 'total_hits'` (and similarly for HR), scoped to `{team_id, coach_id}`.
   - This prevents long-lived legacy rows and ensures future code paths only see the new types.
3. Keep a safe fallback for any `development_milestone` rows that can’t be classified (leave them as-is).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: Playwright (chromium)

Failed stage: Seed BaseballHelm CI accounts [❌]

Failed test name: ""

Failure summary:

The action failed during the seed:baseball:ci step because a required environment variable was not
set.
- The script tsx scripts/seed-baseball-demo.ts --confirm aborted with Missing required env var:
SUPABASE_SERVICE_ROLE_KEY (log line 833).
- SUPABASE_SERVICE_ROLE_KEY is shown as empty in the
environment dump (log line 823), causing the process to exit with code 1 (log line 834).

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

223:  ##[endgroup]
224:  npm warn EBADENGINE Unsupported engine {
225:  npm warn EBADENGINE   package: '@capacitor/cli@8.4.0',
226:  npm warn EBADENGINE   required: { node: '>=22.0.0' },
227:  npm warn EBADENGINE   current: { node: 'v20.20.2', npm: '10.8.2' }
228:  npm warn EBADENGINE }
229:  npm warn EBADENGINE Unsupported engine {
230:  npm warn EBADENGINE   package: 'mute-stream@4.0.0',
231:  npm warn EBADENGINE   required: { node: '^22.22.2 || ^24.15.0 || >=26.0.0' },
232:  npm warn EBADENGINE   current: { node: 'v20.20.2', npm: '10.8.2' }
233:  npm warn EBADENGINE }
234:  npm warn deprecated @types/mapbox-gl@3.5.0: This is a stub types definition. mapbox-gl provides its own type definitions, so you do not need this installed.
235:  npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
236:  npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
237:  npm warn deprecated glob@7.2.3: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
238:  npm warn deprecated serialize-error-cjs@0.1.4: Rolling release, please update to 0.2.0
239:  npm warn deprecated prebuild-install@7.1.3: No longer maintained. Please contact the author of the relevant native addon; alternatives are available.
...

575:  ├ ƒ /api/cron/v3/causality-attribute
576:  ├ ƒ /api/cron/v3/genome-backfill
577:  ├ ƒ /api/cron/v3/genome-nightly
578:  ├ ƒ /api/cron/v3/goal-suggestions-evaluate
579:  ├ ƒ /api/cron/v3/goal-suggestions-write
580:  ├ ƒ /api/cron/v3/ingest-sync
581:  ├ ƒ /api/cron/v3/standing-backfill
582:  ├ ƒ /api/cron/v3/standing-refresh
583:  ├ ƒ /api/cron/v3/weekly-coach-email
584:  ├ ƒ /api/golf/auth/login
585:  ├ ƒ /api/golf/players/[playerId]/putt-tendencies
586:  ├ ƒ /api/golf/rounds/generate-review
587:  ├ ƒ /api/golf/rounds/partial-save
588:  ├ ƒ /api/health
589:  ├ ƒ /api/inngest
590:  ├ ƒ /api/log-error
591:  ├ ƒ /api/push-subscriptions
...

819:  NEXT_PUBLIC_SUPABASE_ANON_KEY: ***
820:  NEXT_PUBLIC_REDESIGN: true
821:  E2E_GOLF_EMAIL: ***
822:  E2E_GOLF_PASSWORD: ***
823:  SUPABASE_SERVICE_ROLE_KEY: 
824:  E2E_BASEBALL_COACH_EMAIL: 
825:  E2E_BASEBALL_COACH_PASSWORD: 
826:  E2E_BASEBALL_PLAYER_EMAIL: 
827:  E2E_BASEBALL_PLAYER_PASSWORD: 
828:  COMMIT_SHA: fc3c19a0b58822a07b8744cac71a426e85cdcad5
829:  NODE_OPTIONS: --max-old-space-size=8192
830:  ##[endgroup]
831:  > helmv3@1.0.0 seed:baseball:ci
832:  > tsx scripts/seed-baseball-demo.ts --confirm
833:  Missing required env var: SUPABASE_SERVICE_ROLE_KEY — cannot seed baseball CI accounts
834:  ##[error]Process completed with exit code 1.
835:  ##[group]Run actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a

@njrini99-code

Copy link
Copy Markdown
Owner Author

🤖 Mission Control — PR summary

Changes: Gives the hit-milestone and HR-milestone insights distinct insight_type values so both persist (reconciliation keys on playerId::insight_type). Closes #478.
Areas / risk: CoachHelm insight reconciliation — adds enum values (no DB CHECK constraint, no migration).
Watch: A player crossing both thresholds now keeps both milestones.
CI: ❌ Unit tests, Supabase lint + RLS tests, Playwright, semgrep (custom rules) failing — review before merge. (lighthouse-preview = known noise.)
Note: Bundled into batch #650.

@njrini99-code njrini99-code merged commit 2e53f2b into main Jul 3, 2026
28 of 36 checks passed
@njrini99-code njrini99-code deleted the fix/baseball-478-auto branch July 3, 2026 01:13
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.

BaseballHelm: Collapse competing development_milestone insights to one per player

1 participant