sweet-search 2.6.14 → 2.6.15
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/infrastructure/code-graph-repository.js +71 -0
- package/core/search/context-expander.js +81 -0
- package/core/search/search-read.js +108 -1
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +18 -2
- package/mcp/read-tool.js +10 -0
- package/mcp/tool-handlers.js +8 -1
- package/package.json +8 -8
|
@@ -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
|
*
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
});
|
package/mcp/tool-handlers.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
3
|
+
"version": "2.6.15",
|
|
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.15",
|
|
171
|
+
"@sweet-search/native-darwin-x64": "2.6.15",
|
|
172
|
+
"@sweet-search/native-linux-arm64-gnu": "2.6.15",
|
|
173
|
+
"@sweet-search/native-linux-arm64-gnu-cuda": "2.6.15",
|
|
174
|
+
"@sweet-search/native-linux-x64-gnu": "2.6.15",
|
|
175
|
+
"@sweet-search/native-linux-x64-gnu-cuda": "2.6.15",
|
|
176
|
+
"@sweet-search/bg-priority": "2.6.15"
|
|
177
177
|
},
|
|
178
178
|
"engines": {
|
|
179
179
|
"node": ">=18.0.0"
|