sweet-search 0.0.1 → 2.3.0

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.
Files changed (161) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +23 -0
  3. package/core/cli.js +51 -0
  4. package/core/config.js +27 -0
  5. package/core/embedding/embedding-cache.js +467 -0
  6. package/core/embedding/embedding-local-model.js +845 -0
  7. package/core/embedding/embedding-remote.js +492 -0
  8. package/core/embedding/embedding-service.js +712 -0
  9. package/core/embedding/embedding-telemetry.js +219 -0
  10. package/core/embedding/index.js +40 -0
  11. package/core/graph/community-detector.js +294 -0
  12. package/core/graph/graph-expansion.js +839 -0
  13. package/core/graph/graph-extractor.js +2304 -0
  14. package/core/graph/graph-search.js +2148 -0
  15. package/core/graph/hcgs-generator.js +666 -0
  16. package/core/graph/index.js +16 -0
  17. package/core/graph/leiden-algorithm.js +547 -0
  18. package/core/graph/relationship-resolver.js +366 -0
  19. package/core/graph/repo-map.js +408 -0
  20. package/core/graph/summary-manager.js +549 -0
  21. package/core/indexing/artifact-builder.js +1054 -0
  22. package/core/indexing/ast-chunker.js +709 -0
  23. package/core/indexing/chunking/chunk-builder.js +170 -0
  24. package/core/indexing/chunking/markdown-chunker.js +503 -0
  25. package/core/indexing/chunking/plaintext-chunker.js +104 -0
  26. package/core/indexing/dedup/dedup-phase.js +159 -0
  27. package/core/indexing/dedup/exemplar-selector.js +65 -0
  28. package/core/indexing/document-chunker.js +56 -0
  29. package/core/indexing/incremental-parser.js +390 -0
  30. package/core/indexing/incremental-tracker.js +761 -0
  31. package/core/indexing/index-codebase-v21.js +472 -0
  32. package/core/indexing/index-maintainer.mjs +1674 -0
  33. package/core/indexing/index.js +90 -0
  34. package/core/indexing/indexer-ann.js +1077 -0
  35. package/core/indexing/indexer-build.js +742 -0
  36. package/core/indexing/indexer-phases.js +800 -0
  37. package/core/indexing/indexer-pool.js +764 -0
  38. package/core/indexing/indexer-sparse-gram.js +98 -0
  39. package/core/indexing/indexer-utils.js +536 -0
  40. package/core/indexing/indexer-worker.js +148 -0
  41. package/core/indexing/li-skip-policy.js +225 -0
  42. package/core/indexing/merkle-tracker.js +244 -0
  43. package/core/indexing/model-pool.js +166 -0
  44. package/core/infrastructure/code-graph-repository.js +120 -0
  45. package/core/infrastructure/codebase-repository.js +131 -0
  46. package/core/infrastructure/config/dedup.js +54 -0
  47. package/core/infrastructure/config/embedding.js +298 -0
  48. package/core/infrastructure/config/graph.js +80 -0
  49. package/core/infrastructure/config/index.js +82 -0
  50. package/core/infrastructure/config/indexing.js +8 -0
  51. package/core/infrastructure/config/platform.js +254 -0
  52. package/core/infrastructure/config/ranking.js +221 -0
  53. package/core/infrastructure/config/search.js +396 -0
  54. package/core/infrastructure/config/translation.js +89 -0
  55. package/core/infrastructure/config/vector-store.js +114 -0
  56. package/core/infrastructure/constants.js +86 -0
  57. package/core/infrastructure/coreml-cascade.js +909 -0
  58. package/core/infrastructure/coreml-cascade.json +46 -0
  59. package/core/infrastructure/coreml-provider.js +81 -0
  60. package/core/infrastructure/db-utils.js +69 -0
  61. package/core/infrastructure/dedup-hashing.js +83 -0
  62. package/core/infrastructure/hardware-capability.js +332 -0
  63. package/core/infrastructure/index.js +104 -0
  64. package/core/infrastructure/language-patterns/maps.js +121 -0
  65. package/core/infrastructure/language-patterns/registry-core.js +323 -0
  66. package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
  67. package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
  68. package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
  69. package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
  70. package/core/infrastructure/language-patterns/registry.js +19 -0
  71. package/core/infrastructure/language-patterns.js +141 -0
  72. package/core/infrastructure/llm-provider.js +733 -0
  73. package/core/infrastructure/manifest.json +46 -0
  74. package/core/infrastructure/maxsim.wasm +0 -0
  75. package/core/infrastructure/model-fetcher.js +423 -0
  76. package/core/infrastructure/model-registry.js +214 -0
  77. package/core/infrastructure/native-inference.js +587 -0
  78. package/core/infrastructure/native-resolver.js +187 -0
  79. package/core/infrastructure/native-sparse-gram.js +257 -0
  80. package/core/infrastructure/native-tokenizer.js +160 -0
  81. package/core/infrastructure/onnx-mutex.js +45 -0
  82. package/core/infrastructure/onnx-session-utils.js +261 -0
  83. package/core/infrastructure/ort-pipeline.js +111 -0
  84. package/core/infrastructure/project-detector.js +102 -0
  85. package/core/infrastructure/quantization.js +410 -0
  86. package/core/infrastructure/simd-distance.js +502 -0
  87. package/core/infrastructure/simd-distance.wasm +0 -0
  88. package/core/infrastructure/tree-sitter-provider.js +665 -0
  89. package/core/infrastructure/webgpu-maxsim.js +222 -0
  90. package/core/query/index.js +35 -0
  91. package/core/query/intent-detector.js +201 -0
  92. package/core/query/intent-router.js +156 -0
  93. package/core/query/query-router-catboost.js +222 -0
  94. package/core/query/query-router-ml.js +266 -0
  95. package/core/query/query-router.js +213 -0
  96. package/core/ranking/cascaded-scorer.js +379 -0
  97. package/core/ranking/flashrank.js +810 -0
  98. package/core/ranking/index.js +49 -0
  99. package/core/ranking/late-interaction-index.js +2383 -0
  100. package/core/ranking/late-interaction-model.js +812 -0
  101. package/core/ranking/local-reranker.js +374 -0
  102. package/core/ranking/mmr.js +379 -0
  103. package/core/ranking/quality-scorer.js +363 -0
  104. package/core/search/context-expander.js +1167 -0
  105. package/core/search/dedup/sibling-expander.js +327 -0
  106. package/core/search/index.js +16 -0
  107. package/core/search/search-boost.js +259 -0
  108. package/core/search/search-cli.js +544 -0
  109. package/core/search/search-format.js +282 -0
  110. package/core/search/search-fusion.js +327 -0
  111. package/core/search/search-hybrid.js +204 -0
  112. package/core/search/search-pattern-chunks.js +337 -0
  113. package/core/search/search-pattern-planner.js +439 -0
  114. package/core/search/search-pattern-prefilter.js +412 -0
  115. package/core/search/search-pattern-ripgrep.js +663 -0
  116. package/core/search/search-pattern.js +463 -0
  117. package/core/search/search-postprocess.js +452 -0
  118. package/core/search/search-semantic.js +706 -0
  119. package/core/search/search-server.js +554 -0
  120. package/core/search/session-daemon-prewarm.mjs +164 -0
  121. package/core/search/session-warmup.js +595 -0
  122. package/core/search/sweet-search.js +632 -0
  123. package/core/search/warmup-metrics.js +532 -0
  124. package/core/start-server.js +6 -0
  125. package/core/training/query-router/features/extractor.js +762 -0
  126. package/core/training/query-router/features/multilingual-patterns.js +431 -0
  127. package/core/training/query-router/features/text-segmenter.js +303 -0
  128. package/core/training/query-router/features/unicode-utils.js +383 -0
  129. package/core/training/query-router/output/v45_router_d4.js +11521 -0
  130. package/core/training/query-router/output/v46_router_d4.js +11498 -0
  131. package/core/vector-store/binary-heap.js +227 -0
  132. package/core/vector-store/binary-hnsw-index.js +1004 -0
  133. package/core/vector-store/float-vector-store.js +234 -0
  134. package/core/vector-store/hnsw-index.js +580 -0
  135. package/core/vector-store/index.js +39 -0
  136. package/core/vector-store/seismic-index.js +498 -0
  137. package/core/vocabulary/index.js +84 -0
  138. package/core/vocabulary/vocab-constants.js +20 -0
  139. package/core/vocabulary/vocab-miner-extractors.js +375 -0
  140. package/core/vocabulary/vocab-miner-nl.js +404 -0
  141. package/core/vocabulary/vocab-miner-utils.js +146 -0
  142. package/core/vocabulary/vocab-miner.js +574 -0
  143. package/core/vocabulary/vocab-prewarm-cli.js +110 -0
  144. package/core/vocabulary/vocab-ranker.js +492 -0
  145. package/core/vocabulary/vocab-warmer.js +523 -0
  146. package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
  147. package/core/vocabulary/vocabulary-utils.js +704 -0
  148. package/crates/wasm-router/pkg/package.json +13 -0
  149. package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
  150. package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
  151. package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
  152. package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
  153. package/mcp/config-gen.js +121 -0
  154. package/mcp/server.js +335 -0
  155. package/mcp/tool-handlers.js +476 -0
  156. package/package.json +131 -9
  157. package/scripts/benchmark-harness.js +794 -0
  158. package/scripts/init.js +1058 -0
  159. package/scripts/smoke-test.js +435 -0
  160. package/scripts/uninstall.js +478 -0
  161. package/scripts/verify-runtime.js +176 -0
