sweet-search 2.6.16 → 2.7.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/README.md +7 -3
- package/core/graph/structural-context-format.js +5 -0
- package/core/graph/structural-context.js +13 -3
- package/core/infrastructure/code-graph-repository.js +58 -0
- package/core/infrastructure/language-patterns/registry-core.js +14 -0
- package/core/infrastructure/model-fetcher.js +7 -0
- package/core/infrastructure/structural-context-repository.js +73 -7
- package/core/infrastructure/structural-context-utils.js +11 -0
- package/core/infrastructure/structural-qualified-resolution.js +29 -0
- package/core/infrastructure/tree-sitter-provider.js +77 -3
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +12 -6
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +4 -2
- package/core/search/agent-pack-completion.js +493 -0
- package/core/search/agent-span-client.js +87 -0
- package/core/search/agent-span-ledger.js +447 -0
- package/core/search/context-expander.js +29 -0
- package/core/search/grep-output-shaping.js +28 -0
- package/core/search/regex-dialect.js +328 -0
- package/core/search/search-format.js +96 -0
- package/core/search/search-pattern.js +62 -10
- package/core/search/search-read.js +57 -10
- package/core/search/search-server.js +375 -7
- package/core/search/session-daemon-prewarm.mjs +75 -0
- package/core/search/sweet-search.js +7 -1
- package/core/search/unread-symbol-ranking.js +90 -0
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +249 -45
- package/mcp/read-tool.js +34 -3
- package/mcp/server.js +55 -9
- package/mcp/tool-handlers.js +60 -7
- package/package.json +9 -9
- package/scripts/init.js +30 -5
- package/scripts/inject-agent-instructions.js +4 -2
- package/scripts/uninstall.js +21 -19
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect grep patterns that may have been written with GNU BRE muscle memory.
|
|
3
|
+
*
|
|
4
|
+
* Sweet Search deliberately keeps Rust-regex semantics for the first search.
|
|
5
|
+
* Agent-format callers may use the translation below for one retry after that
|
|
6
|
+
* exact pattern returns zero matches. Normal users and non-zero searches remain
|
|
7
|
+
* byte-identical.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BRE_OPERATOR_LABELS = Object.freeze({
|
|
11
|
+
'|': '\\|',
|
|
12
|
+
'(': '\\(',
|
|
13
|
+
')': '\\)',
|
|
14
|
+
'+': '\\+',
|
|
15
|
+
'?': '\\?',
|
|
16
|
+
'{': '\\{m,n\\}',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const BRE_RETRY_TRIGGERS = new Set(['|', '(', '+', '{']);
|
|
20
|
+
const BARE_BRE_DIVERGENT_METACHARS = new Set(['(', ')', '+', '?', '{', '|']);
|
|
21
|
+
const ZERO_WIDTH_ESCAPES = new Set(['A', 'b', 'B', 'z']);
|
|
22
|
+
const ADDITIVE_RETRY_STATS = [
|
|
23
|
+
'candidateGenTime_ms',
|
|
24
|
+
'grepTime_ms',
|
|
25
|
+
'literalFilterTime_ms',
|
|
26
|
+
'gramLookupTime_ms',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function isEscapedDoublePipe(pattern, slashStart, escapedIndex) {
|
|
30
|
+
const previous = pattern.slice(Math.max(0, slashStart - 2), slashStart) === '\\|';
|
|
31
|
+
const next = pattern.slice(escapedIndex + 1, escapedIndex + 3) === '\\|';
|
|
32
|
+
return previous || next;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function inspectBreDialect(pattern, { fixedString = false } = {}) {
|
|
36
|
+
if (fixedString || typeof pattern !== 'string' || pattern.length < 2) return null;
|
|
37
|
+
|
|
38
|
+
const operators = new Set();
|
|
39
|
+
let retryable = false;
|
|
40
|
+
let translated = '';
|
|
41
|
+
let inClass = false;
|
|
42
|
+
let hasBareDivergentMetachar = false;
|
|
43
|
+
let hasEscapedQuestion = false;
|
|
44
|
+
let hasAmbiguousEscapeRun = false;
|
|
45
|
+
let hasEmptyAlternationArm = false;
|
|
46
|
+
const armStack = [{ hasContent: false, sawAlternation: false }];
|
|
47
|
+
|
|
48
|
+
const currentArm = () => armStack[armStack.length - 1];
|
|
49
|
+
const markContent = () => { currentArm().hasContent = true; };
|
|
50
|
+
const closeAlternationContext = () => {
|
|
51
|
+
const arm = currentArm();
|
|
52
|
+
if (arm.sawAlternation && !arm.hasContent) hasEmptyAlternationArm = true;
|
|
53
|
+
return arm;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
57
|
+
const ch = pattern[i];
|
|
58
|
+
if (inClass) {
|
|
59
|
+
translated += ch;
|
|
60
|
+
if (ch === '\\') {
|
|
61
|
+
let slashCount = 1;
|
|
62
|
+
while (pattern[i + slashCount] === '\\') slashCount++;
|
|
63
|
+
translated += '\\'.repeat(slashCount - 1);
|
|
64
|
+
i += slashCount - 1;
|
|
65
|
+
if (slashCount % 2 === 1 && i + 1 < pattern.length) {
|
|
66
|
+
translated += pattern[++i];
|
|
67
|
+
}
|
|
68
|
+
} else if (ch === ']') {
|
|
69
|
+
inClass = false;
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (ch === '[') {
|
|
74
|
+
inClass = true;
|
|
75
|
+
translated += ch;
|
|
76
|
+
markContent();
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch !== '\\') {
|
|
80
|
+
translated += ch;
|
|
81
|
+
if (BARE_BRE_DIVERGENT_METACHARS.has(ch)) {
|
|
82
|
+
hasBareDivergentMetachar = true;
|
|
83
|
+
markContent();
|
|
84
|
+
} else if (ch !== '^' && ch !== '$' && ch !== '*') {
|
|
85
|
+
markContent();
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const slashStart = i;
|
|
91
|
+
let slashCount = 1;
|
|
92
|
+
while (pattern[i + slashCount] === '\\') slashCount++;
|
|
93
|
+
const escapedIndex = slashStart + slashCount;
|
|
94
|
+
const escaped = pattern[escapedIndex];
|
|
95
|
+
// Multiple backslashes are ambiguous (for example a literal backslash
|
|
96
|
+
// followed by an escaped operator). Preserve the run, then re-scan the
|
|
97
|
+
// following character so `\\[...\|...]` still enters the character class.
|
|
98
|
+
if (slashCount !== 1 || !escaped) {
|
|
99
|
+
translated += '\\'.repeat(slashCount);
|
|
100
|
+
if (slashCount !== 1) {
|
|
101
|
+
hasAmbiguousEscapeRun = true;
|
|
102
|
+
markContent();
|
|
103
|
+
}
|
|
104
|
+
i = slashStart + slashCount - 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const rawEscape = pattern.slice(slashStart, escapedIndex + 1);
|
|
109
|
+
if (escaped === '{') {
|
|
110
|
+
const interval = pattern.slice(slashStart).match(/^\\\{(\d+(?:,\d*)?)\\\}/);
|
|
111
|
+
if (interval) {
|
|
112
|
+
operators.add('{');
|
|
113
|
+
retryable = true;
|
|
114
|
+
translated += `{${interval[1]}}`;
|
|
115
|
+
i = slashStart + interval[0].length - 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let operator = null;
|
|
121
|
+
if (escaped === '|' && !isEscapedDoublePipe(pattern, slashStart, escapedIndex)) operator = '|';
|
|
122
|
+
else if (escaped === '(' || escaped === ')' || escaped === '+' || escaped === '?') operator = escaped;
|
|
123
|
+
|
|
124
|
+
if (operator) {
|
|
125
|
+
operators.add(operator);
|
|
126
|
+
if (BRE_RETRY_TRIGGERS.has(operator)) retryable = true;
|
|
127
|
+
if (operator === '?') hasEscapedQuestion = true;
|
|
128
|
+
|
|
129
|
+
if (operator === '|') {
|
|
130
|
+
const arm = currentArm();
|
|
131
|
+
if (!arm.hasContent) hasEmptyAlternationArm = true;
|
|
132
|
+
arm.sawAlternation = true;
|
|
133
|
+
arm.hasContent = false;
|
|
134
|
+
} else if (operator === '(') {
|
|
135
|
+
armStack.push({ hasContent: false, sawAlternation: false });
|
|
136
|
+
} else if (operator === ')') {
|
|
137
|
+
const closed = closeAlternationContext();
|
|
138
|
+
if (armStack.length > 1) {
|
|
139
|
+
armStack.pop();
|
|
140
|
+
if (closed.hasContent) markContent();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
translated += operator === '?' ? rawEscape : operator;
|
|
145
|
+
} else {
|
|
146
|
+
translated += rawEscape;
|
|
147
|
+
if (!ZERO_WIDTH_ESCAPES.has(escaped)) markContent();
|
|
148
|
+
}
|
|
149
|
+
i = escapedIndex;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const arm of armStack) {
|
|
153
|
+
if (arm.sawAlternation && !arm.hasContent) hasEmptyAlternationArm = true;
|
|
154
|
+
}
|
|
155
|
+
if (operators.size === 0) return null;
|
|
156
|
+
return {
|
|
157
|
+
operators: [...operators].map(op => BRE_OPERATOR_LABELS[op]),
|
|
158
|
+
retryable,
|
|
159
|
+
translated,
|
|
160
|
+
translationAmbiguous: hasBareDivergentMetachar
|
|
161
|
+
|| hasAmbiguousEscapeRun
|
|
162
|
+
|| hasEmptyAlternationArm
|
|
163
|
+
|| (hasEscapedQuestion && retryable),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* @param {string} pattern
|
|
169
|
+
* @param {{ fixedString?: boolean }} [options]
|
|
170
|
+
* @returns {{ operators: string[] } | null}
|
|
171
|
+
*/
|
|
172
|
+
export function detectBreDialectHint(pattern, { fixedString = false } = {}) {
|
|
173
|
+
const inspection = inspectBreDialect(pattern, { fixedString });
|
|
174
|
+
return inspection ? { operators: inspection.operators } : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Translate only unambiguous GNU BRE operator spellings used by agents.
|
|
179
|
+
* Closing group/interval escapes are translated with their opening operator.
|
|
180
|
+
* `\?` remains diagnostic-only because it is too commonly used literally.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} pattern
|
|
183
|
+
* @param {{ fixedString?: boolean }} [options]
|
|
184
|
+
* @returns {{ pattern: string, operators: string[] } | null}
|
|
185
|
+
*/
|
|
186
|
+
export function translateBreToRustRegex(pattern, { fixedString = false } = {}) {
|
|
187
|
+
const inspection = inspectBreDialect(pattern, { fixedString });
|
|
188
|
+
if (!inspection?.retryable || inspection.translationAmbiguous || inspection.translated === pattern) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
if (new RegExp(inspection.translated).test('')) return null;
|
|
193
|
+
} catch { /* JavaScript cannot parse every Rust-valid regex; structural guards still apply. */ }
|
|
194
|
+
return { pattern: inspection.translated, operators: inspection.operators };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function collectCandidateMatches(candidateResult) {
|
|
198
|
+
return [
|
|
199
|
+
...(candidateResult?.indexedMatches || []),
|
|
200
|
+
...(candidateResult?.overlayMatches || []),
|
|
201
|
+
];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sumNumericFields(original = {}, retried = {}, selected = {}) {
|
|
205
|
+
const merged = { ...selected };
|
|
206
|
+
for (const key of ADDITIVE_RETRY_STATS) {
|
|
207
|
+
const left = Number.isFinite(original[key]) ? original[key] : 0;
|
|
208
|
+
const right = Number.isFinite(retried[key]) ? retried[key] : 0;
|
|
209
|
+
if (Number.isFinite(original[key]) || Number.isFinite(retried[key])) merged[key] = left + right;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const stageKeys = new Set([
|
|
213
|
+
...Object.keys(original.stageTiming || {}),
|
|
214
|
+
...Object.keys(retried.stageTiming || {}),
|
|
215
|
+
]);
|
|
216
|
+
if (stageKeys.size > 0) {
|
|
217
|
+
merged.stageTiming = { ...(selected.stageTiming || {}) };
|
|
218
|
+
for (const key of stageKeys) {
|
|
219
|
+
const left = Number.isFinite(original.stageTiming?.[key]) ? original.stageTiming[key] : 0;
|
|
220
|
+
const right = Number.isFinite(retried.stageTiming?.[key]) ? retried.stageTiming[key] : 0;
|
|
221
|
+
merged.stageTiming[key] = left + right;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return merged;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function withMergedRetryStats(selected, original, retried) {
|
|
228
|
+
if (!selected?.stats && !original?.stats && !retried?.stats) return selected;
|
|
229
|
+
return {
|
|
230
|
+
...selected,
|
|
231
|
+
stats: sumNumericFields(original?.stats, retried?.stats, selected?.stats),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function isRegexSyntaxError(error) {
|
|
236
|
+
const messages = [];
|
|
237
|
+
for (let current = error; current && messages.length < 3; current = current.cause) {
|
|
238
|
+
messages.push(current instanceof Error ? current.message : String(current));
|
|
239
|
+
}
|
|
240
|
+
return /invalid regex|regex parse error|error parsing regex|regex syntax|repetition operator|unclosed (?:group|character class)|unmatched (?:closing|parenthesis)/i
|
|
241
|
+
.test(messages.join('\n'));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Run one agent-only BRE respelling retry after the caller's final result
|
|
246
|
+
* shaping reports zero matches. The supplied shaper is reused for the retry so
|
|
247
|
+
* hint counts and adoption always describe the matches the caller can return.
|
|
248
|
+
*
|
|
249
|
+
* @param {{
|
|
250
|
+
* pattern: string,
|
|
251
|
+
* fixedString?: boolean,
|
|
252
|
+
* agentFormat?: boolean,
|
|
253
|
+
* originalResult: object,
|
|
254
|
+
* shapeResult?: (candidateResult: object) => Array,
|
|
255
|
+
* retry: (translatedPattern: string) => Promise<object>,
|
|
256
|
+
* }} options
|
|
257
|
+
* @returns {Promise<{
|
|
258
|
+
* candidateResult: object,
|
|
259
|
+
* matches: Array,
|
|
260
|
+
* regexDialectHint: object|null,
|
|
261
|
+
* }>}
|
|
262
|
+
*/
|
|
263
|
+
export async function retryBreDialectAfterZero({
|
|
264
|
+
pattern,
|
|
265
|
+
fixedString = false,
|
|
266
|
+
agentFormat = false,
|
|
267
|
+
originalResult,
|
|
268
|
+
shapeResult = collectCandidateMatches,
|
|
269
|
+
retry,
|
|
270
|
+
}) {
|
|
271
|
+
const originalMatches = shapeResult(originalResult);
|
|
272
|
+
if (!agentFormat || originalMatches.length > 0) {
|
|
273
|
+
return { candidateResult: originalResult, matches: originalMatches, regexDialectHint: null };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const fallbackHint = detectBreDialectHint(pattern, { fixedString });
|
|
277
|
+
const translation = translateBreToRustRegex(pattern, { fixedString });
|
|
278
|
+
if (!translation) {
|
|
279
|
+
return { candidateResult: originalResult, matches: originalMatches, regexDialectHint: fallbackHint };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const retried = await retry(translation.pattern);
|
|
284
|
+
const retriedMatches = shapeResult(retried);
|
|
285
|
+
const retryMatched = retriedMatches.length > 0;
|
|
286
|
+
const selected = retryMatched ? retried : originalResult;
|
|
287
|
+
return {
|
|
288
|
+
candidateResult: withMergedRetryStats(selected, originalResult, retried),
|
|
289
|
+
matches: retryMatched ? retriedMatches : originalMatches,
|
|
290
|
+
regexDialectHint: {
|
|
291
|
+
operators: translation.operators,
|
|
292
|
+
retryAttempted: true,
|
|
293
|
+
retryMatched,
|
|
294
|
+
retryMatches: retriedMatches.length,
|
|
295
|
+
translatedPattern: translation.pattern,
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (!isRegexSyntaxError(error)) throw error;
|
|
300
|
+
return { candidateResult: originalResult, matches: originalMatches, regexDialectHint: fallbackHint };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* @param {{
|
|
306
|
+
* operators?: string[], retryAttempted?: boolean, retryMatched?: boolean,
|
|
307
|
+
* retryMatches?: number, translatedPattern?: string,
|
|
308
|
+
* } | null | undefined} hint
|
|
309
|
+
* @returns {string}
|
|
310
|
+
*/
|
|
311
|
+
export function renderRegexDialectHint(hint) {
|
|
312
|
+
const operators = Array.isArray(hint?.operators) ? hint.operators : [];
|
|
313
|
+
if (operators.length === 0) return '';
|
|
314
|
+
if (hint.retryAttempted) {
|
|
315
|
+
if (hint.retryMatched) {
|
|
316
|
+
const count = Number.isInteger(hint.retryMatches) ? hint.retryMatches : 0;
|
|
317
|
+
return `regex note: zero-hit GNU BRE operators were retried with unescaped Rust operators; ` +
|
|
318
|
+
`showing ${count} retry match${count === 1 ? '' : 'es'}.`;
|
|
319
|
+
}
|
|
320
|
+
return 'regex note: GNU BRE operators were retried with unescaped Rust operators; both forms returned 0 matches.';
|
|
321
|
+
}
|
|
322
|
+
if (operators.includes('\\|')) {
|
|
323
|
+
return 'regex note: Rust syntax treats \\| as a literal pipe; use | for alternation ' +
|
|
324
|
+
'(or -F for a literal search). The original pattern was used unchanged.';
|
|
325
|
+
}
|
|
326
|
+
return `regex note: Rust syntax uses unescaped operators (${operators.join(', ')}); ` +
|
|
327
|
+
'escaped forms match punctuation. The original pattern was used unchanged.';
|
|
328
|
+
}
|
|
@@ -13,6 +13,102 @@
|
|
|
13
13
|
// Result formatting
|
|
14
14
|
// =============================================================================
|
|
15
15
|
|
|
16
|
+
const ROUTE_META_MARKER = '<<SS_ROUTE_META>>';
|
|
17
|
+
const COMPACT_ROUTE_RE = /^route=([A-Za-z0-9_.:-]+) confidence=([A-Za-z0-9_.:-]+) sufficient=(YES|no|unknown) reason=([A-Za-z0-9_.:-]+) repo=(ok|mismatch|unknown) results=(\d+)\s*$/gm;
|
|
18
|
+
const AGENT_HEADER_RE = /^#\s*ss-search:\s*routed=([A-Za-z0-9_.:-]+)(?:\s+conf=([-+]?(?:\d+(?:\.\d+)?|\.\d+)))?\s+budget=(\d+)\s+used=(\d+)\s+results=(\d+)\s+subMode=([A-Za-z0-9_.:-]+)\s*$/m;
|
|
19
|
+
|
|
20
|
+
function routeAtom(value, fallback = 'unknown') {
|
|
21
|
+
if (value == null || value === '') return fallback;
|
|
22
|
+
const atom = String(value).trim().replace(/[^A-Za-z0-9_.:-]+/g, '_');
|
|
23
|
+
return atom.slice(0, 96) || fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function routeSufficiency(meta) {
|
|
27
|
+
if (meta.sufficiencyVerdict === 'yes') return 'YES';
|
|
28
|
+
if (meta.sufficiencyVerdict === 'no' || meta.sufficiencyVerdict === 'unknown') {
|
|
29
|
+
return meta.sufficiencyVerdict;
|
|
30
|
+
}
|
|
31
|
+
if (meta.sufficient === true) return 'YES';
|
|
32
|
+
if (meta.sufficient === false) return 'no';
|
|
33
|
+
return 'unknown';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Serialize route metadata for an output surface.
|
|
38
|
+
*
|
|
39
|
+
* Agent output receives only fields that can change the next action. Debug
|
|
40
|
+
* and non-agent callers retain the complete machine-readable object.
|
|
41
|
+
*/
|
|
42
|
+
export function formatRouteMetadata(meta = {}, opts = {}) {
|
|
43
|
+
const routeMeta = meta && typeof meta === 'object' && !Array.isArray(meta) ? meta : {};
|
|
44
|
+
if (!opts._isAgentFormat || opts.debug === true) {
|
|
45
|
+
return `${ROUTE_META_MARKER}${JSON.stringify(routeMeta)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const repo = routeMeta.repoMatches === true
|
|
49
|
+
? 'ok'
|
|
50
|
+
: routeMeta.repoMatches === false ? 'mismatch' : 'unknown';
|
|
51
|
+
const count = Number.isInteger(routeMeta.resultCount) && routeMeta.resultCount >= 0
|
|
52
|
+
? routeMeta.resultCount
|
|
53
|
+
: 0;
|
|
54
|
+
const reason = routeMeta.sufficiencyReason || routeMeta.error;
|
|
55
|
+
|
|
56
|
+
return [
|
|
57
|
+
`route=${routeAtom(routeMeta.routedMode ?? routeMeta.mode)}`,
|
|
58
|
+
`confidence=${routeAtom(routeMeta.confidence)}`,
|
|
59
|
+
`sufficient=${routeSufficiency(routeMeta)}`,
|
|
60
|
+
`reason=${routeAtom(reason)}`,
|
|
61
|
+
`repo=${repo}`,
|
|
62
|
+
`results=${count}`,
|
|
63
|
+
].join(' ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse either the complete debug marker or the compact agent contract.
|
|
68
|
+
* Header telemetry is merged into compact metadata without requiring the
|
|
69
|
+
* verbose JSON to be present in agent context.
|
|
70
|
+
*/
|
|
71
|
+
export function parseRouteMetadata(text) {
|
|
72
|
+
const source = typeof text === 'string' ? text : '';
|
|
73
|
+
const full = source.match(/<<SS_ROUTE_META>>(\{[^\r\n]*\})/);
|
|
74
|
+
if (full) {
|
|
75
|
+
try {
|
|
76
|
+
const parsed = JSON.parse(full[1]);
|
|
77
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Search result bodies can contain arbitrary source text. Prefer the final
|
|
84
|
+
// trailer-shaped line because the engine appends its metadata after results.
|
|
85
|
+
const compactMatches = [...source.matchAll(COMPACT_ROUTE_RE)];
|
|
86
|
+
const compact = compactMatches.at(-1);
|
|
87
|
+
if (!compact) return null;
|
|
88
|
+
|
|
89
|
+
const verdict = compact[3] === 'YES' ? 'yes' : compact[3];
|
|
90
|
+
const meta = {
|
|
91
|
+
routedMode: compact[1],
|
|
92
|
+
serverUsed: true,
|
|
93
|
+
repoMatches: compact[5] === 'ok' ? true : compact[5] === 'mismatch' ? false : null,
|
|
94
|
+
resultCount: Number.parseInt(compact[6], 10),
|
|
95
|
+
confidence: compact[2],
|
|
96
|
+
sufficient: verdict === 'yes' ? true : verdict === 'no' ? false : null,
|
|
97
|
+
sufficiencyVerdict: verdict,
|
|
98
|
+
sufficiencyReason: compact[4] === 'unknown' ? null : compact[4],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const header = source.match(AGENT_HEADER_RE);
|
|
102
|
+
if (header) {
|
|
103
|
+
const routeConfidence = header[2] == null ? null : Number.parseFloat(header[2]);
|
|
104
|
+
if (Number.isFinite(routeConfidence)) meta.routeConfidence = routeConfidence;
|
|
105
|
+
meta.tokenBudget = Number.parseInt(header[3], 10);
|
|
106
|
+
meta.tokensUsed = Number.parseInt(header[4], 10);
|
|
107
|
+
meta.subMode = header[6];
|
|
108
|
+
}
|
|
109
|
+
return meta;
|
|
110
|
+
}
|
|
111
|
+
|
|
16
112
|
/**
|
|
17
113
|
* Format structural results for display
|
|
18
114
|
* Uses `this` — calls this methods (none currently, but kept as regular fn).
|
|
@@ -20,6 +20,8 @@ import { applyGrepFileDiversity, matchesGrepFileFilter } from './grep-output-sha
|
|
|
20
20
|
import { isRipgrepAvailable, runRipgrepJson } from './search-pattern-ripgrep.js';
|
|
21
21
|
import { ensureSparseGramIndex } from './search-pattern-prefilter.js';
|
|
22
22
|
import { packageForAgent } from './context-expander.js';
|
|
23
|
+
import { retryBreDialectAfterZero } from './regex-dialect.js';
|
|
24
|
+
import { buildIndexedGrepFamilyManifest } from './agent-pack-completion.js';
|
|
23
25
|
import { applyFileKindRanking, applyResultDemotions } from '../ranking/file-kind-ranking.js';
|
|
24
26
|
|
|
25
27
|
// =============================================================================
|
|
@@ -126,6 +128,24 @@ async function ensureGrepEngineAvailable(searcher, options, label) {
|
|
|
126
128
|
);
|
|
127
129
|
}
|
|
128
130
|
|
|
131
|
+
const AGENT_FORMATS = new Set(['agent', 'agent_preview', 'agent_full', 'agent_full_xl']);
|
|
132
|
+
|
|
133
|
+
function isAgentFormat(options) {
|
|
134
|
+
return options?._isAgentFormat === true || AGENT_FORMATS.has(options?.format);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function shapeBareGrepMatches(candidateResult, symbolType, searcher, fileFilter) {
|
|
138
|
+
let matches = [
|
|
139
|
+
...(candidateResult?.indexedMatches || []),
|
|
140
|
+
...(candidateResult?.overlayMatches || []),
|
|
141
|
+
];
|
|
142
|
+
matches = filterMatchesBySymbolType(matches, symbolType, searcher);
|
|
143
|
+
if (fileFilter) {
|
|
144
|
+
matches = matches.filter(match => matchesGrepFileFilter(match.file, fileFilter));
|
|
145
|
+
}
|
|
146
|
+
return matches;
|
|
147
|
+
}
|
|
148
|
+
|
|
129
149
|
// =============================================================================
|
|
130
150
|
// Bare grep (wired onto SweetSearch.prototype)
|
|
131
151
|
// =============================================================================
|
|
@@ -150,14 +170,24 @@ export async function bareGrep(query, routing, options = {}) {
|
|
|
150
170
|
await ensureGrepEngineAvailable(this, options, 'Bare grep');
|
|
151
171
|
|
|
152
172
|
// Disable chunk gram for bare grep — bare grep uses file:line matches, not chunk IDs.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
173
|
+
let candidateResult = await generateRegexMatches(this || {}, regex, searchDir, options);
|
|
174
|
+
const shapeResult = result => shapeBareGrepMatches(
|
|
175
|
+
result, symbolType, this, options.fileFilter,
|
|
176
|
+
);
|
|
177
|
+
const dialectRetry = await retryBreDialectAfterZero({
|
|
178
|
+
pattern: regex,
|
|
179
|
+
fixedString: options.fixedString,
|
|
180
|
+
agentFormat: isAgentFormat(options),
|
|
181
|
+
originalResult: candidateResult,
|
|
182
|
+
shapeResult,
|
|
183
|
+
retry: translatedPattern => generateRegexMatches(
|
|
184
|
+
this || {}, translatedPattern, searchDir, options,
|
|
185
|
+
),
|
|
186
|
+
});
|
|
187
|
+
candidateResult = dialectRetry.candidateResult;
|
|
188
|
+
// Symbol and --in filtering happen before retry adoption and before sort/cap,
|
|
189
|
+
// so hint counts always describe the matches this call can actually return.
|
|
190
|
+
let matches = dialectRetry.matches;
|
|
161
191
|
matches.sort((a, b) =>
|
|
162
192
|
a.file.localeCompare(b.file) ||
|
|
163
193
|
a.line - b.line ||
|
|
@@ -165,6 +195,7 @@ export async function bareGrep(query, routing, options = {}) {
|
|
|
165
195
|
);
|
|
166
196
|
|
|
167
197
|
const totalMatches = matches.length;
|
|
198
|
+
const regexDialectHint = dialectRetry.regexDialectHint;
|
|
168
199
|
// Agent-only k-budget file diversity (option-gated; absent → byte-identical
|
|
169
200
|
// output). Streaming per-file cap: matches beyond the cap are counted, not
|
|
170
201
|
// stored, so memory is bounded by perFileCap*maxFiles, never total matches.
|
|
@@ -183,10 +214,14 @@ export async function bareGrep(query, routing, options = {}) {
|
|
|
183
214
|
projectRoot: searchDir,
|
|
184
215
|
contextLines: options.contextLines ?? 0,
|
|
185
216
|
});
|
|
217
|
+
const familyManifest = options._isAgentFormat === true && !options.fileFilter
|
|
218
|
+
? buildIndexedGrepFamilyManifest(results, this?.codeGraphRepo)
|
|
219
|
+
: null;
|
|
186
220
|
|
|
187
221
|
return {
|
|
188
222
|
results,
|
|
189
223
|
...(fileSummary ? { fileSummary } : {}),
|
|
224
|
+
...(familyManifest ? { familyManifest } : {}),
|
|
190
225
|
stats: {
|
|
191
226
|
path: 'grep',
|
|
192
227
|
regex,
|
|
@@ -214,6 +249,7 @@ export async function bareGrep(query, routing, options = {}) {
|
|
|
214
249
|
plannerRoute: candidateResult.stats.plannerRoute,
|
|
215
250
|
gramSelectivity: candidateResult.stats.gramSelectivity,
|
|
216
251
|
nativeGrepUsed: candidateResult.stats.nativeGrepUsed,
|
|
252
|
+
...(regexDialectHint && { regexDialectHint }),
|
|
217
253
|
symbolType,
|
|
218
254
|
total_ms: Math.round(performance.now() - start),
|
|
219
255
|
stageTiming: candidateResult.stats.stageTiming || null,
|
|
@@ -276,7 +312,7 @@ export async function patternSearch(query, routing, options = {}) {
|
|
|
276
312
|
log(`Query: "${effectiveQuery}"`);
|
|
277
313
|
|
|
278
314
|
const parallelStart = performance.now();
|
|
279
|
-
|
|
315
|
+
let [candidateResult, encodedQuery] = await Promise.all([
|
|
280
316
|
generateRegexMatches(this, regex, searchDir, { ...options, lightweightParse: true }),
|
|
281
317
|
(async () => {
|
|
282
318
|
const encodeStart = performance.now();
|
|
@@ -288,11 +324,23 @@ export async function patternSearch(query, routing, options = {}) {
|
|
|
288
324
|
})(),
|
|
289
325
|
]);
|
|
290
326
|
const parallelTime = performance.now() - parallelStart;
|
|
327
|
+
const retryOptions = { ...options, lightweightParse: true };
|
|
328
|
+
const dialectRetry = await retryBreDialectAfterZero({
|
|
329
|
+
pattern: regex,
|
|
330
|
+
fixedString: options.fixedString,
|
|
331
|
+
agentFormat: isAgentFormat(options),
|
|
332
|
+
originalResult: candidateResult,
|
|
333
|
+
retry: translatedPattern => generateRegexMatches(
|
|
334
|
+
this, translatedPattern, searchDir, retryOptions,
|
|
335
|
+
),
|
|
336
|
+
});
|
|
337
|
+
candidateResult = dialectRetry.candidateResult;
|
|
291
338
|
const grepMatches = candidateResult.indexedMatches;
|
|
292
339
|
const overlayMatches = candidateResult.overlayMatches;
|
|
293
340
|
const queryTokens = encodedQuery.tokens;
|
|
294
341
|
const encodeTime = encodedQuery.encodeTime;
|
|
295
342
|
const totalRawMatches = grepMatches.length + overlayMatches.length;
|
|
343
|
+
const regexDialectHint = dialectRetry.regexDialectHint;
|
|
296
344
|
log(
|
|
297
345
|
`Parallel phase: ${totalRawMatches} grep matches ` +
|
|
298
346
|
`(${grepMatches.length} indexed, ${overlayMatches.length} overlay), ` +
|
|
@@ -328,13 +376,15 @@ export async function patternSearch(query, routing, options = {}) {
|
|
|
328
376
|
prefilterDiscardedCount: candidateResult.stats.prefilterDiscardedCount,
|
|
329
377
|
grepStrategy: candidateResult.stats.grepStrategy,
|
|
330
378
|
parallelTime_ms: Math.round(parallelTime),
|
|
379
|
+
...(regexDialectHint && { regexDialectHint }),
|
|
331
380
|
total_ms: Math.round(performance.now() - start),
|
|
332
381
|
};
|
|
333
382
|
|
|
334
383
|
// Agent mode: return proper agent schema even for zero results
|
|
335
384
|
if (format === 'agent' || format === 'agent_preview' || format === 'agent_full' || format === 'agent_full_xl') {
|
|
336
385
|
const agentResponse = packageForAgent([], emptyStats, {
|
|
337
|
-
query, regex, mode: 'pattern', format, tokenBudget, ablations,
|
|
386
|
+
query, regex, mode: 'pattern', format, tokenBudget, ablations,
|
|
387
|
+
projectRoot: this.projectRoot || PROJECT_ROOT, _isAgentFormat: true,
|
|
338
388
|
});
|
|
339
389
|
agentResponse.stats = emptyStats;
|
|
340
390
|
return agentResponse;
|
|
@@ -530,6 +580,7 @@ export async function patternSearch(query, routing, options = {}) {
|
|
|
530
580
|
grepStrategy: candidateResult.stats.grepStrategy,
|
|
531
581
|
plannerRoute: candidateResult.stats.plannerRoute,
|
|
532
582
|
trackerLastIndex: candidateResult.stats.trackerLastIndex,
|
|
583
|
+
...(regexDialectHint && { regexDialectHint }),
|
|
533
584
|
total_ms: Math.round(totalTime),
|
|
534
585
|
allCandidateIds,
|
|
535
586
|
allMappedChunkIds,
|
|
@@ -549,6 +600,7 @@ export async function patternSearch(query, routing, options = {}) {
|
|
|
549
600
|
locationMap,
|
|
550
601
|
projectRoot: searchDir,
|
|
551
602
|
ablations,
|
|
603
|
+
_isAgentFormat: true,
|
|
552
604
|
});
|
|
553
605
|
agentResponse.stats = stats;
|
|
554
606
|
return agentResponse;
|