CLAUDE.md@src/cli/tables · git:20260523.dc8aa06 · 2026-05-23 · sha256 6bb8bb4e516ce786

CLAUDE.md@src/cli/tables git:20260523.dc8aa06A

Immutable. This exact content never changes and is served at /api/v1/blob/6bb8bb4e516ce786.

# 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 model and mutation primitives these verbs build on live in
[`@core/table`](../../core/table/): the merge-aware grid (`buildGrid`,
`cellAt`), the `<w:tcPr>`/`<w:tblPr>` surgery (`setGridSpan`, `setVMerge`,
`emptyCell`, `appendTblGridChange`, …), and the `resolveTableNode` /
`parseTableAt` / `parseRowAt` / `parseColumnAt` / `parseCellRangeAt` /
`parseCellAt` locator helpers. Every locator helper accepts the chained nested
form (`t0:r0c1:t0`, `t0:r0c1:t0:r1`, …) so every verb addresses nested tables
with the same syntax. This folder owns only the CLI surface: arg-parse, the
merge-correctness gates ("don't corrupt a merge", below), `--dry-run`/`--output`
glue, and `noteStructuralChange` (the audit-comment policy for changes Word
won't round-trip).

## 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.

## 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.