fix(words): close 4 cross-layer audit findings (morfo/soma/eidos)

Full audit of the Words component (3 parallel explore agents + deterministic
gates) surfaced 4 carried-over defects, all verified and fixed:

- R2 (soma) engine/operations/visual.ts:147 — the `node as Record` cast in
  updateBlockAtPath failed because WordsNode includes WordsDocument (no index
  signature). Use the double cast `as unknown as Record`, consistent with the
  sibling op at line 118. Clears the only Words type error in `npm run check`.

- R1 (morfo) components/words.ts — the `data-words-node` enum was conflated
  with `data-words-block`: it listed block TYPES (paragraph/heading/quote/
  code/image) that nothing emits on that attr, and OMITTED the real
  structural kinds the engine writes (`block`/`inline`). Aligned to the
  engine's WordsDomNodeKind. eidos-lint invalid selectors 2 → 0; verified in
  the browser that top-level blocks carry node='block' and the active-block
  outline still paints.

- R3 (eidos) langs-inspector.ts — `LABEL_COLUMN_BLOCKS` had no bundle entry,
  so the column "Blocks" panel title fell back to English in ES. Added
  label.column-blocks + label.add-block. Verified: panel now reads "Bloques".

- D3 (morfo) components/words.ts — declared `expression: 'family-default'`
  to make the no-sema-pack choice explicit; `morfo:vocabulary` no longer
  warns about the missing pack/expression.

Gates: npm run check 1 error (pre-existing, web/routes/temas/grafito — not
Words) · words soma+morfo 514/514 · eidos-lint words invalid 0 · prettier
clean · morfo:vocabulary exit 0.

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

