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.
Files changed (33) hide show
  1. package/README.md +7 -3
  2. package/core/graph/structural-context-format.js +5 -0
  3. package/core/graph/structural-context.js +13 -3
  4. package/core/infrastructure/code-graph-repository.js +58 -0
  5. package/core/infrastructure/language-patterns/registry-core.js +14 -0
  6. package/core/infrastructure/model-fetcher.js +7 -0
  7. package/core/infrastructure/structural-context-repository.js +73 -7
  8. package/core/infrastructure/structural-context-utils.js +11 -0
  9. package/core/infrastructure/structural-qualified-resolution.js +29 -0
  10. package/core/infrastructure/tree-sitter-provider.js +77 -3
  11. package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +12 -6
  12. package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +4 -2
  13. package/core/search/agent-pack-completion.js +493 -0
  14. package/core/search/agent-span-client.js +87 -0
  15. package/core/search/agent-span-ledger.js +447 -0
  16. package/core/search/context-expander.js +29 -0
  17. package/core/search/grep-output-shaping.js +28 -0
  18. package/core/search/regex-dialect.js +328 -0
  19. package/core/search/search-format.js +96 -0
  20. package/core/search/search-pattern.js +62 -10
  21. package/core/search/search-read.js +57 -10
  22. package/core/search/search-server.js +375 -7
  23. package/core/search/session-daemon-prewarm.mjs +75 -0
  24. package/core/search/sweet-search.js +7 -1
  25. package/core/search/unread-symbol-ranking.js +90 -0
  26. package/eval/agent-read-workflows/bin/_ss-helpers.mjs +249 -45
  27. package/mcp/read-tool.js +34 -3
  28. package/mcp/server.js +55 -9
  29. package/mcp/tool-handlers.js +60 -7
  30. package/package.json +9 -9
  31. package/scripts/init.js +30 -5
  32. package/scripts/inject-agent-instructions.js +4 -2
  33. package/scripts/uninstall.js +21 -19
package/README.md CHANGED
@@ -23,7 +23,7 @@ Every coding agent today reaches for grep + Read by reflex. *sweet-search* chall
23
23
  ## ✨ Highlights
24
24
 
25
25
  - **Hybrid retrieval** — one of the six tools uses BM25F lexical + dense semantic + structural graph signals, fused per query and reranked by late-interaction
26
- - **Agent-native by design** — token-budgeted output tiers, an optional MCP server (and default zero-overhead CLI), and a GEPA-evolved system prompt installed into Claude Code, Codex, Gemini CLI, and Cursor with one command
26
+ - **Agent-native by design** — token-budgeted output tiers, an optional MCP server (and default zero-overhead CLI), and a GEPA-evolved system prompt one `init` installs it into Claude Code by default (Codex, Gemini CLI, and Cursor via flags)
27
27
  - **Indexed grep, ~10× faster than ripgrep** — a sparse n-gram prefilter skips the files that provably can't match
28
28
  - **ColBERT-style reranking, locally** — per-token MaxSim late interaction on hand-written SIMD kernels
29
29
  - **GPU-accelerated indexing** — Apple Metal, CUDA, CoreML Neural Engine, or plain CPU via ORT; same engine, auto-selected
@@ -263,6 +263,7 @@ We're SOTA in June 2026 on 3/4 attempted benchmarks at HARDER settings (running
263
263
  - **Reproduction:** result artifacts live in [`eval/results/`](eval/results/); rerun via `eval/run_all.js`. The canonical full-pool loaders are in `eval/download_data.py`.
264
264
  - **Full corpus, not distractors.** Published baselines for GCSN- and CoSQA-style benchmarks typically rank the gold against 99 sampled distractors; every number here ranks against the benchmark's *full* corpus (6k–19k candidates) — strictly harder.
265
265
  - **Zero-shot + docstring-stripped.** We never fine-tune on these tasks. For docstring-derived benchmarks (AdvTest, M2CRB) we strip the docstring from the indexed code — otherwise the NL query matches itself verbatim (a no-strip AdvTest run scores a meaningless 0.98). This is the standard protocol; it is also why our AdvTest is lower than naïve setups that leave the docstring in.
