@probelabs/probe 0.6.0-rc291 → 0.6.0-rc292

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.
@@ -234,7 +234,14 @@ export function generateSandboxGlobals(options) {
234
234
  }
235
235
  return tryParseJSONValue(text);
236
236
  };
237
- globals[name] = traceToolCall(name, rawMcpFn, tracer, logFn);
237
+ const tracedFn = traceToolCall(name, rawMcpFn, tracer, logFn);
238
+ globals[name] = tracedFn;
239
+ // Register sanitized alias for names with hyphens/dots/etc that aren't valid JS identifiers
240
+ // e.g. "workable-api" → also available as "workable_api"
241
+ const sanitized = name.replace(/[^a-zA-Z0-9_$]/g, '_');
242
+ if (sanitized !== name) {
243
+ globals[sanitized] = tracedFn;
244
+ }
238
245
  }
239
246
  }
240
247
 
@@ -181,9 +181,17 @@ export function createDSLRuntime(options) {
181
181
  'dsl.error': e.message?.substring(0, 500),
182
182
  });
183
183
 
184
+ // Enrich "X is not defined" errors with available tool names
185
+ let errorMsg = `Execution failed: ${e.message}`;
186
+ if (e.message && e.message.includes('is not defined')) {
187
+ const globalNames = Object.keys(toolGlobals).sort();
188
+ errorMsg += `\nAvailable functions: ${globalNames.join(', ')}`;
189
+ errorMsg += `\nNote: Tools with hyphens (e.g. "my-tool") are available with underscores: my_tool()`;
190
+ }
191
+
184
192
  return {
185
193
  status: 'error',
186
- error: `Execution failed: ${e.message}`,
194
+ error: errorMsg,
187
195
  logs,
188
196
  };
189
197
  }
@@ -778,6 +778,7 @@ return table;
778
778
  - Do NOT define helper functions that call tools. Write all logic inline or use for..of loops.
779
779
  - Do NOT use regex literals (/pattern/) — use String methods like indexOf, includes, startsWith instead.
780
780
  - ONLY use functions listed below. Do NOT call functions that are not listed.
