doxa-maps/internal/check-tab-radius-token.mjs
Doxa Maps bc3a5029a6 Tab-radius gate: find Collapse's rule inside a selector list
The home link joined Collapse's rule (.vtl-collapse, .vtl-home { … }) so the
two share one box, and the gate only recognised a rule that named
.vtl-collapse alone, so it found no reference and failed on development. It
now reads the reference from any rule whose selector list names
.vtl-collapse as a whole selector (not :hover, -tx or a descendant), and its
self-test gains a poison that moves only that rule's radius.
2026-09-30 21:17:42 +00:00

187 lines
10 KiB
JavaScript

#!/usr/bin/env node
/**
* check-tab-radius-token.mjs — the vertical tab rows must read the shared radius token, not a
* hard-coded pill.
*
* THE DEFECT THIS EXISTS FOR. The tabs shipped at border-radius:999px — a full stadium on a 48px
* row — under a real instruction to imitate the YouTube active row. Measured against the rest of
* the product that is the only thing shaped that way: .vtl-collapse is 8px, the filter toggles in
* library/components/search are 8px, and 8px is already the most common radius in the layout file.
* That was reversed: nothing else in the app has that same roundedness.
*
* ★ WHY A GATE AND NOT JUST THE FIX. The 999px rationale is still in the file, kept deliberately as
* history. A superseded rationale that still reads as current is exactly how a reversed decision
* gets quietly undone — the same trap another agent hit today with a stale "navy pin >= 7:1 PASS"
* acceptance line that no longer described anything that ships. A future reader who trusts that
* comment would restore the pill and believe they were fixing a regression.
*
* WHAT IT ASSERTS, and why each assertion is here rather than the obvious one:
* A. .vtl-tab declares its radius through var(--vtl-radius,…), not a literal. Asserting the
* TOKEN rather than the value "8px" is the point: the requirement is that the tabs,
* the collapse button and the filter toggles agree, which a shared knob guarantees and three
* matching literals only coincidentally satisfy.
* B. The token's fallback equals .vtl-collapse's literal radius, READ FROM THE FILE. A hard-coded
* expectation of 8px here would pass while the collapse button moved to 10px and the two
* surfaces silently diverged — the gate would be asserting a number instead of the rule.
* C. Neither .vtl-tab copy carries a pill radius (anything >= 100px). This is the actual
* regression: a literal reintroduced beside the token would leave A passing.
*
* NOT ASSERTED, deliberately: .vtl-cap keeps border-radius:999px and should. The complaint was
* about the ratio of radius to size — a 16px badge at 999px is a chip, a 48px row at 999px is a
* stadium. A gate that banned 999px everywhere would over-apply the correction.
*
* ★ BOTH COPIES. VerticalTabsLayout.vue carries this CSS TWICE (VTL_SHADOW_CSS and the scoped
* <style> block) and they must stay byte-identical. Every .vtl-tab occurrence is checked, so a fix
* applied to only one copy fails here rather than shipping correct in one host and wrong in the
* other.
*
* NEGATIVE-TESTED: replacing the token with border-radius:999px in either copy exits 1 naming the
* line. Restoring exits 0. Run `bun run check:tab-radius:self-test` to re-prove it — an in-memory
* poisoning, so a crash can never leave the tree edited.
*/
import { readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const FILE = join(ROOT, 'library/layouts/VerticalTabsLayout.vue')
const REL = 'library/layouts/VerticalTabsLayout.vue'
const fail = (m) => { console.error('TAB-RADIUS: FAIL — ' + m); process.exit(1) }
// A pill is any radius at least half the row height; 48px rows make 100px a safe floor that still
// catches 999px, 9999px and the occasional 50vh.
const PILL_MIN = 100
// Does this line open a rule whose selector list names `cls` as a WHOLE selector? `.vtl-collapse, .vtl-home {`
// counts (the home link joined Collapse's rule so the two share one box); `.vtl-collapse:hover`,
// `.vtl-collapse-tx` and `.vtl.rail-collapsed .vtl-collapse` do not.
function opensRuleFor(ln, cls) {
const m = ln.match(/^\s*([^{}]+)\{/)
return !!m && m[1].split(',').map((x) => x.trim()).includes(cls)
}
function auditLines(lines) {
const problems = []
// ── the reference: .vtl-collapse's literal radius, read from the file, never assumed ──────────
const collapse = []
lines.forEach((ln, i) => {
if (!opensRuleFor(ln, '.vtl-collapse')) return
const m = ln.match(/border-radius:\s*([^;]+);/)
if (m) collapse.push({ line: i + 1, value: m[1].trim() })
})
if (!collapse.length) {
problems.push('could not find a .vtl-collapse rule with a border-radius to use as the reference.\n' +
' This gate derives the expected value from the collapse button rather than hard-coding it,\n' +
' because the rule is "the tabs agree with the collapse button", not "the tabs are 8px".')
return problems
}
const reference = collapse[0].value
if (collapse.some((c) => c.value !== reference)) {
problems.push('the .vtl-collapse copies disagree with each other: ' +
collapse.map((c) => `:${c.line}=${c.value}`).join(' vs ') +
'\n Fix that first — the reference this gate measures against is not self-consistent.')
}
// ── the subject: every .vtl-tab rule (both synced copies) ────────────────────────────────────
const tabs = []
lines.forEach((ln, i) => {
// `.vtl-tab {` only — not .vtl-tab-badge, .vtl-tab-fallback, or .vtl-tab:hover
if (!/^\s*\.vtl-tab\s*\{/.test(ln)) return
const m = ln.match(/border-radius:\s*([^;]+);/)
tabs.push({ line: i + 1, value: m ? m[1].trim() : null })
})
if (!tabs.length) {
problems.push('found 0 `.vtl-tab {` rules in ' + REL + '.\n' +
' A gate with nothing to assert is not passing; it is not running. If the class was\n' +
' renamed, update this gate in the same commit.')
return problems
}
if (tabs.length < 2) {
problems.push(`found only ${tabs.length} \`.vtl-tab {\` rule. This file carries its CSS TWICE ` +
'(VTL_SHADOW_CSS and the scoped style block) and both must be present and identical — a\n' +
' component that is correct in one host and wrong in the other is this repo\'s most\n' +
' frequent defect shape.')
}
for (const t of tabs) {
if (!t.value) { problems.push(`${REL}:${t.line} — .vtl-tab declares no border-radius at all.`); continue }
// C. the regression: a pill literal reintroduced
const px = t.value.match(/^(\d+)px$/)
if (px && Number(px[1]) >= PILL_MIN) {
problems.push(`${REL}:${t.line} — .vtl-tab is back to a PILL (${t.value}).\n` +
' The 999px stadium was SUPERSEDED on 2026-09-23. The YouTube rationale still in this\n' +
' file is kept as history, NOT as a current instruction — do not restore the pill from it.\n' +
` Expected: border-radius:var(--vtl-radius,${reference});`)
continue
}
// A. the token
const tok = t.value.match(/^var\(\s*--vtl-radius\s*,\s*([^)]+)\)$/)
if (!tok) {
problems.push(`${REL}:${t.line} — .vtl-tab uses the literal "${t.value}" instead of the shared token.\n` +
' The requirement is that the tabs, the collapse button and the filter toggles AGREE.\n' +
' A shared knob guarantees that; three matching literals only coincidentally satisfy it.\n' +
` Expected: border-radius:var(--vtl-radius,${reference});`)
continue
}
// B. the fallback tracks the reference
if (tok[1].trim() !== reference) {
problems.push(`${REL}:${t.line} — the token fallback is ${tok[1].trim()} but .vtl-collapse ` +
`(line ${collapse[0].line}) is ${reference}.\n` +
' These are the two surfaces named as needing to match. Change both or neither.')
}
}
// both copies must agree with each other
const distinct = [...new Set(tabs.map((t) => t.value))]
if (distinct.length > 1) {
problems.push('the two synced copies of .vtl-tab disagree: ' +
tabs.map((t) => `:${t.line}=${t.value}`).join(' vs ') +
'\n Edit both or neither — check:synced-css enforces the same rule file-wide.')
}
return problems
}
// ── self-test: prove the gate catches the real regression, in memory ────────────────────────────
if (process.argv.includes('--self-test')) {
const original = readFileSync(FILE, 'utf8')
if (auditLines(original.split('\n')).length) {
console.error('TAB-RADIUS SELF-TEST: FAIL — the tree is already failing; fix that first.')
process.exit(1)
}
const cases = [
['pill-restored', (s) => s.replace('cursor:pointer; border-radius:var(--vtl-radius,8px); font:inherit;',
'cursor:pointer; border-radius:999px; font:inherit;')],
['literal-instead-of-token', (s) => s.replace('cursor:pointer; border-radius:var(--vtl-radius,8px); font:inherit;',
'cursor:pointer; border-radius:8px; font:inherit;')],
// the reference must be read from Collapse's rule even inside a selector list: change ONLY that rule's
// radius and the (unchanged) tabs no longer agree with it.
['collapse-radius-moved', (s) => s.split('\n').map((l) => (opensRuleFor(l, '.vtl-collapse') ? l.replace(/border-radius:\s*8px;/, 'border-radius:12px;') : l)).join('\n')],
]
let bad = 0
for (const [name, poison] of cases) {
const mutated = poison(original)
if (mutated === original) { console.error(` ${name}: POISON DID NOT APPLY — the gate proves nothing.`); bad++; continue }
const caught = auditLines(mutated.split('\n')).length > 0
console.log(` ${name}: ${caught ? 'caught' : 'NOT CAUGHT'}`)
if (!caught) bad++
}
if (bad) { console.error(`TAB-RADIUS SELF-TEST: FAIL — ${bad} case(s) not caught.`); process.exit(1) }
console.log('TAB-RADIUS SELF-TEST: PASS — every regression caught; tree untouched (poisoned in memory).')
process.exit(0)
}
const problems = auditLines(readFileSync(FILE, 'utf8').split('\n'))
if (problems.length) {
console.error(`TAB-RADIUS: FAIL — ${problems.length} problem(s).\n`)
for (const p of problems) console.error(' ✗ ' + p + '\n')
process.exit(1)
}
console.log('TAB-RADIUS: PASS — .vtl-tab reads var(--vtl-radius) in both synced copies, and the')
console.log(' fallback matches .vtl-collapse. The .vtl-cap badge keeps its pill by design: the')
console.log(' complaint was the ratio of radius to size, not the shape itself.')