From 6731160967479c8b42249f094935cc9ba2c538c1 Mon Sep 17 00:00:00 2001 From: thewrz Date: Tue, 4 Aug 2026 13:48:11 -0700 Subject: [PATCH 01/17] feat(ast): add vanishCharStyleIds to ObjectMetaSchema Resolved w:rStyle -> character-style w:vanish IDs, captured alongside a body object so capture and the edit rewrite path can share one source of truth without needing styles.xml at rewrite time (#650). Additive JSONB field, no migration needed: absent and [] are interchangeable, so an object row captured before this change loads/parses unchanged. openapi.yaml updated to match. Co-Authored-By: Claude Sonnet 5 --- openapi.yaml | 11 +++++++ src/ast/object-schemas.test.ts | 59 ++++++++++++++++++++++++++++++++++ src/ast/object-schemas.ts | 11 +++++++ 3 files changed, 81 insertions(+) diff --git a/openapi.yaml b/openapi.yaml index 082dda69..0a13c5e3 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -8132,6 +8132,17 @@ components: type: integer minimum: 1 description: Table column count (table kind only). + vanishCharStyleIds: + type: array + description: >- + Resolved character-style IDs (#650) carrying an enabled + `w:vanish` — the styles a run's `w:rStyle` must resolve through + to be treated as hidden. Captured at import so hidden text stays + excluded from `objectText` on later edits, without needing + `styles.xml` at edit time. Absent and `[]` are interchangeable; + an object captured before this field existed has neither. + items: + type: string blob: type: array description: >- diff --git a/src/ast/object-schemas.test.ts b/src/ast/object-schemas.test.ts index 83064ab5..0ce7c420 100644 --- a/src/ast/object-schemas.test.ts +++ b/src/ast/object-schemas.test.ts @@ -111,6 +111,65 @@ describe('ObjectMetaSchema', () => { }); }); +// ── vanishCharStyleIds (#650) ─────────────────────────────────────────────── +// Resolved w:rStyle → character-style w:vanish IDs, captured alongside the +// object so capture and rewrite share one source of truth without needing +// styles.xml at rewrite time. Additive JSONB field: absent and [] are +// interchangeable, and a row captured before this change (no key at all) +// must still load/parse identically to today. +describe('ObjectMetaSchema — vanishCharStyleIds (#650)', () => { + const validTable = { + kind: 'table' as const, + floating: false, + generation: 'drawingml' as const, + rows: 1, + columns: 1, + blob: TABLE_BLOB, + }; + + it('a row/object captured before this change (no vanishCharStyleIds key) loads identically', () => { + const result = ObjectMetaSchema.safeParse(validTable); + expect(result.success).toBe(true); + if (!result.success) return; + expect('vanishCharStyleIds' in result.data).toBe(false); + }); + + it('accepts a table object with a populated vanishCharStyleIds array', () => { + const withVanish = { ...validTable, vanishCharStyleIds: ['HiddenChar', 'Redacted'] }; + const result = ObjectMetaSchema.safeParse(withVanish); + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.data.vanishCharStyleIds).toEqual(['HiddenChar', 'Redacted']); + }); + + it('accepts an empty vanishCharStyleIds array, interchangeable with absent', () => { + expect(ObjectMetaSchema.safeParse({ ...validTable, vanishCharStyleIds: [] }).success).toBe( + true + ); + }); + + it('rejects a non-string entry in vanishCharStyleIds', () => { + const bad = { ...validTable, vanishCharStyleIds: ['HiddenChar', 42] }; + expect(ObjectMetaSchema.safeParse(bad).success).toBe(false); + }); + + it('a textBox object (no rows/columns) still validates with vanishCharStyleIds populated — the field never couples to kind', () => { + const textBox = { + kind: 'textBox' as const, + floating: true, + generation: 'vml' as const, + blob: [{ '#text': 'boxed text' }], + vanishCharStyleIds: ['HiddenChar'], + }; + const result = ObjectMetaSchema.safeParse(textBox); + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.data.vanishCharStyleIds).toEqual(['HiddenChar']); + expect(result.data.rows).toBeUndefined(); + expect(result.data.columns).toBeUndefined(); + }); +}); + // ── Editability fixation (#300, ADR-072 decision 2) ──────────────────────── // An 'object' node is always locked (a captured OOXML blob is never // paragraph-editable text) and its 'objectText' children are always editable diff --git a/src/ast/object-schemas.ts b/src/ast/object-schemas.ts index 61e7ecf9..6ac24c65 100644 --- a/src/ast/object-schemas.ts +++ b/src/ast/object-schemas.ts @@ -72,6 +72,16 @@ export const ObjectBlobNodeSchema: z.ZodType = z.custom