266
+ - **Dev/held-out split.** Our ranking work iterates against a fixed dev split of each benchmark (e.g. GCSN: 600 dev + 400 held-out per language, stratified, seed=42) and treats the remainder as held-out, inspected aggregate-only at milestones. The table figures are full-corpus runs — they include the dev portion we tuned against; a held-out-only breakdown ships with the next results refresh.
266
267
  - **What we deliberately don't claim yet.** CoIR (official metric NDCG@10 over per-subtask corpora up to ~1M docs), CoSQA+ (multi-positive, MAP-primary), and CLARC (per-group pools) use protocols and metrics our single-pool MRR@10 harness doesn't currently match. Rather than publish apples-to-oranges numbers, we omit them; faithful per-subtask CoIR (NDCG@10) runs are queued.
267
268
  - **M2CRB** — the paper's metric is *auMRRc* (area under the MRR-vs-pool-size curve; best published **52.7**, fine-tuned). Because that area averages over easier small pools, `auMRRc ≥ full-pool MRR` for any model — so our **54.0 full-pool MRR@10** (all 5,795 functions, zero-shot) clears their best on a strictly harder measure. No one publishes a plain full-corpus MRR@10 on M2CRB, so ours is the best available.
268
269
  - **AdvTest honesty note.** We could not reproduce the commonly-cited 59.5 for the bare CodeRankEmbed encoder on our corpus: the reference FP32 model scores 54.7 on our leak-free, docstring-stripped, full-19,210 setup, and our shipped INT8 build 51.4. We report our measured numbers and the reference check rather than the leaderboard figure.
@@ -399,7 +400,7 @@ flowchart TD
399
400
  | 🎯 **Intent Rerank** | • demote docs / tests / config when you want implementation<br/>• log-scaled call-site boosts surface the most-referenced function |
400
401
  | 🕸️ **Graph Expansion** | • typed-edge walks (`imports`/`extends`/`calls`/`uses`) · adaptive 2-hop on the AST graph · edges picked by intent<br/>• **PathRAG** flow pruning + degree normalization → hubs can't dominate |
401
402
  | 🧮 **Late interaction Rerank** | • Query embedded per-token by **LateOn-Code** (149M; a 17M **edge** variant auto-selected on low-RAM hosts)<br/>• **MaxSim** against the pre-indexed quantized token vectors<br/>• native Rust+Rayon MaxSim kernel ⚡ · WASM-SIMD fallback (1.26 s → 27 ms on a 231-candidate rerank) |
402
- | 📦 **Package** | • entity-aware expansion → whole functions (imports, docstrings, decorators)<br/>• same-file overlap demotion → diverse, non-overlapping spans<br/>• auto-selected **3k / 8k / 12k** token budget |
403
+ | 📦 **Package** | • entity-aware expansion → whole functions (imports, docstrings, decorators)<br/>• same-file overlap demotion → diverse, non-overlapping spans<br/>• **symbol-family completion** (agent mode) — generated/width families surface as a compact indexed manifest instead of truncating silently, inside the same budget<br/>• auto-selected **3k / 8k / 12k** token budget |
403
404
 
404
405
  <details>
405
406
  <summary><b>🌶️ Extra spice — the bits that didn't fit the diagram</b></summary>
@@ -418,7 +419,7 @@ flowchart TD
418
419
  - **Quality priors:** every chunk carries a 0–1 prior from test proximity, git recency, symbol centrality (PageRank), comment density, and complexity — production code surfaces, stale fixtures sink.
419
420
  - **Community structure:** a canonical **Leiden** pass detects code communities on the entity graph at index time, feeding vocabulary prewarming and structural signals — it understands your modules, not just your directories.
420
421
  - **Multilingual:** 14 languages get full tree-sitter AST treatment; a 39-config registry covers 70+ extensions beyond that. Router features handle camelCase/snake_case, CJK density, and German compounds.
