From 6392d6003f89688ca2d6b416dc5e0efac60d0785 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 25 Aug 2026 18:50:39 +0200 Subject: [PATCH] fix(theming): S1 de la GRANDE - el prepareWith GRITA y el STALE ve los patrones Primer barrido de la firma de instrumento grande (cegueras 6 + 1 del expediente VA). Dos cambios en theming-sentinel.ts: - Ceguera 6: el catch del bucle prepareWith avisa SIEMPRE por stderr - el count() previo ya filtro la ausencia, asi que lo que cae ahi es un control que estaba y fallo, la fuente de las muertes fantasma (chat-message midio 59/81 y 77/81 sobre codigo identico). No lanza: un barrido debe terminar, pero una corrida con aviso NO es certificable. - Ceguera 1: stale/oscillating filtran por reasonFor(k) (exactas Y patrones) en vez de k in ledger (solo exactas) - 236 de las 821 ranuras adjudicadas (29%) no podian disparar STALE, el silencio que costo 207 claves en B'. El mensaje distingue: exacta -> retirar la entrada; patron -> ESTRECHARLO (nunca retirarlo: cubre una familia y las demas siguen muertas). Barrido S1 del ledger ENTERO: 91/91 componentes VERDES, cero unadjudicated, cero avisos de prepareWith (corrida certificable), 40,8 min. label 1/2 y switch 27/52 byte a byte identicos a las bases - los cambios no tocan veredictos, solo el informe. Cosecha: 18 claves STALE, todas de PATRON, todas de toast - los dos patrones PLANE_SUPERSEDED ({tono}-{surface|border} y {tono}-accent) adjudicados el 2026-08-24 y dejados rancios ESA MISMA NOCHE por SS12.9 (el plano bajo a :where() y la receta volvio a ganar; el handoff registra los seis tonos restaurados). Reproducido 3/3 sobre codigo identico. 18/18 vivas = familias enteras = ambos patrones RETIRADOS (no estrechados) con acta en el ledger; toast re-corrido tras la retirada: 72/85, exit 0, 0 STALE. La tercera entrada de toast (action-hover-bg) es clase tono-en-hover: cosecha de S3, intacta. Co-Authored-By: Claude Fable 5 --- scripts/theming-sentinel-exceptions.ts | 23 +++++++--------- scripts/theming-sentinel.ts | 36 ++++++++++++++++++++------ 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index a73ac2f8b..c5cc540ab 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -1588,19 +1588,16 @@ export const SENTINEL_PATTERN_EXCEPTIONS: Array<{ TONE_UNREACHED + ' — the three slots share ONE rule, [data-tags-input-item][data-state=active], and the guard sweeps data-focus / data-invalid / data-disabled for this component, never data-state; `border` reads alive because the CONTROL paints it under data-focus, which IS swept. Measured 2026-08-24 forcing data-state=active on the real tag with the tone stamped: --tags-input-secondary-track repaints it (oklch(0.9913 0.0013 286.38) -> rgb(1, 2, 3)) and --tags-input-secondary-text the ink. The eight `-text` keys carry a SECOND cause on top, and it SURVIVES the aria-selected firma of 2026-08-25: what masks the ink of the highlighted tag is [data-archetype=item][data-highlighted]:not([data-state=checked]):not([data-state=on]) at (0,3,0), which still out-ranks the recipe rule [data-tags-input-item][data-state=active] at (0,2,0). Re-measured on the real tag driven by the KEYBOARD: stripping aria-selected alone changes nothing (the ink stays oklch(0.2435 0 0)); stripping data-highlighted too is what lets the tone through (oklch(0.5168 0.1733 305.88)). The old wording blamed aria-selected through the retired item-fg entry, and that attribution was wrong' }, - { - component: 'toast', - pattern: /^(default|affirm|fulfill|risk|threat|loss)-(surface|border)$/, - reason: - PLANE_SUPERSEDED + - ' — the six intents x two slots, 12 of its 85 keys, and this is §12.9 on a PORTALED card: the item carries data-depth=overlay and the plane declares `background` and the `border` shorthand at the same (0,1,0) and later, so the whole per-intent CARD is painted by the plane. Measured 2026-08-24 on the real item with transitions frozen: --toast-risk-surface / -border move nothing (bg stays oklch(0.931 0 0)) while --depth-overlay-surface / -border on the same root repaint it (rgb(4, 5, 6)). Visible consequence, registered in next-features §12.9 and NOT fixed from this axis: a risk toast and a fulfill toast wear the same grey card; only the status disc and the action ink carry the intent' - }, - { - component: 'toast', - pattern: /^(default|affirm|fulfill|risk|threat|loss)-accent$/, - reason: - 'SUPERSEDED by the depth plane AT REST (the accent is a `border-inline-start` longhand and the plane`s later `border` shorthand overwrites all four sides — §12.9, same measurement as the surface/border pattern), but NOT dead: under `[data-toast-item][data-loading]` the pulse rule is (0,2,0) and its @keyframes outranks every normal declaration. Measured 2026-08-24 over 100 frames of the real pulse: without the write the interpolated border-inline-start-color runs oklab L 0.5032..0.8514; with --toast-default-accent=rgb(1, 2, 3) it runs 0.0823..0.5032 — the token reaches, in the one state where the plane cannot reach it' - }, + // RETIRED 2026-08-25 (sweep S1 of the instrument firma): the two + // PLANE_SUPERSEDED patterns of toast — /^(default|affirm|fulfill|risk|threat| + // loss)-(surface|border)$/ and /-accent$/, 18 keys. Both were measured true on + // 2026-08-24 and went stale THE SAME NIGHT: §12.9 dropped the depth plane to + // `:where()` (specificity zero), the per-intent card rules win again, and the + // handoff records the six toast tones restored pixel by pixel. The blind STALE + // check (`k in ledger`, exact-only) could not see keys covered by a PATTERN, + // so they revived in silence — S1's first full pattern census caught them: + // 18/18 alive, reproduced 3/3 on identical code (72/85 each run). Whole + // families alive means both patterns cover nothing: retired, not narrowed. { component: 'toast', pattern: /^(affirm|fulfill|risk|threat|loss)-action-hover-bg$/, diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index 542052c02..68b51e181 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -803,8 +803,16 @@ async function main() { try { await el.click({ timeout: 1500 }); await page.waitForTimeout(350); - } catch { - /* one layer this route cannot show */ + } catch (err) { + // The `count()` above ALREADY filtered the absence, so what lands here + // is never "one layer this route cannot show": the control WAS in the + // document and the click failed anyway (covered, detached, re-rendered + // mid-click). Swallowing it produces PHANTOM DEATHS — chat-message + // measured 59/81 and 77/81 on identical code, 18 tokens whose opt-in + // surface silently never mounted. It must not throw (a sweep has to + // finish), but a run that prints this is NOT CERTIFIABLE: repeat it. + const first = String((err as Error)?.message ?? err).split('\n')[0]; + console.warn(`sentinel ${component}: prepareWith FAILED — ${sel} — ${first}`); } } @@ -1132,8 +1140,8 @@ async function main() { const ledger = SENTINEL_EXCEPTIONS[component] ?? {}; const patterns = SENTINEL_PATTERN_EXCEPTIONS.filter((p) => p.component === component); - const reasonFor = (k: string) => - ledger[k] ?? patterns.find((p) => p.pattern.test(k))?.reason ?? null; + const patternFor = (k: string) => patterns.find((p) => p.pattern.test(k)) ?? null; + const reasonFor = (k: string) => ledger[k] ?? patternFor(k)?.reason ?? null; const adjudicated = dead.filter((k) => reasonFor(k) !== null); const unadjudicated = dead.filter((k) => reasonFor(k) === null); // A reason that opens with OSCILLATES: describes a token measured ALIVE that @@ -1143,16 +1151,28 @@ async function main() { // the next session deletes a correct adjudication (measured 2026-08-24 on // emoji-picker.open-trigger-fg, alive 4 of 5 runs on the same code). const oscillates = (k: string) => (reasonFor(k) ?? '').startsWith('OSCILLATES:'); - const stale = live.filter((k) => k in ledger && !oscillates(k)); - const oscillating = live.filter((k) => k in ledger && oscillates(k)); + // ADJUDICATED, not `k in ledger`: an exception written as a PATTERN covers keys + // the exact map never holds, so a token revived under one could never report + // STALE — 236 of the 821 adjudicated slots (29 %) were invisible to this check, + // and 207 keys came back to life in silence during firma B′. `oscillates()` + // already reads through `reasonFor`, so it generalises to patterns for free. + const stale = live.filter((k) => reasonFor(k) !== null && !oscillates(k)); + const oscillating = live.filter((k) => reasonFor(k) !== null && oscillates(k)); console.log(`sentinel ${component}: ${live.length}/${keys.length} tokens move a computed value`); for (const k of adjudicated) console.log(` adjudicated ${k} — ${reasonFor(k)}`); for (const k of oscillating) console.log(` oscillating ${k} — read ALIVE this run; the entry stays (${reasonFor(k)})`); - if (stale.length) + // The two kinds do NOT get the same instruction. An exact entry names one + // token and retiring it is the whole fix; a PATTERN names a FAMILY, and the + // other members are still dead — retiring it would bury them. The precedent is + // written by hand in the ledger (tag-group's `hover-affirm-soft-bg` is spelled + // out OF the pattern because the stage reaches that one alone). + for (const k of stale) console.log( - ` STALE exception(s) — the token moves now, the ledger entry should go: ${stale.join(', ')}` + k in ledger + ? ` STALE exception ${k} — the token moves now, retire the exact entry (${ledger[k]})` + : ` STALE pattern ${k} — the token moves now; NARROW the pattern ${patternFor(k)!.pattern.source} so it stops covering this key — do NOT retire it, the rest of the family is still dead (${patternFor(k)!.reason})` ); if (unadjudicated.length) console.log(