Put the complexity floor out of the gate's reach, including via consumer config - #27
Merged
Merged
Conversation
The complexity ratchet compares the tree against a committed watermark file and fails on regression. A PASSING run silently emptied that file and exited 0, so the floor vanished and every later run compared against nothing. Root cause, read in the pinned tool's own source: handle_snapshot_watermark calls create_snapshot_file the moment it finds no violation. The tool's green path IS its destructive path. Reproduced two ways, both exit 0: measuring a narrower surface than the floor describes, and raising the threshold above every function. It is not hypothetical. Two working trees on the maintainer's machine already hold a floor of 12 entries / 13 functions committed and a literal [] on disk, one stage away from permanent. So the gate no longer lets the tool near the file. Measurement runs write-free (--snapshot-ignore switches the compare, and its rewrite, off), and the ratchet is graded here as a pure function of the committed floor and the measured census. Detection no longer depends on the tool destroying the artifact first, and there is no window where the write happened and was undone. The census also makes the run answerable about what it graded. Every verdict carries measured_functions, measured_files and the floor size, and a run that measured nothing while there was something to grade REFUSES rather than reporting clean. A floor file that still exists but went unmeasured is a finding, not a shrug: that is the narrowed-surface case, caught without reference to any threshold. Threshold authority stays with the tool, so a consumer who booted at a non-default value is graded at that value and not at ours. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…loor The first pass put the floor out of the gate's reach by measuring write-free. Two refuters then showed configuration walks around it. A consumer's own complexipy config re-opens the destructive branch. snapshot-create resolves CLI-first then TOML, the kit passes no CLI value, and --snapshot-create has no negating secondary name, so a committed snapshot-create = true wins and handle_snapshot_file_creation rewrites the floor from what the run just measured -- a branch --snapshot-ignore never touches. Neutralising it is impossible without breaking the design, because the tool derives the snapshot path AND every reported path from the same working directory, so moving the cwd re-keys the whole census. So the gate reads the consumer's config and REFUSES, naming the key, the call site that would fire, and the remedy -- and it names the keys that ARE honoured, so a real budget or a real exclusion never needs a workaround. Sixteen keys, including three the refuters did not name, and the search order mirrors the tool's own down to an empty config file shadowing a populated one. A raised threshold emptied the offender set and the ratchet graded nothing, green. The rule only ever iterated offenders, and the vacuity guard watched the census, which a raised bar leaves full. A function at 33 could reach 90 unremarked. Closed without a second threshold authority, from data already in hand: a floor function whose measured value is at or above its committed watermark, yet absent from the offender set, proves the bar moved since the floor was booted. The tool keeps sole say over what an offender is, so a consumer who booted at their own budget is still graded at it. Also closed: the instrument's exit code and stderr were discarded, so every tool-side failure was re-attributed to the repo with the reason deleted. A non-UTF-8 ledger raised past the stage and killed the whole board instead of the typed refusal it promised. A Python-free source root beside real code reported PASS having measured nothing, because both vacuity legs consulted the same possibly-wrong surface. An ignore comment voided a watermark silently, now caught per function rather than per file. A symlinked module inside the tree was accused of lying outside it. Duplicate function keys collapsed last-wins, under-reporting the measured count and able to fake a skew. The count is evidence, so it stops being a print to stderr the workflows never tee and rides the verdict and the board instead. Every violation and refusal now names a remedy. An integration fixture wrote OVER the consumer module to inject a type error, deleting its only function; the stricter vacuity leg then refused for having measured nothing and the battery went red for the wrong reason. It appends now. That also exposed the injected literal as formatter-dirty, so ruff-format had been a silent second red gate in a test that claims one. Three documents asserted the snapshot does not shrink itself and only --snapshot-create rewrites it. Both halves are false for the pinned version, and one test pinned the false sentence in place. The README's day-one boot command was worse than stale: it named the rewriting compare rather than the creating flag. Split by what each part answers -- the artifact, the instrument, the rule -- because the runner is at its line limit and the rule may not import the runner. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
CONTRACT
The complexity ratchet compares the tree against a committed
complexipy-snapshot.jsonand fails on regression. The floor is the whole value of the gate.WORLD — a PASSING run silently emptied the floor and exited 0
Root cause, read in the pinned tool's own source:
complexipy/utils/snapshot.py::handle_snapshot_watermarkcallscreate_snapshot_fileon its no-violation branch. The tool's green path is its destructive path. Reproduced two ways, both exit 0:It is not hypothetical. Two working trees on the maintainer's machine already hold a destroyed floor:
One
git addfrom permanent, on two branches.The fix, and the two ways configuration walked around the first attempt
Measurement runs write-free (
--plain --color no --snapshot-ignore) and the ratchet is graded here as a pure function of (committed floor, measured census). Detection no longer depends on the tool destroying the artifact first, and there is no window where the write happened and was undone. Verified against the real tool on a 142-file consumer: 681 stdout rows, every one a census row, 0 bytes of stderr, committed snapshot byte-unchanged.Then two refuters found configuration defeats it.
Blocker 1 — a consumer's own complexipy config re-opens the destructive branch
snapshot-createresolves CLI-first, then TOML (utils/toml.py:235-240). The kit passes no CLI value and--snapshot-createis declared with no negating secondary name (main.py:97-102), so a committedsnapshot-create = truewins andhandle_snapshot_file_creation(main.py:323) rewrites the floor — a branch--snapshot-ignorenever touches (main.py:332).Neutralising it is provably impossible without breaking the design: the tool derives the snapshot path (
main.py:321) and every reported path (main.py:308-310) from the sameos.getcwd(), so moving the cwd re-keys the entire census and drops the legitimately-honouredmax-complexity-allowed.So the gate reads the consumer's config and REFUSES. Sixteen keys, including three the refuters did not name (
ratchet,failed/details,report-ignored). The search order mirrors the tool's own (utils/toml.py:135-148), including that an emptycomplexipy.tomlshadows a populatedpyproject.toml.Blocker 2 — a raised threshold emptied the offender set and the ratchet graded nothing, green
The rule only ever iterated offenders, and the vacuity guard watched the census, which a raised bar leaves full. So with
max-complexity-allowed = 100, a function at 33 could reach 90 and the stage reported clean — the exact defect this rung exists to fix, re-entered inside the fix.Closed without a second threshold authority, from data already in hand: a floor function whose measured value is at or above its committed watermark yet absent from the offender set proves the bar moved since the floor was booted. The tool keeps sole say over what an offender is, so a consumer who booted at their own budget is still graded at it.
measurement_argvstill carries no-mx.FAILURE — the rods, driven against the REAL pinned tool
Rods 3 and 4 are the control rods on the rods: rod 2's red is attributable to the raised bar, not to a blanket "everything is red now", and a genuine improvement still passes. In all four, the floor is untouched.
Two structural rods: reverting to the pre-fix stage turns 12 tests red; removing
--snapshot-ignorefrom the argv turns 2 red by name ("the committed floor must be out of reach").A note on my own first attempt at rod 2: I set the watermark to 33 while the fixture measured 22, so the gate correctly read a shrink and passed. The rod was mis-specified, not the gate. Corrected to watermark 16 / real 22, which is the only shape in which "the bar moved" is provable.
Also closed (each with rods)
UnicodeDecodeErrorpast the stage and killed the whole 12-gate board instead of the typed refusal it promised. Closed for the snapshot, the complexipy config, and the parent-side subprocess decode.source_rootbeside real code reported PASS having measured nothing — both vacuity legs consulted the same possibly-wrong surface. The repo-wide answer is threaded in, and the third tree-pruning rule that caused it is deleted.# complexipy: ignorecomment voided a watermark silently — now caught per function, not per file.FrontDoorServer::on_messagetwice). Under-reported the count and could fake a skew. Nowmax()-aggregated with rows counted separately.normalize_pathwas undeclared; a test now pins it against the installed function, which beats a declaration file nobody gates.printto stderr the workflows never tee and rides the verdict and the board:PASS complexipy — measured 233 function(s) in 16 file(s) against a 0-function committed floor.Documents that asserted the opposite
Three files said "the snapshot does NOT auto-shrink" and "only
--snapshot-createrewrites it." Both halves are false for the pinned version, andtests/test_configs.pypinned the false sentence in place. Corrected, with the version the behaviour was re-measured on, and the test now pins the true rule and asserts the false phrasing cannot return.Worse, and fixed:
README.md's day-one boot command wascomplexipy src— no--snapshot-create, so it creates nothing, and once a floor exists it runs the rewriting compare. A live destructive instruction.An integration fixture that made a test pass for the wrong reason
test_injected_type_error_flips_the_battery_redwrote over the consumer module, deleting its only function. The stricter vacuity leg then correctly refused for having measured nothing, and the battery went red at exit 2 rather than 1. It appends now, so mypy is genuinely the sole failure. That also exposed the injected literal as formatter-dirty —ruff-formathad been a silent second red gate in a test whose comment claims one.The gate at harvest
The single red is environmental, isolated:
tests/test_packaging.pycallsvenv.create(with_pip=True)andensurepipis absent system-wide on this machine. Excluding that one file: 532 passed. Those tests touch no file in this diff and CI'ssetup-pythonprovidesensurepip— but I have not seen them pass, and that is stated rather than assumed.Split by what each part answers:
complexipy_floor.py86 (the artifact, read-only),complexipy_measure.py466 (the instrument),complexipy_ratchet.py446 (the rule).gate_runner.pyunchanged at exactly 500 — the rule takes a structural read-only Protocol so it cannot import the runner. Every file under the cap; the kit ships nofile-budget.json, so nothing to declare.The divergent candidate is preserved on
catrina/gates-r2-complexipy-floorwith its reasoning, rather than discarded. It wrapped-and-restored; this branch was preferred because it removes the failure mode instead of compensating for it.Findings for a maintainer
--no-ignorequestion, deliberately unresolved. Adding it to the gate argv requires the boot command in three documents to carry it too, or every ignored function floods as a new offender. I closed the hole structurally instead, which needs no flag on either side. Your ruling._exec'sUnicodeDecodeErrorhole is only closed for complexipy.text=Truedecodes in the parent, so ruff/mypy/pytest/cf-* keep the same battery-killing hole. Fixing it needs a line in a 500/500 file — a separate rung.layerslist, so they are unconstrained by the kit's own layering contract.pyproject.tomlwas fenced; one line each in a follow-up.excludecovers a floor file will now seeSNAPSHOT_FILE_UNMEASURED.pregate_ledger: file-budget.json is missing) — it requires a ledger the kit legitimately does not have, since the kit has no over-500 offenders to baseline. Same family as its grading markdown by the.pysize law.Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com