# Changelog

> Every stetdoc release and what changed in it, newest first.

Source: https://stetdoc.clauseo.chat/docs/changelog · stetdoc 0.9.1

All notable changes to stetdoc are recorded here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
[semantic versioning](https://semver.org/spec/v2.0.0.html) from 1.0.0. Releases before
1.0.0 may change the interface between minor versions.

## [0.9.1] - 2026-11-02

### Fixed

- A replacement no longer cuts a bracket pair in half. `thirty (30)` to `forty-five (45)` used to strike `thirty (30` and
  leave the `)` standing after the insertion; now the whole `thirty (30)` is struck and the whole `forty-five (45)`
  inserted, however wide the search text. An edit that leaves `(`, `[`, `{` or `“` open takes the unchanged closer right
  after it, and the mirror for a closer, unless the bracket is in another author's pending change. `ops` and `compare`
  both get it, and glued `%` or `'s` still stay outside the edit, as in Word. The compare noise baseline is re-taken
  for it: over the LAB pairs the redline has 0.05% more characters.

## [0.9.0] - 2026-11-02

The first release: the `stetdoc` CLI with its MCP server and the agent plugins, built for seven targets, from GitHub
Releases, npm, PyPI, Homebrew and crates.io (`cargo install stetdoc --locked`, which also publishes the engine as the
`stetdoc-core` library).

### Added

- `Receipt::text_within(cap)` composes a receipt's text in about `cap` bytes, as `text` is in about 11,000. The MCP
  tools that write (`edit`, `replace`, `comment`, `pack`) answer with a receipt of about 2 KB: the head line with its base,
  the first operations and blocks, what was left out, and the check line. `detail: true` returns the whole receipt.
- `stetdoc comment FILE ANCHOR TEXT [ANCHOR TEXT ...]` adds several comments in one edit, all or none, as `replace`
  takes several pairs. The MCP comment tool takes them in `comments`, a list of `{anchor, text}`, beside `anchor` and
  `text` for one.
- `stetdoc view --find TEXT --story STORY` finds only in that story, such as the headers and footers, where `--find`
  and `--story` used to be refused together. `Scope::Find` has `story`
  ([decision 0013](https://github.com/clauseo-ai/stetdoc/blob/main/docs/decisions/0013-mcp-for-fewer-turns.md)).
- The text of a receipt is capped at about 11,000 bytes, so `replace --all` or `comment --all` over hundreds of places
  prints a receipt an agent can read whole: the first operations and blocks, how many it left out, and the `stetdoc view`
  command that shows them. The head line, the base, the warnings and the check line stay whole, and the JSON is unchanged.
- `--clause` may be given more than once on `view`, `replace` and `comment`, such as
  `stetdoc view FILE --clause 4.01 --clause 4.02`: the result is the union of the clauses' blocks in document order, each
  once. `Scope::Clause` holds a list, and JSON takes one clause as a string or several as a list
  ([decision 0012](https://github.com/clauseo-ai/stetdoc/blob/main/docs/decisions/0012-clause-scopes.md)).
- A Python distribution of the CLI in `packaging/pypi/`: maturin builds the native `stetdoc` binary into a wheel per
  release target (`bindings = "bin"`), so `uv tool install stetdoc`, `pipx install stetdoc`, `pip install stetdoc` and
  `uvx stetdoc` run it with no Python at run time. The release workflow's `pypi` and `pypi-sdist` jobs attach the wheels
  and the sdist to the draft release, and `cargo xtask release check` and `stamp.mjs --check` keep them in step.
- The npm package `stetdoc` replaces its Node launcher with the platform's binary in a `postinstall` step, as esbuild does,
  so a call through `npm install -g stetdoc` starts in about 6 ms instead of about 100 ms. It copies a file npm already
  installed and downloads nothing. With `--ignore-scripts` and on Windows the launcher stays.
- The skill stub and `stetdoc skills get core` teach the default output (`NAME.edited.docx`, then `--in-place` on it),
  `--manual` markup and when to use it, reporting tracked changes by others, `--ignore-case`, `comment --all` and
  `--expect`, `view --revisions --context N`, table hits, and the MCP edit tool's typed `ops`.
- `stetdoc init --agent antigravity` installs the skill for Antigravity CLI (`agy`), which replaced Gemini CLI: in
  `~/.gemini/config/skills/stetdoc` (read by its CLI, app and IDE) or a project's `.agents/skills/stetdoc`. It is detected
  by `agy` on the PATH or `~/.gemini/antigravity-cli`, and `--mcp` prints `agy mcp add stetdoc stetdoc mcp`. Picking
  Gemini CLI now notes that Google retired it.
- The skill stub tells the agent to write each output next to its input in the user's folder, never in a temporary
  folder, and, when `stetdoc` is missing, not to offer another way into the .docx.
- `stetdoc view --revisions --context N` shows the blocks that hold the changes whole, as the redline shows them, with N
  blocks on each side, so each change reads in its paragraph. It takes `--author`, `--kind` and `--find` as the list
  does. `RevisionFilter` has `context`. `--context` without `--find` or `--revisions` is refused with a next step.
- The Node bindings have `Document.occurrences(text, scope?)`, typed with `Occurrences` and `Scope`.
- `Occurrence` has `heading`: the nearest heading when the paragraph is in no clause, which the CLI prints as
  `(under "RECITALS")`.
- Every receipt of `edit`, `replace`, `comment` and `compare`, and the head of every view but the ledger, says which
  tracked changes in the document authors other than you made, in one or two lines: how many by whom, the first three
  with their number, kind, ref and text, and "Tell the user about them." The JSON has them in `others`.
- `replace` and `comment` take `--ignore-case` (the MCP tools' `ignore_case`), and a `not_found` refusal lists the
  places where the text is in another case. `Document::occurrences_ignoring_case` finds the same way.
- `comment` takes `--all` and `--expect N`, one comment on each place, as `replace` does.

- Manual markup: `stetdoc replace --manual` and `stetdoc edit --manual` keep the old text, struck through, and add the new
  text underlined, as plain formatting with no tracked changes, for a reader who asks for a redline without Track
  Changes. A deletion is struck text only and an insertion underlined text only. `--manual-color` also makes deleted
  text red and new text blue. The MCP replace and edit tools take `manual` and `manual_color`, the core has
  `Edit::markup(Markup::Manual)`, and the receipt says the markup is manual and lists what each operation struck and
  underlined (`OpResult::marked`). `split`, `merge`, `row` and deleting a
  table are refused in a manual edit.
- The MCP `edit` tool takes typed operations in `ops`: a list of objects, each with `op` and that operation's fields,
  with the schema generated from the same table as `capabilities` and the operations reference. `batch` still takes text,
  and the file the CLI's `--ops` names is now `ops_file`. Every MCP tool's description carries the rules a model needs
  to call it right, for clients that never pass the server's instructions on, and no schema uses a union any more, so
  Gemini's function declarations take them as they are.
- A batch applied to a .docx may leave out its base, in the CLI and the MCP tool alike: the file is read and edited in one
  process, as `replace` does, so its refs are read against the file as it is now. A bare JSON list of operations works
  there too. A working copy still needs the base, and a batch without one is refused with where to find it.
- The eval harness keeps each run's report and transcript, counts only check findings that Word would reject as invalid,
  and its graders accept an edit made as a longer phrase and judge the stale case on any mention of the other author's change.
- An eval suite for the Claude Code plugin in `evals/`, run with `evals/run.sh` on Claude Code's `claude plugin eval`: nine
  cases on real contracts, graded from stetdoc's own reading of the documents the agent wrote, with turns, tokens, cost
  and time per case, and arms to compare with no plugin or another plugin. See `docs/evals.md`.
- `stetdoc replace FILE OLD NEW [OLD NEW ...]` changes text in one step, on a .docx (`-o OUT` or `--in-place`) or a
  working copy: each OLD, found once in the text as the final view reads it, becomes NEW as tracked changes, all pairs in
  one edit, all or nothing. An empty NEW deletes OLD. Text that is not there, or is there more than once, is refused
  with every place listed (ref, clause, text around it). `--all` replaces every occurrence and says how many, `--expect N`
  only when there are exactly N, `--range` and `--clause` limit the search, and `--dry-run` writes nothing.
- `stetdoc comment FILE ANCHOR TEXT` comments on text found the same way, in one step.
- The MCP server has `replace` (its pairs an array of `{old, new}`) and `comment` tools, with the JSON receipts of `edit`
  and the places found in `found`.
- `Document::occurrences` lists where a text is, as `find` matches it, in the whole document, a story, a range or a
  clause, and the new error code `count_mismatch` (exit 3) refuses a request whose expected count does not match.
- `stetdoc view --find` takes several texts (`--find payment --find "Due Date"`) and finds blocks that hold any of them. Each
  line says which texts it holds and how often, and the position line counts each text. The MCP view tool's `find` takes
  one string or a list, and `Scope::Find` has `needles` (JSON still reads `needle` and one string).
- The Claude Code plugin carries a guard hook, `plugins/claude/hooks/docx-guard.sh`, that blocks tool calls reading or
  writing a .docx another way: an archive tool, a script or a .docx library on a Word file, a conversion, an in-place edit
  of an unzipped part, and the Write and Edit tools on a Word file. It allows every `stetdoc` command, needs only `sh` and
  `awk`, and tells the agent to use stetdoc or say it is not installed. `docs/agents.md` lists what it blocks and shows
  how to add it to a skill install, and `stetdoc init --agent claude-code` points there.
- The skill stub carries the everyday commands (`view --find`, `replace`, `comment` and `compare`), so a simple task needs
  no `stetdoc skills get core`. In Claude Code it sends the agent straight to the command. `stetdoc init` replaces the
  previous stub as one it wrote. The core skill makes `replace` the default way to change text and `comment` the way to
  comment, with batches for the rest, and the README shows the same flow.
- The skill stub and the core skill tell the agent to report tracked changes it did not make, such as a colleague's edit
  made since its last view, with who made them and what they change. Its comment example writes a new file, so an
  agent copying it leaves the user's original untouched.
- `stetdoc view --find TEXT --range p40..p60` looks only in that range, in the CLI and the MCP view tool. A find pages like
  a range, and its position line counts the matches: `find "days": 44 matches in 38 blocks, showing 1..9`.
- `stetdoc view --revisions --find TEXT` lists only the changes whose inserted or deleted text contains it, and
  `--revisions --full` shows the whole text of each change (up to 1,000 characters) and not 80, still paged.
- `stetdoc view --all` is the same as `--budget 0`. A view of a body too long for one page shows its outline with an
  `outline_shown` warning that names the range of the whole body and `--all`, and the `-- more:` line of a page says
  where it stops, how much follows and the command for the next page.
- `stetdoc inspect FILE p42` lists the block's tracked changes (ids, kind, author, text) and comments, or says it has
  none, so text such as `[DELETED: ...]` in a document can be told from a real change.
- The MCP view tool's schema names, in each property's description, the arguments it cannot be used with, and a call that
  sends such a pair gets a next step that names both, in the MCP server and in the CLI's `--json` errors.
- The agent guidance picks the surface and keeps agents away from the package. The skill stub sends an agent with
  stetdoc's MCP tools to `skills_get`, and otherwise to the CLI, with a quick reference that covers a simple task. With
  neither, the agent tells the user stetdoc is not installed and reads nothing another way. The core skill says how paging
  works, that `[DELETED: ...]` without braces and an id is document text and not a revision, never to read the XML, to state
  or ask which reading of an ambiguous rule it takes before resolving revisions in bulk (judging each change from
  `--revisions --full` or `--find`), and to name the output and remove the working copy at the end with `pack --done`. It
  documents `--all`, `--find` with `--range`, `inspect`'s list of a block's revisions, `edit --in-place` and the one-line
  summary of problems already in the input. `stetdoc init --mcp`, `docs/agents.md` and the MCP server's instructions say to install the
  skill with the server. The README gives the one-line Claude Code install, and the Claude Code plugin has an icon.
- The CLI is published as the `stetdoc` crate, so `cargo install stetdoc` installs the `stetdoc` binary. The library is
  published as `stetdoc-core`.
- `stetdoc view FILE --revisions` lists the tracked changes as a ledger, one line each: the ids, the kind (`insert`,
  `delete`, `move` or `format`), the author, the day, the block and the text. `--author` and `--kind` filter it, and
  `--after change:N` continues a long list. It lists what `Document::revisions` returns. It was `unsupported` before.
- The agent skill says where the author of a revision comes from: `--author`, then `STETDOC_AUTHOR`, then the name saved
  by `stetdoc init`, so an agent no longer asks for a name the config already holds.

- Docs for users and builders: `docs/agents.md` (setup for each agent), `docs/self-hosting-mcp.md` (serving `stetdoc mcp`
  to cloud apps) and `docs/build-on-stetdoc.md` (the crate, the Node package, the CLI and MCP).
- Packaging for the releases: `packaging/install.sh` (checks the SHA-256 of the release archive), a Homebrew formula template,
  npm packages for the CLI (`packaging/npm/stetdoc`), plugin and listing manifests for Claude Code, Cursor, OpenAI and the MCP
  Registry, and a draft release that carries all of them. See `packaging/README.md`.

- White space at the ends of a `w:t` without `xml:space="preserve"` is text, as Word for the web shows it (hand check 37),
  where LibreOffice drops it, a documented deviation; `xml:space` is read as inherited from an ancestor, and what stetdoc
  writes or copies says it where needed; a `w16se:symEx` emoji shows its character (or its fallback's text); the text of a vertically merged
  continuation cell is flagged hidden; the `Final` view of the notes leaves out a note whose reference is a pending
  deletion, and the `Original` view one whose reference is a pending insertion, each giving a note the mark Word shows once
  its changes are resolved (`mark=`), while its id stays the same in every view; and a text edit never deletes a note's own
  number. Found by the LibreOffice oracle
  (`cargo xtask oracle libreoffice`), which reads a seeded sample of 500 outputs.

- `stetdoc pack` overwrites an existing output only when it is the working copy's original
  or a .docx it packed; anything else is the new error code `stale_target` (exit 4), unless
  `--force`. The default date of a revision honours `TZ`, with the time zone database built
  in as a fallback, and a zone that cannot be read gives UTC with a warning. An error from a
  readable file names its file and line once.
- The `stetdoc` command surface: all twelve commands with their flags, `--json` on every
  command, exit codes from the error table, and the stamp from `--author` or
  `STETDOC_AUTHOR` with the local clock as the default date. `view`, `inspect`, `lookup`,
  `check`, `edit -o`, `compare` and `capabilities` work on a `.docx`.
- The agent skill, embedded in the binary: `stetdoc skills get [core|operations|errors]`
  and `--full`. The operation and error tables are generated from `capabilities()` and the
  command list from the CLI's own definitions, so they cannot drift.
- `stetdoc init` picks agents: Claude Code, Codex, Cursor, OpenCode, OpenClaw, Gemini CLI,
  Copilot, Amp, Hermes, Grok Build, cmx2 and Factory. It installs into the home folder by
  default and into a project with `--project`, which also keeps a short block in `AGENTS.md`.
  It asks in a terminal, installs for the detected agents otherwise, and takes `--agent a,b`
  or `all`; a folder several agents share is written once. `--list` shows each agent's
  detection, folder and installed version, `--remove` deletes only folders that hold nothing
  but an unchanged stetdoc stub, `--mcp` prints how to register `stetdoc mcp` with each agent without
  editing its config, and `--dry-run` changes nothing. The stub records the version that
  wrote it. `stetdoc init --author NAME`, or the answer to its question in a terminal, saves
  the author to `stetdoc/config.toml` in the config folder, and `--list` shows it. `skills/stetdoc/SKILL.md` is the same stub, and a test fails when the two drift.
- The author of a revision comes from `--author`, then `STETDOC_AUTHOR`, then the name saved
  with `stetdoc init`; without any, the command says to run `stetdoc init`. It is never a
  name the user did not confirm.
- `stetdoc mcp`: a Model Context Protocol server on stdio, speaking revision 2026-07-28 and
  the earlier handshake revisions. Every command but `init` is a tool whose schema comes from
  the CLI's own definitions, a call returns the JSON the command prints with `--json`, and a
  failure is a tool error with its message and next step. `--root DIR` confines every path
  argument to `DIR`, refusing `..` and links that lead out of it. `--author NAME` stamps the
  calls that name no author.
- [`docs/reference/cli.md`](https://stetdoc.clauseo.chat/docs/reference/cli.md), generated from the CLI's own definitions
  by `cargo xtask docs`, with examples that run as tests.
- [`FIDELITY.md`](https://stetdoc.clauseo.chat/docs/fidelity.md): per feature, what is kept exactly, what is normalized, what
  is read-only and what is left out, with the design behind each settled detail. The README
  shows the working CLI.
- Working copies on disk: `unpack`, `pack`, `edit`, `check`, `view`, `inspect` and `lookup`
  on a directory. Every write follows the core's write plan with atomic renames under an
  advisory lock on `.stet/lock`, and a command that finds an interrupted write recovers it
  first.
- The workspace: `stetdoc-core`, `stetdoc-cli`, `stetdoc-napi`, `stetdoc-editor`,
  `stetdoc-testkit` and `xtask`, on a pinned toolchain.
- The `stetdoc-core` module tree, with a contract in each module.
- `ErrorCode`, `ExitClass` and `StetError`: the error vocabulary, with a generated
  [error reference](https://stetdoc.clauseo.chat/docs/reference/errors.md).
- The `internal` error code and exit class 7: a bug in stetdoc, which callers report and never
  retry around.
- `cargo xtask check` and `cargo xtask check --quick`, and CI that runs the same check.
- The entry-point docs, the licence files (`MIT OR Apache-2.0`) and `NOTICE`.
- `cargo xtask corpus fetch`: downloads the corpus manifest's documents from their pinned
  commits, verifies every SHA-256 and caches them, offline once cached. `cargo xtask check`
  runs it and sets `STETDOC_CORPUS_DIR` for the tests.
- `stetdoc_testkit::fixtures`: 85 edge, 7 Word and 87 hostile synthetic documents built in
  memory, each with the outcome stetdoc must give and, for a hostile one, why. They include
  the package layer's own 117 cases, ported without duplicates: 53 are new fixtures and the
  rest are the fixtures the testkit already had. A snapshot pins the SHA-256 of every fixture,
  so a change to the builder or to the deflate library shows up as a reviewed diff.
- `stetdoc_testkit::expect`: `Expect` and `Stage`, shared by the corpus and the fixtures. A
  refusal is an error code at a stage (`open`, `read`, `xml` or `document`), and a layer sees
  only the refusals it reaches. A refusal at `read` or `xml` also names the parts that carry
  it (`Entry::parts`, `Fixture::parts`), and `Expect::seen_by_reading` gives `Ok` to a layer
  that never reads them. Every refusal in `corpus/manifest/` has a stage, a part where that
  applies and a reason from `corpus/reasons.tsv`, and `cargo xtask check` enforces it.
- Fuzz targets for the container and the span tree, run with `cargo xtask fuzz <target>`: a
  libFuzzer target that opens, reads, replaces a part of and writes a package, and one that
  parses, serializes clean and with every tag dirty, and edits a part, each aborting when a law
  of the module breaks. They are seeded from the fixtures and the curated corpus, built with a
  nightly toolchain pinned for fuzzing alone, and run for 60 seconds per target in CI. The core
  reaches them through the off-by-default `fuzzing` feature ([decision
  0002](https://github.com/clauseo-ai/stetdoc/blob/main/docs/decisions/0002-fuzz-entry-points.md)), and `cargo xtask check` now runs clippy with
  the default features and with all of them, the tests and rustdoc with all features, and
  cargo-deny on the fuzz crate under its own policy (`fuzz/deny.toml`, the root policy and the
  NCSA licence of libFuzzer).
- `cargo xtask bench` and the benchmark harness, `stetdoc_testkit::bench`: the time and the peak
  memory of package open and save, span tree parse and serialize, schema validation, the state
  (open, a batch of fifty writes, save, export, resume), `wordml` (scan, field pass, text pass,
  refresh, fifty text edits, field edits and paragraph insertions) and the style cascade and clause
  numbers (the gate of 25 ms on a 20,000-paragraph body), on a 300-page contract and on four more
  inputs (the densest real document and three synthetic worst bodies), held to the budgets of `ARCHITECTURE.md` and to a baseline for each platform. The memory
  is counted by an allocator that the core's test binary installs, so it is the same on every run.
  The only exemption from a budget is a named pair of a benchmark and an input that is a known
  memory gap, which ends by itself when the pair fits (there is none today). A time regression against the baseline is a
  warning (`--strict-time` fails it), and the other failures are the budgets, a memory regression
  and a closed gap. `--ci` has the margins of a shared runner, `--out DIR` keeps the table and the
  rows, and a nightly workflow runs it with the budgets as written and uploads both. A benchmark is
  a small file in `stetdoc-core/src/bench` ([decision 0003](https://github.com/clauseo-ai/stetdoc/blob/main/docs/decisions/0003-benchmarks.md)).
- `projection` and `index` in the core: what an agent reads and edits. `view` gives a page of a document
  within a token budget (the estimate is ceil(ascii / 4) plus the other characters): the whole body when it
  fits, otherwise its outline, or a range, a clause, the results of a find or one story, with a cursor that is
  the ref of the last block shown, margin notes (what a find matched, what a citation such as `clause 9.1` came
  to, what cites a clause), and the notes and comments a page cites. `render_files` writes the readable files
  (`document.md`, `notes.md`, `headers.md`, `comments.md`) in the `stet/1` format or as `json/1`, and
  `files::to_batch` turns an edit of them into operations by comparing two drafts, in no syntax, so the
  alignment is written once: a changed slot is one `replace`, a line removed is a `delete`, a line with no ref
  is an `insert` after the paragraph it follows, and a paragraph cut into lines or lines joined are recognized
  as exact splits and merges only when the text is conserved. Everything else is applied as written or refused
  with the file, the line and the way out (`readonly_region`, `unsupported`, `cross_block`, `stale_files`),
  and the files of an unedited document give no operation. `render_blocks` shows the blocks of a receipt as the
  files hold them, with warnings (`looks_hidden`, `duplicate_number` and the rest). `lookup` finds where a
  term is defined and used, which block is `clause 4.2(a)` (computed or typed, composed across lists), and what
  refers to a block, and returns every candidate when it is not unique. Laws M1 to M7, F1 to F8, T1 to T8, P1 to
  P8 and I1 to I6 run over the fetched corpus in `cargo xtask check`, and the benchmarks time the
  first view, a page with notes, the readable files, `to_batch` and the first lookup on a 300-page contract. See
  the [design notes](https://github.com/clauseo-ai/stetdoc/blob/main/docs/design/projection-and-index.md).
- Every testkit fixture is opened with `Package::open` and held to the container laws and to its
  expectation by a test of the core, with the row to add for `State::open` when it merges. The
  laws now also check that the parts a refusal at `read` or `xml` does not name read and parse
  cleanly, which `Expect::seen_by_reading` relies on, and a part refused with another code than the
  index gives is named in the message, where it used to be reported as a clean one.
- The corpus cache is safe to share between checkouts and between concurrent runs: fetching
  holds a lock, work happens in a per-process directory, and a set is built once, under a name
  that carries the manifest hash, and never changed. A repair of a damaged blob builds a new
  generation of the set behind the stable link, so a reader of the old one sees no change.
  A repository that was deleted or made private fails its source alone, with a message that
  asks for a new pin.
- `cargo xtask codegen`, which generates Word's element order and value types from the Open
  XML SDK's schema data (pinned, MIT), and the `schema` module that uses them: where a new
  child element goes (`insert_in_order`, `reorder`) and the violation set behind the
  no-new-violations rule (`validate_part`). The schema code reads the span tree (`SpanTree`
  implements `SchemaTree`, and `Refs` lets code that holds `NodeRef`s call the rules), finds the
  context of an element (`ctx_at`), and is the one place that says which branch of an
  `mc:AlternateContent` Word reads and what `mc:ProcessContent` keeps (`taken_branch`,
  `disposition`).
- `Mode` (`Final`, `Original`, `Redline`) in `api::view`, re-exported from the crate root: how
  tracked changes appear when a paragraph's text is read. `Final` applies every revision,
  `Original` rejects every one, and `Redline` keeps both.
- `wordml` in the core: the stories of a document and the blocks in them, each paragraph's text in
  the three modes with a map from every character to the XML it came from, and the definitions of
  styles and numbering. The map is the base that edits, revisions and projections build on. See
  the [design notes](https://github.com/clauseo-ai/stetdoc/blob/main/docs/design/wordml.md).
- `package` and `xml` in the core: the ZIP container and the XML span tree. A package opens
  lazily, checks a part's size and CRC-32 when it is read, and writes back the input bytes
  exactly when nothing changed. Replacing one part rewrites that entry and moves the offsets
  after it, and an edit through the span tree changes only the bytes of the nodes it touched
  (the root start tag may gain the namespace declarations new content needs). A package is read
  as Python's `zipfile`, `unzip` and `bsdtar` read the same file, or it is refused: the central
  directory is found from the end record, entries lie end to end, and a ZIP64 record has to agree
  with the end record. `Limits` bounds the archive, the entries, the compression ratio and the
  XML (depth, attributes, nodes, namespace declarations in scope). The laws L1 and a one-edit L2
  run over the fetched corpus in `cargo xtask check`, against a second ZIP reader and against
  the stage and part the index gives for each refusal. See the
  [design notes](https://github.com/clauseo-ai/stetdoc/blob/main/docs/design/package-and-xml.md).
- The span tree stores its nodes and attributes in chunks that a clone shares until it changes
  one, so cloning a tree for copy-on-write staging copies a few pointers and an edit copies the
  chunks it changes, and a tree holds what it uses where a growing vector held up to twice that.
  The densest benchmark inputs went from 103% of the memory budget to 70% (writing a part while
  its tree is held), a clone of the densest input with 50 paragraphs changed from 207 MiB to
  97 MiB, and the four known memory gaps closed. `SpanTree` refuses `]]>` in text
  (`malformed_input`, reason `xml.cdata_end_in_text`; none of 51,831 documents has one), an element
  holds at most 4,096 attributes, and a text node that an edit makes meet another text node reports
  `is_dirty`, so a node that is not dirty is written exactly as it was read.
- `check` in the core: the findings and the report of a check (`FindingCode`, `Finding`,
  `CheckReport` in `api::check`, with stable codes and a sentence for each), `check::document`
  (every part: the schema, the Word rules for deletions, `xml:space`, nested revisions, unique ids,
  paired ranges, cells, tables, notes, empty revisions and properties, and fields cut by a
  deletion, and the comment audit) and `check::edit`, the check of a batch under the
  no-new-violations rule. `edit` finds the paragraphs, tables, notes and frames that a transaction
  changed from the span trees, with an equality test against the base so that a long session does
  not pay for its history, and compares each with its origin, so a problem the input had never
  blocks an edit, and an element the compiler renames keeps the key of its problems. `schema`
  gained `walk_unit`, `read_children`, `is_unit_boundary` and `in_taken_branch`, so the Word rules
  read a unit as the validator does. See the [check design](https://github.com/clauseo-ai/stetdoc/blob/main/docs/design/check.md).
- Under a scope, `accept` and `reject` never take a change of the body's last section properties,
  which name no paragraph, and the scope guard's second pass refuses any change to what lies between
  blocks or after the last one unless the block after it is in scope. Typed quotes read the character
  before the text, so `'s` typed after a word becomes an apostrophe. The `document_*` benchmarks
  time an edit, its dry run, an edit of the files, the first view and `export` through `Document`.
- The comment operations through `Document::edit`: `comment`, `reply`, `resolve`, `edit-comment` and
  `delete-comment`, and `delete-note` (a note named as the view shows it, `fn3`). The receipt lists what each
  did to which comment, and `reconcile` runs after the operations. Under a scope a comment operation is checked
  by the paragraphs it is anchored to.
- The operations on properties through `Document::edit`: `set` (any property of a paragraph, run, mark,
  section, table, row or cell), and `format`, `paragraph`, `style` and `list` over it, with values in
  human units (`12pt`, `0.5in`, `1.5cm`). Each is a tracked property change. None of them writes or removes
  revision markup, and a section break through `set` is untracked only.
- The `Comments` scope of `view`: one entry per comment thread, with the root's anchor, every comment's
  author, date and text and whether the thread is resolved, filtered by author and by resolved, and paged by
  the root of the last thread shown (`c7`). The comments story says which comment a reply answers and that its
  thread is resolved, and the summary line counts resolved threads.
- A JSON view gives each paragraph its `final_text`, the text that the offsets of an operation count in.
- A relative cross-reference lists the candidates it was chosen over after its target, in a margin note
  (`relative to this block, or else p3 (...)`) and in a lookup.
- `compare` through `Document`: an original and a revised document into a redline whose transaction
  `check` verifies before it commits, with the original's content, style and numbering definitions and
  copied parts declared as foreign origins, so a problem they already had is carried and not refused. The
  receipt has the revisions by author, the moves as refs, and compare's warnings, with a warning
  naming each block it could not show as changes (`compare_not_redlined`) or that rejecting the
  changes does not give back (`compare_not_restored`), by a block of the redline or, when there is
  none, by the original's first words, and one naming each copy whose paragraphs and tables did not
  pair with the original's and so may keep any of their problems (`compare_keys_merged`).
  `stetdoc compare` now writes the redline.
- `laws::compare` in the testkit: N5 and N7 through `Document`. A redline must pass `check` when
  `compare` writes it, and once saved it has no violation that neither input has after its revisions
  are accepted. It runs over the contract, over the compare pairs of the corpus (`corpus/pairs.tsv`,
  the 241 curated ones in `cargo xtask check`), and over forty curated documents against an edited
  form of themselves, and it counts the `compare_keys_merged` warnings.
- The benchmarks `document_open`, `document_save` (after a fifty-operation edit), `document_resume`
  (the working copy of an edited document) and `document_files` (the readable files alone).
- The fuzz targets `batch` (a batch parsed as JSON or block text, written as JSON and parsed back, then
  applied to a small document tracked, untracked or as a dry run) and `files` (an edit of
  `document.md`), with their dictionaries and seeds. Both run in CI's fuzz job.
- The TypeScript library over Node-API (`crates/stetdoc-napi`, package `@clauseo/stetdoc` in its `npm/` directory): `Document` with
  `open`, `resume`, `view`, `edit`, `save`, `inspect`, `revisions`, `lookup`, `check`, `checkPending`, `files` and
  `export`, and `compare`, `capabilities` and `jsonSchema`. Calls are synchronous, data crosses as the core's JSON, offsets
  count UTF-16 code units unless an edit asks for `"scalar"`, and every failure is a thrown `StetdocError` with its `code`,
  `next`, and the `opIndex`, `file` and `line` the core set. A smoke test runs the same file under Node and Bun.
- `stetdoc-editor`: the backend of Claude's text editor tool (`text_editor_20250728`) over the readable files. `view` shows a
  file with line numbers or lists the files, `str_replace` needs a unique match (otherwise `ambiguous_match`, listing each
  match with context, or `not_found`), `str_replace` and `insert` commit at once as one tracked edit and show the touched
  blocks where they now are, and `create` and `undo_edit` are `unsupported`. The library offers it as
  `Document.textEditor(input, { author, date, pathPrefix })`, which takes the input exactly as the model sends it and returns
  the `tool_result` text, its `isError` flag, whether it committed and the receipt.
- The library's data types in `npm/types.d.ts` are generated from the core's JSON Schema by `cargo xtask codegen`, and a test
  fails when they are stale. The library exports `version` and `formatVersion`, and loads in Node and Bun worker threads.
- Packaging and release: one npm package per target (`@clauseo/stetdoc-<target>`, seven of them) as exact optional dependencies of
  `@clauseo/stetdoc`, with no install script; `.github/workflows/release.yml`, which builds the CLI and the addon for the seven
  targets, smoke-tests them on Bun 1.3.13, Bun 1.3.14 and Node 24, and attaches the archives, their SHA-256 files and the
  npm tarballs to a draft GitHub release without publishing anything; a `napi` job in CI; and `cargo xtask release check`,
  which replaces the placeholder `cargo xtask release` and keeps the packages and the workflow in step with the workspace.
- The library's documentation, `crates/stetdoc-napi/npm/README.md`, shipped in the package, and its sections in `README.md`
  and `CONTRIBUTING.md`.
- `stetdoc edit FILE.docx --in-place` (and `in_place` on the MCP edit tool) writes the result back to the .docx it read,
  atomically: a temporary file beside it, readable by its owner only until it gets the file's permissions, an fsync and a
  rename (and a sync of the folder on Unix). A link is written through, a hard link is broken, the owner is not kept, and
  a read-only file is refused. If someone wrote the file during the edit, the write is refused with `stale_target` and the
  file is untouched. The meaning of `stale_target` now covers this case; its code and exit class are unchanged. It is refused with `--dry-run`, with
  `-o`, and on a working copy, which an edit always writes. `-o` naming the input is still refused, and its `next` now
  names `--in-place`.
- `stetdoc pack DIR --done` (and `done` on the MCP pack tool) removes the working copy once the .docx is written and
  validated. It removes only what stetdoc writes (the manifest first), so files it did not write, in the folder or in
  `.stet/`, stay with their folders and are named. A working copy named through a link has the folder it links to
  removed, then the link. When the pack fails, the working copy stays.
- `stetdoc check FILE --all` lists every finding the report keeps (the first 200), not only the first 20.

### Changed

- The MCP tools' list is 11 tools and about 4,100 tokens (16,419 characters), from 12 and about 7,200: each description
  says the few words a model needs, steers to one `replace`, `comment` or `edit` call on the .docx with no `unpack`,
  `pack`, `check` or dry run, and leaves the detail to `skills_get` and the refusals. `capabilities` is no longer an MCP
  tool (`skills_get` teaches the operations and the error codes; the CLI keeps it), and the edit tool leaves out
  `offsets` ([decision 0013](https://github.com/clauseo-ai/stetdoc/blob/main/docs/decisions/0013-mcp-for-fewer-turns.md)).
- `stetdoc mcp` answers a call with the text the CLI prints, which is what the skill teaches and a tenth of the size of
  the JSON for a typical receipt; `structuredContent` still carries the JSON whole. Next steps and refusals name the
  tool's properties (`all: true`, `range`, `output`), not the CLI's flags.
- `stetdoc mcp` takes `"true"` and `"false"` as text for a boolean argument and for a typed operation's boolean field, and a
  number as text for a typed operation's number field, since some clients (Gemini's among them) send them so.
- `stetdoc mcp` finds a working copy or a document passed by a relative name that is not there as given in the folders of
  the documents the session has named or written, most recent first, so `contract.stet` works after `unpack` wrote it
  beside a document in another folder. A path to write is never moved, and `--root` still confines every path.
- Every receipt's check line reads as the completed validation it is: `validated 3 units: no problems, and this file
  needs no separate check.` The core skill no longer carries the table of refusal codes (`skills get errors` has it), and
  `check` is described, in the MCP tool and the skill, as for files stetdoc did not write and for hand edits.
- An `ambiguous_match` refusal of `replace` or `comment` says how to retry in one call: `--all` for every place, or
  `--range` with the ref of one place, such as `--range p12..p12`, and that no dry run is needed.
- The npm packages move under the Clauseo organisation: the Node library is `@clauseo/stetdoc` with platform packages
  `@clauseo/stetdoc-<target>`, and the CLI's platform packages are `@clauseo/stetdoc-cli-<target>`. The CLI itself stays
  `stetdoc`, and its release tarball is `stetdoc-<version>.tgz`. The Homebrew tap is `clauseo-ai/homebrew-tap`, so the
  install line is `brew install clauseo-ai/tap/stetdoc`.
- Publishing a release on GitHub uploads its wheels and sdist to PyPI by trusted publishing (the `publish-pypi` job of
  `release.yml`, in the `pypi` environment). The tag build still only makes the draft.
- `packaging/README.md` and the npm package's README say how Bun users get the fast install: `bun add -g --trust stetdoc`
  or `bun pm -g trust stetdoc`, and `bunx --bun stetdoc` for one-off runs.
- The skill says to use `--manual` when the user, or a document or note they point to (such as a client email), asks for
  manual markup or no Track Changes, and never `view --all` on a long document: page with `-- more:` or narrow with `--find`
  or `--range`. `skills get core` is shorter, so every part of the skill fits in one tool output (under 25 KB), and the stub
  stays near 450 tokens. The previous stub is `skill/earlier/stub-8.md`.
- The released binaries, the npm addon and the wheels are built with a new `dist` profile (fat LTO, one codegen unit,
  stripped), and the CLI allocates through mimalloc: the macOS binary is 7.4 MB instead of 9.9 MB, the engine benchmarks run
  about 8% faster, and CLI calls about 9% faster. `cargo build --release` is unchanged.
- `docs/agents.md` and `packaging/README.md` recommend a binary on the PATH (Homebrew, the install script, `uv tool install`)
  for agents that call `stetdoc` many times, and `npx stetdoc mcp` for the MCP server.
- A find hit or an occurrence names a clause only when its paragraph is that clause or its own text. A recital after the
  parties "(1)" and "(2)" names no clause and sits under its heading (`{in p15 "RECITALS"}`), and a paragraph after a
  list item such as (g) names the clause the list is in (`{§14.1}`, not `{§14.1(g)}`). A bare label keeps its noun:
  `{§Schedule 1}`.
- A find hit in a table cell shows the other cells of its row (`{row: p36 "Greater than 4.25 to 1.00", p38 "2.25%"}`),
  and a compact find pages by hit line, so a table with many matching cells no longer comes on one page.
- `edit`, `replace` and `comment` on a .docx with neither `-o` nor `--in-place` write `NAME.edited.docx` beside it,
  the name `pack` uses, or `NAME.edited-2.docx` and so on when that exists, and say so. Before, they refused.
- An empty `w:rPr` or `w:pPr` (`empty_properties`) is a note, not a violation: the schema allows it. `check` and
  `check --strict` pass a document that has them, and a receipt no longer counts them as problems.

- The MCP `edit` and `replace` tools' descriptions say to call `skills_get` first for the batch syntax and the rules.
- `stetdoc edit --dry-run` on a .docx with `-o` or `--in-place` writes nothing, and says so on its first line, instead of
  refusing the flag. A dry run needs neither flag, as `replace` and `comment` already allowed.
- `stetdoc view --find` lists one line for each paragraph that matches: its ref, its clause number, and about 100
  characters of its exact text around the match, under a line for each top-level clause, with a tip that names the
  clause to view whole when a clause heading matches. `--context N` (N of 1 or more) shows the blocks whole, as before.
  On a 75,000-character contract, `--find days` went from 11.5 KB for the first 9 of 38 hits to 5.3 KB for all 38.
- The Claude Code plugin no longer registers the MCP server. In Claude Code the skill and the `stetdoc` command are the
  route: Claude Code keeps MCP tools behind its tool search, so with both an agent switched between surfaces, and the
  server is the same binary, so it was no fallback. The Cursor and Codex plugins keep it.

- The receipt of a command that writes a .docx (`edit -o`, `edit --in-place`, `compare`) carries the base of the written
  file, which opens in a new ref namespace, so the next batch on it can start from the receipt. When the edit added or
  removed blocks, the written file numbers them again: the receipt then has no base (`null` in JSON) and a `next:` line
  that says to view the file. Before, the receipt's base was refused as `stale_ref` on the written file.
- The `next` of a `stale_ref` for a batch from another namespace says that a file rewritten since the view or receipt has
  new refs, and to view it again.
- `compare` ends its receipt with `next: stetdoc view OUT --revisions`, which lists the redline's changes without unpacking.
- A batch's `ids` reads `[57, 58]` and `#57, #58` as well as `57, 58`, and a value that is not a number is refused with a
  message that says `ids` is a comma-separated list of whole numbers.
- The agent skill says to view a .docx directly rather than unpack it to read, to use `--in-place` only on a file the user
  asked to update or that the task wrote, and how to take the next base after writing a .docx.

- The default page of a view is 3,000 tokens (about 12 KB), not 8,000, in the CLI, the MCP view tool and
  `Budget::default()`, so that one view, or two or three in one call, stays under what an agent's tool result holds.
- A receipt of `edit`, `pack` or `compare` counts the problems that were already in the input on one line, such as
  "66 problems were already in the input, and no new one was added: `stetdoc check --all` lists them.", instead of listing
  ten of them. The JSON still has the first ten in `validation.carried`. A problem the edit would add still refuses it
  with `validation_failed`, naming the problems.
- The pack receipt says that the working copy stays until the folder is deleted, and that `pack --done` removes it. The
  JSON has `working_copy_removed`.
- The README said that any batch written against an older state is refused. What is refused is a batch from another
  document or history, or one that names a block changed since its base; a batch from an older state of the same history
  that touches only unchanged blocks applies, as the design says (`docs/design/refs-and-state.md`).

- The first view of a large document, and the first range with its margin notes, take about half the time they did: the
  text model is built only for the paragraphs that hold a note reference when the notes are numbered, a UTF-8 part's names and
  text are not checked again on every read, a revision is looked for by its name before its namespace, and mentions of
  clauses are found in one pass over the words. Views, receipts and peak memory are unchanged.

### Fixed

- `stetdoc check` lists `mc_prefix_undeclared` first, on a .docx, a working copy and the MCP check tool, so a prefix
  that `mc:Ignorable` names and nothing declares is the first thing the check says about the input. It used to come after
  the part's schema findings, past the 20 that the text lists without `--all`, and past the 200 that a report keeps when
  the part had more. Its message names the part and the prefixes, says that Word refuses the file or offers to repair
  it, and says which prefixes stetdoc declares and which it removes when it writes the document
  ([decision 0014](https://github.com/clauseo-ai/stetdoc/blob/main/docs/decisions/0014-repair-undeclared-mc-prefixes.md)).

- An input whose `mc:Ignorable` names prefixes it never declares, as python-docx leaves some templates' main part, no
  longer gives an output that Word refuses to open ("Word found unreadable content"). Every edit, compare, accept and
  reject now declares the prefixes Word knows (`w14`, `wp14`, `w15`, the `w16` family and the rest Word writes) on the
  element that names them and drops the ones nobody knows, in every part the check reads, touched or not, and changes
  nothing else. The receipt says what it repaired in a new warning, `repaired`, and `stetdoc check` reports the defect on
  any document as `mc_prefix_undeclared`, a problem already in the input
  ([decision 0014](https://github.com/clauseo-ai/stetdoc/blob/main/docs/decisions/0014-repair-undeclared-mc-prefixes.md)). The Harvey indenture of the long markup task
  opened in Word only after this fix.
- A run that an edit creates or cuts off (new text, the rest of a split run, a comment's anchor, a new row, compare's
  runs) gets its run properties in the order Word writes, even when the run it copies from has them out of order. Before,
  each copy repeated the defect, and the Open XML SDK validator counted every copy as a new schema error (33 new errors
  in one counter-redline of a credit support annex). The runs the document had keep their bytes, including the part of a
  split run that stays where it was. `check` now counts a property out of the order Word writes
  (`schema_non_canonical_order`) as a problem the edit added when a block has more of them than before; the ones the
  input had are still reported as already there.
- `stetdoc edit FILE.docx --dry-run` works with neither `-o` nor `--in-place`, so the two refusals no longer send an agent
  round in a loop. The MCP `edit` tool's `dry_run` takes the same path.
- A missing input path is `not_found` with the message `view` gives, for every command that takes one (`replace`,
  `comment`, `edit`, `pack`, `check`, `inspect`, `lookup`, `view`, `unpack` and `compare`). Before, `replace` and `comment`
  said the file was a .docx that needed `-o`, and `pack` said a folder was not a working copy.

- The random law L2 allows a table next to a removed block to gain, in its first or last paragraph, the comment markers
  that the removal hoists there (design `revisions.md`, 3.6.5). It failed on `floattable-anchorpos.docx`, where deleting a
  table moved its comment into the first cell of the table after it.

- Rejecting the inserted paragraphs of a footnote, an endnote or a comment whose reference stays keeps the note's last
  paragraph, emptied, instead of leaving a note with no paragraph, which the check refused as `validation_failed`
  (`Revision footnotes.docx`). When the reference is rejected too, the note goes with it, as before.
- The random law L2 reads `<w:p a="1" />` opened as `<w:p a="1" >` to hold the comment markers that a deletion hoists into
  it as the same start tag. It took the space before the `>` for a changed paragraph and failed on
  `tdf112450_vml_polyline.docx`.
- Resolving a move removes the range markers of both its sides, as the design says (resolution is by move), also when an
  earlier rejection had emptied one side. The emptied side's markers stayed under the move's name, and `check` refused the
  edit for a move with a source and no destination (`Tika-792.docx`).

- Rejecting a paragraph inserted after an empty last paragraph no longer fails with `validation_failed` when the original
  paragraph had a problem in its attributes, such as a Word 2007 beta `paraId`: the paragraph that stays takes the original's
  attributes back, and the reject now declares that it came from the original, so `check` carries the problem.

- An operation's `added` in a receipt lists only refs the edit minted. When a block a split made was then joined into an
  older block by an `accept` or `reject` of the same batch, the joined block keeps the older ref, and the split listed that
  ref as added, in a dry run too, which must issue none (H14).
- An accept that leaves a range marker between blocks (the paragraph that held only a move's range start goes) no longer
  fails with `validation_failed` when the marker already had a problem, such as a `w:moveFromRangeStart` without its
  `w:date`: `check` gives the body the problems of the paragraph the marker came from, as it does for any other unit.
- Accepting the deletion of a table keeps a comment's range start before its end when the markers stand at different
  depths (the end inside another author's insertion). The end came first, and `check` refused the accept.
- Accepting or rejecting a move removes a deletion that held nothing but the move's range marker, instead of leaving an empty
  `w:del` that the compiler refused as `internal` and `check` as a new violation.
- A table of contents, or another field whose result spans paragraphs, no longer collapses in a view when that hides
  something the view shows. In `Redline` a tracked change on the mark of the paragraph where the field ends keeps it open,
  so a tracked split there is shown. In `Final` that paragraph joins the next one when its mark is deleted, so the field
  stays open when the next paragraph has text, and takes in an empty one, as it does once the deletion is accepted. A paragraph that joins the one where the field
  begins is read the same way.

- The random law L5 compares each paragraph's style against its own view's common style, so a reject that changes which
  style is commonest in the document no longer reads as a change on every line. It tolerates exactly one empty paragraph
  that a reject adds as the last block of a cell whose last block was a table (W9), where it used to drop every such
  paragraph from both sides (`tdf153891.docx`).
- A split before a paragraph's text (`at: 0`, or `before` its first word) keeps the paragraph's ref, element and
  attributes on the text and adds the new empty paragraph, with the new ref, before it. Rejecting the split gives back the
  very paragraph under its old ref; before, the text came back under the new ref and lost the paragraph's `w14:paraId`
  and rsids.
- A deleted paragraph is one `ParagraphDeleted` change with every piece of its deletion, a piece that holds nothing with
  width (a field's `begin` and code) included. Such a piece was a change of its own, so rejecting the paragraph's deletion
  gave back a field's result without its `begin`, and the edit was refused with `validation_failed`.
- A tracked `merge` of two list paragraphs that each hold a legacy `w:numberingChange` is applied. Their change ids made
  their properties look different, so the second took the first's, and the copy carried the first's numbering change and
  its id: two tracked changes had the same id and the edit was refused with `validation_failed`. The copy now follows the
  copy rule and leaves out every change-tracking element.
- A tracked `set` on a table, row, cell or section whose properties already break the schema (a second `w:tblLook`) is
  applied. The copy of the old properties in the change's record carried the same problem, and the check counted it as new
  and refused the edit with `validation_failed`.
- `compare` places the deleted blocks at the end of a story after the last inserted table, before the paragraphs that end the
  story, so that rejecting everything restores the original's last paragraph. Placed before the table, the paragraph that took
  the inserted end mark could not join across the table that rejecting removes, and an empty paragraph was left (`DB010`).
- The random law L3 expects a whole-paragraph `replace` of a note's paragraph to keep the note's own number (U+0003), as
  every text edit does (design `revisions.md`, 3.6.4). It expected the bare text and failed on a note of `tdf129659.docx`.
- A paragraph inserted after the last paragraph of a cell, a note, a comment or the body is written in Word's Enter shape:
  the anchor's mark is inserted and the new paragraph keeps the container's own, so Word's Reject All gives the original
  back. With an inserted mark of its own, Word left the cell an empty paragraph (hand check C11), and stetdoc's own reject
  now does the same for such a paragraph, as Word and LibreOffice do. Where that shape cannot go (the last paragraph's mark
  is a pending deletion or move) the insertion is `readonly_region`. `compare` writes a run of new paragraphs at the end of a
  container the same way. A paragraph that holds only a field and joins the next one in a view keeps its place there, with
  the field.
- `compare` never leaves a deleted table directly against an inserted one, which Word showed with a gap that stayed after
  Accept All and after Reject All: a table replaced by another is one table of deleted rows and inserted rows, whatever
  their grids: the rows take one grid, with every column edge of both when the deleted rows would
  not fit the revised grid as they are.
- Deleting a whole note with `delete-note` deletes the note's own number with its text, as Word writes it (`RP050`): with the
  number left live, Word for the web kept an empty note after Accept All. A text edit of a note still keeps its number.
- A field that spans paragraphs, such as an index, collapses in the `Final` view when the only text beside it is a
  pending deletion, as it does once the deletion is accepted, so accepting no longer changes that view (law L5, release
  seed 303). `Redline` still shows the paragraphs, with the deletion.
- A text or property edit of a paragraph inside a body-level move away (`w:moveFrom`) or deletion (`w:del`) is
  `readonly_region`, as an edit of deleted text is, where it used to write text that the Final view does not show.
- A paragraph inserted next to a block inside a body-level move, insertion or deletion goes outside that change, before
  its first block or after its last, where it used to land inside it and read as empty; anywhere else inside it is
  `readonly_region`.
- An `insert` of many paragraphs costs time in proportion to them, where each new paragraph used to scan the story
  again (4,000 empty paragraphs took 2.2 s, now about 0.1 s). Each paragraph of an `insert` now counts as one operation
  toward `max_batch_ops`, as each new paragraph already did on the files path, and the paragraphs of one `insert` follow
  the quotes of their anchor as it was before the operation.
- A style that does not exist, in `style`, `format`'s `char_style` or an inserted paragraph, is `invalid_payload` with the
  nearest names of that kind, as the error table says, and no longer `not_found`.
- `set` refuses a property its container does not allow wherever the paragraph sits, including a paragraph inside a
  body-level move, where it used to be written.
- An operation that names a block of a header or a footer that an earlier operation of the batch took out of the document
  (an accepted deletion of the section break that named it) is refused as `stale_ref`, and an edit of that footer earlier
  in the batch is applied and its refs retired. Both used to fail as `internal`.
- A receipt's `retired` lists the ref of every block a merge or a join absorbed, as `merged into p4` (or the survivor's
  reason, when the same edit removed the survivor too), as the design says. It used to list only blocks that left the
  document.
- A `delete` from `ref` through `to` compares its two points in document order: `start` in the first paragraph may be
  greater than `end` in the last, and `to` naming the same paragraph deletes the range between the two offsets.
- `check` reads a paragraph, run, table, row or cell by its own content model wherever it stands (inside a body-level
  move, under an unknown element), and the survivor of a join carries the problems the joined paragraphs already had.
- Accepting a merge, or any move, in a document that binds one prefix to different namespaces in different places
  declares the prefix on what moves, where it used to be refused as `unsupported`.
- A block that a dry run creates shows its tags (`{¶+ ...}` and the others) in the receipt, so it reads as the real edit's
  line without the ref.

- A refusal of readable files (a ref on two lines, a syntax error, a read-only line) carries its file and
  line in `StetError::file` and `StetError::line`, not only in its message, as a scope violation does.
- A merge across an imported document part (`w:altChunk`) is refused, and a paragraph deleted right before one keeps
  its mark, so accepting all reads as `Final`.
- Text put into an empty paragraph whose mark is hidden is not hidden.
- A `w:t` in a `w:moveFrom` is no longer reported as text in a deletion.
- Paragraph ids in the `w15` namespace of Word 2013's preview are read and validated as `w15`.
- A row inserted by one author and deleted by another is gone in `Final` and in `Original`.
- `Section 9.2 of This Agreement` and `PARAGRAPH (a) OF THIS SECTION` cite this document, not another: the
  word after `of` is compared in any case with `this`, `these` and `hereof`.
- The readable files are never written or read in `json/1`: `render_files` and `to_batch` refuse it with
  `unsupported`.

- Rejecting a tracked format on a run whose properties hold a property twice (tdf104649) puts
  both back instead of failing with `invalid_payload`.
- A paragraph added after one with a hidden paragraph mark is not hidden text, a copied content
  control loses its `w:dataBinding`, and new paragraphs get `w14:paraId` and `w14:textId` only in
  the Word 2010 namespace.
- A tracked deletion of an equation that already holds a tracked deletion is refused with
  `readonly_region` instead of writing a deletion inside a deletion.
- The quote policy shapes only the quotes an edit types: a paragraph that mixes curly and straight
  quotes, rewritten with one word changed, keeps the quotes it carried over.
- Text put into the empty paragraph after an equation does not copy the mark's `w:oMath` into
  its run.
- Rejecting a table whose rows were all inserted keeps a bookmark that starts in it and ends
  after it.
- The old run properties in a `w:rPrChange` are checked for Word's order, so rejecting a format
  that removed a duplicate property (tdf168070) is not refused as adding it.
- Clearing the last property of a run removes its empty `w:rPr`.
- `check` carries a copy of a defect in a `w:rPr` or a `w:pPr` (a mark's properties copied onto a
  run, a paragraph's into a `w:pPrChange`) instead of refusing the edit for it.
- An untracked edit never rewrites or hides another author's pending change: text inside their insertion,
  properties under their formatting change, a run they inserted, or a paragraph or row that holds their
  changes is `readonly_region`, and the message says to accept or reject that change first. Untracked still
  means your own changes go untracked.
- The Claude Code guard hook reads the tool call to the end when `STETDOC_GUARD=off` turns it off, so Claude Code
  never writes into a closed pipe. Its test failed now and then in CI for that reason.
- A comment added to a package that binds `w15` to the namespace of Word 2013's preview
  (`http://schemas.microsoft.com/office/word/2010/11/wordml`) and has no `commentsExtended.xml` no longer creates that
  part, which made desktop Word report unreadable content. A reply, resolve or reopen there is refused as `unsupported`,
  since a thread and its done state live only in that part.
- An insert at the very start of a paragraph goes after `w:pPr` when an XML comment or processing instruction stands
  before it. It used to land before `w:pPr`, and the edit was refused with `validation_failed`.
- Deleting a table that is the first or last block keeps a comment whose range runs from inside the table to a marker
  between blocks, as an empty-range comment like the others. It used to remove that comment.
- The Final and Original views collapse a long field, such as a table of contents, exactly when the accepted or
  rejected file would: a pending paragraph mark that joins two of its paragraphs is counted as joined.
- After a tracked split and a delete of the same paragraph, the view keeps the fields that come back into the
  paragraph. It used to drop them until the document was opened again.
- A delete from the middle of one paragraph to the middle of another keeps a comment whose range starts or ends in a
  paragraph the delete takes whole, or at a marker between paragraphs, as an empty-range comment if all its text goes.
  The edit used to be refused with `validation_failed`, and accepting such a tracked delete failed the same way.

[0.9.1]: https://github.com/clauseo-ai/stetdoc/releases/tag/v0.9.1
[0.9.0]: https://github.com/clauseo-ai/stetdoc/releases/tag/v0.9.0
