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.
- package/core/infrastructure/code-graph-repository.js +71 -0
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +15 -11
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +3 -3
- 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
- package/scripts/inject-agent-instructions.js +10 -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
|
*
|
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
variant: mcp
|
|
3
|
-
derived_from: p7-v1-
|
|
4
|
-
source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/
|
|
3
|
+
derived_from: p7-v1-mppppp
|
|
4
|
+
source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/Mppppp.md
|
|
5
5
|
benchmarked: false
|
|
6
6
|
note: >-
|
|
7
|
-
Hand-derived MCP-tool variant of the M
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
Hand-derived MCP-tool variant of the M+++++ champion (p7-v1-mppppp = 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
|
-
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.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.
|
|
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.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
|
|
49
|
+
// ─── Canonical policy body = the M+++++ champion (integration seam) ──────────
|
|
50
50
|
//
|
|
51
51
|
// Plan §10 / §3.7.1 step 13 / DDD "Integration seam": scripts/init.js is the
|
|
52
52
|
// SOLE consumer of the prompt-optimization ship-file. We read that artifact at
|
|
53
|
-
// load time and strip its YAML front-matter; the remaining body is the M
|
|
53
|
+
// load time and strip its YAML front-matter; the remaining body is the M+++++
|
|
54
54
|
// champion (PHASE7 M++ — held-out 0.988 Maximin, OOD 0.952, HOMP/SCS/counter
|
|
55
55
|
// all pass, 5-cell cross-harness validated — plus the stop-discipline scoping
|
|
56
|
-
// edits that fix M++'s over-stopping on FIX tasks
|
|
57
|
-
//
|
|
58
|
-
//
|
|
56
|
+
// edits that fix M++'s over-stopping on FIX tasks, plus the verdict-gated
|
|
57
|
+
// trust line that scans already-delivered lower ranks before any new search;
|
|
58
|
+
// 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
|
|
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
|
|
108
|
+
_mcpPolicyBody = readShippedPolicy(MCP_SHIP_FILE_REL, { label: 'M+++++ (MCP variant)' });
|
|
107
109
|
}
|
|
108
110
|
return _mcpPolicyBody;
|
|
109
111
|
}
|