fix(gates): nine checkers matched prose, not code (#191, #196, #220, #224, #226, #230, #235, #236, #266) - #269
Merged
Merged
Conversation
… nine gates Every gate below decided a question about CODE by grepping the raw bytes of a file. Prose is made of the same bytes, so each one failed in BOTH directions at once — the shape first written down in #184: "a checker that greps a STRING LITERAL misses every constant and matches every comment." #191 gate-48 a REMOVED COMMENT naming `#[NoCSRFRequired]` read as a removed attribute. nldesign red for one rewritten docblock sentence. #196 gate-5 a docblock saying `#[NoAdminRequired]` is deliberately NOT used SATISFIED the auth gate. A false NEGATIVE on a security gate, and a pass leaves no log. #220 gate-31 an `<img>` in a JSDoc comment in <script> (launchpad). #235 gate-31 the same, 3 of 3 findings on openbuild. #224 gate-34 false RED on a comment AND false GREEN on window['confirm'](). #226 gate-3 a run() delegating to one helper read as a stub, and the gate was closable by an inert `$unused = 1;`. #230 gate-58 a comment WARNING AGAINST networkidle counted as a use of it. #236 gate-12 `<NcSelect[^>]*>` truncated at the `>` of `option =>`. #236 gate-32 a comment describing the `<div @click>` an element replaced scored as that `<div @click>`. #266 gate-41 a PHP comment mentioning `<html>` made a mount point a page root. ONE SCOPE, NOT NINE ------------------- scripts/lib/source_scope.py generalises the two precedents that already got this right — #184's PHP stripper (which knows `#` opens a comment but `#[` opens an attribute) and #249's gate-19 tokeniser (blank once, PRESERVE OFFSETS, keep string delimiters). Every mask returns a same-length string, so a gate can report a line number computed on the mask and read a suppression marker out of the ORIGINAL at that line — which matters because every suppression marker in this package lives in a comment. Gate-19 keeps its own copy of the JS tokeniser; a drift test asserts the two byte-identical over a corpus and over this package's own .js sources, and asserts the keyword sets equal — the corpus alone SURVIVED deleting "await" from one set, so the corpus alone was not enough. #196 SHIPS WITH A DECLARATION, NOT JUST A TIGHTENING ----------------------------------------------------- Admin-only is expressed in Nextcloud by the ABSENCE of an attribute, and absence is the only thing gate-5 reports. Closing the false negative alone would have converted it into a PERMANENT false positive on correct code, with no legitimate way to satisfy the gate. So `@auth admin-only <reason>` joins the `@spec exclude` family. Making bare absence sufficient was considered and rejected: it would empty the gate completely. MEASURED, NOT ASSUMED --------------------- - 3 fixtures from #226's table, the 4 arms from #224, the nldesign line from #191 and the larpingapp line from #230, all verbatim. - Every relaxation is paired with the true positive it must not swallow, and every wiring is covered both ways: a MISSING helper and a CRASHING helper must report SKIPPED, never PASS (#147, #245, #249). gate-5 additionally runs a positive control on the mask itself, because a mask that silently returns its input is invisible to `[ -f helper ]` and puts the gate straight back into the false negative. - A nested `<template #default>` slot regression was caught by measurement before landing: a lazy `(.*?)` ended the SFC template at the first slot close and deleted a real finding at openconnector EditMapping.vue:376. Boundaries are found by depth now, and there is a test. Closes #191, #196, #220, #224, #226, #230, #235, #266 Refs #236 (parts 1 and 2; part 3 was already fixed by #247) Supersedes #219, whose gate-12 helper is carried here with its 17 tests.
…ribute is one
Both found by MEASURING the fix rather than by reading the issues.
gate-34 — 7 defects reported as 14 findings
------------------------------------------
The first cut accepted any `window.confirm` REFERENCE, called or not, so on
openbuild every native dialog was reported twice:
const ok = typeof window !== 'undefined' && window.confirm <- guard
? window.confirm(t('openbuild', 'Delete this automation?')) <- call
A feature-detection guard is a truthiness test, not a second native dialog,
and inflating a security-adjacent count is its own false report (#254: a count
is not a defect count). A reference now counts only when it is an ALIAS — a
binding whose call site is elsewhere and therefore invisible:
const c = window.confirm counts
const { confirm } = window counts
x && window.confirm ? … : … does not
openbuild: 7 before, 7 after, same seven lines.
The anchor also lost a character it should never have had. Written
`=\s*window\s*[.\[]` it CONSUMED the `window` that follows, and `finditer`
returns non-overlapping matches — so `const r = window.confirm('x')` matched
only the alias rule, failed it because a `(` follows, and reported NOTHING. A
real call dropped by an anchor one character too greedy. It is a lookahead
now, and there is a test.
gate-48 — the old regex could not see a fully-qualified attribute
-----------------------------------------------------------------
Running #191's arm 2 end-to-end through the runner reported PASS on a genuine
removal of
- #[\OCP\AppFramework\Http\Attribute\NoCSRFRequired]
because the pre-fix pattern alternated on the literal `#[NoCSRFRequired]`.
A false NEGATIVE hiding behind the false positive #191 reported — the same
both-ways failure as every other gate in this change. The new bracket-bounded
rule matches it.
Refs #191, #224
…now it
CodeQL raised py/bad-tag-filter (HIGH) against this branch, and it is right.
r'<script(\s[^>]*)?>(.*?)</script\s*>'
does not match `</script bar>` or `</script\t\n foo>`, both of which an HTML
parser treats as the end of the element. When the close is spelled that way
the block regex fails to match AT ALL, the script body is never
comment-masked, and a JSDoc `<img>` inside it is scanned as markup — #235
reintroduced by the mask written to fix it. `</style …>` had the same hole.
⚠️ THE FIRST TEST FOR THIS SURVIVED THE MUTANT. It exercised
`vue_markup_mask`, which keeps `<template>` spans and never goes through
`_SCRIPT_BLOCK` at all, so reverting the regex changed nothing and the suite
stayed green. The assertion now runs through `html_markup_mask` and
`script_mask`, the two functions that actually use it, and the reverted regex
kills both. A mutation test that does not kill is not evidence — it is a
second thing to check.
Refs #235
This was referenced Aug 8, 2026
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
…ver a dead interpreter, and three could not see the defect they exist for (#280) * fix(gates 45-55): eleven gates passed over an unopened scope, eight over a dead interpreter, and three could not see the defect they exist for Every gate in this band was given ONE textbook true positive of exactly what it exists to catch, planted in a real fleet repo, then removed again. Where a gate could not fail, it was repaired; where it could, the plant is now a regression test. Measured at package sha 34370f6. ## 1. All eleven reported PASS over a scope they never opened (#242/#240/#258/#268) On a README-only diff against larpingapp, gates 45-55 printed eleven PASS lines and the summary read "53 of 53 applicable gates ran". Not one of them had opened a file. Gates 4/6/7/19/25/28/62/63 have answered the identical situation with NOT APPLICABLE since #268; this band never adopted it. Gates 47 and 48 are the sharper case: they can only answer a question about a CHANGE SET, so on every builder full-repo run in the fleet — no base ref at all — they printed a co-change verdict they had not formed. ## 2. Eight reported PASS over a crashed interpreter (#147/#249/#262) A planted defect only fires when the gate runs, so no plant can see this. With a `python3` on PATH that exits 1 on every call, on a tree carrying real findings: gate-46 PASS — over the 277 unresolved @SPEC findings, across 104 distinct targets, it had reported one run earlier on the same files gate-47 PASS — on the same diff where it had just reported FAIL gate-45/49/50 PASS (`2>/dev/null` discarded status and traceback) gate-51/54/55 PASS (`|| true` discarded the status) gate-52 FAIL — "1 custom-widget finding(s)", a fabricated finding: the helper returned its COUNT as its exit status, the same channel Python uses for a traceback (#209). The count was also clamped to 99 to fit in a byte. It now prints `findings=N` on stdout and exits boolean; no `findings=` line means the helper died. gate-54 was the quietest: its advisory WARN half reads the same log, so a dead helper silenced both halves at once. ## 3. gate-45 was the residue of #272's fix (.github#274) #272 migrated gates 35/40/42/44 off `[ -d src ]` onto `_a11y_has_markup_dir` and left the twelfth member of the family behind. On a templates-only app gate-45 reported NOT APPLICABLE — "this repo ships no frontend" — over a `<style>` block with `transition:` and no reduced-motion fallback, in the same file gate-43 FAILED on in the same run. `na` is the one verdict that removes a gate from coverage accounting. The regression test was already written and gate-45 was excluded from it by name, with a comment explaining why. Removing the name from ARM 4's skip list in test_gate_a11y_markup_scope.sh IS the test; it fails against 34370f6. ## 4. gate-47: prose satisfied it, and a qualified attribute did not `_ANNOTATION_RE` was an unanchored alternation of string literals, and it was wrong in both directions from that one regex — the pairing #269 found in gate-48 and never carried to its sibling. FALSE POSITIVE rewording ONE docblock sentence that merely NAMES the annotation ("becomes `@NoAdminRequired` again, paired with a real ownership check") made the gate demand a test co-change. A gate satisfiable by prose manufactures the appearance of a security review (#191). FALSE NEGATIVE `#[\OCP\AppFramework\Http\Attribute\NoAdminRequired]` was invisible. A commit adding exactly that to a controller — opening an admin-only endpoint to every authenticated user — with no test in the diff reported PASS. Now position-anchored, by the same rule check_csrf_removal.py already used. ## 5. gate-50: a false positive and a false negative in the same regex FALSE NEGATIVE the app-id argument had to be a QUOTED STRING, so every read written the fleet-standard way — `getValueString( Application::APP_ID, 'listing_register', '')` — was invisible. Identical code with `'larpingapp'` FAILED. Same family as #184. 7 security-relevant reads across 5 repos sit behind a constant today. FALSE POSITIVE the empty-compare guard required a closing paren immediately after the empty string, so the correct compound guard `if ($reg === '' || $sch === '')` was reported as unguarded — twice, on code the gate was asking for. A guard that is a boolean `return` rather than an `if` was rejected too. Both directions are now asserted, including the opencatalogi#86 shape that mixes them: one read guarded, the next unguarded two lines later. ## 6. gate-53 did not block the PR that creates larpingapp#286 Reintroducing #286 exactly — the check-in tab deleted from src/manifest.json, `EventRoster` left registered in src/registry.js — reported PASS. Direction 1 of the registry cross-reference stays advisory for LEGACY orphans, correctly: the gate cannot tell "wire it" from "delete it". But when the DIFF ITSELF removed the last reference it can, and that finding now blocks. Pre-existing orphans are untouched (larpingapp carries one today), so this is prevention, not a burn-down list nobody can close. ## Verified working, repaired nothing gate-46 (dangling file, dangling fragment, valid anchor), gate-48 (short and fully-qualified attribute removal; a comment reword correctly stays green), gate-49, gate-51 (title, description and nested items.properties independently), gate-52's ratchet (growth fails, shrink passes), gate-54 (flat, nested and $ref-carrying), gate-55. ## Deliberately NOT enforced `title == key` on a schema property is a real gate-51 defect — the renderer uses `prop.title || key`, so the user sees the raw technical key. Measured across 10 repos: 148 occurrences, ALL of them in softwarecatalog, where they are VNG-standardised element names (`identifier`, `type`, `name`) that must not be renamed. Enforcing it would produce 148 findings with no legitimate end state in the one repo that has them. Reported rather than gated (#252). ## Divergence to reconcile gate-45 now answers an empty in-scope set with `na`; gate-40 answers it with PASS, by a deliberate choice in #272 that cited the invariant test this PR reworks. The invariant now discriminates on the REASON — the applicability table's own phrasing must not appear once its prerequisite holds — so both behaviours are expressible. The family should pick one. ## Testing New: hydra-gates/scripts/lib/test_gate_45_to_55_acceptance.sh — 31 arms across six families, discovered by run-helper-suites.sh. Against the package as merged on main it fails 20 of 31; the 11 that pass are exactly the anti-widening and no-regression controls. Every mutation asserts its anchor is present before it plants. Repos used, chosen for different shapes: larpingapp (register-owning, manifest-driven, ships registry.js), nldesign (PHP templates, no .vue, no register), doriath (ships no phpcs SpecTagSniff — the #246 control, held at 81 findings across 46 targets before and after the plant), openconnector (41 register files). Full package suite: 52 discovered suites pass, 2 quarantined as documented; 60/60 entry-point invariants. * fix(gate-50): the fail-mode window started where the call began, not where it ended The constant-app-id fix in the parent commit made procest's config reads visible for the first time and immediately produced 3 findings on lib/Service/AiService.php — all three false positives, and both causes are ordinary code the window could never have seen: multi-line call PHPCS formats each read across five lines. Two of them plus a blank line put the guard on the ELEVENTH line, one outside a window counted from the line the match BEGAN on. The guard being missed is a textbook `if (empty($registerId) === true || empty($schemaId) === true) { $this->logger->warning(...); return; }` (AiService.php:580, :967). same-line guard `'ai_api_key_set' => ...getValueString(APP_ID, 'ai_api_key', '') !== ''` handles the empty default ON the match line, and the window started after it (AiService.php:710). The window now anchors to the END of the call expression — parentheses balanced forward from the `(` — and includes the remainder of that line. A single-line read keeps exactly the ten lines it always had. Caught by a before/after sweep of 12 fleet repos: 26 of 121 verdicts changed, 25 of them PASS -> NOT APPLICABLE (the truthfulness correction), and this was the only one that changed to FAIL. procest is PASS again, correctly. Three arms added: the multi-line shape, the same-line shape, and the reverse control — the same multi-line shape with the guard DELETED must still FAIL, so the window cannot have been widened until the gate finds nothing. Also: shellcheck SC2181 in gate-45's new status check, and a file-scoped SC2016 suppression for the acceptance suite, whose PHP fixtures are single-quoted on purpose.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
… an empty scope, and gate-10 excused itself from four real findings (#275) * fix(gates): gate-11 was dead fleet-wide, and eight gates passed over an empty scope Every gate in the 1–11 band was put through one acceptance test: plant ONE textbook true positive of exactly what the gate exists to catch, in a real fleet repo, and require the gate to FAIL and NAME it; then remove the plant and require the prior verdict back. Measured against gate package cdfbd7a. gate-11 admin-router was DEAD ----------------------------- It read four hard-coded paths — src/router/index.{js,ts}, src/router.{js,ts}. ONE fleet app of fifteen (softwarecatalog) has a file at any of them; the other fourteen build their router in src/main.js. Proof it was dead rather than merely unexercised: the doriath c7c72e9 defect re-planted verbatim into larpingapp's real router — routes.push({ path: '/settings', component: AdminRoot }) — reported PASS. The identical line in src/router.js reported FAIL. The detection logic was fine; the gate never opened the file. Routers are now discovered (anything under src/ that constructs one), the four legacy paths are kept, and a repo with no router is `na`. The path rule moved into check_admin_router.py, which resolves the enclosing route object: a bare `path: '/settings'` grep would have flagged openconnector's ADR-079 hand-off — the remediation — as the defect. Fleet churn: 0 findings across 16 repos. EIGHT GATES REPORTED PASS OVER A SCOPE THEY NEVER OPENED -------------------------------------------------------- A README-only commit run with --scope-to-diff produced PASS from gates 1, 2, 3, 5, 8, 9, 10 and 11 — four of them authorization gates. Gates 4/6/7 already said NOT APPLICABLE for the identical situation, which is what made the other eight readable as a result rather than an absence. All eight now say `na` with a reason (`na`, not `structural`, per #268: an empty ADR-020 scope is not a gap the author can close). gate-9 semantic-auth flagged the ESCALATION BRANCH -------------------------------------------------- The `!isAdmin` block was searched for a denial token AT ANY DEPTH, conflating two opposite postures: if (!isAdmin) { return 403; } admin IS required -> true positive if (!isAdmin) { if (!owner) 403; } admin NOT required -> false positive In the second a non-admin OWNER proceeds. Live on docudesk SigningController::cancelRequest (its own docudesk#100 fix) and procest InspectionChecklistController::submitResult. The remedy gate-9 printed for both — remove @NoAdminRequired, or switch to #[AuthorizedAdminSetting] — would have made per-user endpoints admin-only and deleted the owner checks' reason to exist. Now only an UNCONDITIONAL denial counts. Fleet: 28 -> 26 findings; the two removed are exactly those, and larpingapp's genuine CharactersController::downloadPdf finding is preserved. gate-2 forbidden-patterns failed in BOTH directions (#184) ----------------------------------------------------------- Six raw-text greps. Invisible: `var_dump ($x)` (PHP allows whitespace before the argument list), `die;` (a LANGUAGE CONSTRUCT, not a function), and `exit` in every form — die's exact synonym, absent from the pattern list, so one name was banned and the other left open. Reported: a comment saying "never use var_dump( here", and the string literal "select dd(x)". Now judged over a comment- and string-masked copy. `: never` exempts exit/die — a TYPE PHP itself checks, deliberately not the @SuppressWarnings docblock sitting above the one fleet instance, because a docblock is the load-bearing prose #196 was about. Fleet churn: 1 -> 1. gate-8 unsafe-auth-resolver used HARD-CODED INDENTATION ------------------------------------------------------- Body extraction terminated on `/^ \}/`, the catch block on `/^ \}/`. On a tab-indented file neither matches, so "the body" ran to end of file and a resolver that correctly RETHROWS was reported as a fail-open because an unrelated cache method further down returned null from its own catch. The apparent detection of tab-indented fail-opens was the same over-capture by luck. Now brace-walked over a comment-masked copy; the procest ZgwService fail-closed shapes stay excluded by construction rather than by indentation. gate-10 initial-state knew only the single-line shape ----------------------------------------------------- `getElementById(...)...\.dataset` requires lookup and read on ONE line, so the TWO-STEP form — what the doriath line becomes after any refactor — plus querySelector and getAttribute('data-*') all reported PASS. Widened along that one axis only; NOT to a bare `.dataset`, which would flag every legitimate event.target.dataset in the fleet. Two measured exclusions, each a rule rather than a waiver: a dataset key the same file also WRITES is the component's own bookkeeping, and data-requesttoken is not IInitialState data (its accessor is getRequestToken(), so gate-10's remedy would not apply). Fleet churn: 0 -> 0. gate-7 no-admin-idor missed the FULLY-QUALIFIED attribute ---------------------------------------------------------- The look-back matched `#[NoAdminRequired` only. Under `#[\OCP\AppFramework\Http\Attribute\NoAdminRequired]` a textbook IDOR fell out of scope and reported PASS. No fleet file uses that spelling today, which is why it needed closing deliberately — a false negative on a security gate leaves no log. WHAT WAS VERIFIED AND LEFT ALONE -------------------------------- gate-3's #269 repair HOLDS in both directions: an inert `$unused = 1;` no longer closes it, and canonical fail-safe delegation no longer trips it. gate-5's #269 repair HOLDS: prose naming an attribute does not satisfy it, the `@auth admin-only <reason>` declaration does, and a too-short reason does not. gate-4 already distinguishes "no advisories" from "audit did not run". gate-6 detects a zero-caller authorization method and goes quiet on one caller. gate-7 is CORRECTLY correlated — .github#160's anti-correlation does not reproduce: a leak FAILs and all four correct-fix shapes PASS. docudesk EmlPreviewController::preview is a genuine IDOR (un-redacted EML by caller-supplied fileId, no ownership check). Regression tests land in run-helper-suites.sh's discovery path and are mutation-checked against WHOLE pre-fix files, not single-site reverts: the pre-fix runner fails 19 assertions of test_gate_1_11_empty_scope_is_na.sh (including the gate-11 dead-gate case), the pre-fix check_semantic_auth.py fails 3, the pre-fix check_no_admin_idor.py fails 1. Full-tree verdicts before/after are IDENTICAL on larpingapp and openregister, and differ on docudesk only by the intended gate-9 false positive. * test(gates): gate-5's empty-diff verdict is `na`, matching the suite's own comment test_gate_route_auth.sh asserted PASS for a package.json-only diff while its own comment said "a PASS here is scoping, not absence" — a fact PASS does not state and NOT APPLICABLE does. Reclassified per #268. 6b/6c remain the controls: the identical finding must still FAIL once the diff touches the controller or appinfo/routes.php. * fix(gates 5,10): a gate declared `na` over the frontend it was built to check, and gate-5 punted to a package it was standing in Three additions after the first commit, all measured against gate package 34370f6 and reported by the runner's own `[hydra-gates] gate package:` line. gate-10 DECLARED `na` OVER A REPO FULL OF ITS OWN SUBJECT MATTER ----------------------------------------------------------------- The surface was `src/` only. nldesign's `src/` holds exactly one file — `manifest.json` — and its entire hand-written frontend lives in `js/`. So gate-10 announced "this repo ships no frontend" over a repo whose `js/admin.js` does precisely what the gate exists to catch: var settingsEl = document.getElementById('nldesign-settings'); var tokenSets = JSON.parse(settingsEl.getAttribute('data-token-sets')); The doriath AdminRoot defect, two-step form, in an ADMIN settings script. Four real findings. `na` REMOVES A GATE FROM COVERAGE ACCOUNTING, so this did not merely under-report — it left the denominator. The surface is now `src/` + `js/` (Nextcloud's conventional shipped-script directory), minus `*.min.js`: a committed bundle is not authored code. Fleet total 0 -> 4, all four in nldesign, all four genuine. AND A SCOPE-BLINDNESS BUG IN MY OWN TWO-STEP RULE -------------------------------------------------- Widening the surface exposed it. The two-step rule matches by NAME across the whole file. In `js/admin.js`, line 1114 binds `var btn = document.getElementById('nldesign-save-btn')`, and that one line put EVERY `btn` in a 1700-line file into scope — including three `forEach(function (btn) {...})` callbacks whose `btn` is the clicked element, i.e. the component's own markup. Four false positives from one binding. Rather than implement JS scoping, an AMBIGUOUS NAME IS DROPPED: if an identifier is ever a function parameter, the file cannot tell the two apart and declines to guess. That can only under-report, which is the right direction for a rule whose false positives would otherwise bury the real finding. 7 raw hits -> 4, and the 4 that remain are the server-rendered reads. gate-5 CALLED A FILE ABSENT FROM INSIDE THE REPOSITORY CONTAINING IT --------------------------------------------------------------------- Nextcloud resolves a route name `A\B\C` against `OCA\<App>\A\B\C`, which PSR-4 maps to `lib/A/B/CController.php`. The resolver rooted every namespaced name at `lib/Controller/` — correct for `Settings\FileSettings`, wrong for `AppHost\Controller\GenericHealth`, whose file is `lib/AppHost/Controller/GenericHealthController.php`. Measured on openregister, the repository that SHIPS those classes. The derived path did not exist, `_apphost_serves` then matched the name, and both entries were filed as "served by the OpenRegister AppHost generic controller (ADR-040); its auth attribute lives in the openregister package and is NOT visible from this repository" inside openregister. The gate punted to another package while standing in it, so `AppHost\Controller\GenericHealth#index` and `GenericMetrics#index` had their auth posture judged by nobody. A derived path is a GUESS; both roots are tried and an existing one wins. Both now resolve, both are correctly annotated (`#[PublicPage]` + `#[NoCSRFRequired]`), and openregister's gate-5 count is unchanged at 10 — this surfaced two unjudged endpoints without inventing a finding. The AppHost CONSUMER control still classifies its four absent generics as ADR-040-unresolved. TWO ACCEPTANCE ARMS A PLANTED DEFECT CANNOT REACH -------------------------------------------------- A plant only fires when the gate RUNS, so two classes of defect are invisible to it. Both are now asserted: ARM 4 an ATTRIBUTE-ONLY change is a change. `_filter_preexisting` (gates 6, 7, 8 in this band) compares a method against the base ref and moves unchanged entries out of the verdict. Before #276 the comparison began at the `function NAME(` line, so ADDING `#[NoAdminRequired]` above an existing unguarded method left the body byte-identical and the finding was suppressed — the single edit that changes a method's auth posture was the one the filter could not see. Verified BOTH ways on one fixture: package cdfbd7a gives `[gate-7] no-admin-idor: PASS` with the finding in `.log.preexisting`; package 34370f6 gives FAIL. #276 fixed the shared helper; this keeps it fixed for gate-7, whose entire scope is decided by that attribute. ARM 5 a BROKEN INTERPRETER is `SKIPPED (wiring)`, never PASS. With python3 exiting 1, gates 2, 3, 10 and 11 all skip. gate-3 is the control: on the same fixture it FAILS a real stub `run()` when python3 works, so the skip is the gate losing a verdict it demonstrably had. TWO REPO SHAPES, NOT ONE ------------------------ gate-11's repair is proven on three: larpingapp (vue-router 3, `new VueRouter`), scholiq (vue-router 4 `createRouter`, AppHost-adopting) and nldesign (no router at all -> `na`, verified honest: no `createRouter`/`new VueRouter` anywhere in the repo). gate-5's resolver is proven on openregister (ships the generics) and an AppHost consumer (does not). Full-tree verdicts vs cdfbd7a: larpingapp and scholiq IDENTICAL; docudesk differs only by the intended gate-9 false-positive removal; nldesign gains gate-10 FAIL (4 real findings) and gate-11 `na` (previously a green over zero bytes). --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
This was referenced Aug 9, 2026
Merged
Closed
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.
Nine checkers decided a question about code by grepping the raw bytes of a file. Prose is made of the same bytes, so every one of them failed in both directions at once — the shape #184 named: "a checker that greps a STRING LITERAL misses every constant and matches every comment."
#[NoCSRFRequired]— nldesign red for one rewritten docblock sentence#[NoAdminRequired]is deliberately NOT used satisfied the auth gate<img>in a JSDoc comment in<script>(launchpad)window['confirm']()run()delegating to one helper read as a stub; gate closable by$unused = 1;networkidlecounted as a use of it<NcSelect[^>]*>truncated at the>ofoption =><div @click>an element replaced, scored as that element<html>made a mount point a page root#236part 3 (gate-38 ontemplates/settings/admin.php) needs no change — #247 already fixed it; measured PASS on softwarecatalog in both arms, because that template is a fragment.One scope definition, not nine
scripts/lib/source_scope.pygeneralises the two precedents that already got this right, rather than inventing a third dialect:#opens a comment but#[opens an attribute.Every mask returns a same-length string. That is what lets a gate report a line number computed on the mask and read a suppression marker out of the original at that line — which matters because every suppression marker in this package lives in a comment.
Gate-19 keeps its own copy of the JS tokeniser (moving 180 lines out from under the suite that proves it correct is a bad trade mid-change). A drift test asserts the two byte-identical over a corpus, over this package's own
.jssources, and asserts the keyword sets equal — the corpus alone survived deleting"await"from one set, so the corpus alone was not enough.#196 ships with a declaration, not just a tightening
Admin-only is expressed in Nextcloud by the absence of an attribute, and absence is the only thing gate-5 reports.
#[NoAdminRequired]widens; so doesAuthorizedAdminSetting(to delegated admins). Closing the false negative alone would have converted a silent false negative into a permanent false positive on correct code — pipelinq alone would pull 17 routed methods into scope with no legitimate fix available. So the tightening lands with a declaration in the@spec excludefamily:Making bare absence sufficient was considered and rejected: absence is the only thing gate-5 reports, so accepting it would empty the gate completely. gate-9 (semantic-auth) still owns whether the declared posture matches the body. Documented in
hydra-gates/README.md.appinfo/routes.php— so the fleet impact is per-PR, not a one-off wave.Evidence
Planted true positives, both directions, on the real files named in the issues. Read from the runner's stdout, never its exit byte.
<img>— the tell from #235networkidleis not usedinput-labelwritten after:reduce="(o) => …"run(), one file with norun()at allNotificationDispatchJob— 530 lines, 11 private methods:class="{ clickable: … total_errors > 0 }"— the>inside the attribute value truncated the tag, so@clickwas never seen[ -d src ]gated them out of a templates-only repo; now_a11y_has_markup_dir(residual #225)#196, end-to-end on the fixture built from openconnector's
ProductSubscriptionsController: pre-fixmainreports 1 finding (analytics);subscribe(), whose only difference is a docblock sentence naming#[NoAdminRequired], passes. Post-fix reports 2. The suite asserts the count, because FAIL alone would have been FAIL before the fix too.#191, end-to-end through the runner, three arms:
#[\OCP\…\NoCSRFRequired]attribute removed* @NoCSRFRequireddocblock tag removedTests
tests/run-helper-suites.sh).test_source_scope.py(41),test_check_markup_a11y.py(25),test_check_js_call_sites.py(32),test_check_stub_run_body.py(20),test_check_csrf_removal.py(14),test_check_nc_select_labels.py(19, carried from fix(gate-12): the element ended at the arrow of:reduce="(o) => o.id"— 18 of 18 findings false on scholiq #219). Extended:test_gate_route_auth.sh(36),test_php_template_scope.py(23),test_gate_a11y_helper_wiring.sh(50).SKIPPED (wiring), never PASS (fix(gates): 16 gates reported PASS when their helper never ran — 2 are authorization gates #147, gate-17 reports 'FAIL — 0 pass-through method(s)' when python3 dies with 'Argument list too long' — the crash IS the full-tree measurement #245, fix(gate-19): read the test file with a parser, not three regexes (#234, #239, #244) #249). gate-5 additionally runs a positive control on the mask itself — a mask that silently returns its input is invisible to[ -f helper ]and would put the gate straight back into the false negative.old in sourceasserted before each: reverting each gate's fix to its pre-fix expression — the whole pre-fix behaviour, not one site — kills its suite. 7 of 7 mutants killed. Two earlier mutants onsource_scopekilled too, and one survived (deleting a regex keyword), which is why the drift test now compares the keyword sets directly.Caught by measurement before landing
<template…>(.*?)</template>ended the SFC template at the first nested slot close and deleted a real finding at openconnectorEditMapping.vue:376. Boundaries are found by depth now.const r = window.confirm('x').Closes #191, #196, #220, #224, #226, #230, #235, #266
Refs #236 (parts 1 and 2; part 3 already fixed by #247)
Supersedes #219 — its gate-12 helper and 17 tests are carried here, rebased onto current
mainand converged onto the shared scope.🤖 Generated with Claude Code