Operand

odyssey, u hear?

gram: docs

> ./openspec/changes/review-plugin/design.md

## Context
The editor has complete OOXML support for track changes and comments (types, parsers, serializers, ProseMirror marks/commands). Missing: a high-level API that works with paragraph indices and IDs instead of ProseMirror positions.
Monorepo: `packages/core` (`@eigenpal/docx-editor-core`, MIT), `packages/react`, `packages/vue`. Root workspace `"packages/*"`. Build: tsup. New package: `packages/agent-use``@eigenpal/docx-editor-agents` (BSL-1.1).
## Goals / Non-Goals
**Goals:**
- 14-method `DocxReviewer` class that covers: read, discover, comment, propose, resolve, batch, export
- `paragraphIndex` as primary anchor (deterministic, no quoting errors)
- `search` as optional sub-paragraph refinement only
- Chunked reading for long documents
- Headless-first, peer dep on core
**Non-Goals:**
- Section tree / document structure parsing (LLMs understand headings in flat text)
- Filtered bulk operations (`acceptByAuthor`, `rejectByType`) — agents can filter in JS
- Comment management (`resolve`, `delete`, `reopen`) — agents add comments, humans manage them
- Runtime license enforcement — BSL is legal, not technical
- Track changes authoring mode, sidebar UI, real-time collaboration
- MCP tools (can add later, keep v1 focused on the class API)
## Decisions
### 1. Separate BSL-1.1 package, not a plugin
`packages/agent-use/` with `@eigenpal/docx-editor-core` as peer dep. Published on public npm under BSL-1.1.
**Why not CorePlugin?** DocxReviewer is a standalone class with its own lifecycle. Plugin registration adds unnecessary ceremony for something that's just `new DocxReviewer(doc)`.
**Why BSL over private npm?** Lower friction. Public npm, anyone can evaluate. Enterprise customers pay because they respect the license. No registry infra.
### 2. `paragraphIndex` as primary anchor
Every `getContent()` block has an `index`. The agent references that index in `addComment()`, `proposeReplacement()`, etc.
**Why not text search as primary?** LLMs don't reproduce text verbatim — they paraphrase, drop punctuation, change quotes. Paragraph index is deterministic. `search` is only used as optional refinement for sub-paragraph targeting (scoped to one paragraph, much less ambiguous).
```typescript
// Agent sees: { index: 15, text: 'aggregate liability shall not exceed $50,000.' }
// Agent does:
reviewer.addComment({ paragraphIndex: 15, author: 'AI', text: 'Cap too low.' });
reviewer.proposeReplacement({
paragraphIndex: 15,
search: '$50,000',
author: 'AI',
replaceWith: '$500,000',
});

3. Chunked reading, not structure trees

Long docs (50+ pages) don't fit in one LLM call. Solution: getContent({ fromIndex: 50, toIndex: 80 }) returns a slice.

Why not getStructure() with section trees? Over-engineering. LLMs already understand headings in flat text. An agent can scan getContent() output, see headings, and decide which paragraph ranges to drill into. We don't need to parse legal numbering schemes.

4. Mutable operations, immutable output

DocxReviewer deep-clones the document on construction. Operations mutate the clone. toDocument() / toBuffer() returns the final state.

5. Minimal surface — 14 methods

Read: getContent(options?)
Discover: getChanges(filter?), getComments(filter?)
Comment: addComment({paragraphIndex, author, text, search?}), replyTo(commentId, {author, text})
Propose: proposeReplacement({...}), proposeInsertion({...}), proposeDeletion({...})
Resolve: acceptChange(id), rejectChange(id), acceptAll(), rejectAll()
Batch: applyReview({...})
Export: toBuffer(), toDocument()

Everything cut from the earlier design (acceptByAuthor, rejectByType, resolveComment, deleteComment, getSummary, addCommentAtRange) can be done by composing these 14 methods. Agent calls getChanges({ author: 'Jane' }) → loops → acceptChange(id). No need for a dedicated bulk method.

Risks / Trade-offs

[Risk] paragraphIndex changes if agent adds/removes paragraphs mid-session → Mitigation: Document that indices are snapshot-based. Batch operations process in order. For multi-pass workflows, agent should re-read getContent() between passes.

[Risk] search within a paragraph still fails on LLM misquotes → Mitigation: Scoped to one paragraph, so much less ambiguous. Can add fuzzy matching later if needed.

[Risk] No MCP tools in v1 → Accepted trade-off. Class API is the priority. MCP wrappers are trivial to add later — each tool just calls one DocxReviewer method.

[Trade-off] No section-level operations → Accepted. paragraphIndex + fromIndex/toIndex chunking covers the same use cases without inventing a section parser.