Skip to content

fix: B1/B5 read the month's norm from the day sum, not the worked-day column - #139

Merged
svedbg merged 1 commit into
mainfrom
fix-b1-b5-month-norm-false-positives
Sep 21, 2026
Merged

svedbg merged 1 commit into
mainfrom
fix-b1-b5-month-norm-false-positives

Conversation

@svedbg

@svedbg svedbg commented Sep 21, 2026

Copy link
Copy Markdown
Owner

What was red

The Monday deep-suites run has been failing since 2026-09-07 — three scheduled runs, most recently 35595675501. audit_test.py --seeds 3000 reported 21 B1/B5 findings on rows generate_wide.py injected 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.

seed 339, July: 21 worked days + 2 sick out of a 23-day norm, основна prorated to 584.42, reported as under the МРЗ.

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.02 sick "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 share DAY_TOL rather than each spelling out 0.005.

Ten that were not false positives

m_insurable_unexplained restates осигурителен доход 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-built s_b5_insurable_below_min_wage shape 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 injected F1_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.py part 3, each proved by sabotage:

sabotage result
norm reverted to the worked-day column s_b1_norm_only_visible_in_the_day_sum red, and seed 339 comes back
fractional exclusion dropped s_k2_amount_does_not_inflate_the_norm red — a real B5 gated off the sheet

The МРЗ itself is written down in neither file, including in prose: scripts/rates.py reads it from the reference at call time and test/rates_test.py is 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, green
  • python 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 green

No change to SKILL.md's guidance, so no paid eval is implied by this.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DpdxJWRY9P3YGknzhEzBw8

… 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
@svedbg
svedbg merged commit 66dc4ad into main Sep 21, 2026
20 checks passed
@svedbg
svedbg deleted the fix-b1-b5-month-norm-false-positives branch September 21, 2026 12:28
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