refactor(words): table extension stub — name + nodeTypes + factories + commandNames (F2.3i)

Publishes `tableExtension: WordsExtension` in `extensions/table/table-extension.ts`
with the three static facets we can ship without touching the engine:

- `nodeTypes`: ['table', 'table-row', 'table-cell']
- `factories`: typed constructors for the three node types
- `commandNames`: the 12 public table commands (insertTable, deleteTableRow,
  toggleTableHeaderRow, setTableCellTone, …) mapped to internal operation
  types (1:1 today).

The dynamic hooks (`render`, `normalize`, `serialize`, `path`, `keyboard`,
`commands`, `events`) are intentionally absent — F2.3f-h port each one
incrementally. The engine does NOT yet consume the registry; the stub
publishes the shape the engine will respect once F2.3i full lands.

6 smoke tests verify the public shape (nodeTypes, factories produce
valid nodes, registry registers cleanly + finds by every nodeType, all
12 command names exposed, hooks intentionally absent, default 1×1
table). 102/102 in extensions + engine scope, 149/149 in full words
soma scope.

CONTINUE.md updated to reflect F2.3i-stub done + next-session order
(F2.3h first, then F2.3f/g — F2.3i full wire-up follows F2.3h).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 54cd1744c7
commit bde6728590

@ -212,15 +212,16 @@ selectors y type unions coincidan.
- F2.2 skeleton: `extension-types.ts` + `extension-registry.ts` con 14 tests verdes
- F2.3a tipos de table movidos a `extensions/table/types.ts` con re-export en `engine/document.ts`
**F2.3 hecho** (sesión de hoy, 5 checkpoint commits + 143/143 tests verdes después de cada uno):
**F2.3 hecho** (sesiones 2026-05-27, 6 checkpoint commits + tests verdes después de cada uno):
- **F2.3a** ✅ — table types → `extensions/table/types.ts` (commit `088d31ec`)
- **F2.3b** ✅ — table factories + predicates + value-set constants → `extensions/table/factories.ts` (commit `a0afa44d`)
- **F2.3d** ✅ — table markdown serializer (serialize + parse + buildTableFromMarkdownRows) → `extensions/table/serialize-markdown.ts` (commit `d6f0af48`)
- **F2.3c** ✅ — table HTML serializer (serializeTableHtml + parseTableHtml + cell helpers) → `extensions/table/serialize-html.ts` (commit `5797e69b`)
- **F2.3e** ✅ — table render (renderTable + renderTablePlainText) → `extensions/table/render.ts` (commit `d8392735`)
- **F2.3i (stub)** ✅ — `tableExtension: WordsExtension` con `nodeTypes` + `factories` + `commandNames` (12) publicado en `extensions/table/table-extension.ts`. Engine NO consume registry todavía — la stub publica el shape estable que F2.3f-h irán rellenando con hooks. 6 smoke tests + 102 tests en el scope extensions+engine en verde.
**F2.3 pendiente — los 4 sub-pasos restantes son el refactor difícil**:
**F2.3 pendiente — los 3 sub-pasos restantes son el refactor difícil** (la stub F2.3i existe ya como receptor de hooks):
- **F2.3f** — path.ts navigation. Las refs de table están en BRANCHES dentro de funciones grandes (`resolvePath`, `updateNode`, `inferContainerKind`), NO en helpers aislados. Extracción requiere:
- Diseñar callback adapters para recursión (engine → extension → engine)
@ -234,21 +235,20 @@ selectors y type unions coincidan.
- Mover los ~20 reducers a `extensions/table/operations.ts`
- El dispatcher central (`applyWordsCommand`) consulta registry.getCommand(opType) y fallback al switch existente
- **F2.3i** — registry wire-up. Crear `tableExtension: WordsExtension` que cabledea TODOS los hooks (commands, render, serialize, etc.) al registry. El engine constructor registra `tableExtension` por defecto para backward compatibility. Después el dispatcher reescrito consulta registry.
- **F2.3i (full)** — wire-up al engine constructor. Crear el patrón de instanciación del engine que (a) construye una `WordsExtensionRegistry`, (b) registra `tableExtension` por defecto, (c) hace que el dispatcher (`applyWordsCommand`), el render, el path, el normalize y los serializers consulten primero la registry y fallback al switch hardcodeado. Hoy `tableExtension` está como stub publicada — el engine la ignora.
- **F2.3j** — verify final.
**Recomendación de orden para próxima sesión** (lo que dije y se ha confirmado al hacer F2.3a-e):
**Recomendación de orden para próxima sesión** (confirmado tras F2.3a-e + F2.3i-stub):
1. Antes de F2.3f/g/h, hacer **F2.3i con stub vacía**: construir `tableExtension: WordsExtension` que registra sólo nodeTypes ['table','table-row','table-cell'], commandNames y events. El engine al boot lo registra. Después en h-i se mueve la lógica gradualmente hacia los hooks.
2. **F2.3h primero** (operations) usando el stub. Cada reducer extraído pasa de `case 'insertTable':` en switch a entrada en `extension.commands`. El switch del engine consulta registry.getCommand() antes del fallback.
3. **F2.3f y F2.3g** al final, donde el engine ya consulta registry para todo el resto.
1. **F2.3h primero** (operations) — la stub `tableExtension` ya es receptor válido. Mover los ~20 reducers (`insertTable*`, `deleteTable*`, `toggle-table-*`, `setTableCell*`, `moveTableCell`) a `extensions/table/operations.ts`, llenar `tableExtension.commands` con sus key=opType. Cabledar el dispatcher central (`applyWordsCommand`) para consultar `registry.getCommand(opType)` antes del switch. Decisión clave: las utilities compartidas (`tableCellOptions`, `tableOptions`, `replaceAt`, `currentTableCellPath`) van a `extensions/table/utils.ts` o se re-exportan desde engine; la opción a re-exportar las dos primeras (engine NO depende de ellas fuera del table-branch) y mover el resto.
2. **F2.3f y F2.3g** al final, donde el engine ya consulta registry para todo el resto. Diseñar visitor pattern en `path.ts` + `normalize.ts` para que el dispatcher pase el callback "is this a node my extension owns?" al walker.
**Estado del extension system** después de hoy:
- `WordsExtension` interface ✅ (F2.2)
- `WordsExtensionRegistry` con 14 tests verdes ✅ (F2.2)
- `extensions/table/` con types + factories + 2 serializers + render ✅ (F2.3a-e)
- Engine NO consulta registry aún (importa funciones directamente de extension). El cambio a "engine consulta registry para tipo X" llegará en F2.3i.
- `extensions/table/` con types + factories + 2 serializers + render + **`tableExtension` stub** ✅ (F2.3a-e + F2.3i-stub)
- Engine NO consulta registry aún (importa funciones directamente de extension; `tableExtension` publicada pero engine no la ve). El cambio a "engine consulta registry para tipo X" llegará en F2.3h.
Después F2.4 (code-block, plan similar), F2.5 (docs `EXTENSIONS.md`), F2.6 (verify final).

