Skip to content

feat(gates): add gate-105, duplicate-page-refresh - #657

Merged
rubenvdlinde merged 6 commits into
mainfrom
feat/gate-104-duplicate-page-refresh
Sep 1, 2026
Merged

feat(gates): add gate-105, duplicate-page-refresh#657
rubenvdlinde merged 6 commits into
mainfrom
feat/gate-104-duplicate-page-refresh

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What

Every surface built on CnDashboardPage or CnDetailPage already ships a Refresh: CnActionsMenu renders it as the first item of the page-level Actions menu, and showRefresh defaults to true. A surface that declares a Refresh of its own therefore ships two, doing the same thing.

Measured across the fleet on 2026-08-31, after a user reported the dossiq dashboard showing Refresh as a toolbar button and as the first Actions-menu item. Five surfaces had it:

App Surface Source
dossiq Dashboard manifest "type": "refresh" headerAction
hermiq Dashboard manifest "type": "refresh" headerAction
larpinq Dashboard manifest "type": "refresh" headerAction
opencatalogi Dashboard #actions slot button
shillinq BBV compliance #header-actions slot button

All five are fixed in sibling PRs. This is what keeps them fixed.

Why a gate

Nothing downstream could catch it. check:manifest validates against a JSON Schema, and a schema cannot know that the component rendering the page contributes an item of its own. The only detector was a person counting buttons on screen.

The remedy names the listener, not just the deletion

On both Vue apps the hand-written button called a host method that was not subscribed to cn:page:refresh, so the menu item did nothing. Removing the button alone would have left only the dead Refresh. The gate says so.

openregister passes because it opts out with showRefresh: false while keeping its own button. That stays a supported shape; shipping both does not.

Full-tree rather than diff-scoped, for the reason gates 84 and 93 through 96 give.

Two load-bearing test arms

  • Arm 5 plants Refresh markup inside an HTML comment, the exact shape this gate's own remedy leaves behind. Proven load-bearing: with comment stripping disabled it returns rc=1 on a clean file.
  • Arm 6 puts an NcButton's nested <template #icon> between the slot opening and the Refresh label, so a scan matching to the nearest </template> reads an empty slot and reports clean over a real duplicate.

Verified

  • 9 of 9 acceptance arms green; the suite is auto-discovered by run-helper-suites.sh
  • bash tests/run-helper-suites.sh: exit 0, whole package still green
  • check_gate_numbers_unique.sh: 84 gates, every number claimed once
  • Full runner on a planted pre-fix tree: [gate-104] duplicate-page-refresh: FAIL
  • Full runner on the fixed dossiq tree: [gate-104] duplicate-page-refresh: PASS, COVERAGE 72 of 84
  • Swept all 21 fleet apps: no other app fails, and only nldesign and versioniq report empty scope

Conduction Release Bot added 6 commits August 31, 2026 23:11
Every surface built on CnDashboardPage or CnDetailPage already ships a
Refresh. CnActionsMenu renders it as the first item of the page-level Actions
menu, and showRefresh defaults to true. A surface that declares a Refresh of
its own therefore ships two, doing the same thing.

Measured across the fleet on 2026-08-31, after a user reported the dossiq
dashboard showing Refresh as a toolbar button and again as the first
Actions-menu item. Five surfaces had it: dossiq, hermiq and larpinq through a
manifest "type": "refresh" headerAction, opencatalogi and shillinq through a
hand-written button in the #actions or #header-actions slot. All five are
fixed; this is what keeps them fixed.

Nothing downstream could catch it. check:manifest validates against a JSON
Schema, and a schema cannot know that the component rendering the page
contributes an item of its own. The only detector was a person counting
buttons on screen.

The remedy names the @refresh listener, not just the deletion. On both Vue
apps the hand-written button called a host method that was not subscribed to
cn:page:refresh, so the menu item did nothing: removing the button alone
would have left only the dead Refresh.

Full-tree rather than diff-scoped, for the reason gates 84 and 93 through 96
give. openregister passes because it opts out with showRefresh: false, which
stays a supported shape.

