Skip to content

fix(lint): give the file-size grandfather list a ceiling that only shrinks (#14236) - #14463

Merged
mrveiss merged 2 commits into
Dev_new_guifrom
issue-14236
Aug 17, 2026
Merged

mrveiss merged 2 commits into
Dev_new_guifrom
issue-14236

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 17, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

A grandfather list with no ceiling is a ratchet mounted backwards. Entries go
in, nothing takes them out, and the guard quietly stops guarding while
continuing to report green. The defect is the missing ceiling, not the entry
count — so I measured before deciding how hard to push.

Present state. MAX_LINES = 600. Three entries, measured on Dev_new_gui:

file annotated in code today over the limit by
autobot-backend/orchestrator.py # 779 lines (#5060 — target <800) 1114 +514 (1.9x)
autobot-backend/chat_workflow/manager.py no number ever recorded 4068 +3468 (6.8x)
autobot-backend/chat_workflow/tool_handler.py no number ever recorded 4063 +3463 (6.8x)

Has the list grown? No. git log --follow over the hook shows the same
three names in all five commits since it landed on 2026-05-23; the only change
was #14235 tightening endswith to ==. So the list never gained an entry —
the files inside it grew instead, which is the failure mode a bare-name
exemption cannot see. orchestrator.py is 43% past the size it was exempted
at and past its own stated target of <800. The two with no recorded number had
nothing to regress against at all, and grew 6.8x under a passing hook. The
issue quoted 3977 and 4058 yesterday; they are 4068 and 4063 today, so the
drift is live, not historic.

How aggressive. Growth is unbounded and two of three entries were never
measured, so the ceiling is enforced in all three directions rather than only
the obvious one, and the audit is wired into a blocking CI step rather than
left to the staged-files hook.

Do I also shrink the list now? No — explicitly. All three entries stay, at
their current sizes. Splitting a 4000-line module is #5060's campaign, not a
lint fix, and this PR does not reduce any file by a line. The problem this PR
solves is that the exemption had no bound; the files are still 1.9x–6.8x over
the limit after it merges.

What Changed

scripts/check_python_file_size.py

  • KNOWN_LARGE is now dict[str, int] — path to the line count it was last
    measured at, per the issue's proposed shape.
  • The verdict for a grandfathered file is extracted into a pure function so a
    mutation has something to break:
    • line_count > ceiling -> fail. A grandfathered file may not grow; the
      exemption freezes the size it was granted for.
    • line_count < ceiling -> fail, naming the exact number to write back. An
      unlowered ceiling re-licenses the lines just removed, which is how a
      ratchet stops turning.
    • line_count <= MAX_LINES -> fail, asking for the entry to be deleted.
      A list still naming a compliant file exempts nothing while looking
      authoritative.
  • New --audit-ceilings mode applies those rules to every entry regardless of
    what is staged, so a stale entry cannot sit there after its file shrank,
    moved, or was deleted. It reports how many entries it reached and fails when
    that is not the expected count.
  • repo_root() derives from __file__, not the cwd.
  • Findings go through the logger, not stdout (Bug: Pre-commit hooks not blocking print()/console.* violations #1082). stdlib logging
    rather than autobot_shared.logging_manager: this is a pre-commit hook run
    as a bare script on every commit, and the platform logger would pull config
    loading into that path — the trade taken in
    autobot_shared/user_management/password_epoch.py. Findings log at ERROR
    so they clear logging.lastResort's WARNING threshold and stay visible even
    when nothing configured logging; configure_logging() attaches a stderr
    handler at INFO so the clean-run line is not discarded either.
    The pre-existing stdout call in main() moved too, rather than being left
    under the changed-lines carve-out. Said here rather than done silently: it is
    the one line in this PR the failing check did not require, and the reason is
    that keeping it would leave a 176-line file with two output paths for the same
    findings.

.github/workflows/code-quality.yml — new blocking step running
--audit-ceilings, beside the existing extension-import baseline audit. The
pre-commit hook only sees staged files; this is the sink that makes the
repo-wide direction real.

repo_tests/python_file_size_ratchet_test.py — 20 tests, new file.

Verification

The ceiling asserts both directions.

direction test asserts
list may not gain entries test_no_entry_may_be_added set(KNOWN_LARGE) is a subset of a frozen baseline literal
ceilings may not be raised test_no_ceiling_may_be_raised each ceiling <= its frozen baseline value
a file may not grow test_growing_past_the_ceiling_fails verdict(rel, ceiling + 1) is a violation naming the ceiling
a compliant entry must be removed test_an_entry_whose_file_is_now_compliant_fails verdict(rel, 600) demands the entry be deleted
a shrunk file must lower its ceiling test_a_shrunk_file_must_lower_its_ceiling verdict(rel, ceiling - 1) names the new number
a dead entry is a hard error test_the_audit_reports_a_vanished_entry a moved/deleted file is reported, not skipped
the audit reports, not merely reaches test_the_audit_surfaces_a_ceiling_violation a staged breach produces one problem per entry
the exemption still exempts test_a_file_at_its_ceiling_passes a file exactly at its ceiling passes

The frozen baseline is a deliberate second copy of the mapping: a ratchet needs
a fixed reference, and a list only ever compared against itself can drift
anywhere. Lowering a ceiling needs no edit to it; raising one or adding a fourth
file must fail, and the comment says so.

Reach self-check. test_matcher_reaches_every_entry_over_a_tracked_enumeration
drives the hook's own matcher across 4958 paths enumerated by git ls-files,
not by anything in the hook, and asserts the matcher classifies exactly the 3
entries as grandfathered. test_enumeration_reaches_the_repo guards the
enumeration itself with a floor of 3000, so an empty listing cannot agree with
anything. The counter deliberately does not share the matcher's blind spot:
counting the dict's own keys would fail exactly when the matcher does — a
key-form rewrite silences both, and both then agree that all is well. Driving the
matcher from an outside enumeration cannot agree that way, and M6 below is the
demonstration. The audit's own counter is a third mechanism again: it counts
files it actually read off disk, so run_audit fails on "reached 0 of 3"
(test_run_audit_fails_when_the_scan_reached_nothing).

The findings still reach the developer. A silent conversion to logging would
be worse than the stdout call it replaced — a guard whose output vanishes passes
while reporting nothing. Five tests run the checker against a violating fixture
and capture the emitted record: test_a_violation_is_reported_to_the_developer
(the finding text and line count are present),
test_findings_are_emitted_above_the_lastresort_threshold (level >= WARNING,
so a bare invocation that never configured logging still shows them),
test_audit_problems_are_reported_to_the_developer,
test_configure_logging_makes_the_clean_run_visible (a handler exists and INFO
is enabled, or the clean-run line would be discarded) and
test_the_hook_has_no_stdout_calls_left.

Observed from a bare run of a standalone copy:

$ python3 <copy> big_fixture.py            # 605 lines
big_fixture.py: 605 lines (max 600)         -> stderr, exit 1

$ python3 <copy> --audit-ceilings           # all three at ceiling
python-file-size ceilings: 3 entries, all live and at size.   -> exit 0

$ python3 <copy> --audit-ceilings           # orchestrator.py grown to 1120
autobot-backend/orchestrator.py: 1120 lines, over its recorded ceiling of
1114. A grandfathered file may not grow (#14236) ...          -> exit 1

Fixture hygiene. test_the_hook_has_no_stdout_calls_left walks the AST for a
call to the banned name, assembled by implicit string concatenation, so this
fixture cannot trip the very lint it enforces — no exemption was added, and it
self-guards on the assembled needle. The prose comment above it describes the
banned call without quoting it. Fixtures needing an over-limit file build one in
tmp_path rather than shipping one.

Mutation table — standalone harness (assert mutated != original before each
run, baseline green first). Mutations applied to a copy in scratch; nothing
was pushed to the branch.

BASELINE GREEN — 20 passed
# mutation test that went red result
M1 ceiling check removed test_growing_past_the_ceiling_fails (+2) CAUGHT
M2 compliant-entry check removed test_an_entry_whose_file_is_now_compliant_fails CAUGHT
M3 shrink/lower-ceiling check removed test_a_shrunk_file_must_lower_its_ceiling CAUGHT
M4 a ceiling raised 1114 -> 9999 test_no_ceiling_may_be_raised, test_recorded_ceilings_match_the_files_today CAUGHT
M5 a fourth entry added test_no_entry_may_be_added CAUGHT
M6 matcher key form changed (stops matching) test_matcher_reaches_every_entry_over_a_tracked_enumeration (+6) CAUGHT
M7 audit counts entries, not files read test_the_audit_reports_a_vanished_entry (+2) CAUGHT
M8 vanished entry silently skipped test_the_audit_reports_a_vanished_entry CAUGHT
M9 audit swallows verdicts test_the_audit_surfaces_a_ceiling_violation CAUGHT
M10 failing audit exits 0 test_run_audit_fails_when_the_scan_reached_nothing (+3) CAUGHT
M11 reach check in run_audit removed test_run_audit_fails_when_the_scan_reached_nothing CAUGHT
M12 violations logged at debug (invisible) test_findings_are_emitted_above_the_lastresort_threshold (+1) CAUGHT
M13 violations no longer emitted at all test_a_violation_is_reported_to_the_developer (+1) CAUGHT
M14 audit problems logged at debug test_audit_problems_are_reported_to_the_developer CAUGHT
M15 configure_logging attaches nothing test_configure_logging_makes_the_clean_run_visible CAUGHT
M16 stdout call reintroduced test_the_hook_has_no_stdout_calls_left CAUGHT

16/16 mutations caught. M9 and M11 exist because the first pass scored 8/9:
test_recorded_ceilings_match_the_files_today passes on today's tree whether or
not the audit reports anything, since there is nothing to report — a green test
proving nothing about the line it names. Two tests were added to stage a real
breach and a reach-zero scan. M12–M16 arrived with the logging conversion, for
the same reason: the conversion had to be provably not silent.

Syntax: python3 -m py_compile on both Python files, yaml.safe_load on the
workflow. Per repo policy nothing was installed and no application code was run;
the harness is a standalone scratch copy.

Model Used

Claude Opus 5 (1M context)

Discovered, filed not fixed

#14462 — the same guard is unenforced for everything outside the list.
500 tracked .py files exceed 600 lines; 3 are grandfathered, so 497 are over
the limit with no exemption and no enforcement
: the hook is staged-files-only,
and the one repo-wide run (enforce-precommit.yml) routes findings into a
::warning:: and exits 0. Largest is 31019 lines. Out of scope here — this PR
was asked to bound the three named files, and extending ceilings to 497 files is
a different change with a different risk profile. Cross-linked both ways.

Closes #14236

…rinks (#14236)

KNOWN_LARGE exempted three files by name with a comment promising they were
"under active decomposition". Nothing checked the promise: orchestrator.py grew
from the 779 lines noted beside it to 1114, and manager.py and tool_handler.py,
which never had a recorded size at all, reached 4068 and 4063 — 6.8x the
600-line limit — with the hook reporting all three clean.

Each entry now carries the line count it was last measured at, and the ratchet
turns one way only: over the ceiling fails (a grandfathered file may not grow),
under it fails asking for the ceiling to be lowered, and reaching the limit
fails asking for the entry to be deleted — an entry naming a compliant file
exempts nothing while looking authoritative.

--audit-ceilings applies the three rules to every entry regardless of what is
staged, and reports how many entries it reached, so a matcher that has stopped
matching cannot pass by scanning nothing. Wired into code-quality.yml beside
the extension-import baseline audit.

Decomposing the three files stays out of scope (#5060's campaign); this only
stops them growing while it happens.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

…4236)

The two prints added by the previous commit tripped the #1082 guard on lines
this PR touched. Both now go through stdlib logging.

stdlib `logging`, not autobot_shared.logging_manager: this is a pre-commit hook
running as a bare script on every commit, and the platform logger would drag
config loading into that path — the same trade taken in
autobot_shared/user_management/password_epoch.py.

Line 170's pre-existing print in main() moved too, deliberately rather than
silently: leaving it would have left one 176-line file with two output paths
for the same findings, and it is the last print in the file.

A silent conversion to logging is worse than the print it replaces, so the
output path is asserted, not assumed: findings are logged at ERROR so they
clear logging's lastResort threshold and stay visible when nothing configured
logging at all; configure_logging() attaches a stderr handler at INFO so the
clean-run line is not discarded; and five tests run the checker against a
violating fixture and capture the emitted record. Mutating the level to debug,
dropping the emit, or neutering configure_logging all go red.

The no-stdout test parses the AST for a call to the banned name assembled by
implicit concatenation, so the fixture cannot trip the lint it enforces.
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