CLAUDE.md@src/cli/tables · git:20260522.ab75e0f · 2026-05-22 · sha256 4f062ab9ff4d3e3a

CLAUDE.md@src/cli/tables git:20260522.ab75e0fA

Immutable. This exact content never changes and is served at /api/v1/blob/4f062ab9ff4d3e3a.

# src/cli/tables — table restructuring verbs

`docx tables <verb>` reshapes an existing table: `insert-row` / `delete-row` /
`insert-column` / `delete-column` / `set-widths` / `merge` / `unmerge` /
`borders`. There is no `list` verb — `docx read --ast` already returns the full
table structure (grid widths, `gridSpan`, `vMerge`, and tracked-change markers).

## The merge-aware grid model is the foundation

[grid.ts](grid.ts) (`buildGrid`) resolves a `<w:tbl>` into logical coordinates:
each physical `<w:tc>` occupies `colSpan` logical columns from `colStart`, and a
`vMerge="continue"` cell is still a real `<w:tc>`. Every verb queries the model
(`cellAt`, `colCount`) to map a `tN:rR` / `tN:cC` / `tN:rR1cC1-rR2cC2` locator
onto the physical cells it mutates. Don't index `<w:tc>` positionally — a
spanned column has fewer physical cells than logical ones.

## Structural edits never corrupt a merge

`insert-column` rejects a position that **bisects** a horizontal span;
`delete-column` rejects a column that **passes through** one; `delete-row`
rejects a row holding the **restart** of a vMerge that would orphan its
continuations; `merge` rejects a region whose edges **cross** an existing merge.
Those point the agent at `tables unmerge` first. `delete-row`/`delete-column`
also refuse to empty the table (delete the last row/column → use `docx delete
tN`).

**`insert-row` is the exception — it extends rather than rejects.** A row
inserted inside a vertical merge gets a `vMerge="continue"` cell in each merged
column so the merge grows through the new row; a row inserted below the merge is
a normal row. This matches Word's UI behavior exactly (verified empirically: a
Word UI row-insert inside a vmerge extends it, and our output round-trips
through Word unchanged), and is why `insert-row` builds its row via `buildRow`
(consulting the row now below) rather than a flat band of empty cells.

## Fresh XML and `<w:tcPr>`/`<w:tblPr>` surgery live in mutate.tsx

[mutate.tsx](mutate.tsx) holds the constructors (`emptyCell`, `gridColElement`)
and the in-place setters (`setGridSpan`, `setVMerge`, `setCellWidth`,
`setTableLayout`, `setTablePropertiesChild`). The setters splice individual
children into existing `<w:tcPr>`/`<w:tblPr>` **in CT_TcPr/CT_TblPr schema order**
(§17.4.42 / §17.4.60) without disturbing siblings — so unmodeled cell/table
properties survive (the in-place-mutation invariant). `grid.ts` stays pure
read-model (`.ts`, no JSX); construction happens in the `.tsx` files.

## Track-changes: native where a construct exists, audit comment otherwise

See [src/cli/track-changes](../track-changes/CLAUDE.md) for the lifecycle. The
split below was settled **empirically** by driving Microsoft Word (AppleScript
`accept all revisions` / `reject all revisions`) and comparing its result to
ours — Word, not the ECMA-376 schema, is the arbiter of what actually
round-trips. ECMA-376 defines more table-revision markup than Word honors.

- **Rows** → native `<w:trPr><w:ins>` / `<w:del>` (kinds `rowIns`/`rowDel`).
  Word authors and round-trips these.
- **Columns** → per-cell `<w:tcPr><w:cellIns>` / `<w:cellDel>` (`cellIns`/
  `cellDel`); `insert-column` also emits a paired `<w:tblGridChange>` so the
  width growth is reversible. `delete-column` leaves the grid intact — the
  column is trimmed by the accept-time **grid resync** in `apply.ts`
  (`resyncTableGrids`). Word's UI doesn't author `cellIns`/`cellDel`, but it
  **reads and accept/rejects** the ones we author (verified).
- **set-widths** → `<w:tblGridChange>` (grid snapshot) **plus a per-cell
  `<w:tcPrChange>`** (each `<w:tcW>`). Both are needed: Word's reject is driven
  by the per-cell `tcPrChange` — a `tblGridChange` alone is *not* reverted by
  Word. This mirrors exactly what Word emits for a width change.
- **merge / unmerge / borders** → applied **immediately** + a `[docx-cli]` audit
  comment via `noteStructuralChange` ([common.ts](common.ts)). Verified: Word
  does **not** track cell merges/splits (it warns "this action won't be marked
  as a change" and applies a plain `<w:gridSpan>`), and it does **not** revert a
  hand-authored `<w:tblPrChange>` for borders. Authoring a revision Word won't
  round-trip would be dishonest, so we match Word and just note it.

We additionally **read** (list / accept / reject) `tblPrChange` and `tcPrChange`
that *Word* authors (e.g. from an interactive width edit), even though our only
emitter of `tcPrChange` is set-widths and we never emit `tblPrChange`.

When adding a tracked structural kind, follow the "Adding a tracked-change kind"
checklist in the track-changes CLAUDE.md — register it in `core/ast/read.ts`
(table walk) **and** `apply.ts` (`visitTable`) in the same order, or `tcN` ids
drift between `list` and `accept` — and verify it against Word before trusting
the schema.