Skip to content

fix(gates): five gates that reported PASS over real defects — 45, 46, 19, 13, 20 - #328

Merged
rubenvdlinde merged 7 commits into
mainfrom
fix/blind-gates-45-46-19-13-20
Aug 10, 2026
Merged

fix(gates): five gates that reported PASS over real defects — 45, 46, 19, 13, 20#328
rubenvdlinde merged 7 commits into
mainfrom
fix/blind-gates-45-46-19-13-20

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Five gates that reported PASS over real defects. Every green each of them has produced is worth nothing, so each fix is proven in both directions — FAIL on the shape the repos actually contain, PASS on a clean one — and each carries a regression suite that goes red against the pre-fix runner.

# Gate Was blind to Fleet findings
#287 45 prefers-reduced-motion every stylesheet — it had never opened a .css file 0 → 37
#322 46 spec-anchor-existence tests/, never enumerated 679 → 951 (+272)
#308 19 e2e-coverage Playwright testIgnore 0 (hole real, unexploited)
#321 13 modal-isolation every multi-line-opened dialog 8 → 92 files (+84)
#294 20 or-objectservice-api comments — its first live finding was one 2 → 1

#287 — gate-45 has never read a stylesheet

_a11y_markup_files enumerates .vue/.php/.html and scans <style> blocks. In a Nextcloud app the app-wide motion lives in css/, because that is what Util::addStyle() loads; a <style scoped> block only ever styles one component.

Reproduced: css/main.css with animation: + transition: and no guard → [gate-45] prefers-reduced-motion: PASS. Real instances: nldesign 3 stylesheets, 0 guards; openregister css/main.css, 7 motion declarations, 0 guards.

