@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 (44)

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' | 'sectionContinuous'; }`'section'` is Word's Next Page break; `'sectionContinuous'` keeps the new section on the sheet the previous one ended, which is how a mid-page column or margin change is authored. `'column'` belongs to the multi-column lane and is refused for now.
insertContentControl{ target: DocTarget; subtype: InsertableContentControlType; tag?: string; title?: string; }Author a NEW control over the addressed span.
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

HistoryGroupOutcomeinterfaceSource ↗

The history authority's result for this frame.

interface HistoryGroupOutcome
MemberTypeSummary
kind'started' | 'extended' | 'split' | 'none'
reason?'history-boundary' | 'undo-redo' | 'no-history' /** @deprecated No longer reported: writes that span document parts now extend the group. */ | 'package-unit' | 'composition'

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.
leftnumberLeading indent (`w:left`), signed: the left one, or the right one in a right-to-left paragraph. 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.
rightnumberTrailing indent (`w:right`), signed: the right one, or the left one right to left.
rtl?trueThe first selected paragraph reads right to left, so `left` and the first line sit on the right margin. Absent for a left-to-right paragraph.

NumberingRefinterfaceSource ↗

A paragraph's list membership: which numbering.xml definition, and at which level.

interface NumberingRef
MemberTypeSummary
levelnumberZero-based. OOXML numbering has nine levels, 0 through 8.
numIdstring

PageMarginsinterfaceSource ↗

Page margins in twips.

interface PageMargins
MemberTypeSummary
bottomTwipsnumber
leftTwipsnumber
rightTwipsnumber
topTwipsnumber

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

ParagraphDisagreementsinterfaceSource ↗

Which paragraph-level reads the selection disagrees about.

interface ParagraphDisagreements
MemberTypeSummary
alignmentboolean
directionbooleanThe selection mixes left-to-right and right-to-left paragraphs.
lineSpacingboolean
spaceAfterPtboolean
spaceBeforePtboolean
tabStopsboolean

ParagraphFlagsinterfaceSource ↗

The five paragraph flags a Paragraph dialog shows as checkboxes.

contextualSpacing is Word's "Don't add space between paragraphs of the same style"; the rest are its Pagination block. null means the selection disagrees.

interface ParagraphFlags
MemberTypeSummary
contextualSpacingboolean | null
keepLinesboolean | null
keepNextboolean | null
pageBreakBeforeboolean | null
widowControlboolean | null

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

ParagraphTabStopinterfaceSource ↗

One custom tab stop, as a command states it.

interface ParagraphTabStop
MemberTypeSummary
alignment'left' | 'center' | 'right' | 'decimal' | 'bar'
leader?'none' | 'dot' | 'hyphen' | 'underscore' | 'heavy' | 'middleDot'
positionTwipsnumber

ReviewPositioninterfaceSource ↗

A position in the model offset space of one story.

interface ReviewPosition
MemberTypeSummary
offsetnumber
paragraphIdstring

ReviewRangeinterfaceSource ↗

Where an item is anchored: a range in one story.

interface ReviewRange
MemberTypeSummary
endReviewPosition
partNamestring
startReviewPosition

ReviewRevisionIteminterfaceSource ↗

One tracked change as a review card.

Keyed per DECISION rather than per site: a revision spanning three ranges is one card, because accepting it accepts all three.

