Skip to content

fix(baseball): thread engine's nowIso through rolling-window loaders - #811

Merged
njrini99-code merged 2 commits into
mainfrom
fix/engine-clock-injection
Jul 15, 2026
Merged

fix(baseball): thread engine's nowIso through rolling-window loaders#811
njrini99-code merged 2 commits into
mainfrom
fix/engine-clock-injection

Conversation

@njrini99-code

Copy link
Copy Markdown
Owner

Root cause

PR #808 (CI guardrails) was blocked by two failing required checks — "Unit tests" and "Business contracts" — both on the same failure in src/lib/baseball/__tests__/engine-run-helm-lifting.test.ts. That test fixes the engine's clock at nowIso: '2026-06-30T12:00:00.000Z' and seeds an RPE set result at completed_at: '2026-06-29', one day earlier — well inside a 14-day rolling window.

The failure: loadLiftMetrics() in src/lib/coachhelm/baseball/loaders-v10.ts computed its rolling-window cutoff from Date.now() — the real wall clock — instead of the nowIso that runBaseballEngineCore already threads through for deterministic runs. As real time advanced past ~14 days from the seeded date, the fixed-clock test's data aged out of a window measured against today's actual date, and the test started failing with zero code changes. Confirmed: this exact PR branch passed clean on this exact test 3 days ago and started failing today, purely from the calendar advancing.

Fix

Threaded nowIso through the full call chain, not just the one function the failing test touches — the same class of bug was present in three sibling loaders:

  • loadReadinessMetrics (7-day readiness window) and loadLiftMetrics (session-due check + 14-day RPE window) in loaders-v10.ts, called via mergeV10PlayerMetrics
  • loadPlayerMetrics's pitching-workload calc (7-day window) in loaders.ts, called via loadAllPlayerMetrics
  • loadCatchingMetrics's recent-innings calc (7-day window) in loaders-events.ts, called via mergeEventPlayerMetrics

engine-run.ts's three call sites (loadAllPlayerMetrics, mergeV10PlayerMetrics, mergeEventPlayerMetrics) now pass through the nowIso already in scope there.

Each new nowIso parameter defaults to new Date().toISOString(), matching the existing input.now ?? new Date() pattern already used in effectiveness/engine.ts — so the three other callers of loadPlayerMetrics (practice-effectiveness.ts, action-baseline.ts, outcome-sweep.ts) keep their current real-time behavior unchanged. Only the deterministic engine-run path needed fixing.

Explicitly out of scope

generators/v10.ts's importQualityGenerator has the same class of bug (filters recent import runs via Date.now()), but BaseballV10EngineInputs has no now/nowIso field for generators at all today — threading one through would mean adding a field to a widely-shared interface used by every generator, which is a bigger, separate change. Flagging it here rather than silently leaving it.

Verification

Ran the exact commands CI's "Unit tests" and "Business contracts" jobs run (no pipe-masking):

  • npm run test:run (vitest unit project): 494 test files / 4921 tests passed, 0 failures
  • npm run test:business (vitest business project): 505 test files / 4996 tests passed, 0 failures
  • npm run typecheck: clean
  • Also ran the originally-failing spec directly and the full loader/engine test directories — all green

Both of these were the exact two checks failing on PR #808 before this fix.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Root cause of the intermittently-failing engine-run-helm-lifting test
(and a latent correctness gap that would eventually hit every rolling-
window metric the same way): loadLiftMetrics, loadReadinessMetrics,
loadPlayerMetrics' workload calc, and loadCatchingMetrics all computed
their cutoff from Date.now() — the real wall clock — instead of the
nowIso already threaded through runBaseballEngineCore for deterministic
runs. A test fixing "now" at a past date and seeding data just inside a
14-day window would pass only as long as real time hadn't yet carried
that window past the seeded date; it then fails with zero code changes
once it did (confirmed: passed on this exact branch 3 days before it
started failing, purely from the calendar advancing).

