@docx-editor.dev/core/contracts/document

@docx-editor.dev/core/contracts/document — the document-level edit and query vocabulary.

The subset that means something without a live editor: what an automation host or an LLM tool can ask for and change.

A CONTRACT module, not a barrel. contracts/editor builds the Editor command and query surfaces on top of these, so they cannot live in the package root: the root re-exports runtime from ../editor, and a contract importing the root would invert the dependency — safe today only because that import is type-only, and one accidental value import away from pulling the painted engine into a server bundle.

Interfaces (27)

ApplyResultinterfaceSource ↗

What applying a batch of edits produced: the new document, and a per-edit verdict.

results is positionally aligned with the input, so an edit that failed is identified by its index rather than by anything the caller has to correlate.

interface ApplyResult
MemberTypeSummary
docDocxDocument
resultsExecResult[]One per edit, positionally aligned with the input.

ContentControlinterfaceSource ↗

Structured document tag (w:sdt).

interface ContentControl
MemberTypeSummary
alias?string
contentreadonly Block[]
controlTypeContentControlType
idstring
kind'contentControl'
locked?boolean
tag?string

ContentControlFilterinterfaceSource ↗

Narrows a content-control query. Fields combine with AND; an empty filter matches every control.

interface ContentControlFilter
MemberTypeSummary
alias?string
controlType?ContentControlType
tag?string

ContentControlSummaryinterfaceSource ↗

A content control reduced to what a listing needs: its identity, kind, and lock state.

interface ContentControlSummary
MemberTypeSummary
alias?string
controlTypeContentControlType
idstring
locked?boolean
tag?string

DocAnchorinterfaceSource ↗

The LLM- and JSON-facing address for a piece of a document.

paraId is the 8-hex w14:paraId, matched case-insensitively. search is a phrase that must match EXACTLY ONCE inside that paragraph; ambiguous or missing matches fail with 'ambiguous' / 'notFound' rather than falling back to first-match.

Offset-based addressing was tried and abandoned: an agent cannot compute a character offset it has not seen, and offsets do not survive concurrent edits. Do not reintroduce { blockId, offset }.

interface DocAnchor
MemberTypeSummary
occurrence?numberOpt-in disambiguation. Omit to require uniqueness.
paraIdstring

DocAnchorRangeinterfaceSource ↗

A position in one story: a paragraph and a UTF-16 offset inside it.

The same offset space the ops and the caret use, so an anchor read here can be handed straight back to a selection without re-deriving anything.

interface DocAnchorRange
MemberTypeSummary
endOffsetnumber
endParagraphIdstringMay sit in a later paragraph: the range markers are independent elements.
partstringCanonical part name of the story the range lives in, e.g. `/word/document.xml`.
startOffsetnumber
startParagraphIdstring

DocCommentinterfaceSource ↗

One comment or reply, as comments.xml records it.

A reply is a comment with a parentId; OOXML gives replies no separate element, so the thread is reconstructed from that link rather than from nesting.

interface DocComment
MemberTypeSummary
anchor?DocAnchorRangeWhere the comment is anchored, absent when the file gave it no usable range.
authorstring
date?stringOPTIONAL, because `CT_Comment` makes `@w:date` optional and files omit it. A comment with no date is a comment, not a defect, and fabricating one is a content change.
idstring
orphaned?booleanTrue when the file gave this comment no usable range — a reference with no markers, or a start with no end. Reported rather than dropped: a reviewer's remark vanishing silently is worse than one that says it lost its text.
parentId?string
resolved?boolean
textstring

DocEditsinterfaceSource ↗

The document-executable edit vocabulary.

An interface rather than a closed union so extensions can widen it by declaration merging. A sealed union cannot be extended by a plugin, and the runtime dispatch is already registry-backed.