interface ReviewRevisionItem
MemberTypeSummary
addressRevisionAddressThe payload `acceptRevision` / `rejectRevision` take.
addressesreadonly RevisionAddress[]EVERY address this decision covers, `address` first.
authorstring
date?string
formattingChanges?readonly { readonly property: 'bold' | 'italic' | 'underline' | 'strike' | 'fontFamily' | 'fontSize' | 'color' | 'highlight' | 'alignment' | 'leftIndent' | 'rightIndent' | 'firstLineIndent' | 'hangingIndent' | 'spaceBefore' | 'spaceAfter' | 'direction'; readonly value: string | null; }[]Changed direct formatting values, ready for localized review summaries.
formattingKind?stringOOXML property-change element, used to resolve independently numbered formatting kinds.
formattingLanguages?readonly string[]Newly applied proofing language codes in a tracked formatting change.
idstringStable across renders and unique per decision within this editor instance.
kind'revision'
markDirection?'insert' | 'delete' | 'moveFrom' | 'moveTo'WHICH decision a `paragraphMark` records, absent for every other kind.
nestingnumberHow deeply this change is NESTED inside other changes, 0 for an unenclosed one.
pairedWith?stringThe other half of a move, or the other side of a delete/insert replacement.
rangesreadonly ReviewRange[]Every site this decision touches, in document order.
readOnlybooleanTrue when the engine cannot resolve this kind, so accept and reject must not be offered.
replacedRangeCount?numberHow many leading `ranges` are the STRUCK half of a replacement.
replacedTextstringThe words a replacement removes. Empty for every other kind.
replyIdsreadonly string[]Comments answering this change, in document order.
revisionKindReviewRevisionKind
structuralChanges?readonly ('rowInsert' | 'rowDelete' | 'cellInsert' | 'cellDelete' | 'cellMerge' | 'numberingInsert')[]Distinct structural operations covered by this decision, in document order.
textstringText the revision covers, for the card summary. Empty for changes with no characters.

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

RevisionAddressinterfaceSource ↗

How a tracked change is addressed: @w:id, @w:author and @w:date together, within a part.

All of them, always. @w:id is unique only within a part, so an id alone names two revisions in any package with a header or a comments part — and it is not author-scoped either, so (part, id) would merge two authors' distinct revisions. One logical revision is also deliberately many elements sharing an id, as a tracked row insertion is, which no uniqueness rule could express.

interface RevisionAddress
MemberTypeSummary
authorstring
date?stringAbsent when the file wrote no `@w:date`; part of the identity either way.
idstring

RevisionAuthorEntryinterfaceSource ↗

One reattributed change. key and author describe the change after the update, because review keys include the author.

interface RevisionAuthorEntry extends RevisionBatchEntry
MemberTypeSummary
previousAuthorstringThe author the change had before the update.
previousKeystringThe key the caller selected the change by. It differs from `key` when the attribution changed, because review keys include the author and date.

RevisionAuthorResultinterfaceSource ↗

Outcome of one attribution change. Counts refer to review decisions, not XML markers.

interface RevisionAuthorResult
MemberTypeSummary
skippedreadonly { readonly key: string; readonly reason: RevisionAuthorSkipReason; readonly revision?: RevisionBatchEntry; }[]Requested changes that kept their attribution.
updatedreadonly RevisionAuthorEntry[]Changes that now carry the requested attribution, in selection order.

RevisionBatchEntryinterfaceSource ↗

A canonical review decision. Keys are valid only in the open document.

interface RevisionBatchEntry
MemberTypeSummary
authorstring
formattingKind?stringFor a formatting change, its OOXML property-change element, as the review item has it.
keystring
partNamestring
revisionKindReviewRevisionItem['revisionKind']

RevisionBatchResultinterfaceSource ↗

Outcome of one selected-set decision. Counts refer to review decisions, not XML markers.

interface RevisionBatchResult
MemberTypeSummary
remainingnumberAll pending decisions in the addressed scope, including excluded and unsupported changes.
resolvedreadonly RevisionBatchEntry[]
skippedreadonly { readonly key: string; readonly reason: RevisionBatchSkipReason; readonly revision?: RevisionBatchEntry; readonly detail?: RevisionBatchSkipDetail; }[]

RuninterfaceSource ↗

A stretch of text sharing one set of character properties.

interface Run
MemberTypeSummary
formatting?RunFormatting
revisionId?numberSet when the run carries a tracked change. Unique only within its part.
textstring

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, as a PHYSICAL edge. `both` is OOXML's spelling of justify. A right-to-left paragraph with no `w:jc` reads `right`.
bold?boolean
color?ColorValue
direction?'ltr' | 'rtl'Paragraph base direction at the selection (`w:bidi` through the style cascade). Absent when the selection's paragraphs disagree; `disagrees.direction` then says so.
disagrees?ParagraphDisagreementsWhich paragraph-level fields above are absent because the selection DISAGREES, as opposed to because nothing states them.
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. The effective value: a paragraph whose cascade states none reads as single spacing (`multiple`, 1). Absent when the selection's paragraphs disagree.
paragraphFlags?ParagraphFlagsThe paragraph flags the Paragraph dialog shows as checkboxes.
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
tabStops?readonly ParagraphTabStop[]The paragraph's custom tab stops at the selection, cascade included. Absent when the selection disagrees or the paragraphs have none.
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

