You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/process/CONTINUE-perceptual-surface.md

96 KiB

CONTINUE — la superficie perceptual es UNA RANURA

CONGELADO 2026-09-17 — el cierre del framework congela este handoff: no se actualiza. Lo que sigue abierto vive en LEDGER-cierre-2026-09.md, por id.

Kickoff: «Lee docs/process/CONTINUE-perceptual-surface.md y sigue por §0.» Fecha: 2026-08-10, actualizado 2026-08-11 (cierre) · Rama alpha-0.1-dir-prefs, pusheada en 61457baac. ⚠️ COMPARTIDA: otra sesión trabaja en blocks y en un eje de dismissal. git reset -q + stagear por lista explícita, y clasificar por contenido del diff, nunca por nombre — hoy un fichero de proceso estaba mixto (133 líneas, 3 mías). Doctrina viva: architecture/sema.md §The surface is ONE SLOT · architecture/morfo.md §Step 4 (las DOS clases de precedencia de attrs: contract vs naming) · §renderAttrs. Este documento sólo dice qué se cerró, qué queda y cómo medirlo.

0 · Dónde estás (2026-08-11, cierre de jornada)

PUSHEADO a gita/alpha-0.1-dir-prefs en 61457baac — la rama está limpia contra el remoto (0 por delante). Cuatro commits de la jornada del nombrado:

Commit Qué
37c732c3a el guard de traducciones deja de cascar y de ser ciego a los call sites
25e59e18f A-85: el attr de nombrado es un DEFAULT del contrato, no una imposición
b201f190d la consecuencia: espejos, button, las 21 cadenas, cross-element, gemelos
61457baac el handoff dice lo que pasó (dos diagnósticos heredados eran falsos)

⚠️ Rama compartida, y hoy se notó: otra sesión trabaja en blocks y en un eje de dismissal (los types.ts de color-picker / time-picker / time-range-picker). Clasificar por contenido del diff, nunca por nombre: AUDIT-blocks-ledger.md estaba MIXTO (133 líneas, 3 mías) y quedó entero fuera. Mis filas de A-85 en ese ledger siguen SIN COMMITEAR — cuando esa sesión lo commitee, irán en su commit; no hay que rehacerlas.

El eje del nombrado: qué queda, y por qué

Sólo UNA cadena pendiente es mecánica: alert-dialog HECHA 2026-08-12 — la receta de los 21 aplicada tal cual (morfo.md Step 4): el morfo declara #?components.alert-dialog.action|Confirm y .cancel|Cancel, fuera la cadena, la fuente, el prop de opts y el destructure de los dos wrappers; ALERT_DIALOG_LANGS queda huérfano (su langs.ts se suma a la lista de abajo). Medido en navegador (es): aria-label="Confirmar" / "Cancelar" saliendo del contrato. Refs compilados 157 → 159; el harness del test alineado con producción (translate = ts, la trampa de editable). El eje del nombrado queda a CERO cadenas mecánicas.

Las otras 10 cadenas no son deuda — están medidas y clasificadas: clipboard · password-field (toggle) · table (row-detail) · tree-grid (expand) están BLOQUEADAS por estado; tag-group (remove) por interpolación; waveform es correcta (su nombre lo lleva un <Slider> compuesto); progress · radio-group · rating-group · slider conservan sólo la supresión por labelledby sobre un prop sin default.

DECISIÓN TOMADA (2026-08-11): NO se ensancha mapRef. Traducir su salida desbloquearía sólo la clase ESTADO —ni las interpoladas ni las cross-element— y reintroduce la avería que esta jornada mató: el guard v2 no recorre los valores del mapa, y normalizeTranslationRef tampoco los tocaría, así que una clave relativa llegaría cruda a ts() y saldría literal. Si algún día se hace, la forma correcta NO es traducir la salida sino que los valores del mapa sean fuentes (composicional, sin magia de prefijo, y el guard los ve por la vía que ya tiene).

Lo siguiente, en orden:

  1. alert-dialog — la única cadena mecánica que queda (30 min). HECHA 2026-08-12.
  2. Dos decisiones del autor (§4) FIRMADAS 2026-08-12: el chip bloqueado (vía b + su letra pequeña) y mode/scope (retirar). La tercera, fixedWeeks, quedó REVOCADA el 2026-08-13 al aparecer el SPEC del componente — las disposiciones completas viven en §4.
  3. C5 rebanada 3, y empieza por subir a soma el comportamiento del chip — la tabla de §C5-r3 explica por qué no es mecánica.
  4. Arreglar npm run translations:check (casca) y censar los 166 morfos. HECHO 2026-08-11 — y el diagnóstico era doblemente falso: el crash no era una «url multilínea» sino el stripper de comentarios comiéndose el // de un https:// DENTRO de una cadena; y el guard, incluso sano, jamás escaneó los call sites (v.translationRef('key') no contiene literal #? hasta que el compilador lo normaliza) — los cinco defectos de chronos habrían pasado en verde. Reescrito sobre el contrato COMPILADO (importa morfos + compileMorfo, valida cada ref normalizada; catálogos importados, no regex-evaluados; fuentes .ts por AST de literales). Sweep: 172 errores → 0 (6 morfos sin catálogo · media-player 6 claves desfasadas · ~130 del chrome de palabras, con dos colisiones hoja↔rama status/placeholder resueltas a status.label / placeholder.default y descripciones slash en clave plana -d). Detalle: caja ⚠️ de morfo.md §common-mistakes.
  5. El resto del eje: F3 CERRADA 2026-08-13 (censo a 0 y API borrada) · F5 navegador CERRADA 2026-08-13 (los tres sitios medidos tras la campaña; los números y los dos artefactos del entorno, en §3.0 F5) · §3.2 restitución de persistentes · §3.3 eje B · §3.4 cola de sema · §3.5 fable · §3.6 ledger de blocks (A-47, A-69).

Deuda menor, medida y sin urgencia (no la re-descubras):

  • ⚠️ file-upload emite contact-trigger-picker DOS veces por un gesto ARREGLADO 2026-08-13, y el mecanismo era MÁS ANCHO que el diagnóstico inicial: no es sólo el Trigger burbujeando — el <input type=file> oculto también vive DENTRO del Dropzone, así que el click() SINTÉTICO que dispara openPicker vuelve a entrar por el mismo manejador. Medido: un click en el dropzone llegaba dos veces, la segunda con target = INPUT[hidden-input]; sólo el guard de reentrada que la plataforma pone en click() lo paraba en dos. Es decir, TODO click al dropzone duplicaba, no sólo el del botón. Arreglo: guard por propiedad en el onclick del dropzone (closest sobre los marcadores de trigger y hidden-input, construidos con partMarkerAttr, nunca a mano). Test visto fallar con la anidación real; medido después: click en el dropzone → UN contact-trigger-picker en DROPZONE, click en el botón → UNO en TRIGGER. ⚠️ Lección de clasificación: lo anoté como «adyacente, no cascadear» y era la clase que este eje persigue —una ocurrencia por gesto—, encontrada por mi propia medida en un fichero que ya estaba editando. Eso se arregla, no se anota.

  • 10 de 17 harnesses tienen un ts que IGNORA su clave — una clave errónea pero existente pasaría su aserción del bag. Lo tapa parcialmente translations:check (caza las inexistentes) y hoy se verificó a mano que no hay deriva. Cerrarlo es expect(ts).toHaveBeenCalledWith(ref), decidido una vez para todos.

  • 15 langs.ts sin consumidores (meter, popover, drawer, splitter, listbox, breadcrumb, anchor-nav, cropper, feed, grid-list, pin-input, progress, editable, carousel; +alert-dialog 2026-08-12, huérfano de su migración clase 1). Basura declarada. NO borrar sin instrucción explícita.

  • 12 avisos de huérfanos en translations:check (los tab-* de gradient-builder/picker y 8 de palabras): candidatos a borrar, decisión tuya.

  • range-calendar/langs.ts conserva PREV_MONTH/NEXT_MONTH muertas PREEXISTENTES — no las tocó este pase.

Documentación ya actualizada (no hace falta re-hacerla): morfo.md §Step 4 (las dos clases de precedencia, el anti-patrón del propRef + cadena, la tabla de las cuatro exclusiones, el spread condicional y la regla «mira si el elemento es tuyo») · morfo.md §common-mistakes (la trampa de texts + el guard v2) · soma-architecture.md §mergeProps (la fila de naming) · README de soma/chronos · README de eidos/chronos · CONTINUE.md de chronos (NO vigente, apunta aquí).

Línea base al cerrar (mídete contra esto, no contra memoria): check 70 = base con el WIP ajeno presente, 57 sin él — mis commits aportan CERO · soma 1269 ✓ / 1 (el timeout ajeno preexistente de soma-attr-audit) · morfo+sema 496 ✓ · translations:check 0 errores / 12 avisos · docs:check 0/621 · rtl:check 0/177 · refs compilados 157.


1 · El hallazgo, y por qué costó tanto verlo

data-event-* es una ranura por elemento: dos ocurrencias sobre un nodo no pueden expresarse las dos. El defecto lo encontraron tres auditorías independientes y ninguna lo cerró:

Auditoría Fecha Qué dijo
fable S1 2026-07-01 «Race de señales solapadas sobre el mismo target», con el arreglo mínimo escrito. El README de old-deprecated afirmaba «findings executed».
sema S-17 2026-08-05 Re-descubierto solo. Midió el knob: el estampado de commit-set moría a los 24 ms de un hold de 240.
blocks A-36 / A-65 2026-08-01/09 Reproducido en NAVEGADOR: dos estampados a 1,1 ms, press-squeeze jamás.

Tres veces el mismo defecto, tres veces sin arreglar. Lo que faltaba no era diagnóstico: era que nadie podía distinguir una redirección legítima de una deriva, porque la opción que redirigía se llamaba fallbackTarget y decía lo contrario de lo que hacía.


2 · Lo cerrado (todos los guards vistos fallar antes de arreglar)

# Qué Dónde
2.1 fallbackTarget → targetOverride (202 apariciones / 64 ficheros). No cosmético: es la PRECONDICIÓN para auditar la redirección soma/runtime.svelte.ts + todo el árbol
2.2 El desestampado comprueba propiedad (data-event-id). Rojo: expected null to be 'sig-B' sema/stamp.ts · sema/projection/dom.ts
2.3 assertTargetOverride — el runtime lee el marcador de parte del elemento y avisa si no es target ni allowedTargets. Runtime porque el override es una expresión soma/runtime.svelte.ts
2.4 A-36 CERRADO, y sin regime los 4 overlays
2.5 regime definido y cableado, reducido a replace | queue sema/types.ts · engine.ts · signal.ts
2.6 renderAttrs — los attrs que no son partes morfo/types.ts + schema.ts
2.7 ACTIVE_DEV_TRACK vaciado + deuda de chronos (scope · barrel · README) contracts.test.ts
2.8 S-36 barrel 68→71 + guard · S-31 vibrate(NaN) · S5 · SO2 · SO4 varios