interface DocEdits
MemberTypeSummary
acceptAllRevisionsRecord<never, never>
acceptRevision{ id: number; part?: 'body' | 'footnote' | 'endnote'; noteId?: number; }
addComment{ target: DocTarget; text: string; author: string; }
addRepeatingSectionItem{ target: DocTarget; index?: number; }
adjustIndent{ target: DocTarget; direction: 'increase' | 'decrease'; }Word's Increase/Decrease Indent.
applyFormatting{ target: DocTarget; marks: RunFormatting; }
applyVariables{ values: Record<string, string>; }
deleteText{ target: DocTarget; }
insertBreak{ target: DocTarget; kind: 'page' | 'column' | 'line' | 'section'; }
insertImage{ target: DocTarget; data: Uint8Array; extent?: Extent; }
insertTable{ target: DocTarget; rows: number; cols: number; }
insertText{ target: DocTarget; text: string; }
mergeParagraphs{ target: DocTarget; }
proposeDeletion{ target: DocTarget; author: string; }
proposeInsertion{ target: DocTarget; text: string; author: string; }
proposeReplacement{ target: DocTarget; replaceWith: string; author: string; }Authored family. `author` is required: tracked-ness is verb identity, not a boolean flag, so there is no global trackChanges toggle to forget.
rejectAllRevisionsRecord<never, never>
rejectRevision{ id: number; part?: 'body' | 'footnote' | 'endnote'; noteId?: number; }
removeContentControl{ target: DocTarget; }
removeRepeatingSectionItem{ target: DocTarget; index: number; }
replaceText{ target: DocTarget; text: string; }
replyComment{ commentId: string; text: string; author: string; }
resolveComment{ commentId: string; }
setContentControlValue{ target: DocTarget; value: string; }
setParagraphStyle{ target: DocTarget; styleId: string; }
setVariable{ name: string; value: string; }
splitParagraph{ target: DocTarget; }
toggleList{ target: DocTarget; kind: 'bullet' | 'ordered'; }Word's Bullets and Numbering.

DocLocationinterfaceSource ↗

Structural addressing for content the paraId map cannot reach.

interface DocLocation
MemberTypeSummary
containerContainerRef
offset?number
pathnumber[]Block indices, descending into tables and content controls.

DocQueriesinterfaceSource ↗

The document-readable query vocabulary, keyed identically to [DocQueryResults](DocQueryResults).

An interface rather than a closed union for the same reason [DocEdits](DocEdits) is: an extension widens it by declaration merging, and the runtime dispatch is registry-backed.

interface DocQueries
MemberTypeSummary
comments{ resolved?: boolean; }
contentControls{ filter?: ContentControlFilter; }
findText{ text: string; container?: ContainerRef; }
paragraphs{ container?: ContainerRef; }
revisions{ part?: 'body' | 'footnote' | 'endnote'; }
stylesRecord<never, never>
variablesRecord<never, never>

DocQueryResultsinterfaceSource ↗

What each query returns. Keyed identically to DocQueries.

interface DocQueryResults
MemberTypeSummary
commentsreadonly DocComment[]
contentControlsreadonly ContentControlSummary[]
findTextreadonly DocRange[]
paragraphsreadonly ParagraphSummary[]
revisionsreadonly Revision[]
stylesStyleDefinitions
variablesReadonly<Record<string, string>>

DocRangeinterfaceSource ↗

A span between two positions. The endpoints may be addressed either way, independently.

interface DocRange
MemberTypeSummary
fromDocAnchor | DocLocation
toDocAnchor | DocLocation

DocumentBodyinterfaceSource ↗

The main story: its blocks in reading order, plus the sections derived from them.

interface DocumentBody
MemberTypeSummary
contentreadonly Block[]
sectionsreadonly Section[]Derived, not stored: recomputed on read from section-break markers and section inheritance. Never treat it as a spreadable field.

DocxDocumentinterfaceSource ↗

A parsed .docx.

NOT JSON-round-trippable: it holds Maps and Dates, plus an internal side-table of verbatim XML used for lossless round-tripping. Use toJSON / fromJSON before sending it over JSON-RPC or handing it to a model.

interface DocxDocument
MemberTypeSummary
bodyDocumentBody
commentsreadonly DocComment[]
revisionsreadonly Revision[]
stylesStyleDefinitions
theme?Theme

ExtentinterfaceSource ↗

A size in EMUs, the unit DrawingML stores extents in. 914400 EMU = 1 inch.

interface Extent
MemberTypeSummary
heightEmunumber
widthEmunumber

HeaderFooterSetinterfaceSource ↗

A section's three header (or footer) variants, each a relationship id.

All optional: a variant a section does not declare inherits the previous section's, and one absent everywhere means the document simply has none.

interface HeaderFooterSet
MemberTypeSummary
default?string
even?string
first?string

IndentFormattinginterfaceSource ↗

Indent at the selection, in twips.

Unlike every other field here, this does NOT go absent when the selection's paragraphs disagree. A ruler has to draw its handles somewhere, and Word draws them at the FIRST selected paragraph's values — Select All is the commonest indent gesture, and hiding the handles for it would be worse than showing one paragraph's truth. The values are therefore always the first touched paragraph's, and [mixed](mixed) records per field whether the rest agree.

firstLine is ONE SIGNED offset: negative is a hanging indent. OOXML spells it as two mutually exclusive attributes and this collapses them hanging-wins (§17.3.1.12), which is the model Word itself keeps.