TableCellinterfaceSource ↗

One cell. Its content is ordinary blocks, so a cell may hold paragraphs, nested tables and content controls alike.

interface TableCell
MemberTypeSummary
colSpan?number`w:gridSpan`. Absent means 1.
contentreadonly Block[]
rowSpan?numberVertical merge span. Absent means 1.

TableRowinterfaceSource ↗

One table row. Cell count may vary between rows: colSpan and vertical merges reshape it.

interface TableRow
MemberTypeSummary
cellsreadonly TableCell[]

ThemeinterfaceSource ↗

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

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

Type aliases (21)

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';
};

CompareDocumentsEngineErrorCodetypeSource ↗

Why the compare engine refused a compare, as a stable code: the codes that the engine, compareDocuments, and a background engine return. compareDocuments.errors.<code> in @docx-editor.dev/i18n translates every code.

type CompareDocumentsEngineErrorCode = 
/**
 * An option has a value of the wrong type, such as a `detectFormatChanges` that is not a
 * boolean, or the revised document is not a `Uint8Array`.
 */
'invalidOptions'
/** The original document is not a readable DOCX package. */
 | 'invalidOriginal'
/** The revised document is not a readable DOCX package. */
 | 'invalidRevised'
/** The documents exceed the compare's size limits. */
 | 'tooLarge'
/**
 * The original document enforces protection that the compare would break: read-only,
 * comments, forms, or tracked-changes protection. A compare also writes differences that
 * have no tracked form, such as style definitions and content control properties, so
 * tracked-changes protection refuses too.
 */
 | 'protected'
/** The differences include content that no tracked change can record. */
 | 'unsupportedContent';

CompareDocumentsErrorCodetypeSource ↗

Why a compare was refused, as a stable code. compareDocuments.errors.<code> in @docx-editor.dev/i18n translates every code. The engine and compareDocuments return [CompareDocumentsEngineErrorCode](CompareDocumentsEngineErrorCode) values. The editor command and dialog can also return the editor-only codes.

type CompareDocumentsErrorCode = CompareDocumentsEngineErrorCode
/** Editor only: no review module, no open document, or no mount. */
 | 'unavailable'
/** Editor only: a collaboration session is attached. */
 | 'collaboration'
/** Editor only: the document is open for viewing only. */
 | 'viewing'
/** Editor only: the dialog closed, or the compare was canceled, before it finished. */
 | 'closed'
/** Editor only: the revised file is a `Uint8Array` with no bytes. */
 | 'empty'
/**
 * Editor only: another compare of the same dialog runs, or a preview or an external document
 * refresh blocks writes to the document. Try again when it ends.
 */
 | 'busy'
/**
 * Editor only: the open document changed while a background compare ran, so its result
 * no longer applies. The editor never retries by itself.
 */
 | 'documentChanged'
/** Editor only: any other failure. */
 | 'failed';

CompareDocumentsOutcometypeSource ↗

The outcome of one compare.

type CompareDocumentsOutcome = {
    readonly ok: true;
    readonly revisionCount: number;
    readonly changed: boolean;
} | {
    readonly ok: false;
    readonly code: CompareDocumentsErrorCode;
    readonly reason?: CompareDocumentsRefusalReason;
    readonly message: string;
};

CompareDocumentsRefusalReasontypeSource ↗

What an unsupportedContent refusal is about, as a stable code. compareDocuments.reasons.<reason> in @docx-editor.dev/i18n translates every reason.

type CompareDocumentsRefusalReason = 
/** The documents differ inside a content control that is locked or bound to data. */
'lockedContent'
/** A document holds tracked changes that cannot be accepted, so its final state is unknown. */
 | 'unresolvedRevisions'
/** The documents differ in content with no tracked-change form, such as an embedded file. */
 | 'untrackedContent'
/** The revised document adds section or table settings that a reject cannot restore. */
 | 'unrecordedSettings'
/** A header or footer of an earlier section cannot be paired with one of the original. */
 | 'headerFooter'
