Skip to content

ci(sports): gate the copy-forward rule that keeps the scoreboards in step - #478

Merged
ChuckBuilds merged 2 commits into
mainfrom
chore/sports-drift-check
Sep 10, 2026
Merged

ci(sports): gate the copy-forward rule that keeps the scoreboards in step#478
ChuckBuilds merged 2 commits into
mainfrom
chore/sports-drift-check

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Depends on #474 (the ufc-scoreboard throttle fix). This branch is based on it, so the baseline is generated with that fix in place — which is what lets the gate catch the bug's reintroduction. Merge #474 first.

Why

The scoreboards ship copies of sports.py, manager.py, game_renderer.py and scroll_display.py rather than sharing them. That is deliberate — CLAUDE.md non-negotiable #7. The rule that makes it safe is "a fix in one lineage must be ported to its siblings in the same PR", and until now it was enforced only by whoever remembered.

It was missed repeatedly. The has_live_content() throttle went into baseball (1.20.4), then football, then hockey/basketball/lacrosse (#308) — and ufc was still unfixed months later, logging once per display frame during a live card.

What it checks

Both checks are baselined against the current tree, so they start green and fail only on something new.

1. Shape drift

A function whose copies all agree today must not start disagreeing. Bodies compare as normalised ASTs — docstrings dropped, numeric constants collapsed, sport tokens folded so mlb_live and nfl_live are the same shape.

CamelCase folding is load-bearing, not a nicety: each plugin names its manager class after its own sport, so without it nothing in manager.py is ever compared to its siblings — and has_live_content() lives in manager.py.

46 functions are unanimous across 5+ lineages; those are what this protects. 140 already differ and are recorded and ignored. The goal is not uniformity — it is that a shared fix reaches every sibling.

2. Unthrottled display-path logging

An info/warning call in has_live_content(), display() or get_vegas_content() with no throttle guard above it. These run once per frame in Vegas mode.

Shape comparison cannot catch this: has_live_content() legitimately differs between sports (one league versus three), so it is already divergent everywhere and a similarity check has nothing to compare against. This is a property check instead, and it is the one that catches the ufc bug.

Calls inside except: blocks and at error level are excluded — something going wrong is not the per-frame happy path.

Verified, not assumed

scenario result
clean tree exit 0
revert the ufc fix exit 1, naming ufc-scoreboard/manager.py:863
edit one lineage's copy of _coerce_rgb (shared by 8) exit 1, naming hockey as the odd one out

The gate's own regression suite

scripts/test_check_sports_drift.py, following the pattern module-collisions.yml already establishes.

This check reports by absence, so "looked and found nothing wrong" and "did not really look" both print OK and exit 0. That is not hypothetical: the first version recursed into class bodies with a bare walk(child) instead of yield from walk(child), collected 16 functions instead of 386, and reported a clean confident result on 96% of the code it never opened.

So the suite asserts the gate still detects — including the exact nesting shape of the ufc bug, where a guard exists in the function but the offending call sits outside it — and that it is still reading a plausible amount of code.

🤖 Generated with Claude Code

https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9

…frame

has_live_content() runs on the display path -- once per *frame* in Vegas mode.
UFC guarded only the False branch: `should_log and not ufc_live` sat at the
bottom of the function, but a second INFO line lived inside the `if live_games:`
block above it with no guard at all. Whenever a card was live that call fired on
every frame, roughly 50 lines a second on a rig measured at 50 fps.

Because it sat outside the guard, every earlier throttle fix missed it --
baseball 1.20.4, football, and #308 for hockey/basketball/lacrosse all inspected
the guard rather than the whole function body. It has been there since the
plugin landed in #24.

Both branches now share one throttle in baseball-scoreboard's shape: log when
the answer changes, and re-log an unchanged answer once per 60s so a steady
state stays visible.

test_live_content_log_throttle.py is ported from baseball's, plus a case for the
exact state that triggered it -- a live card with favorites configured and none
of them fighting, where ufc_live is False but live_games is not empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
…step

The scoreboards ship copies of sports.py, manager.py, game_renderer.py and
scroll_display.py rather than sharing them, which is deliberate (CLAUDE.md
non-negotiable #7). The rule that makes it safe -- port a fix to its siblings
in the same PR -- was enforced only by whoever remembered.

It was missed repeatedly. The has_live_content() throttle went into baseball
(1.20.4), then football, then hockey/basketball/lacrosse (#308), and ufc was
still unfixed months later, logging once per display frame during a live card.

Two checks, both baselined against the current tree so they start green and
fail only on something new.

1. Shape drift. A function whose copies all AGREE today must not start
   disagreeing. Bodies compare as normalised ASTs: docstrings dropped, numeric
   constants collapsed, and sport tokens folded so mlb_live and nfl_live are
   the same shape. Folding handles CamelCase as well as snake_case, which is
   load-bearing -- each plugin names its manager class after its own sport, so
   without it nothing in manager.py is ever compared to its siblings, and
   has_live_content() lives in manager.py. 46 functions are unanimous across
   five or more lineages; those are what this protects. 140 already differ and
   are recorded and ignored: the goal is not uniformity, it is that a shared
   fix reaches every sibling.

2. Unthrottled display-path logging. An info/warning call in
   has_live_content(), display() or get_vegas_content() with no throttle guard
   above it. Shape comparison cannot catch this: has_live_content()
   legitimately differs between sports -- one league versus three -- so it is
   already divergent everywhere and similarity has nothing to compare. This is
   a property check instead, and it is the one that catches the ufc bug.

Verified end to end rather than assumed:
  - clean tree                                     -> exit 0
  - revert the ufc fix                             -> exit 1, naming
                                                      manager.py:863
  - edit one lineage's copy of _coerce_rgb (shared
    by 8) -> exit 1, naming hockey as the odd one out

test_check_sports_drift.py is the gate's own regression suite, following the
pattern module-collisions.yml already uses. This check reports by absence, so
"looked and found nothing" and "did not really look" both print OK -- the
first version of it recursed into class bodies with a bare walk() instead of
`yield from`, inspected 16 functions instead of 386, and reported a clean
result on code it never opened. The suite asserts it still detects, and that
it is still reading a plausible amount of code.

Depends on the ufc-scoreboard throttle fix: the baseline is generated with
that fix in place, so reintroducing the bug fails the gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 125e333c-d2df-4092-8e9f-25307081475d


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.

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 128 complexity

Metric Results
Complexity 128

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@ChuckBuilds
ChuckBuilds merged commit 8e7eec1 into main Sep 10, 2026
5 checks passed
@ChuckBuilds
ChuckBuilds deleted the chore/sports-drift-check branch September 10, 2026 13:35
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.

2 participants