@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.js CHANGED
@@ -1213,6 +1213,7 @@ function* iter_paragraph_content(paragraph) {
1213
1213
  }
1214
1214
 
1215
1215
  // src/mapper.ts
1216
+ var STYLE_MARKER_TEXTS = /* @__PURE__ */ new Set(["**", "__", "*", "_"]);
1216
1217
  var DocumentMapper = class {
1217
1218
  doc;
1218
1219
  clean_view;
@@ -1222,6 +1223,7 @@ var DocumentMapper = class {
1222
1223
  spans = [];
1223
1224
  appendix_start_index = -1;
1224
1225
  _text_chunks = [];
1226
+ _plain_projection = null;
1225
1227
  constructor(doc, clean_view = false, original_view = false) {
1226
1228
  this.doc = doc;
1227
1229
  this.clean_view = clean_view;
@@ -1234,6 +1236,7 @@ var DocumentMapper = class {
1234
1236
  this.spans = [];
1235
1237
  this._text_chunks = [];
1236
1238
  this.full_text = "";
1239
+ this._plain_projection = null;
1237
1240
  for (const part of iter_document_parts(this.doc)) {
1238
1241
  current_offset = this._map_blocks(part, current_offset);
1239
1242
  if (this.spans.length > 0 && this.spans[this.spans.length - 1].text !== "\n\n") {
@@ -1753,6 +1756,64 @@ ${header}`;
1753
1756
  if (remaining) parts.push(escapeRegExp(remaining));
1754
1757
  return parts.join("");
1755
1758
  }
1759
+ /**
1760
+ * Returns [plain_text, offset_map] where plain_text is full_text with the
1761
+ * VIRTUAL markdown style delimiters (bold/italic markers emitted around
1762
+ * formatted runs) removed, and offset_map[i] is the full_text index of
1763
+ * plain_text[i].
1764
+ *
1765
+ * Formatting run boundaries can fall mid-word (e.g. a paragraph projected
1766
+ * as "**Al**pha"), where neither exact matching nor the whitespace-anchored
1767
+ * fuzzy regex can find the plain target "Alpha". Matching against this
1768
+ * projection and mapping the span back to full_text closes that gap (QA H2).
1769
+ *
1770
+ * Built lazily and invalidated by _build_map(): most batches never need it.
1771
+ */
1772
+ _get_plain_projection() {
1773
+ if (this._plain_projection === null) {
1774
+ const chunks = [];
1775
+ const offsets = [];
1776
+ for (const s of this.spans) {
1777
+ if (s.run === null && s.paragraph !== null && STYLE_MARKER_TEXTS.has(s.text)) {
1778
+ continue;
1779
+ }
1780
+ chunks.push(s.text);
1781
+ for (let k = s.start; k < s.end; k++) offsets.push(k);
1782
+ }
1783
+ this._plain_projection = [chunks.join(""), offsets];
1784
+ }
1785
+ return this._plain_projection;
1786
+ }
1787
+ /**
1788
+ * Matches a markdown-stripped target against the plain projection and maps
1789
+ * each hit back to a [start, length] span in full_text. Interior style
1790
+ * markers end up inside the returned span (so "Alpha" over "**Al**pha"
1791
+ * resolves to the "Al**pha" range); markers just outside the matched
1792
+ * characters are excluded.
1793
+ */
1794
+ _find_plain_projection_matches(target_text) {
1795
+ const [plain_text, offsets] = this._get_plain_projection();
1796
+ if (plain_text.length === this.full_text.length) {
1797
+ return [];
1798
+ }
1799
+ const norm_target = this._replace_smart_quotes(
1800
+ this._strip_markdown_formatting(target_text)
1801
+ );
1802
+ if (!norm_target) return [];
1803
+ const norm_plain = this._replace_smart_quotes(plain_text);
1804
+ const results = [];
1805
+ let from = 0;
1806
+ while (true) {
1807
+ const p_start = norm_plain.indexOf(norm_target, from);
1808
+ if (p_start === -1) break;
1809
+ const p_end = p_start + norm_target.length;
1810
+ const raw_start = offsets[p_start];
1811
+ const raw_end = offsets[p_end - 1] + 1;
1812
+ results.push([raw_start, raw_end - raw_start]);
1813
+ from = p_end;
1814
+ }
1815
+ return results;
1816
+ }
1756
1817
  find_match_index(target_text, is_regex = false) {
1757
1818
  if (is_regex) {
1758
1819
  try {
@@ -1774,6 +1835,8 @@ ${header}`;
1774
1835
  start_idx = this.full_text.indexOf(stripped_target);
1775
1836
  return [start_idx, stripped_target.length];
1776
1837
  }
1838
+ const plain_first = this._find_plain_projection_matches(target_text);
1839
+ if (plain_first.length > 0) return plain_first[0];
1777
1840
  try {
1778
1841
  const pattern = new RegExp(this._make_fuzzy_regex(target_text));
1779
1842
  const match = pattern.exec(this.full_text);
@@ -1794,29 +1857,33 @@ ${header}`;
1794
1857
  }
1795
1858
  return [];
1796
1859
  }
1797
- const escapeRegExp = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1798
- let matches = [
1799
- ...this.full_text.matchAll(new RegExp(escapeRegExp(target_text), "g"))
1800
- ];
1801
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1860
+ const findAllLiteral = (haystack, needle) => {
1861
+ const out = [];
1862
+ if (!needle) return out;
1863
+ let from = 0;
1864
+ while (true) {
1865
+ const idx = haystack.indexOf(needle, from);
1866
+ if (idx === -1) break;
1867
+ out.push([idx, needle.length]);
1868
+ from = idx + needle.length;
1869
+ }
1870
+ return out;
1871
+ };
1872
+ let matches = findAllLiteral(this.full_text, target_text);
1873
+ if (matches.length > 0) return matches;
1802
1874
  const norm_full = this._replace_smart_quotes(this.full_text);
1803
1875
  const norm_target = this._replace_smart_quotes(target_text);
1804
- matches = [
1805
- ...norm_full.matchAll(new RegExp(escapeRegExp(norm_target), "g"))
1806
- ];
1807
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1876
+ matches = findAllLiteral(norm_full, norm_target);
1877
+ if (matches.length > 0) return matches;
1808
1878
  const stripped_target = this._strip_markdown_formatting(target_text);
1809
- matches = [
1810
- ...this.full_text.matchAll(
1811
- new RegExp(escapeRegExp(stripped_target), "g")
1812
- )
1813
- ];
1814
- if (matches.length > 0) return matches.map((m) => [m.index, m[0].length]);
1879
+ matches = findAllLiteral(this.full_text, stripped_target);
1880
+ if (matches.length > 0) return matches;
1881
+ const plain_matches = this._find_plain_projection_matches(target_text);
1882
+ if (plain_matches.length > 0) return plain_matches;
1815
1883
  try {
1816
1884
  const pattern = new RegExp(this._make_fuzzy_regex(target_text), "g");
1817
- matches = [...this.full_text.matchAll(pattern)];
1818
- if (matches.length > 0)
1819
- return matches.map((m) => [m.index, m[0].length]);
1885
+ const fuzzy = [...this.full_text.matchAll(pattern)];
1886
+ if (fuzzy.length > 0) return fuzzy.map((m) => [m.index, m[0].length]);
1820
1887
  } catch (e) {
1821
1888
  }
1822
1889
  return [];
@@ -2806,18 +2873,30 @@ function format_ambiguity_error(edit_index, target_text, haystack, match_positio
2806
2873
  }
2807
2874
  lines.push(" To resolve, re-send this edit using ONE of these strategies:");
2808
2875
  lines.push(
2809
- ` 1. Set "match_mode": "all" to modify ALL ${total} occurrences (same target_text).`
2876
+ ' 1. RECOMMENDED: Provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2810
2877
  );
2811
2878
  lines.push(
2812
- ' 2. Set "match_mode": "first" to modify only the FIRST occurrence (same target_text).'
2879
+ ` 2. Set "match_mode": "all" to modify ALL ${total} occurrences \u2014 only after verifying from the occurrence list above that EVERY occurrence should change.`
2813
2880
  );
2814
2881
  lines.push(
2815
- ' 3. Please provide more surrounding context in your target_text to uniquely identify a single location (keep the default "match_mode": "strict").'
2882
+ ' 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.'
2816
2883
  );
2817
2884
  return lines.join("\n");
2818
2885
  }
2819
2886
 
2887
+ // src/utils/text.ts
2888
+ var REPORT_ECHO_CAP = 500;
2889
+ var PREVIEW_TEXT_CAP = 200;
2890
+ function truncate_middle(text, cap) {
2891
+ if (text === null || text === void 0 || text.length <= cap) return text;
2892
+ const head = Math.max(1, Math.floor(cap * 2 / 3));
2893
+ const tail = Math.max(1, cap - head);
2894
+ const omitted = text.length - head - tail;
2895
+ return `${text.slice(0, head)}\u2026 [${omitted.toLocaleString("en-US")} chars omitted] \u2026${text.slice(-tail)}`;
2896
+ }
2897
+
2820
2898
  // src/engine.ts
2899
+ var PREVIEW_CONTEXT_CHARS = 30;
2821
2900
  function getNextElement(el) {
2822
2901
  let next = el.nextSibling;
2823
2902
  while (next) {
@@ -2892,6 +2971,11 @@ var BatchValidationError = class extends Error {
2892
2971
  this.errors = errors;
2893
2972
  }
2894
2973
  };
2974
+ function sequential_context_hint(applied_so_far) {
2975
+ return `
2976
+ 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).`;
2977
+ }
2978
+ 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.";
2895
2979
  function validate_edit_strings(edits, index_offset = 0) {
2896
2980
  const errors = [];
2897
2981
  for (let i = 0; i < edits.length; i++) {
@@ -2995,7 +3079,7 @@ function validate_edit_strings(edits, index_offset = 0) {
2995
3079
  }
2996
3080
  return errors;
2997
3081
  }
2998
- var RedlineEngine = class {
3082
+ var RedlineEngine = class _RedlineEngine {
2999
3083
  doc;
3000
3084
  author;
3001
3085
  timestamp;
@@ -3142,8 +3226,184 @@ var RedlineEngine = class {
3142
3226
  }
3143
3227
  return "";
3144
3228
  }
3229
+ // CriticMarkup wrapper pairs used when tidying preview context windows.
3230
+ static _PREVIEW_WRAPPER_PAIRS = [
3231
+ ["{--", "--}"],
3232
+ ["{++", "++}"],
3233
+ ["{==", "==}"],
3234
+ ["{>>", "<<}"]
3235
+ ];
3236
+ /**
3237
+ * Makes a fixed-width slice of the raw-view projection presentable: drops
3238
+ * complete {>>...<<} meta blocks (annotations of pre-existing changes, not
3239
+ * part of this edit) and any wrapper fragments the window boundary chopped
3240
+ * in half. Without this, previews leak internal scaffolding like
3241
+ * "[Chg:5 delete]" (QA H1). Mirrors the Python engine.
3242
+ */
3243
+ static _tidy_preview_context(snippet, side) {
3244
+ snippet = snippet.replace(/\{>>[\s\S]*?<<\}/g, "");
3245
+ for (const [open_tok, close_tok] of _RedlineEngine._PREVIEW_WRAPPER_PAIRS) {
3246
+ if (side === "before") {
3247
+ let depth = 0;
3248
+ let cut = 0;
3249
+ let i = 0;
3250
+ while (i < snippet.length) {
3251
+ if (snippet.startsWith(open_tok, i)) {
3252
+ depth += 1;
3253
+ i += open_tok.length;
3254
+ } else if (snippet.startsWith(close_tok, i)) {
3255
+ if (depth === 0) cut = i + close_tok.length;
3256
+ else depth -= 1;
3257
+ i += close_tok.length;
3258
+ } else {
3259
+ i += 1;
3260
+ }
3261
+ }
3262
+ snippet = snippet.substring(cut);
3263
+ } else {
3264
+ const opens = [];
3265
+ let i = 0;
3266
+ while (i < snippet.length) {
3267
+ if (snippet.startsWith(open_tok, i)) {
3268
+ opens.push(i);
3269
+ i += open_tok.length;
3270
+ } else if (snippet.startsWith(close_tok, i)) {
3271
+ if (opens.length > 0) opens.pop();
3272
+ i += close_tok.length;
3273
+ } else {
3274
+ i += 1;
3275
+ }
3276
+ }
3277
+ if (opens.length > 0) snippet = snippet.substring(0, opens[0]);
3278
+ }
3279
+ }
3280
+ if (side === "before") {
3281
+ snippet = snippet.replace(/^[-+=<>]{0,2}\}/, "");
3282
+ } else {
3283
+ snippet = snippet.replace(/\{[-+=<>]{0,2}$/, "");
3284
+ }
3285
+ return snippet;
3286
+ }
3287
+ /**
3288
+ * Snapshots the document text around a resolved edit BEFORE anything is
3289
+ * applied. Previews rendered after the batch mutates the DOM cannot slice
3290
+ * full_text at the stored offsets: applied edits shift offsets and inject
3291
+ * tracked-change markup, which produced garbled previews mixing unrelated
3292
+ * edits and internal scaffolding (QA H1).
3293
+ */
3294
+ _capture_preview_context(edit) {
3295
+ if (edit.type !== "modify") return;
3296
+ const start_idx = edit._resolved_start_idx;
3297
+ if (start_idx === void 0 || start_idx === null) return;
3298
+ const active_mapper = edit._active_mapper_ref || this.mapper;
3299
+ const full_text = active_mapper.full_text;
3300
+ if (!full_text) return;
3301
+ const length = (edit.target_text || "").length;
3302
+ const before = full_text.substring(
3303
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3304
+ start_idx
3305
+ );
3306
+ const after = full_text.substring(
3307
+ start_idx + length,
3308
+ start_idx + length + PREVIEW_CONTEXT_CHARS
3309
+ );
3310
+ edit._preview_context = [
3311
+ _RedlineEngine._tidy_preview_context(before, "before"),
3312
+ _RedlineEngine._tidy_preview_context(after, "after")
3313
+ ];
3314
+ }
3315
+ /**
3316
+ * Like _capture_preview_context, but snapshots the context around the
3317
+ * ORIGINAL edit's full matched span (stashed by _pre_resolve_heuristic_edit),
3318
+ * so the report preview can present the complete logical change of a
3319
+ * compound modification instead of its first sub-edit.
3320
+ */
3321
+ _capture_parent_preview_context(parent) {
3322
+ if (!parent || parent.type !== "modify") return;
3323
+ if (parent._preview_context || !parent._preview_span) return;
3324
+ const [start_idx, match_len] = parent._preview_span;
3325
+ const active_mapper = parent._preview_mapper_ref || this.mapper;
3326
+ const full_text = active_mapper.full_text;
3327
+ if (!full_text) return;
3328
+ const before = full_text.substring(
3329
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3330
+ start_idx
3331
+ );
3332
+ const after = full_text.substring(
3333
+ start_idx + match_len,
3334
+ start_idx + match_len + PREVIEW_CONTEXT_CHARS
3335
+ );
3336
+ parent._preview_context = [
3337
+ _RedlineEngine._tidy_preview_context(before, "before"),
3338
+ _RedlineEngine._tidy_preview_context(after, "after")
3339
+ ];
3340
+ }
3341
+ /**
3342
+ * Renders the preview from the edit's full matched span. The common
3343
+ * prefix/suffix between matched and replacement text is moved into the
3344
+ * surrounding context so the {--...--}{++...++} block shows the minimal
3345
+ * complete change.
3346
+ */
3347
+ _build_full_match_preview(edit) {
3348
+ let [context_before, context_after] = edit._preview_context;
3349
+ let matched = edit._preview_matched_text || "";
3350
+ let new_text = edit._preview_new_text !== void 0 && edit._preview_new_text !== null ? edit._preview_new_text : edit.new_text || "";
3351
+ const [matched_clean, matched_style] = this._parse_markdown_style(matched);
3352
+ const [new_clean, new_style] = this._parse_markdown_style(new_text);
3353
+ if (matched_style && matched_style.startsWith("Heading")) {
3354
+ context_before = context_before + matched.substring(0, matched.length - matched_clean.length);
3355
+ matched = matched_clean;
3356
+ }
3357
+ if (new_style && new_style.startsWith("Heading")) {
3358
+ new_text = new_clean;
3359
+ }
3360
+ const [prefix_len, suffix_len] = trim_common_context(matched, new_text);
3361
+ let display_target = matched.substring(
3362
+ prefix_len,
3363
+ matched.length - suffix_len
3364
+ );
3365
+ let display_new = new_text.substring(
3366
+ prefix_len,
3367
+ new_text.length - suffix_len
3368
+ );
3369
+ context_before = context_before + matched.substring(0, prefix_len);
3370
+ if (suffix_len) {
3371
+ context_after = matched.substring(matched.length - suffix_len) + context_after;
3372
+ }
3373
+ display_target = truncate_middle(display_target, PREVIEW_TEXT_CAP);
3374
+ display_new = truncate_middle(display_new, PREVIEW_TEXT_CAP);
3375
+ let critic_markup;
3376
+ if (!display_target && !display_new) {
3377
+ const anchor = truncate_middle(matched, PREVIEW_TEXT_CAP);
3378
+ const body = anchor ? `{==${anchor}==}` : "";
3379
+ critic_markup = `${context_before.substring(0, context_before.length - matched.length)}${body}${context_after}`;
3380
+ } else {
3381
+ const deletion = display_target ? `{--${display_target}--}` : "";
3382
+ const insertion = display_new ? `{++${display_new}++}` : "";
3383
+ critic_markup = `${context_before}${deletion}${insertion}${context_after}`;
3384
+ }
3385
+ let clean_text = critic_markup;
3386
+ clean_text = clean_text.replace(/\{>>[\s\S]*?<<\}/g, "");
3387
+ clean_text = clean_text.replace(/\{--[\s\S]*?--\}/g, "");
3388
+ clean_text = clean_text.replace(/\{\+\+([\s\S]*?)\+\+\}/g, "$1");
3389
+ return [critic_markup, clean_text];
3390
+ }
3391
+ /**
3392
+ * The "new text" a batch report should show for an edit. InsertTableRow has
3393
+ * no new_text field — surface its cell contents instead of the misleading
3394
+ * empty string the report used to print (QA M4).
3395
+ */
3396
+ static _report_new_text(edit) {
3397
+ if (edit && edit.type === "insert_row" && Array.isArray(edit.cells)) {
3398
+ return edit.cells.join(" | ");
3399
+ }
3400
+ return edit && edit.new_text || "";
3401
+ }
3145
3402
  _build_edit_context_previews(edit) {
3146
3403
  if (edit.type !== "modify") return [null, null];
3404
+ if (edit._preview_span && edit._preview_context) {
3405
+ return this._build_full_match_preview(edit);
3406
+ }
3147
3407
  if (edit._resolved_proxy_edit) {
3148
3408
  edit = edit._resolved_proxy_edit;
3149
3409
  }
@@ -3162,17 +3422,33 @@ var RedlineEngine = class {
3162
3422
  new_text = clean_new;
3163
3423
  }
3164
3424
  const length = target_text.length;
3165
- const active_mapper = edit._active_mapper_ref || this.mapper;
3166
- const full_text = active_mapper.full_text;
3167
- if (!full_text) return [null, null];
3168
- const before_start = Math.max(0, start_idx - 30);
3169
- const context_before = full_text.substring(before_start, start_idx);
3170
- const context_after = full_text.substring(
3171
- start_idx + length,
3172
- start_idx + length + 30
3173
- );
3174
- const insertion = new_text ? `{++${new_text}++}` : "";
3175
- const critic_markup = `${context_before}{--${target_text}--}${insertion}${context_after}`;
3425
+ let context_before;
3426
+ let context_after;
3427
+ if (edit._preview_context) {
3428
+ [context_before, context_after] = edit._preview_context;
3429
+ } else {
3430
+ const active_mapper = edit._active_mapper_ref || this.mapper;
3431
+ const full_text = active_mapper.full_text;
3432
+ if (!full_text) return [null, null];
3433
+ context_before = _RedlineEngine._tidy_preview_context(
3434
+ full_text.substring(
3435
+ Math.max(0, start_idx - PREVIEW_CONTEXT_CHARS),
3436
+ start_idx
3437
+ ),
3438
+ "before"
3439
+ );
3440
+ context_after = _RedlineEngine._tidy_preview_context(
3441
+ full_text.substring(
3442
+ start_idx + length,
3443
+ start_idx + length + PREVIEW_CONTEXT_CHARS
3444
+ ),
3445
+ "after"
3446
+ );
3447
+ }
3448
+ const display_target = truncate_middle(target_text, PREVIEW_TEXT_CAP);
3449
+ const display_new = truncate_middle(new_text, PREVIEW_TEXT_CAP);
3450
+ const insertion = display_new ? `{++${display_new}++}` : "";
3451
+ const critic_markup = `${context_before}{--${display_target}--}${insertion}${context_after}`;
3176
3452
  let clean_text = critic_markup;
3177
3453
  clean_text = clean_text.replace(/\{>>.*?<<\}/gs, "");
3178
3454
  clean_text = clean_text.replace(/\{--.*?--\}/gs, "");
@@ -4113,7 +4389,7 @@ var RedlineEngine = class {
4113
4389
  const hint = this._nearest_match_hint(edit.target_text, is_regex);
4114
4390
  errors.push(
4115
4391
  `- Edit ${i + 1 + index_offset} Failed: Target text not found in document:
4116
- "${edit.target_text}"${hint}`
4392
+ "${truncate_middle(edit.target_text, REPORT_ECHO_CAP)}"${hint}`
4117
4393
  );
4118
4394
  }
4119
4395
  } else if (matches.length > 1 && match_mode === "strict") {
@@ -4214,9 +4490,48 @@ var RedlineEngine = class {
4214
4490
  );
4215
4491
  }
4216
4492
  }
4493
+ if ((edit.type === "insert_row" || edit.type === "delete_row") && matches.length > 0) {
4494
+ const [start, length] = matches[0];
4495
+ const n_cols = _RedlineEngine._column_count_at(
4496
+ target_mapper,
4497
+ start,
4498
+ length
4499
+ );
4500
+ if (n_cols === null) {
4501
+ errors.push(
4502
+ `- 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.`
4503
+ );
4504
+ } else if (edit.type === "insert_row" && Array.isArray(edit.cells) && edit.cells.length > n_cols) {
4505
+ errors.push(
4506
+ `- 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.`
4507
+ );
4508
+ }
4509
+ }
4217
4510
  }