@ -42,35 +42,24 @@ export const WORDS_INSPECTOR_LANGS = {
BLOCK_COLUMNS: '#?components.words.inspector.block.columns|Columns',
LABEL_COLUMN_WIDTH: '#?components.words.inspector.label.column-width|Width',
LABEL_COLUMN_GAP: '#?components.words.inspector.label.column-gap|Gap',
PLACEHOLDER_COLUMN_GAP:
'#?components.words.inspector.placeholder.column-gap|1rem, 24px, 0',
PLACEHOLDER_COLUMN_GAP: '#?components.words.inspector.placeholder.column-gap|1rem, 24px, 0',
LABEL_COLUMN_N: '#?components.words.inspector.label.column-n|Column {n}',
ARIA_ADD_COLUMN_END:
'#?components.words.inspector.aria.add-column-end|Add column at end',
ARIA_REMOVE_COLUMN_N:
'#?components.words.inspector.aria.remove-column-n|Remove column {n}',
ARIA_ADD_COLUMN_END: '#?components.words.inspector.aria.add-column-end|Add column at end',
ARIA_REMOVE_COLUMN_N: '#?components.words.inspector.aria.remove-column-n|Remove column {n}',
ARIA_ADD_BLOCK_TO_COLUMN_N:
'#?components.words.inspector.aria.add-block-to-column-n|Add block to column {n}',
LABEL_ADD_BLOCK:
'#?components.words.inspector.label.add-block|Add block',
LABEL_COLUMN_BLOCKS:
'#?components.words.inspector.label.column-blocks|Blocks',
LABEL_ADD_BLOCK: '#?components.words.inspector.label.add-block|Add block',
LABEL_COLUMN_BLOCKS: '#?components.words.inspector.label.column-blocks|Blocks',
ARIA_SELECT_BLOCK_IN_COLUMN:
'#?components.words.inspector.aria.select-block-in-column|Select this block',
ARIA_DELETE_BLOCK_IN_COLUMN:
'#?components.words.inspector.aria.delete-block-in-column|Delete this block',
ARIA_MOVE_BLOCK_UP:
'#?components.words.inspector.aria.move-block-up|Move block up',
ARIA_MOVE_BLOCK_DOWN:
'#?components.words.inspector.aria.move-block-down|Move block down',
BREADCRUMB_DOCUMENT:
'#?components.words.inspector.breadcrumb.document|Document',
BREADCRUMB_LIST_ITEM:
'#?components.words.inspector.breadcrumb.list-item|Item {n}',
BREADCRUMB_TABLE_ROW:
'#?components.words.inspector.breadcrumb.table-row|Row {n}',
PLACEHOLDER_COLUMN_WIDTH:
'#?components.words.inspector.placeholder.column-width|1fr, 200px, 30%',
ARIA_MOVE_BLOCK_UP: '#?components.words.inspector.aria.move-block-up|Move block up',
ARIA_MOVE_BLOCK_DOWN: '#?components.words.inspector.aria.move-block-down|Move block down',
BREADCRUMB_DOCUMENT: '#?components.words.inspector.breadcrumb.document|Document',
BREADCRUMB_LIST_ITEM: '#?components.words.inspector.breadcrumb.list-item|Item {n}',
BREADCRUMB_TABLE_ROW: '#?components.words.inspector.breadcrumb.table-row|Row {n}',
PLACEHOLDER_COLUMN_WIDTH: '#?components.words.inspector.placeholder.column-width|1fr, 200px, 30%',
// Section titles -----------------------------------------------------
SECTION_BLOCK: '#?components.words.inspector.section.block|Block',
@ -90,8 +79,7 @@ export const WORDS_INSPECTOR_LANGS = {
LABEL_WIDTH: '#?components.words.inspector.label.width|Width',
LABEL_HEIGHT: '#?components.words.inspector.label.height|Height',
LABEL_IMAGE_ALIGN: '#?components.words.inspector.label.image-align|Align',
LABEL_IMAGE_FULL_WIDTH:
'#?components.words.inspector.label.image-full-width|Full width',
LABEL_IMAGE_FULL_WIDTH: '#?components.words.inspector.label.image-full-width|Full width',
IMAGE_FULL_WIDTH_ON: '#?components.words.inspector.image-full-width.on|On',
IMAGE_FULL_WIDTH_OFF: '#?components.words.inspector.image-full-width.off|Off',
LABEL_INTENT: '#?components.words.inspector.label.intent|Intent',
@ -108,10 +96,8 @@ export const WORDS_INSPECTOR_LANGS = {
VALIGN_TOP: '#?components.words.inspector.valign.top|Top',
VALIGN_MIDDLE: '#?components.words.inspector.valign.middle|Middle',
VALIGN_BOTTOM: '#?components.words.inspector.valign.bottom|Bottom',
ARIA_CELL_ALIGN:
'#?components.words.inspector.aria.cell-align|Cell horizontal align',
ARIA_CELL_VALIGN:
'#?components.words.inspector.aria.cell-valign|Cell vertical align',
ARIA_CELL_ALIGN: '#?components.words.inspector.aria.cell-align|Cell horizontal align',
ARIA_CELL_VALIGN: '#?components.words.inspector.aria.cell-valign|Cell vertical align',
// Callout intent options — labels carry the perceptual reading the
// sema doctrine assigns to each value (cap. 12). Used as chip text
@ -143,26 +129,19 @@ export const WORDS_INSPECTOR_LANGS = {
'#?components.words.inspector.aria.upload-image|Upload image (encodes inline as data URL)',
ARIA_INTENT: '#?components.words.inspector.aria.intent|Callout intent',
ARIA_TITLE: '#?components.words.inspector.aria.title|Callout title',
ARIA_ADD_ROW_ABOVE:
'#?components.words.inspector.aria.add-row-above|Insert row above',
ARIA_ADD_ROW_BELOW:
'#?components.words.inspector.aria.add-row-below|Insert row below',
ARIA_ADD_ROW_ABOVE: '#?components.words.inspector.aria.add-row-above|Insert row above',
ARIA_ADD_ROW_BELOW: '#?components.words.inspector.aria.add-row-below|Insert row below',
ARIA_REMOVE_ROW: '#?components.words.inspector.aria.remove-row|Delete row',
ARIA_ADD_COLUMN_LEFT:
'#?components.words.inspector.aria.add-column-left|Insert column left',
ARIA_ADD_COLUMN_RIGHT:
'#?components.words.inspector.aria.add-column-right|Insert column right',
ARIA_REMOVE_COLUMN:
'#?components.words.inspector.aria.remove-column|Delete column',
ARIA_ADD_COLUMN_LEFT: '#?components.words.inspector.aria.add-column-left|Insert column left',
ARIA_ADD_COLUMN_RIGHT: '#?components.words.inspector.aria.add-column-right|Insert column right',
ARIA_REMOVE_COLUMN: '#?components.words.inspector.aria.remove-column|Delete column',
ARIA_HEADER_ROW: '#?components.words.inspector.aria.header-row|Toggle header row',
ARIA_HEADER_COLUMN:
'#?components.words.inspector.aria.header-column|Toggle header column',
ARIA_HEADER_COLUMN: '#?components.words.inspector.aria.header-column|Toggle header column',
PLACEHOLDER_CODE_LANGUAGE:
'#?components.words.inspector.placeholder.code-language|e.g. javascript',
PLACEHOLDER_SRC: '#?components.words.inspector.placeholder.src|https://...',
PLACEHOLDER_ALT: '#?components.words.inspector.placeholder.alt|Describe the image',
PLACEHOLDER_CAPTION:
'#?components.words.inspector.placeholder.caption|Optional caption',
PLACEHOLDER_CAPTION: '#?components.words.inspector.placeholder.caption|Optional caption',
PLACEHOLDER_CALLOUT_TITLE:
'#?components.words.inspector.placeholder.callout-title|Optional title',
@ -307,6 +286,8 @@ export const WORDS_INSPECTOR_BUNDLE = {
'cell-valign': { en: 'Vertical align', es: 'Alineación vertical' },
'column-width': { en: 'Width', es: 'Ancho' },
'column-n': { en: 'Column {n}', es: 'Columna {n}' },
'column-blocks': { en: 'Blocks', es: 'Bloques' },
'add-block': { en: 'Add block', es: 'Añadir bloque' },
font: { en: 'Font', es: 'Fuente' },
'font-size': { en: 'Font size', es: 'Tamaño' },
weight: { en: 'Weight', es: 'Grosor' },
@ -443,4 +424,3 @@ export const WORDS_INSPECTOR_BUNDLE = {
es: 'Selecciona un bloque para editar su estilo.'
}
} as const;

@ -5,6 +5,13 @@ export const wordsMorfo = {
name: 'Words',
kebab: 'words',
scope: ['soma', 'sema'],
// No bespoke sema pack: the 12 events fire into the engine and take
// their perceptual signature from each family's BASE in SEMA_MAP
// (commits = neutral tap, signal-warn = warning tone). `family-default`
// makes that absence explicit so `morfo:vocabulary` stops flagging it
// as an ambiguous omission. Apps can layer cascade rules at
// `defineEngineSemantic({ overrides: { cascade } })` if they want more.
expression: 'family-default',
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/textbox/',
texts: {
label: '#?components.words.label|Rich text editor',
@ -247,20 +254,23 @@ export const wordsMorfo = {
severity: 'optional'
},
{
// Structural node KIND — mirrors `WordsDomNodeKind` in the
// engine (`soma/.../engine/dom.ts`). This is NOT the block
// TYPE (paragraph / heading / image / …) — that lives on
// `data-words-block` below. Top-level blocks carry
// `node='block'`; the container kinds (list / table / …) and
// the leaf inlines (link / text) carry their own kind.
attr: 'data-words-node',
values: [
'text',
'block',
'inline',
'list',
'list-item',
'table',
'table-row',
'table-cell',
'link',
'paragraph',
'heading',
'quote',
'code',
'image'
'text'
],
severity: 'optional'
},

@ -143,10 +143,15 @@ export function updateBlockAtPath(
safePatch[k] = v;
}
const nextDoc = updateNodeAtPath(state.document, blockPath, (node) => ({
...(node as Record<string, unknown>),
...safePatch
}) as unknown as WordsBlock);
const nextDoc = updateNodeAtPath(
state.document,
blockPath,
(node) =>
({
...(node as unknown as Record<string, unknown>),
...safePatch
}) as unknown as WordsBlock
);
if (nextDoc === state.document) return noOp(state);
const normalized = normalizeDocument(nextDoc).document;
return changed({ ...state, document: normalized });

Loading…
Cancel
Save

Powered by TurnKey Linux.