@adeu/core 1.20.0 → 1.21.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
@@ -1264,6 +1264,7 @@ function* iter_paragraph_content(paragraph) {
1264
1264
  }
1265
1265
 
1266
1266
  // src/mapper.ts
1267
+ var STYLE_MARKER_TEXTS = /* @__PURE__ */ new Set(["**", "__", "*", "_"]);
1267
1268
  var DocumentMapper = class {
1268
1269
  doc;
1269
1270
  clean_view;
@@ -1273,6 +1274,7 @@ var DocumentMapper = class {
1273
1274
  spans = [];
1274
1275
  appendix_start_index = -1;
1275
1276
  _text_chunks = [];
1277
+ _plain_projection = null;
1276
1278
  constructor(doc, clean_view = false, original_view = false) {
1277
1279
  this.doc = doc;
1278
1280
  this.clean_view = clean_view;
@@ -1285,6 +1287,7 @@ var DocumentMapper = class {
1285
1287
  this.spans = [];
1286
1288
  this._text_chunks = [];
1287
1289
  this.full_text = "";
1290
+ this._plain_projection = null;
1288
1291
  for (const part of iter_document_parts(this.doc)) {
1289
1292
  current_offset = this._map_blocks(part, current_offset);
1290
1293
  if (this.spans.length > 0 && this.spans[this.spans.length - 1].text !== "\n\n") {
@@ -1804,6 +1807,64 @@ ${header}`;
1804
1807
  if (remaining) parts.push(escapeRegExp(remaining));
1805
1808
  return parts.join("");
1806
1809
  }
1810
+ /**
1811
+ * Returns [plain_text, offset_map] where plain_text is full_text with the
1812
+ * VIRTUAL markdown style delimiters (bold/italic markers emitted around
1813
+ * formatted runs) removed, and offset_map[i] is the full_text index of
1814
+ * plain_text[i].
1815
+ *
1816
+ * Formatting run boundaries can fall mid-word (e.g. a paragraph projected
1817
+ * as "**Al**pha"), where neither exact matching nor the whitespace-anchored
1818
+ * fuzzy regex can find the plain target "Alpha". Matching against this
1819
+ * projection and mapping the span back to full_text closes that gap (QA H2).
1820
+ *
1821
+ * Built lazily and invalidated by _build_map(): most batches never need it.
1822
+ */
1823
+ _get_plain_projection() {
1824
+ if (this._plain_projection === null) {
1825
+ const chunks = [];
1826
+ const offsets = [];
1827
+ for (const s of this.spans) {
1828
+ if (s.run === null && s.paragraph !== null && STYLE_MARKER_TEXTS.has(s.text)) {
1829
+ continue;
1830
+ }
1831
+ chunks.push(s.text);
1832
+ for (let k = s.start; k < s.end; k++) offsets.push(k);
1833
+ }
1834
+ this._plain_projection = [chunks.join(""), offsets];
1835
+ }
1836
+ return this._plain_projection;
1837
+ }
1838
+ /**
1839
+ * Matches a markdown-stripped target against the plain projection and maps
1840
+ * each hit back to a [start, length] span in full_text. Interior style
1841
+ * markers end up inside the returned span (so "Alpha" over "**Al**pha"
1842
+ * resolves to the "Al**pha" range); markers just outside the matched
1843
+ * characters are excluded.
1844
+ */
1845
+ _find_plain_projection_matches(target_text) {
1846
+ const [plain_text, offsets] = this._get_plain_projection();
1847
+ if (plain_text.length === this.full_text.length) {
1848
+ return [];
1849
+ }
1850
+ const norm_target = this._replace_smart_quotes(
1851
+ this._strip_markdown_formatting(target_text)
1852
+ );
1853
+ if (!norm_target) return [];
1854
+ const norm_plain = this._replace_smart_quotes(plain_text);
1855
+ const results = [];
1856
+ let from = 0;
1857
+ while (true) {
1858
+ const p_start = norm_plain.indexOf(norm_target, from);
1859
+ if (p_start === -1) break;
1860
+ const p_end = p_start + norm_target.length;
1861
+ const raw_start = offsets[p_start];
1862
+ const raw_end = offsets[p_end - 1] + 1;
1863
+ results.push([raw_start, raw_end - raw_start]);
1864
+ from = p_end;
1865
+ }
1866
+ return results;
1867
+ }
1807
1868
  find_match_index(target_text, is_regex = false) {
1808
1869
  if (is_regex) {
1809
1870
  try {
@@ -1825,6 +1886,8 @@ ${header}`;
1825
1886
  start_idx = this.full_text.indexOf(stripped_target);
1826
1887
  return [start_idx, stripped_target.length];
1827
1888
  }
1889
+ const plain_first = this._find_plain_projection_matches(target_text);
1890
+ if (plain_first.length > 0) return plain_first[0];
1828
1891
  try {
1829
1892
  const pattern = new RegExp(this._make_fuzzy_regex(target_text));
1830
1893
  const match = pattern.exec(this.full_text);
@@ -1845,29 +1908,33 @@ ${header}`;
1845
1908
  }
1846
1909
  return [];
1847
1910
  }
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]);
1911
+ const findAllLiteral = (haystack, needle) => {
1912
+ const out = [];
1913
+ if (!needle) return out;
1914
+ let from = 0;
1915
+ while (true) {
1916
+ const idx = haystack.indexOf(needle, from);
1917
+ if (idx === -1) break;
1918
+ out.push([idx, needle.length]);
1919
+ from = idx + needle.length;
1920
+ }
1921
+ return out;
1922
+ };
1923
+ let matches = findAllLiteral(this.full_text, target_text);
1924
+ if (matches.length > 0) return matches;
1853
1925
  const norm_full = this._replace_smart_quotes(this.full_text);
1854
1926
  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]);
1927
+ matches = findAllLiteral(norm_full, norm_target);
1928
+ if (matches.length > 0) return matches;
1859
1929
  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]);
1930
+ matches = findAllLiteral(this.full_text, stripped_target);
1931
+ if (matches.length > 0) return matches;
1932
+ const plain_matches = this._find_plain_projection_matches(target_text);
1933
+ if (plain_matches.length > 0) return plain_matches;
1866
1934
  try {
1867
1935
  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]);
1936
+ const fuzzy = [...this.full_text.matchAll(pattern)];
1937
+ if (fuzzy.length > 0) return fuzzy.map((m) => [m.index, m[0].length]);
1871
1938
  } catch (e) {
1872
1939
  }
1873
1940
  return [];
@@ -2857,18 +2924,30 @@ function format_ambiguity_error(edit_index, target_text, haystack, match_positio
2857
2924
  }
2858
2925
  lines.push(" To resolve, re-send this edit using ONE of these strategies:");
2859
2926
  lines.push(
2860
- ` 1. Set "match_mode": "all" to modify ALL ${total} occurrences (same target_text).`
2927
+ ' 1. RECOMMENDED: Provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2861
2928
  );
2862
2929
  lines.push(
2863
- ' 2. Set "match_mode": "first" to modify only the FIRST occurrence (same target_text).'
2930
+ ` 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
2931
  );
2865
2932
  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").'
2933
+ ' 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
2934
  );
2868
2935
  return lines.join("\n");
2869
2936
  }
2870
2937
 
2938
+ // src/utils/text.ts
2939
+ var REPORT_ECHO_CAP = 500;
2940
+ var PREVIEW_TEXT_CAP = 200;
2941
+ function truncate_middle(text, cap) {
2942
+ if (text === null || text === void 0 || text.length <= cap) return text;
2943
+ const head = Math.max(1, Math.floor(cap * 2 / 3));
2944
+ const tail = Math.max(1, cap - head);
2945
+ const omitted = text.length - head - tail;
2946
+ return `${text.slice(0, head)}\u2026 [${omitted.toLocaleString("en-US")} chars omitted] \u2026${text.slice(-tail)}`;
2947
+ }
2948
+
2871
2949
  // src/engine.ts
2950
+ var PREVIEW_CONTEXT_CHARS = 30;
2872
2951
  function getNextElement(el) {
2873
2952
  let next = el.nextSibling;
2874
2953
  while (next) {
@@ -2943,6 +3022,11 @@ var BatchValidationError = class extends Error {
2943
3022
  this.errors = errors;
2944
3023
  }
2945
3024
  };
3025
+ function sequential_context_hint(applied_so_far) {
3026
+ return `
3027
+ 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).`;
3028
+ }
3029
+ 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.";
2946
3030
  function validate_edit_strings(edits, index_offset = 0) {
2947
3031
  const errors = [];
2948
3032
  for (let i = 0; i < edits.length; i++) {
@@ -3046,7 +3130,7 @@ function validate_edit_strings(edits, index_offset = 0) {
3046
3130
  }
3047
3131
  return errors;
3048
3132
  }
3049
- var RedlineEngine = class {
3133
+ var RedlineEngine = class _RedlineEngine {
3050
3134
  doc;
3051
3135
  author;
3052
3136
  timestamp;
@@ -3193,8 +3277,184 @@ var RedlineEngine = class {
3193
3277
  }
3194
3278
  return "";
3195
3279
  }
3280
+ // CriticMarkup wrapper pairs used when tidying preview context windows.
3281
+ static _PREVIEW_WRAPPER_PAIRS = [
3282
+ ["{--", "--}"],
3283
+ ["{++", "++}"],
3284
+ ["{==", "==}"],
3285
+ ["{>>", "<<}"]
3286
+ ];
3287
+ /**
3288
+ * Makes a fixed-width slice of the raw-view projection presentable: drops
3289
+ * complete {>>...<<} meta blocks (annotations of pre-existing changes, not
3290
+ * part of this edit) and any wrapper fragments the window boundary chopped
3291
+ * in half. Without this, previews leak internal scaffolding like
3292
+ * "[Chg:5 delete]" (QA H1). Mirrors the Python engine.
3293
+ */
3294
+ static _tidy_preview_context(snippet, side) {
3295
+ snippet = snippet.replace(/\{>>[\s\S]*?<<\}/g, "");
3296
+ for (const [open_tok, close_tok] of _RedlineEngine._PREVIEW_WRAPPER_PAIRS) {
3297
+ if (side === "before") {
3298
+ let depth = 0;
3299
+ let cut = 0;
3300
+ let i = 0;
3301
+ while (i < snippet.length) {
3302
+ if (snippet.startsWith(open_tok, i)) {
3303
+ depth += 1;
3304
+ i += open_tok.length;
3305
+ } else if (snippet.startsWith(close_tok, i)) {
3306
+ if (depth === 0) cut = i + close_tok.length;
3307
+ else depth -= 1;
3308
+ i += close_tok.length;
3309
+ } else {
3310
+ i += 1;
3311
+ }
3312
+ }
3313
+ snippet = snippet.substring(cut);
3314
+ } else {
3315
+ const opens = [];
3316
+ let i = 0;
3317
+ while (i < snippet.length) {
3318
+ if (snippet.startsWith(open_tok, i)) {
3319
+ opens.push(i);
3320
+ i += open_tok.length;
3321
+ } else if (snippet.startsWith(close_tok, i)) {
3322
+ if (opens.length > 0) opens.pop();
3323
+ i += close_tok.length;
3324
+ } else {
3325
+ i += 1;
3326
+ }
3327
+ }
3328
+ if (opens.length > 0) snippet = snippet.substring(0, opens[0]);
3329
+ }
3330
+ }
3331
+ if (side === "before") {
3332
+ snippet = snippet.replace(/^[-+=<>]{0,2}\}/, "");
3333
+ } else {
3334
+ snippet = snippet.replace(/\{[-+=<>]{0,2}$/, "");
3335
+ }
3336
+ return snippet;
3337
+ }
3338
+ /**
3339
+ * Snapshots the document text around a resolved edit BEFORE anything is
3340
+ * applied. Previews rendered after the batch mutates the DOM cannot slice
3341
+ * full_text at the stored offsets: applied edits shift offsets and inject
3342
+ * tracked-change markup, which produced garbled previews mixing unrelated
3343
+ * edits and internal scaffolding (QA H1).
3344
+ */
3345
+ _capture_preview_context(edit) {
3346
+ if (edit.type !== "modify") return;
3347
+ const start_idx = edit._resolved_start_idx;
3348
+ if (start_idx === void 0 || start_idx === null) return;
3349
+ const active_mapper = edit._active_mapper_ref || this.mapper;
3350
+ const full_text = active_mapper.full_text;
3351
+ if (!full_text) return;
3352
+ const length = (edit.target_text || "").length;
3353
+ const before = full_text.substring(
3354
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3355
+ start_idx
3356
+ );
3357
+ const after = full_text.substring(
3358
+ start_idx + length,
3359
+ start_idx + length + PREVIEW_CONTEXT_CHARS
3360
+ );
3361
+ edit._preview_context = [
3362
+ _RedlineEngine._tidy_preview_context(before, "before"),
3363
+ _RedlineEngine._tidy_preview_context(after, "after")
3364
+ ];
3365
+ }
3366
+ /**
3367
+ * Like _capture_preview_context, but snapshots the context around the
3368
+ * ORIGINAL edit's full matched span (stashed by _pre_resolve_heuristic_edit),
3369
+ * so the report preview can present the complete logical change of a
3370
+ * compound modification instead of its first sub-edit.
3371
+ */
3372
+ _capture_parent_preview_context(parent) {
3373
+ if (!parent || parent.type !== "modify") return;
3374
+ if (parent._preview_context || !parent._preview_span) return;
3375
+ const [start_idx, match_len] = parent._preview_span;
3376
+ const active_mapper = parent._preview_mapper_ref || this.mapper;
3377
+ const full_text = active_mapper.full_text;
3378
+ if (!full_text) return;
3379
+ const before = full_text.substring(
3380
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3381
+ start_idx
3382
+ );
3383
+ const after = full_text.substring(
3384
+ start_idx + match_len,
3385
+ start_idx + match_len + PREVIEW_CONTEXT_CHARS
3386
+ );
3387
+ parent._preview_context = [
3388
+ _RedlineEngine._tidy_preview_context(before, "before"),
3389
+ _RedlineEngine._tidy_preview_context(after, "after")
3390
+ ];
3391
+ }
3392
+ /**
3393
+ * Renders the preview from the edit's full matched span. The common
3394
+ * prefix/suffix between matched and replacement text is moved into the
3395
+ * surrounding context so the {--...--}{++...++} block shows the minimal
3396
+ * complete change.
3397
+ */
3398
+ _build_full_match_preview(edit) {
3399
+ let [context_before, context_after] = edit._preview_context;
3400
+ let matched = edit._preview_matched_text || "";
3401
+ let new_text = edit._preview_new_text !== void 0 && edit._preview_new_text !== null ? edit._preview_new_text : edit.new_text || "";
3402
+ const [matched_clean, matched_style] = this._parse_markdown_style(matched);
3403
+ const [new_clean, new_style] = this._parse_markdown_style(new_text);
3404
+ if (matched_style && matched_style.startsWith("Heading")) {
3405
+ context_before = context_before + matched.substring(0, matched.length - matched_clean.length);
3406
+ matched = matched_clean;
3407
+ }
3408
+ if (new_style && new_style.startsWith("Heading")) {
3409
+ new_text = new_clean;
3410
+ }
3411
+ const [prefix_len, suffix_len] = trim_common_context(matched, new_text);
3412
+ let display_target = matched.substring(
3413
+ prefix_len,
3414
+ matched.length - suffix_len
3415
+ );
3416
+ let display_new = new_text.substring(
3417
+ prefix_len,
3418
+ new_text.length - suffix_len
3419
+ );
3420
+ context_before = context_before + matched.substring(0, prefix_len);
3421
+ if (suffix_len) {
3422
+ context_after = matched.substring(matched.length - suffix_len) + context_after;
3423
+ }
3424
+ display_target = truncate_middle(display_target, PREVIEW_TEXT_CAP);
3425
+ display_new = truncate_middle(display_new, PREVIEW_TEXT_CAP);
3426
+ let critic_markup;
3427
+ if (!display_target && !display_new) {
3428
+ const anchor = truncate_middle(matched, PREVIEW_TEXT_CAP);
3429
+ const body = anchor ? `{==${anchor}==}` : "";
3430
+ critic_markup = `${context_before.substring(0, context_before.length - matched.length)}${body}${context_after}`;
3431
+ } else {
3432
+ const deletion = display_target ? `{--${display_target}--}` : "";
3433
+ const insertion = display_new ? `{++${display_new}++}` : "";
3434
+ critic_markup = `${context_before}${deletion}${insertion}${context_after}`;
3435
+ }
3436
+ let clean_text = critic_markup;
3437
+ clean_text = clean_text.replace(/\{>>[\s\S]*?<<\}/g, "");
3438
+ clean_text = clean_text.replace(/\{--[\s\S]*?--\}/g, "");
3439
+ clean_text = clean_text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
3440
+ return [critic_markup, clean_text];
3441
+ }
3442
+ /**
3443
+ * The "new text" a batch report should show for an edit. InsertTableRow has
3444
+ * no new_text field — surface its cell contents instead of the misleading
3445
+ * empty string the report used to print (QA M4).
3446
+ */
3447
+ static _report_new_text(edit) {
3448
+ if (edit && edit.type === "insert_row" && Array.isArray(edit.cells)) {
3449
+ return edit.cells.join(" | ");
3450
+ }
3451
+ return edit && edit.new_text || "";
3452
+ }
3196
3453
  _build_edit_context_previews(edit) {
3197
3454
  if (edit.type !== "modify") return [null, null];
3455
+ if (edit._preview_span && edit._preview_context) {
3456
+ return this._build_full_match_preview(edit);
3457
+ }
3198
3458
  if (edit._resolved_proxy_edit) {
3199
3459
  edit = edit._resolved_proxy_edit;
3200
3460
  }
@@ -3213,17 +3473,33 @@ var RedlineEngine = class {
3213
3473
  new_text = clean_new;
3214
3474
  }
3215
3475
  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}`;
3476
+ let context_before;
3477
+ let context_after;
3478
+ if (edit._preview_context) {
3479
+ [context_before, context_after] = edit._preview_context;
3480
+ } else {
3481
+ const active_mapper = edit._active_mapper_ref || this.mapper;
3482
+ const full_text = active_mapper.full_text;
3483
+ if (!full_text) return [null, null];
3484
+ context_before = _RedlineEngine._tidy_preview_context(
3485
+ full_text.substring(
3486
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3487
+ start_idx
3488
+ ),
3489
+ "before"
3490
+ );
3491
+ context_after = _RedlineEngine._tidy_preview_context(
3492
+ full_text.substring(
3493
+ start_idx + length,
3494
+ start_idx + length + PREVIEW_CONTEXT_CHARS
3495
+ ),
3496
+ "after"
3497
+ );
3498
+ }
3499
+ const display_target = truncate_middle(target_text, PREVIEW_TEXT_CAP);
3500
+ const display_new = truncate_middle(new_text, PREVIEW_TEXT_CAP);
3501
+ const insertion = display_new ? `{++${display_new}++}` : "";
3502
+ const critic_markup = `${context_before}{--${display_target}--}${insertion}${context_after}`;
3227
3503
  let clean_text = critic_markup;
3228
3504
  clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
3229
3505
  clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
@@ -4164,7 +4440,7 @@ var RedlineEngine = class {
4164
4440
  const hint = this._nearest_match_hint(edit.target_text, is_regex);
4165
4441
  errors.push(
4166
4442
  `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
4167
- "${edit.target_text}"${hint}`
4443
+ "${truncate_middle(edit.target_text, REPORT_ECHO_CAP)}"${hint}`
4168
4444
  );
4169
4445
  }
4170
4446
  } else if (matches.length > 1 && match_mode === "strict") {
@@ -4265,9 +4541,48 @@ var RedlineEngine = class {
4265
4541
  );
4266
4542
  }
4267
4543
  }
4544
+ if ((edit.type === "insert_row" || edit.type === "delete_row") && matches.length > 0) {
4545
+ const [start, length] = matches[0];
4546
+ const n_cols = _RedlineEngine._column_count_at(
4547
+ target_mapper,
4548
+ start,
4549
+ length
4550
+ );
4551
+ if (n_cols === null) {
4552
+ errors.push(
4553
+ `- 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.`
4554
+ );
4555
+ } else if (edit.type === "insert_row" && Array.isArray(edit.cells) && edit.cells.length > n_cols) {
4556
+ errors.push(
4557
+ `- 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.`
4558
+ );
4559
+ }
4560
+ }
4268
4561
  }
4269
4562
  return errors;
4270
4563
  }