4218
4511
  return errors;
4219
4512
  }
4513
+ /**
4514
+ * Number of columns (w:tc elements) in the table row containing the text at
4515
+ * [start, start+length) in `mapper`, or null if that text is not inside a
4516
+ * table row.
4517
+ */
4518
+ static _column_count_at(mapper, start, length) {
4519
+ for (const s of mapper.spans) {
4520
+ if (s.run === null || s.end <= start || s.start >= start + length) {
4521
+ continue;
4522
+ }
4523
+ let curr = s.run._element;
4524
+ while (curr) {
4525
+ if (curr.nodeType === 1 && curr.tagName === "w:tr") {
4526
+ return findAllDescendants(curr, "w:tc").filter(
4527
+ (tc) => tc.parentNode === curr
4528
+ ).length;
4529
+ }
4530
+ curr = curr.parentNode;
4531
+ }
4532
+ }
4533
+ return null;
4534
+ }
4220
4535
  validate_review_actions(actions) {
4221
4536
  const errors = [];
4222
4537
  for (let i = 0; i < actions.length; i++) {
@@ -4359,19 +4674,25 @@ var RedlineEngine = class {
4359
4674
  });
4360
4675
  if (dry_run_mode) {
4361
4676
  const reports_by_input = new Array(edits.length);
4677
+ const validation_failed_idx = /* @__PURE__ */ new Set();
4362
4678
  for (const { edit, i } of ordered_edits) {
4363
- const single_errors = this.validate_edits([edit], i);
4679
+ let single_errors = this.validate_edits([edit], i);
4364
4680
  if (single_errors.length > 0) {
4681
+ if (applied_edits > 0) {
4682
+ const hint = sequential_context_hint(applied_edits);
4683
+ single_errors = single_errors.map((err) => err + hint);
4684
+ }
4685
+ validation_failed_idx.add(i);
4365
4686
  skipped_edits++;
4366
4687
  const warning = this._check_punctuation_warning(
4367
4688
  edit.target_text || ""
4368
4689
  );
4369
4690
  reports_by_input[i] = {
4370
4691
  status: "failed",
4371
- target_text: edit.target_text || "",
4372
- new_text: edit.new_text || "",
4692
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4693
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4373
4694
  warning,
4374
- error: single_errors[0],
4695
+ error: single_errors.join("\n"),
4375
4696
  critic_markup: null,
4376
4697
  clean_text: null
4377
4698
  };
@@ -4383,8 +4704,8 @@ var RedlineEngine = class {
4383
4704
  const previews = this._build_edit_context_previews(edit);
4384
4705
  reports_by_input[i] = {
4385
4706
  status: "applied",
4386
- target_text: edit.target_text || "",
4387
- new_text: edit.new_text || "",
4707
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4708
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4388
4709
  warning: null,
4389
4710
  error: null,
4390
4711
  critic_markup: previews[0],
@@ -4404,8 +4725,8 @@ var RedlineEngine = class {
4404
4725
  );
4405
4726
  reports_by_input[i] = {
4406
4727
  status: "failed",
4407
- target_text: edit.target_text || "",
4408
- new_text: edit.new_text || "",
4728
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4729
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4409
4730
  warning,
4410
4731
  error: error_msg,
4411
4732
  critic_markup: null,
@@ -4413,18 +4734,45 @@ var RedlineEngine = class {
4413
4734
  };
4414
4735
  }
4415
4736
  }
4737
+ if (validation_failed_idx.size > 0) {
4738
+ applied_edits = 0;
4739
+ skipped_edits = edits.length;
4740
+ for (let i = 0; i < reports_by_input.length; i++) {
4741
+ const report = reports_by_input[i];
4742
+ if (!report || validation_failed_idx.has(i)) continue;
4743
+ if (report.status === "applied") {
4744
+ reports_by_input[i] = {
4745
+ status: "failed",
4746
+ target_text: report.target_text,
4747
+ new_text: report.new_text,
4748
+ warning: null,
4749
+ error: TRANSACTIONAL_NOT_APPLIED_ERROR,
4750
+ critic_markup: null,
4751
+ clean_text: null
4752
+ };
4753
+ }
4754
+ }
4755
+ }
4416
4756
  edits_reports.push(...reports_by_input);
4417
4757
  } else {
4418
4758
  const snapshot = takeSnapshot(this.doc);
4419
4759
  const originalCurrentId = this.current_id;
4420
4760
  try {
4421
4761
  const sequential_errors = [];
4762
+ let applied_so_far = 0;
4422
4763
  for (const { edit, i } of ordered_edits) {
4423
- const single_errors = this.validate_edits([edit], i);
4764
+ let single_errors = this.validate_edits([edit], i);
4424
4765
  if (single_errors.length > 0) {
4766
+ if (applied_so_far > 0) {
4767
+ const hint = sequential_context_hint(applied_so_far);
4768
+ single_errors = single_errors.map((err) => err + hint);
4769
+ }
4425
4770
  sequential_errors.push(...single_errors);
4426
4771
  } else {
4427
4772
  this.apply_edits([edit], page_offsets);
4773
+ if (edit._applied_status) {
4774
+ applied_so_far++;
4775
+ }
4428
4776
  this.mapper = new DocumentMapper(this.doc);
4429
4777
  this.clean_mapper = null;
4430
4778
  }
@@ -4459,8 +4807,8 @@ var RedlineEngine = class {
4459
4807
  }
4460
4808
  edits_reports.push({
4461
4809
  status: success ? "applied" : "failed",
4462
- target_text: edit.target_text || "",
4463
- new_text: edit.new_text || "",
4810
+ target_text: truncate_middle(edit.target_text || "", REPORT_ECHO_CAP),
4811
+ new_text: truncate_middle(_RedlineEngine._report_new_text(edit), REPORT_ECHO_CAP),
4464
4812
  warning,
4465
4813
  error: error_msg,
4466
4814
  critic_markup,
@@ -4562,6 +4910,12 @@ var RedlineEngine = class {
4562
4910
  resolved_edits.sort(
4563
4911
  (a, b) => (b[0]._resolved_start_idx || 0) - (a[0]._resolved_start_idx || 0)
4564
4912
  );
4913
+ for (const [res_edit] of resolved_edits) {
4914
+ this._capture_preview_context(res_edit);
4915
+ if (res_edit._parent_edit_ref) {
4916
+ this._capture_parent_preview_context(res_edit._parent_edit_ref);
4917
+ }
4918
+ }
4565
4919
  const occupied_ranges = [];
4566
4920
  const counted_split_groups = /* @__PURE__ */ new Set();
4567
4921
  for (const [edit, orig_new] of resolved_edits) {
@@ -4762,7 +5116,15 @@ var RedlineEngine = class {
4762
5116
  const trPr = tr.ownerDocument.createElement("w:trPr");
4763
5117
  new_tr.appendChild(trPr);
4764
5118
  trPr.appendChild(this._create_track_change_tag("w:ins"));
4765
- for (const cellText of edit.cells) {
5119
+ const anchor_cols = findAllDescendants(tr, "w:tc").filter(
5120
+ (tc) => tc.parentNode === tr
5121
+ ).length;
5122
+ let cell_values = Array.isArray(edit.cells) ? [...edit.cells] : [];
5123
+ if (anchor_cols > 0) {
5124
+ while (cell_values.length < anchor_cols) cell_values.push("");
5125
+ cell_values = cell_values.slice(0, anchor_cols);
5126
+ }
5127
+ for (const cellText of cell_values) {
4766
5128
  const tc = tr.ownerDocument.createElement("w:tc");
4767
5129
  const p = tr.ownerDocument.createElement("w:p");
4768
5130
  const r = tr.ownerDocument.createElement("w:r");
@@ -4858,6 +5220,12 @@ var RedlineEngine = class {
4858
5220
  } catch (e) {
4859
5221
  }
4860
5222
  }
5223
+ if (!edit._preview_span) {
5224
+ edit._preview_span = [start_idx, match_len];
5225
+ edit._preview_matched_text = actual_doc_text;
5226
+ edit._preview_new_text = current_effective_new_text;
5227
+ edit._preview_mapper_ref = active_mapper;
5228
+ }
4861
5229
  const [edit_target_clean, edit_target_style] = this._parse_markdown_style(
4862
5230
  edit.target_text
4863
5231
  );