421
- - **Format-gated signals:** structure-aware boosts and demotions (symbol-exact, path-token, mega-entity) fire only in agent mode — they help agent-shaped queries and would hurt plain NL, so they stay gated by default.
422
+ - **Format-gated signals:** structure-aware boosts and demotions (symbol-exact, path-token, anomalous-chunk, mega-entity) fire only in agent mode — they help agent-shaped queries and would hurt plain NL, so they stay gated by default.
422
423
 
423
424
  **🛟 Rescues & honest trade-offs.**
424
425
  - **Long-query rescue:** wordy NL queries that FTS5 would tokenize into an unsatisfiable `AND` fall back to multi-query BM25F + RRF — one query per content keyword, fused.
@@ -449,6 +450,7 @@ Every match comes back in stable `file:line` order — ripgrep-identical counts,
449
450
 
450
451
  - Full methodology, per-repo table, and the optimization log: [`docs/GREP_INDEXING_STRATEGY.md`](docs/GREP_INDEXING_STRATEGY.md).
451
452
  - Regexes with no extractable literals fall back to native grep over the indexed file set; fixed-string and glob queries use a ripgrep fallback.
453
+ - **Dialect recovery** (agent mode): patterns written in GNU-grep BRE muscle memory (`foo\|bar`, `\(group\)`) are literals in Rust's regex dialect and used to silently match nothing — a zero-hit exact search now gets one gated auto-retry with the translated pattern instead of a false "no matches".
452
454
 
453
455
  </details>
454
456
 
@@ -553,6 +555,8 @@ without another search.
553
555
  <summary><b>More</b></summary>
554
556
 
555
557
  - The CLI/MCP form scales it up: `sweet-search read <file...>` (and the `read` MCP tool) batches **1–20 files in a single call**, each with the same symbol metadata — twenty files for the price of one tool invocation.
558
+ - **Query-aware continuation** (agent mode): a range read that stops before EOF names the unread symbols below it — ranked by relevance to the session's recent queries, not just declaration order — plus the exact command to continue.
559
+ - **Shown-span receipts** (agent mode, default-on): the daemon remembers what a session has already been shown; an exact re-read collapses to a one-line receipt instead of resending the bytes.
556
560
 
557
561
  </details>
558
562
 
@@ -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 callersRaw = [...this.repo.getCallers(target, { limit: 160 }), ...(this.repo.getAliasCallers?.(target, { limit: 80 }) || [])].map(x => ({ ...x, depth: 1 }));
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(Boolean).map(x => ({ ...x, relationship: 'handoff', depth: 1 }));
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,
@@ -13,6 +13,10 @@ import { existsSync, statSync } from 'fs';
13
13
  import { applyReadPragmas, prepareCached } from './db-utils.js';
14
14
  import { readAdjacentManifest, resolveManifestCodeGraphPath, sqlAliasPrefix } from './code-graph-visibility.js';
15
15
 