4564
+ /**
4565
+ * Number of columns (w:tc elements) in the table row containing the text at
4566
+ * [start, start+length) in `mapper`, or null if that text is not inside a
4567
+ * table row.
4568
+ */
4569
+ static _column_count_at(mapper, start, length) {
4570
+ for (const s of mapper.spans) {
4571
+ if (s.run === null || s.end <= start || s.start >= start + length) {
4572
+ continue;
4573
+ }
4574
+ let curr = s.run._element;
4575
+ while (curr) {
4576
+ if (curr.nodeType === 1 && curr.tagName === "w:tr") {
4577
+ return findAllDescendants(curr, "w:tc").filter(
4578
+ (tc) => tc.parentNode === curr
4579
+ ).length;
4580
+ }
4581
+ curr = curr.parentNode;
4582
+ }
4583
+ }
4584
+ return null;
4585
+ }
4271
4586
  validate_review_actions(actions) {
4272
4587
  const errors = [];
4273
4588
  for (let i = 0; i < actions.length; i++) {
@@ -4410,19 +4725,25 @@ var RedlineEngine = class {
4410
4725
  });
4411
4726
  if (dry_run_mode) {
4412
4727
  const reports_by_input = new Array(edits.length);
4728
+ const validation_failed_idx = /* @__PURE__ */ new Set();
4413
4729
  for (const { edit, i } of ordered_edits) {
4414
- const single_errors = this.validate_edits([edit], i);
4730
+ let single_errors = this.validate_edits([edit], i);
4415
4731
  if (single_errors.length > 0) {
4732
+ if (applied_edits > 0) {
4733
+ const hint = sequential_context_hint(applied_edits);
4734
+ single_errors = single_errors.map((err) => err + hint);
4735
+ }
4736
+ validation_failed_idx.add(i);
4416
4737
  skipped_edits++;
4417
4738
  const warning = this._check_punctuation_warning(
4418
4739
  edit.target_text || ""
4419
4740
  );
4420
4741
  reports_by_input[i] = {
4421
4742
  status: "failed",
4422
- target_text: edit.target_text || "",
4423
- new_text: edit.new_text || "",
4743
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4744
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4424
4745
  warning,
4425
- error: single_errors[0],
4746
+ error: single_errors.join("\n"),
4426
4747
  critic_markup: null,
4427
4748
  clean_text: null
4428
4749
  };
@@ -4434,8 +4755,8 @@ var RedlineEngine = class {
4434
4755
  const previews = this._build_edit_context_previews(edit);
4435
4756
  reports_by_input[i] = {
4436
4757
  status: "applied",
4437
- target_text: edit.target_text || "",
4438
- new_text: edit.new_text || "",
4758
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4759
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4439
4760
  warning: null,
4440
4761
  error: null,
4441
4762
  critic_markup: previews[0],
@@ -4455,8 +4776,8 @@ var RedlineEngine = class {
4455
4776
  );
4456
4777
  reports_by_input[i] = {
4457
4778
  status: "failed",
4458
- target_text: edit.target_text || "",
4459
- new_text: edit.new_text || "",
4779
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4780
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4460
4781
  warning,
4461
4782
  error: error_msg,
4462
4783
  critic_markup: null,
@@ -4464,18 +4785,45 @@ var RedlineEngine = class {
4464
4785
  };
4465
4786
  }
4466
4787
  }
4788
+ if (validation_failed_idx.size > 0) {
4789
+ applied_edits = 0;
4790
+ skipped_edits = edits.length;
4791
+ for (let i = 0; i < reports_by_input.length; i++) {
4792
+ const report = reports_by_input[i];
4793
+ if (!report || validation_failed_idx.has(i)) continue;
4794
+ if (report.status === "applied") {
4795
+ reports_by_input[i] = {
4796
+ status: "failed",
4797
+ target_text: report.target_text,
4798
+ new_text: report.new_text,
4799
+ warning: null,
4800
+ error: TRANSACTIONAL_NOT_APPLIED_ERROR,
4801
+ critic_markup: null,
4802
+ clean_text: null
4803
+ };
4804
+ }
4805
+ }
4806
+ }
4467
4807
  edits_reports.push(...reports_by_input);
4468
4808
  } else {
4469
4809
  const snapshot = takeSnapshot(this.doc);
4470
4810
  const originalCurrentId = this.current_id;
4471
4811
  try {
4472
4812
  const sequential_errors = [];
4813
+ let applied_so_far = 0;
4473
4814
  for (const { edit, i } of ordered_edits) {
4474
- const single_errors = this.validate_edits([edit], i);
4815
+ let single_errors = this.validate_edits([edit], i);
4475
4816
  if (single_errors.length > 0) {
4817
+ if (applied_so_far > 0) {
4818
+ const hint = sequential_context_hint(applied_so_far);
4819
+ single_errors = single_errors.map((err) => err + hint);
4820
+ }
4476
4821
  sequential_errors.push(...single_errors);
4477
4822
  } else {
4478
4823
  this.apply_edits([edit], page_offsets);
4824
+ if (edit._applied_status) {
4825
+ applied_so_far++;
4826
+ }
4479
4827
  this.mapper = new DocumentMapper(this.doc);
4480
4828
  this.clean_mapper = null;
4481
4829
  }
@@ -4510,8 +4858,8 @@ var RedlineEngine = class {
4510
4858
  }
4511
4859
  edits_reports.push({
4512
4860
  status: success ? "applied" : "failed",
4513
- target_text: edit.target_text || "",
4514
- new_text: edit.new_text || "",
4861
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4862
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4515
4863
  warning,
4516
4864
  error: error_msg,
4517
4865
  critic_markup,
@@ -4613,6 +4961,12 @@ var RedlineEngine = class {
4613
4961
  resolved_edits.sort(
4614
4962
  (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
4615
4963
  );
4964
+ for (const [res_edit] of resolved_edits) {
4965
+ this._capture_preview_context(res_edit);
4966
+ if (res_edit._parent_edit_ref) {
4967
+ this._capture_parent_preview_context(res_edit._parent_edit_ref);
4968
+ }
4969
+ }
4616
4970
  const occupied_ranges = [];
4617
4971
  const counted_split_groups = /* @__PURE__ */ new Set();
4618
4972
  for (const [edit, orig_new] of resolved_edits) {
@@ -4813,7 +5167,15 @@ var RedlineEngine = class {
4813
5167
  const trPr = tr.ownerDocument.createElement("w:trPr");
4814
5168
  new_tr.appendChild(trPr);
4815
5169
  trPr.appendChild(this._create_track_change_tag("w:ins"));
4816
- for (const cellText of edit.cells) {
5170
+ const anchor_cols = findAllDescendants(tr, "w:tc").filter(
5171
+ (tc) => tc.parentNode === tr
5172
+ ).length;
5173
+ let cell_values = Array.isArray(edit.cells) ? [...edit.cells] : [];
5174
+ if (anchor_cols > 0) {
5175
+ while (cell_values.length < anchor_cols) cell_values.push("");
5176
+ cell_values = cell_values.slice(0, anchor_cols);
5177
+ }
5178
+ for (const cellText of cell_values) {
4817
5179
  const tc = tr.ownerDocument.createElement("w:tc");
4818
5180
  const p = tr.ownerDocument.createElement("w:p");
4819
5181
  const r = tr.ownerDocument.createElement("w:r");
@@ -4909,6 +5271,12 @@ var RedlineEngine = class {
4909
5271
  } catch (e) {
4910
5272
  }
4911
5273
  }
5274
+ if (!edit._preview_span) {
5275
+ edit._preview_span = [start_idx, match_len];
5276
+ edit._preview_matched_text = actual_doc_text;
5277
+ edit._preview_new_text = current_effective_new_text;
5278
+ edit._preview_mapper_ref = active_mapper;
5279
+ }
4912
5280
  const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
4913
5281
  edit.target_text
4914
5282
  );