Threaded nowIso through the full call chain instead of just the one
function the failing test touches: mergeV10PlayerMetrics -> {
loadReadinessMetrics, loadLiftMetrics }, loadAllPlayerMetrics ->
loadPlayerMetrics, mergeEventPlayerMetrics -> loadCatchingMetrics, with
engine-run.ts's three call sites now passing its own nowIso through.
Each function's new nowIso param defaults to new Date().toISOString()
(matching the existing input.now ?? new Date() pattern in
effectiveness/engine.ts) so the three OTHER callers of loadPlayerMetrics
(practice-effectiveness.ts, action-baseline.ts, outcome-sweep.ts) keep
their current real-time behavior unchanged — only the deterministic
engine-run path needed fixing.

Left out of scope (same class, but a bigger architectural change and
not the cause of any current failure): generators/v10.ts's
importQualityGenerator also filters recent import runs via Date.now(),
but BaseballV10EngineInputs has no now/nowIso field for generators at
all today — adding one is a separate, larger PR.

Verified: full `npm run test:run` (unit project, 494 files/4921 tests)
and `npm run test:business` (business project, 505 files/4996 tests)
both pass clean — these are the exact commands CI's "Unit tests" and
"Business contracts" jobs run, and the ones that were failing on
PR #808 before this fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

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

@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

@vercel

vercel Bot commented Jul 15, 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 15, 2026 2:10am

Request Review

@supabase

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

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 78c22544-7001-4e89-9155-55203236760d

📥 Commits

Reviewing files that changed from the base of the PR and between 5958e37 and 307b5c0.

📒 Files selected for processing (4)
  • src/lib/baseball/coachhelm/engine-run.ts
  • src/lib/coachhelm/baseball/loaders-events.ts
  • src/lib/coachhelm/baseball/loaders-v10.ts
  • src/lib/coachhelm/baseball/loaders.ts

Summary by CodeRabbit

  • Bug Fixes
    • Improved consistency of rolling 7-day workload, readiness, lifting, and catching metrics.
    • Ensured metric calculations use a consistent reference time, producing more reliable results across reports and processing runs.
    • Preserved existing behavior when no reference time is provided.

Walkthrough

The change threads nowIso through baseball metric loaders and engine calls. Rolling workload, catching, readiness, and lift windows now derive from the supplied timestamp rather than independently reading the current clock.

Changes

Deterministic metric windows