16
+ function pathIsUnsafe(value) {
17
+ return value.startsWith('/') || value === '..' || value.startsWith('../') || value.includes('/../');
18
+ }
19
+
16
20
  export class CodeGraphRepository {
17
21
  constructor(dbPath, options = {}) {
18
22
  this._baseDbPath = dbPath;
@@ -383,6 +387,60 @@ export class CodeGraphRepository {
383
387
  }
384
388
  }
385
389
 
390
+ /**
391
+ * Find exact indexed symbol-family candidates under a bounded directory.
392
+ * The search domain supplies an identifier stem discovered from indexed
393
+ * seeds; this adapter performs only parameterized persistence work.
394
+ *
395
+ * @param {string} stem - alphanumeric identifier fragment (3-64 chars)
396
+ * @param {{ filePrefix:string, types?:string[], limit?:number }} opts
397
+ * @returns {Array<{id,name,type,filePath,startLine,endLine,parentClass}>}
398
+ */
399
+ findFamilyCandidates(stem, opts = {}) {
400
+ const db = this._open();
401
+ if (!db || typeof stem !== 'string' || !/^[A-Za-z][A-Za-z0-9]{2,63}$/.test(stem)) return [];
402
+ const prefix = typeof opts.filePrefix === 'string'
403
+ ? opts.filePrefix.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '')
404
+ : '';
405
+ if (!prefix || prefix === '.' || pathIsUnsafe(prefix)) return [];
406
+ const types = Array.isArray(opts.types)
407
+ ? [...new Set(opts.types.filter((type) => typeof type === 'string' && /^[A-Za-z][\w-]{0,31}$/.test(type)))].slice(0, 16)
408
+ : [];
409
+ const limit = Math.max(1, Math.min(128, opts.limit ?? 64));
410
+ const escapeLike = (value) => value.replace(/[\\%_]/g, '\\$&');
411
+ try {
412
+ const typeSql = types.length > 0 ? `AND type IN (${types.map(() => '?').join(',')})` : '';
413
+ const sql = `
414
+ SELECT id, name, type, file_path, start_line, end_line, parent_class
415
+ FROM entities
416
+ WHERE lower(name) LIKE lower(?) ESCAPE '\\'
417
+ AND file_path LIKE ? ESCAPE '\\'
418
+ ${typeSql}
419
+ AND ${this._entityVisibilitySql(db)}
420
+ ORDER BY name ASC, file_path ASC, start_line ASC
421
+ LIMIT ?
422
+ `;
423
+ const args = [
424
+ `%${escapeLike(stem)}%`,
425
+ `${escapeLike(prefix)}/%`,
426
+ ...types,
427
+ ...this._entityVisibilityParams(db),
428
+ limit,
429
+ ];
430
+ return prepareCached(db, sql).all(...args).map((row) => ({
431
+ id: row.id,
432
+ name: row.name,
433
+ type: row.type,
434
+ filePath: row.file_path,
435
+ startLine: row.start_line,
436
+ endLine: row.end_line,
437
+ parentClass: row.parent_class || null,
438
+ }));
439
+ } catch {
440
+ return [];
441
+ }
442
+ }
443
+
386
444
  /**
387
445
  * Get a single entity by id, with file:line metadata.
388
446
  *
@@ -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+[<"]([^>"]+)[>"]/,
@@ -292,6 +292,13 @@ export async function fetchModelFile(hfId, filePath, destDir, options = {}) {
292
292
  if (startByte > 0) {
293
293
  headers['Range'] = `bytes=${startByte}-`;
294
294
  }
295
+ // HF 403s anonymous downloads from datacenter IPs (CI runners), even on
296
+ // public files. A read token lifts that. Only sent to huggingface.co —
297
+ // never to a custom hfEndpoint, which may be an untrusted mirror.
298
+ const hfToken = process.env.HF_TOKEN || process.env.HUGGING_FACE_HUB_TOKEN;
299
+ if (hfToken && url.startsWith('https://huggingface.co/')) {
300
+ headers['Authorization'] = `Bearer ${hfToken}`;
301
+ }
295
302
 
296
303
  const resp = await fetch(url, { headers, signal });
297
304
 
@@ -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 1
83
+ LIMIT 8
83
84
  `).all(...entityParams, ...names);
84
- return this._entityFromRow(rows[0]);
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,15 +1,19 @@
1
1
  ---
2
2
  variant: mcp
3
- derived_from: p7-v1-mppppp
3
+ derived_from: p7-v1-mppppp-fs
4
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+++++ champion (p7-v1-mppppp = M++++ +
8
- the verdict-gated trust line: trust rank 1 outright when the response
9
- reports sufficiency yes, otherwise scan the already-delivered lower
10
- ranks/same-file map before any new search; smoke-validated on the
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
11
  task-completion bench, see memory
12
- project_mppppp_conditional_trust_candidate). The STRATEGY core routing by
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
13
17
  what-you-hold, verdict-gated trust, sufficiency stops, the two-probe absence
14
18
  rule, the <state_summary> gate, and the search-output discipline — is
15
19
  preserved (semantics intact; the rest carries only the tool-mechanics
@@ -57,3 +61,5 @@ Before your third sweet-search probe in the current search iteration — or befo
57
61
 
58
62
  ## Search output
59
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-mppppp
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: 1221
9
+ token_count: 1307
10
10
  scs_cwSCS: 0.931
11
11
  scs_min_paraphrase_accuracy: 1
12
12
  homp_family_pass: true
@@ -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.