@ansonlai/docx-redline-js 0.8.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/ARCHITECTURE.md +679 -460
  2. package/CHANGELOG.md +208 -1
  3. package/README.md +152 -16
  4. package/core/current-properties.js +136 -0
  5. package/core/paragraph-text.js +211 -209
  6. package/core/types.js +9 -3
  7. package/core/word-revision-semantics.js +256 -0
  8. package/core/word-xml.js +160 -118
  9. package/dist/docx-redline-js.esm.js +4338 -796
  10. package/dist/docx-redline-js.esm.js.map +4 -4
  11. package/dist/docx-redline-js.esm.min.js +111 -108
  12. package/dist/docx-redline-js.esm.min.js.map +4 -4
  13. package/dist/docx-redline.bundle.cjs +4463 -921
  14. package/dist/docx-redline.bundle.cjs.map +4 -4
  15. package/dist/docx-redline.bundle.js +4463 -921
  16. package/dist/docx-redline.bundle.js.map +4 -4
  17. package/docs/AGENT_FAST_START.md +2 -0
  18. package/docs/AGENT_KNOWLEDGE_BASE.md +49 -7
  19. package/docs/SKILL_AUTHORING.md +169 -150
  20. package/docs/TESTING.md +1146 -1059
  21. package/docs/schemas/document-operations.schema.json +55 -2
  22. package/document/docx-document.js +1022 -1010
  23. package/engine/oxml-engine.js +21 -22
  24. package/engine/paragraph-mark-revisions.js +292 -0
  25. package/engine/reconstruction-mapper.js +312 -311
  26. package/engine/reconstruction-writer.js +67 -10
  27. package/engine/rpr-helpers.js +42 -0
  28. package/engine/run-builders.js +0 -73
  29. package/engine/surgical-diff-application.js +604 -605
  30. package/engine/table-mode.js +63 -63
  31. package/index.d.ts +53 -2
  32. package/index.js +13 -2
  33. package/node/cli-help.js +1 -0
  34. package/node/cli.js +21 -3
  35. package/orchestration/list-structural-fallback.js +67 -9
  36. package/package.json +1 -1
  37. package/pipeline/diff-engine.js +130 -1
  38. package/pipeline/ingestion-export.js +227 -243
  39. package/pipeline/ingestion-paragraph.js +93 -98
  40. package/pipeline/list-generation.js +415 -419
  41. package/pipeline/markdown-processor.js +5 -2
  42. package/pipeline/pipeline.js +206 -204
  43. package/scripts/export-validation-fixtures.mjs +19 -6
  44. package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
  45. package/scripts/lib/word-coverage-catalogue.mjs +7 -2
  46. package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
  47. package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
  48. package/scripts/word-com-differential.ps1 +443 -335
  49. package/scripts/word-com-visual-suite.ps1 +2 -2
  50. package/scripts/word-revision-semantics-matrix.ps1 +108 -0
  51. package/services/batch-operation-orchestrator.js +23 -3
  52. package/services/capture-engine.js +228 -226
  53. package/services/comment-replies.js +1 -1
  54. package/services/document-operation-applier.js +214 -0
  55. package/services/document-operation-contract.js +76 -0
  56. package/services/document-operation-mutations.js +688 -51
  57. package/services/error-recovery.js +17 -0
  58. package/services/inspection-comparison.js +82 -0
  59. package/services/list-conversion-diagnostics.js +296 -0
  60. package/services/list-format-change.js +586 -0
  61. package/services/list-format-mutation.js +416 -0
  62. package/services/list-format.js +305 -0
  63. package/services/numbering-allocation.js +642 -0
  64. package/services/numbering-helpers.js +354 -524
  65. package/services/operation-batch-compiler.js +183 -11
  66. package/services/operation-preflight.js +43 -33
  67. package/services/receipt-collector.js +288 -288
  68. package/services/revision-comment-management.js +38 -5
  69. package/services/revision-fidelity-verifier.js +484 -0
  70. package/services/standalone-docx-plumbing.js +627 -631
  71. package/services/standalone-operation-runner.d.ts +182 -5
  72. package/services/table-reconciliation.js +59 -7
@@ -41,6 +41,8 @@ If the user requests deleted text, search with `--view rejected` first. Otherwis
41
41
 
42
42
  - Require `completion: true` and a non-null `outputPath`; the CLI computes this from write, operation, commit, and localized-verification results.
43
43
  - On failure, follow `error.recovery.action`; never retry unchanged arguments.
44
+ - `REVISION_FIDELITY_VIOLATION` (Reject All would not restore the source): retry
45
+ narrower per `recovery.retryGuidance`; never pass `--revision-fidelity off`.
44
46
  - Use `retryPlan.base: "original"` to replay a batch and `"output"` to retain
45
47
  committed progressive work and retry the reported indexes.
46
48
  - The source is not overwritten unless `--in-place` is explicit.
@@ -71,6 +71,8 @@ instructions that will exist in another clone or in CI.
71
71
  | Full-document operation scheduling or rollback | `services/document-operation-applier.js`, `services/batch-operation-orchestrator.js`, `services/document-operation-session.js` | `tests/standalone_operation_runner_tests.mjs`, `tests/performance_phase1_session_tests.mjs` |
72
72
  | DOCX ZIP wiring or CLI behavior | `node/docx-document.js`, `node/zip-archive.js`, `node/cli.js` | `tests/docx_package_facade_tests.mjs`, `tests/node_zip_archive_tests.mjs`, `tests/agent_cli_tests.mjs` |
73
73
  | Validation, receipts, and lifecycle oracles | `core/redline-validation.js`, `services/receipt-collector.js`, `services/revision-comment-management.js` | `tests/redline_validation_tests.mjs`, `tests/mutation_receipt_tests.mjs`, `tests/roundtrip_oracle_tests.mjs` |
74
+ | Existing-revision policy resolution (`auto`) | `core/word-xml.js` (`resolveExistingRevisionsPolicy`), `services/document-operation-mutations.js`, `services/operation-preflight.js` | `tests/existing_revisions_auto_tests.mjs`, `tests/existing_revisions_auto_paths_tests.mjs` |
75
+ | Comparing a committed document with a prepared one | `services/inspection-comparison.js` | `tests/inspection_comparison_tests.mjs` |
74
76
 