Layer / File(s) Summary
Propagate the run timestamp through base loaders
src/lib/coachhelm/baseball/loaders.ts
loadPlayerMetrics and loadAllPlayerMetrics accept nowIso, use it for workload cutoffs, and share it across players.
Align event and V10 metric windows
src/lib/coachhelm/baseball/loaders-events.ts, src/lib/coachhelm/baseball/loaders-v10.ts
Catching, readiness, and lift calculations derive rolling-window baselines from the supplied timestamp.
Thread the run clock through engine execution
src/lib/baseball/coachhelm/engine-run.ts
The RUN phase passes nowIso into player metric loading and V10 metric merging.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 12
✅ Passed checks (12 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits with the required baseball scope and matches the loader time-threading change.
Description check ✅ Passed The description directly explains the rolling-window clock bug and the nowIso threading fix reflected in the changed files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No Service-Role In Client Bundles ✅ Passed Changed files are only engine-run.ts and three baseball loader files; none contain SUPABASE_SERVICE_ROLE_KEY or service-role client creation.
Rls Coverage On New Tables ✅ Passed No migration files changed in this PR diff, so the RLS migration check is not applicable.
Auth Check In Server Actions ✅ Passed No changed files under src/app//actions//*.ts; PR diff only touches four src/lib/baseball files.
Sport-Prefixed Table Names ✅ Passed PASS: engine-run.ts only queries baseball_* tables (291-531, 588-974); loaders-events.ts/loaders-v10.ts/loaders.ts have no .from() calls.
No Destructive Writes ✅ Passed PASS: PR-only files are timestamp-only loader changes; no DELETE+INSERT rebuild path, and engine-run uses upsert/update only (engine-run.ts:40-41,720-777,875-901,959-973).
No Edits To Historical Migrations ✅ Passed PR diff vs origin/main touches only src/lib/baseball/coachhelm/engine-run.ts and loaders files; no supabase/migrations/ files are changed.
Conventional Commits ✅ Passed HEAD^1 subject is fix(baseball): thread engine's nowIso through rolling-window loaders, which matches the required conventional-commit regex.
✨ 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/engine-clock-injection
  • 🛠️ 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

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/lib/baseball/coachhelm/engine-run.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

src/lib/coachhelm/baseball/loaders-events.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

src/lib/coachhelm/baseball/loaders-v10.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.

  • 1 others

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.

@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

Fixes a wall-clock leak in four rolling-window loaders that caused deterministic engine-run tests to fail silently as real time advanced past the fixed test clock. All affected loaders now accept a nowIso parameter (defaulting to new Date().toISOString()) so the engine's threaded clock drives every window cutoff.

  • loaders-v10.ts: loadReadinessMetrics (7-day) and loadLiftMetrics (session-due + 14-day RPE) replace Date.now() with Date.parse(nowIso), surfaced via mergeV10PlayerMetrics.
  • loaders.ts: loadPlayerMetrics's pitching-workload 7-day window and its loadAllPlayerMetrics wrapper gain the same parameter; the three non-engine callers omit it and correctly keep real-time behavior.
  • loaders-events.ts: loadCatchingMetrics's recent-innings 7-day window and mergeEventPlayerMetrics gain the parameter; all other event loaders (season aggregates) are untouched.

Confidence Score: 5/5

Safe to merge. The change is a minimal, well-scoped threading of an existing clock parameter through four loader functions with no logic mutations.

Every rolling-window cutoff that previously called Date.now() in the deterministic engine path now uses Date.parse(nowIso). The three real-time callers outside the engine are unaffected — they omit the new parameter and get new Date().toISOString() as the default. The default parameter is evaluated at call time so there is no stale-clock risk. No new logic, no schema changes, no RLS surface added.

No files require special attention. The generators/v10.ts wall-clock gap acknowledged in the PR description is out of scope here and warrants a separate follow-up.

Important Files Changed

Filename Overview
src/lib/baseball/coachhelm/engine-run.ts Threads the already-scoped nowIso through all three loader call sites; no logic changes to the engine itself.
src/lib/coachhelm/baseball/loaders-v10.ts Replaces Date.now() with Date.parse(nowIso) in loadReadinessMetrics (7-day) and loadLiftMetrics (session-due + 14-day RPE); both updated consistently via the public mergeV10PlayerMetrics facade.
src/lib/coachhelm/baseball/loaders.ts Adds nowIso to loadPlayerMetrics and loadAllPlayerMetrics; only the pitching-workload 7-day window used it — hitting/contact/command metrics are season aggregates unaffected.
src/lib/coachhelm/baseball/loaders-events.ts Threads nowIso through loadCatchingMetrics (7-day window) and mergeEventPlayerMetrics; the four other event loaders are season-aggregate and correctly untouched.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant E as engine-run.ts
    participant LA as loadAllPlayerMetrics
    participant V10 as mergeV10PlayerMetrics
    participant EV as mergeEventPlayerMetrics

    Note over E: nowIso = context.nowIso ?? new Date().toISOString()
    E->>LA: loadAllPlayerMetrics(playerIds, rows, nowIso)
    LA-->>E: "pitching workload cutoff = Date.parse(nowIso) - 7d"
    E->>V10: mergeV10PlayerMetrics(p, readiness, sessions, setResults, nowIso)
    V10-->>E: readiness 7d + RPE 14d cutoffs driven by nowIso
    E->>EV: mergeEventPlayerMetrics(base, eventInputs, nowIso)
    EV-->>E: catching innings 7d cutoff driven by nowIso
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant E as engine-run.ts
    participant LA as loadAllPlayerMetrics
    participant V10 as mergeV10PlayerMetrics
    participant EV as mergeEventPlayerMetrics

    Note over E: nowIso = context.nowIso ?? new Date().toISOString()
    E->>LA: loadAllPlayerMetrics(playerIds, rows, nowIso)
    LA-->>E: "pitching workload cutoff = Date.parse(nowIso) - 7d"
    E->>V10: mergeV10PlayerMetrics(p, readiness, sessions, setResults, nowIso)
    V10-->>E: readiness 7d + RPE 14d cutoffs driven by nowIso
    E->>EV: mergeEventPlayerMetrics(base, eventInputs, nowIso)
    EV-->>E: catching innings 7d cutoff driven by nowIso
Loading

Reviews (1): Last reviewed commit: "fix(baseball): thread engine's nowIso th..." | Re-trigger Greptile

@njrini99-code
njrini99-code merged commit dcaa59f into main Jul 15, 2026
40 checks passed
@njrini99-code
njrini99-code deleted the fix/engine-clock-injection branch July 15, 2026 02:42
njrini99-code pushed a commit that referenced this pull request Jul 15, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
njrini99-code added a commit that referenced this pull request Jul 15, 2026
…ce-table + event-derived wiring (#851)

loaders.ts now honestly threads stat-layer provenance instead of hardcoding
baseball_player_stats everywhere, without changing any existing caller's
output:

- Per-row hittingSourceTable/pitchingSourceTable tags (set by the new
  normalizeBoxScoreBattingRow/normalizeBoxScorePitchingRow helpers) let a
  future caller that has normalized rows from the canonical
  baseball_box_score_batting/_pitching tables get an honest source_refs
  citation instead of the legacy default.
- An optional eventDerived input (built via eventDerivedVelocityFromMetrics
  from elite-stat-events.ts's DerivedMetric[] output) makes exit/pitch
  velocity metrics prefer real event-grain data over the legacy scalar
  columns, per-field, whenever it exists — never blended, never fabricated.
- Both inputs are additive/optional appended AFTER the existing nowIso
  param (preserved exactly, including its threading from #811), so
  engine-run.ts/outcome-sweep.ts/action-baseline.ts/practice-effectiveness.ts
  (Phase 4b/2, not touched here) keep working byte-for-byte unchanged —
  verified by running their existing test suites.

registry.ts's header doc comment no longer asserts baseball_player_stats/
baseball_player_aggregates as the metrics' only source, since loaders.ts can
now source some fields canonically; the file has zero remaining references
to the deprecated tables, so its stat-layer-manifest.ts grandfathered entry
is removed. registry.role-visibility.test.ts's source_refs fixture switches
to a canonical table name for the same reason and is also removed from the
manifest. engine-v10.test.ts gains new coverage for the loaders behavior
above (still legitimately cites baseball_player_stats to pin the
legacy-fallback path, so its manifest entry is kept, with an updated note).

Pre-existing, unrelated drift found but NOT fixed here (confirmed via
git-stash A/B on the base branch, present before this change): the
stat-layer-contract test already fails on 3 files outside this chunk
(practice-effectiveness.test.ts, player-today-self-scope.test.ts,
player-today-honest-loop.test.ts reference deprecated tables without a
manifest entry) and 1 stale entry (insights.ts, already migrated by #819,
never removed from the manifest). Flagged for a separate fix — out of
scope for the loaders/registry file list.

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
njrini99-code added a commit that referenced this pull request Jul 15, 2026
…scoped precedence (supersedes #852) (#854)

* fix(baseball): CoachHelm engine Phase 4b — canonical stat reads for sweep/baseline/engine-run + deterministic import-quality clock (#379, #811 residual)

Reader migration (#379 Phase 4b, the highest-blast-radius chunk):
- NEW src/lib/baseball/coachhelm/engine-stat-rows.ts — the ONE consolidated
  per-session stat-row read for the engine. Prefers canonical
  baseball_box_score_batting/_pitching rows (normalized onto the loader shape
  via #851's normalizeBoxScoreBattingRow/normalizeBoxScorePitchingRow, with
  session_date joined from baseball_games and source-table provenance tags),
  reconciled over legacy baseball_player_stats rows per the #379 precedence
  rule: canonical rows replace a player's legacy GAME rows outright (never
  blended — the #827 seed writes the same games into both layers), legacy
  practice/other rows always survive (practice carve-out), and a player with
  zero canonical rows keeps full legacy history (fallback tier). Canonical-side
  read failures degrade all-or-nothing to the legacy pool; a legacy read
  failure remains the callers' hard error. All reads paginate past the
  PostgREST 1000-row cap with stable ordering.
- outcome-sweep.ts / action-baseline.ts / engine-run.ts all swap their direct
  baseball_player_stats reads for the shared helper, so baseline capture, the
  outcome sweep, and the engine run measure the SAME reconciled pool
  (apples-to-apples did-it-move). action-baseline's old single-page
  .limit(1000) read is replaced by the paginated shared read.

#811 residual (deterministic engine clock):
- BaseballV10EngineInputs gains an optional now (ISO); engine-run threads its
  nowIso through it; importQualityGenerator's 14-day recency window computes
  from the caller-supplied nowIso instead of raw Date.now() (default preserves
  real-time behavior for non-engine callers). New generators/v10.test.ts pins
  the window against a fixed 2020 clock; engine-run-helm-lifting.test.ts pins
  that runBaseballEngineCore passes its own nowIso end-to-end.

Provenance labels:
- generators/index.ts driver() last-resort fallback label no longer hardcodes
  the deprecated table (loaders now cite the real per-row table); v10.ts's
  practice-effectiveness cite stays deliberately (its feeder still reads
  legacy practice rows) with an explanatory comment.

Manifest (stat-layer contract kept green in both directions for this chunk):
- Removed migrated entries: outcome-sweep.ts, engine-run.ts,
  action-baseline.ts, generators/index.ts, ai-policy-enforcement.test.ts,
  signal-from-insight.test.ts (fixtures moved to canonical table names).
- Added: engine-stat-rows.ts + its test (the one allowed legacy-fallback read).
- Updated notes: loaders.ts, generators/v10.ts, effectiveness/engine.ts and
  operational-rule-engine.ts (both reviewed, deliberately deferred — their
  cites are honest while their feeders still read layer 1), plus the three
  engine test entries now pinning the fallback tier.

Tests: engine-stat-rows.test.ts pins the precedence rule directly;
action-baseline.test.ts + outcome-sweep-insight-resolve.test.ts gain
canonical-preferred, never-blended coverage alongside the existing
legacy-fallback pins.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9QAYqFTKsXGsVw6wXYssa

* fix(baseball): scope engine-stat-rows precedence to (player, date), not player alone (#379, #852 review fix)

The #379 exclusion rule dropped ALL of a player's legacy stat_type='game'
rows once they had ANY canonical box-score row, even for games with no
canonical counterpart — a mid-season box-score import start would silently
erase that player's earlier legacy-logged games from every engine caller
(engine-run, outcome-sweep, action-baseline), shrinking sample_n and
flipping confidence/verdicts. baseball_player_stats has no game_id, so we
now correlate on the resolved canonical game date (truncated to YYYY-MM-DD
on both sides) instead: a legacy game row is dropped only when that same
player has canonical coverage on that exact calendar day: a same-day
heuristic, not a guaranteed game-identity match, since there's no FK to
lean on (documented in the module comment as an accepted double-header
collision risk).

Existing precedence tests encoded the bug: their legacy-row fixture dates
never matched the canonical game dates, yet still asserted full drop —
only possible under the old player-scoped exclusion. Realigned those
fixture dates to same-day overlap (preserving each test's 100%-coverage
intent) and added a mixed-coverage case (3 legacy-only + 2 canonical -> 5
rows survive) plus a same-day-collision case (legacy row on a
canonically-covered date -> dropped).

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

---------

Co-authored-by: Fable Integrator <fable@helm.local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant