# Unified Office DOCX reads

The `office` executable exposes four read-only DOCX commands over one bounded,
canonical document projection:

```sh
moonx bobzhang/office outline report.docx --json
moonx bobzhang/office get report.docx '/docx/body/p[1]' --json
moonx bobzhang/office text report.docx --under '/docx/header[1]' --json
moonx bobzhang/office query report.docx --kind paragraph --text revenue --ignore-case --json
```

Run `moonx bobzhang/office help docx` for the installed command catalog and
`moonx bobzhang/office help <command> --json` for the declared inputs, outputs,
and bounds.
The file's validated package format selects the result contract. This document
covers the DOCX branch; passing an XLSX package invokes the corresponding XLSX
read contract in [office-xlsx-read.md](office-xlsx-read.md). Passing an
`/xlsx/...` selector to a DOCX package fails with
`office.docx.selector_format_mismatch`.

## Output envelope

`--json` always emits one `office.output/1` document. A successful response
has `success: true` and a versioned object in `data`; bounded projection
diagnostics appear in the optional top-level `warnings` array. A failure has
`success: false`, a stable `error.code`, a bounded message, and optional
bounded details. Failures exit non-zero.

The four data schemas are:

| command | data schema | purpose |
| --- | --- | --- |
| `outline` | `office.docx.outline/1` | structural counts, story roots, headings, styles, images, sections, diagnostics |
| `get` | `office.docx.element/1` | one resolved story, annotation item, or addressable element |
| `text` | `office.docx.text/1` | path-tagged paragraph text with pagination |
| `query` | `office.docx.query/1` | deterministic matches for declared predicates |

Human output is intended for terminals. JSON is the automation contract.

## Canonical projection

Projection order is deterministic:

1. body;
2. headers in reader part order;
3. footers in reader part order;
4. footnotes;
5. endnotes; and
6. comments.

Each story is walked in document order. Addressable kinds are `p`, `r`, `tbl`,
`tr`, `tc`, `hyperlink`, and `image`; text nodes, tabs, breaks, bookmarks, and
reference markers are read through their nearest addressable parent. Nested
table content keeps its complete ancestry, for example:

```text
/docx/body/tbl[1]/tr[2]/tc[1]/p[1]
```

The body and annotation collection roots exist even when empty. Header and
footer roots exist only for discovered parts. See
[office-selectors.md](office-selectors.md) for the complete selector grammar.

Unique, non-empty note and comment ids that fit the selector bounds use stable
paths:

```text
/docx/footnotes/note[id="7"]
/docx/comments/comment[id="review/one]=\"ready\""]
```

A missing, duplicated, or unrepresentable annotation id falls back to a
snapshot-relative positional path and produces a warning. Selecting a
duplicated id is an explicit `office.docx.selector_ambiguous_id` failure.
Descendants such as `.../comment[id="7"]/p[1]` are snapshot-relative because
they contain a positional segment.

Anchor and reference paths inside annotation metadata are rewritten through
the same ordinal-to-emitted-root index. A unique note/comment therefore uses
its stable `id=` root everywhere, and every canonical-looking metadata path can
be passed back to `get` in the same snapshot.

Every projection entry also carries a `source` object. `story` is the logical
story name, `authority` is `relationship`, `legacy-fallback`, or `absent`, and
`part` is the exact physical ZIP entry whenever a part was read. Header/footer
sources also carry `part_index`; annotation items carry `ordinal`. Anchor
`story`, `start`, `end`, and `references` values are canonical `/docx/...`
selectors rather than reader-internal paths.

## `outline`

```text
office outline FILE [--max-elements N] [--max-output-chars N] [--json]
```

`office.docx.outline/1` contains:

- `file`, `format`, and `scanned_elements`;
- `counts` for body stories, headers, footers, note definitions, comments,
  paragraphs, runs, tables, rows, cells, hyperlinks, and images;
- `stories`, each with its canonical path, kind, direct-child count,
  stability, and source information;
- bounded heading previews and first-use-deduplicated styles;
- image metadata without embedded image bytes;
- the effective section header/footer references; and
- bounded reader diagnostics.

It is an orientation command: use `get`, `text`, or `query` for content.

## `get`

```text
office get FILE SELECTOR [--max-elements N] [--max-output-chars N] [--json]
```

`office.docx.element/1` echoes the canonical `path` and reports `kind`, `role`,
`stability`, `source`, optional `parent` and stable `id`, direct `children`,
kind-specific `properties`, annotation `metadata`, and bounded raw `text`.

Paragraph/run formatting, table spans, hyperlink targets, and image metadata
are represented as typed JSON properties. Comment metadata can include author,
initials, lexical date, resolved state, parent id, and canonicalized anchors.
Notes include canonical reference locations.

