sweet-search 2.6.14 → 2.6.16

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.
@@ -312,6 +312,77 @@ export class CodeGraphRepository {
312
312
  }
313
313
  }
314
314
 
315
+ /**
316
+ * Find the named entities immediately ADJACENT to a shown line window in
317
+ * the same file: the nearest ones lying fully ABOVE the window
318
+ * (end_line < startLine) and the nearest ones starting BELOW it
319
+ * (start_line > endLine). Powers the agent-format same-file span map —
320
+ * the pointer that tells an agent which sibling symbols sit just outside
321
+ * the chunk window it was shown.
322
+ *
323
+ * Entities enclosing the window (e.g. the surrounding class) never match
324
+ * either predicate, so results are true siblings. Inner entities contained
325
+ * in an already-kept row (closures inside the adjacent function) are
326
+ * filtered so the map names outermost neighbors only.
327
+ *
328
+ * @param {string} filePath
329
+ * @param {number} startLine - first shown line (1-based)
330
+ * @param {number} endLine - last shown line (1-based)
331
+ * @param {{ perSide?: number }} [opts] - max entities per side (default 2)
332
+ * @returns {{ above: Array<{id,name,type,startLine,endLine,parentClass}>,
333
+ * below: Array<{id,name,type,startLine,endLine,parentClass}> }}
334
+ */
335
+ findAdjacentEntities(filePath, startLine, endLine, opts = {}) {
336
+ const perSide = opts.perSide ?? 2;
337
+ const empty = { above: [], below: [] };
338
+ if (!filePath || !Number.isFinite(startLine) || !Number.isFinite(endLine)) return empty;
339
+ const db = this._open();
340
+ if (!db) return empty;
341
+ const mapRow = row => ({
342
+ id: row.id,
343
+ name: row.name,
344
+ type: row.type,
345
+ startLine: row.start_line,
346
+ endLine: row.end_line,
347
+ parentClass: row.parent_class || null,
348
+ });
349
+ // Keep outermost neighbors: drop rows contained in an already-kept row.
350
+ const keepOutermost = rows => {
351
+ const kept = [];
352
+ for (const row of rows) {
353
+ if (kept.some(k => row.start_line >= k.start_line && row.end_line <= k.end_line)) continue;
354
+ kept.push(row);
355
+ if (kept.length >= perSide) break;
356
+ }
357
+ return kept.map(mapRow);
358
+ };
359
+ try {
360
+ const aboveRows = prepareCached(db, `
361
+ SELECT id, name, type, start_line, end_line, parent_class
362
+ FROM entities
363
+ WHERE file_path = ?
364
+ AND end_line < ?
365
+ AND name IS NOT NULL AND name != ''
366
+ AND ${this._entityVisibilitySql(db)}
367
+ ORDER BY end_line DESC, start_line ASC
368
+ LIMIT 8
369
+ `).all(filePath, startLine, ...this._entityVisibilityParams(db));
370
+ const belowRows = prepareCached(db, `
371
+ SELECT id, name, type, start_line, end_line, parent_class
372
+ FROM entities
373
+ WHERE file_path = ?
374
+ AND start_line > ?
375
+ AND name IS NOT NULL AND name != ''
376
+ AND ${this._entityVisibilitySql(db)}
377
+ ORDER BY start_line ASC, end_line DESC
378
+ LIMIT 8
379
+ `).all(filePath, endLine, ...this._entityVisibilityParams(db));
380
+ return { above: keepOutermost(aboveRows), below: keepOutermost(belowRows) };
381
+ } catch {
382
+ return empty;
383
+ }
384
+ }
385
+
315
386
  /**
316
387
  * Get a single entity by id, with file:line metadata.
317
388
  *
@@ -1,17 +1,21 @@
1
1
  ---
2
2
  variant: mcp
3
- derived_from: p7-v1-mpppp
4
- source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/Mpppp.md
3
+ derived_from: p7-v1-mppppp
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-mpppp = M++ +
8
- stop-discipline scoping edits: search-rules-only intro, "stop searching" /
9
- "Search output" wording; routing unchanged from the frozen, benchmarked
10
- M++). The STRATEGY core routing by what-you-hold, trust-the-top-hit,
11
- sufficiency stops, the two-probe absence rule, the <state_summary> gate, and
12
- the search-output disciplineis preserved (semantics intact; the rest
13
- carries only the tool-mechanics
14
- rename). Only the tool-mechanics layer is remapped: the six ss-* CLI commands collapse onto the sweet-search MCP
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
11
+ task-completion bench, see memory
12
+ project_mppppp_conditional_trust_candidate). The STRATEGY core routing by
13
+ what-you-hold, verdict-gated trust, sufficiency stops, the two-probe absence
14
+ rule, the <state_summary> gate, and the search-output discipline is
15
+ preserved (semantics intact; the rest carries only the tool-mechanics
16
+ rename). The trust gate is phrased against the MCP response surface
17
+ (sufficiency verdict in structured content; "Also found" summary list;
18
+ "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
19
  tool surface (a UNIFIED `search` that subsumes ss-search/ss-find/ss-grep, plus
16
20
  `trace` / `read-semantic` / `read` / `repo-map`). The anti-raw-scan discipline
17
21
  is KEPT and retargeted from "ss-* vs raw grep" to "sweet-search tools vs native
@@ -40,7 +44,7 @@ The sweet-search MCP server exposes these (call each by the name your client lis
40
44
  - **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
45
  - **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
46
 
43
- Trust the top ranked result; confirm with at most one narrow `read`, never a re-run of a matching hit.
47
+ 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
48
 
45
49
  ## Multi-file
46
50
  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.
@@ -1,12 +1,12 @@
1
1
  ---
2
- run_id: p7-v1-mpppp
2
+ run_id: p7-v1-mppppp
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: 1175
9
+ token_count: 1221
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
- Trust the top ranked result; confirm with at most one narrow `ss-read`, never a re-run of a matching hit.
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.
@@ -1931,6 +1931,40 @@ export function selectAgentBudget(format, signals, opts = {}) {
1931
1931
  * 'no-syntax-expansion', 'no-header', 'no-diversity', 'no-adaptive-budget'
1932
1932
  * @returns {object} Agent mode response
1933
1933
  */
