fix(gate-50): the guard window crossed method boundaries (#429) - #437
Merged
Conversation
The window was eleven lines of CODE following the read, counted over the FILE, with no notion of where the method ends. A guard belonging to a DIFFERENT method therefore cleared an unguarded read in the method above it. Reproduced on c26f9a3, one file, one variable: readToken() unguarded + an ordinary guarded readRegister() below it PASS <- the defect delete readRegister() and nothing else FAIL — 1, api_token at :6 #420 changed the window's BUDGET (comments no longer spend it) but not its EXTENT, so that fix moved this defect closer rather than away: blanking comments means real code lines from the next method arrive sooner. The window is now clipped at the enclosing method's closing brace, via the same brace walk gate-49 runs over masked text — kept local to this checker so gate-50 gains no new import and no new wiring failure mode. _strip_php_comments grows a for_structure mode that also blanks string contents and heredoc bodies; that copy is only ever brace-walked, never searched for a guard, so no guard vocabulary changes meaning. FAILS LOOSE. A walk that never balances yields a span to EOF, i.e. exactly the pre-#429 window — a false negative at worst, never a finding at a line where no guard could be written. Findings now name the method the window was clipped to, because without it the reader cannot tell "unguarded" from "the guard is out of range". FAMILY J in test_gate_45_to_55_acceptance.sh. J1 is the evidence arm (PASS on origin/main, FAIL here). J2-J5 hold their verdict in BOTH arms and are labelled CONTROLs: J2 the issue's positive control, J3 a guard genuinely in the same method inside budget (anti-false-positive), J4 the same method one code line OUTSIDE budget (proves the clip did not shrink the budget), J5 a } inside a string literal and a heredoc body (load-bearing: walking the un-blanked text computes readToken() as ending on its second line).
Both branches appended a new test family to the tail of test_gate_45_to_55_acceptance.sh, so git could not tell they were independent additions rather than competing edits. Resolved by keeping BOTH: FAMILY I (gate-48, the plain-git-pathspec blind spot, #428) as it landed on main, then FAMILY J (gate-50, the guard window crossing a method boundary, #429). Neither family references the other and both assert on their own fixtures. Verified after the resolve: no conflict markers remain, both family headers are present exactly once, bash -n is clean and shellcheck exits 0.
rubenvdlinde
pushed a commit
that referenced
this pull request
Aug 13, 2026
…move its heredoc handling into php_mask #429 (#437) reworked gate-50's window inside the runner while this branch was converting the same block off its private _strip_php_comments. Resolved so BOTH survive: * #429's _method_spans stays, and the runner still clips the guard window at the enclosing method's brace. * _strip_php_comments is gone. The structural copy it fed is now php_mask(src, blank_strings=True); the guard-search copy is php_mask(src). #429 had added HEREDOC handling to the private copy, which php_mask lacked. Deleting the copy without moving that first would have been a capability loss dressed as a cleanup, so php_mask learned heredocs and nowdocs — a strict correctness gain for gates 5, 8, 50, 59 and 64, which until now parsed a <<<SQL body as code (a // inside it blanked a line, an apostrophe opened a literal that ran to the next stray quote, a { mis-balanced every brace walk). Nine arms, four of them mutation-checked EVIDENCE. Also fixes three QUADRATIC scans this branch introduced or inherited, found by timing at three input sizes rather than one: _skip_tag '<a b="' x N -> 12.6 / 134 / 1190 ms (x94) html_comment_spans '{{ ' x N has_prelude/loadApp 'registerAutoloading( ' x N -> 41 / 388 / 3038 ms A tag cannot contain an unquoted '<', so the scan now aborts there; one missed '}}' settles the question for the whole remainder; and the paren walk is a single-pass table instead of a rescan per call site. All paths now measure x5-x17 for a 9x input.
This was referenced Aug 13, 2026
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.
Closes #429.
The defect
The guard window is eleven lines of code following the read, counted over the file, with no notion of where the method ends. A guard belonging to a different method therefore cleared an unguarded read in the method above it.
#420 changed the window's budget (comment lines no longer spend it) but not its extent, so that fix moved this defect closer rather than away: blanking comments means real code lines from the next method arrive inside the window sooner.
Reproduced on c26f9a3 — one file, one variable
Delete
readRegister()and nothing else:The fix
Clip the window at the enclosing method's closing brace, using the brace walk gate-49's checker already runs over masked text — kept local to this checker so gate-50 gains no new import and no new wiring failure mode.
_strip_php_commentsgrows afor_structuremode that additionally blanks string contents and heredoc bodies. That copy is only ever brace-walked, never searched for a guard, so no guard vocabulary changes meaning.Fails in the loose direction. A walk that never balances yields a span to EOF, i.e. exactly the pre-#429 window — a false negative at worst, never a finding at a line where no guard could be written.
Findings now name the method the window was clipped to; without it the reader cannot tell "unguarded" from "the guard is just out of range", which is the question the clip decides.
Acceptance — FAMILY J in
test_gate_45_to_55_acceptance.shAgainst this branch: whole suite green. Against
origin/main's runner (a real revert viaHYDRA_GATES_RUNNER_UNDER_TEST=, notgit checkout --): 2 failures, both belonging to the evidence arm.readToken())}inside a string literal and inside a heredoc bodyJ5 is load-bearing rather than decorative: measured directly on the helper, walking the un-blanked text computes
readToken()as ending on its second line — spans(4,6)un-blanked vs(4,15)blanked — which would clip the real guard out and invent a finding.The budget boundary was characterised separately in both arms — guard 9 filler lines down →
PASS, 10 down →FAIL, identical before and after. The clip changes the window's extent and not its budget.Fleet numbers — 6 repos, whole-tree scope
gate-50 is not a delta gate, so a whole-tree run exercises it. Six fresh
developmentclones.PASS0PASS0FAIL4FAIL4PASS0PASS0PASS0 (47 demoted in.notes)PASS0 (47)PASS0 (1 demoted)PASS0 (1)PASS0PASS0Zero new findings, zero removed. Six identical verdicts is what a dead rig looks like, so the zero was measured rather than assumed, two ways:
1. Exposure, at the checker layer — running this branch's own
_method_spans/_strip_php_comments(extracted from the runner, so it cannot drift) over everylib/**/*{Controller,Service}.phpin the six trees:23 of 130 reads were structurally in the #429 shape — their window did reach past the enclosing method's brace. None flipped, because in every one of the 23 the read either had a guard inside its own method or had none in either window. That harness reports
verdict-flips=1when handed the issue's fixture, so its zero is a measured zero.2. Positive control at the verdict layer, on the real trees — plant the #429 shape into each repo and re-run both arms through the shipped runner: base
PASS/FAIL nunchanged, this branchFAIL n+1namingreadToken(). (Table in the thread.)Positive-control table (verdict layer, real trees)
PASSFAIL — 1readToken()FAIL — 4FAIL — 5readToken()PASSFAIL — 1readToken()PASSFAIL — 1readToken()PASSFAIL — 1readToken()PASSFAIL — 1readToken()Exactly +1 each, and opencatalogi's four pre-existing findings are preserved.
_enum_tracked, i.e.git ls-files, and the planted file was untracked. A control that produces no hits in either arm is indistinguishable from a fix that does nothing; the numbers above are from the corrected run, with the probe staged.