@ansonlai/docx-redline-js 0.7.1 → 0.7.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
@@ -21,53 +21,39 @@ specific advanced operation, API, or recovery topic you need.
21
21
 
22
22
  ## Ordinary document edits
23
23
 
24
- For a literal mechanical edit with known old and new text, try one fail-closed
25
- apply call. When global matching is too broad, use a longer, fairly unique
26
- nearby phrase plus a directional range:
27
-
28
- ```bash
29
- docx-redline apply contract.docx --search "distinctive nearby heading or phrase" --context-range 1:3 --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
30
- ```
31
-
32
- Generic or repeated anchors widen the unioned scope and may return
33
- `AMBIGUOUS_TARGET`, costing a recovery turn. On success, treat
34
- `results[i].change.context.anchorMatchCount > 1` as a signal to confirm the
35
- returned location and excerpts.
36
-
37
- For semantic drafting, inspect once and apply once. With a structured wrapper,
38
- use revision-bound handles. For shell-only work, use serializer-backed stdin:
24
+ Use one focused extraction and one apply call. With a structured wrapper, use
25
+ its revision-bound target handles. For shell-only work, use serializer-backed
26
+ stdin and the explicit agent profile:
39
27
 
40
28
  ```bash
41
29
  docx-redline extract contract.docx --search "termination" --around 3
42
30
  node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
43
31
  ```
44
32
 
45
- A single deleted paragraph found in rejected view can be restored verbatim with
46
- `apply --restore --target-id ID`; add one `--find`/`--replace` pair for a small
47
- exact correction. This shortcut does not infer ranges or companion paragraphs.
48
-
49
- For every ordinary text operation, `modified` is the complete desired
50
- accepted-view content. Copy inspected `exactText` verbatim and include
51
- `paragraphId` or `fingerprint`. Independent strong targets are bound against the
52
- batch-start document, so do not manually sort around structural edits.
53
- Consolidate incompatible writes to the same source; use captures for intentional
54
- created-content dependencies.
55
-
56
- Require `completion: true`, `written: true`, a non-null output path, and no
57
- per-operation errors. Localized edits also require
58
- `results[i].change.committed: true`, `finalDisposition: "applied"`, and
59
- `verification.acceptedViewMatchesCompiledText: true`. Follow
33
+ Retain inspected `exactText` for drafting and target with its compact
34
+ `paragraphId` plus `fingerprint`. Use complete `modified` text for a broad
35
+ semantic revision; for a small literal change, use `replacements` so the apply
36
+ payload need not repeat the whole paragraph. Independent strong targets bind to the batch start, so do
37
+ not manually sort around structural edits. Consolidate incompatible writes to
38
+ one source; use captures only for intentional created-content dependencies.
39
+
40
+ If the user requests restoration, search `--view rejected` directly. Otherwise
41
+ follow `selection.hint` if extraction identifies an alternate revision view.
42
+ Restore only the inspected strong target; do not infer adjacent content.
43
+
44
+ Require `completion: true` and a non-null output path. Follow
60
45
  `error.recovery.action` and `retryPlan`; never retry unchanged failed arguments.
61
- Follow `selection.hint` for restorable text. Do not accept/reject another
62
- reviewer's work or remove comments without explicit user authorization. The
63
- source is never overwritten unless `--in-place` is explicit.
46
+ Do not accept/reject another reviewer's work or remove comments without explicit
47
+ user authorization. The source is never overwritten unless `--in-place` is
48
+ explicit.
64
49
 
65
50
  The agent profile preserves progressive execution and the ordinary revision
66
51
  policy. Add `--atomic` or `--existing-revisions slice-cross-author` only when
67
52
  that policy is intended, and confirm the resolved `effectiveOptions`.
68
53
 