1934
+ /**
1935
+ * Same-file span map (2026-07, within-file blind-spot fix). When the top-1
1936
+ * pack entry is a WINDOWED view of a file (kind chunk/sandwich/syntax — not
1937
+ * a fully-shown symbol) and the sufficiency verdict is not a clear YES, one
1938
+ * compact line names the sibling symbols immediately above/below the shown
1939
+ * window so the agent can sweep the fix surface instead of leaving the file
1940
+ * (the fhir/sushi-1175 miss shape: right file, bug 30 lines above the
1941
+ * window). Names+kinds+lines only — never bodies; ~25-45 tokens.
1942
+ *
1943
+ * @param {{ file:string, startLine:number, endLine:number }} top - top-1 agent result
1944
+ * @param {{ above:Array, below:Array }} adjacent - findAdjacentEntities() result
1945
+ * @returns {{ rendered:string, tokens:number, neighbors:Array }|null}
1946
+ */
1947
+ export function buildSameFileMap(top, adjacent) {
1948
+ const above = adjacent?.above || [];
1949
+ const below = adjacent?.below || [];
1950
+ if (above.length === 0 && below.length === 0) return null;
1951
+ const shortType = t => (t === 'function' ? 'fn' : (t || 'sym'));
1952
+ const fmt = (e, pos) => `${e.name} (${shortType(e.type)} ${e.startLine}-${e.endLine} ${pos})`;
1953
+ const neighbors = [
1954
+ ...above.map(e => ({ ...e, position: 'above' })),
1955
+ ...below.map(e => ({ ...e, position: 'below' })),
1956
+ ];
1957
+ const parts = [
1958
+ ...above.map(e => fmt(e, 'above')),
1959
+ ...below.map(e => fmt(e, 'below')),
1960
+ ];
1961
+ // Placeholder-style drill-in hint (the v2.6.13 `ss-grep "<regex>" --in
1962
+ // <file>` convention, which agents demonstrably follow) — embedding the
1963
+ // live query costs ~25-30 tokens per pack for no extra signal.
1964
+ const rendered = `# same file: ${parts.join(' · ')} — sweep: ss-semantic ${top.file} "<query>"`;
1965
+ return { rendered, tokens: estimateTokens(rendered), neighbors };
1966
+ }
1967
+
1934
1968
  export function packageForAgent(rankedResults, searchStats, opts) {
1935
1969
  const {
1936
1970
  query,
@@ -2347,6 +2381,53 @@ export function packageForAgent(rankedResults, searchStats, opts) {
2347
2381
  unresolvedExternalCount = sufficiency.unresolvedExternalCount || 0;
2348
2382
  }
2349
2383
 
2384
+ // Phase 7: same-file span map (top-1 only). Emitted ONLY when the verdict
2385
+ // is not a clear YES (composes with the query-conditioned verdict: the map
2386
+ // supplies the "where to look next" exactly when the engine says "keep
2387
+ // looking") AND top-1 is a windowed view (kind != 'full' — fully-shown
2388
+ // symbols stay byte-identical) AND the line fits the remaining tier
2389
+ // budget (dropped on overflow, never a truncated pack). Its tokens are
2390
+ // counted inside tokensUsed. Disabled by 'no-same-file-map' ablation.
2391
+ if (!ablations.has('no-same-file-map')
2392
+ && agentResults.length > 0
2393
+ && sufficiencyVerdict !== 'yes'
2394
+ && codeGraphRepo
2395
+ && typeof codeGraphRepo.findAdjacentEntities === 'function') {
2396
+ const top = agentResults[0];
2397
+ const windowed = top.code
2398
+ && top.presentation !== 'summary'
2399
+ && top.expansionKind
2400
+ && top.expansionKind !== 'full'
2401
+ && top.file
2402
+ && Number.isFinite(top.startLine)
2403
+ && Number.isFinite(top.endLine);
2404
+ if (windowed) {
2405
+ let adjacent = null;
2406
+ try {
2407
+ adjacent = codeGraphRepo.findAdjacentEntities(top.file, top.startLine, top.endLine, { perSide: 2 });
2408
+ } catch { adjacent = null; }
2409
+ // Don't name neighbors whose code is ALREADY visible in the pack —
2410
+ // locality clustering pulls same-file companions to ranks 2-3; a map
2411
+ // entry for a shown span is pure noise.
2412
+ if (adjacent) {
2413
+ const shownSameFile = agentResults.filter(r =>
2414
+ r !== top && r.code && r.file === top.file
2415
+ && Number.isFinite(r.startLine) && Number.isFinite(r.endLine));
2416
+ const overlapsShown = e => shownSameFile.some(r =>
2417
+ e.startLine <= r.endLine && e.endLine >= r.startLine);
2418
+ adjacent = {
2419
+ above: adjacent.above.filter(e => !overlapsShown(e)),
2420
+ below: adjacent.below.filter(e => !overlapsShown(e)),
2421
+ };
2422
+ }
2423
+ const map = adjacent ? buildSameFileMap(top, adjacent) : null;
2424
+ if (map && map.tokens <= Math.max(0, tokenBudget - tokensUsed)) {
2425
+ top.sameFile = map;
2426
+ tokensUsed += map.tokens;
2427
+ }
2428
+ }
2429
+ }
2430
+
2350
2431
  return {
2351
2432
  query,
2352
2433
  regex,
@@ -239,6 +239,48 @@ function _attachIndexMetadata(filePathRel, projectRoot) {
239
239
  return { indexed: true, chunks, language };
240
240
  }
241
241
 
242
+ // ---------------------------------------------------------------------------
243
+ // Remainder definition sniffing — fallback symbol names for the "what
244
+ // remains" trailer when the index has no named chunks in the unread span
245
+ // (e.g. C++ files where the chunker recorded `name: null`). Scans ONLY the
246
+ // remainder lines of the buffer already in memory: zero I/O, capped.
247
+ // ---------------------------------------------------------------------------
248
+
249
+ const SNIFF_MAX_LINES = 4000;
250
+ const UNREAD_SYMBOLS_MAX = 5; // hard cap on named symbols in the trailer
251
+ const UNREAD_SYMBOLS_MIN_LINES = 20; // smaller remainders get the short form
252
+ const C_FAMILY_EXTS = new Set(['.c', '.h', '.cc', '.cpp', '.cxx', '.hpp', '.hh', '.hxx', '.java', '.cs', '.m', '.mm']);
253
+
254
+ // Keyword-introduced definitions (Python/Ruby/JS/TS/Go/Rust/Kotlin/PHP/...).
255
+ const KEYWORD_DEF_RE = /^\s*(?:export\s+|default\s+|pub(?:\([^)]*\))?\s+|static\s+|async\s+|abstract\s+|final\s+|public\s+|private\s+|protected\s+|inline\s+|constexpr\s+|unsafe\s+|override\s+|open\s+|sealed\s+)*(?:def|fn|func|function\*?|class|struct|enum|trait|interface|impl|object|module|proc)\s+(?:\([^)]*\)\s*)?([A-Za-z_][\w]*(?:(?:::|\.)[A-Za-z_][\w]*)*)/;
256
+ // C-family definitions at low indent: `[return-type] Qualified::name(args...`
257
+ // with no trailing `;` (declarations) — captures the identifier before the
258
+ // first `(`. The return-type prefix is lazy so qualification stays intact.
259
+ const C_DEF_RE = /^(?:[A-Za-z_][\w:<>,*&~\s]*?[\s*&]+)?((?:[A-Za-z_~][\w]*::)*(?:~?[A-Za-z_][\w]*|operator\s*[^\s(]{1,3}))\s*\(/;
260
+ const C_CONTROL_RE = /^\s*(?:if|for|while|switch|return|else|do|catch|case|sizeof|new|delete|throw|goto|using|typedef)\b/;
261
+
262
+ function _sniffRemainderDefinitions(text, isCFamily) {
263
+ const names = [];
264
+ const seen = new Set();
265
+ const lines = text.split('\n', SNIFF_MAX_LINES);
266
+ for (let i = 0; i < lines.length; i++) {
267
+ const line = lines[i];
268
+ if (!/\S/.test(line)) continue;
269
+ let name = null;
270
+ const kw = line.match(KEYWORD_DEF_RE);
271
+ if (kw) name = kw[1];
272
+ else if (isCFamily && /^[A-Za-z_]/.test(line) && !line.trimEnd().endsWith(';') && !C_CONTROL_RE.test(line)) {
273
+ const m = line.match(C_DEF_RE);
274
+ if (m) name = m[1].replace(/\s+/g, '');
275
+ }
276
+ if (name && !seen.has(name)) {
277
+ seen.add(name);
278
+ names.push({ symbol: name, type: null, startLine: i + 1 }); // startLine relative; caller offsets
279
+ }
280
+ }
281
+ return names;
282
+ }
283
+
242
284
  // ---------------------------------------------------------------------------
243
285
  // Public API — single read
244
286
  // ---------------------------------------------------------------------------
@@ -293,6 +335,47 @@ async function _readFileUnpinned(req) {
293
335
  language = meta.language;
294
336
  }
295
337
 
338
+ // "What remains" trailer data (2026-07, within-file blind-spot fix): when
339
+ // a range read stops before EOF, record what the UNREAD remainder below
340
+ // the window contains — computed from the full chunk table BEFORE the
341
+ // overlap-narrowing just below. A bare "(lines a-b of N)" marker is
342
+ // provably ignored by agents (the botan-2738 shape: three reads, never
343
+ // past line 205 of 272, fix surface below); naming the symbols is what
344
+ // makes the remainder actionable. Whole-file reads and read-to-EOF stay
345
+ // byte-identical (unreadBelow stays null).
346
+ let unreadBelow = null;
347
+ if (wantsRange && sliced.totalLines > 0 && sliced.endLine < sliced.totalLines) {
348
+ // Token diet: a tiny remainder needs no symbol list — the range plus the
349
+ // continue command IS the affordance; names only earn their tokens when
350
+ // the unread span is big enough to hide a sibling branch.
351
+ const remainderLines = sliced.totalLines - sliced.endLine;
352
+ const seen = new Set();
353
+ let symbols = [];
354
+ if (remainderLines >= UNREAD_SYMBOLS_MIN_LINES) {
355
+ for (const c of chunks) {
356
+ if (c.startLine == null || c.startLine <= sliced.endLine) continue;
357
+ if (!c.symbol || seen.has(c.symbol)) continue;
358
+ seen.add(c.symbol);
359
+ symbols.push({ symbol: c.symbol, type: c.type ?? null, startLine: c.startLine });
360
+ }
361
+ // Index had no named chunks in the remainder (common for C/C++ where the
362
+ // chunker stores name:null) — sniff definition lines from the in-memory
363
+ // buffer instead. Zero I/O; capped at SNIFF_MAX_LINES.
364
+ if (symbols.length === 0 && disk.text != null) {
365
+ const remainder = _sliceLines(disk.text, disk.lineOffsets, sliced.endLine + 1, sliced.totalLines);
366
+ const isCFamily = C_FAMILY_EXTS.has(path.extname(absPath).toLowerCase());
367
+ symbols = _sniffRemainderDefinitions(remainder.text, isCFamily)
368
+ .map(s => ({ ...s, startLine: sliced.endLine + s.startLine }));
369
+ }
370
+ }
371
+ unreadBelow = {
372
+ startLine: sliced.endLine + 1,
373
+ endLine: sliced.totalLines,
374
+ symbols: symbols.slice(0, UNREAD_SYMBOLS_MAX),
375
+ moreCount: Math.max(0, symbols.length - UNREAD_SYMBOLS_MAX),
376
+ };
377
+ }
378
+
296
379
  // If a line range was requested, narrow attached chunks to the overlap.
297
380
  if (wantsRange && chunks.length) {
298
381
  chunks = chunks.filter(c =>
@@ -315,6 +398,7 @@ async function _readFileUnpinned(req) {
315
398
  range: wantsRange ? { startLine: sliced.startLine, endLine: sliced.endLine } : null,
316
399
  text: sliced.text,
317
400
  chunks,
401
+ unreadBelow,
318
402
  timings: { totalMs: +(performance.now() - t0).toFixed(2) },
319
403
  };
320
404
  }
@@ -362,6 +446,28 @@ export async function readFiles(files, opts = {}) {
362
446
  // Formatting
363
447
  // ---------------------------------------------------------------------------
364
448
 
449
+ /**
450
+ * Render the "what remains" trailer for a range read that stopped before
451
+ * EOF. Names the symbols in the unread remainder plus the exact continue
452
+ * command — the actionable form (a bare truncation marker is ignored;
453
+ * see the 2026-07 within-file design note). Returns '' when the read
454
+ * covered the whole file / reached EOF.
455
+ *
456
+ * @param {Object} result - readFile() result
457
+ * @param {{ command?: 'read'|'ss-read' }} [opts] - continue-command surface
458
+ * @returns {string} one line without trailing newline, or ''
459
+ */
460
+ export function renderUnreadBelow(result, { command = 'read' } = {}) {
461
+ const u = result?.unreadBelow;
462
+ if (!u) return '';
463
+ const names = (u.symbols || []).map(s => s.symbol).join(', ');
464
+ const more = u.moreCount > 0 ? ` +${u.moreCount} more` : '';
465
+ const cont = command === 'ss-read'
466
+ ? `ss-read ${result.file} ${u.startLine} ${u.endLine}`
467
+ : `read ${result.file} ${u.startLine}-${u.endLine}`;
468
+ return `# unread below (${u.startLine}-${u.endLine})${names ? ': ' + names + more : ''} — continue: ${cont}`;
469
+ }
470
+
365
471
  function _formatAgent(result) {
366
472
  if (!result.ok) {
367
473
  return `### ${result.file}\n[error] ${result.error}\n`;
@@ -377,7 +483,8 @@ function _formatAgent(result) {
377
483
  .filter(Boolean);
378
484
  if (names.length) symbolHint = `\nsymbols: ${names.join(', ')}`;
379
485
  }
380
- return `### ${result.file}${range}${symbolHint}\n${fence}\n${result.text}\n\`\`\`\n`;
486
+ const remainder = renderUnreadBelow(result);
487
+ return `### ${result.file}${range}${symbolHint}\n${fence}\n${result.text}\n\`\`\`${remainder ? '\n' + remainder : ''}\n`;
381
488
  }
382
489
 
383
490
  export function formatReadResults(results, format = 'agent') {
@@ -242,6 +242,11 @@ async function cmdFind(rawArgs) {
242
242
  if (r.neighbors && r.neighbors.rendered) {
243
243
  process.stdout.write(`### related (1-hop graph, ~${r.neighbors.tokens} tok)\n${r.neighbors.rendered}\n`);
244
244
  }
245
+ // Same-file span map (top-1, windowed chunk, verdict != YES): sibling
246
+ // symbols just outside the shown window + a copy-paste drill-in command.
247
+ if (r.sameFile && r.sameFile.rendered) {
248
+ process.stdout.write(`${r.sameFile.rendered}\n`);
249
+ }
245
250
  }
246
251
  if (!response.results || response.results.length === 0) process.stdout.write('(no matches)\n');
247
252
  process.exit(0);
@@ -297,7 +302,7 @@ async function cmdRead(args) {
297
302
  }
298
303
  }
299
304
  }
300
- const { readFile } = await import(path.join(REPO_ROOT, 'core/search/search-read.js'));
305
+ const { readFile, renderUnreadBelow } = await import(path.join(REPO_ROOT, 'core/search/search-read.js'));
301
306
  const r = await readFile({ path: file, projectRoot: PROJECT_ROOT, startLine: start ?? undefined, endLine: end ?? undefined });
302
307
  if (!r.ok) {
303
308
  process.stderr.write(`[ss-read] error: ${r.error}\n`);
@@ -305,7 +310,11 @@ async function cmdRead(args) {
305
310
  }
306
311
  const range = r.range ? ` (lines ${r.range.startLine}-${r.range.endLine} of ${r.totalLines})` : ` (${r.totalLines} lines)`;
307
312
  const fence = r.language ? '```' + r.language : '```';
308
- process.stdout.write(`# ss-read ${r.file}${range}\n${fence}\n${r.text}\n\`\`\`\n`);
313
+ // "What remains" trailer: on a range read that stops before EOF, one final
314
+ // line names the symbols in the unread remainder + the exact continue
315
+ // command (last line for recency — the actionable form of truncation).
316
+ const remainder = renderUnreadBelow(r, { command: 'ss-read' });
317
+ process.stdout.write(`# ss-read ${r.file}${range}\n${fence}\n${r.text}\n\`\`\`${remainder ? '\n' + remainder : ''}\n`);
309
318
  process.exit(0);
310
319
  }
311
320
 
@@ -441,6 +450,11 @@ async function cmdAgentSearch(rawArgs) {
441
450
  if (r.neighbors && r.neighbors.rendered) {
442
451
  process.stdout.write(`### related (1-hop graph, ~${r.neighbors.tokens} tok)\n${r.neighbors.rendered}\n`);
443
452
  }
453
+ // Same-file span map (top-1, windowed chunk, verdict != YES): sibling
454
+ // symbols just outside the shown window + a copy-paste drill-in command.
455
+ if (r.sameFile && r.sameFile.rendered) {
456
+ process.stdout.write(`${r.sameFile.rendered}\n`);
457
+ }
444
458
  }
445
459
 
446
460
  if (!response.results || response.results.length === 0) {
@@ -479,6 +493,8 @@ async function cmdAgentSearch(rawArgs) {
479
493
  sufficiencyReasons: Array.isArray(response.sufficiencyReasons) ? response.sufficiencyReasons : null,
480
494
  unresolvedExternalCount: typeof response.unresolvedExternalCount === 'number'
481
495
  ? response.unresolvedExternalCount : null,
496
+ sameFileMapTokens: response.results?.[0]?.sameFile?.tokens ?? null,
497
+ sameFileNeighborCount: response.results?.[0]?.sameFile?.neighbors?.length ?? null,
482
498
  };
483
499
  process.stdout.write(`\n<<SS_ROUTE_META>>${JSON.stringify(meta)}\n`);
484
500
  process.exit(0);
package/mcp/read-tool.js CHANGED
@@ -23,6 +23,16 @@ const ReadFileResultSchema = z.object({
23
23
  endLine: z.number().int().nullable().optional(),
24
24
  signature: z.string().nullable().optional(),
25
25
  })).optional(),
26
+ unreadBelow: z.object({
27
+ startLine: z.number().int(),
28
+ endLine: z.number().int(),
29
+ symbols: z.array(z.object({
30
+ symbol: z.string(),
31
+ type: z.string().nullable().optional(),
32
+ startLine: z.number().int().nullable().optional(),
33
+ })),
34
+ moreCount: z.number().int(),
35
+ }).nullable().optional(),
26
36
  error: z.string().optional(),
27
37
  timings: z.object({ totalMs: z.number() }).optional(),
28
38
  });
@@ -127,7 +127,14 @@ export async function handleSearch({ query, k, mode, structural, regex, format,
127
127
  const header = `${r.rank}. ${r.file}:${r.startLine}-${r.endLine} (score: ${r.score.toFixed(3)}, ${r.presentation})`;
128
128
  const symbolInfo = r.symbol ? `\n ${r.symbolType || 'symbol'}: ${r.symbol}` : '';
129
129
  const code = r.code ? `\n\`\`\`\n${r.code}\n\`\`\`` : '';
130
- return header + symbolInfo + code;
130
+ // Same-file span map (top-1, windowed chunk, verdict != YES) —
131
+ // MCP phrasing references the MCP drill-in tool name.
132
+ const sameFile = (r.sameFile && r.sameFile.neighbors?.length)
133
+ ? `\n Same file: ${r.sameFile.neighbors
134
+ .map(n => `${n.name} (${n.type === 'function' ? 'fn' : (n.type || 'sym')} ${n.startLine}-${n.endLine} ${n.position})`)
135
+ .join(' · ')} — sweep: read-semantic ${r.file}`
136
+ : '';
137
+ return header + symbolInfo + code + sameFile;
131
138
  });
132
139
 
133
140
  const summaries = agentResults
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sweet-search",
3
- "version": "2.6.14",
3
+ "version": "2.6.16",
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.14",
171
- "@sweet-search/native-darwin-x64": "2.6.14",
172
- "@sweet-search/native-linux-arm64-gnu": "2.6.14",
173
- "@sweet-search/native-linux-arm64-gnu-cuda": "2.6.14",
174
- "@sweet-search/native-linux-x64-gnu": "2.6.14",
175
- "@sweet-search/native-linux-x64-gnu-cuda": "2.6.14",
176
- "@sweet-search/bg-priority": "2.6.14"
170
+ "@sweet-search/native-darwin-arm64": "2.6.16",
171
+ "@sweet-search/native-darwin-x64": "2.6.16",
172
+ "@sweet-search/native-linux-arm64-gnu": "2.6.16",
173
+ "@sweet-search/native-linux-arm64-gnu-cuda": "2.6.16",
174
+ "@sweet-search/native-linux-x64-gnu": "2.6.16",
175
+ "@sweet-search/native-linux-x64-gnu-cuda": "2.6.16",
176
+ "@sweet-search/bg-priority": "2.6.16"
177
177
  },
178
178
  "engines": {
179
179
  "node": ">=18.0.0"
@@ -46,18 +46,20 @@ function escapeRegex(s) {
46
46
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
47
47
  }
48
48
 
49
- // ─── Canonical policy body = the M++++ champion (integration seam) ───────────
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; routing unchanged),
57
- // injected VERBATIM into the harness files. Per-harness shims
58
- // (Cursor frontmatter, @imports) are applied at write-time, not embedded here.
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
+ // tool routing unchanged), injected VERBATIM into the harness files.
59
+ // Per-harness shims (Cursor frontmatter, @imports) are applied at write-time,
60
+ // not embedded here.
59
61
  //
60
- // The artifact is generated from Mpppp.md by
62
+ // The artifact is generated from Mppppp.md by
61
63
  // `core/prompt-optimization/sweep/finalize-mpp.mjs` and shipped via the
62
64
  // package.json "files" list. If it is missing we fail LOUDLY rather than
63
65
  // silently shipping a placeholder/older policy.
@@ -76,7 +78,7 @@ export function stripFrontMatter(text) {
76
78
  return text.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
77
79
  }
78
80
 
79
- function readShippedPolicy(rel = SHIP_FILE_REL, { label = 'M++++' } = {}) {
81
+ function readShippedPolicy(rel = SHIP_FILE_REL, { label = 'M+++++' } = {}) {
80
82
  const here = dirname(fileURLToPath(import.meta.url)); // <pkg>/scripts
81
83
  const shipPath = join(here, '..', rel);
82
84
  let raw;
@@ -103,7 +105,7 @@ let _mcpPolicyBody = null;
103
105
  /** Lazily read + cache the MCP-variant policy body. */
104
106
  export function getMcpPolicyBody() {
105
107
  if (_mcpPolicyBody == null) {
106
- _mcpPolicyBody = readShippedPolicy(MCP_SHIP_FILE_REL, { label: 'M++++ (MCP variant)' });
108
+ _mcpPolicyBody = readShippedPolicy(MCP_SHIP_FILE_REL, { label: 'M+++++ (MCP variant)' });
107
109
  }
108
110
  return _mcpPolicyBody;
109
111
  }