@adeu/core 1.16.0 → 1.17.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adeu/core",
3
- "version": "1.16.0",
3
+ "version": "1.17.1",
4
4
  "description": "",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,10 +1,11 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import {
3
- _replace_smart_quotes,
4
- _make_fuzzy_regex,
5
- _find_match_in_text,
6
- _build_critic_markup,
7
- apply_edits_to_markdown
2
+ import {
3
+ _replace_smart_quotes,
4
+ _make_fuzzy_regex,
5
+ _find_match_in_text,
6
+ _build_critic_markup,
7
+ apply_edits_to_markdown,
8
+ format_ambiguity_error
8
9
  } from './markup.js';
9
10
  import { ModifyText } from './models.js';
10
11
 
@@ -147,4 +148,38 @@ describe('apply_edits_to_markdown', () => {
147
148
  const result = apply_edits_to_markdown(text, [{ type: 'modify', target_text: target, new_text: newText }]);
148
149
  expect(result).toContain(expectedSubstring);
149
150
  });
151
+ });
152
+
153
+ describe('format_ambiguity_error (Turn Loop Trap mitigation)', () => {
154
+ // Mirror the DPA scenario: the same placeholder appears in multiple clauses.
155
+ const haystack =
156
+ 'PROVIDER: [official company name] shall process the data. ' +
157
+ 'PROVIDER: [official company name] is the data processor.';
158
+ const target = 'PROVIDER: [official company name]';
159
+ const positions: [number, number][] = [
160
+ [0, target.length],
161
+ [58, 58 + target.length],
162
+ ];
163
+
164
+ it('names BOTH match_mode escape hatches so the agent can re-call instead of looping', () => {
165
+ const msg = format_ambiguity_error(1, target, haystack, positions);
166
+
167
+ expect(msg).toContain('Edit 1 Failed: Ambiguous match');
168
+ expect(msg).toContain('appears 2 times');
169
+
170
+ // The actionable remediation that breaks the loop.
171
+ expect(msg).toContain('"match_mode": "all"');
172
+ expect(msg).toContain('"match_mode": "first"');
173
+ expect(msg).toContain('ALL 2 occurrences');
174
+ expect(msg).toContain('FIRST occurrence');
175
+
176
+ // The original "add more context" guidance is preserved as a third option.
177
+ expect(msg).toContain('Please provide more surrounding context');
178
+ });
179
+
180
+ it('still throws for fewer than two matches', () => {
181
+ expect(() => format_ambiguity_error(1, target, haystack, [[0, 5]])).toThrow(
182
+ /requires at least 2 matches/,
183
+ );
184
+ });
150
185
  });
package/src/markup.ts CHANGED
@@ -422,8 +422,19 @@ export function format_ambiguity_error(
422
422
  lines.push(` ... and ${remaining} more occurrence(s) not shown.`);
423
423
  }
424
424
 
425
+ // Tell the agent EXACTLY how to re-call. Without this, agents loop forever
426
+ // refining target_text/regex because they never learn that match_mode is the
427
+ // built-in escape hatch for genuine ambiguity.
428
+ lines.push(" To resolve, re-send this edit using ONE of these strategies:");
425
429
  lines.push(
426
- " Please provide more surrounding context in your target_text to uniquely identify the location.",
430
+ ` 1. Set "match_mode": "all" to modify ALL ${total} occurrences (same target_text).`,
431
+ );
432
+ lines.push(
433
+ ' 2. Set "match_mode": "first" to modify only the FIRST occurrence (same target_text).',
434
+ );
435
+ lines.push(
436
+ ' 3. Please provide more surrounding context in your target_text to uniquely ' +
437
+ 'identify a single location (keep the default "match_mode": "strict").',
427
438
  );
428
439
 
429
440
  return lines.join("\n");
@@ -25,6 +25,47 @@ describe('Search and Targeted Write Engine', () => {
25
25
  expect(() => engine.process_batch(edits)).toThrowError(BatchValidationError);
26
26
  });
27
27
 
28
+ it('strict rejection carries match_mode guidance, and the suggested re-call applies (Turn Loop Trap)', async () => {
29
+ // The DPA scenario: one placeholder repeated across clauses.
30
+ const doc = await createTestDocument();
31
+ addParagraph(doc, "PROVIDER: [official company name] shall process the data.");
32
+ addParagraph(doc, "PROVIDER: [official company name] is the data processor.");
33
+
34
+ const engine = new RedlineEngine(doc);
35
+ const strictEdit: any = {
36
+ type: 'modify',
37
+ target_text: "PROVIDER: [official company name]",
38
+ new_text: "PROVIDER: Acme Corp",
39
+ };
40
+
41
+ // 1. The strict edit is rejected — and the error MUST name the escape hatch
42
+ // so the agent re-calls instead of looping on context/regex refinement.
43
+ let caught: BatchValidationError | null = null;
44
+ try {
45
+ engine.process_batch([strictEdit]);
46
+ } catch (e) {
47
+ caught = e as BatchValidationError;
48
+ }
49
+ expect(caught).toBeInstanceOf(BatchValidationError);
50
+ const msg = caught!.errors.join("\n");
51
+ expect(msg).toContain("Ambiguous match");
52
+ expect(msg).toContain('"match_mode": "all"');
53
+ expect(msg).toContain('"match_mode": "first"');
54
+
55
+ // 2. Follow the guidance verbatim — the same target_text with match_mode="all"
56
+ // applies cleanly and mutates BOTH clauses on the saved document.
57
+ const doc2 = await createTestDocument();
58
+ addParagraph(doc2, "PROVIDER: [official company name] shall process the data.");
59
+ addParagraph(doc2, "PROVIDER: [official company name] is the data processor.");
60
+ const engine2 = new RedlineEngine(doc2);
61
+ const stats = engine2.process_batch([{ ...strictEdit, match_mode: 'all' }]);
62
+ expect(stats.edits[0].occurrences_modified).toBe(2);
63
+
64
+ const text = await extractTextFromBuffer(await doc2.save(), true);
65
+ expect(text.match(/Acme Corp/g)?.length).toBe(2);
66
+ expect(text).not.toContain("PROVIDER: [official company name]");
67
+ });
68
+
28
69
  it('match_mode="first" modifies only the first occurrence', async () => {
29
70
  const doc = await createTestDocument();
30
71
  addParagraph(doc, "This is a repetitive clause.");