diff --git a/hydra-gates/scripts/lib/test_gate_45_selector_parity.sh b/hydra-gates/scripts/lib/test_gate_45_selector_parity.sh new file mode 100644 index 00000000..148d5759 --- /dev/null +++ b/hydra-gates/scripts/lib/test_gate_45_selector_parity.sh @@ -0,0 +1,515 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: EUPL-1.2 +# +# test_gate_45_selector_parity.sh — gate-45 must judge the SELECTORS that carry +# motion, not the presence of a reduced-motion block somewhere in the file. +# +# WHAT THIS GUARDS (thematiq#604, 2026-09-17) +# -------------------------------------------- +# Until 2026-09-21 the checker asked one question per file: is there a +# `@media (prefers-reduced-motion …)` block in it? thematiq's +# css/systems/nldesign/theme.css had one, so the gate said PASS — at `6dcbbaf9`, +# where `c495c99c` had added `:not(.action-button)` to four `!important` motion +# selectors while the reset kept the bare `.button-vue, button, .button`. Equal +# specificity before (reset wins on source order); strictly less after (reset +# loses on every button). The gate said PASS again at `67caeb85`, after the +# selectors were mirrored back. +# +# Measured 2026-09-21 with the package runner at full scope, one variable: +# theme.css @ 6dcbbaf9 (reset no longer names the motion selectors) PASS <- the defect +# theme.css @ 67caeb85 (reset names them again) PASS +# +# Same verdict on the broken and the fixed file: the gate did not observe the +# property it is named after. The arms below are that file's shape reduced to +# its cascade essentials, plus the anti-widening controls for every correct +# idiom the fleet writes — the change turns a file-level question into a +# per-selector one, and that is precisely the kind of change that turns a gate +# into a noise generator if the controls are not pinned. +# +# P1 the thematiq shape: !important motion on `.btn:not(.x)`, reset on bare `.btn` → FAIL, names the selector +# P2 CONTROL: the same file with the reset mirroring the selectors → PASS +# P3 a LESS specific guard covers a plain motion when the guard is !important → PASS (.card covers .card:hover) +# P3b …and does NOT when both are plain: `.card:hover` beats `.card` by specificity → FAIL +# P4 a universal `*` reset covers plain motion (ARM 6 of the scope suite) → PASS +# P4b …and does NOT cover !important motion: `*` has specificity 0 → FAIL +# P5 `@media (prefers-reduced-motion: no-preference) { …motion… }` is guarded by construction → PASS +# P6 a duration token zeroed inside the reduced-motion block covers its users → PASS +# P7 the markup arm judges per selector too: a scoped +VUE + ( + cd "$1" || exit 1 + git init -q . + git add -A + git -c user.email=t@t -c user.name=t commit -qm init + ) >/dev/null 2>&1 +} + +# Written to a FILE, not a variable: `$(_run45 …)` is a subshell and an +# assignment made inside it never reaches the caller. +_LAST_LOG_PTR="${_tmp}/last-log-path" +_run45() { # _run45 -> echoes the gate-45 verdict line + local logs="${_tmp}/logs.$$.${RANDOM}" + mkdir -p "${logs}" + printf '%s' "${logs}/hydra-gate-prefers-reduced-motion.log" > "${_LAST_LOG_PTR}" + ( + cd "$1" || exit 1 + git add -A >/dev/null 2>&1 + git -c user.email=t@t -c user.name=t commit -qm wip >/dev/null 2>&1 + HYDRA_GATE_LOG_DIR="${logs}" bash "${_runner}" . 2>/dev/null + ) | grep -E '^\[gate-45\]' || true +} + +_assert() { # _assert