2.4 A-36 — la lección: no era arbitrar, era dejar de redirigir

Los cuatro overlays cedían el estampado del open al trigger. La razón escrita en el código («el content no ha montado») era cierta cuando open era pre. Dialog / Drawer / Popover migraron a post hace tiempo y nadie retiró el override; float-panel seguía en pre (último fuera de la doctrina que sema.md escribe). Con <Drawer.Trigger> compuesto sobre <Button> —que es UN solo elemento— el open caía encima del contact-activate.

Medido en /blocks/site-header/preview a 375 px:

ANTES   contact-activate TRIGGER +329ms · emerge/open TRIGGER +331ms · press-squeeze: NUNCA
AHORA   contact-activate TRIGGER  +18,7ms · open CONTENT +20,6ms
        getAnimations() → press-squeeze running (120ms) + slide-from-right-full

2.5 regime — dos valores, no cuatro

  • replace (default) no había que construirlo: 2.2 lo terminó. Antes era el comportamiento roto.
  • queue es el único nuevo. Aplicado a las 4 parejas irreducibles: los tres toggles (el provider ES el botón) y el knob (handle-drop + commit-set).
  • collapse RETIRADO — su caso (el parpadeo continuo) ya lo mató 2.2: medido, 0 huecos en 5 emisiones a 72 ms con hold de 240. Y reutilizar el data-event-id habría minado la propiedad que lo curó.
  • lock RETIRADO — bajo la semántica limpia significaría «un cierre que no se anuncia si hay algo proyectando». Con él, sus 4 declaraciones muertas.

⚠️ EL GUARD DE queue PASÓ EN VERDE SIENDO INCORRECTO. Con timers falsos no hay animaciones, así que awaitExpression volvía al instante. El navegador midió que el commit encolado llegaba 1,6 s tarde, porque esperaba la expresión entera (tope 1500 ms) en vez del hold. queue espera el hold; el desplazado está a salvo porque su limpieza ya es consciente del dueño. El test corregido inyecta un getAnimations() que nunca termina. Medir la envolvente no es medir la salida — otra vez.

2.6 renderAttrs + los fixes de palabras

Al vaciar ACTIVE_DEV_TRACK salieron 23 data-palabras-* que ningún morfo declaraba y que palabras.css sí estiliza. No son partes ni pueden serlo: aterrizan en el árbol que el usuario escribió. Campo nuevo, con la línea afilada —si el consumidor puede componerlo o direccionarlo, es una PARTE— y ≥3 consumidores (palabras 23 · waveform 2 · aura 2).

Y tres más: 5 selectores muertos en el predicado del focus-scope (barrido de todo el repo: aparecían sólo ahí) · RTL-1 real en palabras-chrome.css (inset-inline-start lógico + translate físico = el doble volteo; centrado con margen derivado de un token, rtl:check 1→0) · el dir?: 'ltr'|'rtl' inline al Direction canónico — lo que destapó que el censo de dirección grepea el NOMBRE del tipo, así que un union inline lo escondía (exención firmada, misma razón que field-langs).


3 · LA COLA — por dónde seguir, en orden

3.0 ⚠️ GIRO DE ENFOQUE (2026-08-10, aprobado por el autor)

La campaña de §3.1 quedó SUPERADA como mecanismo: censo + campaña + aviso de runtime es la misma defensa que ya falló tres veces (depende de que alguien mire), y el propio censo de hoy demostró el modo de fallo al declarar supervivientes de más y de menos. El plan vigente hace inexpresable el estado ilegal — el defecto de clase no es que N eventos redirijan sin declarar, es que la API de emisión acepta un HTMLElement anónimo, compensando que el registro de partes pierde la identidad de instancia (indexa por nombre, gana el último montado):

  • F1 ✅ (2026-08-10) — SomaRuntime<M> genérico SIN default: nombres de evento, kebabs de parte y claves de events/actions tipados contra la declaración (EventNameOf / PartKebabOf / ActionNameOf, los extractores de semaSelector). 104 anotaciones migradas + 3 wrappers estrechados (createMenuDialRuntime / createOnionMenuRuntime / createMetricsRuntime — devolvían tipado pero tomaban sources ANCHO: agujero). SomaRuntime<Morfo> sólo en la tabla de contratos. Doc: soma-architecture.md §"typed by ITS morfo". Delta de check contra base medida con stash: CERO (74/74 únicos, 87/87 brutos); server 454✓/6 ajenos; navegador 1258✓/1 ajeno. Los tipos ya desmontaron DOS filas del censo: los 'clear-document'/'unlink' de palabras eran nombres de COMANDO en un ternario que mapea a eventos reales — falso positivo del censo, resuelto por inferencia.

  • F2 ✅ (2026-08-10) — registro por instancia: Map<part, PartRegistration[]>, la pertenencia la mantiene el stream del attachment (poda al desmontar, se re-añade al final al re-montar — sin crecimiento en listas virtualizadas), resolución liveReg = la instancia VIVA más nueva con caída a la más nueva a secas (pre-attach conserva la conducta anterior). Arregla de paso la clase «la más nueva muerta ensombrece a una viva». Delta de check CERO; suites idénticas (454✓/6 · 1258✓/1).

  • F3 — ✅ CERRADA (2026-08-13, censo 0 y API borrada). Siete olas en un día: gestos handle (64→51) · grids de fecha (→44) · css/number-field (→38) · vistas de eidos que REGISTRAN (→29) · select/combobox con el A/B de present-rise (→11 nav-menu incl.) · colecciones (→11) · palabras + los 2 declarados de pagination/toolbar (→0). ENDGAME EJECUTADO: targetOverride borrado de TriggerOptions (con lápida doctrinal: no volver a añadir una opción de elemento), assertTargetOverride fuera (queda assertAnchor para la puerta anclada), el scratch retirado — y el compilador, ya como censo, cazó 9 usos en tests que el grep del scratch nunca escaneó. El estado ilegal es INEXPRESABLE: ningún elemento anónimo viaja por la API de emisión. Historial del framework (2026-08-10):

    • PartRef<T = string> genérico (el builder v.partRef preserva el literal; default string = cero ondas, doctrina «ensancha el tipo primero»).
    • targetFallback declarado en el morfo (tipo + schema con 3 invariantes testeados: parte desconocida / re-declarar el target / duplicado) y pre-aplanado en ActionPlan; resolveEmitTarget es LA resolución única para validación, emit y foco a11y (no pueden discrepar).
    • SomaRuntimePart<M, K> sin default + trigger anclado tipado por EventNameTargeting<M, K> (target ∪ allowedTargets; el fallback EXCLUIDO a propósito — anclar al degradado forzaría el aterrizaje pobre con el primario montado) + runtime.partInstance(part, el) (identidad por referencia contra el registro; elemento ajeno → null) + guard dev assertAnchor para llamantes JS. Codemod: 101 ficheros + 6 uniones a mano (clases parametrizadas por parte: paginación de calendarios, sidebar).
    • Tests nuevos: cadena de fallback resuelta por el runtime · anclaje estampa SU instancia (no la más nueva) · partInstance identidad. 71✓.
    • Ola 1 ✅ — los 4 overlays: su allowedTargets: [trigger] del close era el EJE EQUIVOCADO (es estado de montaje, no elección del llamante) → targetFallback: [trigger] en dialog/drawer/popover/float-panel, y los 4 providers dejan de resolver content ?? partRef ?? trigger a mano. El prewrite de data-last-action queda SOLO en content (verificado: todos los selectores de eidos que lo leen son [data-*-content]; escribirlo en el trigger era incidental). stampPartsOf del pack-census cuenta ahora targetFallback como estampable. 218✓ (overlays + census + morfo).
    • Olas 2-3 EN CURSO (2026-08-10, censo 145 → 107). Migrados y en verde (check DELTA CERO · navegador 1258✓+2 nuevos/1 ajeno):
      • Anclado al handle propio — dropdown-menu item · context-menu item · command item (ratón) · table row · timeline item (emerge-reveal).
      • partInstance por identidad — listbox · toggle-group (las TRES emisiones del gesto anclan a UNA superficie, lo que el régimen queue del par contact+commit asume) · tag-group select/remove · tree-view (commit-select discrimina item/branch por identidad) · tree-grid · command (camino Enter) · radio-group · rating-group (item si el gesto nombró estrella, provider si teclado — partInstance discrimina) · grid-list · feed · tabs · carousel.
      • Gesto DENTRO de la superficie — table sort-trigger→column-header y detail-trigger→row: closest(partMarkerAttr(…)) + partInstance; el marcador sale de la función canónica, nunca string a mano. partMarkerAttr exportado del barrel de morfo para esto. ⚠️ El elemento de partida es opts.ref.current de la clase, NO e.currentTarget — los tests fabrican eventos sin currentTarget.
      • No-op retirados — proof-of-human (3) · tag-group clear · checkbox.
      • CROSS retirados (lección A-36: sobra la redirección) — editable (shift-enter-mode/commit-save/commit-cancel estampan el PROVIDER declarado; el subparte era deriva sin declarar).
      • aura → targetFallback — sus 4 allowedTargets:[provider] de la mañana eran también eje equivocado; provider sin condicionales.
    • Más migrados en el mismo pase (censo 107 → 99, check DELTA CERO, 21✓): search-field y stepper (CROSS retirados — el provider declarado ES la superficie; params de gesto eliminados de clear/submit/setStep/ goTo*Step) · form (no-op retirados de submit/warn/reset) · clipboard (copy(triggerEl?) ancla por identidad al Trigger pulsado — varios Triggers por provider quedan bien — y el README lo documenta).
    • QUEDA de las olas 2-4 (99 sitios): tags-input · file-upload (HECHOS 2026-08-13 con el resto de colecciones, censo 29 → 11) · month/year-grid · calendar (day) · range-calendar (HECHOS 2026-08-13, censo 51 → 44: clase B migrada al anclaje por identidad — day/cell registran, así que partInstance + trigger anclado; range-calendar gana un helper triggerOnDay para sus 5 sitios) · navigation-menu · menubar · virtual-list/grid · pagination (declarado; anclar desde los 5 controles) · css-field · number-field (HECHOS 2026-08-13, censo 44 → 38: el targetOverride público de commit() RETIRADO — sólo lo pasaban sus propios scrubbers, redirigiendo el TERMINAL declarado provider al scrubber contra la fila de la doctrina del sello, y de paso silenciando las reglas de pack que seleccionan el provider; los 4 handle-pick/drag-scrub con e.currentTarget eran no-op sobre el scrubber singleton registrado) · drag-drop · path-trace · rotate-align · float-panel (HECHOS 2026-08-13, censo 64 → 51: los «campos de elemento propios» eran los MISMOS elementos que las partes registran — 10 no-op retirados; las 3 emisiones de drag-drop ancladas por identidad a sus partes repetidas [source/target SON los refs registrados, verificado]; y la pregunta de float-panel la respondió la fila de la doctrina del sello: el content ES la posición → retirar los 4, el ?? dragEl era cinturón de la era fallbackTarget) · metrics/menu-dial/onion-menu (HECHOS 2026-08-13, censo 38 → 29: las vistas REGISTRAN — metrics value se registra y emite ANCLADO él mismo, el contexto deja de acarrear elementos; menu-dial registra list + cada action [el attachment atraviesa Fab→Button→button, medido: ids del runtime en los mini-FABs]; onion-menu registra surface + cada sector item por attachment CACHEADO por clave [la membresía es el stream, F2] — todo medido en navegador: expand sella el sector d1 pulsado, select la hoja d2, y el update de metrics aterriza en el Value del bloque live con 4 instancias registradas, no en el más nuevo. ⚠️ Trampa pagada: importar state en un .svelte con runas $state rompe TODOS los $state<T> del fichero — alias state as refState) · select/combobox (HECHOS 2026-08-13: NO era elección del llamante — era el superviviente A-36 con el open YA en post: el override al trigger forzaba el aterrizaje pobre con el content montado, y la firma de motion present-rise corría SOBRE el botón [medido ANTES/DESPUÉS: open/close TRIGGER→CONTENT, present fuera del trigger, commit-select→el ITEM elegido anclado]. Receta: open pierde allowedTargets [vestigio legalizado], close pasa a targetFallback [trigger] / [trigger, input], selección por partInstance) · dropdown/context open+close (F4, gestureTarget) · palabras (13 filas, diseño fino por evento — lo ÚNICO que queda del censo: 11 sitios) · navigation-menu · menubar · virtual-list/grid · time/color-field · css-field (HECHOS 2026-08-13: no-op de singleton retirados [virtual-list ×2 · virtual-grid · css-field, donde el elemento ERA partRef del destino declarado, leído para clearTarget y no para redirigir]; anclaje por identidad en las repetidas [menubar trigger · navigation-menu trigger ×2 + link · tags-input item · file-upload item]; terminales devueltos al provider declarado [tags-input commit-set-add ×3 + commit-reset, file-upload commit-set-add · signal-warn-reject · commit-reset]; y contact-trigger-picker de file-upload gana allowedTargets: [dropzone] — el gesto se sella donde está la MANO y las dos superficies son elección del llamante, no estado de montaje. Medido: nav-menu abre/cierra sobre el trigger PULSADO [«Solutions», el 2º de 2] y commit-select sobre el link «Pricing»; file-upload sella DROPZONE al pulsar la zona y TRIGGER al pulsar el botón. En color/time-field el input lo registran DOS partes [grupo visible + input oculto], así que ahí partInstance no es adorno: nombra la visible por identidad) · chronos (los botones del editor no son partes; la exclusión de escritura se levantó el 2026-08-10 — CONTINUE-sema-audit.md §3.1: «ya accionables»). ⚠️ La otra mitad de esta fila es falsa y se corrige el 2026-08-12: «sus chips tampoco REGISTRAN» no se sostiene. Medido al cablear el acarreo (ledger D9): runtime.partInstance('event-chip', el) RESUELVE, y el sello de handle-drag aterriza en el event-chip; el arrastre del tirador da 6 handle-resize sobre event-resize-handle. Los chips registran, así que el plain trigger de esta migración no fallaría por ese motivo. Al llegar a 0: BORRAR targetOverride + assertTargetOverride + retirar el script scratch. EJECUTADO 2026-08-13 (ver la fila F3). allowedTargets (elección del llamante) y targetFallback (estado de montaje) son dos ejes, precedente intentRequirement/intentGuidance.
  • F4 — dropdown open pre→post (receta float-panel) + decisiones de autor.

  • F5 ✅ (2026-08-13) — navegador, no opcional: los tres sitios que las tres auditorías encontraron, medidos después de la campaña entera y del borrado de la API (es la comprobación de regresión del eje, no la de cada ola):

    site-header 375px   contact-activate TRIGGER +8,4ms · emerge-open CONTENT +10,3ms
                        press-squeeze RUNNING sobre el TRIGGER · emerge-close en CONTENT
    knob                handle-pick · handle-drop +0ms · commit-set +245,4ms  (todo en control)
                        valor 40 → 56, o sea el commit existe porque el gesto cambió algo
    dropdown+Button     contact-activate TRIGGER +109ms · emerge-open CONTENT +112,7ms
                        press-squeeze TRIGGER + slide-from-top/scale-in/fade-in CONTENT
                        emerge-close en CONTENT (Escape) · commit-select en el ítem PULSADO
                        («Log out», el 3.º de 3 — no el primero ni el último registrado)
    

    El número que cierra S-17 es 245,4 ms: el commit-set encolado entra justo tras el hold de 240 del handle-drop. Ni aplastado a los 24 ms (lo que S-17 midió), ni 1,6 s tarde (el falso verde de queue, §2.5). Los tres sitios son el mismo elemento con DOS morfos —[data-drawer-trigger]+[data-button], [data-dropdown-menu-trigger]+[data-button], control para drop+commit— y en los tres las dos ocurrencias expresan.

    ⚠️ Dos artefactos del entorno, no hallazgos (el panel del navegador no compone frames en esta sesión): requestAnimationFrame está SUSPENDIDO, así que el handle-drag del knob —tick continuo, rAF-throttled— no llega a estampar nunca y un muestreo por fotograma da 0 muestras; y la línea de tiempo de las animaciones no avanza, así que getAnimations() sirve para decir cuál corre sobre qué elemento pero no cuánto dura, y el desestampado cae siempre en el tope de 1500 ms de la espera de expresión (de ahí los ~1,7 s de limpieza en las tres trazas, donde el hold real es 120/240). Para medir DURACIÓN de pintura hace falta Chrome real.

  • F6 ✅ (2026-08-11) — targetFallback estaba en el corpus SÓLO como JSDoc: ni morfo.md ni sema.md lo nombraban, con 8 componentes ya gobernados por él. Escrito: morfo.md §Step 5.5 (los DOS ejes en tabla, invariantes del schema, resolveEmitTarget única resolución con el foco a11y, el anclado EXCLUYE el fallback) · sema.md §ONE SLOT (el close de los overlays como mount-state declarado, no targetOverride) · y la aserción de pack-census.test.ts deja de mentir sobre su criterio (nombraba target+allowedTargets mientras stampPartsOf contaba TRES fuentes). ⚠️ Nota perceptual escrita en ambos: una parte de allowedTargets se estampa en uso rutinario; una de targetFallback sólo en montaje degradado — una regla de sonido que case SÓLO fallback casi nunca suena (hoy: 0 reglas así; si aparece una, valorar el warn por-evento del census).

