diff --git a/coverage/emitted-schema-known-failures.json b/coverage/emitted-schema-known-failures.json
index 7abd554..fe51488 100644
--- a/coverage/emitted-schema-known-failures.json
+++ b/coverage/emitted-schema-known-failures.json
@@ -1,8 +1 @@
-[
- {
- "id": "synthetic-table-missing-tblgrid",
- "issue": "#453",
- "match": "}tr': This element is not expected.",
- "reason": "synthetic table fixtures omit the required w:tblGrid, and the engine faithfully preserves the gap into emitted output"
- }
-]
+[]
diff --git a/packages/docx-core/src/integration/canonical-emission-regression.test.ts b/packages/docx-core/src/integration/canonical-emission-regression.test.ts
index 5920af9..3ffffde 100644
--- a/packages/docx-core/src/integration/canonical-emission-regression.test.ts
+++ b/packages/docx-core/src/integration/canonical-emission-regression.test.ts
@@ -215,7 +215,7 @@ describe('Canonical emission catalog', () => {
await given('a table row with a prior height definition', async () => {
const { doc } = await loadIndexedDoc(
await makeMinimalDocx(
- 'Cell',
+ 'Cell',
),
);
@@ -240,7 +240,7 @@ describe('Canonical emission catalog', () => {
await given('a table cell with prior padding properties', async () => {
const { doc } = await loadIndexedDoc(
await makeMinimalDocx(
- 'Cell',
+ 'Cell',
),
);
diff --git a/packages/docx-core/src/primitives/layout.test.ts b/packages/docx-core/src/primitives/layout.test.ts
index 3ce1146..cea8034 100644
--- a/packages/docx-core/src/primitives/layout.test.ts
+++ b/packages/docx-core/src/primitives/layout.test.ts
@@ -143,56 +143,61 @@ describe('layout tracked-change emission', () => {
});
});
- test('setTableCellPadding emits tcPrChange with the prior cell properties snapshot', async ({ given, when, then }: AllureBddContext) => {
- let doc: Document;
- let tcMar: Element;
- let left: Element;
- let tcPrChange: Element;
- let previousTcMar: Element;
-
- await given('a table cell that already has top padding', () => {
- doc = makeDocument(
- `Cell`,
- );
- });
-
- await when('tracked left padding is added', () => {
- const result = setTableCellPadding(
- doc,
- { tableIndexes: [0], leftDxa: 240 },
- createRevisionContext({
- author: 'SafeDocX AI',
- date: '2026-05-03T14:15:16Z',
- idState: createRevisionIdState(),
- }),
- );
- expect(result).toEqual({
- affectedCells: 1,
- missingTableIndexes: [],
- missingRowIndexes: [],
- missingCellIndexes: [],
- });
-
- const table = doc.getElementsByTagNameNS(W_NS, W.tbl).item(0) as Element;
- const row = firstDirectChild(table, W.tr);
- const cell = firstDirectChild(row, W.tc);
- const tcPr = firstDirectChild(cell, W.tcPr);
- tcMar = firstDirectChild(tcPr, W.tcMar);
- left = firstDirectChild(tcMar, W.left);
- tcPrChange = firstDirectChild(tcPr, 'tcPrChange');
- previousTcMar = firstDirectChild(firstDirectChild(tcPrChange, W.tcPr), W.tcMar);
- });
-
- await then('the outer cell properties are updated while the inner tcPr snapshot preserves the old padding', () => {
- expect(wordAttr(tcPrChange, 'author')).toBe('SafeDocX AI');
- expect(wordAttr(tcPrChange, 'date')).toBe('2026-05-03T14:15:16Z');
- expect(revisionId(tcPrChange)).toBe(1);
- expect(wordAttr(left, 'w')).toBe('240');
- expect(wordAttr(left, 'type')).toBe('dxa');
- expect(getDirectChildrenByName(previousTcMar, W.left)).toHaveLength(0);
- expect(firstDirectChild(previousTcMar, W.top)).toBeDefined();
- });
- });
+ test
+ .conformance({ spec: 'ECMA-376', edition: 5, part: 1, section: '17.4.68' })(
+ 'setTableCellPadding emits tcPrChange with the prior cell properties snapshot',
+ async ({ given, when, then }: AllureBddContext) => {
+ let doc: Document;
+ let tcMar: Element;
+ let left: Element;
+ let tcPrChange: Element;
+ let previousTcMar: Element;
+
+ await given('a table cell that already has top padding', () => {
+ doc = makeDocument(
+ `Cell`,
+ );
+ });
+
+ await when('tracked left padding is added', () => {
+ const result = setTableCellPadding(
+ doc,
+ { tableIndexes: [0], leftDxa: 240 },
+ createRevisionContext({
+ author: 'SafeDocX AI',
+ date: '2026-05-03T14:15:16Z',
+ idState: createRevisionIdState(),
+ }),
+ );
+ expect(result).toEqual({
+ affectedCells: 1,
+ missingTableIndexes: [],
+ missingRowIndexes: [],
+ missingCellIndexes: [],
+ });
+
+ const table = doc.getElementsByTagNameNS(W_NS, W.tbl).item(0) as Element;
+ const row = firstDirectChild(table, W.tr);
+ const cell = firstDirectChild(row, W.tc);
+ const tcPr = firstDirectChild(cell, W.tcPr);
+ tcMar = firstDirectChild(tcPr, W.tcMar);
+ left = firstDirectChild(tcMar, W.left);
+ tcPrChange = firstDirectChild(tcPr, 'tcPrChange');
+ previousTcMar = firstDirectChild(firstDirectChild(tcPrChange, W.tcPr), W.tcMar);
+ });
+
+ await then('the outer cell properties are updated while the inner tcPr snapshot preserves the old padding', () => {
+ expect(wordAttr(tcPrChange, 'author')).toBe('SafeDocX AI');
+ expect(wordAttr(tcPrChange, 'date')).toBe('2026-05-03T14:15:16Z');
+ expect(revisionId(tcPrChange)).toBe(1);
+ expect(Array.from(tcMar.children).map((child) => child.localName)).toEqual([W.top, W.left]);
+ expect(wordAttr(left, 'w')).toBe('240');
+ expect(wordAttr(left, 'type')).toBe('dxa');
+ expect(getDirectChildrenByName(previousTcMar, W.left)).toHaveLength(0);
+ expect(firstDirectChild(previousTcMar, W.top)).toBeDefined();
+ });
+ },
+ );
test('layout primitives preserve legacy mutation behavior when revision context is omitted', async ({ given, when, then }: AllureBddContext) => {
let doc: Document;
diff --git a/packages/docx-core/src/primitives/layout.ts b/packages/docx-core/src/primitives/layout.ts
index 63bb952..e5d7e8d 100644
--- a/packages/docx-core/src/primitives/layout.ts
+++ b/packages/docx-core/src/primitives/layout.ts
@@ -92,6 +92,19 @@ function ensureChild(parent: Element, localName: string): Element {
return created;
}
+/**
+ * Keep per-cell margins in the CT_TcMar child sequence required by the schema.
+ *
+ * @conformance ECMA-376 edition 5, Part 1 § 17.4.68
+ */
+function reorderCellMarginEdges(tcMar: Element): void {
+ const orderedNames = [W.top, W.left, W.bottom, W.right];
+ const ordered = orderedNames.flatMap((name) => getDirectChildrenByName(tcMar, name));
+ for (const child of ordered) {
+ tcMar.appendChild(child);
+ }
+}
+
function setWAttr(el: Element, localName: string, value: string): void {
el.setAttributeNS(OOXML.W_NS, `w:${localName}`, value);
}
@@ -274,6 +287,7 @@ export function setTableCellPadding(
setWAttr(right, W.w, String(mutation.rightDxa));
setWAttr(right, W.type, 'dxa');
}
+ reorderCellMarginEdges(tcMar);
if (ctx) {
// CT_TcPr permits at most one child.
for (const stale of getDirectChildrenByName(tcPr, 'tcPrChange')) {
diff --git a/packages/docx-core/src/primitives/minimal_save.test.ts b/packages/docx-core/src/primitives/minimal_save.test.ts
index c3af726..a0d6c12 100644
--- a/packages/docx-core/src/primitives/minimal_save.test.ts
+++ b/packages/docx-core/src/primitives/minimal_save.test.ts
@@ -193,7 +193,7 @@ describe('minimal re-serialization on save (issue #408)', () => {
`cell` +
` text`;
const TBL = (cellP: string) =>
- `${cellP}`;
+ `${cellP}`;
let untouched: { originalXml: string; savedXml: string };
let modified: { originalXml: string; savedXml: string };
@@ -240,10 +240,10 @@ describe('minimal re-serialization on save (issue #408)', () => {
// proofErr/rsid-bearing paragraph in the table must keep its original
// XML — issue #408's repro document is almost entirely tables.
const cellP = (id: string, marker: string) =>
- `${marker}` +
- ` tail`;
+ `${marker}` +
+ ` tail`;
const tableBody =
- `` +
+ `` +
`${cellP('00000A01', 'AA')}${cellP('00000A02', 'AB')}${cellP('00000B01', 'BA')}` +
`${cellP('00000C01', 'CA')}${cellP('00000D01', 'DA')}` +
``;
diff --git a/packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts b/packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts
index b6517af..b8da33c 100644
--- a/packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts
+++ b/packages/docx-mcp/src/integration/canonical-emission-mcp.test.ts
@@ -268,6 +268,8 @@ describe('Tool integration through SessionManager: canonical revision emission',
xml: makeDocXml(
'Spacing paragraph.' +
'' +
+ '' +
+ '' +
'' +
'A1' +
'B1' +
diff --git a/packages/docx-mcp/src/tools/add_accept_tracked_changes.test.ts b/packages/docx-mcp/src/tools/add_accept_tracked_changes.test.ts
index fab92e2..431a2fa 100644
--- a/packages/docx-mcp/src/tools/add_accept_tracked_changes.test.ts
+++ b/packages/docx-mcp/src/tools/add_accept_tracked_changes.test.ts
@@ -161,7 +161,7 @@ describe('Traceability: Accept Tracked Changes', () => {
`` +
`bold text` +
`` +
- `` +
+ `` +
`` +
`cell`;
const filePath = await writeTestDocx(dir, 'prchange.docx', bodyXml);
diff --git a/packages/docx-mcp/src/tools/add_safe_docx_layout_format_controls.test.ts b/packages/docx-mcp/src/tools/add_safe_docx_layout_format_controls.test.ts
index c69989c..16d2e91 100644
--- a/packages/docx-mcp/src/tools/add_safe_docx_layout_format_controls.test.ts
+++ b/packages/docx-mcp/src/tools/add_safe_docx_layout_format_controls.test.ts
@@ -77,6 +77,8 @@ describe('Traceability: Layout Format Controls', () => {
`` +
`Table heading` +
`` +
+ `` +
+ `` +
`` +
`A1` +
`B1` +
diff --git a/packages/docx-mcp/src/tools/extract_revisions.test.ts b/packages/docx-mcp/src/tools/extract_revisions.test.ts
index 7150e12..a4be2b4 100644
--- a/packages/docx-mcp/src/tools/extract_revisions.test.ts
+++ b/packages/docx-mcp/src/tools/extract_revisions.test.ts
@@ -799,7 +799,7 @@ describe('extract_revisions tool', () => {
`` +
`` +
`` +
- `` +
+ `` +
`Cell ` +
`edited` +
`` +
diff --git a/packages/docx-mcp/src/tools/format_layout.validation.test.ts b/packages/docx-mcp/src/tools/format_layout.validation.test.ts
index a524616..278d780 100644
--- a/packages/docx-mcp/src/tools/format_layout.validation.test.ts
+++ b/packages/docx-mcp/src/tools/format_layout.validation.test.ts
@@ -50,6 +50,8 @@ describe('format_layout validation + strictness', () => {
`` +
`Only paragraph` +
`` +
+ `` +
+ `` +
`A1` +
`` +
`` +
@@ -109,6 +111,8 @@ describe('format_layout validation + strictness', () => {
`` +
`P` +
`` +
+ `` +
+ `` +
`A1` +
`` +
`` +
diff --git a/packages/docx-mcp/src/tools/non_body_part_preservation.test.ts b/packages/docx-mcp/src/tools/non_body_part_preservation.test.ts
index e516e76..6edd956 100644
--- a/packages/docx-mcp/src/tools/non_body_part_preservation.test.ts
+++ b/packages/docx-mcp/src/tools/non_body_part_preservation.test.ts
@@ -31,6 +31,8 @@ describe('format_layout: non-body part preservation', () => {
`` +
`Cover Terms` +
`` +
+ `` +
+ `` +
`` +
`A1` +
`B1` +
diff --git a/packages/docx-mcp/src/tools/strict_transactionality.test.ts b/packages/docx-mcp/src/tools/strict_transactionality.test.ts
index 304144a..297cc07 100644
--- a/packages/docx-mcp/src/tools/strict_transactionality.test.ts
+++ b/packages/docx-mcp/src/tools/strict_transactionality.test.ts
@@ -40,6 +40,8 @@ describe('format_layout: strict failure transactionality', () => {
`` +
`Alpha` +
`` +
+ `` +
+ `` +
`A1` +
`A2` +
`` +