75
77
  Run one focused suite with `node tests/<name>.mjs`. Run `npm test` only when the
76
78
  change crosses several subsystems or before release-level handoff. The Word COM,
@@ -163,6 +165,14 @@ insertions and deletions inside their pending insertion. Pass
163
165
  via CLI) to normalize all prior revisions first, or `'reject-input'` to refuse any
164
166
  paragraph with open revisions. Use `'accept-all-first-keep-normalized'` only when
165
167
  accepted revisions should be returned as a real change even on a no-op edit.
168
+ `existingRevisions: 'auto'` (or `--existing-revisions auto`, capability
169
+ `existing-revisions-auto-v1`) chooses per target: `'merge-same-author'` when the
170
+ target has no tracked changes or only the operation author's, otherwise
171
+ `'slice-cross-author'` (unattributed changes count as another author's).
172
+ Preflight reports the resolved policy. `auto` opts into cross-author slicing
173
+ wherever it is needed, so choose it only when editing inside other reviewers'
174
+ pending insertions is intended for the whole batch; it never accepts or rejects
175
+ anyone's revisions.
166
176
  Same-author merging also fails with `COMMENTED_CONTENT_MERGE` when the revised
167
177
  paragraph contains comment anchors, because reverting the prior revision could
168
178
  remove or orphan those comments. Resolve the comments before re-editing.
@@ -375,6 +385,22 @@ const inspection = inspectDocumentParts({ documentXml, commentsXml, numberingXml
375
385
  Reuse `exactText` plus `paragraphId` or `fingerprint` in an operation. Computed
376
386
  list labels and excerpts are for display, not replacements for exact targets.
377
387
 
388
+ ### Verify a committed document against the prepared one
389
+
390
+ ```js
391
+ import { inspectDocumentParts, compareDocumentInspections } from '@ansonlai/docx-redline-js';
392
+ const check = compareDocumentInspections(inspectDocumentParts(preparedParts), inspectDocumentParts(committedParts));
393
+ // { matches, differences: [{ path, expected, actual }] }, at most the first 10
394
+ ```
395
+
396
+ Use it when a host writes a prepared package through its own API (for example a
397
+ Word add-in's `insertOoxml`) and then reads the result back. It compares paragraph
398
+ text, table position, list labels, review segments (kind, author, position;
399
+ adjacent same-author segments merged) and comments (text, author, anchor,
400
+ resolved state, thread parent). It ignores revision ids, dates, rsids, paragraph
401
+ ids, comment ids and order, and run boundaries. A mismatch is a signal to
402
+ re-inspect, not something to retry blindly.
403
+
378
404
  ### Safely edit a complete DOCX in Node
379
405
 
380
406
  ```js
@@ -462,10 +488,14 @@ fragment to insert. Do not choose a type from its English name alone.
462
488
  |---|---|
463
489
  | Change text within one paragraph or replace its content | `{ type: 'redline', target, modified }` (`replace` is a compatibility alias) |
464
490
  | Delete a whole paragraph | `{ type: 'delete', target }` (normalized to a redline with `modified: ''`) |
465
- | Change native list structure | `{ type: 'list-change', target, modified: '<complete Markdown list>' }` |
491
+ | Change native list structure | `{ type: 'list-change', target, modified: '<complete Markdown list>' }`. It inserts, removes and nests items; it never changes the list's kind or numbering format (retyped markers warn `LIST_FORMAT_NOT_APPLIED` or fail `UNSUPPORTED_LIST_FORMAT_CHANGE` with a `change-format` `suggestedOperation`): use `list-format` `change-format` for that |
466
492
  | Reconcile a Word table | `{ type: 'table-reconciliation', target, modified: '<complete Markdown table>' }` |
467
493
  | Comment or highlight existing text | `comment` with `commentContent`, or `highlight` with `textToHighlight` |
468
494
  | Change character or paragraph formatting | `character-format`/`format` with `textToFormat` and `properties`, or `paragraph-format` with `properties`; add `textOccurrence` (1-based, default 1) to format a later match of `textToFormat` inside the targeted paragraph (out of range fails `PATCH_SOURCE_NOT_FOUND`; independent of `target.occurrence`, which selects the paragraph; legacy calls without `textOccurrence` still read `target.occurrence`; capability `format-text-occurrence-v1`) |
495
+ | Put an existing paragraph in (or out of) an existing list | `{ type: 'paragraph-format', target, properties: { numbering: { numId, level } } }` with `numId`/`level` copied from an inspected list paragraph of the target list, or `numbering: null` to remove the paragraph's direct list binding. A tracked `w:pPrChange` keeps the previous `w:numPr`, so Reject All restores the old membership and Accept All shows the new one. It binds only: it never creates a list, marker text is not stripped, and sibling numbering is not remapped. Refused `PARAGRAPH_NUMBERING_NOT_FOUND` (unknown `numId` or undefined level) and `PARAGRAPH_FORMAT_REVISION_CONFLICT` (another author's pending paragraph-property change). Capability `paragraph-numbering-binding-v1` |
496
+ | Turn typed headers (`A.`, `1.`, `I.`, `-` typed as text) into a real list | `{ type: 'list-format', target, targetEnd?, list: { mode: 'convert-plain' } }` on the plain paragraphs that carry the typed markers. Each marker becomes a tracked deletion and the paragraph a list item with that label (tracked `w:pPrChange`, same paragraph); the marker's value is the start value, consecutive markers share one list, body paragraphs in the range stay plain, and a later `list-format` in the batch continuing the count joins the list. A lone `I.`/`C.` is ambiguous (letter or roman): pass `list.format`. `strip: 'none'` with `format` (and `start`) binds a paragraph without a typed marker and leaves its text alone. Do not express this as a `redline` that adds or keeps marker text. A changed-text header (`1. Old` -> `1. New`) is a `redline` of that paragraph that keeps the typed marker exactly (`modified: '1. New'`, or `replacements`) plus this `list-format` in the same batch, in either order: the redline runs first and the list-format strips the unchanged marker, so Accept All shows label `1.` with `New` and Reject All the literal `1. Old`. A redline in that batch that adds, removes or changes the marker, spans paragraphs, or leaves the text unchanged is refused before mutation (`LIST_FORMAT_COMPOSITION_CONFLICT`). Capability `list-format-v1` (contract version 9) |
497
+ | Change a list's kind or numbering style (bullets to `1.`, `1.` to `A.`, a nested level to `(i)`) | `{ type: 'list-format', target, list: { mode: 'change-format', format, lvlText?, level?, start?, scope? } }` with `target` on any item of the list. A NEW definition is allocated (levels, indentation and fonts copied from the list; only `level`, default the target's level, is redefined) and every item is rebound with a tracked `w:pPrChange`: Reject All restores the old labels, Accept All shows the new ones; the shared definition is never edited. `scope: 'list'` (default) rebinds every item, including items outside `target` (`result.list.items[].inTarget`); `scope: 'range'` with `targetEnd` changes only a range that runs to the end of the list and continues its count. `lvlText` keeps the source text unless given (`"%1."`, `"(%2)"`; a bullet's character). Do not express this as a `list-change`/`redline` that retypes the markers. `scope: 'list'` counts as a write to every item it rebinds: in the same batch each of those items may also carry one single-paragraph `redline` that adds no marker (it runs first); any other write to them fails `LIST_FORMAT_COMPOSITION_CONFLICT` (`reason: list-rebind`), so send it in a separate apply or use `scope: 'range'`. Capability `list-format-change-v1` (contract version 9) |
498
+ | Make a list continue an earlier list (`1. 2.` / `1. 2.` -> `1. 2.` / `3. 4.`) | `{ type: 'list-format', target, list: { mode: 'join', continueFrom: { paragraphId, fingerprint } } }` with `target` on the later list and `continueFrom` on an item of the earlier one; items keep their levels and take the earlier list's format. `scope: 'range'` joins only a range that runs to the end of the later list. Capability `list-format-change-v1` |
469
499
  | Counterpropose a paragraph wholly deleted by another author | `restore` with a rejected-view target |
470
500
  | Insert text inside another author's rejected-view content | `insert` with `target.revisionView: 'rejected'`, an exact `anchor`, and `existingRevisions: 'slice-cross-author'` |
471
501
 
@@ -543,7 +573,7 @@ docx-redline apply contract.docx --target "Typo fix" --modified "Fixed typo" --n
543
573
 
544
574
  Key CLI defaults and behaviors:
545
575
  - **Author**: Automatically defaults to `'AI Redliner'` (overridable via `--author` or `DOCX_REDLINE_AUTHOR` environment variable).
546
- - **Existing revisions**: Defaults to `'merge-same-author'`. Pass `--existing-revisions slice-cross-author` to edit inside another reviewer's pending insertions with native carrier slicing.
576
+ - **Existing revisions**: Defaults to `'merge-same-author'`. Pass `--existing-revisions slice-cross-author` to edit inside another reviewer's pending insertions with native carrier slicing, or `--existing-revisions auto` to slice only the targets that carry another reviewer's changes.
547
577
  - **Overwrite behavior**: Destination files provided via `--output` overwrite by default. To protect existing destination files, pass `--no-overwrite` or `--no-clobber`. The source document is never overwritten unless `--in-place` is specified.
548
578
  - **Tracked changes**: Defaults to `generateRedlines: true`. When clean direct text is needed, pass `--no-redlines`.
549
579
  - **Atomic rollback (optional)**: Operations apply progressively by default (`atomic: false`). For all-or-nothing transactional rollback where any error halts and reverts all changes, pass `--atomic`.
@@ -770,13 +800,25 @@ When the CLI or runner returns an error code, follow these specific recovery act
770
800
  | `ANCHOR_NOT_FOUND` / `AMBIGUOUS_ANCHOR` | A comment or rejected-view insertion anchor was not uniquely matched. | For comments, narrow `textToComment` or omit it to anchor the whole paragraph. For rejected-view insertion, copy exact rejected text and provide `anchor.occurrence`. |
771
801
  | `OVERLAPPING_SOURCE_TARGETS` / `OVERLAPPING_TEXT_EDITS` | Multiple complete text operations target the same batch-start source. | Consolidate all changes to that paragraph into one `redline` or `replace`; the library cannot choose between incompatible complete desired states. |
772
802
  | `REVISION_ORDER_CONFLICT` | A text rewrite and formatting/highlight operation overlap the same source. | Consolidate or split the work at an intentional created-content dependency; do not try a different arbitrary order. |
803
+ | `LIST_FORMAT_COMPOSITION_CONFLICT` | A `redline` and a `list-format` on the same paragraph do not compose (`reason`: `marker-changed` with `sourceMarker`/`modifiedMarker`, `redline-unchanged`, `redline-shape` for a multi-paragraph, ranged, anchored, `list-change` or structured redline), or a `list-format` with `scope: 'list'` rebinds an item another operation also writes (`list-rebind`, `rebindingOperationIndexes`). Detected at batch compilation; nothing was written. | `recovery.action: split_text_and_list_changes` (`recovery.hint`). Keep the typed marker byte-identical in the redline (the list-format strips it) and express a new label through `list-format`; drop a no-op redline; send other writes to rebound items in a separate apply or use `scope: 'range'`. |
773
804
  | `CAPTURE_FANOUT_CONFLICT` | Multiple mutating consumers share one capture without distinct selectors. | Give consumers distinct selectors, chain them explicitly, or split the batch. |
774
- | `EXISTING_REVISIONS` | Target paragraph contains tracked changes from another author. | Fails closed to protect third-party review marks. If editing inside that reviewer's pending insertion is intended, pass `--existing-revisions slice-cross-author` (or `existingRevisions: 'slice-cross-author'`). Do not pass `accept-all-first` without explicit user authorization. |
775
- | `PATCH_ROUNDTRIP_MISMATCH` | A cross-author surgical edit did not reconstruct the requested modified text exactly. | Treat the operation as unapplied. Re-extract the exact paragraph text and split the edit into a narrower operation that does not cross the reported structural boundary. |
805
+ | `EXISTING_REVISIONS` | Target paragraph contains tracked changes from another author. | Fails closed to protect third-party review marks. If editing inside that reviewer's pending insertion is intended, pass `--existing-revisions slice-cross-author` (or `existingRevisions: 'slice-cross-author'`; `'auto'` does this per target). Do not pass `accept-all-first` without explicit user authorization. |
806
+ | `PATCH_ROUNDTRIP_MISMATCH` | A cross-author surgical edit did not reconstruct the requested modified text exactly, or a localized `replacements` operation would not leave the compiled accepted text in the target (checked before commit on every path). | Treat the operation as unapplied. Re-extract the exact paragraph text and split the edit into a narrower operation that does not cross the reported structural boundary. |
776
807
  | `FOREIGN_PARAGRAPH_MARK_DELETION` | A normal edit attempted to write into a paragraph wholly deleted by another reviewer. | Use an explicit `restore` operation if the user intends to counterpropose that paragraph; otherwise leave the deletion unresolved. |
777
808
  | `RESTORATION_STATE_REQUIRED` / `RESTORATION_COUNT_MISMATCH` | A `restore` target is not a wholly foreign-deleted paragraph, or its replacement count does not match the paragraph range. | Re-inspect the document and target the deleted paragraph by stable descriptor; provide exactly one replacement string per source paragraph. |
778
809
  | `REJECTED_INSERTION_STATE_REQUIRED` / `UNSAFE_REVISION_BOUNDARY` | An explicit rejected-view insertion did not resolve to supported plain run text inside a wholly foreign-deleted paragraph. | Do not fall back to a generic edit. Narrow the exact anchor/offset, or handle comments, bookmarks, fields, hyperlinks, moves, or other structural boundaries manually. |
779
810
  | `GENERATED_OOXML_INVALID` | The operation introduced a new validation error relative to its baseline. | Treat the operation as unapplied and inspect `generatedIssues`; correct the generating operation or builder rather than repairing or accepting the source document's unrelated baseline defects. |
811
+ | `LIST_MARKER_NOT_BOUND` (warning) / `UNSUPPORTED_LIST_CONVERSION` (error) | A plain paragraph was edited so its text starts with a list marker (`A.`, `1.`, `iv.`, `-`) its original did not carry, but text edits cannot bind a list: the marker would be literal text and `list` stays `null`. Warning (`diagnostics[]` plus a `CODE: message` string in `warnings`) by default; error under the strict policy (`--profile agent`, `--atomic`, or `strictListConversion: true`). Separate from `REVISION_FIDELITY_VIOLATION`. Fields: `listMarker`, `requestedList`, `targetExcerpt`. Capability `list-conversion-diagnostics-v1`. | The error applied nothing. To create the list item, send `recovery.suggestedOperation` (a `list-format` `convert-plain` request) with the same target and leave the text unmarked (`recovery.action: use_list_format_convert_plain`); if the original already carries another marker, retype it with a `redline` first and convert with `strip: "marker"` in a later batch. If literal marker text is intended, repeat with `listMarkers: "literal"`. Do not treat a warning-only `ok` as a list conversion. |
812
+ | `LIST_MARKER_NOT_FOUND` / `LIST_MARKER_AMBIGUOUS` / `LIST_MARKER_UNSUPPORTED` (error) | `list-format` `convert-plain` found no typed marker to strip; a lone roman-looking letter (`I.`, `C.`, `D.`) reads as letter or roman (`formats` lists both); or the marker shape is unsupported (`reason`: `mixed` markers in one range, `multilevel` `2.1.`, `parenthesized` `(a)`, `zero`, `format-mismatch`, `marker-shape` when the marker sits in a hyperlink, field, content control or pending revision). Nothing was written. | NOT_FOUND: target the marker paragraphs, or use `strip: "none"` with `list.format`. AMBIGUOUS: repeat with `list.format` from `recovery.allowedValues`. UNSUPPORTED: split mixed ranges into one operation per format; leave other shapes literal or convert them in the host. |
813
+ | `LIST_FORMAT_TARGET_IS_LIST` / `LIST_FORMAT_STYLE_NUMBERED` (error) | `convert-plain` converts plain paragraphs only: the paragraph already has a direct `w:numPr`, or its paragraph style (or a `basedOn` ancestor) carries numbering. Nothing was written. | Narrow the range to plain paragraphs. To move an existing list item, use `paragraph-format` `numbering`; style-linked numbering is changed in the host. |
814
+ | `LIST_FORMAT_NOT_APPLIED` (warning) / `UNSUPPORTED_LIST_FORMAT_CHANGE` (error) | A multi-line `list-change`/`redline` kept a list item's text with a marker whose kind or format (bullet vs numbered, decimal vs `A.`) differs from the numbering definition the item keeps (`sourceList.{numId,level,kind,format}`); the old definition is not replaced. Same warning/strict policy as above. Inserted items are not compared. | `recovery.action: use_list_format_change_format`: send `recovery.suggestedOperation` (a `list-format` `change-format` request, scope `list`) with a target on one item of the list; `list-change` never applies a format change itself. Text changes go in single-paragraph `redline`s without the new markers, which may share the batch with the `change-format` (they run first). Or pass `listMarkers: "literal"` to accept the result. |
815
+ | `LIST_FORMAT_TARGET_NOT_LIST` / `LIST_FORMAT_MIXED_RANGE` (error) | `change-format`/`join`: the target range has no list item; or it mixes list items with plain paragraphs (`reason: non-list`), spans several lists (`several-lists`, `numIds`), or, for `change-format` without `list.level`, several levels (`levels`, `recovery.allowedValues`). Tracked-deleted paragraphs are skipped, never rebound. Nothing was written. | NOT_LIST: use `convert-plain` (typed markers) or `paragraph-format` `numbering`. MIXED: target one item of the list (scope `list`), one operation per list, or pass `list.level`. |
816
+ | `LIST_FORMAT_STYLE_LINKED` / `LIST_FORMAT_DEFINITION_UNSUPPORTED` (error) | The list's definition is style-linked (`w:numStyleLink`/`w:styleLink`, a level `w:pStyle` such as heading numbering, or a paragraph style that binds it), so items can belong to it without a direct binding; or the definition cannot be rebound exactly (`reason`: `missing` numbering part or numId, `level-override` `w:lvlOverride/w:lvl`, `level-restart` `w:lvlRestart` in a `scope: 'range'` split, `undefined-level`). Nothing was written. | Change these lists in the host. `missing` on a bare call: pass the package's `word/numbering.xml` as `numberingXml`. `level-restart`: use `scope: 'list'`. |
817
+ | `LIST_FORMAT_SPLIT_UNSUPPORTED` / `LIST_FORMAT_JOIN_INVALID` (error) | The rebinding would renumber paragraphs it does not name, or Word could not show the same count: `mid-sequence` (a `scope: 'range'` that stops before the list's last item; `followingParagraphIndexes`, `lastListParagraphIndex`), `nested-restart` (a split that starts below a level that later restarts inside the range), `shared-definition` (another list shares the abstract definition, which Word counts as one list), `interleaved` (the continued list has items after the joined ones), `textbox-member`; join: `continueFrom` `after-target`, `not-a-list`, `deleted`, `inside-target`. Nothing was written. | Extend `targetEnd` to `lastListParagraphIndex` or use `scope: 'list'`; start a split at a top-level item; join a list to the list that immediately precedes it. A `LIST_FORMAT_UNCHANGED` warning (`ok`, no change) means the list already has the requested format or already continues `continueFrom`. |
818
+ | `INVALID_OPERATION` with `field: properties.<key>` (error) | `paragraph-format.properties` contained keys outside `alignment`, `keepNext`, `keepLines`, `pageBreakBefore`, `style`, `numbering` (0.9.0; it was the `UNKNOWN_PARAGRAPH_FORMAT_PROPERTY` warning in 0.8.x). Nothing was applied; `properties` lists the rejected keys. | Remove the unsupported key, or use the dedicated operation (`list-format`, `format`). |
819
+ | `PARAGRAPH_NUMBERING_NOT_FOUND` (error) | `paragraph-format` `numbering.numId` is not defined in the document's numbering part (and was not allocated earlier in the batch), its `abstractNum` is missing, or the level is not defined. `reason` is `unknown-num`, `missing-abstract` or `undefined-level`. Nothing was changed. | Inspect a paragraph already in the target list and copy its `list.numId` and `list.level`. A binding never creates a list. |
820
+ | `PARAGRAPH_FORMAT_REVISION_CONFLICT` (error) | The target paragraph already has a pending `w:pPrChange` by another author; overwriting its snapshot would change what Reject All restores. A pending change by the same author keeps its earliest snapshot. | Ask the user to accept or reject the existing formatting change first. |
821
+ | `REVISION_FIDELITY_VIOLATION` | Word's Reject All of the operation's output would not reproduce the source (`violations[].invariant`: `REJECT_PARAGRAPH_COUNT`, `REJECT_PARAGRAPH_TEXT`, `REJECT_PARAGRAPH_PROPS`, `REJECT_NUMBERING_REFS` (also: a bound or restored numId with no `w:num`/`w:abstractNum`/`w:lvl` in the output numbering), `REJECT_WRAPPERS`, `ACCEPT_INTENT` (a plain-text `redline`'s Accept All target does not equal its complete `modified` text); for `generateRedlines: false` operations also `ACCEPT_NEIGHBORS` and `UNTRACKED_CREATED_REVISION`, the latter meaning the untracked edit wrote a tracked revision the source lacked). Refused by default (`enforce`); `verifyRevisionFidelity: 'warn'` / `--revision-fidelity warn` applies and appends the same text to `warnings`. Each violation has `explanation`, `expected`/`actual` excerpts and `source.sourceParagraphIndex` (the 1-based extraction index) with a text excerpt. | Nothing was applied. Follow `recovery.retryGuidance` for the reported invariant: target one paragraph, use `replacements` for literal edits, keep breaks/tabs and hyperlink boundaries, and move formatting or list changes into their own operation. If a narrower retry is also refused, report the operation and `violations`; never disable verification to force it through. For `UNTRACKED_CREATED_REVISION`, retry once with the fresh extraction; if it repeats, report it (or make the edit tracked with `generateRedlines: true` if a reviewable change is acceptable). |
780
822
  | `UNSAFE_DELETED_TABLE_ROW` / `UNSUPPORTED_MOVE_REVISION` / `SECTION_BREAK_PARAGRAPH` / `UNSAFE_PARAGRAPH_PLACEMENT` | Paragraph restoration cannot preserve the source structural boundary safely. | Do not retry as an ordinary redline. Resolve the row/move/section/placement condition manually or narrow the restoration to a safe paragraph. |
781
823
  | `PART_NOT_FOUND` / `PART_AMBIGUOUS` | The `part` selector matched no header/footer, or several. | Re-run `inspect` and use `headersFooters[].path`, or add `section`. |
782
824
  | `FIELD_EDIT_REFUSED` | The edit would change a field (PAGE, NUMPAGES, DATE) in a header or footer. | Edit the text around the field. |
@@ -869,10 +911,10 @@ reconstruct the architecture by repeatedly listing the repository.
869
911
  | `generateRedlines` | `boolean` | `true` | When `true`, emits Word-native tracked changes (`w:ins`/`w:del`). When `false`, applies direct text edits without revision markup. **Note: Redlines are not always the preferred method** — pass `generateRedlines: false` (or `--no-redlines` via CLI) when producing clean execution drafts, restructuring documents, or when revision clutter is unwanted. |
870
912
  | `author` | `string` | `'AI Redliner'` | Reviewer/author name stamped on generated tracked changes and comments. Overridable via `DOCX_REDLINE_AUTHOR` environment variable. |
871
913
  | `atomic` | `boolean` | `false` | Batch transaction mode. By default (`false`), valid edits are applied directly and failing operations report errors. When `true`, any operation failure rolls back the entire batch to the original document state (`rolledBack: true`, `hasChanges: false`). Use `atomic: true` (or `--atomic` in CLI) for high-assurance workflows. |
872
- | `structuredContent` | `boolean` | `true` | Auto-detects Markdown tables, headings (`#`), and lists in replacement text and renders them as native Word elements (`w:tbl`, `w:pStyle`, `w:numPr`). Pass `false` to treat replacement text strictly as plain text. |
914
+ | `structuredContent` | `boolean` | `true` | Auto-detects Markdown tables, headings (`#`), and lists in replacement text and renders them as native Word elements (`w:tbl`, `w:pStyle`, `w:numPr`). Pass `false` to treat replacement text strictly as plain text: inline Markdown markers (`**`, `__`, `_`, `~~`, `++`) are kept literally. With `true`, underscores follow CommonMark flanking rules, so signature lines (`_____`) and `snake_case` stay literal. |
873
915
  | `pairReplacements` | `boolean` | `true` | Links adjacent `<w:del>` and `<w:ins>` revisions with matching timestamps so Word groups them as a single replacement in the Reviewing Pane. |
874
916
  | `strictTargets` | `boolean` | `true` (CLI/facade) | Requires exact target descriptors (`exactText`, `paragraphId`, `index`, `occurrence`, `fingerprint`) and forbids ambiguous matching. Defaults to `false` in low-level runner for backwards compatibility. |
875
- | `existingRevisions` | `string` | `'merge-same-author'` | How to handle paragraphs with existing tracked changes. `'merge-same-author'` merges the same author's work and protects other authors with `EXISTING_REVISIONS`. `'slice-cross-author'` retains same-author merging while allowing Word-native edits inside another author's pending insertion. Pass `'accept-all-first'` to normalize prior revisions or `'reject-input'` to refuse editing revised paragraphs. |
917
+ | `existingRevisions` | `string` | `'merge-same-author'` | How to handle paragraphs with existing tracked changes. `'merge-same-author'` merges the same author's work and protects other authors with `EXISTING_REVISIONS`. `'slice-cross-author'` retains same-author merging while allowing Word-native edits inside another author's pending insertion. Pass `'accept-all-first'` to normalize prior revisions or `'reject-input'` to refuse editing revised paragraphs. `'auto'` chooses per target: `'merge-same-author'` when the target has no tracked changes or only the operation author's, otherwise `'slice-cross-author'` (preflight reports the resolved policy). |
876
918
  | `removeFormatting` | `boolean` | `false` | When `true` and the text is unchanged with no Markdown hints, strips existing bold/italic/underline/strikethrough formatting. |
877
919
  | `sanitizeInput` | `boolean` | `false` | Opt-in removal of standalone leading assistant-preface lines. Literal dollar signs and `\n` sequences are always preserved. |
878
920
 
@@ -963,7 +1005,7 @@ directly into `word/document.xml`.
963
1005
  1. Call `configureXmlProvider` first in Node.js.
964
1006
  2. `applyRedlineToOxml` is async.
965
1007
  3. Paragraph APIs expect paragraph-level OOXML, not full `word/document.xml` in all cases.
966
- 4. List operations may return `numberingXml` that must be merged into package parts. When `word/numbering.xml` already exists, pass `mergeNumberingXmlBySchemaOrder` to `ensureNumberingArtifactsInZip`; without a merge callback the helper replaces the prior payload.
1008
+ 4. List operations may return `numberingXml` that must be merged into package parts. `ensureNumberingArtifactsInZip` always merges them into an existing `word/numbering.xml` (the former `mergeNumberingXml` option is ignored); a conflicting id fails with `NUMBERING_ID_CONFLICT`, so pass `numberingXml` or a shared `numberingIdState` to every call.
967
1009
  That replacement behavior is deprecated and will become an error in the next major version.
968
1010
  5. `useNativeApi: true` means standalone mode cannot fully handle that operation path.
969
1011
  6. `deleteCommentsByAuthorInOoxml` removes definitions and linked anchors only when they are present in the same OOXML payload. In a real `.docx`, `word/comments.xml` and `word/document.xml` are separate parts and must both be updated by the package integration layer.
@@ -1,150 +1,169 @@
1
- # Skill and Harness Authoring Contract
2
-
3
- Use this page to create or update an agent skill, MCP server, or custom harness
4
- for `@ansonlai/docx-redline-js`. Ordinary editing agents should load
5
- [Agent Fast Start](AGENT_FAST_START.md), not this design contract.
6
-
7
- > [!WARNING]
8
- > Before 1.0, every integration must identify and pin its exact tested library
9
- > release and required CLI contract/capabilities. Treat any different release or
10
- > contract as unverified until compatibility tests pass.
11
-
12
- Example declaration for a contract-8 shell integration that uses localized
13
- operations over stdin:
14
-
15
- ```json
16
- {
17
- "testedLibraryRelease": "0.7.1",
18
- "requiredCliContractVersion": 8,
19
- "requiredCapabilities": [
20
- "operations-stdin",
21
- "agent-safety-profile-v2",
22
- "recovery-envelope-v1",
23
- "localized-replacements-v1"
24
- ]
25
- }
26
- ```
27
-
28
- ## Runtime negotiation
29
-
30
- Run `docx-redline version` before the first document operation. Require CLI
31
- contract version 8 and only the capabilities the integration actually uses. Check once per stable
32
- runtime, fail closed when a requirement is absent, and never inspect bundles or
33
- ZIP parts as a fallback.
34
-
35
- Do not expose negotiated but unused capabilities as choices in the editing
36
- prompt. `command-help-v1`, restore shortcuts, speculative apply, and localized
37
- change summaries are optional requirements only for integrations that use them.
38
-
39
- ## Safety invariants
40
-
41
- Every generated integration must:
42
-
43
- - inspect narrowly, retain `exactText`, and target with `paragraphId` plus
44
- `fingerprint`;
45
- - use strict targeting, package validation, and a derived output path;
46
- - never overwrite the source unless `--in-place` is explicitly requested;
47
- - require `completion: true` and a non-null `outputPath` before reporting
48
- success;
49
- - follow `error.recovery.action` and never retry unchanged failed arguments; and
50
- - require explicit authorization before accepting/rejecting another reviewer's
51
- work or deleting comments.
52
-
53
- The CLI computes `completion` from write status, operation outcomes, commit
54
- disposition, and localized accepted-view verification. Preserve the detailed
55
- results for audit and recovery, but do not make the editing agent re-evaluate
56
- those fields during ordinary success handling.
57
-
58
- ## Workflow policy choices
59
-
60
- Keep these exceptions separate from the default workflow:
61
-
62
- | Choice | Default | Override only when |
63
- |---|---|---|
64
- | Transaction | progressive | Partial output is unacceptable (`--atomic`). |
65
- | Existing revisions | `merge-same-author` | Editing inside another reviewer's pending insertion is intended (`slice-cross-author`). |
66
- | Reviewer identity | `AI Redliner` is a valid visible fallback | The host supplies a preferred flag, operation author, or environment value. |
67
- | Output | derived sibling | The caller deliberately selects another destination. |
68
-
69
- Check `effectiveOptions` when an integration deliberately sets a policy.
70
-
71
- ## Transport choices
72
-
73
- A UTF-8 operations file and serializer-backed stdin are peer transports:
74
-
75
- ```bash
76
- docx-redline apply input.docx --operations operations.json --profile agent --output reviewed.docx
77
- node emit-operations.mjs | docx-redline apply input.docx --operations - --profile agent --compact --output reviewed.docx
78
- ```
79
-
80
- Use `JSON.stringify` or a structured tool API. Never demonstrate `echo`, a
81
- heredoc, or shell-interpolated document text. In JavaScript/TypeScript runtimes
82
- (Node.js, browsers, Cloudflare Workers, Deno, Bun, or sandboxed environments like
83
- n8n), an in-process wrapper should call `openDocx(uint8Array)` directly from
84
- `@ansonlai/docx-redline-js` (or `@ansonlai/docx-redline-js/bundle` for zero-dependency
85
- standalone sandboxes) and return the structured result instead of recreating CLI
86
- serialization, ZIP handling, targeting, validation, or rollback.
87
-
88
- ## Ordinary generated workflow
89
-
90
- The first executable editing example in a generated skill must be focused
91
- extraction:
92
-
93
- ```bash
94
- docx-redline extract input.docx --search "renewal notice" --around 3
95
- ```
96
-
97
- Then apply once using the inspected strong target. Use complete `modified` text
98
- for a broad semantic revision or `replacements` for a small literal change in
99
- that target. Keep requested relative wording relative unless the user asks for
100
- a concrete value.
101
-
102
- Follow `selection.nextAfter` when extraction is truncated. Output projections must retain
103
- `selection`, target descriptors, `humanReference`, and revision
104
- cues.
105
-
106
- ### Observed restore branch
107
-
108
- If the user requests restoration, search `--view rejected` first. Otherwise
109
- follow an alternate-view `selection.hint` when returned. Restore only the
110
- inspected strong target. Require `restore-shortcuts-v1` only if the integration
111
- uses inline `--restore`; canonical operations remain available.
112
-
113
- ### Optional speculative execution
114
-
115
- Do not make speculative apply part of an ordinary generated workflow. A harness
116
- may opt into `speculative-search-apply-v1` only when it owns a measured confidence
117
- policy and accepts the recovery cost of a miss. Keep anchor selection,
118
- directional windows, occurrence handling, and exact matching semantics out of
119
- the ordinary editing prompt; link to the knowledge base or CLI help instead.
120
-
121
- ## Recovery contract
122
-
123
- Lead failure handling with these runtime fields:
124
-
125
- 1. `error.recovery.action`
126
- 2. `error.recovery.requiresReinspection`
127
- 3. `error.recovery.requiresUserAuthorization`
128
- 4. `error.recovery.sameArgumentsSafe`
129
-
130
- Use `retryPlan.base` and its operation indexes to select the original or partial
131
- output package. Do not reproduce an error-code decision tree in the skill.
132
-
133
- ## Presentation rules
134
-
135
- Use `humanReference`, `provision`, or `nearestHeading` in completion reports.
136
- Paragraph indexes and `P55`-style references are machine coordinates, not
137
- locations a document user can follow. Keep the exact machine target internally.
138
-
139
- ## Generation checklist
140
-
141
- - Exact tested release, contract 8, and the minimum capability set are pinned.
142
- - Runtime compatibility is checked once.
143
- - The first ordinary command is focused extraction.
144
- - Apply uses the inspected target with `modified` or localized `replacements`.
145
- - Restore appears only as a user-requested or observed-view branch.
146
- - Speculative execution is absent unless the harness owns a measured policy.
147
- - Ordinary success requires `completion: true` and a non-null `outputPath`.
148
- - Recovery, authorization, source immutability, and structured serialization
149
- invariants are preserved.
150
- - Wrappers delegate mutation and OOXML packaging to the library.
1
+ # Skill and Harness Authoring Contract
2
+
3
+ Use this page to create or update an agent skill, MCP server, or custom harness
4
+ for `@ansonlai/docx-redline-js`. Ordinary editing agents should load
5
+ [Agent Fast Start](AGENT_FAST_START.md), not this design contract.
6
+
7
+ > [!WARNING]
8
+ > Before 1.0, every integration must identify and pin its exact tested library
9
+ > release and required CLI contract/capabilities. Treat any different release or
10
+ > contract as unverified until compatibility tests pass.
11
+
12
+ Example declaration for a contract-8 shell integration that uses localized
13
+ operations over stdin:
14
+
15
+ ```json
16
+ {
17
+ "testedLibraryRelease": "0.7.1",
18
+ "requiredCliContractVersion": 8,
19
+ "requiredCapabilities": [
20
+ "operations-stdin",
21
+ "agent-safety-profile-v2",
22
+ "recovery-envelope-v1",
23
+ "localized-replacements-v1"
24
+ ]
25
+ }
26
+ ```
27
+
28
+ ## Runtime negotiation
29
+
30
+ Run `docx-redline version` before the first document operation. Require CLI
31
+ contract version 9 only when the integration sends `list-format` operations (see
32
+ List structure); otherwise contract version 8 suffices. Require
33
+ only the capabilities the integration actually uses. Check once per stable
34
+ runtime, fail closed when a requirement is absent, and never inspect bundles or
35
+ ZIP parts as a fallback.
36
+
37
+ ### List structure (optional)
38
+
39
+ Keep list structure out of an ordinary editing prompt. A wrapper that converts typed headers, changes a list's
40
+ format, or joins lists:
41
+
42
+ - negotiates `list-format-v1` (`convert-plain`) and, only if used, `list-format-change-v1` (`change-format`,
43
+ `join`);
44
+ - without the capability keeps its host path, never a `redline` or `list-change` that retypes markers (that
45
+ warns, or fails under the agent profile, and binds nothing);
46
+ - follows `use_list_format_*` recovery by sending `recovery.suggestedOperation` with the inspected target;
47
+ - sends a changed-text header (`1. Old` -> `1. New`) as a marker-preserving `redline` plus a `list-format` of that
48
+ paragraph in one batch, and splits them on `LIST_FORMAT_COMPOSITION_CONFLICT`.
49
+
50
+ ### Existing-revision policy (optional)
51
+
52
+ Only with `existing-revisions-auto-v1`: `existingRevisions: 'auto'` consents to slicing around other or unattributed authors' changes for the whole batch. Report the resolved policy.
53
+
54
+ Do not expose negotiated but unused capabilities as choices in the editing
55
+ prompt. `command-help-v1`, restore shortcuts, speculative apply, and localized
56
+ change summaries are optional requirements only for integrations that use them.
57
+
58
+ ## Safety invariants
59
+
60
+ Every generated integration must:
61
+
62
+ - inspect narrowly, retain `exactText`, and target with `paragraphId` plus
63
+ `fingerprint`;
64
+ - use strict targeting, package validation, and a derived output path;
65
+ - never overwrite the source unless `--in-place` is explicitly requested;
66
+ - require `completion: true` and a non-null `outputPath` before reporting
67
+ success;
68
+ - follow `error.recovery.action` and never retry unchanged failed arguments; and
69
+ - require explicit authorization before accepting/rejecting another reviewer's
70
+ work or deleting comments.
71
+
72
+ The CLI computes `completion` from write status, operation outcomes, commit
73
+ disposition, and localized accepted-view verification. Preserve the detailed
74
+ results for audit and recovery, but do not make the editing agent re-evaluate
75
+ those fields during ordinary success handling.
76
+
77
+ ## Workflow policy choices
78
+
79
+ Keep these exceptions separate from the default workflow:
80
+
81
+ | Choice | Default | Override only when |
82
+ |---|---|---|
83
+ | Transaction | progressive | Partial output is unacceptable (`--atomic`). |
84
+ | Existing revisions | `merge-same-author` | Editing inside another reviewer's pending insertion is intended (`slice-cross-author`, or `auto` to slice only targets that carry another reviewer's changes; capability `existing-revisions-auto-v1`). |
85
+ | Reviewer identity | `AI Redliner` is a valid visible fallback | The host supplies a preferred flag, operation author, or environment value. |
86
+ | Output | derived sibling | The caller deliberately selects another destination. |
87
+
88
+ Check `effectiveOptions` when an integration deliberately sets a policy.
89
+
90
+ ## Transport choices
91
+
92
+ A UTF-8 operations file and serializer-backed stdin are peer transports:
93
+
94
+ ```bash
95
+ docx-redline apply input.docx --operations operations.json --profile agent --output reviewed.docx
96
+ node emit-operations.mjs | docx-redline apply input.docx --operations - --profile agent --compact --output reviewed.docx
97
+ ```
98
+
99
+ Use `JSON.stringify` or a structured tool API. Never demonstrate `echo`, a
100
+ heredoc, or shell-interpolated document text. In JavaScript/TypeScript runtimes
101
+ (Node.js, browsers, Cloudflare Workers, Deno, Bun, or sandboxed environments like
102
+ n8n), an in-process wrapper should call `openDocx(uint8Array)` directly from
103
+ `@ansonlai/docx-redline-js` (or `@ansonlai/docx-redline-js/bundle` for zero-dependency
104
+ standalone sandboxes) and return the structured result instead of recreating CLI
105
+ serialization, ZIP handling, targeting, validation, or rollback.
106
+
107
+ ## Ordinary generated workflow
108
+
109
+ The first executable editing example in a generated skill must be focused
110
+ extraction:
111
+
112
+ ```bash
113
+ docx-redline extract input.docx --search "renewal notice" --around 3
114
+ ```
115
+
116
+ Then apply once using the inspected strong target. Use complete `modified` text
117
+ for a broad semantic revision or `replacements` for a small literal change in
118
+ that target. Keep requested relative wording relative unless the user asks for
119
+ a concrete value.
120
+
121
+ Follow `selection.nextAfter` when extraction is truncated. Output projections must retain
122
+ `selection`, target descriptors, `humanReference`, and revision
123
+ cues.
124
+
125
+ ### Observed restore branch
126
+
127
+ If the user requests restoration, search `--view rejected` first. Otherwise
128
+ follow an alternate-view `selection.hint` when returned. Restore only the
129
+ inspected strong target. Require `restore-shortcuts-v1` only if the integration
130
+ uses inline `--restore`; canonical operations remain available.
131
+
132
+ ### Optional speculative execution
133
+
134
+ Do not make speculative apply part of an ordinary generated workflow. A harness
135
+ may opt into `speculative-search-apply-v1` only when it owns a measured confidence
136
+ policy and accepts the recovery cost of a miss. Keep anchor selection,
137
+ directional windows, occurrence handling, and exact matching semantics out of
138
+ the ordinary editing prompt; link to the knowledge base or CLI help instead.
139
+
140
+ ## Recovery contract
141
+
142
+ Lead failure handling with these runtime fields:
143
+
144
+ 1. `error.recovery.action`
145
+ 2. `error.recovery.requiresReinspection`
146
+ 3. `error.recovery.requiresUserAuthorization`
147
+ 4. `error.recovery.sameArgumentsSafe`
148
+
149
+ Use `retryPlan.base` and its operation indexes to select the original or partial
150
+ output package. Do not reproduce an error-code decision tree in the skill.
151
+
152
+ ## Presentation rules
153
+
154
+ Use `humanReference`, `provision`, or `nearestHeading` in completion reports.
155
+ Paragraph indexes and `P55`-style references are machine coordinates, not
156
+ locations a document user can follow. Keep the exact machine target internally.
157
+
158
+ ## Generation checklist
159
+
160
+ - Exact tested release, contract 8 (9 only with `list-format`), and the minimum capability set are pinned.
161
+ - Runtime compatibility is checked once.
162
+ - The first ordinary command is focused extraction.
163
+ - Apply uses the inspected target with `modified` or localized `replacements`.
164
+ - Restore appears only as a user-requested or observed-view branch.
165
+ - Speculative execution is absent unless the harness owns a measured policy.
166
+ - Ordinary success requires `completion: true` and a non-null `outputPath`.
167
+ - Recovery, authorization, source immutability, and structured serialization
168
+ invariants are preserved.
169
+ - Wrappers delegate mutation and OOXML packaging to the library.