Absent inside a table. The value would be correct there, but it is measured from the cell's content edge while a ruler is drawn against the page's margin, and the ruler does not know the cell.

interface IndentFormatting
MemberTypeSummary
firstLinenumberFirst-line offset from [left](left), signed. Negative is a hanging indent.
leftnumberLeft indent, signed. Negative pulls text into the margin, as Word allows.
mixed{ readonly left: boolean; readonly right: boolean; readonly firstLine: boolean; }Per field, whether the selection's paragraphs disagree about it.
rightnumberRight indent, signed.

ParagraphinterfaceSource ↗

One paragraph: its runs, the style it names, and its list membership.

interface Paragraph
MemberTypeSummary
kind'paragraph'
numbering?NumberingRef
paraId?string`w14:paraId`. The stable handle `DocAnchor` addresses.
runsreadonly Run[]
styleId?string

ParagraphSummaryinterfaceSource ↗

A paragraph reduced to what a listing needs: its stable handle, its text, and its style.

paraId is what a follow-up edit addresses, so a summary without one names a paragraph the file gave no w14:paraId and that a DocAnchor cannot reach.

interface ParagraphSummary
MemberTypeSummary
paraId?string
styleId?string
textstring

RevisioninterfaceSource ↗

One tracked change.

Addressing needs BOTH id and part: @w:id is unique only within a part, so an id alone names two revisions in any package that has a header or a comments part.

interface Revision
MemberTypeSummary
authorstring
date?stringOPTIONAL. `CT_TrackChange` requires `@w:id` and `@w:author` and makes `@w:date` optional, and producers that omit it are ordinary. Requiring it here forced either a fabricated date — a content change — or dropping the revision from the list.
idnumberNumeric, and unique only WITHIN a part. Pair with `part` to address one.
partstringREQUIRED, and a canonical PART NAME rather than a three-value enum.
typeRevisionType

RunFormattinginterfaceSource ↗

Character formatting, and — at selection level — the paragraph properties a toolbar reads alongside it.

One type serves both roles so a toolbar reads alignment, style and script state from the same object as bold and italic. On a [Run](Run) the selection-level fields stay absent: a run has no alignment or paragraph style of its own.

interface RunFormatting
MemberTypeSummary
alignment?'left' | 'center' | 'right' | 'both'Paragraph alignment at the selection. `both` is OOXML's spelling of justify.
bold?boolean
color?ColorValue
fontFamily?string
fontSizePt?number
highlight?string
indent?IndentFormattingThe EFFECTIVE paragraph indent at the selection — cascade and numbering merge included, so a numbered item that authors no `w:ind` reports the indent its list definition gives it. Absent when nothing is loaded, or when the selection is inside a table (see [IndentFormatting](IndentFormatting)).
italic?boolean
lineSpacing?{ readonly rule: 'multiple' | 'exact' | 'atLeast'; readonly value: number; }Line spacing at the selection, in the unit its rule implies — LINES for `multiple`, points for `exact` and `atLeast`. The same vocabulary `setLineSpacing` takes, so a control can show what it reads and send back what it shows. Absent when the selection's paragraphs disagree or state no line spacing.
spaceAfterPt?number
spaceBeforePt?numberSpace above and below the paragraph at the selection, in points.
strike?boolean
styleId?stringParagraph style id (`w:pStyle`) at the selection.
subscript?boolean
superscript?boolean
underline?boolean

SectioninterfaceSource ↗

One section: the page it lays out on, and the header/footer stories it declares.

interface Section
MemberTypeSummary
footersHeaderFooterSet
headersHeaderFooterSet
propertiesSectionProperties

SectionPropertiesinterfaceSource ↗

w:sectPr: the page a section lays out on. Twips throughout, as the file stores them.

interface SectionProperties
MemberTypeSummary
columns?{ count: number; gapTwips: number; }
marginsPageMargins
pageSize{ widthTwips: number; heightTwips: number; }
titlePage?boolean`w:titlePg` — whether the section's first page takes the `first` header/footer variant.

StyleDefinitioninterfaceSource ↗

One style: the ID content references, the name a reader sees, and its inheritance link.

interface StyleDefinition
MemberTypeSummary
basedOn?string`w:basedOn` — the style this one inherits from. Absent at the root of a chain.
idstring
namestring

StyleDefinitionsinterfaceSource ↗

styles.xml, split by the three style families that address separately.

Keyed by style ID rather than by the name a reader sees, because the ID is what a paragraph or run actually references.

interface StyleDefinitions
MemberTypeSummary
characterReadonlyMap<string, StyleDefinition>
paragraphReadonlyMap<string, StyleDefinition>
tableReadonlyMap<string, StyleDefinition>

