@adeu/mcp-server 1.18.1 → 1.18.4
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 +136 -22
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/verify-reasoning-order.mjs +113 -0
- package/src/index.ts +170 -11
- package/src/mcp.bugs.test.ts +10 -1
- package/src/mcp.cloud.test.ts +4 -3
- package/src/mcp.schema-gaps.test.ts +4 -0
- package/src/parity_live.test.ts +189 -98
- package/src/repro.feedback.test.ts +24 -7
- package/src/response-builders.ts +21 -5
package/src/parity_live.test.ts
CHANGED
|
@@ -4,9 +4,17 @@ import { spawn, ChildProcess } from "node:child_process";
|
|
|
4
4
|
import { resolve, join } from "node:path";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
|
|
7
|
+
// Each Python CLI invocation spawns a fresh interpreter costing ~3-5s on
|
|
8
|
+
// Windows even when warm, so tests that shell out to Python need more than
|
|
9
|
+
// vitest's 5s default timeout.
|
|
10
|
+
const PYTHON_PARITY_TIMEOUT_MS = 60_000;
|
|
11
|
+
|
|
7
12
|
describe("Parity Live Server Integration Verification", () => {
|
|
8
13
|
let serverProc: ChildProcess;
|
|
9
|
-
const fixturePath = resolve(
|
|
14
|
+
const fixturePath = resolve(
|
|
15
|
+
__dirname,
|
|
16
|
+
"../tests/fixtures/gap2_minimal_repro.docx",
|
|
17
|
+
);
|
|
10
18
|
|
|
11
19
|
beforeAll(async () => {
|
|
12
20
|
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
@@ -76,86 +84,117 @@ describe("Parity Live Server Integration Verification", () => {
|
|
|
76
84
|
});
|
|
77
85
|
|
|
78
86
|
it("exposes the build stamp via serverInfo.version in initialize", async () => {
|
|
79
|
-
const initRes = await sendRpc(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const initRes = await sendRpc(
|
|
88
|
+
"initialize",
|
|
89
|
+
{
|
|
90
|
+
protocolVersion: "2024-11-05",
|
|
91
|
+
capabilities: {},
|
|
92
|
+
clientInfo: { name: "test-client", version: "1.0.0" },
|
|
93
|
+
},
|
|
94
|
+
202,
|
|
95
|
+
);
|
|
84
96
|
expect(initRes.result).toBeDefined();
|
|
85
97
|
expect(initRes.result.serverInfo).toBeDefined();
|
|
86
98
|
expect(initRes.result.serverInfo.version).not.toBe("1.0.0");
|
|
87
99
|
expect(initRes.result.serverInfo.version).not.toContain("unknown");
|
|
88
100
|
});
|
|
89
101
|
|
|
90
|
-
|
|
91
102
|
it("read_docx appends [Debug] build stamp footer to mode=outline and mode=full", async () => {
|
|
92
|
-
const res = await sendRpc(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
const res = await sendRpc(
|
|
104
|
+
"tools/call",
|
|
105
|
+
{
|
|
106
|
+
name: "read_docx",
|
|
107
|
+
arguments: {
|
|
108
|
+
reasoning: "test",
|
|
109
|
+
file_path: fixturePath,
|
|
110
|
+
mode: "outline",
|
|
111
|
+
},
|
|
97
112
|
},
|
|
98
|
-
|
|
113
|
+
202,
|
|
114
|
+
);
|
|
99
115
|
|
|
100
116
|
expect(res.result).toBeDefined();
|
|
101
117
|
expect(res.result.content[0].text).not.toContain("[Debug] build=");
|
|
102
118
|
});
|
|
103
119
|
|
|
104
|
-
it("GAP 2 (Live): process_document_batch modify straddling deleted text returns actionable deletion error", async () => {
|
|
105
|
-
const outPath = join(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
it("GAP 2 (Live): process_document_batch modify straddling deleted text returns actionable deletion error", { timeout: PYTHON_PARITY_TIMEOUT_MS }, async () => {
|
|
121
|
+
const outPath = join(
|
|
122
|
+
resolve(__dirname, "../../../../tmp"),
|
|
123
|
+
`live_gap2_out_${Date.now()}.docx`,
|
|
124
|
+
);
|
|
125
|
+
const res = await sendRpc(
|
|
126
|
+
"tools/call",
|
|
127
|
+
{
|
|
128
|
+
name: "process_document_batch",
|
|
129
|
+
arguments: {
|
|
130
|
+
reasoning: "test",
|
|
131
|
+
original_docx_path: fixturePath,
|
|
132
|
+
author_name: "Ed",
|
|
133
|
+
changes: [
|
|
134
|
+
{
|
|
135
|
+
type: "modify",
|
|
136
|
+
target_text: "Foo bar old phrase here.",
|
|
137
|
+
new_text: "X",
|
|
138
|
+
comment: "c",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
output_path: outPath,
|
|
142
|
+
dry_run: true,
|
|
143
|
+
},
|
|
121
144
|
},
|
|
122
|
-
|
|
145
|
+
203,
|
|
146
|
+
);
|
|
123
147
|
|
|
124
148
|
expect(res.result).toBeDefined();
|
|
125
149
|
const text = res.result.content[0].text;
|
|
126
|
-
expect(text).toContain(
|
|
127
|
-
|
|
150
|
+
expect(text).toContain(
|
|
151
|
+
"Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.",
|
|
152
|
+
);
|
|
153
|
+
expect(text).toContain(
|
|
154
|
+
"Reject/accept that change first or target the active replacement text instead.",
|
|
155
|
+
);
|
|
128
156
|
expect(text).not.toContain("Target text not found in document.");
|
|
129
157
|
|
|
130
158
|
// Run Python engine to assert cross-engine exact error parity
|
|
131
159
|
const { execSync } = await import("node:child_process");
|
|
132
160
|
const projectRoot = resolve(__dirname, "../../../..");
|
|
133
|
-
const pythonCli =
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
161
|
+
const pythonCli =
|
|
162
|
+
process.platform === "win32"
|
|
163
|
+
? join(projectRoot, "python/.venv/Scripts/adeu.exe")
|
|
164
|
+
: join(projectRoot, "python/.venv/bin/adeu");
|
|
165
|
+
|
|
137
166
|
// Create temporary changes JSON for Python CLI
|
|
138
167
|
const fs = await import("node:fs");
|
|
139
168
|
const tempJsonPath = join(projectRoot, "tmp", `changes_${Date.now()}.json`);
|
|
140
169
|
fs.mkdirSync(join(projectRoot, "tmp"), { recursive: true });
|
|
141
|
-
fs.writeFileSync(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
tempJsonPath,
|
|
172
|
+
JSON.stringify([
|
|
173
|
+
{
|
|
174
|
+
type: "modify",
|
|
175
|
+
target_text: "Foo bar old phrase here.",
|
|
176
|
+
new_text: "X",
|
|
177
|
+
comment: "c",
|
|
178
|
+
},
|
|
179
|
+
]),
|
|
180
|
+
);
|
|
149
181
|
|
|
150
182
|
try {
|
|
151
|
-
const pythonOut = execSync(
|
|
152
|
-
|
|
153
|
-
|
|
183
|
+
const pythonOut = execSync(
|
|
184
|
+
`"${pythonCli}" apply "${fixturePath}" "${tempJsonPath}" --dry-run --author Ed`,
|
|
185
|
+
{
|
|
186
|
+
encoding: "utf-8",
|
|
187
|
+
},
|
|
188
|
+
);
|
|
154
189
|
// Should not succeed normally since dry run exits with code 1 on errors, so we catch in the try block
|
|
155
190
|
} catch (err: any) {
|
|
156
191
|
const pyErrorOutput = err.stdout || err.stderr || "";
|
|
157
|
-
expect(pyErrorOutput).toContain(
|
|
158
|
-
|
|
192
|
+
expect(pyErrorOutput).toContain(
|
|
193
|
+
"Edit 1 Failed: Target text matches text inside a tracked deletion by Test Negotiator.",
|
|
194
|
+
);
|
|
195
|
+
expect(pyErrorOutput).toContain(
|
|
196
|
+
"Reject/accept that change first or target the active replacement text instead.",
|
|
197
|
+
);
|
|
159
198
|
} finally {
|
|
160
199
|
if (fs.existsSync(tempJsonPath)) {
|
|
161
200
|
fs.unlinkSync(tempJsonPath);
|
|
@@ -163,35 +202,51 @@ describe("Parity Live Server Integration Verification", () => {
|
|
|
163
202
|
}
|
|
164
203
|
});
|
|
165
204
|
|
|
166
|
-
it("GAP 1 (Live): read_docx mode=outline heading count and content parity with Python", async () => {
|
|
167
|
-
const gap1FixturePath = resolve(
|
|
205
|
+
it("GAP 1 (Live): read_docx mode=outline heading count and content parity with Python", { timeout: PYTHON_PARITY_TIMEOUT_MS }, async () => {
|
|
206
|
+
const gap1FixturePath = resolve(
|
|
207
|
+
__dirname,
|
|
208
|
+
"../tests/fixtures/gap1_deleted_row_repro.docx",
|
|
209
|
+
);
|
|
168
210
|
const { execSync } = await import("node:child_process");
|
|
169
211
|
const projectRoot = resolve(__dirname, "../../../..");
|
|
170
|
-
const pythonCli =
|
|
171
|
-
|
|
172
|
-
|
|
212
|
+
const pythonCli =
|
|
213
|
+
process.platform === "win32"
|
|
214
|
+
? join(projectRoot, "python/.venv/Scripts/adeu.exe")
|
|
215
|
+
: join(projectRoot, "python/.venv/bin/adeu");
|
|
173
216
|
|
|
174
217
|
// 1. clean_view = true
|
|
175
|
-
const nodeResClean = await sendRpc(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
218
|
+
const nodeResClean = await sendRpc(
|
|
219
|
+
"tools/call",
|
|
220
|
+
{
|
|
221
|
+
name: "read_docx",
|
|
222
|
+
arguments: {
|
|
223
|
+
reasoning: "test",
|
|
224
|
+
file_path: gap1FixturePath,
|
|
225
|
+
mode: "outline",
|
|
226
|
+
clean_view: true,
|
|
227
|
+
},
|
|
181
228
|
},
|
|
182
|
-
|
|
229
|
+
204,
|
|
230
|
+
);
|
|
183
231
|
|
|
184
232
|
expect(nodeResClean.result).toBeDefined();
|
|
185
233
|
const nodeTextClean = nodeResClean.result.content[0].text;
|
|
186
|
-
|
|
234
|
+
|
|
187
235
|
// Get Python output for clean_view = true
|
|
188
|
-
const pythonOutClean = execSync(
|
|
189
|
-
|
|
190
|
-
|
|
236
|
+
const pythonOutClean = execSync(
|
|
237
|
+
`"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`,
|
|
238
|
+
{
|
|
239
|
+
encoding: "utf-8",
|
|
240
|
+
},
|
|
241
|
+
);
|
|
191
242
|
|
|
192
243
|
// Extract raw markdown headings (lines starting with #) to assert perfect parity
|
|
193
|
-
const getHeadings = (text: string) =>
|
|
194
|
-
|
|
244
|
+
const getHeadings = (text: string) =>
|
|
245
|
+
text
|
|
246
|
+
.split("\n")
|
|
247
|
+
.filter((line) => line.startsWith("#"))
|
|
248
|
+
.map((l) => l.trim());
|
|
249
|
+
|
|
195
250
|
const nodeHeadingsClean = getHeadings(nodeTextClean);
|
|
196
251
|
const pythonHeadingsClean = getHeadings(pythonOutClean);
|
|
197
252
|
|
|
@@ -200,56 +255,86 @@ describe("Parity Live Server Integration Verification", () => {
|
|
|
200
255
|
expect(nodeHeadingsClean).toEqual(pythonHeadingsClean);
|
|
201
256
|
|
|
202
257
|
// 2. clean_view = false
|
|
203
|
-
const nodeResDirty = await sendRpc(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
const nodeResDirty = await sendRpc(
|
|
259
|
+
"tools/call",
|
|
260
|
+
{
|
|
261
|
+
name: "read_docx",
|
|
262
|
+
arguments: {
|
|
263
|
+
reasoning: "test",
|
|
264
|
+
file_path: gap1FixturePath,
|
|
265
|
+
mode: "outline",
|
|
266
|
+
clean_view: false,
|
|
267
|
+
},
|
|
209
268
|
},
|
|
210
|
-
|
|
269
|
+
205,
|
|
270
|
+
);
|
|
211
271
|
|
|
212
272
|
expect(nodeResDirty.result).toBeDefined();
|
|
213
273
|
const nodeTextDirty = nodeResDirty.result.content[0].text;
|
|
214
274
|
|
|
215
|
-
const pythonOutDirty = execSync(
|
|
216
|
-
|
|
217
|
-
|
|
275
|
+
const pythonOutDirty = execSync(
|
|
276
|
+
`"${pythonCli}" extract "${gap1FixturePath}" --mode outline`,
|
|
277
|
+
{
|
|
278
|
+
encoding: "utf-8",
|
|
279
|
+
},
|
|
280
|
+
);
|
|
218
281
|
|
|
219
282
|
const nodeHeadingsDirty = getHeadings(nodeTextDirty);
|
|
220
283
|
const pythonHeadingsDirty = getHeadings(pythonOutDirty);
|
|
221
284
|
|
|
222
|
-
expect(nodeHeadingsDirty).toEqual([
|
|
223
|
-
|
|
285
|
+
expect(nodeHeadingsDirty).toEqual([
|
|
286
|
+
"# Active Heading (p1)",
|
|
287
|
+
"# Deleted Heading (p1)",
|
|
288
|
+
]);
|
|
289
|
+
expect(pythonHeadingsDirty).toEqual([
|
|
290
|
+
"# Active Heading (p1)",
|
|
291
|
+
"# Deleted Heading (p1)",
|
|
292
|
+
]);
|
|
224
293
|
expect(nodeHeadingsDirty).toEqual(pythonHeadingsDirty);
|
|
225
294
|
});
|
|
226
295
|
|
|
227
|
-
it("GAP 1 - Style Def (Live): style-definition outlineLvl on non-heading style classification parity", async () => {
|
|
228
|
-
const gap1FixturePath = resolve(
|
|
296
|
+
it("GAP 1 - Style Def (Live): style-definition outlineLvl on non-heading style classification parity", { timeout: PYTHON_PARITY_TIMEOUT_MS }, async () => {
|
|
297
|
+
const gap1FixturePath = resolve(
|
|
298
|
+
__dirname,
|
|
299
|
+
"../tests/fixtures/gap1_minimal_repro.docx",
|
|
300
|
+
);
|
|
229
301
|
const { execSync } = await import("node:child_process");
|
|
230
302
|
const projectRoot = resolve(__dirname, "../../../..");
|
|
231
|
-
const pythonCli =
|
|
232
|
-
|
|
233
|
-
|
|
303
|
+
const pythonCli =
|
|
304
|
+
process.platform === "win32"
|
|
305
|
+
? join(projectRoot, "python/.venv/Scripts/adeu.exe")
|
|
306
|
+
: join(projectRoot, "python/.venv/bin/adeu");
|
|
234
307
|
|
|
235
308
|
// 1. clean_view = true (both engines must return exactly 2 headings, excluding Subtitle)
|
|
236
|
-
const nodeResClean = await sendRpc(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
309
|
+
const nodeResClean = await sendRpc(
|
|
310
|
+
"tools/call",
|
|
311
|
+
{
|
|
312
|
+
name: "read_docx",
|
|
313
|
+
arguments: {
|
|
314
|
+
reasoning: "test",
|
|
315
|
+
file_path: gap1FixturePath,
|
|
316
|
+
mode: "outline",
|
|
317
|
+
clean_view: true,
|
|
318
|
+
},
|
|
242
319
|
},
|
|
243
|
-
|
|
320
|
+
206,
|
|
321
|
+
);
|
|
244
322
|
|
|
245
323
|
expect(nodeResClean.result).toBeDefined();
|
|
246
324
|
const nodeTextClean = nodeResClean.result.content[0].text;
|
|
247
325
|
|
|
248
|
-
const pythonOutClean = execSync(
|
|
249
|
-
|
|
250
|
-
|
|
326
|
+
const pythonOutClean = execSync(
|
|
327
|
+
`"${pythonCli}" extract "${gap1FixturePath}" --mode outline --clean-view`,
|
|
328
|
+
{
|
|
329
|
+
encoding: "utf-8",
|
|
330
|
+
},
|
|
331
|
+
);
|
|
251
332
|
|
|
252
|
-
const getHeadings = (text: string) =>
|
|
333
|
+
const getHeadings = (text: string) =>
|
|
334
|
+
text
|
|
335
|
+
.split("\n")
|
|
336
|
+
.filter((line) => line.startsWith("#"))
|
|
337
|
+
.map((l) => l.trim());
|
|
253
338
|
|
|
254
339
|
const nodeHeadingsClean = getHeadings(nodeTextClean);
|
|
255
340
|
const pythonHeadingsClean = getHeadings(pythonOutClean);
|
|
@@ -258,8 +343,14 @@ describe("Parity Live Server Integration Verification", () => {
|
|
|
258
343
|
expect(nodeHeadingsClean.length).toBe(2);
|
|
259
344
|
expect(pythonHeadingsClean.length).toBe(2);
|
|
260
345
|
|
|
261
|
-
expect(nodeHeadingsClean).toEqual([
|
|
262
|
-
|
|
346
|
+
expect(nodeHeadingsClean).toEqual([
|
|
347
|
+
"# Real Heading One (p1)",
|
|
348
|
+
"# Real Heading Two (p1)",
|
|
349
|
+
]);
|
|
350
|
+
expect(pythonHeadingsClean).toEqual([
|
|
351
|
+
"# Real Heading One (p1)",
|
|
352
|
+
"# Real Heading Two (p1)",
|
|
353
|
+
]);
|
|
263
354
|
expect(nodeHeadingsClean).toEqual(pythonHeadingsClean);
|
|
264
355
|
});
|
|
265
356
|
});
|
|
@@ -57,14 +57,17 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
57
57
|
|
|
58
58
|
const serverPath = resolve(__dirname, "../dist/index.js");
|
|
59
59
|
if (!existsSync(serverPath)) {
|
|
60
|
-
throw new Error(
|
|
60
|
+
throw new Error(
|
|
61
|
+
"MCP server not built. Run 'npm run build' before tests.",
|
|
62
|
+
);
|
|
61
63
|
}
|
|
62
64
|
serverProc = spawn("node", [serverPath]);
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
afterAll(() => {
|
|
66
68
|
if (serverProc && !serverProc.killed) serverProc.kill();
|
|
67
|
-
if (workDir && existsSync(workDir))
|
|
69
|
+
if (workDir && existsSync(workDir))
|
|
70
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
68
71
|
});
|
|
69
72
|
|
|
70
73
|
function sendRpc(method: string, params: any, id: number): Promise<any> {
|
|
@@ -105,9 +108,12 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
105
108
|
const r1 = await callTool(
|
|
106
109
|
"process_document_batch",
|
|
107
110
|
{
|
|
111
|
+
reasoning: "test",
|
|
108
112
|
original_docx_path: contractPath,
|
|
109
113
|
author_name: "Agent",
|
|
110
|
-
changes: [
|
|
114
|
+
changes: [
|
|
115
|
+
{ type: "modify", target_text: "Provider", new_text: "Supplier" },
|
|
116
|
+
],
|
|
111
117
|
},
|
|
112
118
|
201,
|
|
113
119
|
);
|
|
@@ -120,7 +126,10 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
120
126
|
// NOT back onto contract.docx / contract_processed.docx.
|
|
121
127
|
const r2 = await callTool(
|
|
122
128
|
"accept_all_changes",
|
|
123
|
-
{
|
|
129
|
+
{
|
|
130
|
+
reasoning: "test",
|
|
131
|
+
docx_path: join(workDir, "contract_processed.docx"),
|
|
132
|
+
},
|
|
124
133
|
202,
|
|
125
134
|
);
|
|
126
135
|
const msg2 = r2.result.content[0].text as string;
|
|
@@ -145,15 +154,20 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
145
154
|
const r = await callTool(
|
|
146
155
|
"process_document_batch",
|
|
147
156
|
{
|
|
157
|
+
reasoning: "test",
|
|
148
158
|
original_docx_path: join(workDir, "contract_processed.docx"),
|
|
149
159
|
author_name: "Agent",
|
|
150
|
-
changes: [
|
|
160
|
+
changes: [
|
|
161
|
+
{ type: "modify", target_text: "goods", new_text: "products" },
|
|
162
|
+
],
|
|
151
163
|
},
|
|
152
164
|
203,
|
|
153
165
|
);
|
|
154
166
|
const msg = r.result.content[0].text as string;
|
|
155
167
|
expect(savedFile(msg)).toBe("contract_processed.docx");
|
|
156
|
-
expect(existsSync(join(workDir, "contract_processed_processed.docx"))).toBe(
|
|
168
|
+
expect(existsSync(join(workDir, "contract_processed_processed.docx"))).toBe(
|
|
169
|
+
false,
|
|
170
|
+
);
|
|
157
171
|
});
|
|
158
172
|
|
|
159
173
|
it("Issue 1 (end-to-end via shipped server): accept + modify the same foreign insertion in one batch succeeds", async () => {
|
|
@@ -162,7 +176,9 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
162
176
|
const foreign = join(workDir, "lease.docx");
|
|
163
177
|
{
|
|
164
178
|
const doc = await DocumentObject.load(
|
|
165
|
-
readFileSync(
|
|
179
|
+
readFileSync(
|
|
180
|
+
resolve(__dirname, "../../../../shared/fixtures/initial.docx"),
|
|
181
|
+
),
|
|
166
182
|
);
|
|
167
183
|
const body = doc.element;
|
|
168
184
|
while (body.firstChild) body.removeChild(body.firstChild);
|
|
@@ -191,6 +207,7 @@ describe("Field feedback repro — Issue 3 file-path ambiguity (real MCP server)
|
|
|
191
207
|
const res = await callTool(
|
|
192
208
|
"process_document_batch",
|
|
193
209
|
{
|
|
210
|
+
reasoning: "test",
|
|
194
211
|
original_docx_path: foreign,
|
|
195
212
|
author_name: "Acme's Counsel",
|
|
196
213
|
output_path: join(workDir, "lease_out.docx"),
|
package/src/response-builders.ts
CHANGED
|
@@ -220,13 +220,27 @@ export function build_search_response(
|
|
|
220
220
|
const [body] = split_structural_appendix(text);
|
|
221
221
|
const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
222
222
|
const flags = search_case_sensitive ? "g" : "gi";
|
|
223
|
-
const patternStr = search_regex ? search_query : escapeRegExp(search_query);
|
|
224
223
|
|
|
224
|
+
// When the caller asked for a regex but supplied something the engine can't
|
|
225
|
+
// compile (e.g. an unterminated character class `\[`, or an inline-flag group
|
|
226
|
+
// `(?i)...` that JS RegExp rejects), do NOT hard-error and burn the turn.
|
|
227
|
+
// Downgrade to a literal search of the raw string and tell the model, so it
|
|
228
|
+
// can either accept the literal hits or fix its pattern — instead of retrying
|
|
229
|
+
// the same broken regex.
|
|
230
|
+
let regexDowngradedNote = "";
|
|
225
231
|
let regex: RegExp;
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
if (search_regex) {
|
|
233
|
+
try {
|
|
234
|
+
regex = new RegExp(search_query, flags);
|
|
235
|
+
} catch (e: any) {
|
|
236
|
+
regexDowngradedNote =
|
|
237
|
+
`> **Note:** \`${search_query}\` is not a valid regular expression ` +
|
|
238
|
+
`(${e.message}), so it was searched as literal text instead. ` +
|
|
239
|
+
`If you meant a regex, fix the pattern; if you meant literal text, set \`search_regex\` to false.`;
|
|
240
|
+
regex = new RegExp(escapeRegExp(search_query), flags);
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
regex = new RegExp(escapeRegExp(search_query), flags);
|
|
230
244
|
}
|
|
231
245
|
|
|
232
246
|
const allMatches = Array.from(body.matchAll(regex));
|
|
@@ -295,6 +309,7 @@ export function build_search_response(
|
|
|
295
309
|
} else {
|
|
296
310
|
body_msg = `> **Search Results** — No matches found for query \`${search_query}\` in \`${basename(file_path)}\`.\n\nVerify your search spelling, or try setting \`search_case_sensitive\` to false or enabling \`search_regex\` if you used pattern wildcards.`;
|
|
297
311
|
}
|
|
312
|
+
if (regexDowngradedNote) body_msg = `${regexDowngradedNote}\n\n${body_msg}`;
|
|
298
313
|
const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${body_msg}`;
|
|
299
314
|
return {
|
|
300
315
|
content: [{ type: "text", text: llm_content }],
|
|
@@ -415,6 +430,7 @@ export function build_search_response(
|
|
|
415
430
|
i++;
|
|
416
431
|
}
|
|
417
432
|
|
|
433
|
+
if (regexDowngradedNote) ui_parts.unshift(regexDowngradedNote);
|
|
418
434
|
const ui_markdown = ui_parts.join("\n\n");
|
|
419
435
|
const llm_content = `> **File Path:** \`${resolve(file_path)}\`\n\n${ui_markdown}`;
|
|
420
436
|
|