Skip to content

fix(gate-39): a BOUND name is still a name, and [^>]* could not see 19 buttons - #259

Merged
rubenvdlinde merged 2 commits into
mainfrom
fix/gate-39-bound-attributes
Aug 8, 2026
Merged

fix(gate-39): a BOUND name is still a name, and [^>]* could not see 19 buttons#259
rubenvdlinde merged 2 commits into
mainfrom
fix/gate-39-bound-attributes

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

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-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.

The gate already accepted static title=, so accepting the bound form 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 were invisible to this gate for that reason alone:

app invisible buttons
openregister 8
docudesk 3
pipelinq 3
opencatalogi 2
doriath 2
procest 1

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

app before after
openbuild 22 0
procest 32 24
openregister 7 6
docudesk 5 2
opencatalogi 4 3
pipelinq 4 6 ⬆

Also accepted, on the same "the name arrives" principle

  • the camelCase ariaLabel prop form
  • an explicit <template #default>, 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, and mutating the slot rule to accept any <template> turns it red.

Mutation-checked

reverted fix result
old bind-blind regex restored 6 failures
default-slot acceptance widened to any slot icon-slot true positive fails

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 in set +e because gate-19's block leaves errexit on 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.

… 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
rubenvdlinde merged commit fbdae57 into main Aug 8, 2026
31 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant