@ansonlai/docx-redline-js 0.8.0 → 0.8.1

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 (40) hide show
  1. package/CHANGELOG.md +539 -486
  2. package/README.md +331 -309
  3. package/dist/docx-redline-js.esm.js +1083 -163
  4. package/dist/docx-redline-js.esm.js.map +4 -4
  5. package/dist/docx-redline-js.esm.min.js +107 -107
  6. package/dist/docx-redline-js.esm.min.js.map +4 -4
  7. package/dist/docx-redline.bundle.cjs +1083 -163
  8. package/dist/docx-redline.bundle.cjs.map +4 -4
  9. package/dist/docx-redline.bundle.js +1083 -163
  10. package/dist/docx-redline.bundle.js.map +4 -4
  11. package/docs/AGENT_KNOWLEDGE_BASE.md +69 -8
  12. package/docs/schemas/document-operations.schema.json +29 -0
  13. package/document/docx-document.js +416 -22
  14. package/engine/reconstruction-writer.js +18 -0
  15. package/index.d.ts +598 -575
  16. package/index.js +349 -348
  17. package/node/cli-help.js +5 -5
  18. package/node/cli.js +760 -754
  19. package/node/index.d.ts +1 -1
  20. package/package.json +2 -1
  21. package/scripts/generate-word-comment-edge-case-fixture.ps1 +58 -0
  22. package/scripts/generate-word-comment-thread-fixture.ps1 +36 -0
  23. package/scripts/generate-word-header-footer-fixture.ps1 +32 -0
  24. package/scripts/generate-word-multi-section-fixture.ps1 +27 -0
  25. package/scripts/generate-word-package-integrity-cases.mjs +141 -0
  26. package/scripts/generate-word-thread-state-fixtures.ps1 +50 -0
  27. package/scripts/word-com-package-integrity.ps1 +98 -0
  28. package/services/batch-operation-orchestrator.js +262 -259
  29. package/services/comment-replies.js +96 -95
  30. package/services/comment-thread-parts.js +280 -0
  31. package/services/document-inspection.js +366 -365
  32. package/services/document-operation-applier.js +230 -202
  33. package/services/document-operation-contract.js +17 -2
  34. package/services/error-recovery.js +6 -0
  35. package/services/headers-footers.js +206 -0
  36. package/services/operation-batch-compiler.js +1 -0
  37. package/services/operation-preflight.js +231 -218
  38. package/services/package-parts.js +80 -0
  39. package/services/standalone-docx-plumbing.js +631 -569
  40. package/services/standalone-operation-runner.d.ts +19 -3
@@ -399,7 +399,7 @@ algorithms:
399
399
  - Shell/file wrappers invoke the `docx-redline` CLI and preserve its JSON stdout
400
400
  and exit code.
401
401
  - Node byte-oriented wrappers use `openDocx`, `inspect`, `applyOperations`,
402
- `resolveRevisions`, `deleteComments`, and `toBuffer`.
402
+ `resolveRevisions`, `deleteComments`, `resolveComment`, and `toBuffer`.
403
403
  - XML-only hosts use the standalone runner and remain responsible for package
404
404
  artifacts returned beside `documentXml`.
405
405
  - Paragraph/range hosts use root exports and remain responsible for deciding how
@@ -609,7 +609,9 @@ normalize or reconstruct `exactText`. Operation files follow
609
609
  - `preflight` checks targets, anchors, revisions, conflicts, authors, and needed artifacts without mutation (read-only).
610
610
  - `apply` applies an operation file transactionally with automatic rollback and internal markup validation.
611
611
  - `accept` and `reject` resolve revisions selected by `--author` or `--all-authors`.
612
- - `delete-comments` removes matching definitions and document anchors together.
612
+ - `delete-comments` removes matching definitions and document anchors together, selected by `--author`,
613
+ `--all-authors`, or `--comment-id <id[,id]>` (ids from `inspect`). Deleting a thread root also deletes its
614
+ replies; unknown ids fail with `COMMENT_NOT_FOUND`.
613
615
  - A whole-paragraph delete stops with `COMMENTED_CONTENT_DELETE` when the
614
616
  paragraph has an existing comment. Surface the returned reviewer and comment
615
617
  text for human follow-up; do not silently convert this into comment removal.
@@ -656,8 +658,57 @@ To reply inside an existing Word comment thread, use the comment ID returned by
656
658
  { "type": "comment_reply", "parentCommentId": "8", "commentContent": "Agreed; updated.", "author": "Editor" }
