@ansonlai/docx-redline-js 0.6.0 → 0.6.2

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.
package/AGENTS.md CHANGED
@@ -9,7 +9,7 @@ or an installed plugin bundle to infer public behavior.
9
9
  | Task | Start here |
10
10
  |---|---|
11
11
  | Edit or review a complete `.docx` | [Agent Fast Start](docs/AGENT_FAST_START.md) and the `docx-redline` CLI |
12
- | Build an agent/tool wrapper | [README wrapper example](README.md#example-agent-session-wrapper-development-only), then [knowledge base](docs/AGENT_KNOWLEDGE_BASE.md#designing-a-thin-agent-wrapper) |
12
+ | Build or update an agent skill/tool wrapper | [Skill Authoring Contract](docs/SKILL_AUTHORING.md), then [README wrapper example](README.md#example-agent-session-wrapper-development-only) |
13
13
  | Change paragraph/range reconciliation | `index.js` → `engine/oxml-engine.js` → selected `engine/*-mode.js` |
14
14
  | Change complete-document operations | `services/standalone-operation-runner.js` → `services/document-operation-*.js` |
15
15
  | Change DOCX ZIP or CLI behavior | `node/index.js`, `node/docx-document.js`, `node/cli.js` |
@@ -22,12 +22,13 @@ specific advanced operation, API, or recovery topic you need.
22
22
  ## Ordinary document edits
23
23
 
24
24
  Use one focused extraction and one apply call. With a structured wrapper, use
25
- its revision-bound target handles. For shell-only work, serialize operations to
26
- stdin and use the explicit agent profile:
25
+ its revision-bound target handles. Follow `selection.hint` when a search for
26
+ restorable text yields 0 matches in accepted view. For shell-only work, use a
27
+ UTF-8 operations file or serializer-backed stdin and the explicit agent profile:
27
28
 
28
29
  ```bash
29
- docx-redline extract contract.docx --range 10:30
30
- node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --output reviewed.docx
30
+ docx-redline extract contract.docx --search "termination" --around 3
31
+ node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
31
32
  ```
32
33
 
33
34
  For every ordinary text operation, `modified` is the complete desired
@@ -43,6 +44,10 @@ unchanged failed arguments. Do not accept/reject another reviewer's work or
43
44
  remove comments without explicit user authorization. The source is never
44
45
  overwritten unless `--in-place` is explicit.
45
46
 
47
+ The agent profile preserves progressive execution and the ordinary revision
48
+ policy. Add `--atomic` or `--existing-revisions slice-cross-author` only when
49
+ that policy is intended, and confirm the resolved `effectiveOptions`.
50
+
46
51
  Advanced restore, rejected-view insertion, list, table, formatting, comments,
47
52
  revision policies, and failure examples live in the
48
53
  [knowledge base](docs/AGENT_KNOWLEDGE_BASE.md#agent-document-workflow-cli) and
package/CHANGELOG.md CHANGED
@@ -13,6 +13,9 @@
13
13
 
14
14
  ### New Features
15
15
 
16
+ - **Self-describing and bounded agent discovery (WP-00–WP-02)**: CLI contract version 6 adds command-specific JSON help, canonical redline/comment/restore examples, case-insensitive contextual search with `--around`/`--context`/`-C`, direct-hit pagination through `--limit` and `--after`, and deliberate `--all` opt-out. Unscoped inspection is capped at 20 direct records and a 48 KiB soft budget without truncating any returned `exactText`; extraction now distinguishes matches from context and prioritizes human-facing provision/heading references over machine paragraph ordinals.
17
+ - **Composable agent safety and compact output (WP-03–WP-04)**: CLI contract version 7 replaces the atomic-coupled agent profile capability with `agent-safety-profile-v2`. `--profile agent` now enforces complete-success exits while preserving progressive execution and the ordinary revision policy; `--atomic` and `--existing-revisions` compose explicitly. Compact CLI JSON keeps receipts only in the authoritative top-level array, omits exact-match diagnostics, summarizes successful equivalent-whitespace matches, and supports one-line `--compact` output without changing Node or standalone-runner results.
18
+ - **Packaged skill-authoring contract (WP-05)**: `docs/SKILL_AUTHORING.md` separates automation invariants from transaction/revision policy, file-versus-stdin transport, reviewer preferences, recovery-envelope behavior, and human-facing presentation. It requires contract/capability negotiation, focused contextual extraction, and library delegation while retaining the visible `AI Redliner` fallback without a new author guard.
16
19
  - **Batch-start source binding (WP-04)**: Strong document-operation targets are compiled once against the immutable source DOM and carried through structural index/fingerprint drift with rollback-aware session identities. Independent edits no longer need manual bottom-up sorting; same-source writes, incompatible revision/format combinations, and unsafe capture fan-out fail before mutation with causal conflict codes. Unique references to paragraph text created elsewhere in the same batch become explicit internal capture dependencies.
17
20
  - **Machine-actionable recovery (WP-05)**: Operation, package-facade, example-session, and compact CLI failures now carry recovery envelope version 1 with stable stage/category/action metadata, bounded corrective context, and authorization flags. Failed and partial mutation results include an explicit original-vs-output retry plan and operation indexes. `apply --require-complete` exits with code 3 for partial work while preserving legacy behavior without the flag.
18
21
  - **Compact shell-agent protocol (WP-06)**: CLI contract version 5 adds `operations-stdin` and `agent-profile-v1`. `--operations -` accepts UTF-8 operation arrays/envelopes without a temporary operations file, while explicit `--profile agent` combines atomic rollback and complete-success exit behavior and reports its effective settings. `AGENTS.md` is now a 527-word routing card, and the 349-word `docs/AGENT_FAST_START.md` carries the ordinary edit contract. Ambient project configuration was deliberately deferred because it would save only the profile flag while adding hidden state.
package/README.md CHANGED
@@ -144,10 +144,16 @@ import { inspectDocumentParts } from '@ansonlai/docx-redline-js';
144
144
  const inventory = inspectDocumentParts({ documentXml, commentsXml, numberingXml });
145
145
  ```
146
146
 
147
- Inspection returns exact paragraph text, target IDs/fingerprints, headings,
148
- table/list context, revision authors, and joined comment anchors. Filters such
149
- as `search`, `indexes`, `range`, `revisedOnly`, `inTable`, and `skipEmpty`
150
- limit output. `revisionView` accepts `accepted`, `rejected`, or `current`.
147
+ Inspection returns exact paragraph text, target IDs/fingerprints, headings,
148
+ table/list context, revision authors, and joined comment anchors. Filters such
149
+ as `search`, `indexes`, `range`, `revisedOnly`, `inTable`, and `skipEmpty`
150
+ limit output. Search is a case-insensitive substring match; `around` adds nearby drafting
151
+ context, while `limit` and `after` page direct hits. When a search yields 0
152
+ matches in the active view, `selection.hint` provides guidance if matches exist
153
+ in the alternate revision view (such as searching for deleted text to restore).
154
+ `revisionView` accepts `accepted`, `rejected`, or `current`. Returned
155
+ paragraphs identify matches versus context and include `humanReference` for
156
+ user-facing summaries.
151
157
 
152
158
  For complete `.docx` buffers in Node:
153
159
 
@@ -211,11 +217,11 @@ latency. Checked comparative results are in the
211
217
 
212
218
  ### Agent CLI
213
219
 
214
- ```bash
215
- docx-redline extract contract.docx --range 10:30
216
- docx-redline preflight contract.docx --operations operations.json --author "Editor"
217
- docx-redline apply contract.docx --operations operations.json --author "Editor" --output reviewed.docx
218
- docx-redline validate reviewed.docx
220
+ ```bash
221
+ docx-redline extract contract.docx --search "termination" --around 3
222
+ docx-redline preflight contract.docx --operations operations.json --author "Editor"
223
+ docx-redline apply contract.docx --operations operations.json --author "Editor" --output reviewed.docx
224
+ docx-redline validate reviewed.docx
219
225
  ```
220
226
 
221
227
  ```bash
@@ -232,7 +238,7 @@ docx-redline apply contract.docx --target "Another author's clause" --modified "
232
238
  docx-redline apply contract.docx --operations operations.json --atomic --require-complete --output reviewed.docx
233
239
 
234
240
  # Agent shell path: JSON is emitted by a serializer, not interpolated by the shell
235
- node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --output reviewed.docx
241
+ node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
236
242
  ```
237
243
 
238
244
  All commands emit JSON on stdout. `apply` defaults:
@@ -241,18 +247,36 @@ All commands emit JSON on stdout. `apply` defaults:
241
247
  - **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.
242
248
  - **Transactionality**: Defaults to `atomic: false` (applies valid operations and reports any failures). Pass `--atomic` for all-or-nothing rollback on any operation error.
243
249
  - **Complete-success exit**: Pass `--require-complete` when a progressive `partial` result must exit with code `3`; errors exit with code `2`. Without the flag, partial results retain the legacy zero exit code, so always inspect `completion`.
244
- - **Agent profile**: `--profile agent` explicitly enables atomic rollback and complete-success exit behavior while retaining strict targeting, validation, tracked changes, and `merge-same-author`. It reports the resolved `effectiveOptions`; explicit flags take precedence.
250
+ - **Agent profile**: `--profile agent` enables complete-success exit behavior while retaining the ordinary progressive transaction and existing-revision defaults. Compose it with `--atomic` or an explicit `--existing-revisions` policy when intended, and verify the resolved `effectiveOptions`.
245
251
  - **Operations from stdin**: `--operations -` reads the same array or `{ operations, expectedRevision }` envelope accepted from a file. Feed it from a JSON serializer or structured process API, not shell-interpolated legal text.
252
+ - **Compact stdout**: `--compact` emits one-line mutation JSON. It changes serialization only, not operation semantics.
246
253
  - **Tracked changes**: Defaults to `generateRedlines: true`. Pass `--no-redlines` when clean direct text edits are desired.
247
254
  - **Inline edits**: Use `--target <text>` with `--modified <text>` or `--comment <text>` for quick one-liners without creating a JSON file.
248
- - **Compact mutation results**: `apply`, `accept`, `reject`, and `delete-comments` omit full OOXML/package payloads and inspection text from stdout. They report `written`, `outputPath`, per-operation results and receipts, compact validation counts, and a derived `completion` boolean. Use `validate` when full issue arrays are needed.
255
+ - **Compact mutation results**: `apply`, `accept`, `reject`, and `delete-comments` omit full OOXML/package payloads and inspection text from stdout. CLI operation results omit duplicate nested receipt bodies; the ordered top-level `receipts` array is authoritative. Successful exact target-match diagnostics are omitted and equivalent-whitespace matches retain only mode/count. Node and standalone-runner results remain unchanged. Use `validate` when full issue arrays are needed.
249
256
 
250
- `docx-redline version` reports contract version 5 and the additive
251
- `batch-start-source-binding`, `recovery-envelope-v1`, and
252
- `require-complete-exit`, `operations-stdin`, and `agent-profile-v1`
253
- capabilities. Wrappers should negotiate only the capabilities they use.
254
-
255
- See the [compact agent fast start](./docs/AGENT_FAST_START.md) and the
257
+ Inspection commands default to 20 direct matches and a 48 KiB soft response
258
+ budget when no explicit positional scope is supplied. When a search returns 0
259
+ matches in the requested view, `selection.hint` provides immediate guidance if
260
+ matches exist in the alternate view (such as searching for a deleted clause to
261
+ restore). Use `--limit` with `--after <paragraph-index>` to continue, `--around N`
262
+ (aliases `--context` and `-C`) to include nearby paragraphs, and `--all` only
263
+ for deliberate unbounded inspection. Every retained target is complete; an
264
+ individually oversized paragraph is returned whole with an `oversizeItem`
265
+ marker. Machine `index` and `ref` fields are for targeting and pagination, not
266
+ user-facing Word locations; use `humanReference`, `provision`, or
267
+ `nearestHeading` in reports.
268
+
269
+ `docx-redline version` reports contract version 7 and the additive
270
+ `command-help-v1`, `inspection-context-v1`, `bounded-inspection-v1`,
271
+ `human-document-references-v1`, `batch-start-source-binding`,
272
+ `recovery-envelope-v1`, `require-complete-exit`, `operations-stdin`,
273
+ `agent-safety-profile-v2`, `deduplicated-cli-receipts`, and
274
+ `compact-cli-json-v1` capabilities. Wrappers should negotiate only the
275
+ capabilities they use. Run `docx-redline <command> --help` for that command's
276
+ machine-readable options, behavior, exit codes, canonical GitHub documentation links, and compact examples.
277
+
278
+ See the [compact agent fast start](./docs/AGENT_FAST_START.md), the
279
+ [skill/harness authoring contract](./docs/SKILL_AUTHORING.md), and the
256
280
  [operation JSON Schema](docs/schemas/document-operations.schema.json).
257
281
 
258
282
  ### Configuration (call once at startup)
@@ -554,10 +578,14 @@ guidance. Callers should pass `{ strictTargets: true }` and use strict descripto
554
578
 
555
579
  ### Mutation Receipts
556
580
 
557
- Both single-operation (`applyOperationToDocumentXml`) and batch
558
- (`applyOperationsToDocumentXml`) results expose commit-aware **Mutation Receipts**
559
- (`result.receipt` on single results and per-item `results[i].receipt`, plus `result.receipts`
560
- for the full batch).
581
+ Both single-operation (`applyOperationToDocumentXml`) and batch
582
+ (`applyOperationsToDocumentXml`) results expose commit-aware **Mutation Receipts**
583
+ (`result.receipt` on single results and per-item `results[i].receipt`, plus `result.receipts`
584
+ for the full batch).
585
+
586
+ This full receipt shape is retained by the Node facade and standalone runner.
587
+ Only compact CLI JSON removes duplicate `results[i].receipt` bodies and keeps
588
+ the ordered top-level `receipts` array.
561
589
 
562
590
  ```js
563
591
  const result = await applyOperationsToDocumentXml(documentXml, operations, 'Agent');
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.6.0 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.6.2 — https://github.com/AnsonLai/docx-redline-js
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -11466,6 +11466,70 @@ function collectDocumentCommentAnchors(paragraphNodes, revisionView) {
11466
11466
  });
11467
11467
  return anchors;
11468
11468
  }
11469
+ function selectInspectionParagraphs(allParagraphs, options) {
11470
+ let matches = allParagraphs;
11471
+ if (options.revisedOnly) matches = matches.filter((item) => item.hasRevisions);
11472
+ if (options.inTable != null) matches = matches.filter((item) => item.inTable === !!options.inTable);
11473
+ if (options.skipEmpty) matches = matches.filter((item) => item.text.length > 0);
11474
+ if (options.search) {
11475
+ const needle = String(options.search).toLowerCase();
11476
+ matches = matches.filter((item) => item.text.toLowerCase().includes(needle));
11477
+ }
11478
+ if (Array.isArray(options.indexes)) {
11479
+ const indexes = new Set(options.indexes);
11480
+ matches = matches.filter((item) => indexes.has(item.index));
11481
+ }
11482
+ let rangeStart = 1;
11483
+ let rangeEnd = allParagraphs.length;
11484
+ if (options.range) {
11485
+ rangeStart = Number(options.range.start ?? options.range[0]);
11486
+ rangeEnd = Number(options.range.end ?? options.range[1]);
11487
+ matches = matches.filter((item) => item.index >= rangeStart && item.index <= rangeEnd);
11488
+ }
11489
+ const totalMatches = matches.length;
11490
+ const after = Number.isInteger(options.after) && options.after > 0 ? options.after : null;
11491
+ const remaining = after == null ? matches : matches.filter((item) => item.index > after);
11492
+ const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : null;
11493
+ const selectedMatches = limit == null ? remaining : remaining.slice(0, limit);
11494
+ const truncated = selectedMatches.length < remaining.length;
11495
+ const around = Number.isInteger(options.around) && options.around > 0 ? options.around : 0;
11496
+ const exposeSelection = !!options.search || around > 0 || limit != null || after != null;
11497
+ let paragraphs = selectedMatches;
11498
+ if (around > 0 && options.search) {
11499
+ const directIndexes = new Set(selectedMatches.map((item) => item.index));
11500
+ const contextFor = /* @__PURE__ */ new Map();
11501
+ for (const match of selectedMatches) {
11502
+ const start = Math.max(rangeStart, match.index - around);
11503
+ const end = Math.min(rangeEnd, match.index + around);
11504
+ for (let index = start; index <= end; index++) {
11505
+ if (directIndexes.has(index)) continue;
11506
+ const owners = contextFor.get(index) || [];
11507
+ owners.push(match.index);
11508
+ contextFor.set(index, owners);
11509
+ }
11510
+ }
11511
+ const returnedIndexes = /* @__PURE__ */ new Set([...directIndexes, ...contextFor.keys()]);
11512
+ paragraphs = allParagraphs.filter((item) => returnedIndexes.has(item.index)).map((item) => directIndexes.has(item.index) ? { ...item, selectionRole: "match" } : { ...item, selectionRole: "context", contextFor: contextFor.get(item.index) || [] });
11513
+ } else if (exposeSelection) {
11514
+ paragraphs = selectedMatches.map((item) => ({ ...item, selectionRole: "match" }));
11515
+ }
11516
+ return {
11517
+ paragraphs,
11518
+ ...exposeSelection ? {
11519
+ selection: {
11520
+ ...options.search ? { search: String(options.search), caseSensitive: false } : {},
11521
+ totalMatches,
11522
+ returnedMatches: selectedMatches.length,
11523
+ returnedParagraphs: paragraphs.length,
11524
+ truncated,
11525
+ nextAfter: truncated && selectedMatches.length > 0 ? selectedMatches[selectedMatches.length - 1].index : null,
11526
+ ...limit != null ? { limit } : {},
11527
+ ...after != null ? { after } : {},
11528
+ ...around > 0 ? { around } : {}
11529
+ }
11530
+ } : {}
11531
+ };
11532
+ }
11469
11533
  function inspectDocumentParts(parts, options = {}) {
11470
11534
  const documentPart = parseXml2(parts?.documentXml, "word/document.xml", true);
11471
11535
  if (documentPart.error) return { status: "error", error: documentPart.error, paragraphs: [], comments: [], warnings: [] };
@@ -11495,7 +11559,8 @@ function inspectDocumentParts(parts, options = {}) {
11495
11559
  const index = zeroIndex + 1;
11496
11560
  const provision = list?.label && list.format !== "bullet" ? list.label : null;
11497
11561
  const headingText = nearestHeading?.text || null;
11498
- const humanReference = [provision, headingText, text.slice(0, options.excerptLength || 120)].filter(Boolean).join(" \u2014 ");
11562
+ const excerpt = text.slice(0, options.excerptLength || 120);
11563
+ const humanReference = level ? text : provision ? [provision, headingText].filter(Boolean).join(" \u2014 ") : headingText ? [headingText, excerpt].filter(Boolean).join(" \u2014 ") : excerpt;
11499
11564
  const segments = extractParagraphRevisionSegments(paragraph);
11500
11565
  return {
11501
11566
  index,
@@ -11505,8 +11570,9 @@ function inspectDocumentParts(parts, options = {}) {
11505
11570
  revisionView,
11506
11571
  text,
11507
11572
  exactText: text,
11508
- excerpt: text.slice(0, options.excerptLength || 120),
11573
+ excerpt,
11509
11574
  humanReference,
11575
+ provision,
11510
11576
  inTable: hasAncestor(paragraph, "tc"),
11511
11577
  table: structure.table,
11512
11578
  styleId,
@@ -11527,22 +11593,36 @@ function inspectDocumentParts(parts, options = {}) {
11527
11593
  definition.anchoredText ?? (definition.anchoredText = commentAnchors.get(id) || paragraph.text);
11528
11594
  comments.set(id, definition);
11529
11595
  }
11530
- if (options.revisedOnly) paragraphs = paragraphs.filter((item) => item.hasRevisions);
11531
- if (options.inTable != null) paragraphs = paragraphs.filter((item) => item.inTable === !!options.inTable);
11532
- if (options.skipEmpty) paragraphs = paragraphs.filter((item) => item.text.length > 0);
11533
- if (options.search) {
11596
+ const selected = selectInspectionParagraphs(paragraphs, options);
11597
+ if (options.search && selected.selection && selected.selection.totalMatches === 0) {
11534
11598
  const needle = String(options.search).toLowerCase();
11535
- paragraphs = paragraphs.filter((item) => item.text.toLowerCase().includes(needle));
11536
- }
11537
- if (Array.isArray(options.indexes)) {
11538
- const indexes = new Set(options.indexes);
11539
- paragraphs = paragraphs.filter((item) => indexes.has(item.index));
11540
- }
11541
- if (options.range) {
11542
- const start = Number(options.range.start ?? options.range[0]);
11543
- const end = Number(options.range.end ?? options.range[1]);
11544
- paragraphs = paragraphs.filter((item) => item.index >= start && item.index <= end);
11545
- }
11599
+ const alternateView = revisionView === "accepted" ? "rejected" : "accepted";
11600
+ let rangeStart = 1;
11601
+ let rangeEnd = paragraphNodes.length;
11602
+ if (options.range) {
11603
+ rangeStart = Number(options.range.start ?? options.range[0]);
11604
+ rangeEnd = Number(options.range.end ?? options.range[1]);
11605
+ }
11606
+ let alternateMatches = 0;
11607
+ for (let i = 0; i < paragraphNodes.length; i++) {
11608
+ const index = i + 1;
11609
+ if (options.range && (index < rangeStart || index > rangeEnd)) continue;
11610
+ if (Array.isArray(options.indexes) && !options.indexes.includes(index)) continue;
11611
+ const pNode = paragraphNodes[i];
11612
+ if (options.table && !hasAncestor(pNode, "tc")) continue;
11613
+ if (options.body && hasAncestor(pNode, "tc")) continue;
11614
+ if (options.revised && revisionAuthors(pNode).length === 0) continue;
11615
+ const altText = extractCanonicalParagraphText(pNode, { revisionView: alternateView });
11616
+ if (options.skipEmpty && altText.length === 0) continue;
11617
+ if (altText.toLowerCase().includes(needle)) {
11618
+ alternateMatches++;
11619
+ }
11620
+ }
11621
+ if (alternateMatches > 0) {
11622
+ selected.selection.hint = revisionView === "accepted" ? `0 matches in accepted view, but ${alternateMatches} match${alternateMatches === 1 ? "" : "es"} found in --view rejected. If searching for deleted text to restore, re-run with --view rejected.` : `0 matches in rejected view, but ${alternateMatches} match${alternateMatches === 1 ? "" : "es"} found in --view accepted. Re-run with --view accepted.`;
11623
+ }
11624
+ }
11625
+ paragraphs = selected.paragraphs;
11546
11626
  const allRevisionAuthors = [...new Set(paragraphs.flatMap((item) => item.revisionAuthors))].sort();
11547
11627
  const coveredEntries = extractDocumentPartsEntries(parts);
11548
11628
  const coveredParts = coveredEntries.map((e) => e.name).sort();
@@ -11559,6 +11639,7 @@ function inspectDocumentParts(parts, options = {}) {
11559
11639
  revisionToken,
11560
11640
  coveredParts,
11561
11641
  paragraphs,
11642
+ ...selected.selection ? { selection: selected.selection } : {},
11562
11643
  comments: [...comments.values()],
11563
11644
  revisionAuthors: allRevisionAuthors,
11564
11645
  commentAuthors: [...new Set([...comments.values()].map((item) => item.author).filter(Boolean))].sort(),