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.mdy sigue por §0.» Fecha: 2026-08-10, actualizado 2026-08-11 (cierre) · Ramaalpha-0.1-dir-prefs, pusheada en61457baac. ⚠️ COMPARTIDA: otra sesión trabaja enblocksy 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: HECHA
2026-08-12 — la receta de los 21 aplicada tal cual (alert-dialogmorfo.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, ynormalizeTranslationReftampoco los tocaría, así que una clave relativa llegaría cruda ats()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:
HECHA 2026-08-12.alert-dialog— la única cadena mecánica que queda (30 min).Dos decisiones del autor (§4)FIRMADAS 2026-08-12: el chip bloqueado (vía b + su letra pequeña) ymode/scope(retirar). La tercera,fixedWeeks, quedó REVOCADA el 2026-08-13 al aparecer el SPEC del componente — las disposiciones completas viven en §4.- 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.
ArreglarHECHO 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 elnpm run translations:check(casca) y censar los 166 morfos.//de unhttps://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.tspor 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↔ramastatus/placeholderresueltas astatus.label/placeholder.defaulty descripciones slash en clave plana-d). Detalle: caja ⚠️ demorfo.md§common-mistakes.- El resto del eje:
F3CERRADA 2026-08-13 (censo a 0 y API borrada) ·F5 navegadorCERRADA 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 emiteARREGLADO 2026-08-13, y el mecanismo era MÁS ANCHO que el diagnóstico inicial: no es sólo el Trigger burbujeando — elcontact-trigger-pickerDOS veces por un gesto<input type=file>oculto también vive DENTRO del Dropzone, así que elclick()SINTÉTICO que disparaopenPickervuelve a entrar por el mismo manejador. Medido: un click en el dropzone llegaba dos veces, la segunda contarget = INPUT[hidden-input]; sólo el guard de reentrada que la plataforma pone enclick()lo paraba en dos. Es decir, TODO click al dropzone duplicaba, no sólo el del botón. Arreglo: guard por propiedad en elonclickdel dropzone (closestsobre los marcadores detriggeryhidden-input, construidos conpartMarkerAttr, nunca a mano). Test visto fallar con la anidación real; medido después: click en el dropzone → UNcontact-trigger-pickeren 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
tsque IGNORA su clave — una clave errónea pero existente pasaría su aserción del bag. Lo tapa parcialmentetranslations:check(caza las inexistentes) y hoy se verificó a mano que no hay deriva. Cerrarlo esexpect(ts).toHaveBeenCalledWith(ref), decidido una vez para todos. -
15
langs.tssin 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(lostab-*de gradient-builder/picker y 8 de palabras): candidatos a borrar, decisión tuya. -
range-calendar/langs.tsconservaPREV_MONTH/NEXT_MONTHmuertas 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.queuees 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).collapseRETIRADO — 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 eldata-event-idhabría minado la propiedad que lo curó.lockRETIRADO — 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
queuePASÓ EN VERDE SIENDO INCORRECTO. Con timers falsos no hay animaciones, así queawaitExpressionvolví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.queueespera el hold; el desplazado está a salvo porque su limpieza ya es consciente del dueño. El test corregido inyecta ungetAnimations()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 deevents/actionstipados contra la declaración (EventNameOf/PartKebabOf/ActionNameOf, los extractores desemaSelector). 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ónliveReg= 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:
targetOverrideborrado deTriggerOptions(con lápida doctrinal: no volver a añadir una opción de elemento),assertTargetOverridefuera (quedaassertAnchorpara 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 builderv.partRefpreserva el literal; defaultstring= cero ondas, doctrina «ensancha el tipo primero»).targetFallbackdeclarado en el morfo (tipo + schema con 3 invariantes testeados: parte desconocida / re-declarar el target / duplicado) y pre-aplanado enActionPlan;resolveEmitTargetes LA resolución única para validación, emit y foco a11y (no pueden discrepar).SomaRuntimePart<M, K>sin default +triggeranclado tipado porEventNameTargeting<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 devassertAnchorpara 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) ·
partInstanceidentidad. 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 resolvercontent ?? partRef ?? triggera mano. El prewrite dedata-last-actionqueda SOLO en content (verificado: todos los selectores de eidos que lo leen son[data-*-content]; escribirlo en el trigger era incidental).stampPartsOfdel pack-census cuenta ahoratargetFallbackcomo estampable. 218✓ (overlays + census + morfo). - Olas 2-3 EN CURSO (2026-08-10, censo 145 → 107). Migrados y en
verde (
checkDELTA 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).
partInstancepor identidad — listbox · toggle-group (las TRES emisiones del gesto anclan a UNA superficie, lo que el régimenqueuedel 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 —partInstancediscrimina) · 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.partMarkerAttrexportado del barrel de morfo para esto. ⚠️ El elemento de partida esopts.ref.currentde la clase, NOe.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 4allowedTargets:[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/cellregistran, así quepartInstance+ trigger anclado; range-calendar gana un helpertriggerOnDaypara 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: eltargetOverridepúblico decommit()RETIRADO — sólo lo pasaban sus propios scrubbers, redirigiendo el TERMINAL declaradoprovideral scrubber contra la fila de la doctrina del sello, y de paso silenciando las reglas de pack que seleccionan el provider; los 4handle-pick/drag-scrubcone.currentTargeteran 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/targetSON 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?? dragElera cinturón de la era fallbackTarget) ·metrics/menu-dial/onion-menu(HECHOS 2026-08-13, censo 38 → 29: las vistas REGISTRAN — metricsvaluese registra y emite ANCLADO él mismo, el contexto deja de acarrear elementos; menu-dial registralist+ cadaaction[el attachment atraviesa Fab→Button→button, medido: ids del runtime en los mini-FABs]; onion-menu registrasurface+ cada sectoritempor 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: importarstateen un.sveltecon runas$staterompe TODOS los$state<T>del fichero — aliasstate as refState) ·select/combobox(HECHOS 2026-08-13: NO era elección del llamante — era el superviviente A-36 con el open YA enpost: el override al trigger forzaba el aterrizaje pobre con el content montado, y la firma de motionpresent-risecorrí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 pierdeallowedTargets[vestigio legalizado], close pasa atargetFallback[trigger] / [trigger, input], selección porpartInstance) · 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 ERApartRefdel destino declarado, leído paraclearTargety 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-inputcommit-set-add×3 +commit-reset, file-uploadcommit-set-add·signal-warn-reject·commit-reset]; ycontact-trigger-pickerde file-upload ganaallowedTargets: [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] ycommit-selectsobre el link «Pricing»; file-upload sella DROPZONE al pulsar la zona y TRIGGER al pulsar el botón. En color/time-field elinputlo registran DOS partes [grupo visible + input oculto], así que ahípartInstanceno 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 dehandle-dragaterriza en elevent-chip; el arrastre del tirador da 6handle-resizesobreevent-resize-handle. Los chips registran, así que elplain triggerde esta migración no fallaría por ese motivo.Al llegar a 0: BORRAREJECUTADO 2026-08-13 (ver la fila F3).targetOverride+assertTargetOverride+ retirar el script scratch.allowedTargets(elección del llamante) ytargetFallback(estado de montaje) son dos ejes, precedenteintentRequirement/intentGuidance.
-
F4 — dropdown
openpre→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-setencolado entra justo tras el hold de 240 delhandle-drop. Ni aplastado a los 24 ms (lo que S-17 midió), ni 1,6 s tarde (el falso verde dequeue, §2.5). Los tres sitios son el mismo elemento con DOS morfos —[data-drawer-trigger]+[data-button],[data-dropdown-menu-trigger]+[data-button],controlpara 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):
requestAnimationFrameestá SUSPENDIDO, así que elhandle-dragdel 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í quegetAnimations()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) —
targetFallbackestaba en el corpus SÓLO como JSDoc: nimorfo.mdnisema.mdlo 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, notargetOverride) · y la aserción depack-census.test.tsdeja de mentir sobre su criterio (nombraba target+allowedTargetsmientrasstampPartsOfcontaba TRES fuentes). ⚠️ Nota perceptual escrita en ambos: una parte deallowedTargetsse estampa en uso rutinario; una detargetFallbacksó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/nextlos tienen doce, y Calendar heredó elbranch-controlde TreeView. El salto no puede salir del componente. opts.ref.currentcomo RECEPTOR de una consulta (…current.querySelector(…)) es la raíz donde se busca, no el aterrizaje. Contarlo inventaba unprovideren carousel, command y todo el que busca su item desde la raíz.e.currentTargetNO es la parte de la clase que declara el handler:CalendarProvideraloja el keydown del día y su clase registraprovider. 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-offerydelegate-escalateestampaban con el ref del provider, que es exactamente lo que el runtime resuelve solo: no-op retirado.sustain-processing(declaraindicator) y los tres terminales (declaranring) caen al provider cuando la parte decorativa no está montada: override sólo en la ausencia +allowedTargets: [provider]en los cuatro. ⚠️ Retirar el override desustain-processingdel todo PARECÍA correcto — «espost, sólo se pierde el ornamento»— y el navegador lo desmintió: la prueba monta el Aura sin Indicator y salieron 10 rechazos deSomaRuntimeTargetError. 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ámetrotriggerEldetoggle()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 registraninput; pasa la visible a propósito) · command · listbox · radio-group · range-calendarcommit-*· toggle-group · tree-grid · tree-view · month-gridcommit-set· tablecommit-select· tabs · feed · proof-of-human · formcommit-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ñadidosemerge-expand/emerge-collapsesobreitem, con la forma de TreeView (misma familia+verbo: un nivel revela sus hijos en el sitio, libro cap. 26). Tocar fondo del drill sigue siendoclose, no un collapse. Medido:emerge-expandestampa el sector (depth 1) y desestampa limpio;Backspace→emerge-collapse, depths 2→1. ⚠️ Probé primero conEscapeesperando el collapse y salióclose: la tecla dedrill-upesBackspacesegún el mapa del propio morfo. El código estaba bien; la prueba estaba mal. - dropdown-menu y context-menu —
MenuSubProvider.handleOpen/Closesólo volteaban estado. Añadidossub-open/sub-closesobresub-contenten AMBOS gemelos (arreglar uno solo era la incoherencia que este eje persigue). Eventos distintos delopenraíz a propósito: el sub es su propia superficie flotante, sucommitsescribe su propiodata-state, y un pack debe poder distinguir «se abrió el menú» de «se abrió el submenú».sub-opennacepostysub-closepre+targetFallback: [sub-trigger]— la receta de float-panel, para no repetir A-36 en código nuevo. Medido:sub-openestampadata-dropdown-menu-sub-content. ⚠️sub-closeNO se ejerció en navegador: su camino es la coordinación SafePolygon (onPointerExit), que unpointerleavesinté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-menuSÍ.eidos/…/dropdown-menu-trigger.svelterenderiza el Trigger de soma por su snippetchildcon 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 snippetchild, 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.svelterenderiza 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: siguepre+allowedTargets: [v.partRef('trigger')]. - provider: el flip de estado pasa a
events: { open, close }y elopenemite sin override; elcloseresuelvecontent ?? partRef('trigger')— singestureTarget, que es lo que hoy arrastraitem/checkbox-item/radio-item/sub-triggeral 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 declaraprovidersinallowedTargets; ceder el botón pulsado era deriva (lección A-36).commit-save/commit-delete/commit-move— anclados en el CHIP que nombran víapartInstance, contargetFallback: [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 sonIconButtoncompuestos dentro de unDialogcompuesto 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 claseRegion, 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, conrole="row"/role="columnheader"saliendo del morfo. - Con
propRef(necesitan valores por instancia): en vez de una clase por parte,Regionrecibe un bagsources— 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-valuedesde elpropRef),day-column,time-slotyagenda-group. agenda-item: clase propia (ChronosAgendaItemProvider+ componente), porque renderiza un<button>REAL — su morfo declararole+ el literaltype, 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.
CORREGIDO 2026-08-11: el crash era el stripper de comentarios rompiendo un
npm run translations:check —el guard que lo cazaría— CASCAhttps:// 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:
- Decía «las 5 partes reales que el CSS de eidos estiliza».
search-buttonno tiene ni una regla CSS ni unonclicken todo el repo: una sola aparición, el atributo. No es una parte por declarar, es una lupa que no hace nada. - Suponía que estos marcadores estaban en rojo.
eidos-lint chronosmedido antes y después (restaurando morfo+css de HEAD):invalid: 0en 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 --checksobre 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.svelteYA estaba sucio en HEAD; lo descubrí sólo al comprobarlo in situ.prettier --writesobre é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 quegoTodayera un caso particular (mismo ancla rovinga, anuncio y emisiónshift-navigate). - El texto buscable viaja en
keywords: el scorer de Command leevalue+keywordsy NUNCA los hijos renderizados, y elvaluees 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: pasarchildrenREEMPLAZA 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 estiloCalendar.PrevButton: componen elIconButton/Buttondel sistema, su glifo y la llamada al provider, así que el consumidor recibe un control que FUNCIONA, no un gancho vacío.childrensustituye 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).
searchOpensube 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, comoediting.- ⚠️ Y la prueba compuesta destapó un hueco real: el
SearchButtoncompuesto no abría nada, porque la paleta vivía en el cuerpo por defecto. Los overlays pertenecen a la RAÍZ. Extraída achronos-search-palette.sveltey 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:
- Partes del grid de mes —
Grid·GridHead·HeadCell·GridBody·GridRow·DayCell·DayNumber·DayAdd·EventChip·EventResizeHandle·MoreLinkcomo wrappers de eidos, y unChronos.MonthViewque los componga (patrónCalendar.MonthView, que se auto-gatea sobreprovider.view). WeekView/DayView/AgendaViewcon 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.
-
La 6ª fila fantasma.NO ERA UN DEFECTO — REVOCADO 2026-08-13, ver §4.fixedWeeksvienetrueen chronos yfalseen 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. -
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) ymoveEventToDayla buscaba encalendar.events, que sólo guarda la SERIE →finddevolvíaundefinedy salía en silencio, después de haber iluminado el destino. Iba a resolver el id a la serie (como haceopenEdit) hasta ver quecanResizeya 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 decanResize, ydisableden los TRESDragDrop.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 (Retro18 jun → 25 jun). ⚠️ El camino de TECLADO ya estaba bloqueado — porcanResize, 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 concursor: graby sindata-disabled(elDraggableno lo estampa). Para pintarlo hace falta un gancho: oevent-chipdeclara un estado nuevo, oDragDrop.Draggableestampadata-disabled— decisión, no bug. Los eventos normales SÍ caen en las celdas de fuera de mes — medido:Retro→ 2026-07-08 yLaunch→ 2026-07-09 aterrizan y se repintan allí. -
El chip pierdeNO REPRODUCIBLE — RETIRADO 2026-08-11. Lo reporté como defecto de contrato C1 (el attr esdata-event-idtras un intento de arrastre.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 · asentado17/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 definedy dossoma::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-saveycommit-deletedeclarabanevent-chipy aterrizaban en los botones Guardar / Eliminar del editor;commit-moveen la celda del drop;shift-navigateen los botones de navegación yday-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 (F4, open pre→post + dropdown-menutargetFallback)
· editablesearch-field (CROSS retirados — el
provider declarado ES la superficie) · stepper (sort-trigger →
column-header por tableclosest + partInstance) · ·
tag-group (identidad item/branch) · tree-view (item ratón + camino
Enter; queda sólo commandlink-item) · (emerge-reveal anclado).timeline
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 apre); 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-fieldemiten en CADApointermove. Cinco compañeros sí throttlean (DRAG_SIGNAL_MS). ⚠️rotate-alignNO 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 deemerge-closesuenaopenycommit-deletesuenaticken vez detick.loss. El bloque está copiado 68 veces; extraer un helper a$demoes refactor aparte. knob.handle-dropesprecon handler →data-draggingsobrevive 240 ms al soltar.button.contact-activateretrasaonPress120 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) · ARREGLADO 2026-08-12 (el gate de S-27/S-41shouldPrime 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) · ARREGLADO
2026-08-11, y el veredicto se INVIRTIÓ: el censo dio 132 declaraciones en 59
morfos, y la solución no fue el A-85 confirmadoprop-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 —
translationRefnombra una clave, no admite params: carouselSlide N of Me indicator · tag-groupRemove {tag}· nav-tree (nombre por nodo). CROSS-ELEMENTARREGLADA 2026-08-11 (ver abajo).SUPRIMIDA PORARREGLADA con ella — eran la misma clase por sus dos mitades.<label for>
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 estabaorientation), 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, ymorfo.textses justo la forma sancionada para un slot que resuelve el provider. El defecto era otro y peor: su prop público eraariaLabelcamelCase (único en el catálogo) y su tipoWithout<PrimitiveDivAttributes, {}>no filtraba nada — unaria-labeldel 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-sliderpasabaariaLabel—, que es exactamente lo que un tipo estrecho debe hacer. Medido: raíz<div>SINaria-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.
fixedWeeksvaletruepor defecto sólo en chronos; Calendar, DatePicker, RangeCalendar y DateRangePicker lo traenfalse. 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:fixedWeeksse queda entrue. 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») ySPEC.md:311+:461(data-outside-monthes un estado declarado deday-cellcon 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
fixedWeekCountvienetrue(«the calendar will always be 6 weeks tall») y Toast UImonth.isAlways6Weeksvienetrue— 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 sonmin-block-sizey 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 concalc(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:20yengine/state.svelte.ts:64), ychronos.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 conmin-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) +
maxLanesderivado 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: grabporqueDragDrop.Draggableno estampadata-disabled. Dos vías: (a)event-chipdeclara un estado nuevo — local y barato; (b)Draggableestampadata-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 declaradata-disableden el draggable (drag-drop.ts:89, severity optional) — hoy es una declaración sin emisor, la clase de siempre — y elcursor: grabvive endrag-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])dearchetypes.css:259(not-allowed + opacidad) hay que anularla en la receta del chip — es:where, especificidad cero — y (ii) elaria-disabledprop-truthy del mismo morfo NO debe aterrizar en el elemento fusionado: estamparlo mentiría a AT sobre un click que funciona.
Del eje perceptual:
modeyscope— FIRMADA (2026-08-12): SE RETIRAN. El barrido de la regla 4 del ledger está hecho: 0 hits endocs/, 0 lectores del compilado, y el alcance real son 4 ficheros (SemaMode/SemaScopeensema/types.ts+morfo/types.ts:644-646+compile.ts:196-198+ exports) — misma clase quecollapseylock. Pregunta hermana ABIERTA:SemaCauseestá más muerto aún (sólo types + exports, ni atraviesa morfo) — decidir si entra en el mismo pase.regimees 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
requestAnimationFramese cuelga,animationstartno dispara y elscreenshotfalla. Muestrear con temporizador y leergetAnimations()/getComputedStyle, que sí son fiables ahí. awaitExpressionllega 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?».