69
- Advanced restore, rejected-view insertion, list, table, formatting, comments,
70
- revision policies, and failure examples live in the
54
+ Speculative apply, directional search ranges, advanced restore, rejected-view
55
+ insertion, list, table, formatting, comments, revision policies, and failure
56
+ examples live in the
71
57
  [knowledge base](docs/AGENT_KNOWLEDGE_BASE.md#agent-document-workflow-cli) and
72
58
  [operation schema](docs/schemas/document-operations.schema.json).
73
59
 
@@ -82,10 +68,8 @@ targeting, revision allocation, comments, numbering, validation, or rollback.
82
68
  The stateful wrapper in `examples/agent-session-wrapper.mjs` is a testable
83
69
  development demonstration only. It is excluded from package files and exports.
84
70
  Production harnesses own their transport and negotiate the minimum CLI
85
- `contractVersion`/capabilities they use. The 0.7.0 localized fast path requires
86
- contract 8 and its localized replacement, speculative apply, and change-summary
87
- capabilities; the narrow restore shortcut additionally requires
88
- `restore-shortcuts-v1`.
71
+ `contractVersion` and capabilities they actually use. Optional capabilities do
72
+ not belong in an ordinary editing prompt merely because the CLI supports them.
89
73
 
90
74
  ## Code map
91
75
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Localized replacement matching now tries exact case-sensitive text first,
6
+ then a narrow ASCII-space/non-breaking-space equivalent match. Ambiguous
7
+ equivalent matches still fail closed, and unchanged boundary NBSPs are
8
+ retained to avoid incidental whitespace redlines.
9
+
5
10
  - Added a narrow single-paragraph restore shortcut. A restore with a strong
6
11
  rejected-view target may omit `modified` for verbatim restoration or provide
7
12
  exact `replacements`; the CLI exposes the same path through `apply --restore
package/README.md CHANGED
@@ -16,18 +16,16 @@ Converts AI-generated or programmatic text/markdown edits into valid Office Open
16
16
  - Highlights: apply highlight colors to runs
17
17
  - Markdown and OOXML conversion in both directions
18
18
  - Status/error result fields for parse, targeting, and existing-revision failures
19
- - Localized exact replacements and fail-closed one-turn CLI edits with verified before/after evidence
19
+ - Localized exact replacements against inspected targets, plus optional fail-closed speculative CLI edits
20
20
  - Package plumbing helpers for numbering.xml, comments.xml, content types, and relationships
21
21
  - Zero host dependencies: works in Node.js, browsers, Deno, and similar JS runtimes with DOM parsing support
22
22
 
23
23
  ## What's New in 0.7.0
24
24
 
25
- Version 0.7.0 adds a contract-8 fast path for exact mechanical edits. Agents can
26
- send a small `find`/`replace` request instead of reproducing a complete legal
27
- paragraph, and may omit a strong target when the source literal resolves to one
28
- accepted-view paragraph. A longer, fairly unique nearby phrase can narrow the
29
- attempt with a signed directional window. Generic or repeated anchors widen the
30
- unioned scope and may cause a safe ambiguity failure and another tool turn.
25
+ Version 0.7.0 adds contract-8 localized edits. Agents can send a small
26
+ `find`/`replace` request against an inspected target instead of reproducing a
27
+ complete paragraph. An optional speculative form may omit the strong target
28
+ when the source literal resolves uniquely; advanced scoping is described below.
31
29
 
32
30
  Speculative edits fail without writing when the anchor, paragraph, or source
33
31
  span is missing or ambiguous. Successful localized results include a committed
@@ -249,18 +247,10 @@ latency. Checked comparative results are in the
249
247
  ### Agent CLI
250
248
 
251
249
  ```bash
252
- # One-turn exact mechanical edit; fails closed unless one paragraph matches
253
- docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
254
-
255
- # Directional context using a longer phrase with a higher chance of uniqueness
256
- docx-redline apply contract.docx --search "distinctive nearby heading or phrase" --context-range 1:3 --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
257
-
258
- # Semantic drafting still starts with focused context
250
+ # Default agent workflow: inspect once, then apply once
259
251
  docx-redline extract contract.docx --search "termination" --around 3
260
- docx-redline preflight contract.docx --operations operations.json --author "Editor"
261
- docx-redline apply contract.docx --operations operations.json --author "Editor" --output reviewed.docx
262
- docx-redline validate reviewed.docx
263
- ```
252
+ node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
253
+ ```
264
254
 
265
255
  ```bash
266
256
  # Inline one-liner edit (no operations file needed)
@@ -275,9 +265,9 @@ docx-redline apply contract.docx --target "Another author's clause" --modified "
275
265
  # High-assurance atomic batch with nonzero exit on any incomplete result
276
266
  docx-redline apply contract.docx --operations operations.json --atomic --require-complete --output reviewed.docx
277
267
 
278
- # Agent shell path: JSON is emitted by a serializer, not interpolated by the shell
279
- node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
280
- ```
268
+ # Optional read-only audit before a deliberately staged batch
269
+ docx-redline preflight contract.docx --operations operations.json --author "Editor"
270
+ ```
281
271
 
282
272
  All commands emit JSON on stdout. `apply` defaults:
283
273
  - **Author**: Defaults to `'AI Redliner'` (or `DOCX_REDLINE_AUTHOR` environment variable).
@@ -290,7 +280,8 @@ All commands emit JSON on stdout. `apply` defaults:
290
280
  - **Compact stdout**: `--compact` emits one-line mutation JSON. It changes serialization only, not operation semantics.
291
281
  - **Tracked changes**: Defaults to `generateRedlines: true`. Pass `--no-redlines` when clean direct text edits are desired.
292
282
  - **Inline edits**: Use `--target <text>` with `--modified <text>` or `--comment <text>` for quick one-liners without creating a JSON file.
293
- - **Localized edits**: Use `--find`/`--replace` with a known strong target, or omit the target for fail-closed global resolution. Add a longer, fairly unique `--search` phrase with `--context-range 1:3` for directional scope; `--around 3` is symmetric. Generic or repeated anchors can widen the unioned scope until multiple paragraphs contain `find`, producing a safe `AMBIGUOUS_TARGET` failure and an extra recovery turn. On success, `results[i].change.context.anchorMatchCount > 1` signals a non-unique anchor; confirm the returned location and bounded before/after evidence.
283
+ - **Localized edits**: After extraction, use `replacements` with the inspected `paragraphId` plus `fingerprint` to avoid restating a long paragraph. Retain `exactText` locally for drafting. Targetless `--find`/`--replace` is an optional speculative path described below.
284
+ - **Invisible spaces**: Localized matching is case-sensitive and tries exact text first. If no exact span exists, ordinary spaces may match DOCX non-breaking spaces; ambiguous equivalent matches still fail closed, and `change.replacements[i].matchMode` reports `space_equivalent` when used.
294
285
  - **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.
295
286
 
296
287
  Inspection commands default to 20 direct matches and a 48 KiB soft response
@@ -312,10 +303,31 @@ user-facing Word locations; use `humanReference`, `provision`, or
312
303
  `agent-safety-profile-v2`, `deduplicated-cli-receipts`, and
313
304
  `compact-cli-json-v1`, `localized-replacements-v1`,
314
305
  `speculative-search-apply-v1`, `localized-change-summary-v1`, and
315
- `restore-shortcuts-v1` capabilities.
306
+ `restore-shortcuts-v1`, plus `localized-space-equivalence-v1` capabilities.
316
307
  Wrappers should negotiate only the
317
308
  capabilities they use. Run `docx-redline <command> --help` for that command's
318
309
  machine-readable options, behavior, exit codes, canonical GitHub documentation links, and compact examples.
310
+
311
+ #### Optional speculative CLI path
312
+
313
+ Harnesses with a measured confidence policy may attempt a targetless localized
314
+ edit. This is not the default agent workflow:
315
+
316
+ ```bash
317
+ # Globally unique literal
318
+ docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
319
+
320
+ # Optional directional scope using a longer, fairly unique nearby phrase
321
+ docx-redline apply contract.docx --search "distinctive nearby heading or phrase" --context-range 1:3 --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
322
+ ```
323
+
324
+ Generic or repeated anchors can widen the unioned scope until multiple
325
+ paragraphs contain `find`, causing a safe `AMBIGUOUS_TARGET` failure and a
326
+ recovery turn. `--around 3` is symmetric. On success,
327
+ `change.context.anchorMatchCount > 1` signals a repeated anchor; confirm the
328
+ returned location and bounded evidence. Matching remains case-sensitive and
329
+ exact-first, with a narrow ordinary-space/NBSP fallback that still refuses
330
+ ambiguity.
319
331
 
320
332
  See the [compact agent fast start](./docs/AGENT_FAST_START.md), the
321
333
  [skill/harness authoring contract](./docs/SKILL_AUTHORING.md), and the
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.7.1 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.7.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;
@@ -1,58 +1,55 @@
1
1
  # Agent Fast Start
2
2
 
3
- Use this page for ordinary `.docx` edits. For source changes or unusual Word structures, follow the links at the end.
3
+ Use this page for ordinary `.docx` edits. For unusual Word structures or optional shortcuts, follow the links at the end.
4
4
 
5
5
  ## Structured agent tool
6
6
 
7
- If your host provides a document-session wrapper:
7
+ If the host provides a document-session wrapper:
8
8
 
9
- 1. Inspect/search once with enough surrounding context to draft the change.
10
- 2. Apply with the revision-bound handle and complete desired text or exact replacements.
11
- 3. Treat only `ok: true` as complete; refresh failed handles before reuse.
9
+ 1. Inspect once with enough surrounding context to draft the change.
10
+ 2. Apply once with the returned revision-bound handle.
11
+ 3. Accept only the wrapper's complete-success result; refresh failed handles.
12
12
 
13
- The repository's `examples/agent-session-wrapper.mjs` demonstrates this pattern.
14
- It is a development sample, not a package API.
13
+ The repository's `examples/agent-session-wrapper.mjs` is a development sample, not a package API.
15
14
 
16
- ## CLI fallback
15
+ ## CLI workflow
17
16
 
18
- 1. For a literal mechanical edit with known old and new text, try one-turn apply:
19
-
20
- ```bash
21
- docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
22
- ```
23
-
24
- The source must occur in one eligible paragraph. To narrow it, use a longer, fairly unique phrase: `--search "distinctive nearby heading or phrase" --context-range 1:3`.
25
- Generic/repeated anchors can cause `AMBIGUOUS_TARGET` and waste a recovery turn. If `anchorMatchCount > 1`, confirm the returned location and excerpts. `--around 3` is symmetric.
26
-
27
- 2. For semantic drafting such as “make this provision mutual,” extract enough
28
- context, then copy `exactText` with `paragraphId` or `fingerprint`:
17
+ 1. Extract the relevant clause or range:
29
18
 
30
19
  ```bash
31
20
  docx-redline extract contract.docx --search "termination" --around 3
32
21
  ```
33
22
 
34
- Search is case-insensitive; follow `selection.hint` and `selection.nextAfter`, and report `humanReference`. With `--revised`, use `hasRevisions`, `revisionAuthors`, and `deleted` or `inserted` to interpret paragraphs whose active-view `exactText` is empty. Restore one rejected-view paragraph verbatim with `apply --restore --target-id ID`; add one `--find`/`--replace` pair for an exact correction. The shortcut never expands to adjacent paragraphs. Otherwise, apply once with a UTF-8 operations file or serializer-backed stdin:
23
+ Search is case-insensitive. Follow `selection.nextAfter` when truncated. Preserve `selection`, `humanReference`, target descriptors, and revision cues when reshaping output. Report `humanReference`, not a machine paragraph number.
24
+
25
+ 2. Retain inspected `exactText` for drafting and target with its `paragraphId` plus `fingerprint`, then apply once with a UTF-8 operations file or serializer-backed stdin:
35
26
 
36
27
  ```bash
37
28
  node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
38
29
  ```
39
30
 
40
- Use `JSON.stringify`, not shell interpolation. `modified` is the complete desired accepted-view paragraph.
31
+ Use complete `modified` text for a broad semantic revision. For a small literal change in that inspected target, use
32
+ `replacements: [{ "find": "old text", "replace": "new text" }]` instead.
33
+ Use `JSON.stringify`; never interpolate document text through shell quoting. Preserve intentionally relative wording unless the user asks for a concrete value.
34
+
35
+ ## Restore branch
41
36
 
42
- The `agent` profile preserves progressive execution and the ordinary revision policy while making incomplete work exit nonzero. Add `--atomic` deliberately; check `effectiveOptions`. Run `docx-redline apply --help` for redline, comment, and rejected-view restore shapes.
37
+ If the user requests deleted text, search with `--view rejected` first. Otherwise follow `selection.hint` only when it identifies an alternate revision view. Restore the inspected strong target; do not infer adjacent content. Run `docx-redline apply --help` for canonical restore shapes.
43
38
 
44
- ## Batch and result rules
39
+ ## Completion and recovery
45
40
 
46
- - Strong inspected targets bind to the batch start; independent edits need no bottom-up sorting.
47
- - Consolidate writes to one source; use captures for created-content dependencies.
48
- - Require `completion: true`, `written: true`, a non-null `outputPath`, and no
49
- per-operation error. For localized edits also require
50
- `results[i].change.committed: true`, `finalDisposition: "applied"`, and
51
- `verification.acceptedViewMatchesCompiledText: true`. The source is
52
- not overwritten unless `--in-place` is explicit.
53
- - On failure, follow `error.recovery.action`. Never retry unchanged arguments.
54
- - `retryPlan.base: "original"` means replay the batch; `"output"` means retain committed work and retry reported indexes.
55
- - Never accept/reject revisions or remove comments without user authorization. `slice-cross-author` preserves history for edits inside foreign insertions.
41
+ - Require `completion: true` and a non-null `outputPath`; the CLI computes this from write, operation, commit, and localized-verification results.
42
+ - On failure, follow `error.recovery.action`; never retry unchanged arguments.
43
+ - Use `retryPlan.base: "original"` to replay a batch and `"output"` to retain
44
+ committed progressive work and retry the reported indexes.
45
+ - The source is not overwritten unless `--in-place` is explicit.
46
+ - Never accept/reject revisions or remove comments without authorization.
47
+ - Add `--atomic` or a non-default revision policy only when deliberately needed;
48
+ confirm `effectiveOptions`.
49
+ - Strong inspected targets bind to batch start. Consolidate writes to one source
50
+ and use captures only for intentional created-content dependencies.
56
51
 
57
- Advanced operations and recovery: [Agent Knowledge Base](AGENT_KNOWLEDGE_BASE.md); canonical contract: [document-operations.schema.json](schemas/document-operations.schema.json)
52
+ Advanced speculative apply, directional scoping, operation constraints, and
53
+ recovery: [Agent Knowledge Base](AGENT_KNOWLEDGE_BASE.md). Canonical schema:
54
+ [document-operations.schema.json](schemas/document-operations.schema.json).
58
55
  Wrapper design: [README](../README.md#example-agent-session-wrapper-development-only)
@@ -476,66 +476,41 @@ The canonical machine-readable contract is
476
476
  `docs/schemas/document-operations.schema.json`. Read that schema or the examples
477
477
  here instead of grepping bundled implementation code.
478
478
 
479
- #### Standard Workflow (Fast & Direct)
480
-
481
- Use this for everything by default. `apply` is fast, progressive, and self-validating by default—it validates the resulting package and revision markup internally before writing. **Do not insert a `preflight` or baseline `validate` step on top of it "to be safe"**; `apply` already covers that internally. It supports inline one-liners as well as batch operations files:
482
-
483
- The short route for a document-editing request is:
484
-
485
- 1. For an exact mechanical change with known old and new literals, use localized
486
- `--find`/`--replace`. Omit the target for global fail-closed resolution, or
487
- scope it with a longer, fairly unique nearby `--search` phrase and a
488
- directional range such as `--context-range 1:3`. Generic or repeated anchors
489
- widen the unioned scope and may fail with `AMBIGUOUS_TARGET`, costing another
490
- turn. `--occurrence` selects only within one uniquely resolved paragraph.
491
- 2. For semantic drafting, run one focused `extract`. Add `--around 3` when
492
- surrounding context on either side is needed, then copy `exactText` plus
493
- `paragraphId` or `fingerprint`.
494
- 3. Build the final operations from the operation table above. Use one operation
495
- per target paragraph, and consolidate multiple changes to that paragraph.
496
- 4. Run `apply` once per stable batch. Strong inspected targets are bound against
497
- the batch-start document, so independent operations do not need manual
498
- bottom-up sorting around structural edits. Consolidate multiple complete
499
- desired states for the same source. A unique exact reference to paragraph
500
- text created elsewhere in the batch is scheduled automatically; use explicit
501
- captures/selectors for non-unique or advanced created-content dependencies.
502
- 5. Walk every result and require `completion: true`, `written: true`, and no
503
- per-operation error. For localized patches also require
504
- `results[i].change.committed: true`, `finalDisposition: "applied"`, and
505
- positive accepted-view verification. If `anchorMatchCount > 1`, confirm the
506
- returned location and excerpts because the anchor was repeated.
507
- 6. Run a focused `extract` on changed clauses only when placement or list/table
508
- structure needs confirmation.
509
-
510
- Do not probe operation behavior with disposable apply commands or read a vendor
511
- bundle before this route. If `apply` returns an error, use the recovery matrix
512
- below and make one cause-specific correction.
513
-
514
- ```bash
515
- # 1. Globally unique mechanical edit in one call
516
- docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
479
+ #### Standard Workflow (Extract Then Apply)
480
+
481
+ Use this for ordinary edits. `apply` validates the resulting package and
482
+ revision markup internally before writing, so do not add `preflight` or baseline
483
+ `validate` merely for reassurance.
484
+
485
+ 1. Run one focused `extract`. Add `--around 3` when surrounding context is
486
+ needed. Retain `exactText` for drafting and target with `paragraphId` plus
487
+ `fingerprint`.
488
+ 2. Build one final operation per target. Use complete `modified` text for broad
489
+ semantic revisions or localized `replacements` for small literal changes in
490
+ an inspected target. Consolidate multiple changes to one paragraph.
491
+ 3. Run `apply` once per stable batch. Independent strong targets bind against
492
+ the batch-start document and do not need bottom-up sorting. Use captures only
493
+ for intentional created-content dependencies.
494
+ 4. Require `completion: true` and a non-null `outputPath`. The CLI derives
495
+ completion from write status, operation outcomes, commit disposition, and
496
+ localized accepted-view verification.
497
+ 5. Run a focused post-apply extraction only when placement or structural output
498
+ needs confirmation.
499
+
500
+ If `apply` returns an error, follow the recovery envelope below and make one
501
+ cause-specific correction. Do not probe behavior with disposable mutations or
502
+ read a vendor bundle.
517
503
 
518
- # 2. Directional edit using a longer phrase with a higher chance of uniqueness
519
- docx-redline apply contract.docx --search "distinctive nearby heading or phrase" --context-range 1:3 --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
520
-
521
- # 3. Focused contextual discovery for semantic drafting
504
+ ```bash
505
+ # 1. Focused contextual discovery
522
506
  docx-redline extract contract.docx --search "termination" --around 3
523
507
 
524
- # 4. Inline complete-paragraph edit
525
- docx-redline apply contract.docx --target "Original clause" --modified "New clause" --output reviewed.docx
526
-
527
- # 5. Direct edit without tracked changes (clean text, no revision clutter)
528
- docx-redline apply contract.docx --target "Typo fix" --modified "Fixed typo" --no-redlines --output clean.docx
529
-
530
- # 6. Cross-author edit inside another reviewer's pending insertion
531
- docx-redline apply contract.docx --target "Pending clause text" --modified "Updated clause text" --existing-revisions slice-cross-author --output reviewed.docx
532
-
533
- # 7. Batch operations with ops.json
534
- docx-redline apply contract.docx --operations operations.json --output reviewed.docx
535
-
536
- # 8. Serializer-backed stdin with compact stdout
508
+ # 2. Serializer-backed apply against the inspected target
537
509
  node emit-operations.mjs | docx-redline apply contract.docx --operations - --profile agent --compact --output reviewed.docx
538
- ```
510
+
511
+ # Optional clean edit without tracked changes
512
+ docx-redline apply contract.docx --target "Typo fix" --modified "Fixed typo" --no-redlines --output clean.docx
513
+ ```
539
514
 
540
515
  Key CLI defaults and behaviors:
541
516
  - **Author**: Automatically defaults to `'AI Redliner'` (overridable via `--author` or `DOCX_REDLINE_AUTHOR` environment variable).
@@ -547,9 +522,36 @@ Key CLI defaults and behaviors:
547
522
  - **Agent profile**: `--profile agent` enables complete-success exit behavior but preserves progressive execution and the ordinary revision policy. Compose it with `--atomic` or an explicit `--existing-revisions` choice when intended; resolved values appear in `effectiveOptions`.
548
523
  - **Operation transport**: A UTF-8 operations file and serializer-backed `--operations -` are peers. Use whichever the host can construct without interpolating legal text in the shell.
549
524
  - **Compact mutation JSON**: `apply`, `accept`, `reject`, and `delete-comments` omit document/package XML, full validation arrays, and duplicate nested receipts. Top-level `receipts` is authoritative. Pass `--compact` for one-line JSON; run `validate` for full issue records.
550
- - Check `completion: true`, `written: true`, and a non-null `outputPath` on stdout. `completion` is derived from the write result, top-level status, and every operation status, so failed, partial, and unwritten work cannot appear complete. If an error occurs, inspect `error.code` or `results[i].error.code` (e.g. `TARGET_NOT_FOUND`, `ANCHOR_NOT_FOUND`) before correcting the cause and re-applying.
525
+ - Check `completion: true` and a non-null `outputPath` on stdout. Failed,
526
+ partial, rolled-back, unverified, and unwritten work cannot appear complete.
527
+ If an error occurs, follow `error.recovery.action` before correcting the cause.
551
528
 
552
- For multi-clause or multi-page reviews, apply edits **section-by-section** or clause-by-clause (e.g., using `--in-place` on a working copy) rather than bundling dozens of edits into one massive batch. This keeps context compact, simplifies error diagnosis, and prevents cascading anchor drift.
529
+ For multi-clause or multi-page reviews, use focused batches rather than one
530
+ massive batch. This keeps context compact and simplifies error diagnosis.
531
+
532
+ #### Optional Speculative Localized Apply
533
+
534
+ Targetless localized apply remains available to expert users and harnesses with
535
+ a measured confidence policy. It is not the ordinary agent workflow:
536
+
537
+ ```bash
538
+ # Globally unique literal
539
+ docx-redline apply contract.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
540
+
541
+ # Optional directional scope using a longer, fairly unique phrase
542
+ docx-redline apply contract.docx --search "distinctive nearby heading or phrase" --context-range 1:3 --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
543
+ ```
544
+
545
+ Omitting a target asks the CLI to resolve one eligible paragraph. Generic or
546
+ repeated anchors union their windows and may produce `AMBIGUOUS_TARGET`, costing
547
+ a recovery turn. `--around 3` is symmetric; `--occurrence` disambiguates only
548
+ inside an already selected paragraph. `anchorMatchCount > 1` signals a repeated
549
+ anchor even after a successful edit. Matching is exact and case-sensitive first,
550
+ then permits only ASCII-space/NBSP equivalence; ambiguity still fails closed.
551
+
552
+ Localized `change` evidence retains bounded before/after excerpts, commit
553
+ disposition, and accepted-view verification for audit consumers. Ordinary
554
+ agents rely on the computed top-level `completion` signal.
553
555
 
554
556
  #### High-Assurance / Staged Verification Workflow (Optional)
555
557
 
@@ -1,86 +1,76 @@
1
1
  # Skill and Harness Authoring Contract
2
2
 
3
- Use this page when creating or updating an agent skill, MCP server, or custom
4
- harness for `@ansonlai/docx-redline-js`. Ordinary document-editing agents should
5
- load [Agent Fast Start](AGENT_FAST_START.md), not this design contract.
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
6
 
7
7
  > [!WARNING]
8
- > Before 1.0, every skill or harness must identify and pin its exact tested
9
- > library release and required CLI contract/capabilities. Treat any different
10
- > release or contract as unverified until compatibility tests pass.
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
11
 
12
- For example, an integration authored for the current contract-8 release should
13
- record a compatibility declaration equivalent to:
12
+ Example declaration for a contract-8 shell integration that uses localized
13
+ operations over stdin:
14
14
 
15
15
  ```json
16
16
  {
17
- "testedLibraryRelease": "0.7.0",
17
+ "testedLibraryRelease": "0.7.1",
18
18
  "requiredCliContractVersion": 8,
19
19
  "requiredCapabilities": [
20
- "command-help-v1",
20
+ "operations-stdin",
21
+ "agent-safety-profile-v2",
21
22
  "recovery-envelope-v1",
22
- "localized-replacements-v1",
23
- "speculative-search-apply-v1",
24
- "localized-change-summary-v1"
23
+ "localized-replacements-v1"
25
24
  ]
26
25
  }
27
26
  ```
28
27
 
29
- The package pin anchors the release; runtime negotiation proves the executable
30
- exposes the required contract. A new release cannot inherit an old claim.
31
-
32
28
  ## Runtime negotiation
33
29
 
34
- Run this before the first document operation when the skill vendors or invokes
35
- the CLI:
36
-
37
- ```bash
38
- docx-redline version
39
- ```
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.
40
34
 
41
- Require contract version 8 (the currently declared contract) and only the
42
- capabilities the integration uses. A typical shell skill should require
43
- `agent-safety-profile-v2`, `deduplicated-cli-receipts`, and
44
- `recovery-envelope-v1`. Require other capabilities only when used. Fail closed
45
- when one is absent; never inspect bundles or ZIP parts as a fallback.
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.
46
38
 
47
39
  ## Safety invariants
48
40
 
49
- Every generated integration must preserve these rules:
41
+ Every generated integration must:
50
42
 
51
- - Inspect narrowly and copy `exactText` plus `paragraphId` or `fingerprint`.
52
- - Treat `modified` as the complete desired accepted-view target content.
53
- - Use strict targeting, package validation, and a derived output path; never
54
- overwrite the source unless the caller explicitly requests `--in-place`.
55
- - Require `completion: true`, `written: true`, a non-null `outputPath`, and no
56
- per-operation error before reporting completion.
57
- - Never accept or reject another reviewer's work or delete comments without
58
- explicit user authorization.
59
- - Never retry unchanged failed arguments.
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.
60
52
 
61
- The `agent` profile enables complete-success exit behavior and retains the
62
- ordinary progressive default. It does not select atomic execution or a special
63
- existing-revision policy. A skill must state those choices explicitly when its
64
- workflow needs them.
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.
65
57
 
66
58
  ## Workflow policy choices
67
59
 
68
- Choose and document these separately from safety invariants:
60
+ Keep these exceptions separate from the default workflow:
69
61
 
70
- | Choice | Options | Guidance |
62
+ | Choice | Default | Override only when |
71
63
  |---|---|---|
72
- | Transaction | progressive default or `--atomic` | Use progressive for independent edits; use atomic when partial output is unacceptable. |
73
- | Existing revisions | `merge-same-author` default or explicit policy | Use `slice-cross-author` only when editing inside another reviewer's pending insertion is intended. |
74
- | Reviewer identity | flag, operation author, environment, or fallback | `AI Redliner` is a valid visible fallback. `DOCX_REDLINE_AUTHOR` is an optional harness preference, not a guard. |
75
- | Output naming | derived sibling or explicit destination | Keep source immutability unless in-place mutation is deliberately authorized. |
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. |
76
68
 
77
- Check `effectiveOptions` rather than assuming the resolved transaction,
78
- revision, author, or redline policy.
69
+ Check `effectiveOptions` when an integration deliberately sets a policy.
79
70
 
80
71
  ## Transport choices
81
72
 
82
- A UTF-8 operations file and serializer-backed stdin are peer transports. Choose
83
- the form the host can construct safely:
73
+ A UTF-8 operations file and serializer-backed stdin are peer transports:
84
74
 
85
75
  ```bash
86
76
  docx-redline apply input.docx --operations operations.json --profile agent --output reviewed.docx
@@ -88,46 +78,46 @@ node emit-operations.mjs | docx-redline apply input.docx --operations - --profil
88
78
  ```
89
79
 
90
80
  Use `JSON.stringify` or a structured tool API. Never demonstrate `echo`, a
91
- heredoc, or shell-interpolated legal text. `--compact` changes stdout formatting,
92
- not result semantics. A Node byte-oriented wrapper should call `openDocx` and
93
- return the full structured result rather than recreating the CLI serializer.
81
+ heredoc, or shell-interpolated document text. A Node byte-oriented wrapper should
82
+ call `openDocx` and return the structured result instead of recreating CLI
83
+ serialization, ZIP handling, targeting, validation, or rollback.
94
84
 
95
85
  ## Ordinary generated workflow
96
86
 
97
- For an exact mechanical edit whose existing and replacement literals are known,
98
- the first executable example should use the one-turn fast path:
87
+ The first executable editing example in a generated skill must be focused
88
+ extraction:
99
89
 
100
90
  ```bash
101
- docx-redline apply input.docx --find "thirty (30) days" --replace "sixty (60) days" --profile agent --output reviewed.docx
91
+ docx-redline extract input.docx --search "renewal notice" --around 3
102
92
  ```
103
93
 
104
- If the literal is not globally unique, use a longer, fairly unique nearby phrase
105
- as `--search` context. Generic or repeated anchors widen the unioned scope and
106
- may cause `AMBIGUOUS_TARGET`, wasting a recovery turn. Use a directional window
107
- such as `1:3`; use symmetric `--around 3` only when either side is eligible. On
108
- success, `anchorMatchCount > 1` signals a repeated anchor: confirm the returned
109
- location and excerpts. Require `results[i].change` with `committed: true`,
110
- `finalDisposition: "applied"`, and positive accepted-view verification.
94
+ Then apply once using the inspected strong target. Use complete `modified` text
95
+ for a broad semantic revision or `replacements` for a small literal change in
96
+ that target. Keep requested relative wording relative unless the user asks for
97
+ a concrete value.
111
98
 
112
- Semantic requests such as “make this provision mutual” still begin with focused
113
- contextual extraction because the agent must identify and draft the complete
114
- legal change:
99
+ Follow `selection.nextAfter` when extraction is truncated. Output projections must retain
100
+ `selection`, target descriptors, `humanReference`, and revision
101
+ cues.
115
102
 
116
- ```bash
117
- docx-redline extract input.docx --search "termination" --around 3
118
- ```
103
+ ### Observed restore branch
104
+
105
+ If the user requests restoration, search `--view rejected` first. Otherwise
106
+ follow an alternate-view `selection.hint` when returned. Restore only the
107
+ inspected strong target. Require `restore-shortcuts-v1` only if the integration
108
+ uses inline `--restore`; canonical operations remain available.
109
+
110
+ ### Optional speculative execution
119
111
 
120
- Use `selection.nextAfter` with `--after` when truncated. When a search for
121
- deleted or restorable content yields 0 matches in accepted view, follow
122
- `selection.hint` and re-run with `--view rejected`. Apply once per stable batch.
123
- Run `docx-redline apply --help` for canonical redline, whole-paragraph comment,
124
- and rejected-view restore shapes; CLI help documentation fields return canonical
125
- GitHub URLs. The operation schema remains
126
- [document-operations.schema.json](schemas/document-operations.schema.json).
112
+ Do not make speculative apply part of an ordinary generated workflow. A harness
113
+ may opt into `speculative-search-apply-v1` only when it owns a measured confidence
114
+ policy and accepts the recovery cost of a miss. Keep anchor selection,
115
+ directional windows, occurrence handling, and exact matching semantics out of
116
+ the ordinary editing prompt; link to the knowledge base or CLI help instead.
127
117
 
128
118
  ## Recovery contract
129
119
 
130
- Lead error handling with exactly these runtime fields:
120
+ Lead failure handling with these runtime fields:
131
121
 
132
122
  1. `error.recovery.action`
133
123
  2. `error.recovery.requiresReinspection`
@@ -135,32 +125,23 @@ Lead error handling with exactly these runtime fields:
135
125
  4. `error.recovery.sameArgumentsSafe`
136
126
 
137
127
  Use `retryPlan.base` and its operation indexes to select the original or partial
138
- output package. Do not generate a second prose decision tree for every error
139
- code; error messages and bounded diagnostics explain the cause, while the four
140
- fields above determine the next action.
128
+ output package. Do not reproduce an error-code decision tree in the skill.
141
129
 
142
130
  ## Presentation rules
143
131
 
144
- Use `humanReference`, `provision`, or `nearestHeading` in user-facing completion
145
- reports. `P55`, `index: 55`, and phrases such as “the 55th paragraph” are machine
146
- targeting and pagination coordinates, not locations a Word user can follow.
147
- When no formal heading exists, use the returned short text lead. Never replace
148
- an exact machine target with a human-facing reference.
132
+ Use `humanReference`, `provision`, or `nearestHeading` in completion reports.
133
+ Paragraph indexes and `P55`-style references are machine coordinates, not
134
+ locations a document user can follow. Keep the exact machine target internally.
149
135
 
150
136
  ## Generation checklist
151
137
 
152
- - The exact tested library release is recorded and pinned; floating installs
153
- are rejected while the library remains before 1.0.
154
- - The required CLI contract version and minimum capability set are declared,
155
- checked at runtime, and updated only after compatibility tests pass.
156
- - Runtime version and required capabilities are checked once.
157
- - Literal edits use localized replacements and one-turn speculative apply;
158
- semantic drafting starts with focused extraction.
159
- - Successful localized edits require committed, post-mutation `change`
160
- verification before completion is reported.
161
- - Safety invariants are not mixed with transaction or revision-policy choices.
162
- - Operations-file and stdin examples use structured serialization.
163
- - The four-field recovery contract precedes any diagnostic commentary.
164
- - Completion prose uses legal references rather than machine ordinals.
165
- - The wrapper delegates mutation, validation, rollback, comments, numbering,
166
- receipts, and OOXML packaging to the library.
138
+ - Exact tested release, contract 8, and the minimum capability set are pinned.
139
+ - Runtime compatibility is checked once.
140
+ - The first ordinary command is focused extraction.
141
+ - Apply uses the inspected target with `modified` or localized `replacements`.
142
+ - Restore appears only as a user-requested or observed-view branch.
143
+ - Speculative execution is absent unless the harness owns a measured policy.
144
+ - Ordinary success requires `completion: true` and a non-null `outputPath`.
145
+ - Recovery, authorization, source immutability, and structured serialization
146
+ invariants are preserved.
147
+ - Wrappers delegate mutation and OOXML packaging to the library.
@@ -0,0 +1,75 @@
1
+ # Agent Surface Simplification Replay
2
+
3
+ **Date:** 2026-09-14
4
+ **Runtime:** Node.js 24.11.1 on Windows x64
5
+ **Engine:** Current post-0.7.1 source for every policy
6
+ **Command:** `npm run benchmark:agent-surface`
7
+ **Configuration:** 7 measured iterations and 1 warmup per scenario/policy
8
+
9
+ ## Scope
10
+
11
+ This repository-controlled replay compares three workflow policies without
12
+ changing the underlying engine:
13
+
14
+ 1. `v0.6.2-extract-full`: extraction followed by complete modified text.
15
+ 2. `v0.7.1-speculation-led`: speculative literal edits with extraction/apply
16
+ recovery, and extraction for semantic or batch work.
17
+ 3. `simplified-extract-localized`: extraction followed by a compact localized
18
+ operation against `paragraphId` plus `fingerprint`.
19
+
20
+ The six wholly synthetic scenarios cover a long literal edit, semantic drafting,
21
+ repeated visible text, rejected-view restoration, an NBSP-bearing target, and an
22
+ independent two-target batch. Every output is re-extracted and compared with its
23
+ predetermined accepted-view text. Benchmark-only verification calls are not
24
+ counted as workflow calls.
25
+
26
+ ## Aggregate Results
27
+
28
+ | Policy | Correct runs | Total calls | Calls/run | Failed calls | Edit request bytes | Median local ms |
29
+ |---|---:|---:|---:|---:|---:|---:|
30
+ | v0.6.2 extract/full | 42/42 | 84 | 2.00 | 0 | 27,034 | 3.86 |
31
+ | v0.7.1 speculation-led | 42/42 | 77 | 1.83 | 7 | 8,988 | 3.86 |
32
+ | simplified extract/localized | 42/42 | 84 | 2.00 | 0 | 10,661 | 3.77 |
33
+
34
+ The simplified policy reduced generated edit-request bytes by 60.6% relative to
35
+ the complete-paragraph baseline while preserving a fixed two-call workflow and
36
+ zero failed calls. On the long-paragraph case, its median edit request was 193
37
+ bytes rather than 2,104 bytes, a 90.8% reduction.
38
+
39
+ The speculation-led policy saved seven total calls because two unique literal
40
+ scenarios completed in one call. The repeated-text scenario cost three calls and
41
+ one failure on every iteration. This demonstrates both the legitimate one-turn
42
+ benefit and the recovery cost that the simplified default is intended to avoid.
43
+
44
+ The localized form is not always smaller for a short paragraph. In the NBSP
45
+ scenario it used 344 bytes versus 343 bytes for complete modified text. The
46
+ benefit is strongest when the inspected source is long relative to the changed
47
+ span.
48
+
49
+ ## Interpretation
50
+
51
+ The repository evidence supports the surface decision:
52
+
53
+ - keep speculative apply as an available expert/harness capability;
54
+ - do not make ordinary agents classify whether to use it;
55
+ - lead with deterministic extraction and one apply;
56
+ - retain localized replacements after inspection to avoid long output payloads;
57
+ - use compact `paragraphId` plus `fingerprint` targets for localized requests,
58
+ while retaining extracted text locally for drafting; and
59
+ - keep restoration as a request/hint-driven branch.
60
+
61
+ Local execution time is effectively neutral at this scale and remains far below
62
+ agent deliberation or provider latency. No performance claim should be based on
63
+ the small local timing differences in this report.
64
+
65
+ ## Limitations
66
+
67
+ This is a deterministic workflow replay, not a model evaluation. It cannot
68
+ measure reasoning tokens, provider tokenization, transport latency, stochastic
69
+ instruction following, or subjective drafting quality. Repeated external agent
70
+ runs using the same model, prompt, document, and engine are still required before
71
+ claiming end-to-end latency improvement.
72
+
73
+ The machine-readable output is written to
74
+ `tmp/benchmarks/agent-surface-simplification-latest.json` and is intentionally
75
+ not committed.
package/node/cli-help.js CHANGED
@@ -134,7 +134,7 @@ export const CLI_COMMAND_HELP = Object.freeze({
134
134
  option('targetId', '--target-id <paragraphId>', 'Strong inline target using a paragraphId from fresh inspection.'),
135
135
  option('restore', '--restore', 'Restore one rejected-view paragraph identified by --target-id; omit --modified for verbatim restoration.'),
136
136
  option('modified', '--modified <text>', 'Complete desired accepted-view target content.'),
137
- option('find', '--find <exact text>', 'Exact case-sensitive source span for one localized replacement.'),
137
+ option('find', '--find <exact text>', 'Case-sensitive source span; exact match first, then ASCII-space/NBSP equivalence.'),
138
138
  option('replace', '--replace <text>', 'Replacement text for --find; an empty string deletes the span.'),
139
139
  option('occurrence', '--occurrence <N>', 'Positive 1-based --find occurrence within the uniquely resolved target paragraph.'),
140
140
  option('search', '--search <text>', 'Case-insensitive context anchor for speculative find/replace; prefer a longer, fairly unique phrase because all matches contribute scope.'),
package/node/cli.js CHANGED
@@ -7,6 +7,7 @@ import { validateRedlineOoxml } from '../core/redline-validation.js';
7
7
  import { configureLogger } from '../adapters/logger.js';
8
8
  import { isExistingRevisionsPolicy } from '../services/document-operation-contract.js';
9
9
  import { normalizeErrorWithRecovery } from '../services/error-recovery.js';
10
+ import { localizedReplacementOffsets } from '../services/localized-replacement-compiler.js';
10
11
  import { buildCliHelp, CLI_COMMANDS, commandOptionKeys } from './cli-help.js';
11
12
 
12
13
  const suffixes = { apply: 'redlined', accept: 'accepted', reject: 'rejected', 'delete-comments': 'comments-removed' };
@@ -31,6 +32,7 @@ const CLI_CAPABILITIES = [
31
32
  'localized-replacements-v1',
32
33
  'speculative-search-apply-v1',
33
34
  'localized-change-summary-v1',
35
+ 'localized-space-equivalence-v1',
34
36
  'restore-shortcuts-v1',
35
37
  'deduplicated-cli-receipts',
36
38
  'compact-cli-json-v1'
@@ -242,21 +244,27 @@ function resolveSpeculativePatchTarget(document, flags) {
242
244
  }
243
245
 
244
246
  const find = String(flags.find);
245
- const matches = eligible.filter(paragraph => paragraph.exactText.includes(find));
247
+ const patchCandidates = eligible.map(paragraph => ({
248
+ paragraph,
249
+ match: localizedReplacementOffsets(paragraph.exactText, find)
250
+ })).filter(candidate => candidate.match.offsets.length > 0);
251
+ const exactCandidates = patchCandidates.filter(candidate => candidate.match.matchMode === 'exact');
252
+ const matches = (exactCandidates.length > 0 ? exactCandidates : patchCandidates)
253
+ .map(candidate => candidate.paragraph);
246
254
  const context = {
247
255
  ...(search ? { search, range: range.text, anchorMatchCount: anchors.length } : {})
248
256
  };
249
257
  if (matches.length === 0) {
250
258
  throw speculativeError('PATCH_SOURCE_NOT_FOUND', search
251
- ? `No paragraph in the contextual range contains the exact patch source: "${find}".`
252
- : `No paragraph contains the exact patch source: "${find}".`, {
259
+ ? `No paragraph in the contextual range contains the patch source: "${find}".`
260
+ : `No paragraph contains the patch source: "${find}".`, {
253
261
  context,
254
262
  ...(anchors.length ? { candidates: anchors.map(speculativeCandidate) } : {}),
255
263
  recovery: { action: 'reinspect', requiresReinspection: true, sameArgumentsSafe: false }
256
264
  });
257
265
  }
258
266
  if (matches.length > 1) {
259
- throw speculativeError('AMBIGUOUS_TARGET', `The exact patch source matched ${matches.length} eligible paragraphs.`, {
267
+ throw speculativeError('AMBIGUOUS_TARGET', `The patch source matched ${matches.length} eligible paragraphs.`, {
260
268
  context,
261
269
  candidates: matches.map(speculativeCandidate),
262
270
  recovery: { action: 'choose_candidate', requiresReinspection: false, sameArgumentsSafe: false }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ansonlai/docx-redline-js",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Host-independent OOXML reconciliation engine for .docx manipulation with track changes",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,6 +47,7 @@
47
47
  "!scripts/benchmark-agent-workflow.mjs",
48
48
  "!scripts/benchmark-agent-cli-discovery.mjs",
49
49
  "!scripts/benchmark-localized-turn-reduction.mjs",
50
+ "!scripts/benchmark-agent-surface-simplification.mjs",
50
51
  "!scripts/lib/agent-performance-cases.mjs",
51
52
  "docs/AGENT_FAST_START.md",
52
53
  "docs/AGENT_KNOWLEDGE_BASE.md",
@@ -58,6 +59,7 @@
58
59
  "docs/validation-reports/2026-09-12-agent-cli-discovery-baseline.md",
59
60
  "docs/validation-reports/2026-09-13-agent-cli-efficiency-rollout.md",
60
61
  "docs/validation-reports/2026-09-14-localized-patching-rollout.md",
62
+ "docs/validation-reports/2026-09-14-agent-surface-simplification.md",
61
63
  "index.js",
62
64
  "index.d.ts",
63
65
  "dist/",
@@ -97,6 +99,7 @@
97
99
  "benchmark:agent": "node scripts/benchmark-agent-workflow.mjs",
98
100
  "benchmark:agent-cli": "node scripts/benchmark-agent-cli-discovery.mjs",
99
101
  "benchmark:localized": "node scripts/benchmark-localized-turn-reduction.mjs",
102
+ "benchmark:agent-surface": "node scripts/benchmark-agent-surface-simplification.mjs",
100
103
  "benchmark:targeting": "node scripts/benchmark-targeting-hot-paths.mjs",
101
104
  "benchmark:tests": "node scripts/benchmark-test-runner.mjs",
102
105
  "profile:routes": "node scripts/profile-route-selection.mjs",
@@ -41,6 +41,65 @@ function occurrenceOffsets(sourceText, find) {
41
41
  return offsets;
42
42
  }
43
43
 
44
+ function equivalentSpace(left, right) {
45
+ return (left === ' ' || left === '\u00a0')
46
+ && (right === ' ' || right === '\u00a0');
47
+ }
48
+
49
+ function spaceEquivalentOffsets(sourceText, find) {
50
+ const offsets = [];
51
+ for (let start = 0; start <= sourceText.length - find.length; start += 1) {
52
+ let matches = true;
53
+ for (let offset = 0; offset < find.length; offset += 1) {
54
+ const sourceCharacter = sourceText[start + offset];
55
+ const findCharacter = find[offset];
56
+ if (sourceCharacter !== findCharacter && !equivalentSpace(sourceCharacter, findCharacter)) {
57
+ matches = false;
58
+ break;
59
+ }
60
+ }
61
+ if (matches) offsets.push(start);
62
+ }
63
+ return offsets;
64
+ }
65
+
66
+ /** Resolve exact offsets first, then the narrow ASCII-space/NBSP equivalent fallback. */
67
+ export function localizedReplacementOffsets(sourceText, find) {
68
+ const exact = occurrenceOffsets(sourceText, find);
69
+ if (exact.length > 0) return { offsets: exact, matchMode: 'exact' };
70
+ return { offsets: spaceEquivalentOffsets(sourceText, find), matchMode: 'space_equivalent' };
71
+ }
72
+
73
+ function preserveEquivalentBoundarySpaces(sourceSlice, find, replace) {
74
+ const effective = replace.split('');
75
+ let prefix = 0;
76
+ while (
77
+ prefix < find.length
78
+ && prefix < replace.length
79
+ && (find[prefix] === replace[prefix] || equivalentSpace(find[prefix], replace[prefix]))
80
+ ) {
81
+ if (equivalentSpace(sourceSlice[prefix], replace[prefix])) effective[prefix] = sourceSlice[prefix];
82
+ prefix += 1;
83
+ }
84
+ let sourceOffset = find.length - 1;
85
+ let replacementOffset = replace.length - 1;
86
+ while (
87
+ sourceOffset >= prefix
88
+ && replacementOffset >= prefix
89
+ && (
90
+ find[sourceOffset] === replace[replacementOffset]
91
+ || equivalentSpace(find[sourceOffset], replace[replacementOffset])
92
+ )
93
+ ) {
94
+ if (equivalentSpace(sourceSlice[sourceOffset], replace[replacementOffset])) {
95
+ effective[replacementOffset] = sourceSlice[sourceOffset];
96
+ }
97
+ sourceOffset -= 1;
98
+ replacementOffset -= 1;
99
+ }
100
+ return effective.join('');
101
+ }
102
+
44
103
  function offsetCandidate(sourceText, start, length) {
45
104
  const excerptStart = Math.max(0, start - 40);
46
105
  const excerptEnd = Math.min(sourceText.length, start + length + 40);
@@ -168,15 +227,15 @@ export function compileExactReplacements(
168
227
  const replacement = validation.replacements[index];
169
228
  const itemField = `${field}[${index}]`;
170
229
  const occurrence = replacement.occurrence ?? null;
171
- const offsets = occurrenceOffsets(sourceText, replacement.find);
230
+ const { offsets, matchMode } = localizedReplacementOffsets(sourceText, replacement.find);
172
231
  if (offsets.length === 0 || (occurrence != null && occurrence > offsets.length)) {
173
232
  return {
174
233
  ok: false,
175
234
  error: patchError(
176
235
  'PATCH_SOURCE_NOT_FOUND',
177
236
  occurrence == null
178
- ? `Exact patch source was not found: "${replacement.find}".`
179
- : `Occurrence ${occurrence} of exact patch source was not found: "${replacement.find}".`,
237
+ ? `Patch source was not found: "${replacement.find}".`
238
+ : `Occurrence ${occurrence} of patch source was not found: "${replacement.find}".`,
180
239
  itemField,
181
240
  'change_patch',
182
241
  { matchCount: offsets.length }
@@ -188,7 +247,7 @@ export function compileExactReplacements(
188
247
  ok: false,
189
248
  error: patchError(
190
249
  'AMBIGUOUS_PATCH_SOURCE',
191
- `Exact patch source matched ${offsets.length} locations; provide occurrence.`,
250
+ `Patch source matched ${offsets.length} locations; provide occurrence.`,
192
251
  itemField,
193
252
  'choose_occurrence',
194
253
  { candidates: offsets.map(start => offsetCandidate(sourceText, start, replacement.find.length)) }
@@ -197,12 +256,17 @@ export function compileExactReplacements(
197
256
  }
198
257
 
199
258
  const start = offsets[(occurrence || 1) - 1];
259
+ const sourceSlice = sourceText.slice(start, start + replacement.find.length);
200
260
  resolved.push({
201
261
  requestIndex: index,
202
262
  start,
203
263
  end: start + replacement.find.length,
204
264
  find: replacement.find,
205
265
  replace: replacement.replace,
266
+ effectiveReplace: matchMode === 'space_equivalent'
267
+ ? preserveEquivalentBoundarySpaces(sourceSlice, replacement.find, replacement.replace)
268
+ : replacement.replace,
269
+ matchMode,
206
270
  occurrence: occurrence || 1
207
271
  });
208
272
  }
@@ -212,7 +276,7 @@ export function compileExactReplacements(
212
276
  for (const replacement of resolved) {
213
277
  const previous = unique.at(-1);
214
278
  if (previous && replacement.start === previous.start && replacement.end === previous.end) {
215
- if (replacement.replace !== previous.replace) {
279
+ if (replacement.effectiveReplace !== previous.effectiveReplace) {
216
280
  return {
217
281
  ok: false,
218
282
  error: patchError(
@@ -245,7 +309,7 @@ export function compileExactReplacements(
245
309
  let desiredText = '';
246
310
  for (const replacement of unique) {
247
311
  desiredText += sourceText.slice(cursor, replacement.start);
248
- desiredText += replacement.replace;
312
+ desiredText += replacement.effectiveReplace;
249
313
  cursor = replacement.end;
250
314
  }
251
315
  desiredText += sourceText.slice(cursor);
@@ -259,9 +323,10 @@ export function compileExactReplacements(
259
323
  end: replacement.end,
260
324
  find: replacement.find,
261
325
  replace: replacement.replace,
326
+ matchMode: replacement.matchMode,
262
327
  occurrence: replacement.occurrence,
263
328
  removedLength: replacement.end - replacement.start,
264
- insertedLength: replacement.replace.length
329
+ insertedLength: replacement.effectiveReplace.length
265
330
  }))
266
331
  };
267
332
  }
@@ -280,6 +345,7 @@ export function buildLocalizedReplacementChange(compilation, outputText, target
280
345
  return {
281
346
  find: replacement.find,
282
347
  replace: replacement.replace,
348
+ ...(replacement.matchMode === 'space_equivalent' ? { matchMode: replacement.matchMode } : {}),
283
349
  occurrence: replacement.occurrence,
284
350
  beforeExcerpt: boundedExcerpt(sourceText, replacement.start, replacement.end),
285
351
  afterExcerpt: boundedExcerpt(desiredText, afterStart, afterEnd)
@@ -252,6 +252,7 @@ export interface LocalizedReplacementChange {
252
252
  find: string;
253
253
  replace: string;
254
254
  occurrence?: number;
255
+ matchMode?: 'space_equivalent';
255
256
  beforeExcerpt: string;
256
257
  afterExcerpt: string;
257
258
  }>;