fix(gate-39): a BOUND name is still a name, and [^>]* could not see 19 buttons - #259
Merged
Conversation
… 19 buttons
THE FALSE POSITIVES
-------------------
The accepted-attribute regex was
r'(^|\s)(:?aria-label|aria-labelledby|v-bind:aria-label|title)\s*='
and `:?` binds to the FIRST alternative only. `:aria-label` was accepted;
`:title`, `v-bind:title` and `:aria-labelledby` were not. Vue binds nearly
every user-visible string because it has to pass through `t()`:
<button type="button" class="settings-page-editor__remove"
:title="t('openbuild', 'Remove tab')"
@click="removeTab(index)">
That button HAS a name. ALL 22 of openbuild's findings were this exact shape,
and the only way to close them was to add a second, redundant name. What
matters is whether the attribute reaches the DOM, and `:title` reaches it
exactly as `title` does — the gate already accepted static `title=`, so this
is a consistency fix, not a new claim about how strong a name `title` is.
THE FALSE NEGATIVES, which nobody had counted
---------------------------------------------
`<button\b([^>]*)>` ends the attribute run at the FIRST `>` — including one
inside an attribute VALUE:
<NcButton :disabled="!staffId || submitting || pin.length >= 6" @click="…">
The old parser ended that tag at `pin.length >`, took `= 6" @click=…` as the
button's BODY, found more than two characters in it, and passed. 19 buttons
across 6 apps (opencatalogi 2, docudesk 3, procest 1, openregister 8,
pipelinq 3, doriath 2) were invisible to this gate for that reason alone. The
parser is quote-aware now, which is why pipelinq's count RISES from 4 to 6
while every other app falls.
MEASURED ACROSS 11 REPOS: 77 findings -> 44.
openbuild 22 -> 0 procest 32 -> 24 docudesk 5 -> 2
opencatalogi 4 -> 3 openregister 7 -> 6 pipelinq 4 -> 6 (see above)
Also accepted, on the same "the name arrives" principle: the camelCase
`ariaLabel` prop form, and an explicit `<template #default>` slot, which
nc-vue renders as the button's label. The icon slot ALONE is not a name and
is still flagged — that pair is asserted.
Implementation moved to scripts/lib/check_button_name.py with 20 tests.
Comments and script/style blocks are excluded: markup that does not ship is
not a control.
MUTATION-CHECKED:
* the old bind-blind regex restored -> 6 failures
* default-slot acceptance widened to ANY slot -> the icon-slot true
positive fails, which is the whole point of keeping it
A missing OR CRASHING helper now reports SKIPPED (wiring), not PASS (#147 /
#249): exit code is a status, findings are stdout, stderr is kept in
<log>.err, and the call is wrapped in `set +e` because gate-19's block leaves
errexit on for every gate after it.
Refs #222
… errexit invariant Same resolution as the gate-37/43 merge: * KEPT from main (#225/#261): gate-39 enumerates _a11y_markup_files and guards on _a11y_has_markup_dir. Taking my side wholesale would have REVERTED the glob fix for this gate. check_button_name.py reads markup, not Vue specifically, so an unnamed <button> in a .php template is now caught too. * KEPT from this branch: bound-attribute acceptance, the quote-aware parser that could finally see 19 previously-invisible buttons, and the tested helper. * DROPPED (#243): the `set -e` restore dance. errexit is OFF for the whole script and nothing may turn it on; restore sites say `set +e`.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 8, 2026
…o full of markup, and three reported PASS over a crashed checker (#272) * fix(gates 35,40,42,44): four a11y gates excused themselves from a repo full of markup, and three reported PASS over a crashed checker Measured at package sha cdfbd7a against opencatalogi (93 .vue) and nldesign (zero .vue, one PHP template), one textbook true positive planted per gate in BOTH — the asymmetry that made #225/#261 possible. All 11 gates in the 34-44 band fired and named the plant in both arms, and all returned to their exact prior verdict on removal. Two defects survive that. 1. FOUR GATES GO `na` ON A TEMPLATES-ONLY REPO Gates 35, 40, 42 and 44 still guarded on `[ -d src ]` while 34/36/37/39/43 had moved to `_a11y_has_markup_dir`, and the central applicability table listed the whole family under `[ -d src ]`. On a repo with a `templates/` full of markup and no `src/`, same run, same files: gate-34/36/37/38/39/41/43 ran; four of them FAILED on the plants gate-35/40/42/44 NOT APPLICABLE — "this repo ships no frontend, so there is no .vue/.js/.ts source for this gate to inspect" `na` is the one verdict that removes a gate from coverage accounting, and the reason was contradicted by the same run's own output three lines above it. No fleet app is templates-only today; nldesign is one `rm` away, since its `src/` holds a single `manifest.json` — the exact shape that made twelve gates pass over nothing in #225. The guards now call `_a11y_has_markup_dir`, and the applicability declaration calls THE SAME FUNCTION rather than restating it, so the two cannot drift again. No third scope definition was added. 2. A CRASHED CHECKER REPORTED PASS (#147 / #249) — gates 40, 42, 44 With a `python3` on PATH that exits 1 on every call, run against opencatalogi: gate-40 PASS gate-42 PASS gate-44 PASS <- the three inline ones gate-34/37/38/39/41/43 SKIPPED (wiring) <- the six behind a helper gate-40 printed PASS over the 13 real findings it had reported one run earlier. gate-40 discarded its status with `2>/dev/null || true`; 42 and 44 ran per-file inline heredocs and never had one. 42 and 44 move to scripts/lib/check_link_text.py and scripts/lib/check_autocomplete.py — one interpreter for the whole file set, findings on stdout, exit code as a status — and 40 gains the same return-code guard. FOUND WHILE WRITING THE TESTS * gate-44 judged an input on the FIRST of name/id/v-model and stopped, so `<input id="e" type="text" name="email">` — the plainest textbook case this gate has — passed. Fleet effect, measured across 15 repos: openregister 0 -> 1 (an OpenAI Organization ID field), pipelinq 4 -> 5 (a "Colleague email" field). Both genuine, nothing lost. * gates 35, 36 and 44 read attribute values out of DOUBLE QUOTES ONLY. `tabindex='5'`, `alt=''` and `name='telephone'` render identically and reported PASS in both arms. Zero occurrences in the fleet today, which is why they could sit there indefinitely. * `[^>]*` in gates 42 and 44: a `>` inside an attribute value is not the end of a tag — the parse that hid 19 buttons from gate-39 (#259, #198, #236). * gates 42 and 44 scanned RAW text, so a commented-out `<a>click here</a>` or `<input name="email">` counted. That is gate-64's defect (#184), the one gate-38 (#247) and gate-41 (#266) each shipped a fix for. MEASURED AFTER, NOT ONLY BEFORE * 15 repos, gates 34-44, before vs after: every verdict and every finding count identical except the two new gate-44 true positives above. The rewrites of 42 and 44 removed nothing. * opencatalogi and nldesign return to their exact pre-plant baselines. * ARM 4 of test_gate_a11y_markup_scope.sh was mutation-checked: reverting gate-42's guard to `[ -d src ]` turns it red with the finding it was written for. TESTS * scripts/lib/test_check_link_text.py, test_check_autocomplete.py — 32 assertions; every relaxation ships with the true positive it must not swallow, comment/script exclusions ship with their positive control, and each ends with the whole PRE-FIX checker replayed as the mutant, asserting it answers DIFFERENTLY on every fixture. * test_gate_a11y_helper_wiring.sh gains gates 39, 40, 42, 44 (39 was wired correctly but never listed, so nothing held it to that) — 70 assertions. * test_gate_a11y_markup_scope.sh gains ARM 4, the templates-only repo. * Full discovered suite: 49 passed, 0 failed, 2 pre-existing quarantines. tests/test-hydra-gates-bin.sh: 59 passed, 0 failed. * fix(test): SC2194 — the case word was the constant, not the subject `case " 38 45 " in *" ${_g} "*)` matches a constant against a pattern built from the variable, which is the comparison written backwards. It happened to work, and ShellCheck is right that it reads as a mistake. Verified with shellcheck 0.10.0 at full severity: clean.
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.
Part of the a11y gate repair (#222 family).
The false positives
r'(^|\s)(:?aria-label|aria-labelledby|v-bind:aria-label|title)\s*=':?binds to the first alternative only.:aria-labelwas accepted;:title,v-bind:titleand:aria-labelledbywere not. Vue binds nearly every user-visible string, because it has to pass throught():That button has a name. All 22 of openbuild's findings were this exact shape, and the only way to close them was to add a second, redundant name.
The gate already accepted static
title=, so accepting the bound form is a consistency fix, not a new claim about how strong a nametitleis.The false negatives, which nobody had counted
<button\b([^>]*)>ends the attribute run at the first>— including one inside an attribute value:The old parser ended that tag at
pin.length >, took= 6" @click=…as the button's body, found more than two characters in it, and passed.19 buttons across 6 apps were invisible to this gate for that reason alone:
The parser is quote-aware now — which is why pipelinq's count rises 4 → 6 while every other app falls. I checked each of those two by hand; both are real buttons the old regex could not parse.
Measured across 11 repos: 77 → 44
Also accepted, on the same "the name arrives" principle
ariaLabelprop form<template #default>, which nc-vue renders as the button's labelThe icon slot alone is not a name and is still flagged — that pair is asserted, and mutating the slot rule to accept any
<template>turns it red.Mutation-checked
Wiring
A missing or crashing helper now reports
SKIPPED (wiring), not PASS (#147 / #249): exit code is a status, findings are stdout, stderr is kept in<log>.err, and the call is wrapped inset +ebecause gate-19's block leaveserrexiton for every gate after it — without that, a failing helper kills the whole runner mid-sweep.Implementation:
scripts/lib/check_button_name.py, 20 tests. Suites: 29 helper suites pass.