## `text`

```text
office text FILE [--under SELECTOR] [--offset N] [--limit N]
                 [--max-elements N] [--max-output-chars N] [--json]
```

Only paragraph elements are returned. Each entry has `path`, `stability`, and
raw `text`. `--under` restricts the scan to a resolved selector subtree.
`matched_total` is the exact paragraph count for the completed bounded scan;
`returned`, `offset`, `limit`, and `truncated` make pagination explicit.

## `query`

```text
office query FILE [--under SELECTOR] [--kind KIND] [--text TEXT] [--id ID]
                  [--property NAME=VALUE]... [--ignore-case]
                  [--offset N] [--limit N]
                  [--max-elements N] [--max-output-chars N] [--json]
```

All supplied predicates are ANDed. Matching is literal and deterministic;
regular expressions and arbitrary expressions are never evaluated.

Kinds are the story and element names above. The readable aliases are
`paragraph`, `run`, `table`, `row`, `cell`, `link`, and `picture`.

The declared property set is:

| property | applies to |
| --- | --- |
| `style_id`, `style_name` | paragraphs, runs, tables |
| `alignment` | paragraphs |
| `bold`, `italic`, `underline` | runs; values are `true` or `false` |
| `content_type` | images |
| `href` | hyperlinks |
| `author`, `done` | comments; `done` is `true` or `false` |

Hyphenated aliases (`style-id`, `style-name`, `content-type`) plus `align`,
`url`, and `resolved` normalize to those names. `--ignore-case` applies only
to `--text` and uses locale-independent Unicode simple lowercase mapping: one
Unicode scalar maps to one scalar, without locale tailoring or multi-character
expansion. Results carry a bounded text preview, declared properties,
canonical identity, exact `matched_total`, and explicit pagination metadata.
Exact text, id, and property values accept up to 1,048,576 Unicode scalar
characters, matching the reader's maximum XML token size. An annotation id
too long for `office.selector/1` therefore uses a positional projection path
but remains discoverable through an exact `--id` query.

## Limits

The implementation uses `moonbitlang/async` for bounded file I/O and has no C
stub. Default and hard user-facing limits are:

| resource | default | hard limit |
| --- | ---: | ---: |
| projection elements | 50,000 | 200,000 |
| successful command output characters | 1,048,576 | 4,194,304 |
| text rows per page | 2,000 | 10,000 |
| query matches per page | 100 | 1,000 |
| exact query value characters | — | 1,048,576 |
| property predicates | — | 16 |
| query predicate work units | — | 134,217,728 |

`--max-output-chars` covers every Unicode scalar written to stdout by a
successful DOCX read, including its single trailing line feed. A failure
envelope is emitted independently so a too-small success ceiling can still
report the stable error code and exhausted resource; failure envelopes are not
charged against this option.

The package reader additionally caps the input file at 64 MiB, ZIP entries at
4,096, one inflated entry at 32 MiB, total inflated bytes at 128 MiB, and uses
cumulative XML source, token, materialization, and token-size budgets during
both structural OPC preflight and document projection. Reader diagnostics are
deduplicated in first-seen order, capped at 128 retained entries, and bounded
to 512 characters each, including the final omission notice. Query text
scanning is capped cumulatively at 16 Mi characters, every element's
materialized text independently at 1 Mi characters, and all scope, exact-value,
folding, and guaranteed-linear substring work at 128 Mi conservative work
units. OPC preflight preserves a
typed parser-guard status before part names enter bounded human diagnostics, so
document-controlled text cannot forge or hide resource exhaustion. Limit
failures use `office.docx.resource_limit` and identify the exhausted resource.

## Correctable failures

Important stable codes include:

| code | meaning |
| --- | --- |
| `office.docx.selector_not_found` | canonical selector does not exist in this snapshot |
| `office.docx.selector_ambiguous_id` | annotation id appears more than once |
| `office.docx.unsupported_stable_id` | `id=` was used on a kind other than note/comment |
| `office.docx.selector_format_mismatch` | an XLSX-shaped selector was passed to a DOCX read |
| `office.selector.*` | selector syntax or shape is invalid |
| `office.docx.invalid_query_kind` | unknown `--kind` value |
| `office.docx.invalid_query_property` | unknown or ill-typed property predicate |
| `office.docx.resource_limit` | an explicit package, scan, text, or output ceiling was reached |
| `office.invalid_package` / `office.docx.read_failed` | the package or DOCX graph cannot be read safely |

After any mutation, rerun `outline` or `query` before reusing a positional
selector. Stable annotation ids can still disappear, but they do not silently
retarget another item.