657
659
  ```
658
660
 
659
- Replies are represented in `word/commentsExtended.xml` and deliberately add no
660
- new `commentRangeStart`, `commentRangeEnd`, or `commentReference` to the body.
661
+ A reply is defined in `word/comments.xml` and linked to its thread in `word/commentsExtended.xml`
662
+ (`w15:paraIdParent`, keyed on the last paragraph of each comment, as Word does). Word only displays a
663
+ reply that also has its own `commentRangeStart`, `commentRangeEnd` and `commentReference` around the same
664
+ range as its thread, so the operation adds those markers next to the thread's existing ones. A reply to a
665
+ reply joins the root thread. If the parent has no anchor in the body the operation fails with
666
+ `PARENT_ANCHOR_NOT_FOUND`.
667
+
668
+ #### Headers and footers
669
+
670
+ `inspect().headersFooters` lists every header/footer part the document uses, with its paragraph text:
671
+ `path`, `kind`, `type` (`default`/`first`/`even`), `appliesToSections`, `active`, `hasFields`. Add `part` to an
672
+ operation to edit one instead of the body; everything else in the operation is unchanged:
673
+
674
+ ```json
675
+ { "type": "replace", "part": { "kind": "footer", "type": "default", "section": 0 },
676
+ "target": { "exactText": "Draft v1" }, "modified": "Draft v2" }
677
+ ```
678
+
679
+ `part` is a selector (`kind`, optional `type` defaulting to `default`, optional zero-based `section`) or a part path such as
680
+ `word/footer2.xml`. Omitting `part` always targets the body; headers and footers are opt-in. Rules:
681
+
682
+ - A footer or header shared by several sections is one part; the result lists `partSections`. A section with no reference of its
683
+ own inherits the previous section's part (Word's "link to previous"), and `section` finds it.
684
+ - Several distinct parts matching a selector fail with `PART_AMBIGUOUS` (add `section` or use the path); none fail with
685
+ `PART_NOT_FOUND`. A bad selector rejects the whole request before anything is written.
686
+ - Fields are atomic. An edit that would change a `PAGE`/`NUMPAGES`/`DATE` field's text fails with `FIELD_EDIT_REFUSED`; edit the
687
+ text around the field instead.
688
+ - `comment`, `comment_reply` and `comment_resolve` with `part` fail with `COMMENT_IN_HEADER_FOOTER` (Word has no comments in headers or footers).
689
+ - Body and part operations can share a batch; results keep the original operation indexes and carry `part`. Atomic batches roll
690
+ back across body and parts. Revision ids stay unique across the whole package.
691
+ - `accept` / `reject` (API and CLI) also resolve tracked changes inside headers and footers.
692
+ - Creating a header/footer that does not exist is not supported.
693
+ - `exactText` in a header or footer is the whole paragraph text as `headersFooters[].paragraphs` shows it, including the
694
+ current result of any field (for example `Page 1`).
695
+
696
+ #### Resolving comment threads
697
+
698
+ Resolve (or reopen with `"resolved": false`) the thread containing a comment id from `inspect`:
699
+
700
+ ```json
701
+ { "type": "comment_resolve", "commentId": "8" }
702
+ ```
703
+
704
+ The API form is `await doc.resolveComment(commentId, { resolved })`. Word resolves whole threads, so any id in
705
+ the thread updates the root and every reply together; a reply added to a resolved thread is written resolved,
706
+ as Word does. Only `word/commentsExtended.xml` changes. Resolving an already-resolved thread is `no_change`.
707
+ Unknown ids fail with `COMMENT_NOT_FOUND`, in `preflight` as well as `apply`. Operations run in batch order, so
708
+ `comment_reply` followed by `comment_resolve` closes the thread including the new reply. Like any comment
709
+ operation it is refused with `part`.
710
+
711
+ Resolving and deleting other reviewers' comments are review decisions: do them only when the user asked.
661
712
 
662
713
  #### Legacy skill wrapper migration
663
714
 
@@ -724,6 +775,11 @@ When the CLI or runner returns an error code, follow these specific recovery act
724
775
  | `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. |
725
776
  | `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. |
726
777
  | `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. |
778
+ | `PART_NOT_FOUND` / `PART_AMBIGUOUS` | The `part` selector matched no header/footer, or several. | Re-run `inspect` and use `headersFooters[].path`, or add `section`. |
779
+ | `FIELD_EDIT_REFUSED` | The edit would change a field (PAGE, NUMPAGES, DATE) in a header or footer. | Edit the text around the field. |
780
+ | `COMMENT_IN_HEADER_FOOTER` | A comment operation carried `part`. | Comment on body text instead. |
781
+ | `COMMENT_NOT_FOUND` | `comment_resolve` or `delete-comments --comment-id` named an id that is not in the document. | Re-run `inspect` and use `comments[].id`. |
782
+ | `PARENT_COMMENT_NOT_FOUND` / `PARENT_ANCHOR_NOT_FOUND` | A reply's parent id is unknown, or the parent has no anchor in the body. | Re-run `inspect`; reply to a comment that is anchored in the body. |
727
783
  | `COMMENTED_CONTENT_MERGE` / `COMMENTED_CONTENT_DELETE` | Operation would overwrite, revert, or delete content with comments. | Fails closed to prevent orphaned comment threads. Report the comment author and text to the user; resolve the comment before re-editing. |