El censo scratch queda como herramienta de MEDIDA hasta que F3 lo deje en 0 y se retire. RETIRADO 2026-08-13: con la opción borrada del tipo, el censo es npm run check — un error de compilación en vez de un informe. Lo de abajo (§3.1) se conserva como inventario de hallazgos.

3.1 Campaña de emisión — censada, clasificada — SUPERADA POR §3.0

Redirigen el estampado con targetOverride y su morfo no lo declara. El guard del runtime los señala uno a uno al ejecutarse.

No anotar a ciegas. Por cada uno, la pregunta primero es ¿sobra la redirección? — como en A-36, donde el arreglo fue retirarla. Anotar una redirección innecesaria la fosiliza. Sólo cuando es legítima (parte repetida: el day pulsado, el item clicado) se declara allowedTargets.

El censo, ahora reproducible

scripts/__scratch-target-override-census.ts (SCRATCH, sin commitear):

node --import tsx/esm scripts/__scratch-target-override-census.ts

Cruza cada trigger(name, { targetOverride }) de soma/components + eidos/components + soma/layers contra target + allowedTargets, y resuelve en qué parte aterriza cada override con el AST de TypeScript en dos fases. Hace falta interprocedimental: el elemento llega casi siempre como PARÁMETRO (select(date, target?)), y quien lo pasa vive en otro fichero.

Marcas del informe: ~ la parte se dedujo del e.currentTarget siguiendo dónde se cablea el handler · ↑ la trajo un llamante · «markup» la trae el template de un .svelte, que el censo NO lee.

El número real no era 71/33 sino 119 eventos / 46 componentes (13 declarados). La diferencia no es un error del censo viejo: incluye eidos/components y los nombres computados.

Tres trampas medidas al construirlo, por si se re-hace:

  • Resolver el nombre del método globalmente importa partes de otro componente: select / prev / next los tienen doce, y Calendar heredó el branch-control de TreeView. El salto no puede salir del componente.
  • opts.ref.current como RECEPTOR de una consulta (…current.querySelector(…)) es la raíz donde se busca, no el aterrizaje. Contarlo inventaba un provider en carousel, command y todo el que busca su item desde la raíz.
  • e.currentTarget NO es la parte de la clase que declara el handler: CalendarProvider aloja el keydown del día y su clase registra provider. Hay que seguir el handler hasta dónde se cablea.

Punto ciego conocido: un override pasado en una VARIABLE (trigger(name, opts)) no se detecta — se mira el texto del segundo argumento.

Las cuatro clases

Clase Qué Qué se hace
A · no-op el override re-dice el destino declarado, que es singleton RETIRAR — mientras esté, el guard no distingue una redirección real
B · parte repetida day · item · row pulsado nada: la parte YA es el target
C · partes hermanas checkbox-item / radio-item junto a item; prev-trigger / next-trigger junto al provider declarar allowedTargets
D · contradicción aterriza donde el morfo no dice, o en algo que NO es parte decidir: retirar, o extender el morfo

Cerrado en este pase

  • aura — delegate-offer y delegate-escalate estampaban con el ref del provider, que es exactamente lo que el runtime resuelve solo: no-op retirado. sustain-processing (declara indicator) y los tres terminales (declaran ring) caen al provider cuando la parte decorativa no está montada: override sólo en la ausencia + allowedTargets: [provider] en los cuatro. ⚠️ Retirar el override de sustain-processing del todo PARECÍA correcto — «es post, sólo se pierde el ornamento»— y el navegador lo desmintió: la prueba monta el Aura sin Indicator y salieron 10 rechazos de SomaRuntimeTargetError. El ornamento no se pierde en silencio.
  • checkbox — el provider ES el botón (archetype: 'trigger'), así que los dos handlers le pasaban al runtime el mismo elemento que ya resuelve. Retirado el parámetro triggerEl de toggle() y actualizados los 6 llamantes.
  • Verificados SIN cambio (clase B, el override elige la instancia correcta de una parte repetida): calendar commit-select/unselect · carousel · clipboard · color-field (dos partes registran input; pasa la visible a propósito) · command · listbox · radio-group · range-calendar commit-* · toggle-group · tree-grid · tree-view · month-grid commit-set · table commit-select · tabs · feed · proof-of-human · form commit-submit.

⚠️ Clase NUEVA — submenús mudos (onion-menu · dropdown-menu · context-menu)