/** The result needs a part type that the original document does not declare. */
 | 'packageStructure'
/**
 * The result fails the check: accepting all changes does not give the revised text, rejecting
 * them does not give the original text, or a tracked change cannot be accepted or rejected.
 */
 | 'unverified';

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' | 'buildingBlockGallery' | '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;
    revisions?: RevisionBatchResult;
    revisionAuthors?: RevisionAuthorResult;
    history?: HistoryGroupOutcome;
    compareDocuments?: CompareDocumentsOutcome;
} | {
    ok: false;
    code: ExecErrorCode;
    reason: string;
    target?: DocTarget;
    revisions?: RevisionBatchResult;
    compareDocuments?: CompareDocumentsOutcome;
    revisionAuthors?: RevisionAuthorResult;
};

InsertableContentControlTypetypeSource ↗

The control kinds an insertion can author.

A NARROWING of [ContentControlType](ContentControlType), not a separate vocabulary: a kind read off an existing control can be handed straight back to insertContentControl. Three kinds are absent because an insertion would have to invent what they carry — a checkbox needs its two declared glyph states, a picture needs an image part, a building block gallery needs a glossary to list, and a repeating section is a container whose items have their own verbs. Reading still answers the wider type; a document may hold kinds this cannot create.

dropDownList is the same kind as dropdown, spelt as OOXML spells it. Both are accepted, so a type read off a control can be handed back without translating it — and the OOXML spelling is the one the automation protocol takes, so a caller who uses both surfaces can write that one everywhere.

type InsertableContentControlType = Extract<ContentControlType, 'richText' | 'plainText' | 'dropdown' | 'comboBox' | 'date'> | 'dropDownList';

ReviewRevisionKindtypeSource ↗

What kind of decision a revision card represents.

Wider than the four content wrappers, because a reviewer has to be shown every pending decision, including the ones that decorate no characters. A card the surface cannot show is a change the reviewer never learns about — and acceptAllRevisions refuses if ANY revision in the document is one the engine cannot resolve, so an invisible one makes Accept All fail for a reason nothing on screen explains.

type ReviewRevisionKind = 'insert' | 'delete'
/**
 * A combined deletion and insertion decision. The built-in reader lists them separately,
 * unless a review query asks to pair replacements or a custom review provider combines them.
 */
 | '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. Supported row revisions are resolvable. */
 | 'structural';

RevisionAuthorSkipReasontypeSource ↗

Why a requested change keeps its attribution.

type RevisionAuthorSkipReason = 'unknown-revision' | 'unsupported-revision';

RevisionBatchSkipDetailtypeSource ↗

The structure behind a skip.

For unsupported-revision: a refused site, a formatting change whose earlier properties were not recorded so it cannot be rejected, a read-only review item, a change with no sites, a change a content control lock keeps (locked-content), or a change in a data-bound content control (bound-content). The last two have their own codes, review.locked-content and review.bound-content; reviewCodeOfSkip reads them.

For incomplete-group, the change the decision would also decide: the other half of a move or merge (paired-change), a change inside content the decision removes (removed-content) or inside properties a reject replaces (replaced-properties), a change the decision would empty (emptied-change), a change that encloses the selected one (enclosing-change), or another site of a group the planner keeps together (grouped-change).

For retained-structure: a row kept for the table nested in it (nested-table).

type RevisionBatchSkipDetail = RevisionSiteRefusal | 'unrecorded-properties' | 'read-only' | 'no-sites' | 'paired-change' | 'removed-content' | 'replaced-properties' | 'emptied-change' | 'enclosing-change' | 'grouped-change' | 'nested-table' | 'locked-content' | 'bound-content';

RevisionBatchSkipReasontypeSource ↗

Why a requested decision remains pending.

type RevisionBatchSkipReason = 'unsupported-revision' | 'incomplete-group' | 'retained-structure' | 'unknown-revision';

RevisionSiteRefusaltypeSource ↗

Why a revision site cannot be resolved: no w:id or author, a content marker where content cannot be, a wrapper whose content is itself marked, a structural revision outside tables and numbering, or a move whose ranges do not pair up.

type RevisionSiteRefusal = 'unaddressed' | 'misplaced-marker' | 'nested-marker' | 'unsupported-structure' | 'unsupported-move';

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>>;