@ -35,3 +35,9 @@ export {
createWordsExtensionRegistry,
WordsExtensionRegistryError
} from './extension-registry';
// First built-in extension. The engine is NOT yet consuming the
// registry — `tableExtension` is published in stub form (nodeTypes +
// factories + commandNames only) so the public shape is stable while
// F2.3f-h port the hooks in.
export { tableExtension } from './table';

@ -8,14 +8,18 @@
* Pieces moved so far:
* - Types (F2.3a)
* - Factories + value-set predicates (F2.3b)
* - HTML serializer (F2.3c)
* - Markdown serializer (F2.3d)
* - Render hooks (F2.3e)
* - `tableExtension` stub (F2.3i — name + nodeTypes + factories +
* commandNames; render / normalize / serialize / path / commands
* hooks still hardcoded in the engine, to be moved in F2.3f-h).
*
* Pending:
* - Serializers (HTML + Markdown) (F2.3c, F2.3d)
* - Render (F2.3e)
* - Path / navigation (F2.3f)
* - Normalize (F2.3g)
* - Operations (F2.3h)
* - Registry wire-up + WordsExtension export (F2.3i)
* - Path / navigation hook (F2.3f)
* - Normalize hook (F2.3g)
* - Operations / commands (F2.3h)
* - Engine boot wire-up + handoff (F2.3 final)
*/
export type {
@ -61,3 +65,5 @@ export type {
TableRenderNode,
TableRenderText
} from './render';
export { tableExtension } from './table-extension';

@ -0,0 +1,85 @@
/**
* Smoke test for the F2.3i `tableExtension` stub.
*
* Validates the public shape published by the stub — it does NOT
* exercise the engine integration (hooks are absent on purpose at this
* stage; F2.3f-h add them).
*/
import { describe, expect, it } from 'vitest';
import { createWordsExtensionRegistry } from '../extension-registry';
import { tableExtension } from './table-extension';
describe('tableExtension (F2.3i stub)', () => {
it('declares the three table node types', () => {
expect(tableExtension.name).toBe('table');
expect(tableExtension.nodeTypes).toEqual(['table', 'table-row', 'table-cell']);
});
it('exposes factories that produce valid nodes for each type', () => {
const factories = tableExtension.factories;
expect(factories).toBeDefined();
const table = factories!.table();
expect(table.type).toBe('table');
const row = factories!['table-row']();
expect(row.type).toBe('table-row');
const cell = factories!['table-cell']();
expect(cell.type).toBe('table-cell');
});
it('registers cleanly and is found by every nodeType', () => {
const reg = createWordsExtensionRegistry();
reg.register(tableExtension);
expect(reg.findByNodeType('table')).toBe(tableExtension);
expect(reg.findByNodeType('table-row')).toBe(tableExtension);
expect(reg.findByNodeType('table-cell')).toBe(tableExtension);
expect(reg.allNodeTypes()).toEqual(['table', 'table-row', 'table-cell']);
});
it('exposes all 12 table command names through the registry', () => {
const reg = createWordsExtensionRegistry();
reg.register(tableExtension);
const expected = [
'insertTable',
'insertTableRow',
'insertTableColumn',
'deleteTableRow',
'deleteTableColumn',
'moveTableCell',
'toggleTableHeaderRow',
'toggleTableHeaderColumn',
'toggleTableStriped',
'toggleTableCompact',
'setTableCellTone',
'setTableCellVerticalAlign'
];
expect([...reg.allCommandNames()].sort()).toEqual(expected.slice().sort());
for (const name of expected) {
expect(reg.findByCommandName(name)).toBe(tableExtension);
}
});
it('intentionally exposes no hooks yet (stub)', () => {
// F2.3i ships the static facets only. F2.3f-h add hooks.
expect(tableExtension.render).toBeUndefined();
expect(tableExtension.normalize).toBeUndefined();
expect(tableExtension.serialize).toBeUndefined();
expect(tableExtension.path).toBeUndefined();
expect(tableExtension.keyboard).toBeUndefined();
expect(tableExtension.commands).toBeUndefined();
expect(tableExtension.events).toBeUndefined();
expect(tableExtension.dispose).toBeUndefined();
});
it('cell factory accepts no args and produces a 1×1 default table', () => {
const factories = tableExtension.factories!;
const table = factories.table() as {
type: string;
children: ReadonlyArray<{ type: string; children: ReadonlyArray<{ type: string }> }>;
};
expect(table.children).toHaveLength(1);
expect(table.children[0].type).toBe('table-row');
expect(table.children[0].children).toHaveLength(1);
expect(table.children[0].children[0].type).toBe('table-cell');
});
});

@ -0,0 +1,80 @@
/**
* Table extension — entrypoint that materialises the `WordsExtension`
* contract for the table node tree (`table` → `table-row` → `table-cell`).
*
* **F2.3i stub stage**: this file currently declares only the three
* static facets of the extension that we can publish without touching
* the engine's hardcoded branches:
*
* 1. `nodeTypes` — the three node `type` strings owned by the extension.
* 2. `factories` — typed constructors so callers can build empty
* `table` / `table-row` / `table-cell` nodes without importing
* the engine internals.
* 3. `commandNames` — the public command-name registry. Every
* table-related command in `engine/commands.ts` is mirrored here
* so the registry knows which public command belongs to this
* extension. The internal operation type matches the public name
* 1:1 today (no renames pending).
*
* The dynamic hooks (`render`, `normalize`, `serialize`, `path`,
* `keyboard`, `commands`) are intentionally absent. F2.3f-h port each
* one of those incrementally, at which point this object grows new
* fields; the public shape (this file) stays stable.
*
* The engine does NOT yet consult the registry at runtime — render /
* normalize / serialize / path / commands still hardcode the `table`
* branch. The extension is published so consumers (tests, future
* tooling, the eventual engine boot wire-up) can already see the shape
* the engine will respect once F2.3i full lands.
*/
import type { WordsExtension, WordsExtensionNode } from '../extension-types';
import {
createTable,
createTableCell,
createTableRow
} from './factories';
// Erase the typed factories to the registry's uniform shape. The factory
// functions remain individually typed for callers that import them
// directly; this adapter is only the contract-boundary view.
type ErasedFactory = (...args: ReadonlyArray<unknown>) => WordsExtensionNode;
const tableFactory = ((...args: ReadonlyArray<unknown>) =>
createTable(...(args as Parameters<typeof createTable>))) as ErasedFactory;
const tableRowFactory = ((...args: ReadonlyArray<unknown>) =>
createTableRow(...(args as Parameters<typeof createTableRow>))) as ErasedFactory;
const tableCellFactory = ((...args: ReadonlyArray<unknown>) =>
createTableCell(...(args as Parameters<typeof createTableCell>))) as ErasedFactory;
export const tableExtension: WordsExtension = {
name: 'table',
version: '1.0.0',
nodeTypes: ['table', 'table-row', 'table-cell'],
factories: {
table: tableFactory,
'table-row': tableRowFactory,
'table-cell': tableCellFactory
},
commandNames: {
// Public command name → internal operation type. They match 1:1
// today; the indirection lets us rename one side without breaking
// the other when needed.
insertTable: 'insertTable',
insertTableRow: 'insertTableRow',
insertTableColumn: 'insertTableColumn',
deleteTableRow: 'deleteTableRow',
deleteTableColumn: 'deleteTableColumn',
moveTableCell: 'moveTableCell',
toggleTableHeaderRow: 'toggleTableHeaderRow',
toggleTableHeaderColumn: 'toggleTableHeaderColumn',
toggleTableStriped: 'toggleTableStriped',
toggleTableCompact: 'toggleTableCompact',
setTableCellTone: 'setTableCellTone',
setTableCellVerticalAlign: 'setTableCellVerticalAlign'
}
// render / normalize / serialize / path / keyboard / commands /
// events / dispose intentionally absent — see file header.
} satisfies WordsExtension;
Loading…
Cancel
Save

Powered by TurnKey Linux.