No es deriva de destino: el morfo no declaraba el evento, así que el provider no tenía qué emitir. Tres componentes, el mismo agujero, y en los tres la anatomía YA lo anticipaba (item de onion-menu declaraba states: ['expanded','collapsed']; sub-content de los menús declaraba states: ['open','closed'] y su data-state).

  • onion-menu — desplegar un submenú, el gesto que DEFINE un menú radial, emitía nada: clickItem() sólo emitía en la rama hoja. Añadidos emerge-expand / emerge-collapse sobre item, con la forma de TreeView (misma familia+verbo: un nivel revela sus hijos en el sitio, libro cap. 26). Tocar fondo del drill sigue siendo close, no un collapse. Medido: emerge-expand estampa el sector (depth 1) y desestampa limpio; Backspace → emerge-collapse, depths 2→1. ⚠️ Probé primero con Escape esperando el collapse y salió close: la tecla de drill-up es Backspace según el mapa del propio morfo. El código estaba bien; la prueba estaba mal.
  • dropdown-menu y context-menu — MenuSubProvider.handleOpen/Close sólo volteaban estado. Añadidos sub-open / sub-close sobre sub-content en AMBOS gemelos (arreglar uno solo era la incoherencia que este eje persigue). Eventos distintos del open raíz a propósito: el sub es su propia superficie flotante, su commits escribe su propio data-state, y un pack debe poder distinguir «se abrió el menú» de «se abrió el submenú». sub-open nace post y sub-close pre + targetFallback: [sub-trigger] — la receta de float-panel, para no repetir A-36 en código nuevo. Medido: sub-open estampa data-dropdown-menu-sub-content. ⚠️ sub-close NO se ejerció en navegador: su camino es la coordinación SafePolygon (onPointerExit), que un pointerleave sintético no dispara. Queda por medir.

✅ F4 CERRADO (2026-08-10) — el A/B de A-36, ahora en verde

Aplicada la receta de float-panel a dropdown-menu Y context-menu (los dos gemelos; arreglar uno era la incoherencia de siempre): open pre→post, close sigue pre + targetFallback: [trigger] declarado, y los providers dejan de resolver contentRef ?? gestureTarget ?? partRef('trigger') a mano. El gestureTarget desaparece de la firma: pasarlo era justo lo que pisaba el contact-activate del Button compuesto.

Medido en /uix/components/dropdown-menu, el A/B que define el eje:

ANTES   contact-activate TRIGGER · contact-activate TRIGGER (+1 ms) · open PERDIDO
AHORA   contact-activate TRIGGER +9903ms · open CONTENT +9906ms
        close CONTENT (Escape)

Dos superficies, ambas expresando. El open ya no se pierde: bajo pre su destino declarado no había montado y no aterrizaba en ningún sitio.

check DELTA CERO · 440✓ (ambos menús + morfo + sema) · chronos sigue sano tras el pase (42 celdas · 17 chips · 3 botones · SSR ok).

⚠️ F4 — cómo se descubrió (medida previa, 2026-08-10)

Instrumentando el submenú salió la prueba del superviviente de A-36: al abrir el menú raíz se estampan dos contact-activate a 1 ms sobre data-dropdown-menu-trigger, y el open de la raíz no aparece en absoluto — es pre con target: content, y a esa altura el content no ha montado. No es que colisione: es que se pierde. La receta de F4 (open pre→post) sigue siendo la correcta y ahora está medida, no inferida.

⚠️ Hallazgo — A-36 tiene UN superviviente: dropdown-menu

Dos componentes comparten la FORMA del código de A-36 —open/close con target: content en sequence: 'pre' y el provider resolviendo contentRef ?? gestureTarget ?? partRef('trigger')— pero sólo uno comparte el DEFECTO, y la diferencia está en la composición, no en el provider:

  • dropdown-menu SÍ. eidos/…/dropdown-menu-trigger.svelte renderiza el Trigger de soma por su snippet child con un <Button> dentro, y su propia cabecera lo dice: «el [data-dropdown-menu-trigger] aterriza en el MISMO <button> que emite Button». Un elemento, dos morfos — la geometría exacta del Drawer en A-36.
  • context-menu, no POR DEFECTO — pero alcanzable. Su Trigger renderiza un <span> envoltorio, así que lo que el consumidor mete dentro es un HIJO: dos elementos, no uno. Pero el Trigger acepta snippet child, y con él el consumidor pone su propio control en el MISMO nodo — la colisión vuelve. Y el <span> es a su vez un defecto aparte, ver abajo.

⚠️ Esta distinción salió de MIRAR la composición. Al primer pase los agrupé por el parecido del provider y di los dos por supervivientes — el mismo error de clase que dejó vivo a dropdown-menu cuando se cerró A-36: casar patrones en vez de medir.

⚠️ Hallazgo aparte — el Trigger de context-menu viola su propio contrato

Encontrado al comprobar lo anterior. Tres fuentes, dos respuestas:

  • el morfo declara defaultElement: 'div' · archetype: 'trigger';
  • components/context-menu-trigger.svelte renderiza un <span>, y el README de soma y la cabecera del componente de eidos lo documentan como <span>;
  • no hay decisión escrita en docs/ que justifique el span.

Y el elemento no cumple lo que su arquetipo promete: sus props son data-state + data-disabled + oncontextmenu, sin tabindex, sin role y sin onkeydown — mientras el morfo le cuelga aria-haspopup="menu" y aria-expanded, que son estado de widget y no significan nada sobre un elemento no interactivo ni enfocable. Tampoco hay camino de teclado (APG: Shift+F10 y la tecla de menú contextual); el componente es sólo ratón / pulsación larga. Los demás archetype: 'trigger' del árbol (checkbox, dialog, dropdown-menu) sí son controles reales.

Para dropdown-menu, la receta ya está escrita en float-panel:

  • morfo open: sequence: 'post' (el content monta durante el trigger y el trigger conserva su superficie para sus propios eventos).
  • morfo close: sigue pre + allowedTargets: [v.partRef('trigger')].
  • provider: el flip de estado pasa a events: { open, close } y el open emite sin override; el close resuelve content ?? partRef('trigger') — sin gestureTarget, que es lo que hoy arrastra item / checkbox-item / radio-item / sub-trigger al aterrizaje.

No lo he tocado: son dos overlays muy usados, el cambio de pre a post mueve el commits de sitio y §5 exige medirlo en navegador. Es lo siguiente.

⚠️ chronos — el diagnóstico real y la decisión revocada (2026-08-10)

Auditado con scratchpad/chronos-parts-audit.mjs: chronos declara 26 partes y registra UNA (provider). Los ~25 marcadores restantes, y 17 role=, están escritos a mano en el template de eidos, así que el ARIA declarado en el morfo (p. ej. grid: aria-labelledby→heading, aria-readonly, aria-disabled) no llegaba nunca al DOM. Por eso chronos dependía de targetOverride: sin registro, partInstance devuelve null para todo.

Decisión revocada. 29ffe902a (2026-07-13) fijó «reuse of a piece of chronos rides the pure engine, not a public compound API». La mitad de REUTILIZACIÓN es correcta y se mantiene (un componente no es librería de otro). La mitad que el comentario de exports.ts extendió a «no public compound parts» como regla de API de consumidor se anula: contradecía la doctrina composicional (feedback-compositional-not-data-driven, con precedente de rechazo en OnionMenu), dejaba a chronos como el ÚNICO de su clase con estructura cerrada (Table y Calendar exponen la suya), y produjo el monolito que causó lo de arriba. Su cautela declarada («hasta que RRULE/resources estabilicen las firmas») ya había caducado. Los datos siguen siendo datos (como el TableInstance de Table); lo que se abre es la ESTRUCTURA.

Hecho: event-chip de punta a punta — ChronosEventChipProvider + componente de soma + barrels + recableado del chip de mes. Cadena Tooltip.Trigger → EventChip → DragDrop.Draggable → un solo elemento, con los tres morfos fundidos en él. Medido en navegador (17 chips): el contrato sale ya del morfo — role, aria-label, data-event-id y data-color, que no se había emitido nunca. Consola limpia; 228✓; check sin errores nuevos.

⚠️ Y el navegador cazó un defecto que la declaración escondía: el morfo daba al chip defaultElement: 'button' + type literal, y el primer render por el runtime puso type="button" sobre un <div>. La causa es de anatomía, no de cableado: el chip CONTIENE el asa de redimensionado, y el modelo de contenido de <button> no admite un control anidado. Corregida la DECLARACIÓN (div + role='button', tabindex propiedad de soma), no el DOM. Era invisible mientras nadie registraba la parte.

C1 CERRADO (2026-08-10) — las 6 partes que emiten registran ya: event-chip · event-resize-handle · day-cell (×42) · prev/next/ today-button. Cuatro clases de provider (los tres botones comparten una: mismo contrato, parte distinta — patrón de paginación de Calendar), cuatro componentes de soma, barrels, y la vista recableada por child.

chronos ya no usa targetOverride en ninguna emisión (censo global 99 → 91):

  • shift-navigate — redirección RETIRADA. El morfo declara provider sin allowedTargets; ceder el botón pulsado era deriva (lección A-36).
  • commit-save / commit-delete / commit-move — anclados en el CHIP que nombran vía partInstance, con targetFallback: [provider] declarado para cuando no hay chip (un alta, o el borrado que acaba de quitarlo). Esto responde al bloqueo original: los botones Guardar/Eliminar son IconButton compuestos dentro de un Dialog compuesto y no son partes de chronos — el SPEC ya decía que el morfo del editor lo aporta Dialog.
  • commit-restore / commit-apply — parámetros muertos retirados.

Medido en navegador: las 4 partes con id del runtime, aria-label traducido saliendo del morfo («Hoy», «lunes, 1 de junio de 2026»), 0 errores de consola, 0 quinientos. 260✓ (6 fallos = base ajena de contracts.test.ts); check DELTA CERO.

⚠️ Trampa de verificación medida dos veces: al exportar una parte nueva del barrel, el grafo de módulos SSR de Vite va por detrás y la página revienta con X is not defined (500) mientras el cliente ya renderiza bien. Se resuelve navegando de nuevo. No confundirlo con un fallo del código — y NO reiniciar el dev server (ver reference-dev-server-restart-orphans-browser).

C2 CERRADO (2026-08-10) — clase ChronosRegionProvider + componente Region para los 8 singletons estructurales (una clase para todos, como SidebarRegionProvider: misma forma, parte distinta, sin ramas). Migrados toolbar · heading · grid · grid-head · grid-body · time-grid · all-day-row · agenda, medidos en las TRES vistas (mes 42 celdas · semana 168 slots · agenda 18 grupos), porque cada vista monta regiones distintas y una sola no las cubre.

Y con eso el contrato ARIA del morfo está vivo por primera vez. Medido en /uix/components/chronos:

grid → role="grid" · aria-readonly="false" · aria-disabled="false"
       aria-labelledby="uix-chronos-region-c519" → «junio de 2026»
grid-head → role="row"      (el template ya no lo escribe)
grid-body → role="rowgroup" (idem)

aria-readonly y aria-disabled no se habían emitido nunca. Y los role de grid-head/grid-body se han BORRADO del template: los declara el morfo, que es el objetivo de C2.

