@adeu/core 1.20.0 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,10 +34,13 @@ __export(index_exports, {
34
34
  DocumentMapper: () => DocumentMapper,
35
35
  DocumentObject: () => DocumentObject,
36
36
  RedlineEngine: () => RedlineEngine,
37
+ RegexTimeoutError: () => RegexTimeoutError,
38
+ USER_PATTERN_TIMEOUT_MS: () => USER_PATTERN_TIMEOUT_MS,
37
39
  _extractTextFromDoc: () => _extractTextFromDoc,
38
40
  apply_edits_to_markdown: () => apply_edits_to_markdown,
39
41
  create_unified_diff: () => create_unified_diff,
40
42
  create_word_patch_diff: () => create_word_patch_diff,
43
+ describe_illegal_control_chars: () => describe_illegal_control_chars,
41
44
  extractTextFromBuffer: () => extractTextFromBuffer,
42
45
  extract_outline: () => extract_outline,
43
46
  finalize_document: () => finalize_document,
@@ -45,7 +48,10 @@ __export(index_exports, {
45
48
  identifyEngine: () => identifyEngine,
46
49
  paginate: () => paginate,
47
50
  split_structural_appendix: () => split_structural_appendix,
48
- trim_common_context: () => trim_common_context
51
+ trim_common_context: () => trim_common_context,
52
+ userFindAllMatches: () => userFindAllMatches,
53
+ userSearch: () => userSearch,
54
+ validate_edit_strings: () => validate_edit_strings
49
55
  });
50
56
  module.exports = __toCommonJS(index_exports);
51
57
 
@@ -794,6 +800,62 @@ function extract_comments_data(pkg) {
794
800
  return data;
795
801
  }
796
802
 
803
+ // src/utils/safe-regex.ts
804
+ var vm = __toESM(require("vm"), 1);
805
+ var USER_PATTERN_TIMEOUT_MS = 2e3;
806
+ var RegexTimeoutError = class extends Error {
807
+ pattern;
808
+ constructor(pattern) {
809
+ super(
810
+ `Regular expression exceeded the ${USER_PATTERN_TIMEOUT_MS / 1e3}s matching time budget (catastrophic backtracking). Simplify the pattern \u2014 nested quantifiers like (a+)+ are the usual cause \u2014 or use a literal target instead.`
811
+ );
812
+ this.name = "RegexTimeoutError";
813
+ this.pattern = pattern;
814
+ }
815
+ };
816
+ function runBudgeted(pattern, script, sandbox) {
817
+ try {
818
+ return vm.runInNewContext(script, sandbox, {
819
+ timeout: USER_PATTERN_TIMEOUT_MS
820
+ });
821
+ } catch (e) {
822
+ if (e && e.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
823
+ throw new RegexTimeoutError(pattern);
824
+ }
825
+ throw e;
826
+ }
827
+ }
828
+ function userFindAllMatches(pattern, text, flags = "") {
829
+ const normalized = flags.includes("g") ? flags : flags + "g";
830
+ const re = new RegExp(pattern, normalized);
831
+ const raw = runBudgeted(
832
+ pattern,
833
+ `{
834
+ const out = [];
835
+ let m;
836
+ while ((m = re.exec(text)) !== null) {
837
+ out.push({ start: m.index, end: m.index + m[0].length });
838
+ if (m.index === re.lastIndex) re.lastIndex++;
839
+ }
840
+ out;
841
+ }`,
842
+ { re, text }
843
+ );
844
+ return raw.map((r) => ({ start: r.start, end: r.end }));
845
+ }
846
+ function userSearch(pattern, text, flags = "") {
847
+ const re = new RegExp(pattern, flags.replace("g", ""));
848
+ const raw = runBudgeted(
849
+ pattern,
850
+ `{
851
+ const m = re.exec(text);
852
+ m ? { start: m.index, end: m.index + m[0].length } : null;
853
+ }`,
854
+ { re, text }
855
+ );
856
+ return raw ? { start: raw.start, end: raw.end } : null;
857
+ }
858
+
797
859
  // src/utils/docx.ts
798
860
  var QN_W_P = "w:p";
799
861
  var QN_W_R = "w:r";
@@ -1264,6 +1326,7 @@ function* iter_paragraph_content(paragraph) {
1264
1326
  }
1265
1327
 
1266
1328
  // src/mapper.ts
1329
+ var STYLE_MARKER_TEXTS = /* @__PURE__ */ new Set(["**", "__", "*", "_"]);
1267
1330
  var DocumentMapper = class {
1268
1331
  doc;
1269
1332
  clean_view;
@@ -1273,6 +1336,7 @@ var DocumentMapper = class {
1273
1336
  spans = [];
1274
1337
  appendix_start_index = -1;
1275
1338
  _text_chunks = [];
1339
+ _plain_projection = null;
1276
1340
  constructor(doc, clean_view = false, original_view = false) {
1277
1341
  this.doc = doc;
1278
1342
  this.clean_view = clean_view;
@@ -1285,6 +1349,7 @@ var DocumentMapper = class {
1285
1349
  this.spans = [];
1286
1350
  this._text_chunks = [];
1287
1351
  this.full_text = "";
1352
+ this._plain_projection = null;
1288
1353
  for (const part of iter_document_parts(this.doc)) {
1289
1354
  current_offset = this._map_blocks(part, current_offset);
1290
1355
  if (this.spans.length > 0 && this.spans[this.spans.length - 1].text !== "\n\n") {
@@ -1804,13 +1869,71 @@ ${header}`;
1804
1869
  if (remaining) parts.push(escapeRegExp(remaining));
1805
1870
  return parts.join("");
1806
1871
  }
1872
+ /**
1873
+ * Returns [plain_text, offset_map] where plain_text is full_text with the
1874
+ * VIRTUAL markdown style delimiters (bold/italic markers emitted around
1875
+ * formatted runs) removed, and offset_map[i] is the full_text index of
1876
+ * plain_text[i].
1877
+ *
1878
+ * Formatting run boundaries can fall mid-word (e.g. a paragraph projected
1879
+ * as "**Al**pha"), where neither exact matching nor the whitespace-anchored
1880
+ * fuzzy regex can find the plain target "Alpha". Matching against this
1881
+ * projection and mapping the span back to full_text closes that gap (QA H2).
1882
+ *
1883
+ * Built lazily and invalidated by _build_map(): most batches never need it.
1884
+ */
1885
+ _get_plain_projection() {
1886
+ if (this._plain_projection === null) {
1887
+ const chunks = [];
1888
+ const offsets = [];
1889
+ for (const s of this.spans) {
1890
+ if (s.run === null && s.paragraph !== null && STYLE_MARKER_TEXTS.has(s.text)) {
1891
+ continue;
1892
+ }
1893
+ chunks.push(s.text);
1894
+ for (let k = s.start; k < s.end; k++) offsets.push(k);
1895
+ }
1896
+ this._plain_projection = [chunks.join(""), offsets];
1897
+ }
1898
+ return this._plain_projection;
1899
+ }
1900
+ /**
1901
+ * Matches a markdown-stripped target against the plain projection and maps
1902
+ * each hit back to a [start, length] span in full_text. Interior style
1903
+ * markers end up inside the returned span (so "Alpha" over "**Al**pha"
1904
+ * resolves to the "Al**pha" range); markers just outside the matched
1905
+ * characters are excluded.
1906
+ */
1907
+ _find_plain_projection_matches(target_text) {
1908
+ const [plain_text, offsets] = this._get_plain_projection();
1909
+ if (plain_text.length === this.full_text.length) {
1910
+ return [];
1911
+ }
1912
+ const norm_target = this._replace_smart_quotes(
1913
+ this._strip_markdown_formatting(target_text)
1914
+ );
1915
+ if (!norm_target) return [];
1916
+ const norm_plain = this._replace_smart_quotes(plain_text);
1917
+ const results = [];
1918
+ let from = 0;
1919
+ while (true) {
1920
+ const p_start = norm_plain.indexOf(norm_target, from);
1921
+ if (p_start === -1) break;
1922
+ const p_end = p_start + norm_target.length;
1923
+ const raw_start = offsets[p_start];
1924
+ const raw_end = offsets[p_end - 1] + 1;
1925
+ results.push([raw_start, raw_end - raw_start]);
1926
+ from = p_end;
1927
+ }
1928
+ return results;
1929
+ }
1807
1930
  find_match_index(target_text, is_regex = false) {
1808
1931
  if (is_regex) {
1809
1932
  try {
1810
- const pattern = new RegExp(target_text);
1811
- const match = pattern.exec(this.full_text);
1812
- if (match) return [match.index, match[0].length];
1933
+ const match = userSearch(target_text, this.full_text);
1934
+ if (match) return [match.start, match.end - match.start];
1813
1935
  } catch (e) {
1936
+ if (e instanceof RegexTimeoutError) throw e;
1814
1937
  }
1815
1938
  return [-1, 0];
1816
1939
  }
@@ -1825,6 +1948,8 @@ ${header}`;
1825
1948
  start_idx = this.full_text.indexOf(stripped_target);
1826
1949
  return [start_idx, stripped_target.length];
1827
1950
  }
1951
+ const plain_first = this._find_plain_projection_matches(target_text);
1952
+ if (plain_first.length > 0) return plain_first[0];
1828
1953
  try {
1829
1954
  const pattern = new RegExp(this._make_fuzzy_regex(target_text));
1830
1955
  const match = pattern.exec(this.full_text);
@@ -1837,37 +1962,40 @@ ${header}`;
1837
1962
  if (!target_text) return [];
1838
1963
  if (is_regex) {
1839
1964
  try {
1840
- const pattern = new RegExp(target_text, "g");
1841
- const matches2 = [...this.full_text.matchAll(pattern)];
1842
- if (matches2.length > 0)
1843
- return matches2.map((m) => [m.index, m[0].length]);
1965
+ const matches2 = userFindAllMatches(target_text, this.full_text);
1966
+ if (matches2.length > 0) return matches2.map((m) => [m.start, m.end - m.start]);
1844
1967
  } catch (e) {
1968
+ if (e instanceof RegexTimeoutError) throw e;
1845
1969
  }
1846
1970
  return [];
1847
1971
  }
1848
- const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1849
- let matches = [
1850
- ...this.full_text.matchAll(new RegExp(escapeRegExp(target_text), "g"))
1851
- ];
1852
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1972
+ const findAllLiteral = (haystack, needle) => {
1973
+ const out = [];
1974
+ if (!needle) return out;
1975
+ let from = 0;
1976
+ while (true) {
1977
+ const idx = haystack.indexOf(needle, from);
1978
+ if (idx === -1) break;
1979
+ out.push([idx, needle.length]);
1980
+ from = idx + needle.length;
1981
+ }
1982
+ return out;
1983
+ };
1984
+ let matches = findAllLiteral(this.full_text, target_text);
1985
+ if (matches.length > 0) return matches;
1853
1986
  const norm_full = this._replace_smart_quotes(this.full_text);
1854
1987
  const norm_target = this._replace_smart_quotes(target_text);
1855
- matches = [
1856
- ...norm_full.matchAll(new RegExp(escapeRegExp(norm_target), "g"))
1857
- ];
1858
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1988
+ matches = findAllLiteral(norm_full, norm_target);
1989
+ if (matches.length > 0) return matches;
1859
1990
  const stripped_target = this._strip_markdown_formatting(target_text);
1860
- matches = [
1861
- ...this.full_text.matchAll(
1862
- new RegExp(escapeRegExp(stripped_target), "g")
1863
- )
1864
- ];
1865
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1991
+ matches = findAllLiteral(this.full_text, stripped_target);
1992
+ if (matches.length > 0) return matches;
1993
+ const plain_matches = this._find_plain_projection_matches(target_text);
1994
+ if (plain_matches.length > 0) return plain_matches;
1866
1995
  try {
1867
1996
  const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
1868
- matches = [...this.full_text.matchAll(pattern)];
1869
- if (matches.length > 0)
1870
- return matches.map((m) => [m.index, m[0].length]);
1997
+ const fuzzy = [...this.full_text.matchAll(pattern)];
1998
+ if (fuzzy.length > 0) return fuzzy.map((m) => [m.index, m[0].length]);
1871
1999
  } catch (e) {
1872
2000
  }
1873
2001
  return [];
@@ -2815,7 +2943,9 @@ function apply_edits_to_markdown(markdown_text, edits, include_index = false, hi
2815
2943
  isolated_target,
2816
2944
  isolated_new,
2817
2945
  edit.comment,
2818
- orig_idx,
2946
+ // 1-based, matching apply's "Edit N" reports and batch validation
2947
+ // errors (QA 2026-07-17 F10; mirrors Python).
2948
+ orig_idx + 1,
2819
2949
  include_index,
2820
2950
  highlight_only
2821
2951
  );
@@ -2857,18 +2987,30 @@ function format_ambiguity_error(edit_index, target_text, haystack, match_positio
2857
2987
  }
2858
2988
  lines.push(" To resolve, re-send this edit using ONE of these strategies:");
2859
2989
  lines.push(
2860
- ` 1. Set "match_mode": "all" to modify ALL ${total} occurrences (same target_text).`
2990
+ ' 1. RECOMMENDED: Provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2861
2991
  );
2862
2992
  lines.push(
2863
- ' 2. Set "match_mode": "first" to modify only the FIRST occurrence (same target_text).'
2993
+ ` 2. Set "match_mode": "all" to modify ALL ${total} occurrences \u2014 only after verifying from the occurrence list above that EVERY occurrence should change.`
2864
2994
  );
2865
2995
  lines.push(
2866
- ' 3. Please provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2996
+ ' 3. Set "match_mode": "first" to modify only the FIRST occurrence \u2014 only after verifying the first occurrence above is the one you intend to change.'
2867
2997
  );
2868
2998
  return lines.join("\n");
2869
2999
  }
2870
3000
 
3001
+ // src/utils/text.ts
3002
+ var REPORT_ECHO_CAP = 500;
3003
+ var PREVIEW_TEXT_CAP = 200;
3004
+ function truncate_middle(text, cap) {
3005
+ if (text === null || text === void 0 || text.length <= cap) return text;
3006
+ const head = Math.max(1, Math.floor(cap * 2 / 3));
3007
+ const tail = Math.max(1, cap - head);
3008
+ const omitted = text.length - head - tail;
3009
+ return `${text.slice(0, head)}\u2026 [${omitted.toLocaleString("en-US")} chars omitted] \u2026${text.slice(-tail)}`;
3010
+ }
3011
+
2871
3012
  // src/engine.ts
3013
+ var PREVIEW_CONTEXT_CHARS = 30;
2872
3014
  function getNextElement(el) {
2873
3015
  let next = el.nextSibling;
2874
3016
  while (next) {
@@ -2943,12 +3085,41 @@ var BatchValidationError = class extends Error {
2943
3085
  this.errors = errors;
2944
3086
  }
2945
3087
  };
3088
+ function sequential_context_hint(applied_so_far) {
3089
+ return `
3090
+ Note: ${applied_so_far} earlier edit(s) in this batch were already applied. Batches apply sequentially \u2014 each edit must target the document text as it reads AFTER the preceding edits (e.g. target the replacement text an earlier edit introduced, not the original wording).`;
3091
+ }
3092
+ var TRANSACTIONAL_NOT_APPLIED_ERROR = "Not applied: the batch is transactional and other edits failed validation (see their errors). Fix or remove those edits and re-run.";
3093
+ var XML_ILLEGAL_CHARS_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
3094
+ function describe_illegal_control_chars(text) {
3095
+ if (!text) return null;
3096
+ const found = text.match(XML_ILLEGAL_CHARS_RE);
3097
+ if (!found) return null;
3098
+ const codes = Array.from(new Set(found.map((c) => `0x${c.charCodeAt(0).toString(16).padStart(2, "0")}`))).sort();
3099
+ return codes.join(", ");
3100
+ }
2946
3101
  function validate_edit_strings(edits, index_offset = 0) {
2947
3102
  const errors = [];
2948
3103
  for (let i = 0; i < edits.length; i++) {
2949
3104
  const edit = edits[i];
2950
3105
  const t_text = edit.target_text || "";
2951
3106
  const n_text = edit.new_text || "";
3107
+ const checked_fields = [
3108
+ ["target_text", t_text],
3109
+ ["new_text", n_text]
3110
+ ];
3111
+ if (edit.comment) checked_fields.push(["comment", edit.comment]);
3112
+ (edit.cells || []).forEach((cell, cell_idx) => {
3113
+ checked_fields.push([`cells[${cell_idx}]`, cell || ""]);
3114
+ });
3115
+ for (const [field_name, field_value] of checked_fields) {
3116
+ const described = describe_illegal_control_chars(field_value);
3117
+ if (described) {
3118
+ errors.push(
3119
+ `- Edit ${i + 1 + index_offset} Failed: \`${field_name}\` contains control character(s) (${described}) that cannot be stored in a DOCX. Remove them and re-submit.`
3120
+ );
3121
+ }
3122
+ }
2952
3123
  if (n_text.includes("{++") || n_text.includes("{--") || n_text.includes("{>>") || n_text.includes("{==")) {
2953
3124
  errors.push(
2954
3125
  `- Edit ${i + 1 + index_offset} Failed: Do not manually write CriticMarkup tags ({++, {--, {>>, {==) in \`new_text\`. The engine handles redlining automatically. To add a comment, use the \`comment\` parameter.`
@@ -3046,7 +3217,7 @@ function validate_edit_strings(edits, index_offset = 0) {
3046
3217
  }
3047
3218
  return errors;
3048
3219
  }
3049
- var RedlineEngine = class {
3220
+ var RedlineEngine = class _RedlineEngine {
3050
3221
  doc;
3051
3222
  author;
3052
3223
  timestamp;
@@ -3193,8 +3364,184 @@ var RedlineEngine = class {
3193
3364
  }
3194
3365
  return "";
3195
3366
  }
3367
+ // CriticMarkup wrapper pairs used when tidying preview context windows.
3368
+ static _PREVIEW_WRAPPER_PAIRS = [
3369
+ ["{--", "--}"],
3370
+ ["{++", "++}"],
3371
+ ["{==", "==}"],
3372
+ ["{>>", "<<}"]
3373
+ ];
3374
+ /**
3375
+ * Makes a fixed-width slice of the raw-view projection presentable: drops
3376
+ * complete {>>...<<} meta blocks (annotations of pre-existing changes, not
3377
+ * part of this edit) and any wrapper fragments the window boundary chopped
3378
+ * in half. Without this, previews leak internal scaffolding like
3379
+ * "[Chg:5 delete]" (QA H1). Mirrors the Python engine.
3380
+ */
3381
+ static _tidy_preview_context(snippet, side) {
3382
+ snippet = snippet.replace(/\{>>[\s\S]*?<<\}/g, "");
3383
+ for (const [open_tok, close_tok] of _RedlineEngine._PREVIEW_WRAPPER_PAIRS) {
3384
+ if (side === "before") {
3385
+ let depth = 0;
3386
+ let cut = 0;
3387
+ let i = 0;
3388
+ while (i < snippet.length) {
3389
+ if (snippet.startsWith(open_tok, i)) {
3390
+ depth += 1;
3391
+ i += open_tok.length;
3392
+ } else if (snippet.startsWith(close_tok, i)) {
3393
+ if (depth === 0) cut = i + close_tok.length;
3394
+ else depth -= 1;
3395
+ i += close_tok.length;
3396
+ } else {
3397
+ i += 1;
3398
+ }
3399
+ }
3400
+ snippet = snippet.substring(cut);
3401
+ } else {
3402
+ const opens = [];
3403
+ let i = 0;
3404
+ while (i < snippet.length) {
3405
+ if (snippet.startsWith(open_tok, i)) {
3406
+ opens.push(i);
3407
+ i += open_tok.length;
3408
+ } else if (snippet.startsWith(close_tok, i)) {
3409
+ if (opens.length > 0) opens.pop();
3410
+ i += close_tok.length;
3411
+ } else {
3412
+ i += 1;
3413
+ }
3414
+ }
3415
+ if (opens.length > 0) snippet = snippet.substring(0, opens[0]);
3416
+ }
3417
+ }
3418
+ if (side === "before") {
3419
+ snippet = snippet.replace(/^[-+=<>]{0,2}\}/, "");
3420
+ } else {
3421
+ snippet = snippet.replace(/\{[-+=<>]{0,2}$/, "");
3422
+ }
3423
+ return snippet;
3424
+ }
3425
+ /**
3426
+ * Snapshots the document text around a resolved edit BEFORE anything is
3427
+ * applied. Previews rendered after the batch mutates the DOM cannot slice
3428
+ * full_text at the stored offsets: applied edits shift offsets and inject
3429
+ * tracked-change markup, garbling previews with unrelated edits and
3430
+ * internal scaffolding (QA H1).
3431
+ */
3432
+ _capture_preview_context(edit) {
3433
+ if (edit.type !== "modify") return;
3434
+ const start_idx = edit._resolved_start_idx;
3435
+ if (start_idx === void 0 || start_idx === null) return;
3436
+ const active_mapper = edit._active_mapper_ref || this.mapper;
3437
+ const full_text = active_mapper.full_text;
3438
+ if (!full_text) return;
3439
+ const length = (edit.target_text || "").length;
3440
+ const before = full_text.substring(
3441
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3442
+ start_idx
3443
+ );
3444
+ const after = full_text.substring(
3445
+ start_idx + length,
3446
+ start_idx + length + PREVIEW_CONTEXT_CHARS
3447
+ );
3448
+ edit._preview_context = [
3449
+ _RedlineEngine._tidy_preview_context(before, "before"),
3450
+ _RedlineEngine._tidy_preview_context(after, "after")
3451
+ ];
3452
+ }
3453
+ /**
3454
+ * Like _capture_preview_context, but snapshots the context around the
3455
+ * ORIGINAL edit's full matched span (stashed by _pre_resolve_heuristic_edit),
3456
+ * so the report preview can present the complete logical change of a
3457
+ * compound modification instead of its first sub-edit.
3458
+ */
3459
+ _capture_parent_preview_context(parent) {
3460
+ if (!parent || parent.type !== "modify") return;
3461
+ if (parent._preview_context || !parent._preview_span) return;
3462
+ const [start_idx, match_len] = parent._preview_span;
3463
+ const active_mapper = parent._preview_mapper_ref || this.mapper;
3464
+ const full_text = active_mapper.full_text;
3465
+ if (!full_text) return;
3466
+ const before = full_text.substring(
3467
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3468
+ start_idx
3469
+ );
3470
+ const after = full_text.substring(
3471
+ start_idx + match_len,
3472
+ start_idx + match_len + PREVIEW_CONTEXT_CHARS
3473
+ );
3474
+ parent._preview_context = [
3475
+ _RedlineEngine._tidy_preview_context(before, "before"),
3476
+ _RedlineEngine._tidy_preview_context(after, "after")
3477
+ ];
3478
+ }
3479
+ /**
3480
+ * Renders the preview from the edit's full matched span. The common
3481
+ * prefix/suffix between matched and replacement text is moved into the
3482
+ * surrounding context so the {--...--}{++...++} block shows the minimal
3483
+ * complete change.
3484
+ */
3485
+ _build_full_match_preview(edit) {
3486
+ let [context_before, context_after] = edit._preview_context;
3487
+ let matched = edit._preview_matched_text || "";
3488
+ let new_text = edit._preview_new_text !== void 0 && edit._preview_new_text !== null ? edit._preview_new_text : edit.new_text || "";
3489
+ const [matched_clean, matched_style] = this._parse_markdown_style(matched);
3490
+ const [new_clean, new_style] = this._parse_markdown_style(new_text);
3491
+ if (matched_style && matched_style.startsWith("Heading")) {
3492
+ context_before = context_before + matched.substring(0, matched.length - matched_clean.length);
3493
+ matched = matched_clean;
3494
+ }
3495
+ if (new_style && new_style.startsWith("Heading")) {
3496
+ new_text = new_clean;
3497
+ }
3498
+ const [prefix_len, suffix_len] = trim_common_context(matched, new_text);
3499
+ let display_target = matched.substring(
3500
+ prefix_len,
3501
+ matched.length - suffix_len
3502
+ );
3503
+ let display_new = new_text.substring(
3504
+ prefix_len,
3505
+ new_text.length - suffix_len
3506
+ );
3507
+ context_before = context_before + matched.substring(0, prefix_len);
3508
+ if (suffix_len) {
3509
+ context_after = matched.substring(matched.length - suffix_len) + context_after;
3510
+ }
3511
+ display_target = truncate_middle(display_target, PREVIEW_TEXT_CAP);
3512
+ display_new = truncate_middle(display_new, PREVIEW_TEXT_CAP);
3513
+ let critic_markup;
3514
+ if (!display_target && !display_new) {
3515
+ const anchor = truncate_middle(matched, PREVIEW_TEXT_CAP);
3516
+ const body = anchor ? `{==${anchor}==}` : "";
3517
+ critic_markup = `${context_before.substring(0, context_before.length - matched.length)}${body}${context_after}`;
3518
+ } else {
3519
+ const deletion = display_target ? `{--${display_target}--}` : "";
3520
+ const insertion = display_new ? `{++${display_new}++}` : "";
3521
+ critic_markup = `${context_before}${deletion}${insertion}${context_after}`;
3522
+ }
3523
+ let clean_text = critic_markup;
3524
+ clean_text = clean_text.replace(/\{>>[\s\S]*?<<\}/g, "");
3525
+ clean_text = clean_text.replace(/\{--[\s\S]*?--\}/g, "");
3526
+ clean_text = clean_text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
3527
+ return [critic_markup, clean_text];
3528
+ }
3529
+ /**
3530
+ * The "new text" a batch report should show for an edit. InsertTableRow has
3531
+ * no new_text field — surface its cell contents rather than a misleading
3532
+ * empty string (QA M4).
3533
+ */
3534
+ static _report_new_text(edit) {
3535
+ if (edit && edit.type === "insert_row" && Array.isArray(edit.cells)) {
3536
+ return edit.cells.join(" | ");
3537
+ }
3538
+ return edit && edit.new_text || "";
3539
+ }
3196
3540
  _build_edit_context_previews(edit) {
3197
3541
  if (edit.type !== "modify") return [null, null];
3542
+ if (edit._preview_span && edit._preview_context) {
3543
+ return this._build_full_match_preview(edit);
3544
+ }
3198
3545
  if (edit._resolved_proxy_edit) {
3199
3546
  edit = edit._resolved_proxy_edit;
3200
3547
  }
@@ -3213,17 +3560,33 @@ var RedlineEngine = class {
3213
3560
  new_text = clean_new;
3214
3561
  }
3215
3562
  const length = target_text.length;
3216
- const active_mapper = edit._active_mapper_ref || this.mapper;
3217
- const full_text = active_mapper.full_text;
3218
- if (!full_text) return [null, null];
3219
- const before_start = Math.max(0, start_idx - 30);
3220
- const context_before = full_text.substring(before_start, start_idx);
3221
- const context_after = full_text.substring(
3222
- start_idx + length,
3223
- start_idx + length + 30
3224
- );
3225
- const insertion = new_text ? `{++${new_text}++}` : "";
3226
- const critic_markup = `${context_before}{--${target_text}--}${insertion}${context_after}`;
3563
+ let context_before;
3564
+ let context_after;
3565
+ if (edit._preview_context) {
3566
+ [context_before, context_after] = edit._preview_context;
3567
+ } else {
3568
+ const active_mapper = edit._active_mapper_ref || this.mapper;
3569
+ const full_text = active_mapper.full_text;
3570
+ if (!full_text) return [null, null];
3571
+ context_before = _RedlineEngine._tidy_preview_context(
3572
+ full_text.substring(
3573
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3574
+ start_idx
3575
+ ),
3576
+ "before"
3577
+ );
3578
+ context_after = _RedlineEngine._tidy_preview_context(
3579
+ full_text.substring(
3580
+ start_idx + length,
3581
+ start_idx + length + PREVIEW_CONTEXT_CHARS
3582
+ ),
3583
+ "after"
3584
+ );
3585
+ }
3586
+ const display_target = truncate_middle(target_text, PREVIEW_TEXT_CAP);
3587
+ const display_new = truncate_middle(new_text, PREVIEW_TEXT_CAP);
3588
+ const insertion = display_new ? `{++${display_new}++}` : "";
3589
+ const critic_markup = `${context_before}{--${display_target}--}${insertion}${context_after}`;
3227
3590
  let clean_text = critic_markup;
3228
3591
  clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
3229
3592
  clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
@@ -4164,7 +4527,7 @@ var RedlineEngine = class {
4164
4527
  const hint = this._nearest_match_hint(edit.target_text, is_regex);
4165
4528
  errors.push(
4166
4529
  `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
4167
- "${edit.target_text}"${hint}`
4530
+ "${truncate_middle(edit.target_text, REPORT_ECHO_CAP)}"${hint}`
4168
4531
  );
4169
4532
  }
4170
4533
  } else if (matches.length > 1 && match_mode === "strict") {
@@ -4265,9 +4628,48 @@ var RedlineEngine = class {
4265
4628
  );
4266
4629
  }
4267
4630
  }
4631
+ if ((edit.type === "insert_row" || edit.type === "delete_row") && matches.length > 0) {
4632
+ const [start, length] = matches[0];
4633
+ const n_cols = _RedlineEngine._column_count_at(
4634
+ target_mapper,
4635
+ start,
4636
+ length
4637
+ );
4638
+ if (n_cols === null) {
4639
+ errors.push(
4640
+ `- Edit ${i + 1 + index_offset} Failed: ${edit.type} target text was found, but it is not inside a table row. Anchor the operation on text that appears within the table.`
4641
+ );
4642
+ } else if (edit.type === "insert_row" && Array.isArray(edit.cells) && edit.cells.length > n_cols) {
4643
+ errors.push(
4644
+ `- Edit ${i + 1 + index_offset} Failed: insert_row provides ${edit.cells.length} cells but the target table has ${n_cols} column(s). The extra cell(s) would be dropped. Provide at most ${n_cols} cells \u2014 rows given fewer cells are padded with empty ones.`
4645
+ );
4646
+ }
4647
+ }
4268
4648
  }
4269
4649
  return errors;
4270
4650
  }
4651
+ /**
4652
+ * Number of columns (w:tc elements) in the table row containing the text at
4653
+ * [start, start+length) in `mapper`, or null if that text is not inside a
4654
+ * table row.
4655
+ */
4656
+ static _column_count_at(mapper, start, length) {
4657
+ for (const s of mapper.spans) {
4658
+ if (s.run === null || s.end <= start || s.start >= start + length) {
4659
+ continue;
4660
+ }
4661
+ let curr = s.run._element;
4662
+ while (curr) {
4663
+ if (curr.nodeType === 1 && curr.tagName === "w:tr") {
4664
+ return findAllDescendants(curr, "w:tc").filter(
4665
+ (tc) => tc.parentNode === curr
4666
+ ).length;
4667
+ }
4668
+ curr = curr.parentNode;
4669
+ }
4670
+ }
4671
+ return null;
4672
+ }
4271
4673
  validate_review_actions(actions) {
4272
4674
  const errors = [];
4273
4675
  for (let i = 0; i < actions.length; i++) {
@@ -4410,19 +4812,31 @@ var RedlineEngine = class {
4410
4812
  });
4411
4813
  if (dry_run_mode) {
4412
4814
  const reports_by_input = new Array(edits.length);
4815
+ const validation_failed_idx = /* @__PURE__ */ new Set();
4413
4816
  for (const { edit, i } of ordered_edits) {
4414
- const single_errors = this.validate_edits([edit], i);
4817
+ let single_errors;
4818
+ try {
4819
+ single_errors = this.validate_edits([edit], i);
4820
+ } catch (e) {
4821
+ if (!(e instanceof RegexTimeoutError)) throw e;
4822
+ single_errors = [`- Edit ${i + 1} Failed: ${e.message}`];
4823
+ }
4415
4824
  if (single_errors.length > 0) {
4825
+ if (applied_edits > 0) {
4826
+ const hint = sequential_context_hint(applied_edits);
4827
+ single_errors = single_errors.map((err) => err + hint);
4828
+ }
4829
+ validation_failed_idx.add(i);
4416
4830
  skipped_edits++;
4417
4831
  const warning = this._check_punctuation_warning(
4418
4832
  edit.target_text || ""
4419
4833
  );
4420
4834
  reports_by_input[i] = {
4421
4835
  status: "failed",
4422
- target_text: edit.target_text || "",
4423
- new_text: edit.new_text || "",
4836
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4837
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4424
4838
  warning,
4425
- error: single_errors[0],
4839
+ error: single_errors.join("\n"),
4426
4840
  critic_markup: null,
4427
4841
  clean_text: null
4428
4842
  };
@@ -4434,8 +4848,8 @@ var RedlineEngine = class {
4434
4848
  const previews = this._build_edit_context_previews(edit);
4435
4849
  reports_by_input[i] = {
4436
4850
  status: "applied",
4437
- target_text: edit.target_text || "",
4438
- new_text: edit.new_text || "",
4851
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4852
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4439
4853
  warning: null,
4440
4854
  error: null,
4441
4855
  critic_markup: previews[0],
@@ -4455,8 +4869,8 @@ var RedlineEngine = class {
4455
4869
  );
4456
4870
  reports_by_input[i] = {
4457
4871
  status: "failed",
4458
- target_text: edit.target_text || "",
4459
- new_text: edit.new_text || "",
4872
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4873
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4460
4874
  warning,
4461
4875
  error: error_msg,
4462
4876
  critic_markup: null,
@@ -4464,18 +4878,51 @@ var RedlineEngine = class {
4464
4878
  };
4465
4879
  }
4466
4880
  }
4881
+ if (validation_failed_idx.size > 0) {
4882
+ applied_edits = 0;
4883
+ skipped_edits = edits.length;
4884
+ for (let i = 0; i < reports_by_input.length; i++) {
4885
+ const report = reports_by_input[i];
4886
+ if (!report || validation_failed_idx.has(i)) continue;
4887
+ if (report.status === "applied") {
4888
+ reports_by_input[i] = {
4889
+ status: "failed",
4890
+ target_text: report.target_text,
4891
+ new_text: report.new_text,
4892
+ warning: null,
4893
+ error: TRANSACTIONAL_NOT_APPLIED_ERROR,
4894
+ critic_markup: null,
4895
+ clean_text: null
4896
+ };
4897
+ }
4898
+ }
4899
+ }
4467
4900
  edits_reports.push(...reports_by_input);
4468
4901
  } else {
4469
4902
  const snapshot = takeSnapshot(this.doc);
4470
4903
  const originalCurrentId = this.current_id;
4471
4904
  try {
4472
4905
  const sequential_errors = [];
4906
+ let applied_so_far = 0;
4473
4907
  for (const { edit, i } of ordered_edits) {
4474
- const single_errors = this.validate_edits([edit], i);
4908
+ let single_errors;
4909
+ try {
4910
+ single_errors = this.validate_edits([edit], i);
4911
+ } catch (e) {
4912
+ if (!(e instanceof RegexTimeoutError)) throw e;
4913
+ single_errors = [`- Edit ${i + 1} Failed: ${e.message}`];
4914
+ }
4475
4915
  if (single_errors.length > 0) {
4916
+ if (applied_so_far > 0) {
4917
+ const hint = sequential_context_hint(applied_so_far);
4918
+ single_errors = single_errors.map((err) => err + hint);
4919
+ }
4476
4920
  sequential_errors.push(...single_errors);
4477
4921
  } else {
4478
4922
  this.apply_edits([edit], page_offsets);
4923
+ if (edit._applied_status) {
4924
+ applied_so_far++;
4925
+ }
4479
4926
  this.mapper = new DocumentMapper(this.doc);
4480
4927
  this.clean_mapper = null;
4481
4928
  }
@@ -4510,8 +4957,8 @@ var RedlineEngine = class {
4510
4957
  }
4511
4958
  edits_reports.push({
4512
4959
  status: success ? "applied" : "failed",
4513
- target_text: edit.target_text || "",
4514
- new_text: edit.new_text || "",
4960
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4961
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4515
4962
  warning,
4516
4963
  error: error_msg,
4517
4964
  critic_markup,
@@ -4578,7 +5025,18 @@ var RedlineEngine = class {
4578
5025
  edit._error_msg = msg;
4579
5026
  }
4580
5027
  } else {
4581
- const resolved = this._pre_resolve_heuristic_edit(edit);
5028
+ let resolved;
5029
+ try {
5030
+ resolved = this._pre_resolve_heuristic_edit(edit);
5031
+ } catch (e) {
5032
+ if (!(e instanceof RegexTimeoutError)) throw e;
5033
+ skipped++;
5034
+ edit._applied_status = false;
5035
+ const msg = `- Failed to apply edit targeting: '${(edit.target_text || "").substring(0, 40)}...' (${e.message})`;
5036
+ this.skipped_details.push(msg);
5037
+ edit._error_msg = msg;
5038
+ continue;
5039
+ }
4582
5040
  if (resolved) {
4583
5041
  if (Array.isArray(resolved)) {
4584
5042
  for (const r of resolved) {
@@ -4613,6 +5071,12 @@ var RedlineEngine = class {
4613
5071
  resolved_edits.sort(
4614
5072
  (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
4615
5073
  );
5074
+ for (const [res_edit] of resolved_edits) {
5075
+ this._capture_preview_context(res_edit);
5076
+ if (res_edit._parent_edit_ref) {
5077
+ this._capture_parent_preview_context(res_edit._parent_edit_ref);
5078
+ }
5079
+ }
4616
5080
  const occupied_ranges = [];
4617
5081
  const counted_split_groups = /* @__PURE__ */ new Set();
4618
5082
  for (const [edit, orig_new] of resolved_edits) {
@@ -4731,6 +5195,16 @@ var RedlineEngine = class {
4731
5195
  );
4732
5196
  continue;
4733
5197
  }
5198
+ const dup_authors = Array.from(
5199
+ new Set(all_nodes.map((n) => n.getAttribute("w:author") || "Unknown"))
5200
+ ).sort();
5201
+ if (dup_authors.length > 1) {
5202
+ skipped++;
5203
+ this.skipped_details.push(
5204
+ `- Failed to apply action: ${type} on Chg:${target_id} is ambiguous. The document contains ${all_nodes.length} tracked-change elements sharing w:id=${target_id} from different authors (${dup_authors.join(", ")}) \u2014 duplicate revision IDs produced outside this engine (e.g. by a document merge or copy-paste). Acting on this ID would resolve all of them at once. Resolve these changes individually in Word, or apply the intended outcome as an explicit text edit instead.`
5205
+ );
5206
+ continue;
5207
+ }
4734
5208
  for (const node of all_nodes) {
4735
5209
  const is_ins = node.tagName === "w:ins";
4736
5210
  const parent_tag = node.parentNode ? node.parentNode.tagName : "";
@@ -4813,7 +5287,15 @@ var RedlineEngine = class {
4813
5287
  const trPr = tr.ownerDocument.createElement("w:trPr");
4814
5288
  new_tr.appendChild(trPr);
4815
5289
  trPr.appendChild(this._create_track_change_tag("w:ins"));
4816
- for (const cellText of edit.cells) {
5290
+ const anchor_cols = findAllDescendants(tr, "w:tc").filter(
5291
+ (tc) => tc.parentNode === tr
5292
+ ).length;
5293
+ let cell_values = Array.isArray(edit.cells) ? [...edit.cells] : [];
5294
+ if (anchor_cols > 0) {
5295
+ while (cell_values.length < anchor_cols) cell_values.push("");
5296
+ cell_values = cell_values.slice(0, anchor_cols);
5297
+ }
5298
+ for (const cellText of cell_values) {
4817
5299
  const tc = tr.ownerDocument.createElement("w:tc");
4818
5300
  const p = tr.ownerDocument.createElement("w:p");
4819
5301
  const r = tr.ownerDocument.createElement("w:r");
@@ -4909,6 +5391,12 @@ var RedlineEngine = class {
4909
5391
  } catch (e) {
4910
5392
  }
4911
5393
  }
5394
+ if (!edit._preview_span) {
5395
+ edit._preview_span = [start_idx, match_len];
5396
+ edit._preview_matched_text = actual_doc_text;
5397
+ edit._preview_new_text = current_effective_new_text;
5398
+ edit._preview_mapper_ref = active_mapper;
5399
+ }
4912
5400
  const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
4913
5401
  edit.target_text
4914
5402
  );
@@ -6032,6 +6520,27 @@ function scrub_doc_properties(doc) {
6032
6520
  c.textContent = "";
6033
6521
  }
6034
6522
  });
6523
+ const titles = findDescendantsByLocalName(corePart._element, "title");
6524
+ titles.forEach((c) => {
6525
+ if (c.textContent) {
6526
+ lines.push(`Title kept (review manually): "${c.textContent}"`);
6527
+ }
6528
+ });
6529
+ const leakFields = [
6530
+ ["category", "Category"],
6531
+ ["keywords", "Keywords"],
6532
+ ["subject", "Subject"],
6533
+ ["contentStatus", "Content status"],
6534
+ ["description", "Description/comments"]
6535
+ ];
6536
+ for (const [local, label] of leakFields) {
6537
+ findDescendantsByLocalName(corePart._element, local).forEach((c) => {
6538
+ if (c.textContent) {
6539
+ lines.push(`${label}: ${c.textContent}`);
6540
+ c.textContent = "";
6541
+ }
6542
+ });
6543
+ }
6035
6544
  const revisions = findDescendantsByLocalName(corePart._element, "revision");
6036
6545
  revisions.forEach((c) => {
6037
6546
  if (c.textContent && parseInt(c.textContent) > 1) {
@@ -6237,7 +6746,11 @@ function extract_all_domain_metadata(doc, base_text) {
6237
6746
  for (const term of def_keys) {
6238
6747
  if (definitions[term].count === 0) {
6239
6748
  delete definitions[term];
6240
- duplicates.delete(term);
6749
+ if (!duplicates.has(term)) {
6750
+ diagnostics.push(
6751
+ `[Warning] Unused Definition: '${term}' is defined but never used.`
6752
+ );
6753
+ }
6241
6754
  }
6242
6755
  }
6243
6756
  }
@@ -7573,10 +8086,13 @@ function identifyEngine() {
7573
8086
  DocumentMapper,
7574
8087
  DocumentObject,
7575
8088
  RedlineEngine,
8089
+ RegexTimeoutError,
8090
+ USER_PATTERN_TIMEOUT_MS,
7576
8091
  _extractTextFromDoc,
7577
8092
  apply_edits_to_markdown,
7578
8093
  create_unified_diff,
7579
8094
  create_word_patch_diff,
8095
+ describe_illegal_control_chars,
7580
8096
  extractTextFromBuffer,
7581
8097
  extract_outline,
7582
8098
  finalize_document,
@@ -7584,6 +8100,9 @@ function identifyEngine() {
7584
8100
  identifyEngine,
7585
8101
  paginate,
7586
8102
  split_structural_appendix,
7587
- trim_common_context
8103
+ trim_common_context,
8104
+ userFindAllMatches,
8105
+ userSearch,
8106
+ validate_edit_strings
7588
8107
  });
7589
8108
  //# sourceMappingURL=index.cjs.map