fix(gate-19,gate-26): a comment must not be enough to satisfy a gate - #376
Merged
Conversation
Both gates parsed PROSE. A sentence that merely NAMED an anchor or a component satisfied the check for that anchor or component, so the honest arm and the counterfeit arm emitted byte-identical verdicts and no downstream reader could tell them apart. Reproduced on fixtures before anything changed. gate-19: replacing a leading `@e2e` directive with the same anchor written mid-sentence in a TODO comment left the verdict at `PASS - 2 reference(s)`, unchanged. gate-26: an e2e comment saying a page still OWED a baseline produced `PASS - 1 new page(s), all have a visual proof`, identical to a real spec that navigates to the page and screenshots it. Two agents triggered this while documenting it; one warning paragraph covered the very view it warned about. The rule is that an anchor counts only where it is attached to something that EXECUTES. gate-19 accepts an `@e2e` anchor in exactly two positions: leading in a comment, after nothing but whitespace and markdown or jsdoc leaders; or inside the TITLE of a test or describe, where the anchor is part of the name of a running thing. Prose mid-comment, a selector string, a regex literal and bare code are refused by name, with a remedy that does not send the author hunting for a test to unskip. Separately, a tag that no declaration owns at all - none at or after it anywhere in the file - is no longer read as a live file-level annotation. A file-header tag is unaffected because it already binds forward to the first declaration. gate-26's search corpus now enters through source_scope.js_comment_mask for ts and js files: comments and regex literals blanked, string literals kept. Strings must stay - a goto, a toHaveScreenshot filename and a locator are all real references, and blanking them is the mistake that made gate-58's own evidence invisible. PNG baselines still contribute their filename and json or txt snapshots are untouched. The strictness is measured, not assumed. Classifying every anchor the pristine regexes match across the sixteen app checkouts with an e2e suite gives 1918 anchors, all 1918 already in leading position, and zero with no owning declaration. gate-19's full-tree count is therefore unchanged in every app measured - fifteen apps, 5823 findings, identical before and after. The movement is all in gate-26, all in the strict direction, and spot-checking confirms the new findings are real: on docudesk a comment recording that a component is registered by NOTHING was the only thing crediting it as covered. Deliberately unchanged: the conditional test.skip guard still counts, so a never-false guard stays app debt rather than becoming a gate finding. Header-binding survives; the descendant-skip half of the file-level-tag issue is a different mechanism with its own blast radius and is left to its own change, with the exposure measured at 275 anchors across nine apps. The runtime-generated-spec blindness is also left alone: crediting a manifest-driven sweep is a green-making rule I could only generalise from one app, eleven apps mention a manifest in their e2e suites, and bundling a credit into a tightening would let one half mask the other. Both directions are proven and both are fixtured. The helper suites gain eleven gate-19 assertions and four gate-26 assertions, each paired so that refusing everything cannot score as a repair, and a new repo-shaped acceptance bundle drives both gates through the real wrapper: the planted arm must FAIL and NAME its subject, the clean arm must PASS. Swapping the pristine checkers back in makes that bundle report two failures, so it is a detector rather than a fixture that agrees with whatever ships.
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.
What was wrong
Both gates parsed prose. A sentence that merely named an anchor or a component satisfied the check for that anchor or component — so the honest arm and the counterfeit arm emitted byte-identical verdicts, and no downstream reader could tell them apart. This is
.github#358, and two agents triggered it while documenting it: a warning paragraph about a view ended up covering that view.Both were reproduced on fixtures, against predictions written before anything changed.
e2e-credibility/honestfixture, and change one line: replace a leading@e2edirective with the same anchor written mid-sentence in a TODO comment. The verdict does not move. Same word, same count, same everything.The rule
An anchor counts only where it is attached to something that executes.
For gate-19 that means exactly two accepted positions: leading in a comment, after nothing but whitespace and markdown/jsdoc leaders; or inside the title of a
test(/describe(, where the anchor is part of the name of a running thing. Prose mid-comment, a selector string, a regex literal and bare code are refused by name — with a remedy that does not send the author hunting for a test to unskip. Separately, a tag that no declaration owns at all is no longer read as a live "file-level annotation". A file-header tag is unaffected: it already binds forward to the first declaration in the file.For gate-26 the searched corpus now enters through
source_scope.js_comment_maskfor.ts/.js: comments and regex literals blanked, string literals kept. Strings have to stay — agoto, atoHaveScreenshotfilename and a locator are all real references, and blanking them is the#230mistake that made gate-58's own evidence invisible. PNG baselines still contribute their filename;.json/.txtsnapshots are untouched.This follows a precedent already in
check_e2e_coverage.py:_parse_whole_spec_exclusionalready refuses a Purpose paragraph that merely mentions@e2e exclude, anchored the same way. One rule, two directives.Why the strictness is safe — measured, not assumed
Every anchor the pristine regexes match was classified across the sixteen app checkouts that have an e2e suite: 1,918 anchors, and all 1,918 are already in leading position. None in prose, none in a string, none in code. Anchors with no owning declaration: zero of 1,918.
So gate-19's count does not move at all. Full-tree,
HYDRA_GATE_BASE_REFunset, on clean--depth 1clones ofdevelopment: opencatalogi at963f832stays at thirty-one, nldesign atd18643fstays at one hundred and twenty-six, docudesk at696c381stays at three hundred and ninety-six. Widened to fifteen local checkouts — five thousand eight hundred and twenty-three findings in total — the count is identical in every single app. The fix deletes a counterfeit; it creates no debt.The movement is all in gate-26, and all in the strict direction. On the same three named clones, opencatalogi goes from green to one finding, docudesk from green to six, nldesign stays not-applicable. Across twelve more local checkouts the deltas run from zero to six, and three previously-green cells go red: openbuild, doriath and scholiq. Anyone holding a full-scope green for those three should re-measure after this lands.
The new findings were spot-checked and are true positives, every one credited by a comment and by nothing else. The sharpest is docudesk's
TemplateIndex.vue, where a comment recording that the component is registered by nothing was the only thing crediting it as covered.Deliberately not changed
test.skipguard still counts (#239). A never-false guard stays app debt, not a gate finding. Untouched, not weakened.#343—_ref_is_livewalks ancestors but never descendants — is a different mechanism with its own blast radius and belongs in its own change. Exposure measured for whoever takes it: two hundred and seventy-five anchors bind forward from a header to a describe, across nine apps.#362's runtime-generated-spec blindness is left alone. Crediting a manifest-driven sweep is a green-making rule I could only generalise from one app; eleven apps mention a manifest in their e2e suites and most of those are about a different manifest. Bundling a credit into a tightening would let one half mask the other, which is the shape recorded for gate-61.openspec/changes/still matches neither regex. Making it visible adds a new finding class and is a different change.Both directions proven, both arms fixtured
test_check_e2e_coverage.pygoes from one hundred and twenty-seven to one hundred and thirty-nine assertions;test_check_visual_coverage.pyfrom twenty to twenty-four. Every new arm is paired — a refused position and the accepted position one character away — so "refuse everything" cannot score as a repair. Two of the gate-26 arms exist specifically to stop that: a reference living only inside a string literal, and a PNG baseline named after the component, both of which must keep counting.A new repo-shaped acceptance bundle,
test-fixtures/gate-acceptance/prose-not-proof/, drives both gates through the real wrapper. The planted arm must fail and name its subject; the clean arm must pass. The two arms differ in exactly one property.The controls run in both directions. Four of the eleven new gate-19 assertions and one of the four new gate-26 assertions fail against the pristine checkers and pass against the fixed ones. Swapping the pristine checkers back under the new acceptance bundle makes it report two failures — so the bundle is a detector, not a fixture that agrees with whatever ships.
test_source_scope.py's byte-identical drift assertion against gate-19's tokeniser still holds: the span recorder was added alongside_code_maskrather than replacing it, precisely to keep those two copies proven equal.What I could not verify
The rule is positional, so a prose paragraph that wraps such that the anchor lands at the start of a comment line still counts. My own planted fixture did exactly that and silently defeated the fix I had just written, which is how it was found. Every stricter formulation I could construct kills the legitimate jsdoc header form that two hundred and seventy-five fleet anchors use, so I took the rule I can defend and measure over the cleverer one I cannot. Closing it honestly needs a corpus measurement of wrapped-prose anchors, not a heuristic.
The fifteen-app tables are over working-tree checkouts that other agents are editing; only the three named apps are clean clones at a stated commit. Treat the wide tables as directional. All numbers are local and full-tree — I have not measured a diff-scoped run, and I have not measured launchpad, portaliq, zaakafhandelapp or hrmq at all.
One process note against myself: the four new gate-26 test arms were inserted with a scripted edit rather than by hand, against the standing rule. I re-read the inserted lines byte-for-byte and the result is correct, but the rule exists because the unverified case looks identical to the verified one, so it is recorded rather than quietly relied on.
The self-referential check, and one thing it settled
SHARED-LESSONSflags as untested whether a PR body reaching a checker's corpus would be a coverage-manufacturing vector. It was tested here. The PR body feeds exactly one consumer in the runner, and only three gates read that consumer — none of them these two. Running the real wrapper over the planted fixture with a body carrying the planted anchor, the planted component path and the bare component token changed nothing: both gates still failed and still named their subjects. A PR body cannot manufacture gate-19 or gate-26 coverage. It still lands in the workflow log, so the filter-before-prettifying rule for reading that log is unchanged.This PR changes the verdict for all eighteen apps at once. Not for merge without a human decision on sequencing.