⚠️ La trampa que costó el hallazgo, y que se repetirá en C3: registrar la parte NO basta para un partRef. aria-labelledby en grid es v.partRef('heading') gateado por part-present, y un partRef lee el bag parts del provider, no el registro. Chronos construía su runtime con {}, así que el grid registraba, su role y sus dos aria aparecían, y aria-labelledby se quedaba nulo en silencio. Arreglado publicando headingId como fuente (parts: { heading: … }), que es lo que Dialog hace desde siempre. Cualquier parte que otra referencie por partRef necesita lo mismo.

⚠️ La misma trampa mordió DOS veces, por el otro extremo: omitir una fuente que la parte SÍ declara hace desaparecer el atributo en silencio. Al quitar el aria-label a mano del time-grid —que el morfo declara como propRef('ariaLabel')— la región se quedó sin nombre accesible hasta añadir ariaLabel al bag de props del Region. Igual con el aria-label del grid, que depende de que heading esté registrado. Regla: al migrar una parte, cruzar sus aria/data declarados con las fuentes que el provider publica, y comprobar el VALOR en el DOM — no que el atributo exista.

Por eso la medida fue en las tres vistas: el time-grid y el all-day-row sólo existen en semana/día, y la agenda en la suya. Verificar en la vista por defecto habría dado verde con dos regiones sin comprobar.

C3 EN CURSO — las repetidas se parten en dos por una línea que conviene ver:

  • Sin fuentes de valor (sólo role, o un attr literal): grid-row, head-cell, now-indicator. Migradas reusando la MISMA clase Region, sin componente nuevo: el registro por instancia de F2 es lo que hace segura una clase para muchos nodos. Medido: 6 filas + 7 cabeceras registradas por instancia, con role="row" / role="columnheader" saliendo del morfo.
  • Con propRef (necesitan valores por instancia): en vez de una clase por parte, Region recibe un bag sources — un thunk, no una instantánea, porque el runtime lo re-lee y así los attrs siguen siendo reactivos cuando el mes cambia debajo. Migrados así day-number (42/42, data-value desde el propRef), day-column, time-slot y agenda-group.
  • agenda-item: clase propia (ChronosAgendaItemProvider + componente), porque renderiza un <button> REAL — su morfo declara role + el literal type, y a diferencia del chip del mes no anida ningún control, así que aquí el elemento nativo es el honesto.

C3 CERRADO 9/9 (2026-08-10) — more-link migrado. Clase propia (ChronosMoreLinkProvider + componente), NO Region: el precedente es agenda-item — renderiza un <button> real y Region cae a <div>, que pondría el type declarado sobre un no-botón (el defecto que destapó el chip). Cadena Popover.Trigger → MoreLink → Button → un solo elemento. ⚠️ El bag del popover va el último en el spread: el aria-labelledby de su content cae al id del trigger, así que esa identidad tiene que ganar. Medido en /uix/components/chronos: data-count="6" (nunca se había emitido), role, type y aria-label del morfo conviviendo con aria-haspopup / aria-expanded / aria-controls / data-state del popover en el MISMO nodo. check 74 = base, DELTA CERO · 458✓.

