sweet-search 2.6.15 → 2.6.17
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/core/graph/structural-context-format.js +5 -0
- package/core/graph/structural-context.js +13 -3
- package/core/infrastructure/language-patterns/registry-core.js +14 -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 +21 -11
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +5 -3
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +7 -2
- package/package.json +8 -8
- package/scripts/inject-agent-instructions.js +12 -8
|
@@ -4,6 +4,11 @@ export function formatStructuralContext(result) {
|
|
|
4
4
|
const lines = [];
|
|
5
5
|
lines.push(`# trace ${t.name} [${t.type}] ${t.filePath}:${t.startLine}-${t.endLine}`);
|
|
6
6
|
lines.push(`fan-in=${t.fanIn} fan-out=${t.fanOut} budget=${result.tokensUsed}/${result.tokenBudget} (${result.budgetTier}:${result.budgetReason}) latency=${result.stats.latencyMs}ms`);
|
|
7
|
+
if (t.fanIn === 0 && t.fanOut === 0 && !result.sections.callers.total && !result.sections.callees.total) {
|
|
8
|
+
lines.push(`no stored call edges for this symbol — map its sites with one broad ss-grep of the symbol stem instead.`);
|
|
9
|
+
} else if (t.fanIn === 0 && result.sections.callers.total > 0) {
|
|
10
|
+
lines.push(`note: callers below come from a same-file source scan (no stored cross-file edges).`);
|
|
11
|
+
}
|
|
7
12
|
if (result.disambiguation.length) {
|
|
8
13
|
lines.push(`ambiguous: using first match; alternatives: ${result.disambiguation.slice(0, 5).map(a => `${a.name} ${a.file}:${a.startLine}`).join(', ')}`);
|
|
9
14
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { DB_PATHS } from '../infrastructure/config/index.js';
|
|
9
9
|
import { StructuralContextRepository } from '../infrastructure/structural-context-repository.js';
|
|
10
|
+
import { isLikelyCodeEntity } from '../infrastructure/structural-context-utils.js';
|
|
10
11
|
import { buildAnswerCues } from './structural-answer-cues.js';
|
|
11
12
|
import { callsiteHints } from './structural-callsite-hints.js';
|
|
12
13
|
import { extractHeaderContext } from './structural-header-context.js';
|
|
@@ -249,7 +250,7 @@ function addHintImpactPaths(paths, seen, repo, target, hints, limit) {
|
|
|
249
250
|
for (const name of hints) {
|
|
250
251
|
if (paths.length >= limit) break;
|
|
251
252
|
const hint = repo.findEntityCandidates?.(name, { limit: 1 })?.[0];
|
|
252
|
-
if (!hint || hint.id === target.id) continue;
|
|
253
|
+
if (!hint || hint.id === target.id || !isLikelyCodeEntity(hint)) continue;
|
|
253
254
|
const id = `hint:${target.id}>${hint.id}`;
|
|
254
255
|
if (!seen.has(id)) {
|
|
255
256
|
paths.push({ id, direction: 'downstream', path: [target, hint], edgeTypes: ['handoff'], depth: 1 });
|
|
@@ -301,9 +302,18 @@ export class StructuralContextBuilder {
|
|
|
301
302
|
const targetSource = readFileRange(target.filePath, target.startLine, target.endLine);
|
|
302
303
|
const targetHeaderContext = extractHeaderContext(readFileRange, target.filePath);
|
|
303
304
|
const targetCallsiteHints = callsiteHints(targetSource, new Set([target.name]));
|
|
304
|
-
const
|
|
305
|
+
const storedCallers = [...this.repo.getCallers(target, { limit: 160 }), ...(this.repo.getAliasCallers?.(target, { limit: 80 }) || [])];
|
|
306
|
+
// Same-file callsite scan: recovers callers the extractor stored no edge
|
|
307
|
+
// for (bare local calls, out-of-line C++ methods). Deduped against stored
|
|
308
|
+
// callers by entity id — a stored edge may carry a different context_line
|
|
309
|
+
// for the same call (multi-line invocations), and a same-entity duplicate
|
|
310
|
+
// would double-pack the caller section.
|
|
311
|
+
const storedIds = new Set(storedCallers.map(x => x.id));
|
|
312
|
+
const sameFileCallers = (this.repo.getSameFileCallers?.(target, { limit: 24 }) || [])
|
|
313
|
+
.filter(x => !storedIds.has(x.id));
|
|
314
|
+
const callersRaw = [...storedCallers, ...sameFileCallers].map(x => ({ ...x, depth: 1 }));
|
|
305
315
|
let calleesRaw = this.repo.getCallees(target, { limit: 160 }).map(x => ({ ...x, depth: 1 }));
|
|
306
|
-
if (!calleesRaw.length) calleesRaw = targetCallsiteHints.map(name => this.repo.findEntityCandidates?.(name, { limit: 1 })?.[0]).filter(
|
|
316
|
+
if (!calleesRaw.length) calleesRaw = targetCallsiteHints.map(name => this.repo.findEntityCandidates?.(name, { limit: 1 })?.[0]).filter(isLikelyCodeEntity).map(x => ({ ...x, relationship: 'handoff', depth: 1 }));
|
|
307
317
|
const impactRaw = buildImpactPaths(this.repo, target, {
|
|
308
318
|
maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
309
319
|
limit: 120,
|
|
@@ -313,6 +313,20 @@ export const CORE_LANGUAGES = {
|
|
|
313
313
|
function: /^(?:[\w:*&<>\s]+)\s+(\w+)\s*\([^)]*\)\s*(?:const)?\s*(?:override)?\s*\{/,
|
|
314
314
|
typedef: /^(?:typedef|using)\s+.+\s+(\w+)/,
|
|
315
315
|
enum: /^enum\s+(?:class\s+)?(\w+)/,
|
|
316
|
+
// Out-of-line qualified member definitions — `Type Class::method(...)`
|
|
317
|
+
// — the dominant C++ idiom for .cpp files, previously invisible to the
|
|
318
|
+
// graph (botan's GeneralName::matches_dns / Name_Constraints::validate,
|
|
319
|
+
// E2 in the 2026-07-07 trace audit). Additive only: existing patterns
|
|
320
|
+
// above win first (one entity per line), so no current entity changes.
|
|
321
|
+
// Requires a return-type token before the qualified name (rules out
|
|
322
|
+
// statement-position calls `Foo::bar(x);`) and rejects `;` after `(`
|
|
323
|
+
// (rules out prototypes and single-line calls); tolerates params
|
|
324
|
+
// continuing on the next line and `{` on its own line (Allman style).
|
|
325
|
+
method: /^(?:template\s*<[^<>]*>\s*)?(?!(?:return|if|while|for|switch|case|throw|delete|new|else|do|goto|using|typedef|catch)\b)[\w:*&<>~,\[\]\s]*?[\w>&*]\s+[*&]?(?:\w+(?:<[^<>]*>)?::)+(~?\w+)\s*\([^;]*(?:\)\s*(?:const)?\s*(?:noexcept(?:\([^()]*\))?)?\s*(?:override|final)?\s*(?:\{.*)?)?$/,
|
|
326
|
+
// Out-of-line constructors/destructors: `Class::Class(...)` /
|
|
327
|
+
// `Class::~Class(...)` — the backreference makes the class≡member
|
|
328
|
+
// equality the discriminator, so `std::sort(...)` never matches.
|
|
329
|
+
constructor: /^(?:template\s*<[^<>]*>\s*)?(?:\w+(?:<[^<>]*>)?::)*(\w+)(?:<[^<>]*>)?::~?\1\s*\([^;]*(?:\)\s*(?:noexcept(?:\([^()]*\))?)?\s*(?::\s*[\w({].*)?(?:\{.*)?)?$/,
|
|
316
330
|
},
|
|
317
331
|
relationships: {
|
|
318
332
|
include: /^#include\s+[<"]([^>"]+)[>"]/,
|
|
@@ -6,10 +6,10 @@ import { applyReadPragmas } from './db-utils.js';
|
|
|
6
6
|
import { findAliasCallers } from './structural-alias-resolver.js';
|
|
7
7
|
import { rankStructuralCandidates } from './structural-candidate-ranker.js';
|
|
8
8
|
import { findAssignedMemberDefinitions, findSameFileDefinition } from './structural-source-definitions.js';
|
|
9
|
-
import { shouldTrustQualifiedResolution } from './structural-qualified-resolution.js';
|
|
9
|
+
import { shouldTrustQualifiedResolution, trustedCallerEdge } from './structural-qualified-resolution.js';
|
|
10
10
|
import { fetchPageRank, fetchFrontierBackwardEdges, fetchFrontierForwardEdges } from './structural-graph-signals.js';
|
|
11
11
|
import { CodeGraphReaderVisibility } from './code-graph-visibility.js';
|
|
12
|
-
import { callTargetAliases, clampLimit, isTestPath, lowerCamel, placeholders, qualifiedTargetName, rowToEntity } from './structural-context-utils.js';
|
|
12
|
+
import { callTargetAliases, clampLimit, isLikelyCodeEntity, isTestPath, lowerCamel, placeholders, qualifiedTargetName, rowToEntity } from './structural-context-utils.js';
|
|
13
13
|
|
|
14
14
|
export class StructuralContextRepository {
|
|
15
15
|
constructor(dbPath, opts = {}) {
|
|
@@ -78,10 +78,13 @@ export class StructuralContextRepository {
|
|
|
78
78
|
ORDER BY
|
|
79
79
|
CASE WHEN file_path LIKE '%/test/%' OR file_path LIKE 'test/%' OR file_path LIKE 'tests/%' THEN 1 ELSE 0 END,
|
|
80
80
|
length(name),
|
|
81
|
+
CASE WHEN end_line - start_line = 0 THEN 1 ELSE 0 END,
|
|
81
82
|
(end_line - start_line) ASC
|
|
82
|
-
LIMIT
|
|
83
|
+
LIMIT 8
|
|
83
84
|
`).all(...entityParams, ...names);
|
|
84
|
-
|
|
85
|
+
const entity = rows.map(row => this._entityFromRow(row)).find(isLikelyCodeEntity) || null;
|
|
86
|
+
if (!entity) return null;
|
|
87
|
+
return shouldTrustQualifiedResolution(targetName, entity) ? entity : null;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
_resolveQualifiedAlternative(targetName, excludeId) {
|
|
@@ -100,6 +103,7 @@ export class StructuralContextRepository {
|
|
|
100
103
|
ORDER BY
|
|
101
104
|
CASE WHEN file_path LIKE '%/test/%' OR file_path LIKE 'test/%' OR file_path LIKE 'tests/%' THEN 1 ELSE 0 END,
|
|
102
105
|
CASE type WHEN 'method' THEN 0 WHEN 'function' THEN 1 ELSE 2 END,
|
|
106
|
+
CASE WHEN end_line - start_line = 0 THEN 1 ELSE 0 END,
|
|
103
107
|
(end_line - start_line) ASC
|
|
104
108
|
LIMIT 8
|
|
105
109
|
`).all(...entityParams, name, excludeId);
|
|
@@ -165,6 +169,7 @@ export class StructuralContextRepository {
|
|
|
165
169
|
WHEN 'function' THEN 2 WHEN 'method' THEN 2
|
|
166
170
|
ELSE 3
|
|
167
171
|
END,
|
|
172
|
+
CASE WHEN end_line - start_line = 0 THEN 1 ELSE 0 END,
|
|
168
173
|
(end_line - start_line) ASC
|
|
169
174
|
LIMIT ?
|
|
170
175
|
`).all(...entityParams, ...params, raw, raw, limit);
|
|
@@ -215,7 +220,7 @@ export class StructuralContextRepository {
|
|
|
215
220
|
SELECT DISTINCT
|
|
216
221
|
e.id, e.name, e.type, e.file_path, e.start_line, e.end_line,
|
|
217
222
|
e.signature, e.summary, e.parent_class, e.package,
|
|
218
|
-
r.context_line, r.target_name, r.weight, r.type as rel_type
|
|
223
|
+
r.target_id, r.context_line, r.target_name, r.weight, r.type as rel_type
|
|
219
224
|
FROM relationships r
|
|
220
225
|
JOIN entities e ON e.id = r.source_id
|
|
221
226
|
WHERE r.type IN (${placeholders(types)})
|
|
@@ -237,9 +242,66 @@ export class StructuralContextRepository {
|
|
|
237
242
|
...this._entityFromRow(row),
|
|
238
243
|
relationship: row.rel_type,
|
|
239
244
|
contextLine: row.context_line || null,
|
|
245
|
+
targetId: row.target_id || null,
|
|
240
246
|
targetName: row.target_name || null,
|
|
241
247
|
weight: row.weight ?? 1,
|
|
242
|
-
}));
|
|
248
|
+
})).filter(edge => trustedCallerEdge(edge, target));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Query-time fallback for callers the extractor stored no edge for (bare
|
|
253
|
+
* local calls in JS/TS, out-of-line C++ methods, …): scan the target's own
|
|
254
|
+
* file for `name(` call sites outside the target's span and attribute each
|
|
255
|
+
* to its innermost enclosing entity. Cheap (one cached file read) and
|
|
256
|
+
* language-agnostic.
|
|
257
|
+
*/
|
|
258
|
+
getSameFileCallers(target, opts = {}) {
|
|
259
|
+
const db = this._open();
|
|
260
|
+
if (!db || !target?.id || !target?.filePath || !target?.name || !target?.startLine) return [];
|
|
261
|
+
const limit = clampLimit(opts.limit, 24, 60);
|
|
262
|
+
const source = this.readFileRange(target.filePath, 1, 1000000);
|
|
263
|
+
if (!source) return [];
|
|
264
|
+
const escaped = String(target.name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
265
|
+
const callRe = new RegExp(`(?<![.\\w$:])${escaped}\\s*\\(`);
|
|
266
|
+
const defRe = new RegExp(`\\b(function|def|fn|func|sub|proc)\\s+${escaped}\\s*[(<]`);
|
|
267
|
+
const lines = source.split('\n');
|
|
268
|
+
const hits = [];
|
|
269
|
+
for (let i = 0; i < lines.length && hits.length < limit * 2; i++) {
|
|
270
|
+
const ln = i + 1;
|
|
271
|
+
if (ln >= target.startLine && ln <= (target.endLine || target.startLine)) continue;
|
|
272
|
+
const text = lines[i].replace(/(^|\s)(\/\/|#).*$/, '');
|
|
273
|
+
if (callRe.test(text) && !defRe.test(text)) hits.push(ln);
|
|
274
|
+
}
|
|
275
|
+
if (!hits.length) return [];
|
|
276
|
+
const entitySql = this._entitySql(db);
|
|
277
|
+
const fileEntities = db.prepare(`
|
|
278
|
+
SELECT id, name, type, file_path, start_line, end_line, signature,
|
|
279
|
+
summary, parent_class, package
|
|
280
|
+
FROM entities
|
|
281
|
+
WHERE ${entitySql} AND file_path = ? AND start_line IS NOT NULL AND end_line IS NOT NULL
|
|
282
|
+
ORDER BY start_line
|
|
283
|
+
`).all(...this._entityParams(db), target.filePath);
|
|
284
|
+
const out = [];
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
for (const ln of hits) {
|
|
287
|
+
const host = fileEntities
|
|
288
|
+
.filter(r => r.start_line <= ln && r.end_line >= ln)
|
|
289
|
+
.sort((a, b) => (a.end_line - a.start_line) - (b.end_line - b.start_line))[0];
|
|
290
|
+
if (!host || host.id === target.id || host.name === target.name) continue;
|
|
291
|
+
const key = `${host.id}:${ln}`;
|
|
292
|
+
if (seen.has(key)) continue;
|
|
293
|
+
seen.add(key);
|
|
294
|
+
out.push({
|
|
295
|
+
...this._entityFromRow(host),
|
|
296
|
+
relationship: 'calls',
|
|
297
|
+
contextLine: ln,
|
|
298
|
+
targetId: target.id,
|
|
299
|
+
targetName: target.name,
|
|
300
|
+
weight: 1,
|
|
301
|
+
});
|
|
302
|
+
if (out.length >= limit) break;
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
243
305
|
}
|
|
244
306
|
|
|
245
307
|
getAliasCallers(target, opts = {}) {
|
|
@@ -326,6 +388,10 @@ export class StructuralContextRepository {
|
|
|
326
388
|
LIMIT ?
|
|
327
389
|
`).all(...this._entityParams(db), ...this._relationshipParams(db), ...types, ...ids, ...nameParams, limit);
|
|
328
390
|
|
|
391
|
+
// Rows admitted by the name-pattern clause (not by target_id) are subject
|
|
392
|
+
// to the same receiver-compat gate as getCallers — otherwise the phantom
|
|
393
|
+
// `this.fetch`-style edges re-enter through impact paths.
|
|
394
|
+
const idSet = new Set(ids);
|
|
329
395
|
return rows.map(row => ({
|
|
330
396
|
...this._entityFromRow(row),
|
|
331
397
|
relationship: row.rel_type,
|
|
@@ -333,7 +399,7 @@ export class StructuralContextRepository {
|
|
|
333
399
|
targetName: row.target_name || null,
|
|
334
400
|
contextLine: row.context_line || null,
|
|
335
401
|
weight: row.weight ?? 1,
|
|
336
|
-
}));
|
|
402
|
+
})).filter(edge => (edge.targetId && idSet.has(edge.targetId)) || trustedCallerEdge(edge, target));
|
|
337
403
|
}
|
|
338
404
|
|
|
339
405
|
getForwardDependencies(frontierIds, opts = {}) {
|
|
@@ -49,3 +49,14 @@ export function isTestPath(filePath = '') {
|
|
|
49
49
|
export function placeholders(values) {
|
|
50
50
|
return values.map(() => '?').join(',');
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
// Entities extracted from config/doc files (TOML keys, YAML fields, …) are
|
|
54
|
+
// valid search hits but must never be presented as call-graph neighbours —
|
|
55
|
+
// a `calls` edge into stylua.toml is noise, not structure.
|
|
56
|
+
const NON_CODE_FILE_RE = /\.(toml|ya?ml|json|json5|ini|cfg|conf|properties|lock|md|markdown|rst|txt|xml|html?|css|scss|svg)$/i;
|
|
57
|
+
|
|
58
|
+
export function isLikelyCodeEntity(entity) {
|
|
59
|
+
if (!entity) return false;
|
|
60
|
+
if (entity.filePath && NON_CODE_FILE_RE.test(entity.filePath)) return false;
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
@@ -13,3 +13,32 @@ export function shouldTrustQualifiedResolution(targetName, entity) {
|
|
|
13
13
|
const hay = `${entity.filePath || ''} ${entity.parentClass || ''} ${entity.package || ''} ${entity.signature || ''} ${entity.summary || ''}`.toLowerCase();
|
|
14
14
|
return qualifierTerms(qualifier).some(term => hay.includes(term));
|
|
15
15
|
}
|
|
16
|
+
|
|
17
|
+
// Receivers that mean "the enclosing object", so a qualified edge like
|
|
18
|
+
// `this.fetch` can only belong to a target in the SAME class or file as the
|
|
19
|
+
// calling entity — never to an unrelated plain function that shares the name.
|
|
20
|
+
const SELF_QUALIFIERS = new Set(['this', 'self', 'super', 'cls', 'me']);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Caller-side twin of shouldTrustQualifiedResolution: decide whether a stored
|
|
24
|
+
* call edge (matched to `target` by name pattern) plausibly refers to that
|
|
25
|
+
* target. `edge` carries the CALLING entity's fields (filePath, parentClass)
|
|
26
|
+
* plus targetId/targetName from the relationship row.
|
|
27
|
+
*/
|
|
28
|
+
export function trustedCallerEdge(edge, target) {
|
|
29
|
+
const tn = String(edge?.targetName || '').trim();
|
|
30
|
+
if (!tn || !target?.name) return true;
|
|
31
|
+
if (edge.targetId && edge.targetId === target.id) return true;
|
|
32
|
+
const parts = tn.replace(/::/g, '.').split('.').filter(Boolean);
|
|
33
|
+
if (parts.length < 2) return true; // bare-name edge: exact match already
|
|
34
|
+
const qualifier = parts[parts.length - 2].toLowerCase();
|
|
35
|
+
if (SELF_QUALIFIERS.has(qualifier)) {
|
|
36
|
+
if (edge.filePath && target.filePath && edge.filePath === target.filePath) return true;
|
|
37
|
+
return !!(edge.parentClass && target.parentClass && edge.parentClass === target.parentClass);
|
|
38
|
+
}
|
|
39
|
+
const targetNames = [target.name, target.parentClass]
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.map(s => String(s).toLowerCase());
|
|
42
|
+
if (targetNames.includes(qualifier)) return true;
|
|
43
|
+
return shouldTrustQualifiedResolution(tn, target);
|
|
44
|
+
}
|
|
@@ -544,19 +544,60 @@ const TAGS_QUERIES = {
|
|
|
544
544
|
(protocol_function_declaration name: (simple_identifier) @method.definition)
|
|
545
545
|
(init_declaration) @method.definition
|
|
546
546
|
`,
|
|
547
|
-
// C/C++: function name nested inside declarator chain
|
|
547
|
+
// C/C++: function name nested inside declarator chain. Captures are on the
|
|
548
|
+
// WHOLE function_definition node (not the identifier leaf) so entity spans
|
|
549
|
+
// cover the body — leaf captures gave start_line == end_line, which starved
|
|
550
|
+
// ss-trace targets of code. Names resolve via _cFunctionDefinitionName.
|
|
551
|
+
// Pointer-returning definitions (`char *foo(...)`) wrap the
|
|
552
|
+
// function_declarator in a pointer_declarator — captured separately.
|
|
548
553
|
c: `
|
|
549
554
|
(function_definition
|
|
550
555
|
declarator: (function_declarator
|
|
551
|
-
declarator: (identifier) @function.definition
|
|
556
|
+
declarator: (identifier))) @function.definition
|
|
557
|
+
(function_definition
|
|
558
|
+
declarator: (pointer_declarator
|
|
559
|
+
declarator: (function_declarator
|
|
560
|
+
declarator: (identifier)))) @function.definition
|
|
552
561
|
(struct_specifier name: (type_identifier) @struct.definition)
|
|
553
562
|
(enum_specifier name: (type_identifier) @enum.definition)
|
|
554
563
|
(type_definition declarator: (type_identifier) @type.definition)
|
|
555
564
|
`,
|
|
565
|
+
// C++ additionally has out-of-line qualified members
|
|
566
|
+
// (`Type Class::method(...)` — qualified_identifier), in-class definitions
|
|
567
|
+
// (field_identifier), and destructors — ALL previously invisible to the
|
|
568
|
+
// graph (E2, 2026-07-08 trace audit: botan's GeneralName::matches_dns and
|
|
569
|
+
// Name_Constraints::validate were untraceable). Patterns are mutually
|
|
570
|
+
// exclusive by declarator shape, so the startIndex:type dedupe never sees
|
|
571
|
+
// the same definition twice.
|
|
556
572
|
cpp: `
|
|
557
573
|
(function_definition
|
|
558
574
|
declarator: (function_declarator
|
|
559
|
-
declarator: (identifier) @function.definition
|
|
575
|
+
declarator: (identifier))) @function.definition
|
|
576
|
+
(function_definition
|
|
577
|
+
declarator: (function_declarator
|
|
578
|
+
declarator: (qualified_identifier))) @method.definition
|
|
579
|
+
(function_definition
|
|
580
|
+
declarator: (function_declarator
|
|
581
|
+
declarator: (field_identifier))) @method.definition
|
|
582
|
+
(function_definition
|
|
583
|
+
declarator: (function_declarator
|
|
584
|
+
declarator: (destructor_name))) @method.definition
|
|
585
|
+
(function_definition
|
|
586
|
+
declarator: (pointer_declarator
|
|
587
|
+
declarator: (function_declarator
|
|
588
|
+
declarator: (identifier)))) @function.definition
|
|
589
|
+
(function_definition
|
|
590
|
+
declarator: (pointer_declarator
|
|
591
|
+
declarator: (function_declarator
|
|
592
|
+
declarator: (qualified_identifier)))) @method.definition
|
|
593
|
+
(function_definition
|
|
594
|
+
declarator: (reference_declarator
|
|
595
|
+
(function_declarator
|
|
596
|
+
declarator: (identifier)))) @function.definition
|
|
597
|
+
(function_definition
|
|
598
|
+
declarator: (reference_declarator
|
|
599
|
+
(function_declarator
|
|
600
|
+
declarator: (qualified_identifier)))) @method.definition
|
|
560
601
|
(class_specifier name: (type_identifier) @class.definition)
|
|
561
602
|
(struct_specifier name: (type_identifier) @struct.definition)
|
|
562
603
|
(enum_specifier name: (type_identifier) @enum.definition)
|
|
@@ -891,6 +932,7 @@ export class TreeSitterProvider {
|
|
|
891
932
|
const symbolName = isLeafIdent
|
|
892
933
|
? node.text
|
|
893
934
|
: (node.childForFieldName?.('name')?.text
|
|
935
|
+
|| (C_FAMILY_LANGUAGES.has(languageId) ? this._cFunctionDefinitionName(node) : null)
|
|
894
936
|
|| this._extractNodeName(node)
|
|
895
937
|
|| `<anonymous:${entityType}>`);
|
|
896
938
|
|
|
@@ -1428,6 +1470,38 @@ export class TreeSitterProvider {
|
|
|
1428
1470
|
return text.startsWith('///') || text.startsWith('/**');
|
|
1429
1471
|
}
|
|
1430
1472
|
|
|
1473
|
+
/**
|
|
1474
|
+
* Resolve the leaf name of a C/C++ function_definition by walking its
|
|
1475
|
+
* declarator chain: pointer/reference/parenthesized wrappers →
|
|
1476
|
+
* function_declarator → identifier / field_identifier / destructor_name /
|
|
1477
|
+
* operator_name / qualified_identifier (drilled to its leaf, so
|
|
1478
|
+
* `ns::Class::method` yields `method`). Used ONLY by extractSymbols (graph
|
|
1479
|
+
* entities) — chunker naming goes through _extractNodeName and is
|
|
1480
|
+
* deliberately untouched so NL retrieval inputs stay byte-identical.
|
|
1481
|
+
*/
|
|
1482
|
+
_cFunctionDefinitionName(node) {
|
|
1483
|
+
if (!node || node.type !== 'function_definition') return null;
|
|
1484
|
+
let d = node.childForFieldName?.('declarator');
|
|
1485
|
+
for (let hops = 0; d && hops < 6; hops++) {
|
|
1486
|
+
if (d.type === 'function_declarator') { d = d.childForFieldName?.('declarator'); break; }
|
|
1487
|
+
const inner = d.childForFieldName?.('declarator')
|
|
1488
|
+
|| d.namedChildren?.find?.(c => /declarator/.test(c.type));
|
|
1489
|
+
if (!inner) break;
|
|
1490
|
+
d = inner;
|
|
1491
|
+
}
|
|
1492
|
+
for (let hops = 0; d && hops < 6; hops++) {
|
|
1493
|
+
if (d.type === 'qualified_identifier') {
|
|
1494
|
+
d = d.childForFieldName?.('name') || d.namedChildren?.[d.namedChildCount - 1];
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
if (/^(identifier|field_identifier|destructor_name|operator_name)$/.test(d.type)) {
|
|
1498
|
+
return d.text || null;
|
|
1499
|
+
}
|
|
1500
|
+
break;
|
|
1501
|
+
}
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1431
1505
|
/** Extract symbol name from an AST node */
|
|
1432
1506
|
_extractNodeName(node) {
|
|
1433
1507
|
// Try field name first (most reliable)
|
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
variant: mcp
|
|
3
|
-
derived_from: p7-v1-
|
|
4
|
-
source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/
|
|
3
|
+
derived_from: p7-v1-mppppp-fs
|
|
4
|
+
source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/Mppppp.md
|
|
5
5
|
benchmarked: false
|
|
6
6
|
note: >-
|
|
7
|
-
Hand-derived MCP-tool variant of the M
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
Hand-derived MCP-tool variant of the M+++++ champion (p7-v1-mppppp-fs =
|
|
8
|
+
M++++ + the verdict-gated trust line: trust rank 1 outright when the
|
|
9
|
+
response reports sufficiency yes, otherwise scan the already-delivered
|
|
10
|
+
lower ranks/same-file map before any new search; smoke-validated on the
|
|
11
|
+
task-completion bench, see memory
|
|
12
|
+
project_mppppp_conditional_trust_candidate; + the P2 fix-surface paragraph:
|
|
13
|
+
before editing a symbol with visible siblings spend ONE mapping call —
|
|
14
|
+
trace, or one broad search regex on the stem — and read the edited
|
|
15
|
+
function to its end, single-site edits skip it; ported here against the
|
|
16
|
+
MCP tool surface, smoke codex-fixsurface-smoke10). The STRATEGY core — routing by
|
|
17
|
+
what-you-hold, verdict-gated trust, sufficiency stops, the two-probe absence
|
|
18
|
+
rule, the <state_summary> gate, and the search-output discipline — is
|
|
19
|
+
preserved (semantics intact; the rest carries only the tool-mechanics
|
|
20
|
+
rename). The trust gate is phrased against the MCP response surface
|
|
21
|
+
(sufficiency verdict in structured content; "Also found" summary list;
|
|
22
|
+
"Same file:" map line) instead of the CLI sufficient= trailer. Only the tool-mechanics layer is remapped: the six ss-* CLI commands collapse onto the sweet-search MCP
|
|
15
23
|
tool surface (a UNIFIED `search` that subsumes ss-search/ss-find/ss-grep, plus
|
|
16
24
|
`trace` / `read-semantic` / `read` / `repo-map`). The anti-raw-scan discipline
|
|
17
25
|
is KEPT and retargeted from "ss-* vs raw grep" to "sweet-search tools vs native
|
|
@@ -40,7 +48,7 @@ The sweet-search MCP server exposes these (call each by the name your client lis
|
|
|
40
48
|
- **Only a behavior or concept**: one `search` in natural language for what you're looking for, then anchor on the symbol that surfaces. Shape it lightly by the target language — short and interrogative for JS/TS/Dart, a touch longer with a domain keyword otherwise.
|
|
41
49
|
- **How something flows / dispatches / is called / what a change impacts**: anchor one symbol (a literal, or a `search`), then `trace` it — one call returns callers, callees and impact. Prefer callees over impact (especially Python/Ruby/PHP). If a trace is sparse or empty, anchor the downstream symbol with `search` rather than retrying or hand-crawling; never make `trace` the spine of a multi-file search.
|
|
42
50
|
|
|
43
|
-
|
|
51
|
+
When the response reports sufficiency `yes`, trust the top ranked result outright; confirm with at most one narrow `read`, never a re-run of a matching hit. Otherwise, scan the rest of the results you already have (lower ranks, the `Also found` list, the `Same file:` line) before issuing any new search — the winner is often rank 2-3.
|
|
44
52
|
|
|
45
53
|
## Multi-file
|
|
46
54
|
Chain inside the tools: land the entry file, `read-semantic` it for the import or handoff symbol, then `search` the downstream module. The trace is COMPLETE the moment you can name the link from the entry symbol to the thing it reaches; stop tracing there. Leaf bodies, macro expansions, and the next hop down are not the answer unless asked, and chasing them — or dropping to a native Grep/Read to "just look" — is the main multi-file cost trap.
|
|
@@ -53,3 +61,5 @@ Before your third sweet-search probe in the current search iteration — or befo
|
|
|
53
61
|
|
|
54
62
|
## Search output
|
|
55
63
|
Stop searching the instant your evidence answers what you're looking for — one confirmed file+symbol, or one named cross-file link, is enough; gather no corroboration you were not asked for. Name the file(s) and symbol(s) and how they answer what you need, or `no-match` — then finish whatever the task/prompt asks for.
|
|
64
|
+
|
|
65
|
+
Before editing a symbol with visible siblings — multiple call sites, a name family (`IVec2`/`I64Vec2`), branch variants, a generated family — spend ONE mapping call: `trace` the symbol, or one broad `search` regex on the stem. Read the function you edit to its end; a fix covering only the first matching site is not done. Single-site edits skip this.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
run_id: p7-v1-
|
|
2
|
+
run_id: p7-v1-mppppp-fs
|
|
3
3
|
score_sonnet: 0.993
|
|
4
4
|
score_gpt5_5: 0.988
|
|
5
5
|
joint_maximin: 0.988
|
|
6
6
|
eas_factor: null
|
|
7
7
|
length_penalty: null
|
|
8
8
|
final_score: null
|
|
9
|
-
token_count:
|
|
9
|
+
token_count: 1307
|
|
10
10
|
scs_cwSCS: 0.931
|
|
11
11
|
scs_min_paraphrase_accuracy: 1
|
|
12
12
|
homp_family_pass: true
|
|
@@ -37,7 +37,7 @@ Sweet-search indexes the working tree (uncommitted edits too) and searches it fa
|
|
|
37
37
|
- **Only a behavior or concept**: one `ss-search` in natural language for what you're looking for, then anchor on the symbol that surfaces. Shape it lightly by the target language — short and interrogative for JS/TS/Dart, a touch longer with a domain keyword otherwise.
|
|
38
38
|
- **How something flows / dispatches / is called / what a change impacts**: anchor one symbol (a literal, or `ss-search`), then `ss-trace` it — one call returns callers, callees and impact. Prefer callees over impact (especially Python/Ruby/PHP). If a trace is sparse or empty, anchor the downstream symbol with `ss-find`/`ss-search` rather than retrying or hand-crawling; never make `ss-trace` the spine of a multi-file search.
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
On `sufficient=YES`, trust the top ranked result outright; confirm with at most one narrow `ss-read`, never a re-run of a matching hit. Otherwise, scan the rest of the pack you already have (lower ranks, the `same file:` line) before issuing any new search — the winner is often rank 2-3.
|
|
41
41
|
|
|
42
42
|
## Multi-file
|
|
43
43
|
Chain inside the tools: land the entry file, `ss-semantic` it for the import or handoff symbol, then `ss-search`/`ss-find` the downstream module. The trace is COMPLETE the moment you can name the link from the entry symbol to the thing it reaches; stop tracing there. Leaf bodies, macro expansions, and the next hop down are not the answer unless asked, and chasing them — or dropping to raw `cat`/`grep` to "just look" — is the main multi-file cost trap.
|
|
@@ -50,3 +50,5 @@ Before your third sweet-search probe in the current search iteration — or befo
|
|
|
50
50
|
|
|
51
51
|
## Search output
|
|
52
52
|
Stop searching the instant your evidence answers what you're looking for — one confirmed file+symbol, or one named cross-file link, is enough; gather no corroboration you were not asked for. Name the file(s) and symbol(s) and how they answer what you need, or `no-match` — then finish whatever the task/prompt asks for.
|
|
53
|
+
|
|
54
|
+
Before editing a symbol with visible siblings — multiple call sites, a name family (`IVec2`/`I64Vec2`), branch variants, a generated family — spend ONE mapping call: `ss-trace <symbol>`, or a broad `ss-grep` of the stem. Read the function you edit to its end; a fix covering only the first matching site is not done. Single-site edits skip this.
|
|
@@ -166,9 +166,14 @@ async function cmdGrep(rawArgs) {
|
|
|
166
166
|
|| { files: [], hiddenFileCount: 0, hiddenMatchCount: 0, hiddenSample: [] };
|
|
167
167
|
const body = renderGrepBody(result.results, fileSummary, k);
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
// Sibling-surface signal (E6, 2026-07-08 trace audit): when a symbol/stem
|
|
170
|
+
// matches in more than one file, say so unconditionally in the header —
|
|
171
|
+
// the file count is the objective "visible siblings" trigger for
|
|
172
|
+
// fix-surface mapping, and it must not depend on truncation having occurred.
|
|
173
|
+
const across = body.matchedFileCount > 1 ? ` across ${body.matchedFileCount} files` : '';
|
|
174
|
+
process.stdout.write(`# ss-grep: ${total} total match(es) for /${regex}/${across}\n`);
|
|
170
175
|
if (body.truncatedFileCount > 0 || body.hiddenLine) {
|
|
171
|
-
process.stdout.write(`#
|
|
176
|
+
process.stdout.write(`# (+N more in this file)=truncated — ` +
|
|
172
177
|
`see the rest: ss-grep "<regex>" --in <file>\n`);
|
|
173
178
|
}
|
|
174
179
|
for (const line of body.lines) process.stdout.write(line + '\n');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sweet-search",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.17",
|
|
4
4
|
"description": "Sweet Search - SOTA Hybrid Code Search Engine with WASM CatBoost Query Router, Semantic/Lexical/Structural Search, and Multilingual Support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "core/search/sweet-search.js",
|
|
@@ -167,13 +167,13 @@
|
|
|
167
167
|
"vitest": "^4.0.16"
|
|
168
168
|
},
|
|
169
169
|
"optionalDependencies": {
|
|
170
|
-
"@sweet-search/native-darwin-arm64": "2.6.
|
|
171
|
-
"@sweet-search/native-darwin-x64": "2.6.
|
|
172
|
-
"@sweet-search/native-linux-arm64-gnu": "2.6.
|
|
173
|
-
"@sweet-search/native-linux-arm64-gnu-cuda": "2.6.
|
|
174
|
-
"@sweet-search/native-linux-x64-gnu": "2.6.
|
|
175
|
-
"@sweet-search/native-linux-x64-gnu-cuda": "2.6.
|
|
176
|
-
"@sweet-search/bg-priority": "2.6.
|
|
170
|
+
"@sweet-search/native-darwin-arm64": "2.6.17",
|
|
171
|
+
"@sweet-search/native-darwin-x64": "2.6.17",
|
|
172
|
+
"@sweet-search/native-linux-arm64-gnu": "2.6.17",
|
|
173
|
+
"@sweet-search/native-linux-arm64-gnu-cuda": "2.6.17",
|
|
174
|
+
"@sweet-search/native-linux-x64-gnu": "2.6.17",
|
|
175
|
+
"@sweet-search/native-linux-x64-gnu-cuda": "2.6.17",
|
|
176
|
+
"@sweet-search/bg-priority": "2.6.17"
|
|
177
177
|
},
|
|
178
178
|
"engines": {
|
|
179
179
|
"node": ">=18.0.0"
|
|
@@ -46,18 +46,22 @@ function escapeRegex(s) {
|
|
|
46
46
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// ─── Canonical policy body = the M
|
|
49
|
+
// ─── Canonical policy body = the M+++++ champion (integration seam) ──────────
|
|
50
50
|
//
|
|
51
51
|
// Plan §10 / §3.7.1 step 13 / DDD "Integration seam": scripts/init.js is the
|
|
52
52
|
// SOLE consumer of the prompt-optimization ship-file. We read that artifact at
|
|
53
|
-
// load time and strip its YAML front-matter; the remaining body is the M
|
|
53
|
+
// load time and strip its YAML front-matter; the remaining body is the M+++++
|
|
54
54
|
// champion (PHASE7 M++ — held-out 0.988 Maximin, OOD 0.952, HOMP/SCS/counter
|
|
55
55
|
// all pass, 5-cell cross-harness validated — plus the stop-discipline scoping
|
|
56
|
-
// edits that fix M++'s over-stopping on FIX tasks
|
|
57
|
-
//
|
|
58
|
-
//
|
|
56
|
+
// edits that fix M++'s over-stopping on FIX tasks, plus the verdict-gated
|
|
57
|
+
// trust line that scans already-delivered lower ranks before any new search,
|
|
58
|
+
// plus the P2 fix-surface paragraph that maps a multi-site edit surface in
|
|
59
|
+
// ONE call before the first edit; tool routing unchanged), injected VERBATIM
|
|
60
|
+
// into the harness files.
|
|
61
|
+
// Per-harness shims (Cursor frontmatter, @imports) are applied at write-time,
|
|
62
|
+
// not embedded here.
|
|
59
63
|
//
|
|
60
|
-
// The artifact is generated from
|
|
64
|
+
// The artifact is generated from Mppppp.md by
|
|
61
65
|
// `core/prompt-optimization/sweep/finalize-mpp.mjs` and shipped via the
|
|
62
66
|
// package.json "files" list. If it is missing we fail LOUDLY rather than
|
|
63
67
|
// silently shipping a placeholder/older policy.
|
|
@@ -76,7 +80,7 @@ export function stripFrontMatter(text) {
|
|
|
76
80
|
return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
function readShippedPolicy(rel = SHIP_FILE_REL, { label = 'M
|
|
83
|
+
function readShippedPolicy(rel = SHIP_FILE_REL, { label = 'M+++++' } = {}) {
|
|
80
84
|
const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/scripts
|
|
81
85
|
const shipPath = join(here, '..', rel);
|
|
82
86
|
let raw;
|
|
@@ -103,7 +107,7 @@ let _mcpPolicyBody = null;
|
|
|
103
107
|
/** Lazily read + cache the MCP-variant policy body. */
|
|
104
108
|
export function getMcpPolicyBody() {
|
|
105
109
|
if (_mcpPolicyBody == null) {
|
|
106
|
-
_mcpPolicyBody = readShippedPolicy(MCP_SHIP_FILE_REL, { label: 'M
|
|
110
|
+
_mcpPolicyBody = readShippedPolicy(MCP_SHIP_FILE_REL, { label: 'M+++++ (MCP variant)' });
|
|
107
111
|
}
|
|
108
112
|
return _mcpPolicyBody;
|
|
109
113
|
}
|