TableinterfaceSource ↗

A table: its rows, and the table style they resolve through.

interface Table
MemberTypeSummary
kind'table'
rowsreadonly TableRow[]
styleId?string

ThemeinterfaceSource ↗

theme1.xml — what a [ColorValue](ColorValue) of kind theme resolves against.

interface Theme
MemberTypeSummary
colorSchemeThemeColorScheme
fontScheme?Record<string, string>

Type aliases (11)

BlocktypeSource ↗

Anything that can sit at block level in a story. Discriminate on kind.

type Block = Paragraph | Table | ContentControl;

ColorValuetypeSource ↗

A colour as the FILE expresses it, not as a resolved RGB string.

A theme colour stays a theme reference — slot plus tint or shade — so that changing the theme repaints the document the way Word does. Flattening to hex at read time would freeze the resolved value and break that link. auto is Word's "let the renderer decide", usually black on white.

type ColorValue = {
    readonly kind: 'hex';
    readonly value: string;
} | {
    readonly kind: 'theme';
    readonly slot: string;
    readonly tint?: number;
    readonly shade?: number;
} | {
    readonly kind: 'auto';
};

ContainerReftypeSource ↗

Which STORY a location belongs to.

The body is one container; every header, footer and note is another. A path alone is ambiguous without it, because block index 0 exists in every story a document has.

type ContainerRef = {
    part: 'body';
} | {
    part: 'header' | 'footer';
    rId: string;
} | {
    part: 'footnote' | 'endnote';
    noteId: number;
};

ContentControlTypetypeSource ↗

Which kind of control a w:sdt is, and therefore what a value written into it must be.

type ContentControlType = 'richText' | 'plainText' | 'checkbox' | 'dropdown' | 'comboBox' | 'date' | 'picture' | 'repeatingSection';

DocEdittypeSource ↗

One edit, as a discriminated union derived from [DocEdits](DocEdits).

Adding a key to DocEdits — including by declaration merging from a plugin — widens this automatically, so the union never drifts from the vocabulary it is built out of.

type DocEdit = {
    [K in keyof DocEdits]: {
        type: K;
    } & DocEdits[K];
}[keyof DocEdits];

DocQuerytypeSource ↗

One query, as a discriminated union derived from [DocQueries](DocQueries).

type DocQuery = {
    [K in keyof DocQueries]: {
        type: K;
    } & DocQueries[K];
}[keyof DocQueries];

DocTargettypeSource ↗

Anything an operation can be pointed at: a paragraph-relative [DocAnchor](DocAnchor), a structural [DocLocation](DocLocation), or a [DocRange](DocRange) spanning two of them.

type DocTarget = DocAnchor | DocLocation | DocRange;

ExecErrorCodetypeSource ↗

Why a write was refused, as a value to branch on.

Deliberately finer-grained than a boolean: "no-op", "target not found" and "content control is locked" are different outcomes, and a caller retrying the first should not retry the third.

type ExecErrorCode = 'notFound' | 'ambiguous' | 'locked' | 'bound' | 'typeMismatch' | 'kindMismatch' | 'outOfBounds' | 'unsupported' | 'invalidArgs';

ExecResulttypeSource ↗

Every write returns this rather than boolean.

A bare boolean cannot distinguish "no-op" from "target not found" from "content control is locked", and the editor layer already throws eight distinct ContentControl error classes that a boolean would flatten.

type ExecResult = {
    ok: true;
    changed: boolean;
} | {
    ok: false;
    code: ExecErrorCode;
    reason: string;
    target?: DocTarget;
};

RevisionTypetypeSource ↗

What kind of decision a revision represents.

Wider than insert/delete/format, and it has to be. w:moveFrom/w:moveTo are not a deletion and an insertion — resolving one half alone duplicates or loses the content; w:pPr/w:rPr/w:ins|w:del decorates no characters at all and merges paragraphs when resolved; a row or cell revision is structural. A reviewer shown only three kinds is a reviewer who never learns about the rest.

type RevisionType = 'insert' | 'delete'
/** A deletion and an insertion that are one edit: text typed over a selection. */
 | 'replace' | 'moveFrom' | 'moveTo'
/** `w:rPrChange` / `w:pPrChange` — the words are unchanged, their formatting is not. */
 | 'format'
/** `w:pPr/w:rPr/w:ins|w:del` — a paragraph split or merge. */
 | 'paragraphMark'
/** A row, cell, section or grid revision. */
 | 'structural';

ThemeColorSchemetypeSource ↗

Theme colour slots (accent1, dk1, lt2, …) to hex.

type ThemeColorScheme = Readonly<Record<string, string>>;

On this page