⚠️ CUARTA declaración falsa destapada al registrar — y esta no es de chronos: v.translationRef('moreEvents') normaliza la clave literal (compile.ts:normalizeTranslationRef → #?components.chronos.moreEvents), NO consulta el mapa texts del morfo. El catálogo la tiene como more-events, así que el lookup falla y sale el FALLBACK inglés. Medido con la página en es:

today-button  aria-label="Hoy"              ← clave 'today', coincide con el catálogo
prev-button   aria-label="Previous month"   ← 'prevMonth' ≠ 'prev-month'  → fallback
next-button   aria-label="Next month"       ← 'nextMonth' ≠ 'next-month'  → fallback
more-link     aria-label="Show more events" ← 'moreEvents' ≠ 'more-events' → fallback
day-add       aria-label="Nuevo evento"     ← eidos tr() con clave guionada: SÍ traduce

El guionado no es el problema: tr() lo resuelve. La regla real es que translationRef exige la clave del catálogo, y un alias camelCase muere en silencio. npm run translations:check —el guard que lo cazaría— CASCA CORREGIDO 2026-08-11: el crash era el stripper de comentarios rompiendo un https:// dentro de una cadena (no una «url multilínea»), y además el guard nunca escaneó las llamadas translationRef/commonRef — esta clase pasaba en verde con el script sano. Reescrito sobre el contrato compilado y censados los 166 morfos (§0 punto 3): 172 hallazgos arreglados, 0 errores.

⚠️ Label-in-Name (WCAG 2.5.3) en more-link: el texto visible es {count} more… (además SIN traducir, a diferencia del resto del fichero) y el nombre accesible pasa a ser el aria-label del morfo, que no lleva el número. Decisión de C4: o el aria-label se hace propRef con el conteo, o se retira y nombra el contenido — pero entonces el texto visible tiene que traducirse.

ChronosRegionProps admite class y style explícitos: el morfo posee role/aria, la receta posee la caja. Declarados uno a uno en vez de tragados por la firma de índice, para que el reparto se lea en el tipo.

⚠️ REGISTRAR UNA PARTE DESTAPA DECLARACIONES FALSAS — tres veces ya

El patrón del pase, y hay que contar con él en lo que queda: mientras nadie registraba la parte, el morfo podía mentir sin consecuencia. En cuanto el runtime emite lo declarado, la mentira se materializa.

Parte Declaraba Qué pasó al registrarla
event-chip defaultElement: 'button' + literal type type="button" sobre un <div> — el chip ANIDA el asa de resize y el modelo de contenido de <button> no lo admite
grid-row archetype: 'item' La semana entera se pintó como fila de lista: archetypes.css da a [data-archetype='item'] cursor pointer, suelo de altura y FONDO EN HOVER. Una semana es estructura; el item es la celda
grid / time-grid aria por propRef el atributo desaparecía en SILENCIO si el provider no publicaba la fuente

En los tres casos el arreglo fue la declaración, no el DOM ni el CSS. Y sólo el del grid-row era visible a simple vista — los otros dos exigieron mirar el valor concreto en el DOM.

✔ El botón «Hoy» NO tiene bug (verificado 2026-08-10)

Reportado como que sólo movía el día y no el mes. Medido: desde noviembre de 2026 → pulsar Hoy → agosto de 2026. Correcto.

Lo que despista: hoy es 2026-08-10 y el demo arranca en junio porque le pasan date fijo. Estando ya en agosto, Hoy no tiene mes que cambiar y sólo mueve el foco. ⚠️ Mi primera medición dio un falso positivo por esto mismo — avancé dos meses desde junio y aterricé sin darme cuenta en el mes actual. Al probar navegación temporal, salir primero del mes de hoy.

Estado de chronos al cerrar la sesión del 2026-08-10

C1 · partes que emiten ✅ (6) — chronos con CERO targetOverride
C2 · singletons ✅ (8) — medidos en las TRES vistas
C3 · repetidas ✅ 9 / 9
C4 · morfo ✅ 7 / 7
C5 · composición pública rebanadas 1-2 de 4 — toolbar + overlays

C4 — hecho (2026-08-10)

Los 6 marcadores data-chronos-* que el morfo no conocía, resueltos así:

Marcador Verdicto Cómo
day-add PARTE pública Registrada: ChronosDayAddProvider + componente <DayAdd>; el <IconButton> de la vista se compone por child. El aria-label a mano DESAPARECE — sale del morfo. 42 instancias medidas
timegrid → time-view PARTE privada, RENOMBRADA Era el shell de semana/día, a UN guion de la parte time-grid que vive dentro: trampa. Marcador inline (parte de display puro), 4 reglas CSS renombradas
peek-panel · day-peek PARTES privadas Sin role ni aria: marcador inline, que es la forma sancionada para una región estructural que no debe ser class-hook
edit-action PARTE pública, registrada ChronosActionProvider + <EditAction> sobre el IconButton del editor. Ver decisión 1
search-button PARTE pública, registrada Abre una paleta Command.Dialog sobre TODOS los eventos. Ver decisión 2

⚠️ La premisa del plan era falsa en dos puntos, medidos:

  1. Decía «las 5 partes reales que el CSS de eidos estiliza». search-button no tiene ni una regla CSS ni un onclick en todo el repo: una sola aparición, el atributo. No es una parte por declarar, es una lupa que no hace nada.
  2. Suponía que estos marcadores estaban en rojo. eidos-lint chronos medido antes y después (restaurando morfo+css de HEAD): invalid: 0 en ambos. Los clasificaba como eidos-only, no como inválidos. Lo que cambia es morfo-backed 32 → 43 (y eidos-only 53 → 42): el contrato pasa de señal DOM sin declarar a declarado. Ese es el valor, no un guard en rojo.

⚠️ class-hooks: 115 en chronos, sin cambio. La norma S1 de eidos-lint los prohíbe (component:audit exige 0). Es la familia .chronos-tg-* / .chronos-peek-* entera: deuda grande, PREEXISTENTE, y un eje propio.

Y de paso, el arreglo de i18n medido (§C3): con las claves del catálogo, prev/next/more-link pasan de fallback inglés a «Mes anterior» · «Mes siguiente» · «Ver más eventos» en una página es.

Verificado: check 74 = base, DELTA CERO · 458 ✓ · contracts.test.ts los MISMOS 6 fallos ajenos (ninguna fila de chronos) · docs:check 0/621 · rtl:check 0/177 · navegador en las TRES vistas + el peek abierto, consola limpia en pestaña nueva.

⚠️ Dos trampas de verificación en este pase:

  • prettier --check sobre una copia del fichero FUERA del proyecto no valida nada: no resuelve el plugin de svelte y falla parseando, lo que se lee como «pasa» si sólo miras la lista de warns. chronos-view.svelte YA estaba sucio en HEAD; lo descubrí sólo al comprobarlo in situ.
  • prettier --write sobre él lo pasó a LF y produjo un diff de 719 líneas (reference-git-crlf-wholefile-diff). Revertido y reaplicados los tres cambios a mano: el diff real es 34 líneas. No formatear ficheros que ya estaban sucios.

C4 — las 3 decisiones, resueltas por el autor (2026-08-10)

1 · edit-action → DECLARADA. El plan decía retirarla; el autor decidió lo contrario y es lo coherente: retirarla no era ejecutable (su única regla CSS la posiciona en la esquina del diálogo, y cambiar el gancho por una clase es justo lo que la norma S1 prohíbe), y el precedente del mismo día lo contradecía — more-link dentro de un Popover compuesto y day-add dentro de un IconButton compuesto SON partes. Alcanzar un componente compuesto con tu propio attr con namespace es el patrón sancionado. Medido: role · type · aria-label="Editar evento" del morfo, la caja sigue a 20/20 px de la esquina del diálogo (medido contra el rect real), y el click sigue abriendo la edición (19 campos data-readonly → 0).

2 · search-button → PARTE + PALETA sobre TODOS los eventos. No había spec en ninguna parte (ni SPEC.md ni CONTINUE.md; el README sólo lo mencionaba como composición) y en soma/engine no había una línea de búsqueda, así que la conducta la eligió el autor: paleta que busca en todo el almacén, no filtro de la vista. Construido con Command.Dialog del propio framework — que YA existe (paleta modal sobre el Dialog de eidos, con mod+k de serie) y que estuve a punto de recomponer a mano: leer el barrel antes de escribir markup.

  • Provider: goToDate(date), la generalización de la que goToday era un caso particular (mismo ancla rovinga, anuncio y emisión shift-navigate).
  • El texto buscable viaja en keywords: el scorer de Command lee value + keywords y NUNCA los hijos renderizados, y el value es el id del evento. Por eso se busca por título, por tipo Y por fecha.
  • Una serie recurrente sale UNA vez, igual que openEdit, que edita la serie.

Medido: ctrl+k ✓ · meta+k ✓ · el botón ✓ · 18 eventos listados con «Título — 8 jun 2026 · Todo el día» · filtro por título («trip» → 1) y por fecha («18 jun» → Retro) · vacío traducido · y el salto de marco: noviembre de 2026 → elegir «Retro» → junio de 2026 con el editor abierto.

⚠️ Lo que NO se pudo verificar: que la paleta se DESMONTE al elegir. Su data-state pasa a closed (correcto), pero el nodo permanece porque el panel oculto congela las animaciones de salida — medido: document.hidden = true y scale-out/fade-out en running con currentTime: 0. Control: el diálogo del editor tampoco cierra con Escape en ese estado (22 animaciones congeladas). Es el entorno, no el código — pero queda por confirmar con el panel visible.

3 · undo-button / redo-button → CABLEADOS. ChronosNavPart pasa de 3 a 5 partes (misma clase, el patrón de paginación de Calendar) y la barra los renderiza en su propio chronos-nav-group. Medido de punta a punta:

Retro, 18 jun 2026  → drag → 25 jun  → undo → 18 jun  → redo → 25 jun
undo deshabilitado al arrancar (aria-disabled=true + data-disabled), habilitado tras el move

⚠️ Y destapó la MISMA clase de fallo de traducción, tercera vez: los dos declaraban v.commonRef('action.undo') y common.action no existe — el grupo es common.buttons. Añadidos buttons.undo / buttons.redo («Deshacer» / «Rehacer») y repuntadas las declaraciones. docs:vocabularies regenerado (38 → 40 shared strings), que es lo que puso docs:check en rojo hasta regenerarlo.

⚠️ La abstracción llegó a sus dos consumidores: ChronosDayAddProvider pasa a ChronosActionProvider con parte 'day-add' | 'edit-action' — mismo contrato (botón real, role + type + aria-label traducido, CERO fuentes), el patrón de NavButton. Los componentes siguen siendo dos con nombre propio (DayAdd, EditAction), que es lo que C5 necesita para la API compuesta.

C5 — composición pública, rebanada 1 de 4 (2026-08-11)

Qué partes son públicas NO es cuestión de gusto: es kind en el morfo. 21 públicas · 12 privadas (los internos del time-grid, las filas de agenda, las superficies del day-peek). Ese es el criterio, y evita inventarse una API.

Hecho en esta rebanada:

  • Namespace Chronos.* con la forma de Table/Calendar (index.ts), y la raíz con ranura de composición: pasar children REEMPLAZA el cuerpo por defecto. Antes se renderizaban además de la vista completa —un segundo árbol colgando de un calendario entero—; ningún consumidor los pasaba.
  • 8 partes de toolbar: Toolbar · Heading · PrevButton · NextButton · TodayButton · UndoButton · RedoButton · SearchButton. Ergonómicas al estilo Calendar.PrevButton: componen el IconButton/Button del sistema, su glifo y la llamada al provider, así que el consumidor recibe un control que FUNCIONA, no un gancho vacío. children sustituye el cuerpo de cada una.
  • El cuerpo por defecto se recompone desde ellas. No es un camino privilegiado: es el primer consumidor de la API compuesta. Sólo queda literal el cromo que no es parte del morfo (insignia de fecha, bloque de título, select de vista).
  • searchOpen sube al provider. En un toolbar compuesto el botón y la paleta viven en subárboles distintos; el provider es el único sitio que alcanzan los dos. Es estado de componente, como editing.
  • ⚠️ Y la prueba compuesta destapó un hueco real: el SearchButton compuesto no abría nada, porque la paleta vivía en el cuerpo por defecto. Los overlays pertenecen a la RAÍZ. Extraída a chronos-search-palette.svelte y renderizada siempre.

C5 — rebanada 2 de 4: los overlays a la raíz (2026-08-11)

El editor tenía el mismo hueco que la paleta y se cierra igual: extraído a chronos-editor.svelte (Dialog + editorCtx + save/remove) y renderizado por la RAÍZ. Un árbol compuesto que coloque EditAction o celdas de día — partes públicas— ya recibe editor cuando algo llama a openEdit/openCreate; antes, sencillamente, no tenía ninguno. chronos-view.svelte baja de 1199 a 893 líneas y pierde 7 imports que la extracción dejó huérfanos.

Medido con el discriminador que sí distingue de quién es el editor (el portal del Dialog hace que contains() no sirva): mover sólo la instancia compuesta 5 meses y luego elegir desde SU paleta un evento de junio.

away   → [junio (defecto), noviembre (compuesta)]
elegir «Retro» en la paleta de la COMPUESTA
after  → [junio (defecto), junio (compuesta)]   ← la de por defecto NUNCA se movió
1 editor abierto · title «Evento» · EditAction presente

Es decir: el handler corrió en el provider de la instancia compuesta, y el editor que apareció es el suyo. check 74 = base · 458 ✓ · consola limpia.

Dos declaraciones falsas más, del mismo patrón de siempre: toolbar declaraba header y la vista pintaba un div; heading declaraba div y pintaba un span. Los wrappers renderizan lo declarado, medido sin desplazamiento (la fila del título es flex, así que el div sigue en la misma línea que la insignia de semana, y el aria-labelledby del grid sigue resolviendo).

Medido con DOS instancias en la misma página (la de por defecto y una compuesta a size="sm"):

next en la COMPUESTA  → junio → julio, la de por defecto NO se mueve
prev en la DEFECTO    → junio → mayo,  la compuesta NO se mueve
ids de heading distintos (s15 / s347) → registro por instancia (F2) sostiene
la paleta de cada raíz lista sus 18 eventos por separado

check 74 = base · 458 ✓ · morfo:check PASS chronos · docs:check 0/621 · rtl:check 0/177 · eidos-lint invalid 0 · consola limpia en las tres vistas.

⚠️ C5 rebanada 3 — el bloqueo real, medido antes de empezarla

Extraer MonthView NO es mecánico, y la razón cambia el plan. Cruzando los helpers de la vista contra sus tres regiones:

Helper mes semana/agenda
tooltipBody · tooltipCtx 1 3
chipKeydown 1 2
startResize / moveResize / endResize 1 1
dragPreview · eventTime 1 1
segsForWeek · overflowByDate · handleDrop · startCreateMonth sí 0

Casi todo es COMPARTIDO. Y varios de esos compartidos —chipKeydown, startResize, nudgeGrab— son comportamiento, que por doctrina es de soma, no de eidos: mapean teclas a llamadas del provider y hacen captura de puntero. Partir la vista sin mover eso antes no arregla el monolito: reparte la fuga entre cuatro ficheros.

Así que la rebanada 3 empieza por ahí: subir a soma el comportamiento del chip (teclado de mover/redimensionar, captura de puntero del resize, drag-to-create), y sólo después extraer MonthView / WeekView / AgendaView, que entonces sí son composición pura.

Rebanadas que quedan:

  1. Partes del grid de mes — Grid · GridHead · HeadCell · GridBody · GridRow · DayCell · DayNumber · DayAdd · EventChip · EventResizeHandle · MoreLink como wrappers de eidos, y un Chronos.MonthView que los componga (patrón Calendar.MonthView, que se auto-gatea sobre provider.view).
  2. WeekView / DayView / AgendaView con las 12 partes privadas dentro, y la vista monolítica reducida a un despachador.

Verificación al cierre: check 74 = base, DELTA CERO sostenido en cada lote · 458 ✓ (chronos + morfo + sema) · navegador limpio en las tres vistas. Censo global de targetOverride: 148 → 89.

⚠️ Defectos de conducta medidos en el navegador (2026-08-10) — eje NUEVO

Estado 2026-08-11: de los tres, el 2 está ARREGLADO, el 3 RETIRADO por no reproducible, y el 1 espera decisión tuya.

Salieron al responder dos preguntas del autor sobre la vista de mes. Ninguno es de superficie perceptual; se anotan aquí para que no se pierdan.

  1. La 6ª fila fantasma. NO ERA UN DEFECTO — REVOCADO 2026-08-13, ver §4. fixedWeeks viene true en chronos y false en Calendar / DatePicker / RangeCalendar / DateRangePicker, y de esa asimetría se dedujo una deriva. Es al revés: chronos es el único que escribió su decisión (SPEC.md:270, :492, :311/:461) y coincide con el default de FullCalendar y Toast UI; los otros cuatro son rejillas en popover, otro problema. La ficha además leyó mal el CSS y se dejó la mitad de los defaults — el detalle, en §4.

  2. Una ocurrencia de evento recurrente NO se puede mover. ARREGLADO 2026-08-11 — y el arreglo NO era el que parecía. Su id es sintético (${serie}::${YYYY-MM-DD}, engine/recurrence.ts:145) y moveEventToDay la buscaba en calendar.events, que sólo guarda la SERIE → find devolvía undefined y salía en silencio, después de haber iluminado el destino. Iba a resolver el id a la serie (como hace openEdit) hasta ver que canResize ya excluye las ocurrencias explícitamente («those edit via the series in v1»): la política escrita es bloquear la manipulación directa y editar por el diálogo, no desplazar la serie. Así que el arreglo es hacer VISIBLE ese bloqueo: canMove(event) en el provider, espejo de canResize, y disabled en los TRES DragDrop.Draggable (mes · all-day · timed). Medido: la ocurrencia ya no inicia el arrastre (antes: celda iluminada + nada al soltar), y un evento normal sigue cayendo (Retro 18 jun → 25 jun). ⚠️ El camino de TECLADO ya estaba bloqueado — por canResize, que gateaba las DOS permisos. Ahora cada flecha consulta la suya (Shift → resize, flecha sola → move); hoy los dos predicados valen lo mismo, así que ahí no hay cambio de conducta, sólo de significado. ⚠️ Queda la mitad cosmética: el chip bloqueado sigue con cursor: grab y sin data-disabled (el Draggable no lo estampa). Para pintarlo hace falta un gancho: o event-chip declara un estado nuevo, o DragDrop.Draggable estampa data-disabled — decisión, no bug. Los eventos normales SÍ caen en las celdas de fuera de mes — medido: Retro → 2026-07-08 y Launch → 2026-07-09 aterrizan y se repintan allí.

  3. El chip pierde data-event-id tras un intento de arrastre. NO REPRODUCIBLE — RETIRADO 2026-08-11. Lo reporté como defecto de contrato C1 (el attr es propRef + emit: 'value') tras verlo faltar en tres chips. Al ir a arreglarlo no aparece bajo NINGUNA condición. Probado:

    Condición Resultado
    Carga limpia, lectura inmediata · +50 · +200 · +1000 ms 17/17
    Re-render (mes siguiente y vuelta) 17/17
    Arrastre real que aterriza 17/17
    En mitad del arrastre · mismo tick del pointerup · microtask · macrotask · 1 frame · asentado 17/17

    La explicación más probable —no medida, y por eso no la doy por buena— es que fuera estado rancio de HMR: la misma sesión produjo otros dos fantasmas (DayAdd is not defined y dos soma::runtime.part) que al comprobarlos en pestaña nueva no existían. Lección de método: un hallazgo sobre el DOM en una pestaña que ha sobrevivido a varios HMR no vale; repetirlo en pestaña nueva ANTES de anotarlo.

⚠️ Hallazgo — chronos estampa fuera del morfo RESUELTO POR C1

Superado (2026-08-10, §C1): chronos tiene CERO targetOverride — el censo lo confirma. La decisión que este bloque pedía quedó tomada así: shift-navigate retiró la redirección (deriva, lección A-36); commit-save/commit-delete/commit-move anclan por partInstance al CHIP que nombran con targetFallback: [provider] declarado (los botones del editor son un Dialog compuesto y NUNCA fueron partes de chronos); commit-restore/commit-apply retiraron sus parámetros muertos. Se conserva el texto original sólo como registro de qué preguntaba:

commit-save y commit-delete declaraban event-chip y aterrizaban en los botones Guardar / Eliminar del editor; commit-move en la celda del drop; shift-navigate en los botones de navegación y day-cell.

La cola de la campaña — LISTA CONGELADA, ver §3.0 «QUEDA de las olas 2-4»

⚠️ Esta lista quedó ESCRITA ANTES de las olas 2-3 de F3 y ya no es la cola real — durante un día dio órdenes opuestas a §3.0 sobre nueve componentes. La cola VIVA es la de §3.0; de lo de aquí ya están CERRADOS por F3/F4: context-menu · dropdown-menu (F4, open pre→post + targetFallback) · editable search-field stepper (CROSS retirados — el provider declarado ES la superficie) · table (sort-trigger → column-header por closest + partInstance) · tag-group · tree-view (identidad item/branch) · command (item ratón + camino Enter; queda sólo link-item) · timeline (emerge-reveal anclado).

Siguen pendientes (coinciden con §3.0): file-upload · float-panel (drag/resize → header / resize-handle / resize-grip: ¿allowedTargets o retirar?) · grid-list (selection-checkbox) · month-grid y year-grid (shift-navigate-step → cell) · palabras (13 filas, diseño fino) · range-calendar (shift-navigate → day) · tags-input · command (link-item) · menu-dial / onion-menu / metrics (vistas de eidos: deben REGISTRAR sus partes) · path-trace / rotate-align / drag-drop (gestos handle) · virtual-list / virtual-grid · menubar · navigation-menu · css-field / number-field (commit-set + retirar targetOverride de su API pública) · select / combobox · calendar (day) · pagination (5 controles).

3.2 Restitución de persistentes — eje NUEVO, ninguno de regime lo cubre

Medido con el engine real:

afterWarn:               "signal-warn-invalid"   ← el aviso untilFix proyectado
duringCommit:            "commit-submit"          ← el commit lo pisa
afterCommit:             null                     ← al limpiar se lleva el aviso
engineStillHoldsWarning: true                     ← el engine lo cree vivo

Una persistente no compite por la ranura: es un estado al que la ranura debe VOLVER. Propuesta: tras el hold de una transitoria, el engine re-proyecta la persistente viva de ese nodo. Afecta a form, file-upload, textarea, media-player, announce, aura.

3.3 Eje B — ordenación y emisión

  • sequence: 'coincident' sigue sin implementar (idéntico a pre); 21 eventos lo declaran. Implementarlo (emit sin await + handler inmediato) o marcarlo reservado.
  • 4 continuos sin throttle contra la doctrina de sema.md: gradient-builder, color-picker (área), number-field, css-field emiten en CADA pointermove. Cinco compañeros sí throttlean (DRAG_SIGNAL_MS). ⚠️ rotate-align NO es infractor: throttlea por detente.
  • Las 68 demos emiten sin verb → desde el rediseño del 2026-08-06 el verbo elige el sonido, así que un play de emerge-close suena open y commit-delete suena tick en vez de tick.loss. El bloque está copiado 68 veces; extraer un helper a $demo es refactor aparte.
  • knob.handle-drop es pre con handler → data-dragging sobrevive 240 ms al soltar. button.contact-activate retrasa onPress 120 ms.

3.4 Eje C — cola de AUDIT-sema-2026-08-05, re-verificada abierta

2026-08-13 — esta cola se sigue desde CONTINUE-audit-queue.md, que lleva el protocolo de trabajo, lo firmado y lo abierto. Cerrados desde aquí: S-10 (el canal visual honra su propia bolsa, 44af38e6e) y S-34 (el validador de runtime atado al const, eda3b977b). S-33 firmado y sin ejecutar.

S-19 (AnnounceFn no compila contra uix.announce; ninguna raíz cablea el canal) · S-27/S-41 ARREGLADO 2026-08-12 (el gate de shouldPrime lee la preferencia: con off ni contexto ni listeners; test nacido en rojo) · S-10 (rama visual sin ?? → revienta el boot) · S-33 ('forbidden' no prohíbe) · S-34 (gemelo del schema copiado a mano) · S-35 (la etiqueta esquiva la política) · S-30/S-38 (5 waivers, ya desbloqueados por S-31).

3.5 Eje D — fable, lo que sigue abierto

M1 emission (D.2, aceptada y nunca implementada — cerraría los waivers de tooltip y gradient-picker) · M4 (enumPair frágil con 3+ valores) · M5 (tipar el matcher state) · M6 (IntentExpectedFamily deprecado, ×2) · E2 (globalThis.matchMedia crudo en eidos) · E3 (catch mudo en #renderSchemeCss).

3.6 Eje E — canon del ledger de blocks

A-47 (anillo de foco 1.43:1 sobre panel sólido) · A-69 (count-up: duration no significa segundos) · A-85 confirmado ARREGLADO 2026-08-11, y el veredicto se INVIRTIÓ: el censo dio 132 declaraciones en 59 morfos, y la solución no fue el prop-truthy de button ×132 sino una regla de VOCABULARIO — dos clases de precedencia (contract = runtime gana · naming/aria-label = el attr explícito del consumidor gana, como la propia cadena de nombre accesible de la plataforma). Las 132 declaraciones eran correctas; la precedencia era el defecto. consumerWins en el compilador, el label viaja en el bag (SSR incluido — antes ni salía en SSR, medido) y mergeProps resuelve consumer-first. A/B del ledger re-medido en verde; button simplificado (2026-08-11): retirados la entrada propRef + prop-truthy del morfo, el destructure del wrapper y la fuente del provider — el attr del consumidor fluye por restProps y gana por política; el tipo con su JSDoc se queda (documenta la API). A/B por stash: el insta-cierre del drawer en /blocks/site-header/preview reproduce IGUAL con el código original — es del entorno de esa instancia (el demo plano de drawer abre perfecto), no de la simplificación. Espejos manuales BARRIDOS (2026-08-11): 10 duplicados estrictos retirados — popover close · float-panel close · editable ×3 (su comentario ya confesaba «resolves to the same id») · palabras ×4 (bubble/slash/link/find) · y los 6 hand-merges consumer-first de wrapper (file-upload ×5 + command-list), que re-implementaban a mano la política de mergeProps; con ellos cayeron 4 errores de tipado preexistentes (check 74→70). ⚠️ Trampa de harness cazada: translate: (key) => key en el harness de editable divergía de producción (translate = langs.ts) — el bag resolvía refs crudos mientras el mock alimentaba las líneas a mano; alineado.

Normalización morfo-first de resolvedAriaLabel (2026-08-11) — las ~28 cadenas resultaron ser la danza de Button a escala: el prop público ES aria-label (con Without<…> sacándolo del passthrough), el wrapper lo destructura, el provider lo resuelve contra un default y lo devuelve al bag. Una sola superficie de consumidor, no dos. Cinco clases medidas:

Clase Qué declara el morfo Arreglo
1 propRef('ariaLabel'); el default vive SÓLO en la cadena el morfo pasa a declarar el default (translationRef con el ref VERBATIM del LANGS)
2 ya el translationRef correcto la cadena era duplicado puro: se retira sin tocar el morfo
+supr — la supresión por labelledby pasa a condition: prop-falsy ariaLabelledby (exige que el provider publique la fuente)
sin default propRef sin default real (radio-group, slider) el morfo NO declara nada — precedente Button
ESTADO ⛔ — BLOQUEADA: mapRef devuelve map[String(raw)] sin traducir, así que una etiqueta que cambia con el estado no puede subir (clipboard copy/copied · password-field show/hide · table expand/collapse). Ensanchar mapRef es decisión de contrato, no mecánica

Piloto meter cerrado y verificado (clase 1 + supresión): morfo declara #?components.meter.label|Meter con prop-falsy ariaLabelledby; fuera la cadena, la fuente, el prop de opts, el destructure del wrapper y el import huérfano. check 70 = base · navegador: un meter compuesto con aria-label="Storage usage" conserva el del consumidor. ⚠️ La aserción de supresión hubo que REFORZARLA: mirar el DOM ya pasa trivialmente (los naming no viajan por dom.apply), tiene que mirar el BAG.

Barrido EJECUTADO (2026-08-11) — 21 componentes migrados, 5 rechazados. Refs compilados 145 → 157: doce defaults que vivían en resolvers de provider son ya contrato. check 70 = base · soma 1269✓/1 (el timeout ajeno) · morfo+sema 494✓ · translations:check 0 · SSR de los 21 sin una sola fuga de #? ni __uix_lang_missing__. Medido en SSR: el consumidor gana donde lo pasa («Project location» en breadcrumb, «Uploading assets» en progress) y el default del contrato resuelve en castellano donde no («Opciones», «Carrusel», «Lista en cuadrícula») — y ahora sale en SSR, que antes no ocurría.

⚠️ El residuo es la parte valiosa: cuatro razones, dos que el análisis previo NO había previsto. Además de las state-dependent (bloqueante mapRef ya conocido: table row-detail · password-field toggle · tree-grid expand · media-player por control · clipboard) aparecieron:

  • INTERPOLADAS — translationRef nombra una clave, no admite params: carousel Slide N of M e indicator · tag-group Remove {tag} · nav-tree (nombre por nodo).
  • CROSS-ELEMENT ARREGLADA 2026-08-11 (ver abajo).
  • SUPRIMIDA POR <label for> ARREGLADA con ella — eran la misma clase por sus dos mitades.

Clase CROSS-ELEMENT — arreglada (2026-08-11)

El prop del consumidor se declaraba en una raíz que pinta un <div> sin rol, pero el nombre pertenece a un control HIJO (textarea → su <textarea>; mask-field · search-field · password-field → su <input>). No era «bloqueada»: era declarable en la parte hija. Y había un defecto de contrato real: el morfo declaraba la etiqueta INCONDICIONAL mientras el provider la suprimía ante un Field.Label y luego la pisaba siempre — su propio comentario lo confesaba («Overrides the morfo's raw translationRef default»). El plan del morfo se evaluaba y se tiraba.

Arreglo, en los 4: el default vive en la parte que posee el nombre, con la supresión DECLARADA (condition: prop-falsy fieldLabelled, leyendo un prop virtual que el provider publica — regla 2-de-3); la cadena resolvedAriaLabel desaparece; y el prop de conveniencia de la raíz se reenvía con spread CONDICIONAL.

⚠️ El spread tiene que ser condicional. Escribir la clave sin condición la pone a undefined cuando el prop falta y borra el default que el bag acaba de aportar — una clave presente gana a una ausente en un spread. Es la trampa de esta clase.

Medido: SSR con el prop del consumidor reenviado («Notes», «Masked value») y con el default del contrato en castellano donde no hay prop («Buscar», «Contraseña»); 0 fugas de #?. ⚠️ El tercer caso (dentro de Field.Label) NO está verificado en navegador: ningún demo compone estos controles dentro de un Field.Label. Queda cubierto por test (search-field y mask-field asertan not.toHaveProperty('aria-label')) — y de paso mejoró: la clave ahora está genuinamente AUSENTE en vez de presente-con-undefined.

navigation-menu y waveform CERRADOS (2026-08-11) — y resultaron ser dos casos distintos, no uno:

  • navigation-menu: el mismo patrón que los cuatro (raíz <nav> → <ul>). Mismo arreglo; la condición vive en las fuentes POR PARTE de la List (donde ya estaba orientation), no en el runtime raíz. Medido: <ul aria-label="Principal"> del catálogo, <nav> sin etiqueta.
  • waveform: NO era la misma clase. Su default se queda en el provider — correcto: el nombre lo lleva un <Slider> compuesto, que no es una parte suya, y morfo.texts es justo la forma sancionada para un slot que resuelve el provider. El defecto era otro y peor: su prop público era ariaLabel camelCase (único en el catálogo) y su tipo Without<PrimitiveDivAttributes, {}> no filtraba nada — un aria-label del consumidor, que es lo natural, aterrizaba en el <div> sin rol y se perdía en silencio mientras el Slider conservaba el default. Arreglado alineándolo con el catálogo: prop 'aria-label' + Without<…, { 'aria-label' }> + reenvío al Slider. ⚠️ El cambio de tipo cazó un consumidor real —media-player-time-slider pasaba ariaLabel—, que es exactamente lo que un tipo estrecho debe hacer. Medido: raíz <div> SIN aria-label, Slider con «Forma de onda».

Lección de la clase: antes de mover un default, mira si el elemento es tuyo. Si lo es, va al morfo de esa parte; si es de un componente compuesto, el texts + provider es correcto y lo que hay que auditar es que el prop del consumidor no acabe en el envoltorio sin rol.

table es caso aparte: su cadena la alimenta un prop público DISTINTO (label, con su propio JSDoc), no el passthrough — y su aria-label ya gana consumer-first hoy. Nada que migrar. Tabla completa de exclusiones en morfo.md Step 4. QUEDAN legítimos (no espejos ni cadenas): supresión por labelledby de navigation-menu/tree-view · handles de float-panel (fuente ÚNICA). La incoherencia de gemelos quedó RESUELTA (2026-08-11): range-calendar declara ya month/year-select con los MISMOS refs #?common.calendar.* que calendar, espejos del provider fuera, y un test fija que el bag los lleva (con translate identidad el ref crudo aflora — prueba de que la DECLARACIÓN llega). ⚠️ Ningún demo compone los selects de SOMA: el «month-select» del demo de calendar es la composición de EIDOS (Popover.Trigger+Button, nombrado por su TEXTO visible, marcador passthrough) — no confundirlo con la parte del morfo al verificar. En range-calendar/langs.ts quedan PREV_MONTH/NEXT_MONTH muertas PREEXISTENTES (no las tocó este pase). Detalle: morfo.md Step 4 + ledger §A-85 RESOLUCIÓN.


4 · Decisiones del autor, pendientes

Las dos de chronos, medidas y listas para decidir (2026-08-11):

  • La 6ª fila fantasma. fixedWeeks vale true por defecto sólo en chronos; Calendar, DatePicker, RangeCalendar y DateRangePicker lo traen false. Junio de 2026 cabe en 5 filas y se inventa una 6ª entera de julio. ¿Alineamos el defecto con la familia, o mantenemos la altura estable creciendo las filas en vez de añadir una semana? (Google Calendar hace lo segundo.) FIRMADA (2026-08-12): crecer las filas. REVOCADA POR EL AUTOR (2026-08-13) — NO SE TOCA: fixedWeeks se queda en true. La firma se dio sobre una premisa incompleta: nadie puso el SPEC del componente sobre la mesa, y el SPEC no dice una línea sino tres coordinadas — SPEC.md:270 («Rejilla de 6 filas constante (estabiliza la altura). Default: true.»), SPEC.md:492 (el inventario de reuso pide «Rejilla de mes 6×7») y SPEC.md:311+:461 (data-outside-month es un estado declarado de day-cell con su propio token de tema). La 6ª fila no es un fantasma: es la rejilla especificada, y la altura estable que la decisión perseguía ya está — medido: 115 px por fila y 724 px de rejilla en TODOS los meses.

    Y la comparación competitiva, verificada en las fuentes en vez de citada: FullCalendar fixedWeekCount viene true («the calendar will always be 6 weeks tall») y Toast UI month.isAlways6Weeks viene true — los dos referentes de librería de la propia matriz del SPEC (§2.1) hacen lo que chronos hace. Google/Apple/Outlook sí varían las filas, pero porque son aplicaciones que llenan el viewport y la rejilla se estira hasta el alto de la ventana (Google llega a exponer un ajuste de densidad para regularlo). Esa condición chronos no la tiene: sus filas son min-block-size y la rejilla crece con el contenido, así que no hay altura dentro de la que estirarse — por eso la receta propuesta tenía que INVENTARSE la altura con calc(6 * var(--_chronos-cell-min-block)), que es la altura de 6 filas. Era «seguir midiendo 6 filas de alto, pero con 5 filas más gordas»: misma altura, menos información.

    ⚠️ Dos errores de la ficha original, corregidos para que no se repitan: el default vive en DOS sitios (components/chronos.svelte:20 y engine/state.svelte.ts:64), y chronos.css:278 (grid-auto-rows: var(--_chronos-lane-height)) no es la altura de las filas del mes — es la pista de los carriles de chips DENTRO de una fila (.chronos-overlay, absoluta). Las filas del mes son hijos flex con min-block-size (chronos.css:188-193).

    Si algún día se quiere la conducta de Google, es una FUNCIONALIDAD, no un flip de flag: contrato de altura (la rejilla llena su contenedor) + maxLanes derivado de la altura medida — hoy es $state(3) fijo (engine/state.svelte.ts:66), así que crecer las filas no enseña ni un evento más. Eso necesita firma propia y una enmienda deliberada del SPEC.

  • Cómo pintar un chip bloqueado. Una ocurrencia recurrente ya no se arrastra, pero sigue con cursor: grab porque DragDrop.Draggable no estampa data-disabled. Dos vías: (a) event-chip declara un estado nuevo — local y barato; (b) Draggable estampa data-disabled — beneficia a TODOS sus consumidores pero es cambio transversal. FIRMADA (2026-08-12): la vía (b), con su letra pequeña medida e INCLUIDA. A favor, más de lo que la pregunta sabía: el morfo de drag-drop ya declara data-disabled en el draggable (drag-drop.ts:89, severity optional) — hoy es una declaración sin emisor, la clase de siempre — y el cursor: grab vive en drag-drop.css:24, así que la corrección del cursor aterriza allí para todos los consumidores. La letra pequeña: el chip fusiona tres morfos en UN elemento que sigue siendo clicable (abre el editor), así que (i) la regla :where([data-archetype='item'][data-disabled]) de archetypes.css:259 (not-allowed + opacidad) hay que anularla en la receta del chip — es :where, especificidad cero — y (ii) el aria-disabled prop-truthy del mismo morfo NO debe aterrizar en el elemento fusionado: estamparlo mentiría a AT sobre un click que funciona.

Del eje perceptual:

  • mode y scope — FIRMADA (2026-08-12): SE RETIRAN. El barrido de la regla 4 del ledger está hecho: 0 hits en docs/, 0 lectores del compilado, y el alcance real son 4 ficheros (SemaMode/SemaScope en sema/types.ts + morfo/types.ts:644-646 + compile.ts:196-198 + exports) — misma clase que collapse y lock. Pregunta hermana ABIERTA: SemaCause está más muerto aún (sólo types + exports, ni atraviesa morfo) — decidir si entra en el mismo pase.
  • regime es por EVENTO, pero la colisión es de la SUPERFICIE. Para la clase intra-morfo el evento vale; en A-36 chocaban dos morfos distintos y ninguno podía saber del otro. Si vuelve a aparecer una colisión cruzada que no se resuelva quitando la redirección, hay que decidir quién manda.

5 · Cómo verificar en este hilo

Línea base ANTES de tocar — hay 6 fallos AJENOS y preexistentes en contracts.test.ts (waveform barrel · provider filenames · MOR-4 · DOM writes de menubar · data-attrs · namespaces de aura), y 1 en el proyecto de navegador de soma (un timeout de 5 s en migrated providers re-declare no morfo attrs, reproducido con los cambios en stash). Clasificar míos vs ajenos por contenido del diff, nunca por confianza.

npx vitest run src/uix/sema src/uix/morfo src/uix/contracts.test.ts --project=server
npx vitest run src/uix/soma            # proyecto navegador
npm run check        # 74 errores = base
npm run docs:check   # 0 / 618
npm run rtl:check    # 0 / 177

El navegador NO es opcional en este eje. El A/B que aísla la causalidad: MutationObserver ligado al nodo (no a un selector) sobre data-event*, más getAnimations(). Dos trampas medidas hoy:

  • El panel oculto suspende rAF y no compone: un bucle sobre requestAnimationFrame se cuelga, animationstart no dispara y el screenshot falla. Muestrear con temporizador y leer getAnimations() / getComputedStyle, que sí son fiables ahí.
  • awaitExpression llega a su tope de 1500 ms con el panel oculto, porque las transiciones nunca terminan. No confundirlo con un defecto del código.

Audio: render offline replicando el grafo (OfflineAudioContext) y medir pico / RMS. Contar osciladores responde «¿llegó algo?», nunca «¿se oye?».

Powered by TurnKey Linux.