@@ -0,0 +1,46 @@
1
+ {
2
+ "$comment": "CoreML variant cascade shape set — SINGLE SOURCE OF TRUTH. Consumed by (1) core/infrastructure/coreml-cascade.js for state inspection, cache resolution, and HF fetch, (2) scripts/spike-coreml/trace_cascade.py for local builds, and (3) the Rust filename parser in crates/sweet-search-native/src/inference/{embedding_model,li_model}.rs (parse_embed_variant_filename, parse_li_variant_filename) via the filePattern convention. Changing shapes here requires re-running scripts/build-coreml-cascade.js to regenerate the .mlpackage artifacts AND re-uploading tarballs to `hfRepo` with fresh checksums. The publish-coreml-cascade helper in the build script automates the tarball+upload+checksum backfill loop.",
3
+ "version": 2,
4
+ "description": "Cascade of fixed-shape NomicBERT embedding and ModernBERT LI mlpackages covering the stair-step output of the JS bucketer in core/embedding/embedding-local-model.js + core/indexing/indexer-pool.js. Distributed as per-variant .tar.gz archives from `hfRepo` so `sweet-search init` can fetch them alongside the other models without requiring Python/coremltools on the end-user machine.",
5
+ "hfRepo": "mrsladoje/sweet-search-coreml-cascade",
6
+ "embed": {
7
+ "modelKey": "nomic-bert",
8
+ "filePattern": "nomic_bert_b{batch}_s{seq}_fp16.mlpackage",
9
+ "tarballPattern": "embed/nomic_bert_b{batch}_s{seq}_fp16.mlpackage.tar.gz",
10
+ "hiddenDim": 768,
11
+ "traceSource": {
12
+ "hfId": "nomic-ai/CodeRankEmbed",
13
+ "safetensors": "model.safetensors",
14
+ "config": "config.json"
15
+ },
16
+ "variants": [
17
+ { "batch": 64, "seq": 96, "rationale": "hardCap × short seq — covers observed (64, 52..107) regime", "tarballSha256": "76eeb863ffe11cc9d21cd0be0bd47d932630a86480c8a4f019d3360990b8ee14", "tarballSizeBytes": 252549277 },
18
+ { "batch": 64, "seq": 192, "rationale": "hardCap × short-med", "tarballSha256": "138ae2b406ecc0c2ce87730157791667f007472745079298c9eb605dc4b7ec1c", "tarballSizeBytes": 252608808 },
19
+ { "batch": 32, "seq": 384, "rationale": "tokenBudget regime", "tarballSha256": "580c67c9b9d3ac85e2c3a76b2262ad0b0c0489889dddfa3bc8cf1f215e71643a", "tarballSizeBytes": 252627736 },
20
+ { "batch": 16, "seq": 512, "rationale": "long, cache-bound start", "tarballSha256": "6e8dfc0b83befc905f2e90df45ae21d232d7983956c8c1526176fb175e774fbe", "tarballSizeBytes": 252608341 },
21
+ { "batch": 4, "seq": 1024, "rationale": "long tail", "tarballSha256": "62afa26642d5531fc227ef730cd372312331631be43281464c9c1fb2c59b3f2f", "tarballSizeBytes": 252626657 },
22
+ { "batch": 1, "seq": 2048, "rationale": "extreme tail / oversize docs", "tarballSha256": "4e3073e231a03d3c7fca673af81ac1fa9e6d28c4ff8da7856d64183eff090532", "tarballSizeBytes": 252722456 }
23
+ ]
24
+ },
25
+ "li": {
26
+ "modelKey": "modernbert-li",
27
+ "filePattern": "li_modernbert_b{batch}_s{seq}_fp16.mlpackage",
28
+ "tarballPattern": "li/li_modernbert_b{batch}_s{seq}_fp16.mlpackage.tar.gz",
29
+ "tokenDim": 128,
30
+ "backboneDim": 768,
31
+ "traceSource": {
32
+ "hfId": "lightonai/LateOn-Code",
33
+ "backbone": "model.safetensors",
34
+ "projection": "1_Dense/model.safetensors",
35
+ "config": "config.json"
36
+ },
37
+ "variants": [
38
+ { "batch": 128, "seq": 48, "rationale": "upperCap × very short — covers observed (128, 18..33)", "tarballSha256": "01dddc23d4a83be98977b0500d9fcf977fb7649bb548bb9977c6d6395f22e1c2", "tarballSizeBytes": 275367487 },
39
+ { "batch": 128, "seq": 128, "rationale": "upperCap × short", "tarballSha256": "f81af66aef693c4c1994b560d23f1b0fbf97c38c66e0fadda0774af358b9b1a1", "tarballSizeBytes": 275382654 },
40
+ { "batch": 64, "seq": 256, "rationale": "medium", "tarballSha256": "f5958100ab4a81e724f37338b5ad45c396a5c649402552191632a79d4ef826a9", "tarballSizeBytes": 275405899 },
41
+ { "batch": 16, "seq": 512, "rationale": "long, cache-bound start", "tarballSha256": "37906ffe3cc9205306cfb50d0b5821e569fd6ee3ec13e4d71f481f1e53649031", "tarballSizeBytes": 275451379 },
42
+ { "batch": 4, "seq": 1024, "rationale": "long tail", "tarballSha256": "e618f8bd981d24e4984c61ee1788c02578aaf601efc16cd926885a2930833d40", "tarballSizeBytes": 275545042 },
43
+ { "batch": 1, "seq": 2048, "rationale": "LI max length", "tarballSha256": "d5262bf49f145cadbdbddb82b7934499fc310f5c654c5aae34cfb9c2d90487d2", "tarballSizeBytes": 275743803 }
44
+ ]
45
+ }
46
+ }
@@ -0,0 +1,81 @@
1
+ // core/coreml-provider.js
2
+ // CoreML execution provider detection and configuration for macOS Apple Silicon.
3
+
4
+ import os from 'os';
5
+
6
+ /**
7
+ * Check if running on macOS Apple Silicon (ARM64).
8
+ * CoreML EP only benefits Apple Silicon; on Intel Macs it falls back to CPU anyway.
9
+ */
10
+ export function isAppleSilicon() {
11
+ return process.platform === 'darwin' && os.arch() === 'arm64';
12
+ }
13
+
14
+ let _coremlAvailable = null;
15
+
16
+ /**
17
+ * Check if CoreML EP is available in the onnxruntime-node binary.
18
+ * Uses ort.listSupportedBackends() which is available in ORT >= 1.18.
19
+ * Result is cached after first call.
20
+ */
21
+ export async function isCoreMLProviderAvailable() {
22
+ if (_coremlAvailable !== null) return _coremlAvailable;
23
+ try {
24
+ const ort = await import('onnxruntime-node');
25
+ if (typeof ort.listSupportedBackends === 'function') {
26
+ const backends = ort.listSupportedBackends();
27
+ _coremlAvailable = backends.some(b => b.name === 'coreml' && b.bundled);
28
+ return _coremlAvailable;
29
+ }
30
+ } catch { /* ignore — ORT not installed or too old */ }
31
+ _coremlAvailable = false;
32
+ return false;
33
+ }
34
+
35
+ /**
36
+ * Determine if CoreML EP should be used, following the same tri-state pattern
37
+ * as SWEET_SEARCH_USE_OPENVINO: auto | on | off.
38
+ *
39
+ * "auto" (default): enable on Apple Silicon when provider is available.
40
+ * "on"/"1"/"true": force enable (still gated to darwin+arm64).
41
+ * "off"/"0"/"false": force disable.
42
+ *
43
+ * @param {boolean} providerAvailable - result of isCoreMLProviderAvailable()
44
+ */
45
+ export function shouldUseCoreML(providerAvailable) {
46
+ const raw = (process.env.SWEET_SEARCH_USE_COREML ?? '').trim().toLowerCase();
47
+ if (raw === '0' || raw === 'false' || raw === 'off') return false;
48
+ if (!isAppleSilicon()) return false;
49
+
50
+ const autoMode = raw === '' || raw === 'auto';
51
+ const explicitOn = raw === '1' || raw === 'true' || raw === 'on';
52
+ if (!autoMode && !explicitOn) return false;
53
+
54
+ return providerAvailable;
55
+ }
56
+
57
+ /**
58
+ * Build the executionProviders array for CoreML with CPU fallback.
59
+ *
60
+ * Two CoreML model formats exist:
61
+ * - MLProgram (coreMlFlags 0x010): newer, supports ANE, requires Core ML 5+.
62
+ * Better performance when supported, but some models fail to compile.
63
+ * - NeuralNetwork (coreMlFlags 0): older default, broader op coverage,
64
+ * silently casts FP32→FP16. Works with more model architectures.
65
+ *
66
+ * ORT 1.24+ uses lowercase backend names ('cpu', 'coreml', 'webgpu').
67
+ * The legacy PascalCase names ('CPUExecutionProvider') are NOT recognized.
68
+ *
69
+ * @param {boolean} useMLProgram - true for MLProgram format, false for NeuralNetwork
70
+ */
71
+ export function getCoreMLExecutionProviders(useMLProgram = true) {
72
+ return [
73
+ { name: 'coreml', coreMlFlags: useMLProgram ? 0x010 : 0 },
74
+ 'cpu',
75
+ ];
76
+ }
77
+
78
+ /** Reset cached state (for testing and A/B benchmark pass switching). */
79
+ export function _resetCoreMLCache() {
80
+ _coremlAvailable = null;
81
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Database Utilities - Shared SQLite configuration helpers.
3
+ */
4
+
5
+ import Database from 'better-sqlite3';
6
+
7
+ /**
8
+ * Apply read-path PRAGMA optimizations to a read-only database connection.
9
+ *
10
+ * - mmap_size = 256MB: Maps entire DB into OS page cache (typical DBs are 5-50MB).
11
+ * Subsequent reads bypass SQLite's page cache via xFetch().
12
+ * - cache_size = 20MB: Modest bump from default ~16MB. Partially redundant with mmap
13
+ * but acts as safety net for internal bookkeeping.
14
+ * - temp_store = MEMORY (optional): Use only for bounded, latency-sensitive read paths.
15
+ *
16
+ * These are best-effort optimizations. PRAGMA failures should not change the caller's
17
+ * fallback behavior or make a readable database look unavailable.
18
+ *
19
+ * @param {import('better-sqlite3').Database} db
20
+ * @param {{ tempStoreMemory?: boolean }} [options]
21
+ */
22
+ export function applyReadPragmas(db, options = {}) {
23
+ if (!db || typeof db.pragma !== 'function') return;
24
+ const { tempStoreMemory = false } = options;
25
+
26
+ for (const pragma of [
27
+ 'mmap_size = 268435456', // 256MB
28
+ 'cache_size = -20000', // 20MB
29
+ ]) {
30
+ try {
31
+ db.pragma(pragma);
32
+ } catch {
33
+ // Best-effort only.
34
+ }
35
+ }
36
+
37
+ if (tempStoreMemory) {
38
+ try {
39
+ db.pragma('temp_store = MEMORY');
40
+ } catch {
41
+ // Best-effort only.
42
+ }
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Warm the graph database page cache with lightweight queries.
48
+ * Opens an ephemeral connection, touches FTS5/relationship/summary pages, closes.
49
+ * Intentionally minimal — no heavy imports, no query infrastructure.
50
+ *
51
+ * @param {string} dbPath - Path to code_graph.db
52
+ * @returns {{ summaries: number }}
53
+ */
54
+ export function warmGraphDbCache(dbPath) {
55
+ const db = new Database(dbPath, { readonly: true, timeout: 5000 });
56
+ applyReadPragmas(db);
57
+ try {
58
+ try { db.prepare('SELECT count(*) FROM entities_fts WHERE name MATCH "warmup"').get(); } catch { /* table may not exist */ }
59
+ try { db.prepare('SELECT count(*) FROM relationships').get(); } catch { /* table may not exist */ }
60
+ let summaries = 0;
61
+ try {
62
+ const row = db.prepare('SELECT count(*) as cnt FROM entities WHERE summary IS NOT NULL').get();
63
+ summaries = row?.cnt || 0;
64
+ } catch { /* column may not exist */ }
65
+ return { summaries };
66
+ } finally {
67
+ db.close();
68
+ }
69
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Dedup hashing — thin JS wrapper over the Rust NAPI addon's
3
+ * SimHash + MinHash + LSH clustering entrypoints.
4
+ *
5
+ * Zero domain logic lives here. If the native addon is missing
6
+ * (unsupported platform), `isAvailable()` returns false and callers
7
+ * fall back to a no-op that treats every chunk as its own exemplar.
8
+ */
9
+
10
+ import { createRequire } from 'module';
11
+ import { resolveNativeAddon } from './native-resolver.js';
12
+ import { DEDUP_CONFIG } from './config/dedup.js';
13
+
14
+ const require = createRequire(import.meta.url);
15
+
16
+ let _addon = null;
17
+ let _loadAttempted = false;
18
+ let _loadError = null;
19
+
20
+ function loadAddon() {
21
+ if (_loadAttempted) return _addon;
22
+ _loadAttempted = true;
23
+ const addonPath = resolveNativeAddon();
24
+ if (!addonPath) {
25
+ _loadError = new Error('dedup-hashing: native addon not resolved for this platform');
26
+ return null;
27
+ }
28
+ try {
29
+ _addon = require(addonPath);
30
+ } catch (e) {
31
+ _loadError = e;
32
+ _addon = null;
33
+ }
34
+ return _addon;
35
+ }
36
+
37
+ /**
38
+ * Returns true iff the Rust NAPI dedup functions are callable on this machine.
39
+ * Used by callers to decide whether to run the dedup phase or skip it cleanly.
40
+ */
41
+ export function isAvailable() {
42
+ const addon = loadAddon();
43
+ return !!(
44
+ addon &&
45
+ typeof addon.dedupFingerprintBatch === 'function' &&
46
+ typeof addon.dedupCluster === 'function'
47
+ );
48
+ }
49
+
50
+ export function getLoadError() {
51
+ loadAddon();
52
+ return _loadError;
53
+ }
54
+
55
+ /**
56
+ * Compute per-chunk SimHash + MinHash fingerprints for a batch of texts.
57
+ * Returns `[{simhashHex: string, minhash: Buffer(512)}]`, one per input.
58
+ * Throws if the native addon isn't available — callers should check `isAvailable()` first.
59
+ */
60
+ export function computeFingerprints(texts, config = DEDUP_CONFIG) {
61
+ const addon = loadAddon();
62
+ if (!addon) throw _loadError ?? new Error('dedup native addon unavailable');
63
+ return addon.dedupFingerprintBatch(texts, config.ngramSize, config.numPerm, config.seed);
64
+ }
65
+
66
+ /**
67
+ * Cluster fingerprints into near-duplicate groups via banded LSH +
68
+ * SimHash Hamming secondary filter + union-find collapse.
69
+ * Returns `[{exemplarIdx, siblingIdxs, clusterId}]`. Entries include
70
+ * singletons (siblingIdxs.length === 0) so callers can enumerate every chunk's
71
+ * cluster assignment — filter them out to get only true clusters.
72
+ */
73
+ export function clusterFingerprints(fingerprints, config = DEDUP_CONFIG) {
74
+ const addon = loadAddon();
75
+ if (!addon) throw _loadError ?? new Error('dedup native addon unavailable');
76
+ return addon.dedupCluster(
77
+ fingerprints,
78
+ config.numPerm,
79
+ config.numBands,
80
+ config.jaccardThreshold,
81
+ config.simhashHammingMax,
82
+ );
83
+ }
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Hardware Capability Detection — identifies the current machine's chip
3
+ * family, accelerator generation, and preferred native inference backend.
4
+ *
5
+ * Used by:
6
+ * - coreml-cascade.js → gates cascade fetch/build on M3+ ANE viability
7
+ * - native-inference.js → backend selection diagnostics
8
+ * - scripts/init.js → decides what artifacts to fetch for the profile
9
+ * - scripts/uninstall.js → only removes cascade cache if it exists
10
+ *
11
+ * Design notes:
12
+ * - `sysctl` is a cheap (~5 ms) one-shot call. The result is cached so
13
+ * repeated consumers (init, native-inference, uninstall) all share one
14
+ * detection. Hardware doesn't change at runtime.
15
+ * - Never throws. Unknown hardware degrades to "candle-cpu fallback" —
16
+ * this module is only advisory; absence of a capability is never an
17
+ * error.
18
+ * - Unknown new Apple chips (e.g. an M5 shipped after this file) are
19
+ * admitted as cascade-eligible via the ">= 3" rule — we prefer
20
+ * optimistic new-hardware behavior to silently refusing to try.
21
+ * - CUDA detection combines two signals: (1) an "addon built with the
22
+ * cuda feature?" probe via `native_cuda_available()`, and
23
+ * (2) `nvidia-smi` output for GPU name + compute capability + VRAM.
24
+ * The addon probe is authoritative for "is CUDA end-to-end usable";
25
+ * `nvidia-smi` fills in the diagnostic fields. See the CUDA Backend
26
+ * section in docs/INIT_STRATEGY.md for the full contract.
27
+ */
28
+
29
+ import { execFileSync } from 'node:child_process';
30
+ import os from 'node:os';
31
+ import { resolveNativeAddon } from './native-resolver.js';
32
+ import { createRequire } from 'node:module';
33
+
34
+ let _cached = null;
35
+
36
+ /**
37
+ * Parse an Apple chip brand string from `sysctl -n machdep.cpu.brand_string`
38
+ * into a structured descriptor.
39
+ *
40
+ * Known formats:
41
+ * "Apple M1" → { family: "M1", generation: 1, variant: "base" }
42
+ * "Apple M2 Pro" → { family: "M2", generation: 2, variant: "pro" }
43
+ * "Apple M3 Max" → { family: "M3", generation: 3, variant: "max" }
44
+ * "Apple M4 Ultra" → { family: "M4", generation: 4, variant: "ultra" }
45
+ *
46
+ * Returns `null` for unrecognised strings (including Intel brand strings).
47
+ */
48
+ export function parseAppleChipBrandString(raw) {
49
+ if (!raw || typeof raw !== 'string') return null;
50
+ const match = raw.match(/\bApple\s+(M(\d+))(?:\s+(Pro|Max|Ultra))?\b/i);
51
+ if (!match) return null;
52
+ return {
53
+ family: match[1].toUpperCase(),
54
+ generation: parseInt(match[2], 10),
55
+ variant: match[3] ? match[3].toLowerCase() : 'base',
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Read CPU brand string via `sysctl`. Only called on darwin; returns
61
+ * `null` on any failure (sandboxed env, sysctl missing, non-zero exit).
62
+ */
63
+ function sysctlBrandString() {
64
+ try {
65
+ const out = execFileSync('sysctl', ['-n', 'machdep.cpu.brand_string'], {
66
+ encoding: 'utf-8',
67
+ stdio: ['ignore', 'pipe', 'ignore'],
68
+ timeout: 2000,
69
+ });
70
+ return out.trim() || null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Minimum Apple generation for which the CoreML variant cascade is
78
+ * expected to beat candle Metal BF16 at our NomicBERT + ModernBERT
79
+ * workload.
80
+ *
81
+ * Why M3:
82
+ * - M1 ANE ≈ 11 TOPS int8; M2 ≈ 15.8; M3 ≈ 18; M4 ≈ 38.
83
+ * - Our end-to-end measurement on M3 Max saw 18% wall-clock reduction
84
+ * vs Metal BF16 baseline (commit 4fd9c9a).
85
+ * - M1/M2 ANE TOPS are below M3 and the spike's measured per-batch
86
+ * latency improvement on smaller shapes does not cover the mlmodelc
87
+ * compile overhead for those generations. Rather than ship a
88
+ * fancier feature that regresses on older hardware, we gate on M3+.
89
+ * - A future measurement on M1/M2 with a smaller cascade subset could
90
+ * lower this threshold. For now, conservative gating.
91
+ */
92
+ const MIN_APPLE_GENERATION_FOR_CASCADE = 3;
93
+
94
+ /**
95
+ * Minimum NVIDIA compute capability (as a float) we target for the CUDA
96
+ * backend.
97
+ *
98
+ * Why 7.0:
99
+ * - SM 6.x (Pascal, e.g. GTX 1080) has no tensor cores. Candle's
100
+ * stock matmul on CUDA cores runs slower than modern CPU BLAS for
101
+ * our small (b≤64, s≤1024) shapes. Net regression vs CPU.
102
+ * - SM 7.0 (Volta, V100) has first-gen tensor cores + F16 matmul with
103
+ * F32 accumulate. Works, but we pin F32 dtype here because F16 has
104
+ * precision issues on this generation.
105
+ * - SM 7.5 (Turing, T4/RTX 20xx) has second-gen tensor cores. F16
106
+ * works cleanly with F32 accumulate via cuBLASLt / cuDNN.
107
+ * - SM 8.0+ (Ampere/Ada/Hopper) adds BF16 tensor cores. BF16 is the
108
+ * default on this tier, matching the Metal story.
109
+ *
110
+ * See the per-CC dtype policy in
111
+ * crates/sweet-search-native/src/inference/mod.rs::optimal_dtype for
112
+ * the full mapping. JS layer is responsible for parsing the
113
+ * `nvidia-smi` compute_cap output and setting
114
+ * SWEET_SEARCH_CUDA_COMPUTE_CAP before the addon loads models.
115
+ */
116
+ const MIN_CUDA_COMPUTE_CAPABILITY = 7.0;
117
+
118
+ /**
119
+ * Parse `nvidia-smi --query-gpu=name,compute_cap,memory.total,driver_version`
120
+ * CSV output into a structured descriptor. Returns `null` on anything
121
+ * unparseable — the surrounding `detectNvidiaGpu()` treats that as
122
+ * "no GPU" just like a missing `nvidia-smi` binary.
123
+ *
124
+ * Expected first data line:
125
+ * "NVIDIA GeForce RTX 3090, 8.6, 24576, 535.129.03"
126
+ */
127
+ export function parseNvidiaSmiOutput(raw) {
128
+ if (!raw || typeof raw !== 'string') return null;
129
+ const firstLine = raw.split(/\r?\n/).find((l) => l.trim().length > 0);
130
+ if (!firstLine) return null;
131
+ const cols = firstLine.split(',').map((s) => s.trim());
132
+ if (cols.length < 4) return null;
133
+ const [name, computeCapability, memoryMB, driverVersion] = cols;
134
+ const memInt = parseInt(memoryMB, 10);
135
+ const ccFloat = parseFloat(computeCapability);
136
+ if (!name || !Number.isFinite(ccFloat) || !Number.isFinite(memInt)) return null;
137
+ return {
138
+ name,
139
+ computeCapability, // string "8.6" — kept as string for faithful passthrough
140
+ computeCapabilityFloat: ccFloat, // float for comparisons
141
+ memoryMB: memInt,
142
+ driverVersion,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Shell out to `nvidia-smi` on Linux and return the first GPU's metadata.
148
+ * Silent failure (returns `null`) on any of: non-Linux host, missing
149
+ * binary, non-zero exit, malformed output. Bounded at 2s timeout so a
150
+ * hung GPU doesn't block init.
151
+ */
152
+ function detectNvidiaGpu() {
153
+ if (process.platform !== 'linux') return null;
154
+ try {
155
+ const out = execFileSync(
156
+ 'nvidia-smi',
157
+ [
158
+ '--query-gpu=name,compute_cap,memory.total,driver_version',
159
+ '--format=csv,noheader,nounits',
160
+ ],
161
+ {
162
+ encoding: 'utf-8',
163
+ stdio: ['ignore', 'pipe', 'ignore'],
164
+ timeout: 2000,
165
+ },
166
+ );
167
+ return parseNvidiaSmiOutput(out);
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Probe the native Rust addon for CUDA readiness. Returns the boolean
175
+ * reported by `native_cuda_available()` or `null` when the addon is
176
+ * missing / doesn't expose that export (older builds).
177
+ *
178
+ * The addon may return `Some(false)` when the `cuda` feature IS built in
179
+ * but the runtime driver is missing — in that case we know the package
180
+ * is the `-cuda` variant but libcuda.so didn't load, which is worth
181
+ * surfacing to the user.
182
+ */
183
+ function probeAddonCudaAvailability() {
184
+ try {
185
+ const addonPath = resolveNativeAddon();
186
+ if (!addonPath) return null;
187
+ const addonRequire = createRequire(import.meta.url);
188
+ const addon = addonRequire(addonPath);
189
+ if (typeof addon?.nativeCudaAvailable !== 'function') return null;
190
+ return Boolean(addon.nativeCudaAvailable());
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Detect the current hardware capability. Returns a stable, read-only
198
+ * descriptor that callers use to pick inference backends and decide
199
+ * which artifacts to fetch at init time.
200
+ *
201
+ * Fields:
202
+ * platform — process.platform
203
+ * arch — process.arch
204
+ * totalMemGB — os.totalmem() in GiB (float)
205
+ * logicalCores — os.cpus().length
206
+ * brandString — raw sysctl output (darwin-arm64 only)
207
+ * appleSilicon — parsed chip descriptor or null
208
+ * coremlCascadeEligible — boolean; M3+ darwin-arm64 only
209
+ * coremlCascadeReason — human string explaining eligible/not
210
+ * nvidiaGpu — { name, computeCapability, computeCapabilityFloat,
211
+ * memoryMB, driverVersion } | null
212
+ * cudaAddonEnabled — boolean; addon built with cuda feature + libcuda.so loaded
213
+ * cudaAvailable — boolean; cudaAddonEnabled AND nvidiaGpu AND CC ≥ 7.0
214
+ * cudaReason — human string explaining eligible/not
215
+ * candleGpuBackend — "metal" | "cuda" | null
216
+ * inferenceBackendPreference — "coreml-cascade" | "candle-metal"
217
+ * | "candle-cuda" | "candle-cpu"
218
+ */
219
+ export function detectHardwareCapability() {
220
+ if (_cached) return _cached;
221
+
222
+ const platform = process.platform;
223
+ const arch = process.arch;
224
+ const totalMemGB = os.totalmem() / (1024 ** 3);
225
+ const logicalCores = os.cpus().length;
226
+
227
+ let brandString = null;
228
+ let appleSilicon = null;
229
+ if (platform === 'darwin' && arch === 'arm64') {
230
+ brandString = sysctlBrandString();
231
+ appleSilicon = parseAppleChipBrandString(brandString);
232
+ }
233
+
234
+ let coremlCascadeEligible = false;
235
+ let coremlCascadeReason;
236
+ if (platform !== 'darwin') {
237
+ coremlCascadeReason = `CoreML is macOS-only (current platform: ${platform})`;
238
+ } else if (arch !== 'arm64') {
239
+ coremlCascadeReason = `CoreML cascade requires Apple Silicon (current arch: ${arch})`;
240
+ } else if (!appleSilicon) {
241
+ coremlCascadeReason = `Could not identify Apple Silicon chip (sysctl brand: ${brandString || 'unavailable'})`;
242
+ } else if (appleSilicon.generation < MIN_APPLE_GENERATION_FOR_CASCADE) {
243
+ coremlCascadeReason = `${brandString}: ${appleSilicon.family} ANE below cascade threshold (M${MIN_APPLE_GENERATION_FOR_CASCADE}+ required)`;
244
+ } else {
245
+ coremlCascadeEligible = true;
246
+ coremlCascadeReason = `${brandString}: ${appleSilicon.family} ANE suitable for cascade`;
247
+ }
248
+
249
+ // NVIDIA GPU detection — Linux only. `nvidia-smi` tells us what's
250
+ // installed; the addon probe tells us whether the installed native
251
+ // package actually knows how to dispatch to it.
252
+ const nvidiaGpu = detectNvidiaGpu();
253
+ const addonCudaProbe = probeAddonCudaAvailability();
254
+ const cudaAddonEnabled = addonCudaProbe === true;
255
+
256
+ // Diagnostic opt-out: SWEET_SEARCH_CUDA=0 force-disables CUDA even on
257
+ // an otherwise-eligible host. Parallels SWEET_SEARCH_COREML_CASCADE=0.
258
+ const cudaForceDisabled = ['0', 'false', 'off'].includes(
259
+ (process.env.SWEET_SEARCH_CUDA ?? '').toLowerCase(),
260
+ );
261
+
262
+ let cudaAvailable = false;
263
+ let cudaReason;
264
+ if (cudaForceDisabled) {
265
+ cudaReason = 'CUDA force-disabled via SWEET_SEARCH_CUDA=0';
266
+ } else if (platform !== 'linux') {
267
+ cudaReason = `CUDA native backend ships for Linux only (current platform: ${platform})`;
268
+ } else if (!nvidiaGpu) {
269
+ cudaReason = 'No NVIDIA GPU detected (nvidia-smi missing or no device 0)';
270
+ } else if (nvidiaGpu.computeCapabilityFloat < MIN_CUDA_COMPUTE_CAPABILITY) {
271
+ cudaReason = `${nvidiaGpu.name}: compute capability ${nvidiaGpu.computeCapability} below threshold (${MIN_CUDA_COMPUTE_CAPABILITY}+ required)`;
272
+ } else if (addonCudaProbe === null) {
273
+ cudaReason = `${nvidiaGpu.name} detected, but the installed native package does not expose a CUDA probe — reinstall with @sweet-search/native-linux-${arch === 'arm64' ? 'arm64' : 'x64'}-gnu-cuda to enable GPU indexing`;
274
+ } else if (addonCudaProbe === false) {
275
+ cudaReason = `${nvidiaGpu.name} detected, but the native addon reports Device::new_cuda(0) failed (driver / libcuda.so mismatch?)`;
276
+ } else {
277
+ cudaAvailable = true;
278
+ cudaReason = `${nvidiaGpu.name} (compute ${nvidiaGpu.computeCapability}, ${nvidiaGpu.memoryMB} MB, driver ${nvidiaGpu.driverVersion}) — suitable for candle-cuda`;
279
+ }
280
+
281
+ // Candle GPU backend availability.
282
+ // darwin-arm64 → metal (bundled with the darwin-arm64 native package)
283
+ // linux-*-gnu + NVIDIA + cuda-enabled addon → cuda
284
+ // everything else → null (falls through to candle CPU)
285
+ let candleGpuBackend = null;
286
+ if (platform === 'darwin' && arch === 'arm64') {
287
+ candleGpuBackend = 'metal';
288
+ } else if (cudaAvailable) {
289
+ candleGpuBackend = 'cuda';
290
+ }
291
+
292
+ // Preference order: coreml-cascade > candle-metal > candle-cuda > candle-cpu.
293
+ // coreml-cascade and candle-cuda never co-exist on the same host
294
+ // (one is darwin, the other is linux), so the ordering is orthogonal
295
+ // in practice.
296
+ let inferenceBackendPreference;
297
+ if (coremlCascadeEligible) {
298
+ inferenceBackendPreference = 'coreml-cascade';
299
+ } else if (candleGpuBackend === 'metal') {
300
+ inferenceBackendPreference = 'candle-metal';
301
+ } else if (candleGpuBackend === 'cuda') {
302
+ inferenceBackendPreference = 'candle-cuda';
303
+ } else {
304
+ inferenceBackendPreference = 'candle-cpu';
305
+ }
306
+
307
+ _cached = Object.freeze({
308
+ platform,
309
+ arch,
310
+ totalMemGB,
311
+ logicalCores,
312
+ brandString,
313
+ appleSilicon,
314
+ coremlCascadeEligible,
315
+ coremlCascadeReason,
316
+ nvidiaGpu,
317
+ cudaAddonEnabled,
318
+ cudaAvailable,
319
+ cudaReason,
320
+ candleGpuBackend,
321
+ inferenceBackendPreference,
322
+ });
323
+ return _cached;
324
+ }
325
+
326
+ /**
327
+ * Reset the cached detection result. Tests only — production callers
328
+ * should never need this because hardware doesn't change at runtime.
329
+ */
330
+ export function _resetHardwareCapabilityCache() {
331
+ _cached = null;
332
+ }