Repository navigation
ci: no-growth ratchet for file length and function complexity - #93
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #93 +/- ##
==========================================
+ Coverage 91.51% 91.62% +0.10%
==========================================
Files 216 216
Lines 27074 27079 +5
Branches 4457 4457
==========================================
+ Hits 24777 24810 +33
+ Misses 1632 1606 -26
+ Partials 665 663 -2
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
Local CI does not pass its selected base ref to the growth gate, allowing incorrect or skipped rationale checks.
1 open issue
What changed in this PR
Adds a CI ratchet to prevent unapproved growth in Python file length and function complexity.
Changes:
- Adds measurement, baseline validation, update, and reporting commands.
- Records existing over-cap code and tests enforcement.
- Documents the gate and adds CI summary reporting.
| File | Description |
|---|---|
dokimi/tests/test_growth_ratchet.py |
Adds enforcement gates and regression tests. |
dokimi/scripts/growth_baseline.json |
Records existing over-cap measurements. |
dokimi/scripts/check_growth.py |
Implements measurement, validation, and reporting. |
dokimi/CONTRIBUTING.md |
Documents limits and baseline maintenance. |
AGENTS.md |
Adds growth-gate guidance. |
.github/workflows/ci.yml |
Publishes growth metrics in step summaries. |
Review effort: Balanced
6161d27 to
6c4cc20
Compare
A first-party .py file over 800 lines (1000 under tests/) or a function over cyclomatic complexity 10 (ruff's C901 count, computed with ast so the test job needs no ruff) now fails tests/test_growth_ratchet.py unless scripts/growth_baseline.json records it. A recorded value is exact: the code may shrink but not grow, and a shrunk, gone or back-under-cap entry must be lowered or removed (--update does that, never raising). An entry raised or added relative to the base branch needs a rationale of at most 300 characters. The baseline holds the 14 files and 81 functions over the caps today. `check_growth.py --report --markdown` adds the totals to the static job's step summary, non-blocking. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…TH_BASE ci_local.py resolves the base ref to handle non-origin remotes, but did not pass it to the pytest environment. This caused the growth check to use the wrong baseline or skip the rationale check if the baseline was missing, allowing misleading local CI passes when using --remote with a non-origin remote. Pass the runner's resolved base_ref as DOKIMI_GROWTH_BASE environment variable to the subprocess steps. Add a regression test that verifies the check correctly uses a non-origin remote's baseline. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
antonwolfy
left a comment
There was a problem hiding this comment.
The ratchet is sound. The complexity count matches ruff's C901 on the whole tree and the baseline matches the code. Three things are worth fixing before merge, all about the rule that guards raising an entry.
What I ran, on a Linux review host at 6c4cc20:
make ci-local: 20 passed, 0 failed, 4 skipped (docker smoke, test-latest, docs deploy, semgrep). 5248 tests passed, 2 skipped.- Reproductions for the findings, with no network and no LLM calls.
- A mutation run on
check_growth.pyandci_local.py: 33 mutants, 12 of which pass the full growth test file. check_growth.pycomplexity againstruff --select C901on all 6421 defs in the tree: 0 differences.
Checked, no issues
- Baseline: every entry equals the current measurement and the set of over-cap files and functions equals the baseline.
- Line counts:
len(text.splitlines())equals the newline count in every file in the tree. - CI step: the new
staticstep runs fromdokimi/, uses only the standard library, and is not a gate. The test job usesfetch-depth: 0, soorigin/mainis there for the rationale check. - Repo rules: SPDX headers are present (the baseline JSON is covered by
paths-ignore). No changelog fragment is needed for a CI, scripts, tests and docs change.
I agree with @copilot-pull-request-reviewer's point that make ci-local must pass its resolved base to the growth check; the fix at ci_local.py:283 is right. Its test does not cover it, see the inline comment.
ci_local.py now passes the resolved base reference to workflow steps as DOKIMI_GROWTH_BASE, allowing check_growth.py to use it without requiring --base argument when ci_local is used to run workflows locally. Adds test to verify the env var is passed to all subprocess steps. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
… and match guards Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
3 open issues
Review effort: Lite
antonwolfy
left a comment
There was a problem hiding this comment.
Review at 18fb57e. The three test gaps from my last review are closed, and the new tests fail for the right mutants. One new gap in the top_k test is worth fixing before merge, and there is one nit.
What I ran, on a Linux review host at 18fb57e:
make ci-local: 21 passed, 0 failed, 4 skipped (docker smoke, test-latest, docs deploy, semgrep). 5363 tests passed, 2 skipped.- Mutants of the three new tests, run against a copy of the tree: replacing
1 + _complexity(case.body)with1fails the newif-in-case row (assert 3 == 4); making_is_irrefutablereturn True for anyMatchAsfails the(2 as z)row (assert 2 == 3); dropping the./from thegit showpath fails the subdirectory test only; replacing the config lookup withDEFAULT_RETRIEVAL_TOP_Kfails the config row (assert 3 == 7); dropping the default fails the default row (assert None == 3). - The
top_kflag mutant below, with the LLM and MCP transports mocked as in the test file.
Other points
- Nit: the counts in the comments are stale.
check_growth.py:55says 3 test files are over 1200 lines; the baseline has 4 (1223, 1423, 1480, 1704).check_growth.py:59says 81 of 6354 functions are over the cap; the baseline has 80, and--reportcounts 6548 functions. Also,answers.py:72assigns_product_config, which nothing reads.
Checked, no issues
- Subdirectory test: the project sits under
proj/in the git root, the base baseline is read with 22 lines, and a raise to 23 with no rationale exits 1. - Match rows: each new row is the only row that fails its mutant, and each fails on the expected count.
- CI step and docs: unchanged since 2a2bdd2.
|
Review at 18fb57e, pushed in 4c8c624:
The |
…growth comments The flag row used top_k=3, which is also the built-in default, so a command ignoring --top-k still passed (mutant: if True:). Use 5 with a config of 7. Correct the test and function counts in check_growth.py comments and drop the unused _product_config assignment in answers.py. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
ccdf6bf to
8346024
Compare
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Items from the review comment above, pushed in fb5275f:
|
… --update after rebase Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…baselined base After this lands, `--seed` on a branch that grew llm.py wrote the entry at the new size with "pre-existing when the gate was introduced", and the gate passed. The rationale check now treats that text as missing, so only the one-time seeding (base with no baseline) can use it. Signed-off-by: Alexander Rybkin <alexander.rybkin@intel.com>
|
Ran at fb5275f: the full suite, ruff, mypy and bandit; the growth gate against a base with and without a baseline; and each earlier point's original input.
Pushed:
Left for the author: update the description. Static checks fails on the license header of two files from main (#112), not from this PR. |
antonwolfy
left a comment
There was a problem hiding this comment.
Review at a38b9be. The --seed mode and the seed-text rule hold up, and the top_k thread is fixed. One test gap in the seed test is worth closing before merge, and there is one nit.
What I ran, on a Linux review host at a38b9be:
make ci-local: 27 passed, 0 failed, 4 skipped (docker smoke, test-latest, docs deploy, semgrep).- A mutation run on the new
seeded()andSEED_RATIONALEcode againsttest_growth_ratchet.py: 13 mutants, 1 of which passes the file (inline). - The
top_kmutant from my earlier thread (if True:atanswers.py:73): the flag row now fails (1 failed, 9 passed).
Checked, no issues
- Seed rule: a base entry whose rationale is the seed text, raised in the head with that text kept, is reported as raised without a rationale. So is a hand-added entry with the seed text. A raise that reuses the base's rationale is still reported.
--seedagainst a git base:test_seed_cannot_explain_growth_once_the_base_has_a_baselinegoes through the real git path;main --base mainexits 1 and flags exactly the raised and the new file.- Docs and flags:
--update,--seed,--report,--markdown,--json,--base, the 800/1000/10 caps and the 300-character limit match the docstring,AGENTS.md,CONTRIBUTING.mdand the changelog fragment.
…stale counts Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…baseline) Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
Re @xaleryb's description point: the PR body is updated to the current head (edf7e19): 15 files and 94 functions in the baseline, and it names the |
|
Re-checked at edf7e19: full suite, ruff, mypy and bandit, the original input of each earlier point, and the head merged with current main (ad1cbd0).
No commits pushed this round. Left for the author: merge main, run |
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…r raise Main grew four baselined or near-cap items and added three functions over complexity 10 before the gate landed: test_repo_environment.py 1369 -> 1371 (intel#147), manifest._check_task 21 -> 22 (intel#113), ci_local.run 28 -> 29 (intel#146), cmd_repo_validate 10 -> 11 (intel#150), cli_surface._value 17 (intel#88), research_register.check 19 and main 15 (intel#139). Each entry names its PR. --update after the seed lowers nothing. Now 15 files and 98 functions. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…ntel#115) intel#73 grew test_judge_validity.py 1423 -> 2235, test_judge_reliability.py 1505 -> 1695, model_compare.py 1296 -> 1297, cmd_eval_judge_validity 25 -> 29, and took degrade.build_pairs (17) and judge_sequential.run_sequential (12) over the cap. intel#115 grew l0 _build_names 14 -> 15 and shrank _cmake (now under the cap, entry dropped) and Scan.resolve_path (20 -> 19). Each raise names its PR. Now 15 files and 99 functions. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
|
@xaleryb thanks, done as you suggested. I merged main and ran
With about 30 open PRs, any sibling that lands first will shift these numbers again. If that happens before this merges, the merge plus |
antonwolfy
left a comment
There was a problem hiding this comment.
LGTM. Both threads from my last review are fixed, and the reseed after the merge of main holds up. No new findings.
What I ran, on a Linux review host at e893657:
make ci-local: 28 passed, 1 failed, 7 skipped. 7146 tests passed, 2 skipped. The one failure ispip-audit, which reports a gitpython 3.2.0 advisory (fix in 3.2.1). The PR touches no lock file.- 32 mutants of
check_growth.pyagainsttest_growth_ratchet.py: 28 killed. The 4 survivors are equivalent or cosmetic (theisinstanceguard inseeded(),>vs>=on the 300-character rationale limit,splitlines()vscount("\n")on a file with no trailing newline,startswith("tests/")). Thenow < old[metric]mutant from my thread now fails.
Checked, no issues
- Baseline:
check_growth.py --base HEADexits 0.ruff check --select C901at max-complexity 10 finds the same 99 functions with the same values, and the 15 files over a cap match the baseline line for line. - Rationales: every one is at most 300 characters. I measured each raised or added entry at the commit before and after the PR it names (#73, #88, #113, #115, #139, #146, #147, #150, #72, #101); the "from X to Y" claims are right.
- Docs and CI step: flags, caps and the
DOKIMI_GROWTH_BASEfallback order matchAGENTS.md,CONTRIBUTING.md, the changelog fragment and thestaticstep. answers.pysplit: messages, exit codes and the order of side effects match the PR's base.test_answers_command.py: 10 passed.
…184) #93 merged with a baseline seeded before #120, #165, #171 and #180 landed, so the ratchet test fails on main and on every open PR: llm.py, four functions grew past or over the cap, and novelty/bundle.py shrank under it. Raise those entries with a rationale and drop the bundle.py entry. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…e in intel#153 intel#184 already took main's growth since the intel#93 seed into the baseline with a generic rationale. Name the PR behind each of those entries instead, and add dokimi/repos/compare.py::_provenance_warnings, which intel#153 took over the complexity cap after intel#184 and which turns main's Tests job red again.
…e in intel#153 intel#184 already took main's growth since the intel#93 seed into the baseline with a generic rationale. Name the PR behind each of those entries instead, and add dokimi/repos/compare.py::_provenance_warnings, which intel#153 took over the complexity cap after intel#184 and which turns main's Tests job red again. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…153 (#183) #184 already took main's growth since the #93 seed into the baseline with a generic rationale. Name the PR behind each of those entries instead, and add dokimi/repos/compare.py::_provenance_warnings, which #153 took over the complexity cap after #184 and which turns main's Tests job red again. Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com> Signed-off-by: Anton Volkov <antonwolfy@gmail.com>


Summary
Files and functions can no longer grow without bound. Anything over a cap today is frozen at its current size and may only shrink. Anything new over a cap fails.
How the caps were picked from the tree:
max-complexity, so C901 could later move into ruff with the same number. When the gate was added, 81 of 6354 functions were over 10 (29 over 15); as of 622926c, 99 of 7844. The 99 matchruff check --select C901(max-complexity 10) on the tree, this PR's files included (0 differences). ruff itself is not used because the test job installs onlyrequirements.lock.txt, which does not include ruff.Fourteen entries were raised or added by the last two merges with main. Each has a rationale naming the PR that grew it before this gate existed:
tests/test_repo_environment.py1369 -> 1371 (#147),repos/manifest.py::_check_task21 -> 22 (#113),scripts/ci_local.py::run28 -> 29 (#146),commands/repo.py::cmd_repo_validate11, new (#150),scripts/cli_surface.py::_value17, new (#88),scripts/research_register.py::check19 and::main15, new (#139);tests/test_judge_validity.py1423 -> 2235,tests/test_judge_reliability.py1505 -> 1695,report/model_compare.py1296 -> 1297,commands/evaluate.py::cmd_eval_judge_validity25 -> 29,eval/degrade.py::build_pairs17, new, andeval/judge_sequential.py::run_sequential12, new (all #73);repos/l0/adapters.py::_build_names14 -> 15 (#115). #115 also shrank_cmakeunder the cap (entry dropped) andScan.resolve_path20 -> 19.Entry values are exact, not ceilings. The rule for raising one is checked against the base branch. A slack ceiling that later growth could fill without anyone noticing is a known way this kind of ratchet stops binding, so a shrunk entry has to come down.
Top 5 by complexity:
report/model_compare.py::generate_section_report53,schema_compat.py::_compare41,scripts/check_treatment_batch.py::check_file36,commands/judge_reliability.py::cmd_eval_cross_judge32,commands/evaluate.py::cmd_eval_judge_validity29 (tied withgate/novelty_gate.py::evaluate_noveltyandscripts/ci_local.py::run).Evidence
The gate on this tree is
tests/test_growth_ratchet.py::test_no_file_or_function_grew_past_its_baseline. With one line appended todokimi/llm.py:Tests that the gate binds. Each builds a small project in tmp with the caps lowered:
test_a_baselined_file_that_grows_fails,test_a_baselined_function_that_grows_failstest_a_new_file_over_the_cap_fails(prod and test caps),test_a_new_complex_function_failstest_slack_and_stale_entries_fail(shrunk but still over the cap, function gone),test_an_entry_back_under_the_cap_must_gotest_update_lowers_and_removes_but_never_raises_or_addstest_raised_or_added_entries_need_a_rationale,test_an_overlong_rationale_failstest_the_base_baseline_is_read_from_git: a real git repo in tmp. Raising an entry with no rationale makesmain()exit 1.test_complexity_counts_like_ruff_c901: if/elif, loops with else, try handlers and else, with, bool ops, comprehensions, match with a wildcard, nested defs.test_a_raised_or_added_baseline_entry_says_whyskips on this PR becausemainhas no baseline yet. It binds from the next PR on. The mechanism is covered by the git test above.Local:
make ci-localon cd6bc53 (one merge back): 0 failed, 29 passed, 7 skipped (docker smoke, test-latest, cli-surface packaging, conda-forge (no micromamba), docs deploy, pip-audit report, semgrep). The new report step passes. CI: see checks below.Merge Danger
Door: two-way. Reverting the commit removes the gate and nothing else depends on it.
Blast Radius: contributors
tests/test_growth_ratchet.pyafter rebase. The same happens to a PR that adds a function over complexity 10 or a file over its cap. The fix is to split the code, or to raise the entry with a"rationale". Sibling PRs merged before this one shift the numbers. Merge main into this PR right before it merges and runpython scripts/check_growth.py --seed(main has no baseline yet, so raises are accepted), then give each raised or added entry a rationale.--update). This is deliberate.changelog.d/20261020_091500_napetrov_growth-ratchet.md(internal refactor, no behavior change).Checklist
pytestpassesruff,mypy, and schema checks pass.github/.licenserc.yaml)🤖 Generated with Claude Code