docs(words): sign the export-only serialization decision (no roundtrip)

Closes the long-open "export HTML roundtrip?" question as export-only — the
architecturally-correct, industry-aligned choice (TipTap/ProseMirror, Lexical,
Slate, Notion all keep JSON as the canonical lossless persistence format and
treat HTML/MD as projections).

- README "Serializacion e intercambio" doctrine: JSON = truth; HTML/MD =
  export-only projections; import = JSON + plain text. Explains the
  state-complete-HTML trap that JSON avoids, why markdown is the most lossy
  (GFM cells are inline-only — block cells from P5m can't roundtrip), and the
  conditional `parseHtml` per-block path (parseDOM/importDOM pattern) IF
  paste-from-external is ever needed — as clipboard interop, never persistence.
- continue.md: the "NO firmada" note flipped to FIRMADA with the rationale.

Docs only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 30192bb853
commit aac80f7630

@ -14,6 +14,7 @@ component CSS` pasa.
### Lo cerrado y verificado este sprint ### Lo cerrado y verificado este sprint
**Ronda 1 — 7 ítems (todos DONE + verificados):** **Ronda 1 — 7 ítems (todos DONE + verificados):**
1. Quitado el dropdown "Add block" del panel de BLOQUE Columns (vive solo 1. Quitado el dropdown "Add block" del panel de BLOQUE Columns (vive solo
en el inspector específico de columna). `dropdownsInColumnsBlockPanel: 0`. en el inspector específico de columna). `dropdownsInColumnsBlockPanel: 0`.
2. Validación de ancho de columna (`isValidColumnWidth` + `commitColumnWidth` 2. Validación de ancho de columna (`isValidColumnWidth` + `commitColumnWidth`
@ -26,10 +27,11 @@ component CSS` pasa.
5+7. Rediseño cabecera inspector: mark de acento por profundidad 5+7. Rediseño cabecera inspector: mark de acento por profundidad
(primary/affirm/loss/risk) + wash + divisor + breadcrumb estilado (primary/affirm/loss/risk) + wash + divisor + breadcrumb estilado
(`[data-words-inspector-breadcrumb]`, single-row scroll). (`[data-words-inspector-breadcrumb]`, single-row scroll).
6. Botones add/delete normalizados a icon-only (Trash2 delete, flechas 5. Botones add/delete normalizados a icon-only (Trash2 delete, flechas
Arrow{Up,Down,Left,Right} para table-ops, Plus para add). Arrow{Up,Down,Left,Right} para table-ops, Plus para add).
**Ronda 2 — chrome systémico (DONE + verificado):** **Ronda 2 — chrome systémico (DONE + verificado):**
- Inputs unificados: un solo frame bordeado (text inputs + NumberFields), - Inputs unificados: un solo frame bordeado (text inputs + NumberFields),
un solo focus-ring de acento (NO más `outline` inset sobre el texto, NO un solo focus-ring de acento (NO más `outline` inset sobre el texto, NO
más mezcla ghost/borde-negro). Specificity `[data-number-field][data-size]` más mezcla ghost/borde-negro). Specificity `[data-number-field][data-size]`
@ -45,13 +47,14 @@ component CSS` pasa.
del popover). del popover).
**Ronda 3 — defectos de controles (DONE + verificado):** **Ronda 3 — defectos de controles (DONE + verificado):**
- ToggleGroups con **valor por defecto seleccionado** (uno siempre activo): - ToggleGroups con **valor por defecto seleccionado** (uno siempre activo):
Fuente=`default`, Grosor=`400`/Normal, Alinear=`left`, Borde=`none`. El Fuente=`default`, Grosor=`400`/Normal, Alinear=`left`, Borde=`none`. El
default se almacena como `undefined` en el modelo (limpio). Verificado: default se almacena como `undefined` en el modelo (limpio). Verificado:
`Predeterminada/Normal/Izquierda/Ninguno` seleccionados al activar bloque. `Predeterminada/Normal/Izquierda/Ninguno` seleccionados al activar bloque.
- Borde gana opción **`none`** (chip que limpia `border: undefined`). Nuevo - Borde gana opción **`none`** (chip que limpia `border: undefined`). Nuevo
type `BorderChipValue = 'none' | BorderStyleValue`. Langs `BORDER_NONE` type `BorderChipValue = 'none' | BorderStyleValue`. Langs `BORDER_NONE`
+ bundle `border.none` ({en:'None', es:'Ninguno'}). - bundle `border.none` ({en:'None', es:'Ninguno'}).
- Image "Ancho total" → **Switch** (era Toggle con texto on/off). - Image "Ancho total" → **Switch** (era Toggle con texto on/off).
- **Bug del radio de esquina arreglado**: el ring de selección era un - **Bug del radio de esquina arreglado**: el ring de selección era un
`outline`/`box-shadow` que HEREDA el `border-radius` del bloque, así que `outline`/`box-shadow` que HEREDA el `border-radius` del bloque, así que
@ -108,8 +111,8 @@ Fecha de corte: **2026-05-29**. Rama: `active-uix`.
**V1 del editor Words ELIMINADO. V2 es ahora el único motor.** **V1 del editor Words ELIMINADO. V2 es ahora el único motor.**
La sesión arrancó con una auditoría del componente Words La sesión arrancó con una auditoría del componente Words
(`codex-full-audit.md` en la raíz) y la pregunta del usuario: *"elimina (`codex-full-audit.md` en la raíz) y la pregunta del usuario: _"elimina
v1 y continua v2, ¿qué sentido tiene v1?"*. Decisión firmada y v1 y continua v2, ¿qué sentido tiene v1?"_. Decisión firmada y
ejecutada de cabo a rabo (F1 → F6). Ya no hay coexistencia V1/V2 ni ejecutada de cabo a rabo (F1 → F6). Ya no hay coexistencia V1/V2 ni
migrador: el modelo de documento público es `WordsDocumentV2` y punto. migrador: el modelo de documento público es `WordsDocumentV2` y punto.
@ -179,17 +182,24 @@ inverso fiel). El import sólo acepta **JSON** (`JSON.parse` +
- **Bloques con alineación**: paragraph/heading/quote llevan - **Bloques con alineación**: paragraph/heading/quote llevan
`textAlign?` (a nivel bloque). `textAlign?` (a nivel bloque).
## ⚠️ Decisión consecuente NO firmada — export HTML ## ✅ Decisión FIRMADA (2026-06-02) — export-only (no roundtrip)
`serializeHtmlV2` emite **HTML semántico limpio**, no un formato `serializeHtml` / `serializeMarkdown` emiten proyecciones de export (HTML
roundtrip-lossless. Como V2 no tiene parser HTML, HTML y Markdown son semántico limpio / GFM), **no** un formato re-importable, y así se queda:
export-only. Si el usuario quería export HTML roundtrip-friendly (re- **JSON es el formato canónico lossless; HTML/MD son export-only.** Misma
importable), es un encargo aparte. **Pendiente de confirmar con él.** postura que TipTap/ProseMirror, Lexical, Slate y Notion — el modelo JSON
persiste, HTML/MD son proyecciones. El import acepta sólo JSON (vía
`validateWordsDocument`) + texto plano (`parseWordsPlainText`). Si en el
futuro se necesita _pegar_ HTML externo, se añade un `parseHtml` por-bloque
(patrón `parseDOM`/`importDOM` de ProseMirror/Lexical) como interop
best-effort de portapapeles — **nunca** como persistencia; markdown import no
merece la pena (es lo más lossy). Doctrina completa + tabla de formatos en
`src/uix/soma/components/words/README.md` → "Serializacion e intercambio".
## Verificación al cierre (todo verde) ## Verificación al cierre (todo verde)
| Gate | Status | Notas | | Gate | Status | Notas |
|---|---|---| | ---------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------ |
| `npm run check` | **0 errors** / 5168 files | 26 warnings pre-existentes, ninguno en archivos tocados | | `npm run check` | **0 errors** / 5168 files | 26 warnings pre-existentes, ninguno en archivos tocados |
| `npx vitest run src/uix/soma/components/words` | **455/456** | el único fallo es el flake de `sema-parity` por timeout 5s bajo carga paralela | | `npx vitest run src/uix/soma/components/words` | **455/456** | el único fallo es el flake de `sema-parity` por timeout 5s bajo carga paralela |
| `sema-parity` aislado | **6/6 en 362ms** | confirma que el flake es de infra, no regresión | | `sema-parity` aislado | **6/6 en 362ms** | confirma que el flake es de infra, no regresión |
@ -231,7 +241,7 @@ Borrados los selectores CSS muertos de `words.css`
los `[data-words-cell-header]` huérfanos — los headers se renderizan como los `[data-words-cell-header]` huérfanos — los headers se renderizan como
`<th>`). README actualizado a la realidad V2 (tabla lista solo formato `<th>`). README actualizado a la realidad V2 (tabla lista solo formato
vivo; HTML/MD export-only; import JSON + texto). `ARCHITECTURE_PROPOSAL.md` vivo; HTML/MD export-only; import JSON + texto). `ARCHITECTURE_PROPOSAL.md`
intacto: documenta la *decisión* de eliminarlos, no features stale. intacto: documenta la _decisión_ de eliminarlos, no features stale.
Verificado en browser (tabla 2×2 renderiza, `<th>` conserva su tinte vía Verificado en browser (tabla 2×2 renderiza, `<th>` conserva su tinte vía
`:where(th)`). Commit `4a0661a5`. `:where(th)`). Commit `4a0661a5`.
@ -330,6 +340,7 @@ ok desde el primer click. El problema era exclusivamente el flow
imperativo del inserter (focus + insert + selección). imperativo del inserter (focus + insert + selección).
**Lo que se verificó end-to-end en navegador (Chrome):** **Lo que se verificó end-to-end en navegador (Chrome):**
- Click `+` en columna vacía → dropdown abre, primer-item auto-focus. - Click `+` en columna vacía → dropdown abre, primer-item auto-focus.
- Pick "Heading" → "Title" stub aparece + se selecciona. - Pick "Heading" → "Title" stub aparece + se selecciona.
- Tipear inmediatamente → "Title" se reemplaza con el texto tecleado - Tipear inmediatamente → "Title" se reemplaza con el texto tecleado
@ -338,6 +349,7 @@ imperativo del inserter (focus + insert + selección).
- Doc title y resto del doc intactos — typing va al lugar correcto. - Doc title y resto del doc intactos — typing va al lugar correcto.
**Pieces en disco:** **Pieces en disco:**
- `engine/operations/insert-block-types.ts:381` — - `engine/operations/insert-block-types.ts:381` —
`insertBlockInColumn` op con container-path correcto. `insertBlockInColumn` op con container-path correcto.
- `engine/operations/commands.ts` — command + dispatcher. - `engine/operations/commands.ts` — command + dispatcher.

@ -30,6 +30,46 @@ empezar hasta cerrar contrato Morfo y alcance V1.
- Serializacion JSON/HTML/Markdown. - Serializacion JSON/HTML/Markdown.
- Integracion con logger, clipboard, format y events de ActiveUIX. - Integracion con logger, clipboard, format y events de ActiveUIX.
## Serializacion e intercambio — doctrina V2 (FIRMADA 2026-06-02)
**JSON es la verdad. HTML y Markdown son proyecciones de export-only.**
| Formato | Export | Import | Lossless |
| ------------ | ---------------------------------- | ----------------------------------------- | ----------------- |
| **JSON** | `exportContent('json')` | `importContent` + `validateWordsDocument` | si (es el modelo) |
| Texto plano | si | `parseWordsPlainText` | no (solo texto) |
| **HTML** | `serializeHtml` (semantico limpio) | — | n/a |
| **Markdown** | `serializeMarkdown` (GFM) | — | n/a |
### Por que export-only (no roundtrip)
Es la misma postura que TipTap/ProseMirror, Lexical, Slate y Notion: **el modelo
JSON es el formato canonico de persistencia (lossless); HTML/MD son
proyecciones**. Hacer del HTML _limpio_ un formato de persistencia lossless es la
trampa que todos evitan — el HTML semantico no puede cargar cada visual prop /
metadato de bloque sin ensuciarlo de `data-*` (el approach "state-complete" de
TipTap, donde cada nodo declara `toDOM` + `parseDOM` y serializa TODOS sus attrs
al HTML). JSON esquiva el problema entero; por eso es canonico en los cuatro.
Consecuencias deliberadas:
- `serializeHtml` emite HTML semantico limpio (sin `data-words-*`), pensado para
lectura / copia / preview, **no** para re-importar.
- `serializeMarkdown` es lo mas lossy: sin color ni visual props, y las celdas
GFM son inline-only — desde P5m las celdas llevan bloques, que markdown no
puede representar (se aplanan con `<br>`). MD es proyeccion, no roundtrip.
- El import solo acepta **JSON** (via `validateWordsDocument`, que protege los
`$derived` de un doc invalido) y **texto plano** (`parseWordsPlainText`).
### Si algun dia hace falta _pegar_ HTML externo
No se construye "roundtrip de persistencia". Se anade un `parseHtml`
**por-bloque, simetrico al `toHtml` de cada spec** (el patron `parseDOM` /
`importDOM` de ProseMirror/Lexical), declarado como **interop best-effort de
portapapeles**, explicitamente lossy fuera del schema. La persistencia sigue
siendo JSON; el HTML solo entra por el paste. Markdown import: no merece la pena
(es lo mas lossy).
## Submodulos propuestos ## Submodulos propuestos
```text ```text

Loading…
Cancel
Save

Powered by TurnKey Linux.