Repository navigation
fix(course): improve epilogue analytics signal and hardcode robustness - #73
Merged
Merged
Conversation
Deep-dive variant was warrior_vs_Goblin → hero_win_rate 1.0, same as primary — a hardcoded hero_win_rate=1.0 would pass both. Changed to warrior_vs_Dragon (hero always loses for this frozen L7 stat table), regenerated via the same real BattleService orchestration, verified byte-for-byte deterministic across two clean regenerations. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Quality-gate audit found hardest_monster, rank_monsters, and unfavorable_matchups are mathematically invariant to any run-count reweighting in this frozen combat model — the primary/variant pair alone couldn't catch a hardcoded answer for them. most_one_sided_matchup was always exactly tied across all 9 matchups (every matchup's win rate is 0.0 or 1.0), making it informationally empty; most_balanced_class was a 3-way tie in the recorded primary history, differentiated only by variant sampling weights rather than any real property of a hero class. - balance_conclusions() drops both misleading conclusions, adds longest_matchup (a real, continuously-varying avg_rounds signal) and one_sided_matchup_count (turns the deterministic-win-rate finding into the conclusion itself, rather than hiding it behind a forced ranking). - check.py adds behavioral subset probes: balance_conclusions, rank_monsters, and unfavorable_matchups are additionally run on controlled slices of the real primary tournament_history.csv (e.g. Orc+Dragon rows only, warrior-only rows), so a function that's special-cased to the full fixture fails even though the primary/ variant pair alone wouldn't catch it. No AST/dataflow analysis added. - load_battle_arrays' gold array values are now verified against the fixture, not just shape/type. - README reframes win_rate_by_class as "observed in this history," not inherent class balance, and adds a short note on why two analytical questions were replaced instead of patched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Targeted hotfix for the three problems found by the final quality gate on
final_analytics_epilogue:hero_win_rate=1.0(couldn't distinguish a hardcoded value).most_one_sided_matchupwas informationally empty (every one of the 9 matchups ties atabs(rate-0.5)==0.5in this frozen combat model — confirmed mathematically and empirically), andmost_balanced_classwas a 3-way tie in the recorded primary history, broken only by variant sampling weights, not any real property of a hero class.hardest_monster,rank_monsters, andunfavorable_matchupsare mathematically invariant to any run-count reweighting in this frozen domain — the primary/variant fixture pair alone can't catch a hardcoded answer for them.Per the instructions, this is not solved by expanding AST/dataflow analysis — it's solved with behavioral probes on genuinely different real inputs.
Changes
Commit 1 — fixture signal:
scripts/generate_fixtures.py: deep-dive variant changed fromrogue_vs_Goblin(alsohero_win_rate=1.0) towarrior_vs_Dragon(hero_win_rate=0.0— a genuinely opposite outcome, not just a different hero-wins matchup). Regenerated via the same realBattleServiceorchestration; verified byte-for-byte deterministic across two clean regenerations.Commit 2 — behaviorally robust conclusions:
task.py:balance_conclusions()dropsmost_balanced_classandmost_one_sided_matchup; addslongest_matchup(matchup with highest avgavg_rounds— a real, continuously-varying signal) andone_sided_matchup_count(count of matchups with exactly 0.0 or 1.0 win rate — turns the deterministic-win-rate finding into the conclusion itself instead of hiding it behind a forced ranking). Docstrings make clear these functions must work generically on whateverdfsubset they're given.check.py:load_battle_arrays'sgoldarray values are now verified against the fixture (previously only shape/type were checked — a wrong gold array silently passed).balance_conclusions,rank_monsters, andunfavorable_matchupsare additionally run on controlled slices of the real primarytournament_history.csv(e.g. Orc+Dragon-only rows, warrior-only rows, mage-minus-Orc rows) — every row is genuine, just a subset. A function hardcoded to the full 9-matchup fixture fails these even though it passes the primary/variant pair.groupby) are unchanged — only regression-tested.README.md: reframeswin_rate_by_classas "observed in this recorded history," not inherent class balance; questions section replaces the two removed conclusions and adds a short note on why ("when a planned metric turns out not to distinguish any of your cases, change the question"), without revealing the actual answers.Test plan
np.asarray/np.quantile/np.count_nonzero/np.stack/.query/named.agg/sort_values().index[0]instead ofidxmin/idxmax) → PASShero_win_rate=1.0hardcoded) → FAIL on the new losing variantgoldarray wrong) → FAIL on the new loader value checkhardest_monsterhardcoded) → PASS primary/variant, FAIL on Orc-only subsetrank_monstershardcoded + ceremonial groupby) → FAIL on Orc+Dragon subsetunfavorable_matchupshardcoded + ceremonial groupby) → FAIL on warrior-slice subsetlongest_matchuphardcoded) → FAIL on no-warrior_vs_Dragon subsetone_sided_matchup_count=9hardcoded) → FAIL on warrior-only subsetbuild_final_report) → FAIL on varianttools/author_check.py→ 750 checks passed, 0 errorstools/course_status.py→ epilogue still tracked correctlygit diff --statagainst merge base empty).progress/cache in the working tree🤖 Generated with Claude Code