781
+ - MCP tools with hyphens in their names (e.g. \`workable-api\`) are available using underscores: \`workable_api()\`. Hyphens are not valid in JS identifiers.
781
782
 
782
783
  ### Available functions
783
784
 
@@ -73,7 +73,17 @@ export function lineTrimmedMatch(contentLines, searchLines) {
73
73
  }
74
74
  }
75
75
  if (allMatch) {
76
- const matchedText = contentLines.slice(i, i + windowSize).join('\n');
76
+ // Limit indent tolerance: even though trimmed content matches, reject when
77
+ // the indentation level difference is too large — it likely means the match
78
+ // is in a completely different scope (issue #507).
79
+ const windowLines = contentLines.slice(i, i + windowSize);
80
+ const windowMinIndent = getMinIndent(windowLines);
81
+ const searchMinIndent = getMinIndent(searchLines);
82
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
83
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
84
+ continue; // Skip — too far off in nesting
85
+ }
86
+ const matchedText = windowLines.join('\n');
77
87
  matches.push(matchedText);
78
88
  }
79
89
  }
@@ -134,6 +144,19 @@ export function whitespaceNormalizedMatch(content, search) {
134
144
  }
135
145
 
136
146
  const matchedText = content.substring(originalStart, actualEnd);
147
+
148
+ // Limit indent tolerance: reject matches where the indentation level
149
+ // difference is too large — likely a wrong-scope match (issue #507).
150
+ const matchedLines = matchedText.split('\n');
151
+ const searchLines = search.split('\n');
152
+ const matchMinIndent = getMinIndent(matchedLines);
153
+ const searchMinIndent = getMinIndent(searchLines);
154
+ const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
155
+ if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
156
+ searchStart = idx + 1;
157
+ continue; // Skip — too far off in nesting
158
+ }
159
+
137
160
  matches.push(matchedText);
138
161
 
139
162
  searchStart = idx + 1;
@@ -219,6 +242,15 @@ export function indentFlexibleMatch(contentLines, searchLines) {
219
242
  }
220
243
 
221
244
  if (allMatch) {
245
+ // Limit indent tolerance: reject matches where indentation differs by more than
246
+ // 1 level. Larger differences likely mean the match is in a completely different
247
+ // scope/nesting level — silent file corruption risk (issue #507).
248
+ // For tabs: 1 tab = 1 level, so max diff = 1.
249
+ // For spaces: detect indent unit (2 or 4), allow 1 unit of difference.
250
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
251
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
252
+ continue; // Skip — too far off in nesting
253
+ }
222
254
  const matchedText = windowLines.join('\n');
223
255
  matches.push(matchedText);
224
256
  }
@@ -232,6 +264,25 @@ export function indentFlexibleMatch(contentLines, searchLines) {
232
264
  };
233
265
  }
234
266
 
267
+ /**
268
+ * Check if an indentation difference exceeds the allowed limit.
269
+ * Uses tab-aware threshold: 1 for tabs, 4 for spaces.
270
+ * Checks BOTH sides for tab usage to avoid asymmetric detection.
271
+ *
272
+ * @param {string[]} linesA - First set of lines
273
+ * @param {string[]} linesB - Second set of lines
274
+ * @param {number} indentDiff - Absolute difference in min indent
275
+ * @returns {boolean} true if the diff exceeds the limit
276
+ */
277
+ function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
278
+ if (indentDiff <= 0) return false;
279
+ const sampleA = linesA.find(l => l.trim().length > 0) || '';
280
+ const sampleB = linesB.find(l => l.trim().length > 0) || '';
281
+ const useTabs = sampleA.startsWith('\t') || sampleB.startsWith('\t');
282
+ const maxAllowedDiff = useTabs ? 1 : 4;
283
+ return indentDiff > maxAllowedDiff;
284
+ }
285
+
235
286
  /**
236
287
  * Get the minimum indentation level (number of leading whitespace characters)
237
288
  * across all non-empty lines.
@@ -92,6 +92,17 @@ export function restoreIndentation(newStr, originalLines) {
92
92
  const newIndent = detectBaseIndent(newStr);
93
93
 
94
94
  if (targetIndent !== newIndent) {
95
+ // Limit auto-reindent tolerance: reject when indentation differs by more than
96
+ // 1 level. Larger differences likely mean the match landed in a completely
97
+ // different scope — allowing it risks silent file corruption (issue #507).
98
+ // For tabs: 1 tab = 1 level, so max diff = 1 char.
99
+ // For spaces: 1 level = up to 4 spaces, so max diff = 4 chars.
100
+ const indentDiff = Math.abs(targetIndent.length - newIndent.length);
101
+ const useTabs = targetIndent.includes('\t') || newIndent.includes('\t');
102
+ const maxAllowedDiff = useTabs ? 1 : 4;
103
+ if (indentDiff > maxAllowedDiff) {
104
+ return { result: newStr, modifications };
105
+ }
95
106
  const reindented = reindent(newStr, targetIndent);
96
107
  if (reindented !== newStr) {
97
108
  modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
@@ -29908,7 +29908,14 @@ function lineTrimmedMatch(contentLines, searchLines) {
29908
29908
  }
29909
29909
  }
29910
29910
  if (allMatch) {
29911
- const matchedText = contentLines.slice(i, i + windowSize).join("\n");
29911
+ const windowLines = contentLines.slice(i, i + windowSize);
29912
+ const windowMinIndent = getMinIndent(windowLines);
29913
+ const searchMinIndent = getMinIndent(searchLines);
29914
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
29915
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
29916
+ continue;
29917
+ }
29918
+ const matchedText = windowLines.join("\n");
29912
29919
  matches.push(matchedText);
29913
29920
  }
29914
29921
  }
@@ -29938,6 +29945,15 @@ function whitespaceNormalizedMatch(content, search2) {
29938
29945
  actualEnd++;
29939
29946
  }
29940
29947
  const matchedText = content.substring(originalStart, actualEnd);
29948
+ const matchedLines = matchedText.split("\n");
29949
+ const searchLines = search2.split("\n");
29950
+ const matchMinIndent = getMinIndent(matchedLines);
29951
+ const searchMinIndent = getMinIndent(searchLines);
29952
+ const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
29953
+ if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
29954
+ searchStart = idx + 1;
29955
+ continue;
29956
+ }
29941
29957
  matches.push(matchedText);
29942
29958
  searchStart = idx + 1;
29943
29959
  }
@@ -29989,6 +30005,10 @@ function indentFlexibleMatch(contentLines, searchLines) {
29989
30005
  }
29990
30006
  }
29991
30007
  if (allMatch) {
30008
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
30009
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
30010
+ continue;
30011
+ }
29992
30012
  const matchedText = windowLines.join("\n");
29993
30013
  matches.push(matchedText);
29994
30014
  }
@@ -29999,6 +30019,14 @@ function indentFlexibleMatch(contentLines, searchLines) {
29999
30019
  count: matches.length
30000
30020
  };
30001
30021
  }
30022
+ function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
30023
+ if (indentDiff <= 0) return false;
30024
+ const sampleA = linesA.find((l) => l.trim().length > 0) || "";
30025
+ const sampleB = linesB.find((l) => l.trim().length > 0) || "";
30026
+ const useTabs = sampleA.startsWith(" ") || sampleB.startsWith(" ");
30027
+ const maxAllowedDiff = useTabs ? 1 : 4;
30028
+ return indentDiff > maxAllowedDiff;
30029
+ }
30002
30030
  function getMinIndent(lines) {
30003
30031
  let min = Infinity;
30004
30032
  for (const line of lines) {
@@ -30158,6 +30186,12 @@ function restoreIndentation(newStr, originalLines) {
30158
30186
  const targetIndent = detectBaseIndent(originalCode);
30159
30187
  const newIndent = detectBaseIndent(newStr);
30160
30188
  if (targetIndent !== newIndent) {
30189
+ const indentDiff = Math.abs(targetIndent.length - newIndent.length);
30190
+ const useTabs = targetIndent.includes(" ") || newIndent.includes(" ");
30191
+ const maxAllowedDiff = useTabs ? 1 : 4;
30192
+ if (indentDiff > maxAllowedDiff) {
30193
+ return { result: newStr, modifications };
30194
+ }
30161
30195
  const reindented = reindent(newStr, targetIndent);
30162
30196
  if (reindented !== newStr) {
30163
30197
  modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
@@ -40531,7 +40565,12 @@ function generateSandboxGlobals(options) {
40531
40565
  }
40532
40566
  return tryParseJSONValue(text);
40533
40567
  };
40534
- globals[name15] = traceToolCall(name15, rawMcpFn, tracer, logFn);
40568
+ const tracedFn = traceToolCall(name15, rawMcpFn, tracer, logFn);
40569
+ globals[name15] = tracedFn;
40570
+ const sanitized = name15.replace(/[^a-zA-Z0-9_$]/g, "_");
40571
+ if (sanitized !== name15) {
40572
+ globals[sanitized] = tracedFn;
40573
+ }
40535
40574
  }
40536
40575
  }
40537
40576
  if (llmCall) {
@@ -40886,9 +40925,17 @@ ${validation.errors.join("\n")}`,
40886
40925
  "dsl.duration_ms": elapsed,
40887
40926
  "dsl.error": e.message?.substring(0, 500)
40888
40927
  });
