Skip to content

fix(gates): make the opt-outs reachable, stop rejecting \Throwable, end the icon deadlock - #211

Merged
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-opt-outs-and-icon-registry
Aug 7, 2026
Merged

fix(gates): make the opt-outs reachable, stop rejecting \Throwable, end the icon deadlock#211
rubenvdlinde merged 1 commit into
mainfrom
fix/gate-opt-outs-and-icon-registry

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Closes #203, #204, #205 — three defects found while landing ConductionNL/hermiq#162, where the gates blocked a PR on findings its author could neither fix nor waive.

Each fix ships with a control proving the gate can still fail. A gate that cannot fail is worth as little as one that always does.

1. Every reason-bearing opt-out was unreachable on a PR (#205)

The opt-outs read ${HYDRA_GATE_PR_BODY}, falling back to git log -1. On a pull_request, both read nothing:

  • the variable was never exported by quality.yml, so the first path was always empty
  • actions/checkout checks out the synthetic merge commit (Merge <sha> into <sha>) — never a message an author wrote

Measured on hermiq#162: the tag was placed in the PR body and in an explicit head commit; the finding count did not move either time.

quality.yml now exports HYDRA_GATE_PR_BODY and HYDRA_GATE_HEAD_SHA, and the three call sites go through one _optout_text helper — so they cannot drift apart, and a fourth gate adding an opt-out inherits the fixed behaviour instead of copying the broken pair of lines.

Control — a real author commit behind a merge commit:

git log -1        -> tag NOT found   (the bug)
_optout_text      -> tag found       (fixed)
PR body only      -> tag found

2. gate-49 rejected catch (\Throwable), which is strictly broader (#204)

It matched nine named domain exceptions. \Throwable and \Exception are supersets of all nine, so the broadest possible translation was reported as no translation at all.

The cost is not the red check — it is that the finding pushes the author toward the narrower handler: catch the one named exception, leave everything else to become a framework 500 with a stack trace. For a #[NoAdminRequired] method that trace reaches a non-admin. A gate that rejects the stronger guarantee teaches people to write the weaker one.

Control\Throwable, \Exception, DoesNotExistException now match; no-catch and an unrelated class still do not.

3. gate-55 and gate-60 demanded different icons for the same field (#203)

ADR-077 Tier A requires CogOutline for the settings concept. The widget renderer's own registry — CnWidgetGrid/widgetIcons.js, which gate-55 enforces — ships Cog and not CogOutline. Verified against the installed library rather than inferred:

grep -c CogOutline widgetIcons.js  -> 0
grep -c bCogb   widgetIcons.js  -> 2

So obeying gate-60 on a widget renders the ? fallback, and obeying gate-55 fails gate-60. Unsatisfiable — and gate-60 has no opt-out at all.

Widget icons are now exempt from the concept MUST and from nothing else: a nonexistent icon or an unbridged icon-* still fails wherever it appears, and menus keep the full Tier A vocabulary.

Control — a MENU labelled "Settings" carrying Cog still FAILS; the same value on a widget passes. The previously-deadlocked hermiq manifest goes 1 failure → 0.

The better end state is reconciling the two registries (add the Tier A glyphs to widgetIcons.js), after which this exemption can go. Until then it is the difference between a gate that is strict and one that is impossible.

Rollout note

Fix 1 touches quality.yml, which every caller consumes @main — it takes effect fleet-wide on merge. It is purely additive (two new env vars), so an older pinned gates package simply ignores them.

Fixes 2 and 3 are inside the pinned hydra-gates package, so they reach a repo only when its hydra-gates-ref moves to a tag containing them. Worth a tag and a consumer sweep, as with v1.5.0.

…nd the icon deadlock

Three defects found while landing hermiq#162, where the gates blocked a PR on
findings its author could neither fix nor waive. Each fix is verified with a
control proving the gate can still FAIL — a gate that cannot fail is worth as
little as one that always does.

1. EVERY REASON-BEARING OPT-OUT WAS UNREACHABLE ON A PR (#205)

   The opt-outs read `${HYDRA_GATE_PR_BODY}` and fell back to `git log -1`.
   Both read nothing on a pull_request:

     - the variable was never exported by quality.yml, so the first path was
       always an empty string
     - actions/checkout checks out the synthetic MERGE commit, whose message is
       `Merge <sha> into <sha>` — never a message any author wrote

   Measured on hermiq#162: the tag was placed in the PR body AND in an explicit
   head commit, and the finding count did not move either time.

   quality.yml now exports the PR body and the head SHA; the three call sites
   go through one `_optout_text` helper so they cannot drift, and a fourth gate
   adding an opt-out inherits the fixed behaviour instead of copying the broken
   pair of lines. Control: with a real author commit behind a merge commit,
   `git log -1` does NOT see the tag and `_optout_text` does.

2. GATE-49 REJECTED catch (\Throwable), WHICH IS STRICTLY BROADER (#204)

   The gate matched nine named domain exceptions. `\Throwable` and `\Exception`
   are supersets of all nine, so the broadest possible translation was reported
   as no translation at all.

   The cost is not the red check. The finding pushes the author toward the
   NARROWER handler — catch the one named exception, leave everything else to
   become a framework 500 with a stack trace, which for a #[NoAdminRequired]
   method reaches a non-admin. A gate that rejects the stronger guarantee
   teaches people to write the weaker one.

   Control: \Throwable, \Exception and DoesNotExistException now match; a method
   with no catch, and a catch of an unrelated class, still do not.

3. GATE-55 AND GATE-60 DEMANDED DIFFERENT ICONS FOR THE SAME FIELD (#203)

   ADR-077 Tier A requires "CogOutline" for the `settings` concept. The widget
   renderer's own registry — CnWidgetGrid's widgetIcons.js, which gate-55
   enforces — ships "Cog" and NOT "CogOutline". Verified against the installed
   library rather than inferred:

       grep -c CogOutline widgetIcons.js -> 0
       grep -c '\bCog\b'   widgetIcons.js -> 2

   So obeying gate-60 on a widget renders the "?" fallback and obeying gate-55
   fails gate-60. Unsatisfiable, and gate-60 has no opt-out at all.

   Widget icons are now exempt from the concept MUST, and from nothing else: a
   nonexistent icon or an unbridged `icon-*` still fails wherever it appears,
   and menus keep the full Tier A vocabulary. Control: a MENU labelled
   "Settings" carrying "Cog" still FAILS, while the same value on a widget
   passes.

   The better end state is reconciling the two registries — add the Tier A
   glyphs to widgetIcons.js — after which the exemption can go. Until then this
   is the difference between a gate that is strict and one that is impossible.
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Correction to the rollout note in the description, checked after merging.

I wrote that fixes 2 and 3 sit in the pinned package and would need "a tag and a consumer sweep". That is no longer true. The fleet has since dropped hydra-gates-ref entirely — every repo I checked (openregister, opencatalogi, openconnector, docudesk, procest, pipelinq, openbuild, nldesign) now has no pin and tracks @main, with a comment in each code-quality.yml explaining why:

A pin is a silent expiry date: 22 repos sat on v1.0.1 and 16 gates were dead fleet-wide while every one reported PASS (#159), and a default flipped at @main later reached those old runners and made them red on gates they had no subject matter for (#173).

So all three fixes reach every repo immediately — no sweep required, and the earlier v1.5.0 sweep is superseded by the better arrangement.

I did cut v1.6.0 at the merge commit before establishing this. It is harmless and verified to carry both package-side fixes (_optout_text ×5, in_widget ×6), so it remains available for anyone who deliberately pins with the documented hydra-gates-ref input. Nothing depends on it.

Worth recording: my first pin survey returned "none" for all 15 repos and I assumed my decode had broken. It had not — the data had changed. Checking before acting on that assumption is what surfaced the arrangement.

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.

gate-55 and gate-60 encode two different icon registries for the same widget field — 'settings' is unsatisfiable

1 participant