sweet-search 2.6.10 → 2.6.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/graph/graph-expansion.js +48 -6
- package/core/incremental-indexing/application/production-reconciler-helpers.mjs +48 -11
- package/core/incremental-indexing/application/production-reconciler.mjs +12 -3
- package/core/infrastructure/hamming-kernel.js +293 -0
- package/core/infrastructure/maxsim.wasm +0 -0
- package/core/infrastructure/simd-distance.js +39 -6
- package/core/infrastructure/simd-distance.wasm +0 -0
- package/core/infrastructure/tombstone-bitmap-reader.js +34 -0
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt-mcp.md +17 -13
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +10 -8
- package/core/ranking/late-interaction-index.js +31 -3
- package/core/search/search-semantic.js +8 -1
- package/core/vector-store/binary-heap.js +12 -3
- package/core/vector-store/binary-hnsw-index.js +354 -107
- package/package.json +8 -8
- package/scripts/hooks/remind-tools.mjs +3 -2
- package/scripts/inject-agent-instructions.js +9 -8
|
@@ -1,14 +1,16 @@
|
|
|
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-mpppp
|
|
4
|
+
source_prompt: core/prompt-optimization/data/p7-variant-restarts/p7-gen3-candidates/Mpppp.md
|
|
5
5
|
benchmarked: false
|
|
6
6
|
note: >-
|
|
7
|
-
Hand-derived MCP-tool variant of the
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 discipline — is preserved (semantics intact; the rest
|
|
13
|
+
carries only the tool-mechanics
|
|
12
14
|
rename). Only the tool-mechanics layer is remapped: the six ss-* CLI commands collapse onto the sweet-search MCP
|
|
13
15
|
tool surface (a UNIFIED `search` that subsumes ss-search/ss-find/ss-grep, plus
|
|
14
16
|
`trace` / `read-semantic` / `read` / `repo-map`). The anti-raw-scan discipline
|
|
@@ -21,9 +23,11 @@ note: >-
|
|
|
21
23
|
---
|
|
22
24
|
# Sweet-search — code search tool guide
|
|
23
25
|
|
|
26
|
+
These are code-search rules only — they never decide when the task is done; the deliverable is whatever the task/prompt asks for. When the task calls for a change, locating the right code is a means, not the finish line: apply the edit with your normal file-editing tool and never a description of the fix in place of the fix.
|
|
27
|
+
|
|
24
28
|
Sweet-search indexes the working tree (uncommitted edits too) and searches it faster and cheaper than scanning files by hand. Use the sweet-search tools for all code search and navigation, in preference to native Grep/Read or shell `grep`/`find`/`cat`/`ls` — the index covers every file, so a manual scan only re-confirms a sweet-search result at higher cost, never beats it. Reach for a raw read only for an edit too recent to be reconciled (seconds old). Never re-run a sweet-search hit as a native Grep. Any sub-agent you delegate to must use these sweet-search tools, with this system prompt verbatim.
|
|
25
29
|
|
|
26
|
-
## Tools
|
|
30
|
+
## Tools (search tools, via the sweet-search MCP server)
|
|
27
31
|
The sweet-search MCP server exposes these (call each by the name your client lists it under):
|
|
28
32
|
- **search** — hybrid code search; returns ranked, self-contained code blocks. Your primary tool: it subsumes semantic search, exact-literal search, and semantic-reranked regex. Give it a natural-language query for a concept, or pass a `regex` (or lexical mode) for an exact token.
|
|
29
33
|
- **trace** — a symbol's callers, callees and impact in one call.
|
|
@@ -39,13 +43,13 @@ The sweet-search MCP server exposes these (call each by the name your client lis
|
|
|
39
43
|
Trust the top ranked result; confirm with at most one narrow `read`, never a re-run of a matching hit.
|
|
40
44
|
|
|
41
45
|
## Multi-file
|
|
42
|
-
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 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.
|
|
46
|
+
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.
|
|
43
47
|
|
|
44
|
-
## A confirmed absence is a complete answer
|
|
45
|
-
When what you're looking for may not exist, absence is settled once TWO complementary `search` probes come back empty for the same concept: one in natural language and one as a broad `regex` on its likeliest identifier (a short substring/prefix). A search that returns plausible-but-off-target code is the decoy, not a lead — do not chase it. Two empty index probes over the whole codebase are more conclusive than any native scan or file listing, so state the negative and stop: no third synonym, no native `grep`/`ls`/`cat` enumeration.
|
|
48
|
+
## A confirmed absence is a complete search answer
|
|
49
|
+
When what you're looking for may not exist, absence is settled once TWO complementary `search` probes come back empty for the same concept: one in natural language and one as a broad `regex` on its likeliest identifier (a short substring/prefix). A search that returns plausible-but-off-target code is the decoy, not a lead — do not chase it. Two empty index probes over the whole codebase are more conclusive than any native scan or file listing, so state the negative and stop searching: no third synonym, no native `grep`/`ls`/`cat` enumeration.
|
|
46
50
|
|
|
47
51
|
## Before the third probe
|
|
48
52
|
Before your third sweet-search probe in the current search iteration — or before your final answer, whichever comes first — output a `<state_summary>` block with exactly: (1) one sentence on what you've established, (2) one sentence on your current blind spot.
|
|
49
53
|
|
|
50
|
-
##
|
|
51
|
-
Stop 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
|
|
54
|
+
## Search output
|
|
55
|
+
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.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
run_id: p7-v1-
|
|
2
|
+
run_id: p7-v1-mpppp
|
|
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: 1175
|
|
10
10
|
scs_cwSCS: 0.931
|
|
11
11
|
scs_min_paraphrase_accuracy: 1
|
|
12
12
|
homp_family_pass: true
|
|
@@ -19,9 +19,11 @@ vault_within_15pct_of_heldout: true
|
|
|
19
19
|
---
|
|
20
20
|
# Sweet-search — code search tool guide
|
|
21
21
|
|
|
22
|
+
These are code-search rules only — they never decide when the task is done; the deliverable is whatever the task/prompt asks for. When the task calls for a change, locating the right code is a means, not the finish line: apply the edit with your normal file-editing tool and never a description of the fix in place of the fix.
|
|
23
|
+
|
|
22
24
|
Sweet-search indexes the working tree (uncommitted edits too) and searches it faster and cheaper than raw shell. Use the `ss-*` tools for all code search and navigation. Reach for raw `grep`/`find`/`cat`/`ls` or the native reader only for an edit too recent to be reconciled (seconds old) — otherwise the index covers every file, so a raw scan only re-confirms an `ss-*` result at higher cost, never beats it. Never re-run an `ss-*` hit as raw grep. Any sub-agent you delegate to must use these `ss-*` tools, with this system prompt verbatim.
|
|
23
25
|
|
|
24
|
-
## Tools (
|
|
26
|
+
## Tools (search commands, invoked via Bash)
|
|
25
27
|
`ss-search`/`ss-find` return ranked code blocks; `ss-grep` is file:line only; `ss-semantic` returns the top ranked spans in one file; `ss-trace` returns a symbol's callers, callees and impact. `ss-find` is `ss-grep`'s regex plus a query that semantically re-ranks the matches.
|
|
26
28
|
- `ss-search "<query>" [-k N]` — semantic; default when you have no exact symbol
|
|
27
29
|
- `ss-find "<query>" --regex "<regex>" [-k N]`
|
|
@@ -38,13 +40,13 @@ Sweet-search indexes the working tree (uncommitted edits too) and searches it fa
|
|
|
38
40
|
Trust the top ranked result; confirm with at most one narrow `ss-read`, never a re-run of a matching hit.
|
|
39
41
|
|
|
40
42
|
## Multi-file
|
|
41
|
-
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 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.
|
|
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.
|
|
42
44
|
|
|
43
|
-
## A confirmed absence is a complete answer
|
|
44
|
-
When what you're looking for may not exist, absence is settled once TWO complementary index probes come back empty for the same concept: one `ss-search` in natural language and one broad `ss-grep` on its likeliest identifier (a short substring/prefix). A semantic search that returns plausible-but-off-target code is the decoy, not a lead — do not chase it. Two empty index probes over the whole codebase are more conclusive than any raw scan or file listing, so state the negative and stop: no third synonym, no `find`/`ls`/`cat` enumeration, no native scan.
|
|
45
|
+
## A confirmed absence is a complete search answer
|
|
46
|
+
When what you're looking for may not exist, absence is settled once TWO complementary index probes come back empty for the same concept: one `ss-search` in natural language and one broad `ss-grep` on its likeliest identifier (a short substring/prefix). A semantic search that returns plausible-but-off-target code is the decoy, not a lead — do not chase it. Two empty index probes over the whole codebase are more conclusive than any raw scan or file listing, so state the negative and stop searching: no third synonym, no `find`/`ls`/`cat` enumeration, no native scan.
|
|
45
47
|
|
|
46
48
|
## Before the third probe
|
|
47
49
|
Before your third sweet-search probe in the current search iteration — or before your final answer, whichever comes first — output a `<state_summary>` block with exactly: (1) one sentence on what you've established, (2) one sentence on your current blind spot.
|
|
48
50
|
|
|
49
|
-
##
|
|
50
|
-
Stop 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
|
|
51
|
+
## Search output
|
|
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.
|
|
@@ -15,7 +15,7 @@ import { existsSync, createWriteStream, createReadStream, statSync } from 'fs';
|
|
|
15
15
|
import readline from 'readline';
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { DB_PATHS, LATE_INTERACTION_CONFIG } from '../infrastructure/config/index.js';
|
|
18
|
-
import { wasmMaxSimF32, wasmMaxSimDequantPerToken, wasmMaxSimDequant4Bit, nativeMaxSimBatch, nativeMaxSimBatchPerToken, nativeMaxSimBatch4Bit, initWasm, isNativeMaxSimAvailable, isNativePerTokenAvailable, isNative4BitAvailable } from '../infrastructure/simd-distance.js';
|
|
18
|
+
import { wasmMaxSimF32, wasmMaxSimDequantPerToken, wasmMaxSimDequant4Bit, wasmMaxSimPrepareQuery, nativeMaxSimBatch, nativeMaxSimBatchPerToken, nativeMaxSimBatch4Bit, initWasm, isNativeMaxSimAvailable, isNativePerTokenAvailable, isNative4BitAvailable } from '../infrastructure/simd-distance.js';
|
|
19
19
|
import { fastRotate, generateSignVector, calibrateWUSH, wushRotate } from '../infrastructure/quantization.js';
|
|
20
20
|
import { poolTokens } from './late-interaction-model.js';
|
|
21
21
|
import { loadBitmap, isSet } from '../infrastructure/tombstone-bitmap-reader.js';
|
|
@@ -1281,6 +1281,21 @@ export class LateInteractionIndex {
|
|
|
1281
1281
|
}
|
|
1282
1282
|
|
|
1283
1283
|
_loadSegmentStaleBitmap(segmentPath) {
|
|
1284
|
+
// Per-query memo: scoreWithLateInteraction checks the same segment 2-3×
|
|
1285
|
+
// per candidate, and each uncached check costs a statSync. Within one
|
|
1286
|
+
// scoring pass a single freshness check per segment is equivalent — a
|
|
1287
|
+
// tombstone landing mid-pass races identically either way.
|
|
1288
|
+
const memo = this._staleQueryMemo;
|
|
1289
|
+
if (memo) {
|
|
1290
|
+
if (memo.has(segmentPath)) return memo.get(segmentPath);
|
|
1291
|
+
const bitmap = this._loadSegmentStaleBitmapUncached(segmentPath);
|
|
1292
|
+
memo.set(segmentPath, bitmap);
|
|
1293
|
+
return bitmap;
|
|
1294
|
+
}
|
|
1295
|
+
return this._loadSegmentStaleBitmapUncached(segmentPath);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
_loadSegmentStaleBitmapUncached(segmentPath) {
|
|
1284
1299
|
const sidecarPath = segmentPath + '.stale.bin';
|
|
1285
1300
|
let stat;
|
|
1286
1301
|
try {
|
|
@@ -1581,6 +1596,12 @@ export class LateInteractionIndex {
|
|
|
1581
1596
|
// don't support importance weighting, so we must use the JS-tier weighted path.
|
|
1582
1597
|
const nativeScored = new Set();
|
|
1583
1598
|
|
|
1599
|
+
// One tombstone freshness check (statSync) per segment for this scoring
|
|
1600
|
+
// pass; cleared after the synchronous scoring loops below. An exception
|
|
1601
|
+
// path can leave it set — the next pass overwrites it, and staleness is
|
|
1602
|
+
// bounded by one scoring pass either way.
|
|
1603
|
+
this._staleQueryMemo = new Map();
|
|
1604
|
+
|
|
1584
1605
|
// Resolve a doc-lookup ID for each candidate. Graph-expanded candidates
|
|
1585
1606
|
// carry `_liChunkId` (a chunk id pointing into the LI index) while their
|
|
1586
1607
|
// public `id` is the entity id from the code graph. Honouring _liChunkId
|
|
@@ -1632,6 +1653,11 @@ export class LateInteractionIndex {
|
|
|
1632
1653
|
|
|
1633
1654
|
// Tier 2 & 3: WASM fused dequant or JS fallback for candidates not scored natively.
|
|
1634
1655
|
// Try WASM fused kernels first (avoids JS-side dequant), fall back to JS dequant + wasmMaxSimF32.
|
|
1656
|
+
// Stage the query bytes in WASM memory once — per-candidate calls below
|
|
1657
|
+
// skip the redundant Q×dim×4 query memcpy when the session id matches.
|
|
1658
|
+
const wasmQuerySession = (useFlatPath && !this.useTokenWeights)
|
|
1659
|
+
? wasmMaxSimPrepareQuery(queryFlat, effectiveQueryTokens.length, scoringDim)
|
|
1660
|
+
: 0;
|
|
1635
1661
|
for (const candidate of toScore) {
|
|
1636
1662
|
if (nativeScored.has(candidate.id)) continue;
|
|
1637
1663
|
const docId = lookupDocIdOf(candidate);
|
|
@@ -1649,7 +1675,7 @@ export class LateInteractionIndex {
|
|
|
1649
1675
|
if (doc.quantBits === 4 && doc.minArray && doc.tokenNorms) {
|
|
1650
1676
|
const wasmScore = wasmMaxSimDequant4Bit(
|
|
1651
1677
|
queryFlat, doc.tokens, doc.minArray, doc.scaleArray, doc.tokenNorms,
|
|
1652
|
-
effectiveQueryTokens.length, doc.numTokens, doc.dim,
|
|
1678
|
+
effectiveQueryTokens.length, doc.numTokens, doc.dim, wasmQuerySession,
|
|
1653
1679
|
);
|
|
1654
1680
|
if (wasmScore !== null) { pushScored(candidate, wasmScore); continue; }
|
|
1655
1681
|
}
|
|
@@ -1658,7 +1684,7 @@ export class LateInteractionIndex {
|
|
|
1658
1684
|
if (doc.minArray && doc.tokenNorms && doc.quantBits !== 4) {
|
|
1659
1685
|
const wasmScore = wasmMaxSimDequantPerToken(
|
|
1660
1686
|
queryFlat, doc.tokens, doc.minArray, doc.scaleArray, doc.tokenNorms,
|
|
1661
|
-
effectiveQueryTokens.length, doc.numTokens, doc.dim,
|
|
1687
|
+
effectiveQueryTokens.length, doc.numTokens, doc.dim, wasmQuerySession,
|
|
1662
1688
|
);
|
|
1663
1689
|
if (wasmScore !== null) { pushScored(candidate, wasmScore); continue; }
|
|
1664
1690
|
}
|
|
@@ -1693,6 +1719,8 @@ export class LateInteractionIndex {
|
|
|
1693
1719
|
}
|
|
1694
1720
|
}
|
|
1695
1721
|
|
|
1722
|
+
this._staleQueryMemo = null;
|
|
1723
|
+
|
|
1696
1724
|
for (const candidate of pruned) pushFallback(candidate, { _pruned: true });
|
|
1697
1725
|
|
|
1698
1726
|
scored.sort((a, b) => b.lateInteractionScore - a.lateInteractionScore);
|
|
@@ -302,8 +302,15 @@ export async function semanticSearch3Stage(query, options = {}) {
|
|
|
302
302
|
const validIndices = [];
|
|
303
303
|
let missingInt8Count = 0;
|
|
304
304
|
|
|
305
|
+
// One stale-bitmap snapshot for the whole pool (getInt8Vector would stat
|
|
306
|
+
// the bitmap file once per candidate). Falls back per-id for injected
|
|
307
|
+
// index doubles without the batch method.
|
|
308
|
+
const poolIds = stage2Candidates.map((c) => c.id);
|
|
309
|
+
const poolInt8 = typeof this.binaryHnswIndex.getInt8VectorsForIds === 'function'
|
|
310
|
+
? this.binaryHnswIndex.getInt8VectorsForIds(poolIds)
|
|
311
|
+
: poolIds.map((id) => this.binaryHnswIndex.getInt8Vector(id));
|
|
305
312
|
for (let i = 0; i < stage2Candidates.length; i++) {
|
|
306
|
-
const int8Vector =
|
|
313
|
+
const int8Vector = poolInt8[i];
|
|
307
314
|
if (int8Vector) {
|
|
308
315
|
int8Vectors.push(int8Vector);
|
|
309
316
|
validIndices.push(i);
|
|
@@ -164,11 +164,20 @@ export class TypedMaxHeap {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// Drain heap into sorted
|
|
167
|
+
// Drain heap into sorted order (ascending by distance). Destructive.
|
|
168
|
+
// The returned keys/vals are POOLED buffers reused by the next drain —
|
|
169
|
+
// callers must consume (or copy) them before draining again. Both call
|
|
170
|
+
// sites (searchLayer/searchLayerQuery) box the entries immediately, so
|
|
171
|
+
// this removes two typed-array allocations per query / per insert-level.
|
|
168
172
|
drainSorted() {
|
|
169
173
|
const n = this.size;
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
if (!this._drainKeys || this._drainKeys.length < n) {
|
|
175
|
+
const cap = Math.max(64, n);
|
|
176
|
+
this._drainKeys = new Uint32Array(cap);
|
|
177
|
+
this._drainVals = new Uint32Array(cap);
|
|
178
|
+
}
|
|
179
|
+
const resultKeys = this._drainKeys;
|
|
180
|
+
const resultVals = this._drainVals;
|
|
172
181
|
// Extract max repeatedly → fills from end → ascending order
|
|
173
182
|
for (let i = n - 1; i >= 0; i--) {
|
|
174
183
|
resultKeys[i] = this.keys[0];
|