@adeu/core 1.21.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.d.cts CHANGED
@@ -197,6 +197,8 @@ declare class BatchValidationError extends Error {
197
197
  errors: string[];
198
198
  constructor(errors: string[]);
199
199
  }
200
+ declare function describe_illegal_control_chars(text: string): string | null;
201
+ declare function validate_edit_strings(edits: any[], index_offset?: number): string[];
200
202
  declare class RedlineEngine {
201
203
  doc: DocumentObject;
202
204
  author: string;
@@ -243,8 +245,8 @@ declare class RedlineEngine {
243
245
  * Snapshots the document text around a resolved edit BEFORE anything is
244
246
  * applied. Previews rendered after the batch mutates the DOM cannot slice
245
247
  * full_text at the stored offsets: applied edits shift offsets and inject
246
- * tracked-change markup, which produced garbled previews mixing unrelated
247
- * edits and internal scaffolding (QA H1).
248
+ * tracked-change markup, garbling previews with unrelated edits and
249
+ * internal scaffolding (QA H1).
248
250
  */
249
251
  private _capture_preview_context;
250
252
  /**
@@ -263,8 +265,8 @@ declare class RedlineEngine {
263
265
  private _build_full_match_preview;
264
266
  /**
265
267
  * The "new text" a batch report should show for an edit. InsertTableRow has
266
- * no new_text field — surface its cell contents instead of the misleading
267
- * empty string the report used to print (QA M4).
268
+ * no new_text field — surface its cell contents rather than a misleading
269
+ * empty string (QA M4).
268
270
  */
269
271
  private static _report_new_text;
270
272
  private _build_edit_context_previews;
@@ -475,6 +477,42 @@ interface FinalizeResult {
475
477
  }
476
478
  declare function finalize_document(doc: DocumentObject, options: FinalizeOptions): Promise<FinalizeResult>;
477
479
 
480
+ /**
481
+ * Time-budgeted execution of USER/LLM-supplied regular expressions.
482
+ *
483
+ * `regex: true` on a ModifyText edit and `search_regex` on read_docx hand an
484
+ * LLM-controlled pattern to V8's backtracking engine. A pathological pattern
485
+ * like `(a|a)*$` against a run of repeated characters hangs the event loop
486
+ * indefinitely (QA 2026-07-17 F5 — ReDoS; mirrors the Python fix).
487
+ *
488
+ * JS regexes cannot be interrupted in-thread, but V8 CAN interrupt regex
489
+ * execution running inside a `vm` context with a `timeout` — that is the
490
+ * mechanism used here. `node:vm` is a builtin, preserving the zero-dependency
491
+ * bundle constraint.
492
+ *
493
+ * Only USER-SUPPLIED patterns belong here. The engine's own generated
494
+ * patterns (fuzzy matchers etc.) are built to be linear-time.
495
+ */
496
+ declare const USER_PATTERN_TIMEOUT_MS = 2000;
497
+ declare class RegexTimeoutError extends Error {
498
+ pattern: string;
499
+ constructor(pattern: string);
500
+ }
501
+ /**
502
+ * All non-overlapping matches of a user pattern, under the wall-clock budget.
503
+ * Invalid patterns throw SyntaxError exactly like `new RegExp(...)` would.
504
+ * The budget covers the ENTIRE scan, not just the first match.
505
+ */
506
+ declare function userFindAllMatches(pattern: string, text: string, flags?: string): Array<{
507
+ start: number;
508
+ end: number;
509
+ }>;
510
+ /** First match of a user pattern under the wall-clock budget, or null. */
511
+ declare function userSearch(pattern: string, text: string, flags?: string): {
512
+ start: number;
513
+ end: number;
514
+ } | null;
515
+
478
516
  declare function identifyEngine(): string;
479
517
 
480
- export { BatchValidationError, DocumentMapper, DocumentObject, type FinalizeOptions, type FinalizeResult, type OutlineNode, type PageInfo, type PaginationResult, RedlineEngine, type TextSpan, _extractTextFromDoc, apply_edits_to_markdown, create_unified_diff, create_word_patch_diff, extractTextFromBuffer, extract_outline, finalize_document, generate_edits_from_text, identifyEngine, paginate, split_structural_appendix, trim_common_context };
518
+ export { BatchValidationError, DocumentMapper, DocumentObject, type FinalizeOptions, type FinalizeResult, type OutlineNode, type PageInfo, type PaginationResult, RedlineEngine, RegexTimeoutError, type TextSpan, USER_PATTERN_TIMEOUT_MS, _extractTextFromDoc, apply_edits_to_markdown, create_unified_diff, create_word_patch_diff, describe_illegal_control_chars, extractTextFromBuffer, extract_outline, finalize_document, generate_edits_from_text, identifyEngine, paginate, split_structural_appendix, trim_common_context, userFindAllMatches, userSearch, validate_edit_strings };
package/dist/index.d.ts CHANGED
@@ -197,6 +197,8 @@ declare class BatchValidationError extends Error {
197
197
  errors: string[];
198
198
  constructor(errors: string[]);
199
199
  }
200
+ declare function describe_illegal_control_chars(text: string): string | null;
201
+ declare function validate_edit_strings(edits: any[], index_offset?: number): string[];
200
202
  declare class RedlineEngine {
201
203
  doc: DocumentObject;
202
204
  author: string;
@@ -243,8 +245,8 @@ declare class RedlineEngine {
243
245
  * Snapshots the document text around a resolved edit BEFORE anything is
244
246
  * applied. Previews rendered after the batch mutates the DOM cannot slice
245
247
  * full_text at the stored offsets: applied edits shift offsets and inject
246
- * tracked-change markup, which produced garbled previews mixing unrelated
247
- * edits and internal scaffolding (QA H1).
248
+ * tracked-change markup, garbling previews with unrelated edits and
249
+ * internal scaffolding (QA H1).
248
250
  */
249
251
  private _capture_preview_context;
250
252
  /**
@@ -263,8 +265,8 @@ declare class RedlineEngine {
263
265
  private _build_full_match_preview;
264
266
  /**
265
267
  * The "new text" a batch report should show for an edit. InsertTableRow has
266
- * no new_text field — surface its cell contents instead of the misleading
267
- * empty string the report used to print (QA M4).
268
+ * no new_text field — surface its cell contents rather than a misleading
269
+ * empty string (QA M4).
268
270
  */
269
271
  private static _report_new_text;
270
272
  private _build_edit_context_previews;
@@ -475,6 +477,42 @@ interface FinalizeResult {
475
477
  }
476
478
  declare function finalize_document(doc: DocumentObject, options: FinalizeOptions): Promise<FinalizeResult>;
477
479
 
480
+ /**
481
+ * Time-budgeted execution of USER/LLM-supplied regular expressions.
482
+ *
483
+ * `regex: true` on a ModifyText edit and `search_regex` on read_docx hand an
484
+ * LLM-controlled pattern to V8's backtracking engine. A pathological pattern
485
+ * like `(a|a)*$` against a run of repeated characters hangs the event loop
486
+ * indefinitely (QA 2026-07-17 F5 — ReDoS; mirrors the Python fix).
487
+ *
488
+ * JS regexes cannot be interrupted in-thread, but V8 CAN interrupt regex
489
+ * execution running inside a `vm` context with a `timeout` — that is the
490
+ * mechanism used here. `node:vm` is a builtin, preserving the zero-dependency
491
+ * bundle constraint.
492
+ *
493
+ * Only USER-SUPPLIED patterns belong here. The engine's own generated
494
+ * patterns (fuzzy matchers etc.) are built to be linear-time.
495
+ */
496
+ declare const USER_PATTERN_TIMEOUT_MS = 2000;
497
+ declare class RegexTimeoutError extends Error {
498
+ pattern: string;
499
+ constructor(pattern: string);
500
+ }
501
+ /**
502
+ * All non-overlapping matches of a user pattern, under the wall-clock budget.
503
+ * Invalid patterns throw SyntaxError exactly like `new RegExp(...)` would.
504
+ * The budget covers the ENTIRE scan, not just the first match.
505
+ */
506
+ declare function userFindAllMatches(pattern: string, text: string, flags?: string): Array<{
507
+ start: number;
508
+ end: number;
509
+ }>;
510
+ /** First match of a user pattern under the wall-clock budget, or null. */
511
+ declare function userSearch(pattern: string, text: string, flags?: string): {
512
+ start: number;
513
+ end: number;
514
+ } | null;
515
+
478
516
  declare function identifyEngine(): string;
479
517
 
480
- export { BatchValidationError, DocumentMapper, DocumentObject, type FinalizeOptions, type FinalizeResult, type OutlineNode, type PageInfo, type PaginationResult, RedlineEngine, type TextSpan, _extractTextFromDoc, apply_edits_to_markdown, create_unified_diff, create_word_patch_diff, extractTextFromBuffer, extract_outline, finalize_document, generate_edits_from_text, identifyEngine, paginate, split_structural_appendix, trim_common_context };
518
+ export { BatchValidationError, DocumentMapper, DocumentObject, type FinalizeOptions, type FinalizeResult, type OutlineNode, type PageInfo, type PaginationResult, RedlineEngine, RegexTimeoutError, type TextSpan, USER_PATTERN_TIMEOUT_MS, _extractTextFromDoc, apply_edits_to_markdown, create_unified_diff, create_word_patch_diff, describe_illegal_control_chars, extractTextFromBuffer, extract_outline, finalize_document, generate_edits_from_text, identifyEngine, paginate, split_structural_appendix, trim_common_context, userFindAllMatches, userSearch, validate_edit_strings };
package/dist/index.js CHANGED
@@ -743,6 +743,62 @@ function extract_comments_data(pkg) {
743
743
  return data;
744
744
  }
745
745
 
746
+ // src/utils/safe-regex.ts
747
+ import * as vm from "vm";
748
+ var USER_PATTERN_TIMEOUT_MS = 2e3;
749
+ var RegexTimeoutError = class extends Error {
750
+ pattern;
751
+ constructor(pattern) {
752
+ super(
753
+ `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.`
754
+ );
755
+ this.name = "RegexTimeoutError";
756
+ this.pattern = pattern;
757
+ }
758
+ };
759
+ function runBudgeted(pattern, script, sandbox) {
760
+ try {
761
+ return vm.runInNewContext(script, sandbox, {
762
+ timeout: USER_PATTERN_TIMEOUT_MS
763
+ });
764
+ } catch (e) {
765
+ if (e && e.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
766
+ throw new RegexTimeoutError(pattern);
767
+ }
768
+ throw e;
769
+ }
770
+ }
771
+ function userFindAllMatches(pattern, text, flags = "") {
772
+ const normalized = flags.includes("g") ? flags : flags + "g";
773
+ const re = new RegExp(pattern, normalized);
774
+ const raw = runBudgeted(
775
+ pattern,
776
+ `{
777
+ const out = [];
778
+ let m;
779
+ while ((m = re.exec(text)) !== null) {
780
+ out.push({ start: m.index, end: m.index + m[0].length });
781
+ if (m.index === re.lastIndex) re.lastIndex++;
782
+ }
783
+ out;
784
+ }`,
785
+ { re, text }
786
+ );
787
+ return raw.map((r) => ({ start: r.start, end: r.end }));
788
+ }
789
+ function userSearch(pattern, text, flags = "") {
790
+ const re = new RegExp(pattern, flags.replace("g", ""));
791
+ const raw = runBudgeted(
792
+ pattern,
793
+ `{
794
+ const m = re.exec(text);
795
+ m ? { start: m.index, end: m.index + m[0].length } : null;
796
+ }`,
797
+ { re, text }
798
+ );
799
+ return raw ? { start: raw.start, end: raw.end } : null;
800
+ }
801
+
746
802
  // src/utils/docx.ts
747
803
  var QN_W_P = "w:p";
748
804
  var QN_W_R = "w:r";
@@ -1817,10 +1873,10 @@ ${header}`;
1817
1873
  find_match_index(target_text, is_regex = false) {
1818
1874
  if (is_regex) {
1819
1875
  try {
1820
- const pattern = new RegExp(target_text);
1821
- const match = pattern.exec(this.full_text);
1822
- if (match) return [match.index, match[0].length];
1876
+ const match = userSearch(target_text, this.full_text);
1877
+ if (match) return [match.start, match.end - match.start];
1823
1878
  } catch (e) {
1879
+ if (e instanceof RegexTimeoutError) throw e;
1824
1880
  }
1825
1881
  return [-1, 0];
1826
1882
  }
@@ -1849,11 +1905,10 @@ ${header}`;
1849
1905
  if (!target_text) return [];
1850
1906
  if (is_regex) {
1851
1907
  try {
1852
- const pattern = new RegExp(target_text, "g");
1853
- const matches2 = [...this.full_text.matchAll(pattern)];
1854
- if (matches2.length > 0)
1855
- return matches2.map((m) => [m.index, m[0].length]);
1908
+ const matches2 = userFindAllMatches(target_text, this.full_text);
1909
+ if (matches2.length > 0) return matches2.map((m) => [m.start, m.end - m.start]);
1856
1910
  } catch (e) {
1911
+ if (e instanceof RegexTimeoutError) throw e;
1857
1912
  }
1858
1913
  return [];
1859
1914
  }
@@ -2831,7 +2886,9 @@ function apply_edits_to_markdown(markdown_text, edits, include_index = false, hi
2831
2886
  isolated_target,
2832
2887
  isolated_new,
2833
2888
  edit.comment,
2834
- orig_idx,
2889
+ // 1-based, matching apply's "Edit N" reports and batch validation
2890
+ // errors (QA 2026-07-17 F10; mirrors Python).
2891
+ orig_idx + 1,
2835
2892
  include_index,
2836
2893
  highlight_only
2837
2894
  );
@@ -2976,12 +3033,36 @@ function sequential_context_hint(applied_so_far) {
2976
3033
  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
3034
  }
2978
3035
  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.";
3036
+ var XML_ILLEGAL_CHARS_RE = /[\x00-\x08\x0b\x0c\x0e-\x1f]/g;
3037
+ function describe_illegal_control_chars(text) {
3038
+ if (!text) return null;
3039
+ const found = text.match(XML_ILLEGAL_CHARS_RE);
3040
+ if (!found) return null;
3041
+ const codes = Array.from(new Set(found.map((c) => `0x${c.charCodeAt(0).toString(16).padStart(2, "0")}`))).sort();
3042
+ return codes.join(", ");
3043
+ }
2979
3044
  function validate_edit_strings(edits, index_offset = 0) {
2980
3045
  const errors = [];
2981
3046
  for (let i = 0; i < edits.length; i++) {
2982
3047
  const edit = edits[i];
2983
3048
  const t_text = edit.target_text || "";
2984
3049
  const n_text = edit.new_text || "";
3050
+ const checked_fields = [
3051
+ ["target_text", t_text],
3052
+ ["new_text", n_text]
3053
+ ];
3054
+ if (edit.comment) checked_fields.push(["comment", edit.comment]);
3055
+ (edit.cells || []).forEach((cell, cell_idx) => {
3056
+ checked_fields.push([`cells[${cell_idx}]`, cell || ""]);
3057
+ });
3058
+ for (const [field_name, field_value] of checked_fields) {
3059
+ const described = describe_illegal_control_chars(field_value);
3060
+ if (described) {
3061
+ errors.push(
3062
+ `- 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.`
3063
+ );
3064
+ }
3065
+ }
2985
3066
  if (n_text.includes("{++") || n_text.includes("{--") || n_text.includes("{>>") || n_text.includes("{==")) {
2986
3067
  errors.push(
2987
3068
  `- 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.`
@@ -3288,8 +3369,8 @@ var RedlineEngine = class _RedlineEngine {
3288
3369
  * Snapshots the document text around a resolved edit BEFORE anything is
3289
3370
  * applied. Previews rendered after the batch mutates the DOM cannot slice
3290
3371
  * 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).
3372
+ * tracked-change markup, garbling previews with unrelated edits and
3373
+ * internal scaffolding (QA H1).
3293
3374
  */
3294
3375
  _capture_preview_context(edit) {
3295
3376
  if (edit.type !== "modify") return;
@@ -3390,8 +3471,8 @@ var RedlineEngine = class _RedlineEngine {
3390
3471
  }
3391
3472
  /**
3392
3473
  * 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).
3474
+ * no new_text field — surface its cell contents rather than a misleading
3475
+ * empty string (QA M4).
3395
3476
  */
3396
3477
  static _report_new_text(edit) {
3397
3478
  if (edit && edit.type === "insert_row" && Array.isArray(edit.cells)) {
@@ -4676,7 +4757,13 @@ var RedlineEngine = class _RedlineEngine {
4676
4757
  const reports_by_input = new Array(edits.length);
4677
4758
  const validation_failed_idx = /* @__PURE__ */ new Set();
4678
4759
  for (const { edit, i } of ordered_edits) {
4679
- let single_errors = this.validate_edits([edit], i);
4760
+ let single_errors;
4761
+ try {
4762
+ single_errors = this.validate_edits([edit], i);
4763
+ } catch (e) {
4764
+ if (!(e instanceof RegexTimeoutError)) throw e;
4765
+ single_errors = [`- Edit ${i + 1} Failed: ${e.message}`];
4766
+ }
4680
4767
  if (single_errors.length > 0) {
4681
4768
  if (applied_edits > 0) {
4682
4769
  const hint = sequential_context_hint(applied_edits);
@@ -4761,7 +4848,13 @@ var RedlineEngine = class _RedlineEngine {
4761
4848
  const sequential_errors = [];
4762
4849
  let applied_so_far = 0;
4763
4850
  for (const { edit, i } of ordered_edits) {
4764
- let single_errors = this.validate_edits([edit], i);
4851
+ let single_errors;
4852
+ try {
4853
+ single_errors = this.validate_edits([edit], i);
4854
+ } catch (e) {
4855
+ if (!(e instanceof RegexTimeoutError)) throw e;
4856
+ single_errors = [`- Edit ${i + 1} Failed: ${e.message}`];
4857
+ }
4765
4858
  if (single_errors.length > 0) {
4766
4859
  if (applied_so_far > 0) {
4767
4860
  const hint = sequential_context_hint(applied_so_far);
@@ -4875,7 +4968,18 @@ var RedlineEngine = class _RedlineEngine {
4875
4968
  edit._error_msg = msg;
4876
4969
  }
4877
4970
  } else {
4878
- const resolved = this._pre_resolve_heuristic_edit(edit);
4971
+ let resolved;
4972
+ try {
4973
+ resolved = this._pre_resolve_heuristic_edit(edit);
4974
+ } catch (e) {
4975
+ if (!(e instanceof RegexTimeoutError)) throw e;
4976
+ skipped++;
4977
+ edit._applied_status = false;
4978
+ const msg = `- Failed to apply edit targeting: '${(edit.target_text || "").substring(0, 40)}...' (${e.message})`;
4979
+ this.skipped_details.push(msg);
4980
+ edit._error_msg = msg;
4981
+ continue;
4982
+ }
4879
4983
  if (resolved) {
4880
4984
  if (Array.isArray(resolved)) {
4881
4985
  for (const r of resolved) {
@@ -5034,6 +5138,16 @@ var RedlineEngine = class _RedlineEngine {
5034
5138
  );
5035
5139
  continue;
5036
5140
  }
5141
+ const dup_authors = Array.from(
5142
+ new Set(all_nodes.map((n) => n.getAttribute("w:author") || "Unknown"))
5143
+ ).sort();
5144
+ if (dup_authors.length > 1) {
5145
+ skipped++;
5146
+ this.skipped_details.push(
5147
+ `- 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.`
5148
+ );
5149
+ continue;
5150
+ }
5037
5151
  for (const node of all_nodes) {
5038
5152
  const is_ins = node.tagName === "w:ins";
5039
5153
  const parent_tag = node.parentNode ? node.parentNode.tagName : "";
@@ -6349,6 +6463,27 @@ function scrub_doc_properties(doc) {
6349
6463
  c.textContent = "";
6350
6464
  }
6351
6465
  });
6466
+ const titles = findDescendantsByLocalName(corePart._element, "title");
6467
+ titles.forEach((c) => {
6468
+ if (c.textContent) {
6469
+ lines.push(`Title kept (review manually): "${c.textContent}"`);
6470
+ }
6471
+ });
6472
+ const leakFields = [
6473
+ ["category", "Category"],
6474
+ ["keywords", "Keywords"],
6475
+ ["subject", "Subject"],
6476
+ ["contentStatus", "Content status"],
6477
+ ["description", "Description/comments"]
6478
+ ];
6479
+ for (const [local, label] of leakFields) {
6480
+ findDescendantsByLocalName(corePart._element, local).forEach((c) => {
6481
+ if (c.textContent) {
6482
+ lines.push(`${label}: ${c.textContent}`);
6483
+ c.textContent = "";
6484
+ }
6485
+ });
6486
+ }
6352
6487
  const revisions = findDescendantsByLocalName(corePart._element, "revision");
6353
6488
  revisions.forEach((c) => {
6354
6489
  if (c.textContent && parseInt(c.textContent) > 1) {
@@ -6554,7 +6689,11 @@ function extract_all_domain_metadata(doc, base_text) {
6554
6689
  for (const term of def_keys) {
6555
6690
  if (definitions[term].count === 0) {
6556
6691
  delete definitions[term];
6557
- duplicates.delete(term);
6692
+ if (!duplicates.has(term)) {
6693
+ diagnostics.push(
6694
+ `[Warning] Unused Definition: '${term}' is defined but never used.`
6695
+ );
6696
+ }
6558
6697
  }
6559
6698
  }
6560
6699
  }
@@ -7889,10 +8028,13 @@ export {
7889
8028
  DocumentMapper,
7890
8029
  DocumentObject,
7891
8030
  RedlineEngine,
8031
+ RegexTimeoutError,
8032
+ USER_PATTERN_TIMEOUT_MS,
7892
8033
  _extractTextFromDoc,
7893
8034
  apply_edits_to_markdown,
7894
8035
  create_unified_diff,
7895
8036
  create_word_patch_diff,
8037
+ describe_illegal_control_chars,
7896
8038
  extractTextFromBuffer,
7897
8039
  extract_outline,
7898
8040
  finalize_document,
@@ -7900,6 +8042,9 @@ export {
7900
8042
  identifyEngine,
7901
8043
  paginate,
7902
8044
  split_structural_appendix,
7903
- trim_common_context
8045
+ trim_common_context,
8046
+ userFindAllMatches,
8047
+ userSearch,
8048
+ validate_edit_strings
7904
8049
  };
7905
8050
  //# sourceMappingURL=index.js.map