Skip to content

guard(size): give .sh the size ratchet .py has had since #5060 (#17353) - #17378

Merged
mrveiss merged 2 commits into
mainfrom
issue-17353-shell-size-gate
Sep 24, 2026
Merged

mrveiss merged 2 commits into
mainfrom
issue-17353-shell-size-gate

Conversation

@mrveiss

@mrveiss mrveiss commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Thinking Path

Shell had no size gate of any kind — no checker, no baseline, no ratchet
test. Python has had one since #5060 and a ratchet since #14236, so a .py
file cannot grow past a recorded ceiling.

The asymmetry is not theoretical. scripts/lib/hardcoded-value-rules.sh — the
rule set both the repo-wide scan and the pre-commit hook read — went 787 →
842 lines in a single PR with nothing to say about it. The gate that can say no
shapes the change; the one that does not exist does not.

Measured before building, because the size of the grandfathering problem
determines the design:

Python gate This gate
tracked files ~3000+ 216
over the 600-line ceiling 509 10

Ten entries, not 509 — which is why the ceilings sit inline rather than in a
sibling data module. python_file_size_known_large.py exists only because 512
entries would have pushed that hook over its own limit.

The decision in this change: scope

check_python_file_size.py excludes autobot-infrastructure/. Inheriting
that exclusion would drop 6 of the 10 oversized shell files
, including four of
the five the issue opens with. The gate would ship, pass, and gate almost
nothing — the vacuous-gate failure MEASUREMENT_DISCIPLINE.md names.

That exclusion is defensible for Python (little Python lives there). It is
indefensible for shell: infrastructure is where shell lives, and deployment
scripts are the code with the least type checking and the most operational
consequence. So this gate excludes .worktrees/ only.

The divergence is pinned by a test (test_the_infrastructure_tree_is_in_scope),
so a future "align the two gates" change fails and reads why rather than
silently halving the gate's reach.

What Changed

File Role
scripts/check_shell_file_size.py the gate; staged-file path + --audit-ceilings tree walk
repo_tests/shell_file_size_ratchet_baseline.py the second copy of the ceilings
repo_tests/shell_file_size_ratchet_test.py the ratchet's invariants
.pre-commit-config.yaml hook registration
.github/workflows/code-quality.yml repo-wide audit, beside the Python one