40928
+ let errorMsg = `Execution failed: ${e.message}`;
40929
+ if (e.message && e.message.includes("is not defined")) {
40930
+ const globalNames = Object.keys(toolGlobals).sort();
40931
+ errorMsg += `
40932
+ Available functions: ${globalNames.join(", ")}`;
40933
+ errorMsg += `
40934
+ Note: Tools with hyphens (e.g. "my-tool") are available with underscores: my_tool()`;
40935
+ }
40889
40936
  return {
40890
40937
  status: "error",
40891
- error: `Execution failed: ${e.message}`,
40938
+ error: errorMsg,
40892
40939
  logs
40893
40940
  };
40894
40941
  }
package/cjs/index.cjs CHANGED
@@ -92219,7 +92219,12 @@ function generateSandboxGlobals(options) {
92219
92219
  }
92220
92220
  return tryParseJSONValue(text);
92221
92221
  };
92222
- globals[name15] = traceToolCall(name15, rawMcpFn, tracer, logFn);
92222
+ const tracedFn = traceToolCall(name15, rawMcpFn, tracer, logFn);
92223
+ globals[name15] = tracedFn;
92224
+ const sanitized = name15.replace(/[^a-zA-Z0-9_$]/g, "_");
92225
+ if (sanitized !== name15) {
92226
+ globals[sanitized] = tracedFn;
92227
+ }
92223
92228
  }
