# CONTINUE — la cola de auditorías, y el protocolo con el que se trabaja > **Kickoff para la sesión siguiente**: _«Lee `docs/process/CONTINUE-audit-queue.md` > y empieza por §1 (S-33, firmado y sin ejecutar).»_ > **Fecha**: 2026-08-13 · Rama `alpha-0.1-dir-prefs`, pusheada en `eda3b977b`. > ⚠️ **COMPARTIDA**: otra sesión trabaja en `blocks` **y** en un eje de > dismissal (los `types.ts` de color-picker / time-picker / time-range-picker). > `git reset -q` + stagear por lista explícita, y clasificar **por contenido del > diff, nunca por nombre**. > **Fuentes de los hallazgos**: [`AUDIT-sema-2026-08-05.md`](./AUDIT-sema-2026-08-05.md) > (44 confirmados con refutación adversarial) · > [`CONTINUE-perceptual-surface.md`](./CONTINUE-perceptual-surface.md) §3 (la > cola larga, con el detalle de cada eje). Este documento dice **cómo se > trabaja, qué se firmó y por dónde se sigue**. --- ## 0 · EL PROTOCOLO — va primero porque cambió hoy, y no es opcional **Por CADA modificación o acción, y ANTES de tocar nada**, se responden las cinco preguntas del autor: 1. ¿Qué es lo que se va a solucionar? 2. ¿Has leído toda la documentación al respecto, o sólo estás presuponiendo? 3. ¿Qué afecta a otros componentes? 4. ¿Afecta a las doctrinas del ecosistema? 5. ¿Qué modificaciones planteas? **Un item por mensaje, y se PARA a esperar firma.** Hoy propuse una «puerta» que me dejaba auto-autorizar lo mecánico y presenté cuatro decisiones juntas en una hoja resumida; el autor lo cortó en seco. No hay puerta: se expone y se espera. Y dos precondiciones que hoy costaron **dos rectificaciones completas**: - **El SPEC del COMPONENTE es doctrina, y se busca ANTES de diagnosticar.** El item de la «6ª fila fantasma» de chronos llevaba un mes en la cola y llegó a estar FIRMADO; al ir a ejecutarlo apareció `SPEC.md` del componente diciendo lo contrario en tres sitios coordinados. Se revocó. Una asimetría con la familia NO es evidencia de deriva: puede ser la única decisión escrita. - **Barrer `docs/` ANTES de proponer retirar nada.** Recomendé retirar `intentRequirement: 'forbidden'` «igual que `collapse` y `lock`». Falso: aquellos no tenían una línea en `docs/`; éste está en `CANON.md` §4, en `architecture/sema.md` §, y en la tabla de **D.3**, decisión firmada del autor en `decisions/book-deviations.md:472`. Regla que resume las dos: **el código nunca corrige al canon; se le pone al autor la CONTRADICCIÓN delante y decide él.** --- ## 1 · Mañana lo primero: S-33, FIRMADO el 2026-08-13 y sin ejecutar **El defecto es `Exclude`, no «falta una rama».** `IntentOptionalFamily = Exclude` es una derivación de mundo abierto —«todo lo que no es required»— así que una familia marcada `'forbidden'` cae en el cubo OPCIONAL, donde el intent está permitido. Los dos guardianes de runtime tienen el mismo hueco: `validation.ts:39` sólo comprueba `=== 'required'`, y `event.ts:104-110` (`isSemaEvent`), si el intent está presente, devuelve `isIntent(...) || isIntentBinding(...)` sin mirar la política. **Medido con el `tsc` del proyecto** (probe con los dos estados del const, hoy y endurecido). Un único error en todo el probe, y es el control de la derivación actual: ``` intent-probe.ts(87,1): error TS2578: Unused '@ts-expect-error' directive. ``` es decir: con `contact` en `'forbidden'`, `{ family: 'contact', intent: 'threat' }` **compila**. Lo demás del probe demostró las tres cosas que hacían falta: | Probe | Resultado | | --- | --- | | `{contact,intent}` · `{contact}` · `{commit,intent}` con la tercera rama presente | compilan → **una rama tecleada sobre `never` no cuesta nada hoy** | | `@ts-expect-error` sobre `{family:'commit'}` | se CONSUME → la puerta de `required` sigue intacta | | `@ts-expect-error` sobre `{contact,intent}` con derivación POSITIVA y contact forbidden | se CONSUME → **flipar una línea del const cierra la puerta solo** | | `{family:'contact'}` a secas, misma situación | compila → `'forbidden'` significa «familia usable, intent prohibido», no «familia inutilizable» | **La receta firmada:** 1. `sema/types.ts` — derivar los tres cubos **positivamente** (`extends 'optional'` / `extends 'required'` / `extends 'forbidden'`), matando el `Exclude`, y añadir la rama `{ family: IntentForbiddenFamily; intent?: never }` a `SemaEvent` y a `MorfoEventSemantic` (`morfo/types.ts`). 2. `sema/event.ts:104` y `sema/validation.ts:39` — la condición simétrica en los dos guardianes de runtime. 3. El esquema sium de `morfo/schema.ts` **no necesita nada hoy** (no hay literal que añadir mientras ninguna familia sea `'forbidden'`), y si alguien flipa el const **salta el guard de S-34** que se metió hoy y le dice que lo añada. Las dos mitades cubiertas: los tipos se activan solos, el runtime avisa. 4. El probe se convierte en **test** (`sema/` o `contracts.test.ts`), para que el día que alguien reintroduzca un `Exclude` el `@ts-expect-error` deje de consumirse y falle. Delta de conducta hoy: **cero, medido**. Lo que cambia es que la promesa del canon («editing the const reshapes the discriminated unions») pasa a ser cierta. ⚠️ Y la razón por la que se descartó la alternativa que yo mismo había recomendado —un guard que fallara si alguna familia declara `'forbidden'`— es doctrinal, no de gusto: es **lint donde el tipo puede hablar**, y `CLAUDE.md` §*Eidos drift defense* dice que cuando una capa PUEDE expresar la restricción en TypeScript, hacerlo con un test es una violación de arquitectura. La misma doctrina con la que ayer se cerró F3 borrando `targetOverride`. --- ## 2 · Lo cerrado hoy (6 commits, todos pusheados) | Commit | Qué | | --- | --- | | `b8aa333fd` | **F3 CERRADA** — `targetOverride` BORRADO de `TriggerOptions` + `assertTargetOverride`; el compilador, ya como censo, cazó 9 usos en tests que el grep del scratch nunca escaneó | | `c00e97b91` | **F5 CERRADA** — los tres sitios de las tres auditorías, medidos DESPUÉS de la campaña | | `a15745491` | **chronos `fixedWeeks`: decisión REVOCADA** — el SPEC manda; no era un defecto | | `44af38e6e` | **S-10** — el canal visual honra los servicios de su propia bolsa (`anidado ?? raíz`, como los otros tres) | | `14a169357` | **guard D9** — mira también la vista de eidos, que en los componentes-vista ES el emisor | | `eda3b977b` | **S-34** — el validador de runtime queda atado al const de política de intent, con el guard visto fallar | Con F5 cerrada, **las seis fases del eje perceptual (F1–F6) están completas**. --- ## 3 · Decisiones ABIERTAS — esperan firma, no se tocan Cada una necesita su exposición de cinco preguntas cuando se abra. Aquí sólo el titular y la evidencia ya medida, para no re-descubrirla. ### S-35 · la forma etiqueta regala lo que la estructurada exige `SemaActionEvent = SemaEvent | SemaEventLabel` y la etiqueta incluye la familia pelada: `'commit'` compila aunque `commit` sea `required`, y `normalizeSemaEvent('commit')` devuelve `intent: null` — el documento que la política declara inválido. `validateSemaEvent` remata devolviendo temprano ante cualquier etiqueta (`validation.ts:29`). **Alcance medido**: `SemaActionEvent` no tiene consumidores fuera de los tres ficheros del propio sema. Va pegado a S-33: la misma unión, las mismas líneas. ### ~~S-30/S-38~~ · EJECUTADO 2026-08-13 — las cinco reglas ABREN LA PUERTA **La decision se INVIRTIO al contrastarla con el mundo real y con el autor.** Mi recomendacion era borrar los bloques; el autor pregunto que hacen iOS/Android y quien decide — y la verificacion en las fuentes (Apple HIG: haptic de warning para alertas; tick de seleccion para cambios de valor; Android: «less is more» pero con constantes CONFIRM/REJECT) mas un hecho del propio framework (la intencion estaba ESCRITA y comentada en los packs, y la haptica entera es opt-in del producto) cambiaron el veredicto: las reglas llevaban la intencion del disenador y solo habian olvidado la puerta. Arreglo: `channels: ['sound', 'haptic']` en las cinco, con la razon perceptual escrita en cada una; los 5 waivers de `CHANNEL_EXCEPTIONS` fuera (el guard del censo, re-armado, ES el test que avisa si vuelve a pasar); y un e2e nuevo en `resolver.test.ts` que prueba resolucion + `vibrate()` con valores FINITOS (la defensa S-31 cubre los `{kind:'tick'}` parciales — el aviso del waiver ya estaba pagado). Visto en rojo dos veces: el censo sin waivers listando las 5, y el e2e contra los packs sin ensanchar. ### S-30/S-38 · (ficha original, cerrada) Dialog ×2 (alertdialog y sheet), editable, stepper y timeline escriben `haptic: {…}` sobre eventos de familia `emerge`/`shift`, cuyo `activeChannels` es `['sound']`. Medido por el verificador con resolver y canal reales: firma háptica instalada, `vibrate()` **0 veces**. Y el mapa es coherente en su diseño: las familias que vibran declaran **base háptica Y canal** (contact, commit, signal, handle); emerge y shift no declaran ninguna de las dos. **Opciones**: (a) `channels: ['sound','haptic']` en las cinco (patrón canónico en `slider`, `css-field`, `drag-drop`) · (b) borrar los bloques y escribir que emerge/shift no vibran. **Mi criterio: (b)** — el mapa ya lo dice en dos sitios independientes, y (a) reabriría por la puerta de atrás una decisión perceptual (que un overlay te vibre en la mano) que nadie ha tomado. Con guard que cruce cada regla contra el `activeChannels` alcanzable. ### ~~S-19~~ · EJECUTADO PARCIAL 2026-08-13 — la firma alineada; encenderlo sigue ABIERTO **Mi primer analisis estaba mal y el autor lo mando revisar.** Dije que el canal estaba muerto porque el runtime de soma no mete `message` en la señal; es cierto que no lo mete, pero **es diseño, no defecto**: son DOS emisores para DOS publicos (soma cubre sus componentes; el canal cubre a quien usa `EngineSemantic` SIN soma y escribe su propia señal con el texto). Justo por eso nada se anuncia dos veces, y mi opcion (b) —fusionarlos— habria roto el diseño. Retirada. HECHO: `AnnounceFn` pasa a prioridad POSICIONAL, como el sumidero y como la fuente de soma, asi que `{ announce: uix.announce }` compila literal y sobra el adaptador. Visto en rojo antes: un sumidero con la forma de `uix.announce` recibia `{priority:'assertive'}` en el hueco de la prioridad — y ahi `liveRegionIds[obj]` es `undefined`, o sea que TODO caia en la region polite y un `threat` dejaba de interrumpir. 3 aserciones de test migradas de bolsa a posicional. `sema.md` §announce reescrito: fuera el aviso del adaptador, dentro la razon de los dos publicos. QUEDA ABIERTO (decision, no bug): **ninguna raiz enchufa el canal**. Con el puerto ya alineado es UNA linea en la raiz, pero encenderlo sin inyectar el anunciador cae en las regiones propias del canal y añade un SEGUNDO par `role=status`/`role=alert` junto al compartido. ### S-19 · (ficha original) `AnnounceFn` pide `(message, { priority })` y `uix.announce` es posicional `(message, priority?, timeout?)`: el cableado canónico de `sema.md:762` es un error de tipos, no hay adaptador, y **ninguna de las dos raíces** pasa `announce` al engine. Si alguien fuerza el cast, `priority` llega como objeto, `liveRegionIds[priority]` es `undefined` y todo cae en la región polite — un `threat` deja de interrumpir. **Son DOS decisiones y conviene no mezclarlas**: (i) alinear la firma + corregir el doc — no cambia conducta, porque hoy nadie lo pasa; (ii) enchufar el canal en las raíces, que **sí** cambia conducta y arriesga el «dos sumideros» que la propia doctrina prohíbe, porque soma tiene su propio puerto de anuncio funcionando (`soma.svelte.ts:99`, el de metrics/menu-dial/onion-menu). **Mi criterio**: (i) ahora, (ii) como ítem con censo previo de quién anunciaría por los dos caminos. --- ## 4 · La cola, en orden 1. ~~**S-33**~~ EJECUTADO 2026-08-13 (`1a174d5a6`): los tres cubos derivados en positivo + rama `intent?: never` + `intentRequirementOf()` (leer el const directo ESTRECHA y el gate no compila — arista que la ficha no vio). Desviacion declarada: `validation.ts` NO se toco (su rama de politica es inalcanzable — hallazgo nuevo, abierto). 2. ~~**S-35**~~ EJECUTADO 2026-08-13 (`6a7554415`): `SemaEventKey` derivado de la politica (bare solo sin required; pareja sin forbidden), `SEMA_EVENT_LABELS` 56→54, `isSemaEventLabel('commit')` false → `validateSemaEvent` lanza. Dos hallazgos ABIERTOS de su paso: el vocabulario de etiquetas no tiene NINGUN consumidor en produccion (¿se gana el sitio?), y la rama `required` de `validation.ts:39-46` es codigo muerto (inalcanzable tras `isSemaEvent`). 3. ~~**S-30/S-38**~~ EJECUTADO · ~~**S-19**~~ EJECUTADO PARCIAL (firma alineada; encender el canal sigue abierto, ver §3). 4. **§3.5 fable** — `M1 emission` (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`~~ **EJECUTADO 2026-08-13**: los alias `IntentExpectedFamily`/`SemaEventLabel` BORRADOS (no-shims; D.3 los nombra como HISTORIA, no como prescripción), 13 usos migrados a los canónicos, `semaIntentExpectedFamilySchema` renombrado `…Required…` CON el guard S-34 actualizado en el mismo commit, los 2 `as never` de `compile.ts` retirados (vestigio pre-`DepSink`), y `_resetCompileCache` resultó YA borrado — fuera de la cola · `E2` (`globalThis.matchMedia` crudo en eidos) · `E3` (catch mudo en `#renderSchemeCss`). 5. **§3.6 blocks** — `A-47` (anillo de foco 1,43:1 sobre panel sólido) · `A-69` (`duration` de count-up no significa segundos). 6. **§3.3 eje B** — 4 continuos sin throttle (gradient-builder, color-picker área, number-field, css-field) · `sequence: 'coincident'` sin implementar con 21 eventos declarándolo (**decisión**: implementar o marcar reservado) · las 68 demos emitiendo sin `verb` · medir `knob.handle-drop` (`pre` con handler) y el retraso de 120 ms de `button.contact-activate`. 7. **Firmadas que quedan** — el chip bloqueado vía `DragDrop.Draggable` (transversal, con su letra pequeña ya medida en `CONTINUE-perceptual-surface.md` §4) · retirar `mode`/`scope` (+ la hermana `SemaCause`, que sigue ABIERTA). 8. **§3.2 restitución de persistentes** — eje nuevo del engine; afecta form, file-upload, textarea, media-player, announce, aura. 9. **C5 rebanadas 3-4 de chronos** — subir a soma el comportamiento del chip (teclado, captura de puntero, drag-to-create) y sólo después extraer `MonthView`/`WeekView`/`AgendaView`. Con D14 dentro. El grande. --- ## 5 · Base de verificación (mídete contra esto, no contra memoria) ``` 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 # 69 errores = base npm run docs:check # 0 / 623 ``` - `contracts.test.ts` tiene **6 fallos AJENOS y preexistentes** (barrel de waveform · nombres de fichero de provider · MOR-4 · escrituras DOM de menubar · data-attrs · namespaces de aura). Hoy llegó a haber 7: el séptimo era **mío y de otro commit**, y se cerró (`14a169357`). - soma navegador: **1278 ✓ / 1**, el timeout ajeno preexistente de `soma-attr-audit`. - sema+morfo: **532 ✓**. --- ## 6 · Trampas medidas hoy — no re-descubrirlas - **El guard D9 es laxo A PROPÓSITO.** Compara `includes("'nombre'")` y no `trigger('nombre')` porque el nombre llega a la llamada por VARIABLE en media docena de patrones. Censado: endurecerlo convierte **27 emisiones reales** en violaciones. Su precio —un nombre entrecomillado en un comentario lo satisface— queda escrito en el propio colector. - **El panel del navegador oculto**: `rAF` suspendido (los ticks continuos no estampan nunca) y la línea de tiempo de animaciones congelada, así que el desestampado cae SIEMPRE en el tope de 1500 ms de la espera de expresión. `getAnimations()` dice QUÉ corre sobre QUÉ, nunca cuánto dura. - **Prettier**: hay ficheros sucios en HEAD (`errors.ts`, `test-fixtures.ts`, `runtime.svelte.ts`, `runtime.svelte.test.ts`, `contracts.test.ts`). Probar contra HEAD antes de `--write`, y no reformatear lo que ya estaba sucio. - **`git stash push ` + `stash pop`** es la forma limpia de comprobar si un fallo es tuyo o de HEAD. Hoy demostró que el D9 fallaba sin mis cambios.