The un-blinding needed four false-positive controls, because widening a gate fleet-wide is exactly what turns it into a noise generator:

  • the guard regex demanded @media followed immediately by (, so @media screen and (prefers-reduced-motion: reduce) — valid and common — would have become a finding
  • comments masked; // only in SCSS dialects and never the // of url(https://…)
  • transition: none is how a fallback is written, not motion
  • a repo-wide universal reset in one file guards every other file — otherwise the gate reports every other stylesheet the day the correct fix lands. Measured: no fleet repo has one today, so this suppresses nothing now; it exists so that fixing these 37 actually turns the gate green
  • generated output skipped by content (a >500-char line), which catches webpack's css/main-<hash>.chunk.css that has no .min in its name — 6 findings in app-versions/openwoo dropped

37 findings over 40 repos; 14 in the GitHub fleet (nextcloud-vue 8, nldesign 3, opencatalogi 1, openconnector 1, openregister 1). Existing markup arm unchanged at 143 — nothing widened by accident.

#322 — gate-46, with a correction to the issue's premise

The filed premise does not hold on this package, and I've posted the measurement on the issue so it is corrected where it was read. Planted tags prove tasks.md targets are existence-checked; doriath's 353 all resolve through build_archive_index, which exists precisely for the archived-under-a-date-prefix case, and the archived file exists at the very commit the issue measured. What produced PASS there was ADR-020 diff scoping.

The real hole the investigation found: the enumerator is find lib src. It has never opened a test file. 272 unresolved targets under tests/, in 16 repos. The textbook case is procest — DsoDeadlineJobTest.php annotates tasks.md#T14 against a file numbering T01–T08 and V01–V10.

Landed as a hard FAIL, not a WARN. Diff-scoped under ADR-020, so debt in an untouched test file never blocks a PR; the finding surfaces on the PR that edits it, which is when the tag should be re-checked. A WARN would reproduce the failure mode this band exists to remove.

openspec/ deliberately not added — measured at 292 findings, dominated by documentation templates quoting the syntax (openspec/changes/{name}/tasks.md#task-N).

#308 — gate-19 cannot see testIgnore

Every *.spec.ts under tests/e2e/** counted as a running test. Playwright disagrees. Proven by planting an anchor in a CI-ignored directory: 271 → 270 with the scenario reported COVERED.

A glob list would have been the bug. **/visual/** and **/docs-screenshots.spec.ts sit in a testIgnore in fourteen fleet configs and are pulled back in by the visual / docs-capture projects via testMatch. Treating "named in some testIgnore" as dead would have stripped coverage credit from every visual and docs spec in the fleet. A file is dead only when no project would run it.

Validated against all 21 real fleet configs: every one parses; the only dead files anywhere are the api-direct trees excluded on purpose (openregister 25, openconnector 6). Visual and docs specs: 0 dead.

Fleet finding count: zero. Those api-direct files carry only prose mentions of @e2e plus one whole-spec tag with no slug. The hole is real and proven by a plant; nothing is exploiting it today, so this lands with no churn.

Conservative by construction — no config, an unparsable config, an extglob/brace pattern, a non-literal testMatch/testDir all resolve to LIVE, i.e. to the pre-existing behaviour. Comments masked first: every fleet config explains the replace-not-merge rule in prose containing testIgnore:.

#321 — gate-13 misses every multi-line-opened dialog

grep -qE '<NcModal[ \t>/]|<NcDialog[ \t>/]' matches line by line, so

<NcDialog
    :open="showConfirm"
    name="Delete lead">

has nothing after <NcDialog on its own line and the character class cannot match end-of-line. pipelinq: 0 of 9 real violations seen, while the gate passed its own planted true positive the whole time — because a plant is written on one line and a real dialog is not.

The delimiter is widened to include end-of-line, not dropped, so <NcDialogHeader> still does not match. Comment masking also removes a false positive the old pattern had: it reported a <NcModal> inside a /* */ block.

8 → 92 files across 10 repos — nextcloud-vue 50, procest 13, pipelinq 9, doriath 6, docudesk 4, openregister 3, softwarecatalog 3, decidesk 2, app-versions 1, hermiq 1. nextcloud-vue is the shared component library and accounts for over half; its findings are dialog components outside src/dialogs/ rather than modals inlined in a parent. Hard FAIL, unchanged from what gate-13 already is; diff-scoped, so none of it blocks a PR that does not touch the file.

#294 — gate-20 counted a commented-out call

The first thing the repaired gate reported in the fleet was openconnector lib/Service/SearchService.php:189// $directory = $this->objectService->findObjects(...). A gate whose first live finding is false is a gate people learn to ignore.

Applies the pass gate-5 got in #196: source_scope.py --mask php, which blanks //, # and /* */ while preserving offsets so the reported line number still addresses the real file. #[ is left alone — it opens a PHP 8 attribute, and the line these calls sit under is #[NoAdminRequired]. The log now shows the original source line, not the blanked mask.

The mask inherits the gate's own rule: if it cannot run, the gate reports wiring, not a pass. Falling back to raw text would restore the false positive; treating empty mask output as clean would make gate-20 green everywhere — the same failure mode in a new costume.

2 → 1 findings. The one kept is shillinq's real findObject() on a container-resolved ObjectService, the true yield #271 identified.


Verification

Both-direction proof for every fix, plus a negative control — each new suite run against the pre-fix runner:

Suite vs pre-fix runner
test_gate_45_stylesheet_scope.sh (8 arms) 3 red, 5 anti-widening green
test_gate_46_tests_scope.sh (6 arms) 5 red, lib/ arm green
test_check_e2e_coverage.py (+11 tests, 105 → 116) 4 red, 7 anti-widening green
test_gate_13_multiline_dialog.sh (7 arms) 3 red, 4 anti-widening green
test_gate_20_comment_masking.sh (8 arms) 2 red, 4 true-positive arms green

Each suite's anti-widening arms stay green in both runs — the suites are not "everything fails".

  • Every checker proven to have lived: .err 0 bytes on each measurement, never an exit code read in place of output.
  • The three new .sh suites are picked up automatically by tests/run-helper-suites.sh (discovery-based, no workflow edit).
  • test_gate_45_to_55_acceptance.sh run with NODE_PATH set — ajv preflight from test(gates): refuse to run the 45–55 suite when ajv is unresolvable (it reported 4 gate-53 defects that do not exist) #292 intact, ALL GREEN.
  • test_gate_a11y_markup_scope.sh ALL PASS; test_check_spec_anchors.py 50/50; full run-helper-suites.sh green.
  • No new _skip category introduced, so --require-full-coverage is unaffected. gate-13 and gate-20 gain a wiring skip for a crashed checker, replacing a silent pass.
  • quality.yml untouched.

Closes #287
Closes #308
Closes #321
Closes #294
Refs #322

🤖 Generated with Claude Code

Conduction Release Bot added 7 commits August 10, 2026 10:21
Gate-45 (prefers-reduced-motion) scanned <style> blocks inside markup and
nothing else, so every green it has ever produced is a statement about
markup, not about CSS. In a Nextcloud app the app-wide motion lives in
css/, because that is what Util::addStyle() loads.

Measured before this commit:
  nldesign      3 stylesheets with motion, 0 guards  -> gate-45 PASS
  openregister  css/main.css, 7 motion decls, 0 guards -> gate-45 PASS

The un-blinding is paired with four false-positive controls, because
widening a gate fleet-wide is exactly the change that turns it into a
noise generator:

  * the guard regex now recognises a full media prelude
    (@media screen and (prefers-reduced-motion: reduce)), which the old
    'immediately followed by (' pattern could not have matched
  * comments are masked (#294's lesson), // only in SCSS dialects and
    never the // of a url(https://...)
  * transition/animation: none is how a fallback is WRITTEN, not motion
  * a repo-wide UNIVERSAL reset in one file guards every other file, so
    the gate accepts the fix people will actually write rather than
    reporting every other stylesheet the day that reset lands
  * generated output is skipped by CONTENT (a >500-char line), which
    catches webpack's css/main-<hash>.chunk.css that has no .min in
    its name

Fleet-wide yield measured over 40 repos: 37 new findings, 14 of them in
the GitHub fleet (nextcloud-vue 8, nldesign 3, opencatalogi 1,
openconnector 1, openregister 1). The existing markup arm is unchanged
at 143 findings, so nothing was widened by accident.

test_gate_45_stylesheet_scope.sh proves both directions and goes 3 red
against the pre-fix runner.

Closes #287
…d them

Gate-46's scope was `find lib src`. It had never opened a test file, and
tests/ is where a large share of the fleet's @SPEC tags live, because a
test is the natural place to name the requirement it proves.

Measured over the 21 apps carrying an openspec/: 272 unresolved targets
under tests/, in 16 repos, that no run has ever reported. The textbook
case is procest — tests/Unit/BackgroundJob/DsoDeadlineJobTest.php
annotates openspec/changes/dso-omgevingsloket/tasks.md#T14 against a
tasks.md numbering T01-T08 and V01-V10. There is no T14 and never was.
The identical tag in lib/ would have failed this gate since #246.

Landing as a HARD FAIL, matching the existing lib/src arm. The gate is
diff-scoped under ADR-020, so pre-existing debt in an untouched test file
never blocks a PR — the finding surfaces on the PR that edits that file,
which is exactly when the tag should be re-checked. A named WARN would
reproduce the failure mode this band exists to remove: a gate emitting
something other than FAIL over a real defect.

openspec/ is DELIBERATELY NOT added. Measured: 292 findings, dominated by
documentation TEMPLATES that quote the syntax rather than use it
(openspec/changes/{name}/tasks.md#task-N, <slug>, ...) in context-briefs
and proposals across shillinq, pipelinq and others. Auditing them would
bury real findings under placeholders.

Correction recorded in the docblock: #322 as filed reports that tasks.md
targets are never existence-checked (353 on doriath). That premise does
not hold on this package. A planted
openspec/changes/does-not-exist-at-all/tasks.md#task-1 IS reported as
'target file not found', and #task-99999 against a real tasks.md IS
reported as 'anchor not found'. The 353 all resolve through
build_archive_index, which exists for the archived-under-a-date-prefix
case the issue describes, and the archived file exists at the very commit
the issue measured. What produced PASS there was ADR-020 diff scoping.

test_gate_46_tests_scope.sh proves both directions and goes 5 red against
the pre-fix runner while its lib/ arm stays green.

Refs #322
The gate counted every *.spec.ts under tests/e2e/** as a running test.
Playwright does not: a file excluded by testIgnore, living outside
testDir, or matched by no project's testMatch is never executed, and a
scenario referenced only from such a file has no automated proof at all.
The gate could already see describe.skip (#239) but not the config that
does the same thing to a whole directory.

Reproduced by planting an @e2e anchor in a CI-ignored directory: the
uncovered count dropped 271 -> 270 with the scenario reported COVERED.
The live fleet shape is openregister's
tests/e2e/api-direct/search-views-presentation.spec.ts, under a
**/api-direct/** that is excluded at top level AND repeated in every
project, because a project-level testIgnore REPLACES the top-level one
rather than merging with it.

A GLOB LIST WOULD HAVE BEEN THE BUG. **/visual/** and
**/docs-screenshots.spec.ts sit in a testIgnore in fourteen fleet
configs and are pulled BACK IN by the visual / docs-capture projects via
testMatch. Treating 'named in some testIgnore' as dead would have
stripped coverage credit from every visual and docs spec in the fleet.
A file is dead only when NO project would run it.

Validated against all 21 real fleet configs: every one parses, and the
only dead files anywhere are the api-direct trees excluded on purpose
(openregister 25, openconnector 6). Visual and docs specs: 0 dead.

Fleet-wide finding count: ZERO. Those api-direct files carry only prose
mentions of @e2e plus one whole-spec tag with no slug, so no ref is
currently claimed from an unrun file. The hole is real and proven by a
planted true positive; nothing in the fleet is exploiting it today, so
this lands with no churn.

Conservative by construction: no config, an unparsable config, an
extglob/brace pattern, a non-literal testMatch or testDir all resolve to
LIVE, i.e. to the pre-existing behaviour. Comments are masked first —
every fleet config explains the replace-not-merge rule in prose
containing 'testIgnore:'.

11 new unit tests; the 4 that assert the fix go red against the pre-fix
helper while the 7 anti-widening arms stay green. Suite: 105 -> 116.

Closes #308
The test was grep -qE '<NcModal[ \t>/]|<NcDialog[ \t>/]'. grep matches
line by line, so a tag with its props on following lines — which is how
Vue components with more than a prop or two are actually written, and
what every formatter produces —

    <NcDialog
        :open="showConfirm"
        name="Delete lead">

has nothing after <NcDialog on its own line. The character class cannot
match end-of-line, so the tag was invisible.

Measured on pipelinq: 0 of 9 real violations seen, while the gate passed
its own planted true positive the whole time — a plant is written on one
line and a real dialog is not. That is the trap: a minimal plant and a
real defect differing in precisely the feature the regex depends on.

The delimiter is WIDENED to include end-of-line, not dropped, so
<NcDialogHeader> and <NcModalFooter> still do not match. Comments are
masked first, which also removes a FALSE POSITIVE the old pattern had:
it reported a <NcModal> written inside a /* */ block comment.

Fleet-wide finding count: 8 -> 92 files (+84), across 10 repos —
nextcloud-vue 50, procest 13, pipelinq 9, doriath 6, docudesk 4,
openregister 3, softwarecatalog 3, decidesk 2, app-versions 1, hermiq 1.
nextcloud-vue is the shared component library and accounts for over half;
its findings are dialog components sitting outside src/dialogs/ rather
than modals inlined in a parent. Gate-13 is diff-scoped under ADR-020, so
none of this blocks a PR that does not touch the file. Landing as a hard
FAIL, unchanged from what gate-13 already is.

Comment masking suppresses zero findings across the fleet today; it is
here so a <!-- <NcDialog … --> in a TODO cannot become one later, which
is exactly how gate-20 acquired its commented-out call (#294).

The checker now reports a crashed interpreter as wiring instead of
leaving an empty log this gate would call clean (#147/#249/#262).

test_gate_13_multiline_dialog.sh proves both directions and goes 3 red
against the pre-fix runner while 4 anti-widening arms stay green.

Closes #321
…s not a call

The first thing the un-blinded gate reported in the fleet was not a call.
It was openconnector lib/Service/SearchService.php:189:

    // $directory = $this->objectService->findObjects(filters: [...]);

grep has no idea what a comment is, and a gate whose first live finding
is false is a gate people learn to ignore.

Applies the pass gate-5 received in #196: source_scope.py --mask php,
which blanks //, # and /* */ while PRESERVING offsets and newlines, so
the reported line number still addresses the real file. #[ is left alone
— it opens a PHP 8 attribute, not a comment, and swallowing it would
delete #[NoAdminRequired], the line these calls sit directly under.

The log now prints the ORIGINAL source line rather than the blanked
mask, so a reader sees the code that is actually at that line.

The mask inherits this gate's own rule: if it cannot run, the gate
reports wiring and NOT a pass. Falling back to raw text would silently
restore the false positive; treating empty mask output as clean would
make gate-20 green everywhere — the 2026-08-08 failure mode in a new
costume.

Fleet-wide finding count: 2 -> 1. The one removed is openconnector's
commented-out line; the one kept is shillinq
lib/Controller/BookingNotificationController.php calling findObject() on
a container-resolved OCA\OpenRegister\Service\ObjectService, which is
the real yield #271 identified.

test_gate_20_comment_masking.sh proves both directions and goes 2 red
against the pre-fix runner while all four true-positive arms — including
#271's receiver anchoring and the #[Attribute] non-swallow — stay green.

Closes #294
@rubenvdlinde
rubenvdlinde merged commit 626378d into main Aug 10, 2026
31 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/blind-gates-45-46-19-13-20 branch August 10, 2026 09:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment