fix: gate hygiene sweep — gate-29/46/51/53/54/58 - #776
Merged
Conversation
…commit) The file is gitignored but was still tracked from before the ignore rule landed. Untrack it so the gate's tracked-file-behind-an-ignore-rule check passes.
…chor-existence) - woo-publication design.md: add a resolvable (Fallback) anchor tag to the "Fallback: catalog discovery is best-effort" heading - DigidSamlAdapterInterface / LogDigidSamlAdapter: retarget from the archived zaakportaal-01-schema-foundation change to the canonical zaakportaal-mijngemeente spec's DigiD/eHerkenning requirement - processMiningApi.js / processMiningShaping.js: retarget from the now-archived process-mining-bottlenecks change tasks to its synced canonical spec - RedactionAssistDialog.vue / DocumentAssessmentTable.vue: retarget the 10 woo-llm-anonymisation change-task references to the canonical spec's human-review requirement (the one still-valid #task-4-1 reference is left untouched)
…-property-titles) 23 schema properties across procest_register.json (caseFederatedActivity entries, casetransfer custodyAuditTrail) and 70-cmmn-case-model.json (CMMN plan-item / sentry structure: type, name, description, entry/exit criteria id/standardEvent/caseFileEvent/field/operator/value) were missing a human-friendly description. Added real, CMMN/domain-grounded descriptions rather than placeholder text.
…lation-dialect) - case.decisions: $ref "Decision" -> "decision". Verified against decidesk's real lib/Settings/decidesk_register.json: the schema's JSON key is "Decision" but its declared `slug` is "decision", and a $ref resolves against the slug (per the gate's own documented reasoning). - case.vergunningaanvraagRef: was relation-shaped (format:uuid + a relation description) but had no $ref at all. Added $ref:"vergunningaanvraag" + x-external-register:"dso", verified against openregister/lib/Settings/dso_register.json (schema key and slug are both "vergunningaanvraag", hosted in openregister's own "dso" mock register) and matching the file-name-implies-register-name convention already used by the pipelinq/decidesk external refs in this file. caseType.productsOrServices's $ref:"product" is left unchanged — verified against pipelinq's real register (key and slug both "product", matching ADR-003 verbatim). That finding and the (now correct but still externally-unresolvable) vergunningaanvraagRef finding remain red: the checker's schema-key candidate set is built only from the current app's own register files, so a genuinely-external cross-app/cross-register slug can never resolve from procest's side. Not a defect.
…ate-53 effective-manifest-crossref) - SubstitutionMenu (route /substitution) was wrongly removed as a "duplicate" of SubstitutionAdminMenu (/substitution-admin) in 01c1a89. They are different pages for different roles: SubstitutionSettings is the self-service page every user manages their own substitutions on; SubstitutionAdmin is the coordinator-only console listing all substitutions. Non-coordinators had no menu path to their own self-service page. Restored. - Deleted 8 removals entries that matched no merged menu entry at all (dead cruft from earlier portal/inspection retirements): BezwaarDecisions, BezwaarAdviceRequests, Subsidies, BesluitvormingGroup, LeverancierDashboard, MijnZaken, MijnNotificaties, Inspecties. 8 findings remain intentionally red (BezwaarBeroepGroup, Bezwaren, Beroepen, SubsidiesGroup, CaseMap, Voorstellen, Advice, BesluitvormingAgenda) — each is a deliberate, documented retirement (case-type-navigation commit d6824aa; consume-decidesk-besluitvorming-leaf commit d2df51b) superseded by a runtime surface (Cases' folderSidebar + map viewMode, CaseDetail's BesluitvormingLeafTab) that this gate's static .menu-tree walk cannot see. Restoring them as static entries would reintroduce the exact anti-pattern both changes explicitly rejected; deleting their routes would violate each change's own ADR-044 hard invariant to keep them deep-link/e2e routable. Documented in _meta.removalsCoverageNote for the next reader.
… e2e-networkidle)
networkidle never settles on Nextcloud (NC's notification poll keeps a
request in flight for the whole session), so all 3 waits silently burned
their entire timeout budget behind a .catch(() => {}). Replaced with
waitUntil:'domcontentloaded' + explicit element waits:
- deelzaak-case-email.spec.ts: the goto is immediately followed by real
URL/text assertions with their own timeouts, so the networkidle wait
was redundant on top of them.
- docs-screenshots.spec.ts (2 sites): ported the app's own established
technique from tests/e2e/visual/_visual-helpers.ts's
waitForContentReady() — wait on the actual content region (main /
.app-content / #content-vue, or #content/.section for the NC core
admin-settings page), then let any loading spinner clear.
rubenvdlinde
requested review from
Rem-Dam,
SudoThijn,
WilcoLouwerse,
bbrands02,
remko48 and
rjzondervan
as code owners
August 9, 2026 15:26
Contributor
Quality Report — ConductionNL/procest @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| check-vue3-compile | ✅ | ||||
| test-l10n | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 552/552 | |||
| PHPUnit | ✅ | ||||
| Newman | ⏭️ | ||||
| Playwright | ❌ | ||||
| Hydra gates | ❌ |
Quality workflow — 2026-08-09 15:57 UTC
Download the full PDF report from the workflow artifacts.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
PR #776 added "x-external-register" next to three cross-register $refs as part of a gate-54 fix. Measured, it is inert in both directions: - OpenRegister does not read it: no occurrence of x-external-register anywhere under openregister/lib/. - gate-54 does not read it either: check_relation_dialect.py resolves a $ref against _global_schema_keys(), the union of schema KEYS and declared SLUGS across *register*.json and register.d/*.json in the same settings dir. There is no external-register branch. Proof it changed nothing: gate-54 reports the same 2 findings with and without the key. The 'decision' $ref in that commit was a REAL fix and is untouched — "Decision" -> "decision" resolves against decidesk's declared slug. Only the no-op key is removed; an unread key introduced under a 'fix(relations)' subject reads like a mechanism and is not one. The remaining 2 findings STAY RED, honestly: caseType.productsOrServices -> 'product' (Pipelinq's product register) and case.vergunningaanvraagRef -> 'vergunningaanvraag' (the DSO register) both name schemas owned by another app. Neither is in procest's 85-schema register set, so the gate cannot resolve them and has no dialect that would let it. Both properties are live — 6 and 7 code references respectively — so deleting them would break working relations, and adding foreign schemas to procest's own register would misstate ownership. Each property's description already documents the owning register. This needs a cross-register $ref dialect in the gate, not a change here.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
…laced Found while verifying the gate-53 rationale merged in #776, which states that the standalone Besluitvorming nav (Voorstellen / Advice / Agenda) was 'replaced with a BesluitvormingLeafTab sidebar tab on CaseDetail' and that the replacement is 'live, wired, and menu-reachable'. The first half was true; the second was not. Measured: - src/components/tabs/BesluitvormingLeafTab.vue exists. - src/registry.js:412 registers it, in the same shape as CaseNotesTab. - CaseDetail.config.sidebar.tabs listed audit, version-history, notes, sharing, email — and nothing else. So the component was resolvable by name and rendered by nobody. The consume-decidesk-besluitvorming-leaf change removed three menu entries and registered a tab it never placed: users lost the nav and gained no replacement. Being in the registry is availability, not placement — the manifest decides what renders, and it did not mention this tab. The component's own header says it is meant to be surfaced 'through the path', which is exactly the wiring added here, and it already handles decidesk being absent by rendering a quiet unavailable notice rather than a broken tab. Icon 'Gavel' is registered in src/icons.js, so gate-60 stays green (28 manifests, 0 failures). This does NOT move gate-53: still the same 8 findings, because that gate walks the static .menu tree and a per-object sidebar tab is not in it. The point is the lost surface, not the gate — the gate is what led me to look.
rubenvdlinde
added a commit
that referenced
this pull request
Aug 9, 2026
* fix(a11y): gate-32 semantic-controls — add keyboard equivalents to click targets Non-semantic elements (div/li/a) with @click but no role/tabindex/keyboard handler are unreachable and inoperable for keyboard and screen-reader users (WCAG 2.1.1 / 4.1.2). Adds role="button" tabindex="0" plus real @keydown.enter / @keydown.space handlers that call the same action as the click handler — not decorative attributes. Modal/dialog backdrop overlays (click-outside-to-close) get the same treatment so the dismiss action has a keyboard equivalent. The six "View all" widget footer links move from a bare <a @click.prevent> to a real <a :href> pointing at the actual destination URL (computed via generateUrl), which is both a genuinely better fix (native middle-click / open-in-new-tab support) and clears the gate since a real href exempts an <a> from the semantic-controls check. TaskCreateDialog.vue additionally gets an aria-label on its icon-only close button (gate-39 button-name, same file). gate-32: 40 findings -> 0. * fix(a11y): gate-39 button-name — add accessible names to icon-only buttons Icon-only NcButton/button elements with no aria-label/title/text content announce as just "button" to screen readers (WCAG 2.2 AA SC 4.1.2). Adds a translated aria-label naming the actual action ("Edit {name}", "Delete {name}", "Remove guard", "Close step configuration", etc.) rather than a generic label, using the row's own name field for specificity where one is available. gate-39: 24 findings -> 0. * fix(a11y): gate-34 window-confirm — replace window.confirm() with CnConfirmDialog Native window.confirm() calls break Nextcloud theming and are inaccessible to assistive tech (they bypass the app's dialog stack entirely). Replaces all three call sites with @conduction/nextcloud-vue's shared CnConfirmDialog (NcDialog-wrapped, its own file in the design-system package — importing it does not trigger gate-13 modal-isolation since no <NcModal>/<NcDialog> tag is written inline in these components). Each site now opens the dialog on the original trigger, performs the delete in a `@confirm` handler, and reports success/failure back via setResult() instead of a bare console.error / silent catch. These three files also carry their gate-43 table-headers (<th scope=>) and, for DeelzaakList.vue, gate-45 prefers-reduced-motion fixes — committed together since they land in the same file. gate-34: 3 findings -> 0. * fix(a11y): gate-43 table-headers — add scope= to <th> elements <th> without a scope declaration leaves screen readers unable to associate data cells with their column/row header (WCAG 2.2 AA SC 1.3.1). Adds scope="col" to header-row <th> cells and scope="row" to the row-header <th> in LhsMatrixAdmin.vue's severity/behavior matrix table. ComplaintAnalyticsDashboard.vue's "By category" table had no <th> at all (rule=table-without-th) — adds a real <thead> naming the two actual columns (Category, Count) instead of a header-less table. Self-closed spacer/action <th /> columns (row-actions menus, drag handles) are left alone — they carry no accessible name so scope= on them is inert, per the gate's own exemption for headers with no name. gate-43: 27 findings -> 0. * fix(a11y): gate-45 prefers-reduced-motion — add reduced-motion fallbacks <style> blocks with a transition/animation and no @media (prefers-reduced-motion: reduce) override force motion on users who have asked the OS to minimise it (WCAG 2.2 SC 2.3.3). Adds a media query per affected selector that sets transition/animation: none — a real override, not an empty block satisfying the gate's text match. Skeleton-loading shimmer keyframe animations and hover/click transitions both get the treatment. The remaining ~17 files flagged by this gate had their reduced-motion fix land in an earlier commit because they were also touched for gate-32/39/43 in the same file. gate-45: 25 findings -> 0. * fix(a11y): gate-55 detail-page-discipline — use a registered manifest icon AiOversightDetail's data widget declared icon "RobotOutline", which is not in the shared icon registry (nextcloud-vue CnWidgetGrid/widgetIcons.js) and renders the '?' fallback per ADR-062 rule 8. Swaps it for "ShieldCheckOutline" — already in the registry and semantically fitting for an AI-oversight / human-review audit entry (EU AI Act Art. 14 evidence). gate-55: 1 finding -> 0. The gate-16 spec-coverage regression this branch introduced (8 new frontend methods/computed properties added while fixing gate-34/32 lacked @SPEC tags) was fixed in the earlier gate-32/gate-34 commits by giving each new method the same @SPEC tag as its sibling methods in the same file. * fix(l10n): add the 14 strings the a11y pass introduced, EN and NL The accessible names and the CnConfirmDialog copy added for gate-39/gate-34 were new source strings, and neither l10n file had them. That failed two CI jobs the gate suite does not see — 'l10n coverage (en.json)' and 'quality / Frontend Check (test:l10n)' — and would have shipped raw English keys to a Dutch-locale user. en.json extracted with the repo's own 'test:l10n:write'; nl.json translated by hand. 'Remove guard' is 'Voorwaarde verwijderen' rather than a literal 'bewaker' — in the transition editor a guard is a precondition on the transition, not a person. node tests/l10n/check-l10n.js now reports both halves OK; before this commit it reported 14 keys missing from en.json and then 4 missing Dutch translations. * fix(spec): retarget the signalering-widgets @SPEC tag at its canonical spec All 36 tags pointed at openspec/changes/retrofit-2026-05-24-signalering-widgets/tasks.md, a change directory that was archived to openspec/changes/archive/2026-05-31-retrofit-2026-05-24-signalering-widgets. The target has not existed for months, which is why gate-46 reports them as unresolved. Retargeted at openspec/specs/signalering-widgets/spec.md — the canonical home, which exists — per the rule that a @SPEC tag names a spec, never a change dir. 30 of the 36 predate this branch; the a11y pass added 6 more by copying the surrounding docblock. Diff-scoping means touching these files makes all of them this PR's to answer for, and CI reported exactly that: 10 unresolved findings from 1 distinct target. Measured, not assumed: after this commit no @SPEC target under src/ named signalering-widgets is missing from disk.⚠️ NOT fixed here, and not this PR's: 48 OTHER @SPEC targets under src/ are also missing — mostly archived retrofit-2026-05-* change dirs, plus two that name a canonical spec which was never written (openspec/specs/process-mining-bottlenecks/spec.md, openspec/specs/realtime-updates-ui/spec.md). They are diff-scoped out today and will surface on the next PR that touches each file. That is a separate sweep, and the two missing canonical specs need writing, not retargeting. * fix(register): drop x-external-register — nothing reads it PR #776 added "x-external-register" next to three cross-register $refs as part of a gate-54 fix. Measured, it is inert in both directions: - OpenRegister does not read it: no occurrence of x-external-register anywhere under openregister/lib/. - gate-54 does not read it either: check_relation_dialect.py resolves a $ref against _global_schema_keys(), the union of schema KEYS and declared SLUGS across *register*.json and register.d/*.json in the same settings dir. There is no external-register branch. Proof it changed nothing: gate-54 reports the same 2 findings with and without the key. The 'decision' $ref in that commit was a REAL fix and is untouched — "Decision" -> "decision" resolves against decidesk's declared slug. Only the no-op key is removed; an unread key introduced under a 'fix(relations)' subject reads like a mechanism and is not one. The remaining 2 findings STAY RED, honestly: caseType.productsOrServices -> 'product' (Pipelinq's product register) and case.vergunningaanvraagRef -> 'vergunningaanvraag' (the DSO register) both name schemas owned by another app. Neither is in procest's 85-schema register set, so the gate cannot resolve them and has no dialect that would let it. Both properties are live — 6 and 7 code references respectively — so deleting them would break working relations, and adding foreign schemas to procest's own register would misstate ownership. Each property's description already documents the owning register. This needs a cross-register $ref dialect in the gate, not a change here. * fix(case): wire BesluitvormingLeafTab — it was registered but never placed Found while verifying the gate-53 rationale merged in #776, which states that the standalone Besluitvorming nav (Voorstellen / Advice / Agenda) was 'replaced with a BesluitvormingLeafTab sidebar tab on CaseDetail' and that the replacement is 'live, wired, and menu-reachable'. The first half was true; the second was not. Measured: - src/components/tabs/BesluitvormingLeafTab.vue exists. - src/registry.js:412 registers it, in the same shape as CaseNotesTab. - CaseDetail.config.sidebar.tabs listed audit, version-history, notes, sharing, email — and nothing else. So the component was resolvable by name and rendered by nobody. The consume-decidesk-besluitvorming-leaf change removed three menu entries and registered a tab it never placed: users lost the nav and gained no replacement. Being in the registry is availability, not placement — the manifest decides what renders, and it did not mention this tab. The component's own header says it is meant to be surfaced 'through the path', which is exactly the wiring added here, and it already handles decidesk being absent by rendering a quiet unavailable notice rather than a broken tab. Icon 'Gavel' is registered in src/icons.js, so gate-60 stays green (28 manifests, 0 failures). This does NOT move gate-53: still the same 8 findings, because that gate walks the static .menu tree and a per-object sidebar tab is not in it. The point is the lost surface, not the gate — the gate is what led me to look. * fix(register): express the two cross-app relations in the x-external-register dialect (gate-54 2 -> 0) .github#286 landed after my earlier commit and adds exactly the notion that was missing: check_relation_dialect.py now has _is_external_ref(), so a property owned by another app's register is expressible. Verified against a fresh clone of ConductionNL/.github@main, package 365fa31d09a26f980e6dc76cb0800575ef005a4e — `_is_external_ref` exists at lib/check_relation_dialect.py:341. It did NOT exist at 651e5c5, which is why my previous commit removed the key as inert. That measurement was correct at the time and is now superseded. The dialect is `x-external-register: <app>` on the property, carrying the BARE identifier, and NO $ref — the checker still reports a property that declares both, because OpenRegister resolves $ref within one register set and can never reach another app's schema: caseType.productsOrServices x-external-register: pipelinq, items lose $ref case.vergunningaanvraagRef x-external-register: dso, loses $ref case.decisions is deliberately NOT given the key: `decision` is one of procest's own 85 schemas and resolves locally, so it is not a cross-app reference. Adding the key there would now be a finding rather than a fix. Bidirectional proof, same checker and invocation both ways: as committed -> 0 findings $ref put back on vergunningaanvraagRef -> "$ref 'vergunningaanvraag' does not resolve to a schema key in the register set (case-exact)" restored -> 0 findings Both properties stay live — 6 and 7 code references — and their descriptions already name the owning register.
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.
Summary
Fixes manifest/register-JSON/spec-anchor/e2e-hygiene gate findings on procest per the fleet gate-hygiene sweep (gate package
48c88ba1e0d049f8f38538c33e790d3e603c55d0)..phpunit.result.cachewas both gitignored and tracked. Removed from the index.@spectargets (15 findings) — see gate-by-gate notes below.procest_register.jsonfederated-activity/casetransfer audit trails,70-cmmn-case-model.jsonplan-item/sentry structure).SubstitutionMenuwrongly treated as a duplicate of the admin-onlySubstitutionAdminMenu— they are different pages for different roles); deleted 8 deadremovalsentries that matched no menu item at all. 8 findings remain intentionally red — see below.case.decisions's$refcasing (verified against decidesk's real register: schema keyDecision, slugdecision— the$refmust address the slug) and added the missing$ref/x-external-registertocase.vergunningaanvraagRef(verified againstopenregister/lib/Settings/dso_register.json). 2 findings remain intentionally red — see below.waitForLoadState('networkidle')calls withwaitUntil: 'domcontentloaded'+ deterministic element waits (reusing the repo's own establishedwaitForContentReadytechnique fromtests/e2e/visual/_visual-helpers.ts).gate-53 — per-removal decisions
src/menu-layout.json#removalsoriginally had 17 entries. Verified withcheck_manifest_crossref.jsdirectly (not just the wrapper script) before and after every edit.Fixed (1):
SubstitutionMenu → SubstitutionSettings— restored. Commit01c1a8905removed it as a "duplicate" ofSubstitutionAdminMenu, but the two pages are genuinely different:SubstitutionSettings(/substitution) is the self-service page every user manages their own substitutions on;SubstitutionAdmin(/substitution-admin) is the coordinator-only console listing all substitutions. A non-coordinator user had no menu path to their own self-service page at all. Confirmed via each page's own manifest_note.Deleted as dead cruft (8) — matched no merged menu entry at all (
removal '<id>' matches no merged menu entry (stale removal), WARN-only, per direct tool output):BezwaarDecisions,BezwaarAdviceRequests,Subsidies,BesluitvormingGroup,LeverancierDashboard,MijnZaken,MijnNotificaties,Inspecties.Left red, intentionally (8) —
BezwaarBeroepGroup,Bezwaren,Beroepen,SubsidiesGroup,CaseMap,Voorstellen,Advice,BesluitvormingAgenda. Each is a deliberate, extensively documented retirement across two sequential architecture changes, both still live and working:case-type-navigation(commitd6824aa20,openspec/changes/case-type-navigation/) replaced the standalone Bezwaar/Beroep/Subsidie nav with case-type children resolved onto theCasespage's ownconfig.folderSidebar(verified present insrc/manifest.json), and gaveCasesaviewModes: [...,"map"]+mapConfigthat supersedes the standaloneCaseMapleaf.consume-decidesk-besluitvorming-leaf(commitd2df51b8b) replaced the standalone Besluitvorming nav (Voorstellen/Advice/Agenda) with aBesluitvormingLeafTabsidebar tab onCaseDetail(verified wired insrc/registry.js).Both replacements are real, shipped, and menu-reachable via
Cases/CaseDetail— but gate-53'sremovals-invariantcheck only walks the static.menutree (collectMenuRoutes) and has no way to see a runtimefolderSidebarfilter, a pageviewMode, or a per-object sidebar tab as "reachability". I deliberately did not:I documented this in
src/menu-layout.json#_meta.removalsCoverageNote(plain prose, not a gate suppression — it doesn't affect any check) so the next reader doesn't have to re-derive it.WARN, verified legitimate, no change: widget
case-kpis-hoursonCaseDetailreferences registershillinqschemaUrenRegistratie.shillinqis a real, separate Conduction app (a time-registration system) — this is a genuine, working cross-app OpenRegister integration the gate can't statically verify from a single-repo scope, not a defect.gate-54 — verified against the real external registers
I have local checkouts of
decidesk,pipelinqandopenregisteravailable and read their actual register JSON directly rather than guessing:caseType.productsOrServices→$ref: "product"— verified againstpipelinq/lib/Settings/pipelinq_register.json: schema key and slug are bothproduct. This is already correct and matches ADR-003 verbatim. Left unchanged, left red — this is gate-54's_global_schema_keys()only ever collecting the current app's own register files; a genuinely-external cross-app slug can never resolve from procest's side. Not a defect.case.decisions→$ref: "Decision"→ fixed to"decision". Verified againstdecidesk/lib/Settings/decidesk_register.json: the schema's JSON key isDecisionbut itsslugisdecision, and$refresolves against the slug (check_relation_dialect.py's own documented reasoning: "a $ref is resolved against the slug, not against the key"). Now resolves.case.vergunningaanvraagRef→ added"$ref": "vergunningaanvraag"+"x-external-register": "dso". Verified againstopenregister/lib/Settings/dso_register.json: schema key and slug are bothvergunningaanvraag, hosted in openregister's owndsomock register (dso_register.json, matching thepipelinq_register.json/decidesk_register.jsonfile-name-implies-register-name convention already used by the other twox-external-registervalues in this file). Still red for the same single-repo-scope reason asproductsOrServices— genuinely correct, gate structurally cannot verify a cross-register slug.gate-52 verdict: artefact, not a real defect
Measured directly: full-repo run → gate-52 PASS.
--scope-to-diff --base <repo-root-commit>→base=0 head=3 delta=+3, FAIL. The ratchet is diffing the entire repository's history back to its root commit — every custom widget that has ever existed in the repo reads as "+3 growth" against a base of zero, because the root commit predates all of them. This is an artefact of the base ref choice (root commit), not evidence this PR added anything. No code change made.Can-fail proofs
For gate-29, gate-46, gate-51, gate-53, gate-54 and gate-58 I reverted one representative fix, re-ran the relevant checker directly, confirmed the finding reappeared, then reapplied the fix and reconfirmed clean. (gate-52 had no code change, so no proof needed there.)
Test plan
check_spec_anchors.py— 0 findings across all touched files (was 15)check_schema_property_meta.py— 0 findings (was 23)check_manifest_crossref.jsagainst the freshly-built effective manifest — 8 documented findings remain (was 9), gate-22-equivalent schema validation still PASScheck_relation_dialect.py— 2 documented findings remain (was 3)check_js_call_sites.py --rule networkidle— 0 findings (was 3)git ls-files | grep phpunit.result.cache— emptyrun-hydra-gates.shbefore/after comparison, no regressions on previously-PASS gates