sweet-search 2.5.2 → 2.5.4
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/cli.js +24 -3
- package/core/graph/graph-expansion.js +215 -36
- package/core/graph/graph-extractor.js +196 -11
- package/core/graph/graph-search.js +395 -92
- package/core/graph/hcgs-generator.js +2 -1
- package/core/graph/index.js +2 -0
- package/core/graph/repo-map.js +28 -6
- package/core/graph/structural-answer-cues.js +168 -0
- package/core/graph/structural-callsite-hints.js +40 -0
- package/core/graph/structural-context-format.js +40 -0
- package/core/graph/structural-context.js +450 -0
- package/core/graph/structural-forward-push.js +156 -0
- package/core/graph/structural-header-context.js +19 -0
- package/core/graph/structural-importance.js +148 -0
- package/core/graph/structural-pagerank.js +197 -0
- package/core/graph/summary-manager.js +13 -9
- package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
- package/core/incremental-indexing/application/file-watcher.mjs +197 -0
- package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
- package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
- package/core/incremental-indexing/application/operator-cli.mjs +554 -0
- package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
- package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
- package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
- package/core/incremental-indexing/application/reconciler.mjs +477 -0
- package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
- package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
- package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
- package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
- package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
- package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
- package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
- package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
- package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
- package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
- package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
- package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
- package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
- package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
- package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
- package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
- package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
- package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
- package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
- package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
- package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
- package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
- package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
- package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
- package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
- package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
- package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
- package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
- package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
- package/core/indexing/admission-policy.js +139 -0
- package/core/indexing/artifact-builder.js +29 -12
- package/core/indexing/ast-chunker.js +107 -30
- package/core/indexing/dedup/exemplar-selector.js +19 -1
- package/core/indexing/gitignore-filter.js +223 -0
- package/core/indexing/incremental-tracker.js +99 -30
- package/core/indexing/index-codebase-v21.js +6 -5
- package/core/indexing/index-maintainer.mjs +698 -6
- package/core/indexing/indexer-ann.js +99 -15
- package/core/indexing/indexer-build.js +158 -45
- package/core/indexing/indexer-empty-baseline.js +80 -0
- package/core/indexing/indexer-manifest.js +66 -0
- package/core/indexing/indexer-phases.js +56 -23
- package/core/indexing/indexer-sparse-gram.js +54 -13
- package/core/indexing/indexer-utils.js +26 -208
- package/core/indexing/indexing-file-policy.js +32 -7
- package/core/indexing/maintainer-launcher.mjs +137 -0
- package/core/indexing/merkle-tracker.js +251 -244
- package/core/indexing/model-pool.js +46 -5
- package/core/infrastructure/code-graph-repository.js +758 -6
- package/core/infrastructure/code-graph-visibility.js +157 -0
- package/core/infrastructure/codebase-repository.js +100 -13
- package/core/infrastructure/config/search.js +1 -1
- package/core/infrastructure/db-utils.js +118 -0
- package/core/infrastructure/dedup-hashing.js +10 -13
- package/core/infrastructure/hardware-capability.js +17 -7
- package/core/infrastructure/index.js +8 -2
- package/core/infrastructure/language-patterns/maps.js +4 -1
- package/core/infrastructure/language-patterns/registry-core.js +56 -17
- package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
- package/core/infrastructure/language-patterns.js +69 -0
- package/core/infrastructure/model-registry.js +20 -0
- package/core/infrastructure/native-inference.js +7 -12
- package/core/infrastructure/native-resolver.js +52 -37
- package/core/infrastructure/native-sparse-gram.js +261 -20
- package/core/infrastructure/native-tokenizer.js +6 -15
- package/core/infrastructure/simd-distance.js +10 -16
- package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
- package/core/infrastructure/structural-alias-resolver.js +122 -0
- package/core/infrastructure/structural-candidate-ranker.js +34 -0
- package/core/infrastructure/structural-context-repository.js +472 -0
- package/core/infrastructure/structural-context-utils.js +51 -0
- package/core/infrastructure/structural-graph-signals.js +121 -0
- package/core/infrastructure/structural-qualified-resolution.js +15 -0
- package/core/infrastructure/structural-source-definitions.js +100 -0
- package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
- package/core/infrastructure/tree-sitter-provider.js +811 -37
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
- package/core/query/query-router.js +55 -5
- package/core/ranking/file-kind-ranking.js +2192 -15
- package/core/ranking/late-interaction-index.js +87 -12
- package/core/search/cli-decoration.js +290 -0
- package/core/search/context-expander.js +988 -78
- package/core/search/index.js +1 -0
- package/core/search/output-policy.js +275 -0
- package/core/search/search-anchor.js +499 -0
- package/core/search/search-boost.js +93 -1
- package/core/search/search-cli.js +61 -204
- package/core/search/search-hybrid.js +250 -10
- package/core/search/search-pattern-chunks.js +57 -8
- package/core/search/search-pattern-planner.js +68 -9
- package/core/search/search-pattern-prefilter.js +30 -10
- package/core/search/search-pattern-ripgrep.js +40 -4
- package/core/search/search-pattern-sparse-overlay.js +256 -0
- package/core/search/search-pattern.js +117 -29
- package/core/search/search-postprocess.js +479 -5
- package/core/search/search-read-semantic.js +260 -23
- package/core/search/search-read.js +82 -64
- package/core/search/search-reader-pin.js +71 -0
- package/core/search/search-rrf.js +279 -0
- package/core/search/search-semantic.js +110 -5
- package/core/search/search-server.js +130 -57
- package/core/search/search-trace.js +107 -0
- package/core/search/server-identity.js +93 -0
- package/core/search/session-daemon-prewarm.mjs +33 -10
- package/core/search/sweet-search.js +399 -7
- package/core/skills/sweet-index/SKILL.md +8 -6
- package/core/vector-store/binary-hnsw-index.js +194 -30
- package/core/vector-store/float-vector-store.js +96 -6
- package/core/vector-store/hnsw-index.js +220 -49
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
- package/eval/agent-read-workflows/bin/ss-find +15 -0
- package/eval/agent-read-workflows/bin/ss-grep +12 -0
- package/eval/agent-read-workflows/bin/ss-read +14 -0
- package/eval/agent-read-workflows/bin/ss-search +18 -0
- package/eval/agent-read-workflows/bin/ss-semantic +12 -0
- package/eval/agent-read-workflows/bin/ss-trace +11 -0
- package/mcp/read-tool.js +109 -0
- package/mcp/server.js +55 -15
- package/mcp/tool-handlers.js +14 -124
- package/mcp/trace-tool.js +81 -0
- package/package.json +25 -10
- package/scripts/hooks/intercept-read.mjs +55 -0
- package/scripts/hooks/remind-tools.mjs +40 -0
- package/scripts/init.js +698 -54
- package/scripts/inject-agent-instructions.js +431 -0
- package/scripts/install-prompt-reminders.js +188 -0
- package/scripts/install-tool-enforcement.js +220 -0
- package/scripts/smoke-test.js +12 -9
- package/scripts/uninstall.js +276 -18
- package/scripts/write-claude-rules.js +110 -0
|
@@ -11,12 +11,13 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import fs from 'fs/promises';
|
|
14
|
-
import { existsSync, createWriteStream, createReadStream } from 'fs';
|
|
14
|
+
import { existsSync, createWriteStream, createReadStream, statSync } from 'fs';
|
|
15
15
|
import path from 'path';
|
|
16
16
|
import { DB_PATHS, LATE_INTERACTION_CONFIG } from '../infrastructure/config/index.js';
|
|
17
17
|
import { wasmMaxSimF32, wasmMaxSimDequantPerToken, wasmMaxSimDequant4Bit, nativeMaxSimBatch, nativeMaxSimBatchPerToken, nativeMaxSimBatch4Bit, initWasm, isNativeMaxSimAvailable, isNativePerTokenAvailable, isNative4BitAvailable } from '../infrastructure/simd-distance.js';
|
|
18
18
|
import { fastRotate, generateSignVector, calibrateWUSH, wushRotate } from '../infrastructure/quantization.js';
|
|
19
19
|
import { poolTokens } from './late-interaction-model.js';
|
|
20
|
+
import { loadBitmap, isSet } from '../infrastructure/tombstone-bitmap-reader.js';
|
|
20
21
|
|
|
21
22
|
// =============================================================================
|
|
22
23
|
// CRC32 (IEEE 802.3 polynomial, used for SSLX segment footer checksum)
|
|
@@ -373,6 +374,8 @@ export class LateInteractionIndex {
|
|
|
373
374
|
this._segments = []; // { path, count } of flushed segments
|
|
374
375
|
this._segmentDir = null;
|
|
375
376
|
this._segmentSize = options.segmentSize || LI_SEGMENT_SIZE;
|
|
377
|
+
this._docSegmentPositions = new Map(); // doc id -> { segmentPath, docIndex }
|
|
378
|
+
this._staleBitmapCache = new Map(); // segment path -> { mtimeMs, size, bitmap }
|
|
376
379
|
}
|
|
377
380
|
|
|
378
381
|
/**
|
|
@@ -899,7 +902,12 @@ export class LateInteractionIndex {
|
|
|
899
902
|
}
|
|
900
903
|
const entries = [];
|
|
901
904
|
for (const [aliasId, ptr] of this.aliasPointers) {
|
|
902
|
-
entries.push({
|
|
905
|
+
entries.push({
|
|
906
|
+
aliasId,
|
|
907
|
+
exemplarId: ptr.exemplarId,
|
|
908
|
+
clusterId: ptr.clusterId,
|
|
909
|
+
metadata: ptr.metadata || {},
|
|
910
|
+
});
|
|
903
911
|
}
|
|
904
912
|
const payload = { version: 1, count: entries.length, aliases: entries };
|
|
905
913
|
await fs.writeFile(this._aliasSidecarPath(indexPath), JSON.stringify(payload));
|
|
@@ -913,13 +921,13 @@ export class LateInteractionIndex {
|
|
|
913
921
|
const payload = JSON.parse(raw);
|
|
914
922
|
if (!payload || !Array.isArray(payload.aliases)) return;
|
|
915
923
|
this.aliasPointers.clear();
|
|
916
|
-
for (const { aliasId, exemplarId, clusterId } of payload.aliases) {
|
|
924
|
+
for (const { aliasId, exemplarId, clusterId, metadata } of payload.aliases) {
|
|
917
925
|
// Orphan guard: drop aliases whose exemplar is no longer in documents.
|
|
918
926
|
// Happens if the file containing the exemplar was removed between
|
|
919
927
|
// save and load (incremental re-index removed the exemplar file
|
|
920
928
|
// but did not re-run dedup over the alias files).
|
|
921
929
|
if (!this.documents.has(exemplarId)) continue;
|
|
922
|
-
this.aliasPointers.set(aliasId, { exemplarId, clusterId, metadata: {} });
|
|
930
|
+
this.aliasPointers.set(aliasId, { exemplarId, clusterId, metadata: metadata || {} });
|
|
923
931
|
}
|
|
924
932
|
} catch (_e) {
|
|
925
933
|
// Malformed sidecar — treat as absent; aliases will be skipped at query time.
|
|
@@ -928,6 +936,7 @@ export class LateInteractionIndex {
|
|
|
928
936
|
|
|
929
937
|
getTokens(id) {
|
|
930
938
|
const resolved = this._resolveForRead(id);
|
|
939
|
+
if (this.isDocumentTombstoned(resolved)) return null;
|
|
931
940
|
const doc = this.documents.get(resolved);
|
|
932
941
|
if (!doc) return null;
|
|
933
942
|
|
|
@@ -964,6 +973,7 @@ export class LateInteractionIndex {
|
|
|
964
973
|
*/
|
|
965
974
|
getTokensFlat(id) {
|
|
966
975
|
const resolved = this._resolveForRead(id);
|
|
976
|
+
if (this.isDocumentTombstoned(resolved)) return null;
|
|
967
977
|
const doc = this.documents.get(resolved);
|
|
968
978
|
if (!doc) return null;
|
|
969
979
|
|
|
@@ -1156,6 +1166,49 @@ export class LateInteractionIndex {
|
|
|
1156
1166
|
return totalScore / effectiveQuery.length;
|
|
1157
1167
|
}
|
|
1158
1168
|
|
|
1169
|
+
_loadSegmentStaleBitmap(segmentPath) {
|
|
1170
|
+
const sidecarPath = segmentPath + '.stale.bin';
|
|
1171
|
+
let stat;
|
|
1172
|
+
try {
|
|
1173
|
+
stat = statSync(sidecarPath, { bigint: true });
|
|
1174
|
+
} catch {
|
|
1175
|
+
this._staleBitmapCache.delete(segmentPath);
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
const statKey = `${stat.mtimeNs}:${stat.ctimeNs}:${stat.size}`;
|
|
1179
|
+
|
|
1180
|
+
const cached = this._staleBitmapCache.get(segmentPath);
|
|
1181
|
+
if (cached && cached.statKey === statKey) {
|
|
1182
|
+
return cached.bitmap;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
const bitmap = loadBitmap(sidecarPath);
|
|
1187
|
+
this._staleBitmapCache.set(segmentPath, {
|
|
1188
|
+
statKey,
|
|
1189
|
+
bitmap,
|
|
1190
|
+
});
|
|
1191
|
+
return bitmap;
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
if (process.env.SWEET_DEBUG) {
|
|
1194
|
+
console.debug(`[LateInteraction] ignoring unreadable stale bitmap ${sidecarPath}: ${err.message}`);
|
|
1195
|
+
}
|
|
1196
|
+
this._staleBitmapCache.set(segmentPath, {
|
|
1197
|
+
statKey,
|
|
1198
|
+
bitmap: null,
|
|
1199
|
+
});
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
isDocumentTombstoned(docId) {
|
|
1205
|
+
if (!docId) return false;
|
|
1206
|
+
const position = this._docSegmentPositions.get(docId);
|
|
1207
|
+
if (!position) return false;
|
|
1208
|
+
const bitmap = this._loadSegmentStaleBitmap(position.segmentPath);
|
|
1209
|
+
return bitmap ? isSet(bitmap, position.docIndex) : false;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1159
1212
|
/**
|
|
1160
1213
|
* MaxSim scoring from flat buffers (avoids reshape/sub-array creation).
|
|
1161
1214
|
*
|
|
@@ -1419,11 +1472,14 @@ export class LateInteractionIndex {
|
|
|
1419
1472
|
// public `id` is the entity id from the code graph. Honouring _liChunkId
|
|
1420
1473
|
// lets expanded candidates participate in MaxSim rerank.
|
|
1421
1474
|
const docIdOf = (c) => c._liChunkId || c.id;
|
|
1475
|
+
const lookupDocIdOf = (c) => this._resolveForRead(docIdOf(c));
|
|
1422
1476
|
|
|
1423
1477
|
if (useFlatPath && !this.useTokenWeights) {
|
|
1424
1478
|
const groups = { bit4: [], perToken: [], perDoc: [] };
|
|
1425
1479
|
for (const candidate of toScore) {
|
|
1426
|
-
const
|
|
1480
|
+
const lookupDocId = lookupDocIdOf(candidate);
|
|
1481
|
+
if (this.isDocumentTombstoned(lookupDocId)) continue;
|
|
1482
|
+
const doc = this.documents.get(lookupDocId);
|
|
1427
1483
|
if (!doc) continue;
|
|
1428
1484
|
if (doc.quantBits === 4 && doc.minArray && doc.tokenNorms) {
|
|
1429
1485
|
groups.bit4.push({ candidate, doc });
|
|
@@ -1444,6 +1500,11 @@ export class LateInteractionIndex {
|
|
|
1444
1500
|
const scores = scoreFn(queryFlat, effectiveQueryTokens.length, scoringDim, nativeCands);
|
|
1445
1501
|
if (scores) {
|
|
1446
1502
|
for (let i = 0; i < group.length; i++) {
|
|
1503
|
+
if (this.isDocumentTombstoned(lookupDocIdOf(group[i].candidate))) {
|
|
1504
|
+
pushFallback(group[i].candidate, { _liTombstoned: true });
|
|
1505
|
+
nativeScored.add(group[i].candidate.id);
|
|
1506
|
+
continue;
|
|
1507
|
+
}
|
|
1447
1508
|
pushScored(group[i].candidate, scores[i]);
|
|
1448
1509
|
nativeScored.add(group[i].candidate.id);
|
|
1449
1510
|
}
|
|
@@ -1459,7 +1520,12 @@ export class LateInteractionIndex {
|
|
|
1459
1520
|
// Try WASM fused kernels first (avoids JS-side dequant), fall back to JS dequant + wasmMaxSimF32.
|
|
1460
1521
|
for (const candidate of toScore) {
|
|
1461
1522
|
if (nativeScored.has(candidate.id)) continue;
|
|
1462
|
-
const
|
|
1523
|
+
const docId = lookupDocIdOf(candidate);
|
|
1524
|
+
if (this.isDocumentTombstoned(docId)) {
|
|
1525
|
+
pushFallback(candidate, { _liTombstoned: true });
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
const doc = this.documents.get(docId);
|
|
1463
1529
|
if (!doc) { pushFallback(candidate); continue; }
|
|
1464
1530
|
|
|
1465
1531
|
if (useFlatPath) {
|
|
@@ -1494,7 +1560,7 @@ export class LateInteractionIndex {
|
|
|
1494
1560
|
}
|
|
1495
1561
|
|
|
1496
1562
|
// JS dequant → WASM f32 or JS fallback
|
|
1497
|
-
const flatData = this.getTokensFlat(
|
|
1563
|
+
const flatData = this.getTokensFlat(docId);
|
|
1498
1564
|
if (flatData) {
|
|
1499
1565
|
pushScored(candidate, this.maxSimScoreFlat(
|
|
1500
1566
|
effectiveQueryTokens, flatData.flat, flatData.numTokens, flatData.dim,
|
|
@@ -1504,7 +1570,7 @@ export class LateInteractionIndex {
|
|
|
1504
1570
|
pushFallback(candidate);
|
|
1505
1571
|
}
|
|
1506
1572
|
} else {
|
|
1507
|
-
const docTokens = this.getTokens(
|
|
1573
|
+
const docTokens = this.getTokens(docId);
|
|
1508
1574
|
if (docTokens) {
|
|
1509
1575
|
pushScored(candidate, this.maxSimScore(effectiveQueryTokens, docTokens, pruneOpts));
|
|
1510
1576
|
} else {
|
|
@@ -1997,14 +2063,21 @@ export class LateInteractionIndex {
|
|
|
1997
2063
|
if (manifest.modelId) this.modelId = manifest.modelId;
|
|
1998
2064
|
|
|
1999
2065
|
this.documents.clear();
|
|
2066
|
+
this._docSegmentPositions.clear();
|
|
2067
|
+
this._staleBitmapCache.clear();
|
|
2000
2068
|
|
|
2001
2069
|
const isSSLX = manifest.format === 'sslx-v3';
|
|
2002
2070
|
|
|
2003
2071
|
for (const seg of manifest.segments) {
|
|
2004
2072
|
const segPath = path.join(segmentDir, seg.path);
|
|
2005
2073
|
const docs = await this._readSegmentFile(segPath);
|
|
2074
|
+
const staleBitmap = this._loadSegmentStaleBitmap(segPath);
|
|
2006
2075
|
|
|
2007
|
-
for (
|
|
2076
|
+
for (let docIndex = 0; docIndex < docs.length; docIndex++) {
|
|
2077
|
+
const doc = docs[docIndex];
|
|
2078
|
+
if (staleBitmap && isSet(staleBitmap, docIndex)) {
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2008
2081
|
// SSLX reader returns typed arrays directly; legacy LISE returns plain arrays
|
|
2009
2082
|
const tokens = (doc.tokens instanceof Int8Array || doc.tokens instanceof Float32Array || doc.tokens instanceof Uint8Array)
|
|
2010
2083
|
? doc.tokens
|
|
@@ -2033,6 +2106,7 @@ export class LateInteractionIndex {
|
|
|
2033
2106
|
if (doc.preNorms) entry.preNorms = doc.preNorms;
|
|
2034
2107
|
|
|
2035
2108
|
this.documents.set(doc.id, entry);
|
|
2109
|
+
this._docSegmentPositions.set(doc.id, { segmentPath: segPath, docIndex });
|
|
2036
2110
|
}
|
|
2037
2111
|
}
|
|
2038
2112
|
|
|
@@ -2273,9 +2347,10 @@ export class LateInteractionIndex {
|
|
|
2273
2347
|
hasTokens(chunkIds) {
|
|
2274
2348
|
const available = new Set();
|
|
2275
2349
|
for (const id of chunkIds) {
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
if (
|
|
2350
|
+
const resolved = this._resolveForRead(id);
|
|
2351
|
+
if (!this.documents.has(resolved)) continue;
|
|
2352
|
+
if (this.isDocumentTombstoned(resolved)) continue;
|
|
2353
|
+
available.add(id);
|
|
2279
2354
|
}
|
|
2280
2355
|
return available;
|
|
2281
2356
|
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI decoration rendering for sweet-search tools.
|
|
3
|
+
*
|
|
4
|
+
* Pure presentation: ANSI styling, the pixel-art banner, the stats line, and a
|
|
5
|
+
* compact per-tool identity line. WHERE these may be emitted is decided by
|
|
6
|
+
* output-policy.js — this module only renders to the channel the policy chose.
|
|
7
|
+
*
|
|
8
|
+
* Kept dependency-light (only output-policy + node:fs) so the read/trace/
|
|
9
|
+
* semantic tools can import it without pulling the search engine.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createDecorationWriter, detectOutputPolicy } from './output-policy.js';
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// CLI STYLING (ANSI truecolor w/ fallback)
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
export const STYLE = (() => {
|
|
19
|
+
// 5 shades of dark blue (edge -> center)
|
|
20
|
+
const colors = {
|
|
21
|
+
darkest: { r: 6, g: 10, b: 31 },
|
|
22
|
+
darker: { r: 10, g: 17, b: 52 },
|
|
23
|
+
dark: { r: 14, g: 24, b: 73 },
|
|
24
|
+
lightDark: { r: 18, g: 32, b: 95 },
|
|
25
|
+
lightestDark: { r: 22, g: 40, b: 116 },
|
|
26
|
+
border: { r: 90, g: 115, b: 220 },
|
|
27
|
+
white: { r: 255, g: 255, b: 255 },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const reset = '\x1b[0m';
|
|
31
|
+
const bold = '\x1b[1m';
|
|
32
|
+
|
|
33
|
+
const lerp = (c1, c2, t) => ({
|
|
34
|
+
r: Math.round(c1.r + (c2.r - c1.r) * t),
|
|
35
|
+
g: Math.round(c1.g + (c2.g - c1.g) * t),
|
|
36
|
+
b: Math.round(c1.b + (c2.b - c1.b) * t),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Convert RGB to xterm-256 color code (fallback for terminals without truecolor)
|
|
40
|
+
const rgbToAnsi256 = (r, g, b) => {
|
|
41
|
+
// Grayscale range
|
|
42
|
+
if (r === g && g === b) {
|
|
43
|
+
if (r < 8) return 16;
|
|
44
|
+
if (r > 248) return 231;
|
|
45
|
+
return Math.round(((r - 8) / 247) * 24) + 232;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const to6 = (v) => Math.round((v / 255) * 5);
|
|
49
|
+
const rr = to6(r);
|
|
50
|
+
const gg = to6(g);
|
|
51
|
+
const bb = to6(b);
|
|
52
|
+
return 16 + (36 * rr) + (6 * gg) + bb;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const fg24 = (c) => `\x1b[38;2;${c.r};${c.g};${c.b}m`;
|
|
56
|
+
const bg24 = (c) => `\x1b[48;2;${c.r};${c.g};${c.b}m`;
|
|
57
|
+
|
|
58
|
+
const fg256 = (c) => `\x1b[38;5;${rgbToAnsi256(c.r, c.g, c.b)}m`;
|
|
59
|
+
const bg256 = (c) => `\x1b[48;5;${rgbToAnsi256(c.r, c.g, c.b)}m`;
|
|
60
|
+
|
|
61
|
+
const detectColorMode = () => {
|
|
62
|
+
const forced = (process.env.SWEET_SEARCH_COLOR_MODE || process.env.SMART_SEARCH_COLOR_MODE || '').trim().toLowerCase();
|
|
63
|
+
if (forced === 'none' || forced === '0' || forced === 'off') return 'none';
|
|
64
|
+
if (forced === '256' || forced === 'ansi256' || forced === 'xterm256') return 'ansi256';
|
|
65
|
+
if (forced === 'truecolor' || forced === '24bit' || forced === 'rgb') return 'truecolor';
|
|
66
|
+
|
|
67
|
+
if (process.env.NO_COLOR) return 'none';
|
|
68
|
+
|
|
69
|
+
const colorterm = process.env.COLORTERM || '';
|
|
70
|
+
if (/truecolor|24bit/i.test(colorterm)) return 'truecolor';
|
|
71
|
+
|
|
72
|
+
// Windows Terminal + VS Code terminals are typically truecolor-capable.
|
|
73
|
+
if (process.env.WT_SESSION || process.env.TERM_PROGRAM === 'vscode') return 'truecolor';
|
|
74
|
+
|
|
75
|
+
const term = process.env.TERM || '';
|
|
76
|
+
if (/256color/i.test(term)) return 'ansi256';
|
|
77
|
+
|
|
78
|
+
return 'none';
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const colorMode = detectColorMode(); // 'truecolor' | 'ansi256' | 'none'
|
|
82
|
+
const fg = colorMode === 'truecolor' ? fg24 : colorMode === 'ansi256' ? fg256 : () => '';
|
|
83
|
+
const bg = colorMode === 'truecolor' ? bg24 : colorMode === 'ansi256' ? bg256 : () => '';
|
|
84
|
+
|
|
85
|
+
const headerStyleEnv = (process.env.SWEET_SEARCH_HEADER_STYLE || process.env.SMART_SEARCH_HEADER_STYLE || '').trim().toLowerCase();
|
|
86
|
+
const headerStyle =
|
|
87
|
+
headerStyleEnv === 'zones' || headerStyleEnv === 'gradient'
|
|
88
|
+
? headerStyleEnv
|
|
89
|
+
: (colorMode === 'truecolor' ? 'gradient' : 'zones');
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
colors,
|
|
93
|
+
fg,
|
|
94
|
+
bg,
|
|
95
|
+
reset: colorMode === 'none' ? '' : reset,
|
|
96
|
+
bold: colorMode === 'none' ? '' : bold,
|
|
97
|
+
lerp,
|
|
98
|
+
colorMode,
|
|
99
|
+
headerStyle,
|
|
100
|
+
};
|
|
101
|
+
})();
|
|
102
|
+
|
|
103
|
+
// Colorless style — same geometry as STYLE but emits no ANSI escapes. Used when
|
|
104
|
+
// the output policy keeps the banner but disables color (e.g. NO_COLOR set, or a
|
|
105
|
+
// captured /dev/tty side-channel where we still want plain pixel art).
|
|
106
|
+
export const PLAIN_STYLE = {
|
|
107
|
+
colors: STYLE.colors,
|
|
108
|
+
fg: () => '',
|
|
109
|
+
bg: () => '',
|
|
110
|
+
reset: '',
|
|
111
|
+
bold: '',
|
|
112
|
+
lerp: STYLE.lerp,
|
|
113
|
+
colorMode: 'none',
|
|
114
|
+
headerStyle: STYLE.headerStyle,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Default line writer for decoration when no policy writer is supplied: stdout.
|
|
118
|
+
const _stdoutLine = (line = '') => process.stdout.write(`${line}\n`);
|
|
119
|
+
|
|
120
|
+
// 2-line pixel art using half-blocks - SWEET SEARCH
|
|
121
|
+
const SWEET_SEARCH_L1 = '█▀▀ █ █ █ █▀▀ █▀▀ ▀█▀ █▀▀ █▀▀ ▄▀▄ █▀▄ █▀▀ █▄█';
|
|
122
|
+
const SWEET_SEARCH_L2 = '▄▄█ ▀▄█▄▀ ██▄ ██▄ █ ▄▄█ ██▄ █▀█ ██▄ █▄▄ █▀█';
|
|
123
|
+
|
|
124
|
+
// Per-tool glyphs so each of the six tools is recognizable at a glance.
|
|
125
|
+
const TOOL_ICONS = {
|
|
126
|
+
search: '✨',
|
|
127
|
+
lexical: '⚡',
|
|
128
|
+
semantic: '🧠',
|
|
129
|
+
hybrid: '⚗️',
|
|
130
|
+
pattern: '⌗',
|
|
131
|
+
grep: '#',
|
|
132
|
+
structural: '🔗',
|
|
133
|
+
trace: '🔗',
|
|
134
|
+
read: '📄',
|
|
135
|
+
'read-semantic': '🧠',
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Print styled header - 2-line pixel art with query on right = 2 content lines.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} query
|
|
142
|
+
* @param {Object} [opts]
|
|
143
|
+
* @param {(line?: string) => void} [opts.write] line writer (default: stdout)
|
|
144
|
+
* @param {Object} [opts.style] STYLE or PLAIN_STYLE
|
|
145
|
+
*/
|
|
146
|
+
export function printStyledHeader(query, opts = {}) {
|
|
147
|
+
const write = opts.write || _stdoutLine;
|
|
148
|
+
const width = Math.min(process.stdout.columns || 80, 80);
|
|
149
|
+
const { colors, fg, bg, reset, bold } = opts.style || STYLE;
|
|
150
|
+
|
|
151
|
+
const artLen = SWEET_SEARCH_L2.length;
|
|
152
|
+
const maxQueryLen = width - artLen - 8;
|
|
153
|
+
const displayQuery = query.length > maxQueryLen
|
|
154
|
+
? query.slice(0, maxQueryLen - 3) + '...'
|
|
155
|
+
: query;
|
|
156
|
+
|
|
157
|
+
const palette = [
|
|
158
|
+
colors.darkest,
|
|
159
|
+
colors.darker,
|
|
160
|
+
colors.dark,
|
|
161
|
+
colors.lightDark,
|
|
162
|
+
colors.lightestDark,
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const getBgColor = (i, w) => {
|
|
166
|
+
const pos = i / Math.max(1, w - 1);
|
|
167
|
+
const t = 1 - Math.abs(0.5 - pos) * 2;
|
|
168
|
+
const zone = Math.min(Math.floor(t * palette.length), palette.length - 1);
|
|
169
|
+
return palette[zone];
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const buildLine = (leftContent, rightContent = null, isArt = false) => {
|
|
173
|
+
let result = '';
|
|
174
|
+
const leftPad = 2;
|
|
175
|
+
const rightPad = 2;
|
|
176
|
+
const rightStart = rightContent ? width - rightPad - rightContent.length : width;
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < width; i++) {
|
|
179
|
+
const bgColor = getBgColor(i, width);
|
|
180
|
+
const leftCharIdx = i - leftPad;
|
|
181
|
+
const rightCharIdx = i - rightStart;
|
|
182
|
+
|
|
183
|
+
if (rightContent && rightCharIdx >= 0 && rightCharIdx < rightContent.length) {
|
|
184
|
+
result += bold + fg(colors.white) + bg(bgColor) + rightContent[rightCharIdx];
|
|
185
|
+
} else if (leftCharIdx >= 0 && leftCharIdx < leftContent.length) {
|
|
186
|
+
const fgColor = isArt ? colors.border : colors.white;
|
|
187
|
+
result += bold + fg(fgColor) + bg(bgColor) + leftContent[leftCharIdx];
|
|
188
|
+
} else {
|
|
189
|
+
result += bg(bgColor) + ' ';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return result + reset;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const queryStr = `"${displayQuery}"`;
|
|
196
|
+
|
|
197
|
+
write('');
|
|
198
|
+
write(buildLine(SWEET_SEARCH_L1, null, true));
|
|
199
|
+
write(buildLine(SWEET_SEARCH_L2, queryStr, true));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Print styled stats line. The mode glyph distinguishes search-family variants
|
|
204
|
+
* (lexical/semantic/hybrid/pattern/grep), giving each its own identity.
|
|
205
|
+
*
|
|
206
|
+
* @param {Object} stats
|
|
207
|
+
* @param {boolean} [isWarm=false]
|
|
208
|
+
* @param {Object} [opts]
|
|
209
|
+
* @param {(line?: string) => void} [opts.write] line writer (default: stdout)
|
|
210
|
+
* @param {Object} [opts.style] STYLE or PLAIN_STYLE
|
|
211
|
+
*/
|
|
212
|
+
export function printStyledStats(stats, isWarm = false, opts = {}) {
|
|
213
|
+
const write = opts.write || _stdoutLine;
|
|
214
|
+
const { colors, fg, reset } = opts.style || STYLE;
|
|
215
|
+
const mode = stats.routing?.mode || stats.mode || 'auto';
|
|
216
|
+
const pathType = stats.path || 'hybrid';
|
|
217
|
+
const timeMs = stats.server_ms || stats.total_ms || 0;
|
|
218
|
+
|
|
219
|
+
const modeIcon = TOOL_ICONS[mode] || '◆';
|
|
220
|
+
const warmIcon = isWarm ? `${fg(colors.border)}●${reset}` : `${fg(colors.darker)}○${reset}`;
|
|
221
|
+
|
|
222
|
+
write(
|
|
223
|
+
` ${modeIcon} ${fg(colors.white)}${mode}${reset} ` +
|
|
224
|
+
`${fg(colors.dark)}│${reset} ${fg(colors.border)}${pathType}${reset} ` +
|
|
225
|
+
`${fg(colors.dark)}│${reset} ${fg(colors.white)}${timeMs}ms${reset} ${warmIcon}`
|
|
226
|
+
);
|
|
227
|
+
write('');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Emit the search banner (header + stats) on the channel the policy chose.
|
|
232
|
+
* Results always go to stdout separately; this only handles decoration. When
|
|
233
|
+
* the policy disables the banner, nothing is written.
|
|
234
|
+
*/
|
|
235
|
+
export function emitDecoration(policy, query, stats, isWarm) {
|
|
236
|
+
if (!policy.bannerEnabled) return;
|
|
237
|
+
const style = policy.colorEnabled ? STYLE : PLAIN_STYLE;
|
|
238
|
+
const deco = createDecorationWriter(policy);
|
|
239
|
+
try {
|
|
240
|
+
printStyledHeader(query, { write: deco.write, style });
|
|
241
|
+
printStyledStats(stats, isWarm, { write: deco.write, style });
|
|
242
|
+
} finally {
|
|
243
|
+
deco.close();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Convenience wrapper: resolve the output policy from the current environment +
|
|
249
|
+
* stdout, then emit the tool identity. Used by the read/trace/semantic CLIs.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} tool
|
|
252
|
+
* @param {string} [detail]
|
|
253
|
+
* @param {{plain?: boolean, noBanner?: boolean}} [flags]
|
|
254
|
+
*/
|
|
255
|
+
export function emitToolIdentityAuto(tool, detail = '', flags = {}) {
|
|
256
|
+
const policy = detectOutputPolicy({
|
|
257
|
+
format: flags.plain ? 'plain' : null,
|
|
258
|
+
noBanner: !!flags.noBanner,
|
|
259
|
+
env: process.env,
|
|
260
|
+
stream: process.stdout,
|
|
261
|
+
});
|
|
262
|
+
emitToolIdentity(policy, tool, detail);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Emit a compact, branded one-line identity for a tool (read/trace/semantic and
|
|
267
|
+
* any other tool that should not carry the full banner). Routed to the policy's
|
|
268
|
+
* decoration channel; a no-op when the policy disables the banner.
|
|
269
|
+
*
|
|
270
|
+
* @param {import('./output-policy.js').OutputPolicy} policy
|
|
271
|
+
* @param {string} tool e.g. 'read', 'trace', 'read-semantic'
|
|
272
|
+
* @param {string} [detail] short context shown after the tool name
|
|
273
|
+
*/
|
|
274
|
+
export function emitToolIdentity(policy, tool, detail = '') {
|
|
275
|
+
if (!policy || !policy.bannerEnabled) return;
|
|
276
|
+
const style = policy.colorEnabled ? STYLE : PLAIN_STYLE;
|
|
277
|
+
const { colors, fg, reset, bold } = style;
|
|
278
|
+
const icon = TOOL_ICONS[tool] || '✦';
|
|
279
|
+
const deco = createDecorationWriter(policy);
|
|
280
|
+
try {
|
|
281
|
+
const sep = `${fg(colors.dark)}·${reset}`;
|
|
282
|
+
const detailStr = detail ? ` ${sep} ${fg(colors.border)}${detail}${reset}` : '';
|
|
283
|
+
deco.write('');
|
|
284
|
+
deco.write(
|
|
285
|
+
` ${icon} ${bold}${fg(colors.white)}sweet-search${reset} ${sep} ${fg(colors.white)}${tool}${reset}${detailStr}`,
|
|
286
|
+
);
|
|
287
|
+
} finally {
|
|
288
|
+
deco.close();
|
|
289
|
+
}
|
|
290
|
+
}
|