92224
92229
  }
92225
92230
  if (llmCall) {
@@ -92574,9 +92579,17 @@ ${validation.errors.join("\n")}`,
92574
92579
  "dsl.duration_ms": elapsed,
92575
92580
  "dsl.error": e.message?.substring(0, 500)
92576
92581
  });
92582
+ let errorMsg = `Execution failed: ${e.message}`;
92583
+ if (e.message && e.message.includes("is not defined")) {
92584
+ const globalNames = Object.keys(toolGlobals).sort();
92585
+ errorMsg += `
92586
+ Available functions: ${globalNames.join(", ")}`;
92587
+ errorMsg += `
92588
+ Note: Tools with hyphens (e.g. "my-tool") are available with underscores: my_tool()`;
92589
+ }
92577
92590
  return {
92578
92591
  status: "error",
92579
- error: `Execution failed: ${e.message}`,
92592
+ error: errorMsg,
92580
92593
  logs
92581
92594
  };
92582
92595
  }
@@ -101893,7 +101906,14 @@ function lineTrimmedMatch(contentLines, searchLines) {
101893
101906
  }
101894
101907
  }
101895
101908
  if (allMatch) {
101896
- const matchedText = contentLines.slice(i, i + windowSize).join("\n");
101909
+ const windowLines = contentLines.slice(i, i + windowSize);
101910
+ const windowMinIndent = getMinIndent(windowLines);
101911
+ const searchMinIndent = getMinIndent(searchLines);
101912
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
101913
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
101914
+ continue;
101915
+ }
101916
+ const matchedText = windowLines.join("\n");
101897
101917
  matches.push(matchedText);
101898
101918
  }
101899
101919
  }
@@ -101923,6 +101943,15 @@ function whitespaceNormalizedMatch(content, search2) {
101923
101943
  actualEnd++;
101924
101944
  }
101925
101945
  const matchedText = content.substring(originalStart, actualEnd);
101946
+ const matchedLines = matchedText.split("\n");
101947
+ const searchLines = search2.split("\n");
101948
+ const matchMinIndent = getMinIndent(matchedLines);
101949
+ const searchMinIndent = getMinIndent(searchLines);
101950
+ const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
101951
+ if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
101952
+ searchStart = idx + 1;
101953
+ continue;
101954
+ }
101926
101955
  matches.push(matchedText);
101927
101956
  searchStart = idx + 1;
101928
101957
  }
@@ -101974,6 +102003,10 @@ function indentFlexibleMatch(contentLines, searchLines) {
101974
102003
  }
101975
102004
  }
101976
102005
  if (allMatch) {
102006
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
102007
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
102008
+ continue;
102009
+ }
101977
102010
  const matchedText = windowLines.join("\n");
101978
102011
  matches.push(matchedText);
101979
102012
  }
@@ -101984,6 +102017,14 @@ function indentFlexibleMatch(contentLines, searchLines) {
101984
102017
  count: matches.length
101985
102018
  };
101986
102019
  }
102020
+ function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
102021
+ if (indentDiff <= 0) return false;
102022
+ const sampleA = linesA.find((l) => l.trim().length > 0) || "";
102023
+ const sampleB = linesB.find((l) => l.trim().length > 0) || "";
102024
+ const useTabs = sampleA.startsWith(" ") || sampleB.startsWith(" ");
102025
+ const maxAllowedDiff = useTabs ? 1 : 4;
102026
+ return indentDiff > maxAllowedDiff;
102027
+ }
101987
102028
  function getMinIndent(lines) {
101988
102029
  let min = Infinity;
101989
102030
  for (const line of lines) {
@@ -102058,6 +102099,12 @@ function restoreIndentation(newStr, originalLines) {
102058
102099
  const targetIndent = detectBaseIndent(originalCode);
102059
102100
  const newIndent = detectBaseIndent(newStr);
102060
102101
  if (targetIndent !== newIndent) {
102102
+ const indentDiff = Math.abs(targetIndent.length - newIndent.length);
102103
+ const useTabs = targetIndent.includes(" ") || newIndent.includes(" ");
102104
+ const maxAllowedDiff = useTabs ? 1 : 4;
102105
+ if (indentDiff > maxAllowedDiff) {
102106
+ return { result: newStr, modifications };
102107
+ }
102061
102108
  const reindented = reindent(newStr, targetIndent);
102062
102109
  if (reindented !== newStr) {
102063
102110
  modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@probelabs/probe",
3
- "version": "0.6.0-rc291",
3
+ "version": "0.6.0-rc292",
4
4
  "description": "Node.js wrapper for the probe code search tool",
5
5
  "main": "src/index.js",
6
6
  "module": "src/index.js",
@@ -234,7 +234,14 @@ export function generateSandboxGlobals(options) {
234
234
  }
235
235
  return tryParseJSONValue(text);
236
236
  };
237
- globals[name] = traceToolCall(name, rawMcpFn, tracer, logFn);
237
+ const tracedFn = traceToolCall(name, rawMcpFn, tracer, logFn);
238
+ globals[name] = tracedFn;
239
+ // Register sanitized alias for names with hyphens/dots/etc that aren't valid JS identifiers
240
+ // e.g. "workable-api" → also available as "workable_api"
241
+ const sanitized = name.replace(/[^a-zA-Z0-9_$]/g, '_');
242
+ if (sanitized !== name) {
243
+ globals[sanitized] = tracedFn;
244
+ }
238
245
  }
239
246
  }
240
247
 
@@ -181,9 +181,17 @@ export function createDSLRuntime(options) {
181
181
  'dsl.error': e.message?.substring(0, 500),
182
182
  });
183
183
 
184
+ // Enrich "X is not defined" errors with available tool names
185
+ let errorMsg = `Execution failed: ${e.message}`;
186
+ if (e.message && e.message.includes('is not defined')) {
187
+ const globalNames = Object.keys(toolGlobals).sort();
188
+ errorMsg += `\nAvailable functions: ${globalNames.join(', ')}`;
189
+ errorMsg += `\nNote: Tools with hyphens (e.g. "my-tool") are available with underscores: my_tool()`;
190
+ }
191
+
184
192
  return {
185
193
  status: 'error',
186
- error: `Execution failed: ${e.message}`,
194
+ error: errorMsg,
187
195
  logs,
188
196
  };
189
197
  }
@@ -778,6 +778,7 @@ return table;
778
778
  - Do NOT define helper functions that call tools. Write all logic inline or use for..of loops.
779
779
  - Do NOT use regex literals (/pattern/) — use String methods like indexOf, includes, startsWith instead.
780
780
  - ONLY use functions listed below. Do NOT call functions that are not listed.
781
+ - MCP tools with hyphens in their names (e.g. \`workable-api\`) are available using underscores: \`workable_api()\`. Hyphens are not valid in JS identifiers.
781
782
 
782
783
  ### Available functions
783
784
 
@@ -73,7 +73,17 @@ export function lineTrimmedMatch(contentLines, searchLines) {
73
73
  }
74
74
  }
75
75
  if (allMatch) {
76
- const matchedText = contentLines.slice(i, i + windowSize).join('\n');
76
+ // Limit indent tolerance: even though trimmed content matches, reject when
77
+ // the indentation level difference is too large — it likely means the match
78
+ // is in a completely different scope (issue #507).
79
+ const windowLines = contentLines.slice(i, i + windowSize);
80
+ const windowMinIndent = getMinIndent(windowLines);
81
+ const searchMinIndent = getMinIndent(searchLines);
82
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
83
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
84
+ continue; // Skip — too far off in nesting
85
+ }
86
+ const matchedText = windowLines.join('\n');
77
87
  matches.push(matchedText);
78
88
  }
79
89
  }
@@ -134,6 +144,19 @@ export function whitespaceNormalizedMatch(content, search) {
134
144
  }
135
145
 
136
146
  const matchedText = content.substring(originalStart, actualEnd);
147
+
148
+ // Limit indent tolerance: reject matches where the indentation level
149
+ // difference is too large — likely a wrong-scope match (issue #507).
150
+ const matchedLines = matchedText.split('\n');
151
+ const searchLines = search.split('\n');
152
+ const matchMinIndent = getMinIndent(matchedLines);
153
+ const searchMinIndent = getMinIndent(searchLines);
154
+ const indentDiff = Math.abs(matchMinIndent - searchMinIndent);
155
+ if (isIndentDiffTooLarge(matchedLines, searchLines, indentDiff)) {
156
+ searchStart = idx + 1;
157
+ continue; // Skip — too far off in nesting
158
+ }
159
+
137
160
  matches.push(matchedText);
138
161
 
139
162
  searchStart = idx + 1;
@@ -219,6 +242,15 @@ export function indentFlexibleMatch(contentLines, searchLines) {
219
242
  }
220
243
 
221
244
  if (allMatch) {
245
+ // Limit indent tolerance: reject matches where indentation differs by more than
246
+ // 1 level. Larger differences likely mean the match is in a completely different
247
+ // scope/nesting level — silent file corruption risk (issue #507).
248
+ // For tabs: 1 tab = 1 level, so max diff = 1.
249
+ // For spaces: detect indent unit (2 or 4), allow 1 unit of difference.
250
+ const indentDiff = Math.abs(windowMinIndent - searchMinIndent);
251
+ if (isIndentDiffTooLarge(windowLines, searchLines, indentDiff)) {
252
+ continue; // Skip — too far off in nesting
253
+ }
222
254
  const matchedText = windowLines.join('\n');
223
255
  matches.push(matchedText);
224
256
  }
@@ -232,6 +264,25 @@ export function indentFlexibleMatch(contentLines, searchLines) {
232
264
  };
233
265
  }
234
266
 
267
+ /**
268
+ * Check if an indentation difference exceeds the allowed limit.
269
+ * Uses tab-aware threshold: 1 for tabs, 4 for spaces.
270
+ * Checks BOTH sides for tab usage to avoid asymmetric detection.
271
+ *
272
+ * @param {string[]} linesA - First set of lines
273
+ * @param {string[]} linesB - Second set of lines
274
+ * @param {number} indentDiff - Absolute difference in min indent
275
+ * @returns {boolean} true if the diff exceeds the limit
276
+ */
277
+ function isIndentDiffTooLarge(linesA, linesB, indentDiff) {
278
+ if (indentDiff <= 0) return false;
279
+ const sampleA = linesA.find(l => l.trim().length > 0) || '';
280
+ const sampleB = linesB.find(l => l.trim().length > 0) || '';
281
+ const useTabs = sampleA.startsWith('\t') || sampleB.startsWith('\t');
282
+ const maxAllowedDiff = useTabs ? 1 : 4;
283
+ return indentDiff > maxAllowedDiff;
284
+ }
285
+
235
286
  /**
236
287
  * Get the minimum indentation level (number of leading whitespace characters)
237
288
  * across all non-empty lines.
@@ -92,6 +92,17 @@ export function restoreIndentation(newStr, originalLines) {
92
92
  const newIndent = detectBaseIndent(newStr);
93
93
 
94
94
  if (targetIndent !== newIndent) {
95
+ // Limit auto-reindent tolerance: reject when indentation differs by more than
96
+ // 1 level. Larger differences likely mean the match landed in a completely
97
+ // different scope — allowing it risks silent file corruption (issue #507).
98
+ // For tabs: 1 tab = 1 level, so max diff = 1 char.
99
+ // For spaces: 1 level = up to 4 spaces, so max diff = 4 chars.
100
+ const indentDiff = Math.abs(targetIndent.length - newIndent.length);
101
+ const useTabs = targetIndent.includes('\t') || newIndent.includes('\t');
102
+ const maxAllowedDiff = useTabs ? 1 : 4;
103
+ if (indentDiff > maxAllowedDiff) {
104
+ return { result: newStr, modifications };
105
+ }
95
106
  const reindented = reindent(newStr, targetIndent);
96
107
  if (reindented !== newStr) {
97
108
  modifications.push(`reindented from "${newIndent}" to "${targetIndent}"`);