The acceptance suite carries two load-bearing arms. Arm 5 plants Refresh
markup inside an HTML comment, the shape this gate's own remedy leaves
behind, and fails without comment stripping. Arm 6 puts an NcButton's nested
<template #icon> between the slot opening and the Refresh label, so a scan
that matched to the nearest </template> would read an empty slot and report
clean over a real duplicate.
src/manifest.d/*.json is merged into the manifest at runtime by
require.context, so a checker that opens only src/manifest.json is blind to
whatever the fragments add. Eight fleet apps use them and shillinq ships 87,
which is the same reason gate-96 reads them.

The checker already did. Nothing asserted it, so nothing would have caught a
refactor that stopped.
ShellCheck failed the build with four SC2016 notes. The fixtures wrote Vue
markup as single-quoted arguments and used backticks inside them to dodge
nested quoting, and a backtick in single quotes reads as command
substitution. The workflow's wrapper exits 1 on any finding, note severity
included.

`_vue` now takes the body on stdin from a quoted heredoc, so the fixtures can
use the real `t('app', 'Dashboard')` form the components actually carry.

All ten arms stay green, and arm 5 still holds the `<Refresh>` tag and the
quoted label that make it load-bearing.
The coverage ratchet in test_gate_acceptance_matrix.sh caught this, in its own
words: "gate-104 is DECLARED by the runner but has neither a planted/clean
fixture nor a reasoned row in UNCOVERED.md. A gate can be added to the runner
and never tested; this is that moment." The dedicated checker suite tests the
checker; this drives the whole runner over a real tree.

Two rows, because the defect has two shapes in two file kinds and each is
independently fatal: a "type": "refresh" headerAction in src/manifest.json,
and a Refresh button in a .vue #actions slot. Three fleet apps had the first
and two had the second, so a manifest-only fixture would pass a Vue-blind
checker.

The clean arm is not merely Refresh-free. It keeps three near-misses a widened
gate would turn red, each a shape the fleet actually ships: two surfaces that
keep their own Refresh and stand the menu item down with showRefresh: false
(openregister's dashboard, the exception the rule genuinely has); an api-call
action carrying "icon": "Refresh" (shillinq's renew-consent); and an HTML
comment quoting the removed markup, which is what this gate's own remedy
writes on every surface it touches.

Matrix: 211 passed, 0 failed. Fixtured gates 76 -> 77.
reports-one-page took 104 first, from a branch cut in parallel with this one.
check_gate_numbers_unique.sh caught it on the merge: "gate-104 is claimed by:
duplicate-page-refresh reports-one-page".

104 was genuinely free when this branch was cut, which is the point. A gate
number has to be read off the CALL SITES on the CURRENT main, not off the
`# GATE <n>` comments and not off the highest number anyone remembers, and not
once at the start: another branch can take it while yours is open.

The renumber is anchored on the gate NAME, not on the bare string 104. The
runner mentions 104 sixteen times, six of them reports-one-page's own call
sites and two unrelated (a count of @SPEC targets, and a list of gate ids), so
a blanket replace would have corrupted the gate that just landed.

  gate numbers unique:  85 gates, every number claimed once
  checker suite:        10 of 10 arms green
  acceptance matrix:    220 passed, 0 failed, 78 of 85 gates fixtured
  both gates coexist:   104 reports-one-page, 105 duplicate-page-refresh
@rubenvdlinde rubenvdlinde changed the title feat(gates): add gate-104, duplicate-page-refresh feat(gates): add gate-105, duplicate-page-refresh Sep 1, 2026
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Renumbered 104 → 105: reports-one-page took 104 first, from a branch cut in parallel with this one, and check_gate_numbers_unique.sh caught it on the merge (gate-104 is claimed by: duplicate-page-refresh reports-one-page). 104 was genuinely free when this branch was cut, which is the point: the free number has to be read off the call sites on the current main, not once at the start.

The renumber is anchored on the gate name, not the bare string 104 — the runner mentions 104 sixteen times, six of them reports-one-page's own call sites, so a blanket replace would have corrupted the gate that just landed.

Also added since the first review pass:

  • the planted/clean acceptance bundle the coverage ratchet asked for (test_gate_acceptance_matrix.sh had flagged that gate-105 was declared by the runner but never driven end-to-end)
  • arm 10, covering a duplicate declared only in a src/manifest.d/*.json fragment
  • ShellCheck fix: the Vue fixtures now come from quoted heredocs rather than single-quoted arguments with backticks (4 × SC2016)

Verified locally:

gate numbers unique:  85 gates, every number claimed once
checker suite:        10 of 10 arms green
acceptance matrix:    220 passed, 0 failed, 78 of 85 gates fixtured
shellcheck:           exit 0 (docker, against this repo's .shellcheckrc)
both gates coexist:   104 reports-one-page, 105 duplicate-page-refresh

@rubenvdlinde
rubenvdlinde merged commit 14c4b31 into main Sep 1, 2026
40 checks passed
@rubenvdlinde
rubenvdlinde deleted the feat/gate-104-duplicate-page-refresh branch September 1, 2026 07:30
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