Semantics mirror the Python gate, including the branch that looks wrong: a
file under its ceiling fails until the ceiling is lowered, because an
unlowered ceiling re-licenses the lines just cut. Two copies of the ceilings are
held equal by a test (#14498) — the gap between a lowered hook and an unlowered
mirror is exactly those lines, and it is spendable.

An unreadable file is a violation, not a skip: None from count_lines
means never measured, which is a different claim from within the limit, and
only the second is what exit 0 is entitled to report.

Deliberately not done: the shared engine

~90% of the logic is common with the Python gate, and consolidating is the
repo's stated goal. I did not do it here, and the reason is a trap worth
recording — repo_tests/python_file_size_ratchet_test.py patches module
attributes
:

monkeypatch.setattr(hook, "KNOWN_LARGE", {})
monkeypatch.setattr(hook, "repo_root", lambda: tmp_path)
monkeypatch.setattr(hook, "tracked_python_files", lambda root: [unlisted])

An engine taking a config object resolves those names through itself, so every
patch would stop taking effect — and the failure mode is the dangerous one:
tests keep passing while asserting nothing (a patched KNOWN_LARGE = {} that is
ignored means the test silently exercises the real ceilings). Consolidating
means rewriting that 590-line test's injection strategy and proving the
rewritten tests still fail when they should.

Filed as #17377 with that hazard written down, rather than done badly here.

Verification

Pre-push hook — hook-registered runs, not ad-hoc local ones. The first attempt
failed and blocked the push, which is the evidence that the selection is
real:

FAILED repo_tests/one_repo_root_spelling_15925_test.py::test_no_guard_binds_the_repo_root_by_hand
  repo_tests/shell_file_size_ratchet_test.py:26 binds REPO_ROOT

The test derived the repo root from __file__ instead of
repo_tests._paths.repo_root() (#15925), which answers wrongly under a nested
checkout. Fixed, then re-pushed.

CI is the authority on the gate's own behaviour: it runs --audit-ceilings
repo-wide, which is the first time this walk executes anywhere.

Second commit: the gate did not run for 4 of the 10 files it pins

A code-reviewer pass found this PR doing the thing it exists to prevent.

--audit-ceilings lives in the code-quality job, gated on
needs.changes.outputs.backend. That filter carries a generic '**/*.py' and
had no generic .sh entry — only three specific shell prefixes. Measured
against the ten grandfathered paths, four fell outside every pattern:

Lines File In the trigger?
1265 install.sh no
818 scripts/pr-preflight.sh no
718 autobot-slm-backend/ansible/deploy.sh no
652 autobot-infrastructure/autobot-slm-backend/scripts/bootstrap-slm.sh no

A PR editing only one of those skipped code-quality entirely, so the audit
never ran.

The well-behaved case hid it. Lowering a ceiling touches repo_tests/**,
which is in the filter, so a correct change self-triggers the job. The
misbehaving case — shrink the file, forget to lower the ceiling — is the one
that skipped. This PR's own CI hid it for the same reason: its diff touches
repo_tests/** and .pre-commit-config.yaml, so code-quality ran here and
proved nothing about the scenario above.

Fixed by adding '**/*.sh' beside '**/*.py' in the canonical filter and in
code-quality.yml's on.push.paths copy, which
pipeline-scripts/check_workflow_path_filters.py polices for precisely this
drift. Re-measured against all ten: 0 uncovered.

Three more from the same review

Finding Why it mattered
the exclude-mirror test asserted containment, not equality widening the yaml to ^(\.worktrees/|autobot-infrastructure/) still passed, because .worktrees/ is a substring of it — the exact "align the two gates" mistake the neighbouring test exists to refuse, and it would have silently stopped pre-commit gating infrastructure shell files
the tree walk continued past an unreadable file contradicted this module's own docstring that an unmeasured file is a violation, not a skip. It now reports one, and still does not count toward reached, so it cannot prop up the floor check without being ruled on
no boundary test at exactly MAX_LINES a gate that trips at the limit and one that trips past it read identically in review; 600 passes and 601 fails is now pinned

Acceptance criteria

#17353 asks three things, conditional on a decision. The owner decided shell
is gated
, so all three apply:

Ask Delivered Evidence
1. a .sh size checker with the same ceiling semantics yes scripts/check_shell_file_size.py, MAX_LINES = 600, same three-way verdict()
2. a measured baseline grandfathering what exists, not a flag day yes KNOWN_LARGE, 10 entries at their measured sizes; no file is asked to shrink
3. the ratchet test that freezes rather than licenses yes repo_tests/shell_file_size_ratchet_test.py, incl. the under-ceiling branch

The issue's alternative — "if shell should not be gated, say so where a reader
will find it"
— does not apply, since the decision was to gate.

Closes #17353

Model Used

Claude Opus 5 (1M context).

Shell had no size gate of any kind -- no checker, no baseline, no ratchet test.
Python has had one since #5060 and a ratchet since #14236, so a .py file cannot
grow past a recorded ceiling. Meanwhile scripts/lib/hardcoded-value-rules.sh --
the rule set BOTH the repo-wide scan and the pre-commit hook read -- went
787 to 842 lines in one PR with nothing to say about it. The gate that can say
no shapes the change; the one that does not exist does not.

Measured first: 216 tracked .sh files, 10 over the 600-line ceiling. Not the
509 the Python gate had to grandfather, which is why the ceilings sit inline
rather than in a sibling data module.

SCOPE IS A DELIBERATE DIVERGENCE, and it is the decision in this change.
check_python_file_size.py excludes autobot-infrastructure/. Inheriting that
here would drop 6 of the 10 oversized files -- including four of the five that
motivated the issue -- and the gate would ship green having checked almost
nothing, which is the vacuous-gate failure MEASUREMENT_DISCIPLINE.md names.
Python can afford that exclusion because little Python lives there. Shell
cannot: infrastructure is WHERE SHELL LIVES, and deployment scripts are the
code with the least type checking and the most operational consequence. So
this gate excludes .worktrees/ only, and a test pins that divergence so a later
"align the two gates" change fails and reads why.

Same three-way ratchet as the Python gate, including the branch that looks
wrong: a file UNDER its ceiling fails until the ceiling is lowered, because an
unlowered ceiling re-licenses the lines just cut. Ceilings are kept in two
copies that a test holds equal (#14498) -- the gap between a lowered hook and
an unlowered mirror is exactly those lines, and is spendable.

An unreadable file is a violation, not a skip: None from count_lines means
"never measured", which is a different claim from "within the limit", and only
the second is what exit 0 may report.

NOT extracted into a shared engine with the Python gate, deliberately, though
~90% of the logic is common. repo_tests/python_file_size_ratchet_test.py
monkeypatches hook.KNOWN_LARGE, hook.repo_root and hook.tracked_python_files as
MODULE ATTRIBUTES; an engine taking a config object would stop honouring those
patches and the tests would keep passing while asserting nothing. Consolidating
means rewriting that 590-line test's patching strategy, which is a bigger and
riskier change than this issue asks for. Filed as #17377.

Wired into code-quality.yml beside the Python audit, so an entry whose file
shrank, moved or was deleted is caught repo-wide rather than waiting for
somebody to stage it.
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 3 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: mrveiss/AutoBot-AI/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 51861b93-8f9a-4b6f-83e0-c391607d0970

📥 Commits

Reviewing files that changed from the base of the PR and between 554a15b and 7c2535d.

📒 Files selected for processing (6)
  • .github/filters/code-quality-paths.yml
  • .github/workflows/code-quality.yml
  • .pre-commit-config.yaml
  • repo_tests/shell_file_size_ratchet_baseline.py
  • repo_tests/shell_file_size_ratchet_test.py
  • scripts/check_shell_file_size.py

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.

@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

…files it pins (#17353)

Review found the gate shipping green while not running in the case it exists
for -- the vacuous-gate failure this PR is supposed to prevent, in the PR that
adds it.

`--audit-ceilings` lives in the `code-quality` job, which is gated on
`needs.changes.outputs.backend`. That filter carries a generic `'**/*.py'` and
had NO generic `.sh` entry -- only three specific shell prefixes. Measured
against the ten grandfathered paths, four fell outside every pattern:

    install.sh                                                      (1265)
    scripts/pr-preflight.sh                                         (818)
    autobot-slm-backend/ansible/deploy.sh                           (718)
    autobot-infrastructure/autobot-slm-backend/scripts/bootstrap-slm.sh (652)

So a PR editing ONLY one of those skipped `code-quality` entirely and the audit
never ran. The well-behaved case hid it: lowering a ceiling touches
`repo_tests/**`, which IS in the filter, so a correct change self-triggers the
job. The misbehaving case -- shrink the file, forget the ceiling -- is the one
that skipped. This PR's own run hid it too, for the same reason.

Fixed by adding `'**/*.sh'` beside `'**/*.py'`, in the canonical filter and in
code-quality.yml's `on.push.paths` copy, which `check_workflow_path_filters.py`
polices for exactly this drift. Re-measured: 0 of 10 uncovered.

Two more from the same review:

  * the exclude-mirror test asserted CONTAINMENT, not equality, so widening the
    yaml to `^(\.worktrees/|autobot-infrastructure/)` would still have passed --
    '.worktrees/' is a substring of it. That widening is the "align the two
    gates" mistake the neighbouring test exists to refuse, and it would have
    silently stopped pre-commit gating infrastructure shell files. Now set
    equality, with the alternation parsed rather than string-matched.

  * the tree walk `continue`d past an unreadable file, contradicting this
    module's own docstring that an unmeasured file is a violation and not a
    skip. It now reports one, and still does not count it toward `reached`, so
    it cannot prop up the floor check without having been ruled on.

Plus a boundary test pinning that an unlisted file at exactly MAX_LINES passes
and 601 fails: a gate that trips AT the limit and one that trips past it read
identically in review.
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.

guard(size): .sh files have no size ratchet at all, so shell libraries absorb scope with nothing pushing back

1 participant