728
784
  | `INVALID_OPERATION` | Operation object violates schema or has incompatible fields. | Validate the JSON structure against [`document-operations.schema.json`](schemas/document-operations.schema.json) before targeting is attempted. |
729
785
  | `STRUCTURED_CONTENT_INVALID` | Malformed Markdown table or structure in replacement text. | Ensure tables include a separator row (`\| --- \| --- \|`) and consistent column counts; do not downgrade to raw text. |
@@ -732,11 +788,16 @@ When the CLI or runner returns an error code, follow these specific recovery act
732
788
 
733
789
  #### Document Scope & Boundary Invariants
734
790
 
735
- The `docx-redline` engine and CLI operate specifically on the **main document body**:
791
+ The `docx-redline` engine and CLI operate on the **main document body** and, when an operation sets `part`, on
792
+ **existing header and footer parts**:
736
793
 
737
- - **Supported Content**: Body paragraphs, numbered/bulleted lists, tables and table cells, comments, and comment replies.
738
- - **Unsupported Content**: Headers, footers, footnotes, endnotes, floating text boxes, shape drawings, watermarks, and embedded macros.
739
- - Do not attempt to target, edit, or comment on header/footer text or footnote citations using `docx-redline`. Use specialized document manipulation tools or manual editing for layout frames outside the body text.
794
+ - **Supported Content**: Body paragraphs, numbered/bulleted lists, tables and table cells, comments, comment replies,
795
+ resolving/reopening comment threads, and text in existing headers and footers (see *Headers and footers*).
796
+ - **Unsupported Content**: Footnotes, endnotes, floating text boxes, shape drawings, images, watermarks, embedded macros,
797
+ creating a header or footer that does not exist, editing field codes or results (`PAGE`, `NUMPAGES`, `DATE`), and
798
+ comments in headers or footers (Word has none).
799
+ - Do not attempt to target footnote citations or text boxes with `docx-redline`. Header/footer text is edited only with
800
+ an explicit `part`; body searches never match it.
740
801
 
741
802
  ### Convert paragraph text into a Word list
742
803
 
@@ -71,6 +71,22 @@
71
71
  }
72
72
  ]
73
73
  },
74
+ "headerFooterPart": {
75
+ "description": "Targets a header or footer instead of the body. Use a part path from inspect().headersFooters, or a selector. Comments are not supported in headers or footers.",
76
+ "oneOf": [
77
+ { "type": "string", "minLength": 1, "examples": ["word/footer2.xml"] },
78
+ {
79
+ "type": "object",
80
+ "required": ["kind"],
81
+ "properties": {
82
+ "kind": { "enum": ["header", "footer"] },
83
+ "type": { "enum": ["default", "first", "even"], "description": "Defaults to default." },
84
+ "section": { "type": "integer", "minimum": 0, "description": "Zero-based section; required when several sections have their own part." }
85
+ },
86
+ "additionalProperties": false
87
+ }
88
+ ]
89
+ },
74
90
  "base": {
75
91
  "type": "object",
76
92
  "required": ["type", "target"],
@@ -78,6 +94,7 @@
78
94
  "operationId": { "type": "string", "minLength": 1, "maxLength": 256 },
79
95
  "captureKey": { "type": "string", "minLength": 1, "maxLength": 256 },
80
96
  "target": { "$ref": "#/$defs/target" }, "targetRef": { "type": ["integer", "string", "null"] },
97
+ "part": { "$ref": "#/$defs/headerFooterPart" },
81
98
  "author": { "type": "string", "minLength": 1 }, "generateRedlines": { "type": "boolean" },
82
99
  "existingRevisions": { "enum": ["merge-same-author", "slice-cross-author", "reject-input", "accept-all-first", "accept-all-first-keep-normalized"] },
83
100
  "pairReplacements": { "type": "boolean" },
@@ -109,6 +126,18 @@
109
126
  },
110
127
  "additionalProperties": false
111
128
  },
129
+ {
130
+ "type": "object",
131
+ "description": "Resolve or reopen the comment thread containing commentId. Resolved is thread-level in Word, so the root and every reply change together. Body-only; not allowed with part.",
132
+ "required": ["type", "commentId"],
133
+ "properties": {
134
+ "type": { "const": "comment_resolve" },
135
+ "operationId": { "type": "string", "minLength": 1, "maxLength": 256 },
136
+ "commentId": { "type": ["integer", "string"] },
137
+ "resolved": { "type": "boolean", "default": true }
138
+ },
139
+ "additionalProperties": false
140
+ },
112
141
  {
113
142
  "allOf": [ { "$ref": "#/$defs/base" }, { "type": "object", "required": ["replacements"], "properties": { "type": { "enum": ["redline", "replace"] }, "replacements": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/localizedReplacement" } } } } ]
114
143
  },