fix: B1/B5 read the month's norm from the day sum, not the worked-day column - #139
Merged
Merged
Conversation
… column The Monday deep-suites run has been red since 2026-09-07: audit_test.py at 3000 seeds reported 21 B1/B5 findings on rows generate_wide.py injected no minimum-wage scenario into. Eleven of them (ten B1, one B5) were real false positives. B1/B5 only compare a row against the full МРЗ when it worked full time for the whole month, and with no public-holiday calendar "the whole month" was stood in for by the highest "отработени дни" on the sheet. That is only the norm if somebody that month had no leave and no sick day at all, and on eleven seeds nobody did - the sheet's own maximum was itself short of the norm, and a row sitting exactly on it got compared against the undiminished floor with its own pay legitimately prorated below it (seed 339, July: 21 worked days + 2 sick of a 23-day norm). The norm is now the largest отработени + отпуск + болничен + майчинство any row declares, which is right as soon as ONE row was employed all month rather than needing one with no absence at all. A fractional value adds nothing to that sum - it is an amount typed into a day column, which is what K2 reports it as, and counting seed 339's 161.02 sick "days" would read that month as a 181-day one and gate the whole sheet off. The two places that rule is applied now share DAY_TOL. A sum is never below its own first term, so the new estimate is never below the old one: it can only silence a comparison, never open a new one. The other ten were not false positives. m_insurable_unexplained restates осигурителен доход at 86-95% of the correct figure, and on a row already near the floor the restated figure lands below МРЗ - a real second violation of the same understatement, and the same pairing the hand-built s_b5_insurable_below_min_wage shape already expects in the other direction. run_wide() now excuses exactly that and nothing else: B5, on a row carrying the injected F1_insurable_unexplained, with the F1 finding actually reported there. Both halves of the fix are pinned by a new shape in audit_test.py part 3, each proved by sabotage - reverting the norm to the worked-day column turns s_b1_norm_only_visible_in_the_day_sum red and brings seed 339 back; dropping the fractional exclusion turns s_k2_amount_does_not_inflate_the_norm red by gating a real B5 off the sheet. The МРЗ itself is written down in neither file: rates.py reads it from the reference at call time and rates_test.py is the one place that pins it. Verified: audit_test.py --seeds 3000 (the failing CI step) green, plus run_tests.py --seeds 300, k_checker_test.py --seeds 300, komplekt, lifecycle, rates, skill, checks and preflight. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DpdxJWRY9P3YGknzhEzBw8
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.
What was red
The Monday
deep-suitesrun has been failing since 2026-09-07 — three scheduled runs, most recently 35595675501.audit_test.py --seeds 3000reported 21 B1/B5 findings on rowsgenerate_wide.pyinjected no minimum-wage scenario into. The 300-seed push job never sees it, which is why it sat.Two different causes, and only one of them was a bug.
Eleven real false positives (ten B1, one B5)
B1/B5 only compare a row against the full МРЗ when it worked full time for the whole month — чл. 1, ал. 2 НСОРЗ prorates the floor for anything less. With no public-holiday calendar, "the whole month" was stood in for by the highest
отработени дниon the sheet. That is only the month's norm if somebody that month had no leave and no sick day at all, and on eleven of 3000 seeds nobody did: the sheet's own maximum was itself short of the norm, and a row sitting exactly on it was compared against the undiminished floor with its own pay legitimately prorated below it.The norm is now the largest отработени + отпуск + болничен + майчинство any row declares. Someone employed the whole month sums to the norm whatever mix of the four they had, so this is right as soon as one row was employed all month — a far weaker condition than one row with no absence at all. A sum is never below its own first term, so the new estimate is never below the old one: it can only silence a comparison, never open a new one.
A fractional value contributes nothing to that sum. It is an amount typed into a day column, which is exactly what K2 reports it as — counting seed 339's
161.02sick "days" would read that month as a 181-day one and gate the whole sheet off instead. The two places that apply that rule now shareDAY_TOLrather than each spelling out0.005.Ten that were not false positives
m_insurable_unexplainedrestates осигурителен доход at 86–95% of the correct figure. On a row already near the floor, the restated figure lands below МРЗ — a real second violation of the same understatement, not a false positive, and the same pairing the hand-builts_b5_insurable_below_min_wageshape already expects in the other direction (B5 and F1 together).run_wide()now excuses exactly that and nothing else: B5, on a row carrying the injectedF1_insurable_unexplained, with the F1 finding actually reported at that row. B1 anywhere, or B5 anywhere else, still fails like a miss.Proof
Both halves are pinned by a new shape in
audit_test.pypart 3, each proved by sabotage:s_b1_norm_only_visible_in_the_day_sumred, and seed 339 comes backs_k2_amount_does_not_inflate_the_normred — a real B5 gated off the sheetThe МРЗ itself is written down in neither file, including in prose:
scripts/rates.pyreads it from the reference at call time andtest/rates_test.pyis the one place that pins it.CLAUDE.md's note on this check's false-positive history gets the third round, including that only the Monday run can see this class of bug. One stale claim in the test fixture is corrected while there: the base rows were said to model "nobody has the true norm", which they never did — with no leave or sick day declared anywhere there is no evidence of a longer norm, and that row's основна is above МРЗ regardless, so nothing was being pinned. The new shape pins it for real.Verified
python test/audit_test.py --seeds 3000— the failing step, greenpython test/run_tests.py --seeds 300,k_checker_test.py --seeds 300,komplekt_test.py,lifecycle_test.py,rates_test.py,skill_test.py,checks_test.py,preflight_test.py— all greenNo change to
SKILL.md's guidance, so no paid eval is implied by this.🤖 Generated with Claude Code
https://claude.ai/code/session_01DpdxJWRY9P3YGknzhEzBw8