Firmado por el autor tras C («arregla también /blocks»). El shell de `/blocks`
(`web/routes/blocks/+layout@.svelte`) y el boot de cada preview
(`web/routes/blocks/_lib/BootUix.svelte`) mantenían su propio `$state` de modo
y lo pasaban a eidos por `modeSource` (muerto en f875f60b9), así que su toggle
había dejado de mover a eidos — y como el sobre `uix.prefs` es compartido con
`/uix`, la divergencia estaba a UN CLIC del toggle de docs (adversarial C, R-1).
Además implementaban la DENSIDAD a mano (`:root{--density-space-scale…}` por
`uix.dom.writeStyle`) mientras eidos ya estampa `data-density` desde prefs y la
hoja generada aplica las escalas: un segundo motor de densidad.
- Shell: `mode` y `density` DERIVADOS de las ranuras de `uix.prefs`; los toggles
escriben `setIntent`; mueren `modeSource`, `modeListeners`, `DENSITY_SCALES` y
el `writeStyle`; migración ÚNICA del blob `uix-blocks-prefs` al sobre de prefs
(detalle en el cuerpo del informe / handoff).
- Preview (`BootUix`): la raíz nace con `intent` desde la URL (`mode`, `language`,
`direction`, `density`) y `storage: false` — la preview es una PROYECCIÓN de su
URL y ya no hidrata ni escribe el sobre compartido del documento padre
(contaminación cruzada latente, cerrada).
- `BlockDemo`: prosa que citaba `modeSource` corregida; los ejes de la URL se leen
de prefs.
- Ledger `check-debt.ts`: desaparecen las dos entradas de `blocks` (solo mengua).
Verificado en Chrome por el coordinador (SO oscuro sin blob ⇒ `/blocks`
coherente; toggles de modo y densidad mueven `<html data-mode/data-density>` y
el wrapper; preview `?mode=light` clara con el shell en oscuro sin tocar el sobre).
Adversarial Opus independiente (informe en el handoff). Constructor + adversarial
Opus 5; la sesión coordina.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
La preferencia efectiva de motion la decide prefs UNA vez (`resolveMotion`: la
intención `allow|reduce` gana al hint del SO, `system` deriva) y la proyección la
estampa en `<html data-motion>` — desde e3c0899dd antes del primer pintado. Aun
así el árbol la re-derivaba por su cuenta leyendo el SO en crudo en dos capas:
A) CSS de eidos (§61 del changelog): 65 at-rules `@media (prefers-reduced-motion:
reduce)` en 63 ficheros + 29 emitidas por el generador, conviviendo con 14 + 52
selectores `[data-motion='reduce']` («with or without the JS projection»). Dos
fuentes ⇒ un `allow` explícito no llegaba al CSS. Cada bloque pasa a
`[data-motion='reduce'] SEL` con la MISMA declaración y orden (paridad a
máquina: 63 ficheros, 65 bloques, 89 reglas, 178 pares selector-declaraciones,
0 discrepancias); los 3 ficheros con gemelo idéntico se unifican; ningún bloque
usaba `:root`. Los 4 `!important` dentro de bloques migrados QUEDAN con su rival
nombrado (navigation-menu ×2 contra el swap direccional (0,3,0); text-focus
contra un `style:transition` inline; events.css empate (0,3,0) con las firmas
generadas en otra hoja). Generador: los 4 emisores dejan de emitir el media, ink
marks gana su gemelo; `generated/base.css` 29 → 0 media, `--nombres` 8034 /
5751 únicos INTACTOS. GUARD nuevo `reduced-motion-media.test.ts` (at-rule, no
prosa; anti-vacío > 60 ficheros; mordido por mutación). Cambio de veredicto de
cascada MEDIDO en Chrome real por el adversarial: el prefijo suma (0,1,0), una
veintena de variantes más específicas que ganaban al media hoy pierden — y bajo
la media del SO la forma de HEAD NO paraba el anillo del spinner. `data-motion`
tiene TRES dueños (reduce · navigation-menu from/to · tabs fade|slide), valores
disjuntos, forma ancestro obligatoria. Prosa que afirmaba «both» barrida por la
AFIRMACIÓN (tooltip.css, card.css, recipes/base.ts, skin-media-player, events,
float-panel).
B) JS (§62): el motor de motion (`engine-motion.ts:131`), el motor de escena
(`engine-scene.ts:99`), la háptica de sema (`chans/haptic.ts:129`) y OCHO
lecturas en componentes de eidos preguntaban a `ActiveDom.prefersReducedMotion`
(el media en crudo). El puerto correcto YA EXISTÍA sin consumidores:
`MotionSource = Source<MotionEffective>` (`$libs/motion`). La fuente nace en
prefs (`createMotionSourceFromPrefs`, `src/arts/prefs/motion-source.ts`,
precedente `createLocaleSourceFromPrefs`) y la construyen UNA vez las tres
raíces: `createActiveUix`, `attachActiveUix` (fallbacks desde `app.prefs`) y las
fábricas `defineEngineMotion` / `defineEngineScene` / `defineEngineSemantic`
(`coreDependencies: ['prefs']`, forma de format y langs). MUEREN de los puertos
`MotionDom.prefersReducedMotion`, `SceneDom.prefersReducedMotion` y el interface
`HapticChannelDom` entero (un puerto que contesta política es la puerta por
donde vuelve el defecto); `MotionRunOptions.reduced` se queda como PIN por
ejecución. `ActiveEidos.reducedMotion` con dos puertas: con prefs el efectivo;
standalone sigue al SO con acta (sin primer motor no hay segundo, §60). Los
ocho sitios de componentes leen `eidos.reducedMotion`. Hallazgo del lote: DOS
motores de escena por superficie DOM (aura-indicator, pack Ambient) que se
habrían quedado ciegos a la policy `reduce` obligatoria (P-1) EN SILENCIO —
reciben la fuente vía `eidos.reducedMotion`. Deuda nombrada: escena lee la
fuente en el montaje (paridad con la lectura del media que sustituye).
`ActiveDom.prefersReducedMotion` no se toca: es un hecho del SO que alimenta
el ENTORNO de prefs y nada más.
Soma (adenda firmada «a en B»): el último lector de política que preguntaba al
SO, `soma/runtime.svelte.ts:1280` (migración a11y del morfo: `'state'` ⇒
`channels: []` por S5, `'text'` ⇒ región viva, `'focus'` ⇒ foco), lee la
MISMA fuente: `SomaRuntimeBaseSources.motion: MotionSource` OBLIGATORIO como
`dom` (un runtime que no puede responder «¿reducido?» no puede honrar S5 — lo
garantiza el tipo), llenado por `Soma.runtime()` desde `uix.prefs` (esquema sin
`motion` ⇒ permitir, como los motores). La decisión mayor (b) — que el MOTOR
aplique `'state'` en su pasada de reducción y que un `emit()` directo reciba
tratamiento a11y — queda ABIERTA como fila §3.9 de CONTINUE-sema-audit.md, a
ejecutar junto a D-full. `ActiveEidos.reducedMotion` distingue «sin prefs»
(standalone ⇒ SO, §60) de «prefs sin ranura motion» (⇒ permitir, como los
motores): las dos mitades de una UI ya no discrepan bajo un esquema sin la
dimensión. Hallazgo de la adenda: la bolsa `sources` se construye en 96
sitios (3 de producción — menu-dial, metrics, onion-menu — que reciben la
fuente vía `eidos.reducedMotion`, y 93 harnesses cuyo `as unknown as Soma`
CEGABA la comprobación de miembros: 60 tests rojos hasta declarar el `Omit`
real; el tipo hizo su trabajo en el código de producción y era ciego justo
donde se suponía que bastaba). Test real nuevo
`soma/test/reduced-motion-source.svelte.test.ts` (chromium: `Soma.create()`
lee contexto Svelte) con `createActiveUix` + `EngineSemantic` reales;
mutación (el trigger vuelve al dom) ROJA 3/3. Adversarial dirigido de la
adenda: siete defectos cerrados — `src/uix/contracts.ts` declaraba
`requires: ['dom']` para la bolsa de soma y el test no lo asertaba (un guard
que no inspecciona nada pasa) → `['dom', 'motion']` + aserto; la bolsa #97
(`bag-census.test.ts`) sin `motion` bajo un casteo; el camino `'text'` —el
ÚNICO que declaran 9 morfos de producción— sin test (añadido); dos snippets
de docs que ya no compilaban (component-guide A1, morfo.md); cifras del §62.
C) La raíz GARANTIZA los cuatro ejes visuales (cazado por el autor en el docs
site: texto invisible en oscuro). Cuatro layouts congelados componen su propio
esquema de prefs SIN `mode/theme/density/scaling`; un esquema del app
SUSTITUÍA al de la raíz entero y eidos «degradaba defensivamente» a
`mode = 'light'` CONSTANTE ignorando el SO, mientras el shell estampaba su
wrapper en oscuro: tinta de tema claro sobre superficies oscuras. Forma:
`uixVisualPrefsDimensions()` (pura, en `prefs-schema.ts`; el boot compila la
misma) y `createActiveUix` fusiona SIEMPRE `{ ...ejesVisuales, ...esquemaDelApp }`
— el app puede REDEFINIR un eje, nunca omitirlo; sin un solo cast. Eidos deja
de degradar en silencio: `createPrefsPreferenceSource(prefs, fallbacks,
onMissing)` avisa por `uix.logger.warn` por cada ranura ausente (queda solo
para attach con prefs ajenas; docs de attach: el app compone
`uixVisualPrefsDimensions()`). Tests reales (esquema sin ejes + SO oscuro ⇒
`data-mode="dark"`; app que redefine `theme` gana; prefs ajenas ⇒ 4 avisos por
el logger REAL); el viejo test «degrades to the fallbacks» estaba verde POR
COINCIDENCIA (defaults = fallbacks) y se sustituye; mutación (sin fusión) 2
rojos y restaurada. Boot regenerado (15134 B), delta cero ×5 intacto.
`web/routes/uix/+layout@.svelte` (descongelado por orden del autor, +14/−29):
muere el `modeSource` muerto y el `$state` local; el shell LEE la ranura
`mode` y el toggle escribe `uix.prefs.setIntent('mode', …)`; migración única
de `uix-docs-theme` al sobre de prefs (clave borrada). Ledger 95 → 93 (la
entrada de ese fichero desaparece). VERIFICADO EN CHROME por el coordinador:
SO oscuro sin clave ⇒ `<html data-mode="dark">`, wrapper dark, tinta
`oklch(0.95)`; clave vieja `dark` ⇒ intent `dark`; toggle mueve html, wrapper,
tinta e intent en los dos sentidos. Nombrado, no arreglado: el boot compila
siempre el esquema por defecto (un app que REDEFINE un eje resuelve distinto
que el boot; hoy nadie en web/ usa el boot) · los otros tres layouts congelados
recuperan los ejes pero su wrapper sigue en `$state` local · SSR del docs sirve
`light` y la hidratación corrige (previo).
Verificación: A — vitest eidos+value-channels 43/483, eidos:lint 0, paridad 0
discrepancias, mutación del guard 3/3 y 4/4 (adversarial), prettier solo avisos
preexistentes; B — vitest del scope 90/990 y, con la adenda, 242/2395; +26 tests
(motor 5 · escena 2 · háptica 2 · raíz real jsdom 7 · ActiveEidos 7 · fábrica 1 ·
soma 3 — cifras verificadas una a una por el adversarial), 11 dobles re-firmados,
93 harnesses de soma tipados con el `Omit` real, cuatro mutaciones ROJAS (3/2/3 +
la de soma 3/3) y restauradas por sha256, más la mutación de TIPO del adversarial
(la fuente devuelve la intención ⇒ 2 errores nuevos en src/: el puerto es gate de
tipo, no prosa); C — vitest active-uix+eidos+prefs 57/591, boot 8/8 con delta
cero, mutación 2 rojos, verificación en Chrome por el coordinador; todos —
check src/ 0 (ledger 95 → 93: MENGUA), check:gate OK, docs:check OK,
arts:check OK, packs:check OK. Suite completa 457/5336 verde (adversarial A). Adversariales Opus
independientes por lote (informes en el handoff). Constructores + adversariales
Opus 5; la sesión coordina. ⚠ Lección: la cuenta del brief de A («94 en fuentes»)
era un fallo de medida del coordinador (`grep -rh | grep -v generated` filtra
LÍNEAS con esa palabra, no el directorio); el constructor la re-midió porque el
brief lo exigía. ⚠ Dos constructores cortados por límite de sesión y reanudados
tras releer su diff entero (ley de la casa).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cierra el estado INTERINO de e3c0899dd. `resolvePreferences` lanzaba con `uix` +
escalar y `ActiveEidos.create()` inyecta `uix` SIEMPRE, así que la regla no decía
«no mezcles dos motores»: decía «ningún escalar, nunca», y toda demo que clava un
panel en oscuro arrancaba con excepción. Forma firmada por el autor (c'):
- Pines SÍ: `theme` / `mode` / `density` / `scaling` en `ActiveEidosOptions` son
PINES — el eje queda clavado en la instancia y gana sobre la fuente, prefs
incluido (precedente en el mismo constructor: `options.dom ?? options.uix?.dom`).
Un eje clavado sigue suscrito: `apply()` corre y lo encuentra quieto.
- Sources por eje FUERA sin shim: `modeSource` / `densitySource` / `scalingSource`
eran la API del segundo motor. La puerta de sustitución ENTERA sigue siendo
`preferences`. Mueren `createComposedPreferenceSource`, `createStaticValueSource`
y `PREFERENCE_OPTION_KEYS`; nace `createStandalonePreferenceSource(dom)`:
sin primer motor no hay segundo — standalone sigue al SO EN VIVO para `mode`.
- Sin throw. UNA precedencia por UN envoltorio (`createPinnedPreferenceSource`)
sobre la puerta que responda (`preferences` · `uix.prefs` · standalone):
`pin ?? fuente ?? fallback` en las tres.
- Una función pura, dos lectores: `src/uix/eidos/lib/visual-preference.ts`
(`VisualPreferencePins` + `resolveVisualPreference(pin, value)`), importable por
el boot compilado como `lib/theme-id.ts`. `ActiveEidosOptions extends
VisualPreferencePins`; `UixBootParams.pins` lo toma entero;
`renderUixBootScript({ pins })` los embarca. DOS parámetros y no tres: el
fallback no es compartido (el boot resuelve siempre los cuatro ejes; en runtime
lo aporta cada fuente) — acta en changelog §60.
- Delta cero a CINCO casos: instancia clavada a dark con sobre en light (ni un
attr se mueve al hidratar) + familia clavada `acme` con modo de prefs
(`acme-dark` a los dos lados: el pin atraviesa `resolveThemeId`). Mutaciones
probadas: sin pin en el boot → 2 rojos; sin pin en el envoltorio → 2 rojos;
restauración por sha256.
- `contracts.test.ts`: retirado el guard «guards UIX docs shell from writing
visual prefs through ActivePrefs» — codificaba la doctrina REVOCADA el
2026-09-14 (escribir `theme` en prefs es el camino canónico); no se invierte:
un guard sobre un árbol congelado que se reconstruye no mide nada. Acta en §60.
- Coste en el árbol congelado: diez demos pasan `*Source` y pierden el tipo →
ledger `check-debt.ts` +22 (8 entradas nuevas, 2 subidas), cada una con causa
fechada (excepción firmada 2026-09-13). `src/` a CERO.
- Docs: eidos.md §preferencias · prefs README §Eidos Boundary · guide.md
(`pins`, tercer parámetro del boot) · changelog §60 · active-uix / overview /
active-architecture / blocks (snippets con `modeSource` al flujo canónico).
Verificación: vitest eidos+active-uix+prefs+contracts+value-channels 59/640 ·
check 95 = 73 + 22 exacto, src/ 0 · check:gate OK · docs:check 819 OK ·
generate:boot 15104 bytes con sync verde · adversarial Opus independiente
(informe en el handoff). Constructor + adversarial Opus 5; la sesión coordina.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
39edbc8db dejaba la apariencia como OPCION del que renderiza
(RenderThemeCssOptions.appearance, pasada a mano por generated-css.ts):
apply() y renderCss() salian mudos, y habia DOS fuentes de verdad porque
la apariencia ya vivia implicita en el sufijo del id (/-(light|dark)$/).
Forma firmada por el autor:
- ThemeDefinition.appearance: ThemeEffective (el tipo de $libs/theme, sin
union nueva), OBLIGATORIO. Tres palabras: mode = la preferencia
(data-mode) · theme = lo pintado (data-theme) · appearance = que ES el
tema. No `colorScheme`: ese nombre es de la paleta derivada de seed.
- renderThemeCss lo lee del TEMA y lo emite SIEMPRE como primera
declaracion; la opcion desaparece sin shim. apply(), renderCss() y el
generador lo emiten gratis.
- El sufijo del id baja a convencion de BUSQUEDA del resolver: el
validador exige el campo (gate de forma, el que protege documentos) y
cierra la contradiccion sufijo<->apariencia (gate semantico). Un tema
nuevo por EidosConfigPatch sin el campo cae en el primero.
- Todo bloque que pinta una apariencia la declara: el bloque del seed
(applyColorScheme, que admite forzar el donante) emite color-scheme
desde la apariencia del tema DONANTE resuelto, no del mode crudo;
helper #resolveSchemeDonorThemeId compartido para resolverlo una vez.
- Documento persistido v1 -> v2, sin migracion: nadie puede adivinar la
apariencia de un tema viejo.
generated/ no cambia ni un byte (las dos lineas ya estaban; ahora vienen
del tema): renderGeneratedBaseEidosCss() sigue en 5751 nombres unicos /
8034 ocurrencias, contrato y censo quietos. Suite eidos + value-channels
460/460 (8 tests nuevos, 4 mutaciones probadas). check 71 -> 73: los dos
son web/routes/alpha/lib/docs-theme.ts sin appearance — el arbol
congelado pierde el tipo por diseño y entra en el ledger con la unica
excepcion escrita (crece SOLO cuando el framework se mueve bajo el arbol
provisional); grafito.ts y theme-variants.ts ya fallaban por literal y no
suben. src/ a cero. check:gate OK, eidos:lint 0.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
El hallazgo mayor confirmado de la auditoría 2026-08-26: ~16 validadores y
429 ficheros de test que nada ejecutaba (sin CI en ninguna rama de la
historia, hooks .sample, eidos-lint sin script npm). Esta fase los cablea:
- `check:gate` (scripts/check-gate.ts): svelte-check con política — src/
debe cero; web/ se mide contra el ledger menguante scripts/check-debt.ts
(34 ficheros, 71 errores congelados; sólo puede bajar, la disciplina de
theming-census-debt). Guard anti-vacío: sin marcador COMPLETED o con
conteo que no cuadra, falla — un gate que no inspeccionó nada no pasa.
- `eidos:lint` deja de ser invocación cruda (exit 1 real en invalid/dead).
- `gate`: check:gate → validadores rápidos → vitest al final. `lint` queda
deliberadamente FUERA: hay ~2.300 ficheros de deuda de formato
preexistente (nunca se formateó el repo entero; el one-shot pendiente es
`npm run format` en árbol quieto y re-añadirlo — documentado en el hook).
- scripts/hooks/pre-push: la fuente del hook, SIN armar. Se armó en
caliente, bloqueó el push legítimo del eje theming (secuencia mal: verde
primero, puerta después) y se desarmó — de paso la puerta demostró que
sabe fallar, que era el requisito de verificación de la fase.
- .prettierrc endOfLine:auto — neutraliza la clase CRLF/autocrlf de
Windows (~350 falsos rojos), sin cambiar formato real.
Verificación: check:gate exit 0 sobre HEAD (71 dentro del ledger, src/ a
cero tras 9d6533cb7 del eje navigation-menu) · sondeo individual del resto
de miembros del gate: verdes salvo los adjudicados en fase A.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>