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.
- package/LICENSE +190 -0
- package/NOTICE +23 -0
- package/core/cli.js +51 -0
- package/core/config.js +27 -0
- package/core/embedding/embedding-cache.js +467 -0
- package/core/embedding/embedding-local-model.js +845 -0
- package/core/embedding/embedding-remote.js +492 -0
- package/core/embedding/embedding-service.js +712 -0
- package/core/embedding/embedding-telemetry.js +219 -0
- package/core/embedding/index.js +40 -0
- package/core/graph/community-detector.js +294 -0
- package/core/graph/graph-expansion.js +839 -0
- package/core/graph/graph-extractor.js +2304 -0
- package/core/graph/graph-search.js +2148 -0
- package/core/graph/hcgs-generator.js +666 -0
- package/core/graph/index.js +16 -0
- package/core/graph/leiden-algorithm.js +547 -0
- package/core/graph/relationship-resolver.js +366 -0
- package/core/graph/repo-map.js +408 -0
- package/core/graph/summary-manager.js +549 -0
- package/core/indexing/artifact-builder.js +1054 -0
- package/core/indexing/ast-chunker.js +709 -0
- package/core/indexing/chunking/chunk-builder.js +170 -0
- package/core/indexing/chunking/markdown-chunker.js +503 -0
- package/core/indexing/chunking/plaintext-chunker.js +104 -0
- package/core/indexing/dedup/dedup-phase.js +159 -0
- package/core/indexing/dedup/exemplar-selector.js +65 -0
- package/core/indexing/document-chunker.js +56 -0
- package/core/indexing/incremental-parser.js +390 -0
- package/core/indexing/incremental-tracker.js +761 -0
- package/core/indexing/index-codebase-v21.js +472 -0
- package/core/indexing/index-maintainer.mjs +1674 -0
- package/core/indexing/index.js +90 -0
- package/core/indexing/indexer-ann.js +1077 -0
- package/core/indexing/indexer-build.js +742 -0
- package/core/indexing/indexer-phases.js +800 -0
- package/core/indexing/indexer-pool.js +764 -0
- package/core/indexing/indexer-sparse-gram.js +98 -0
- package/core/indexing/indexer-utils.js +536 -0
- package/core/indexing/indexer-worker.js +148 -0
- package/core/indexing/li-skip-policy.js +225 -0
- package/core/indexing/merkle-tracker.js +244 -0
- package/core/indexing/model-pool.js +166 -0
- package/core/infrastructure/code-graph-repository.js +120 -0
- package/core/infrastructure/codebase-repository.js +131 -0
- package/core/infrastructure/config/dedup.js +54 -0
- package/core/infrastructure/config/embedding.js +298 -0
- package/core/infrastructure/config/graph.js +80 -0
- package/core/infrastructure/config/index.js +82 -0
- package/core/infrastructure/config/indexing.js +8 -0
- package/core/infrastructure/config/platform.js +254 -0
- package/core/infrastructure/config/ranking.js +221 -0
- package/core/infrastructure/config/search.js +396 -0
- package/core/infrastructure/config/translation.js +89 -0
- package/core/infrastructure/config/vector-store.js +114 -0
- package/core/infrastructure/constants.js +86 -0
- package/core/infrastructure/coreml-cascade.js +909 -0
- package/core/infrastructure/coreml-cascade.json +46 -0
- package/core/infrastructure/coreml-provider.js +81 -0
- package/core/infrastructure/db-utils.js +69 -0
- package/core/infrastructure/dedup-hashing.js +83 -0
- package/core/infrastructure/hardware-capability.js +332 -0
- package/core/infrastructure/index.js +104 -0
- package/core/infrastructure/language-patterns/maps.js +121 -0
- package/core/infrastructure/language-patterns/registry-core.js +323 -0
- package/core/infrastructure/language-patterns/registry-data-query.js +155 -0
- package/core/infrastructure/language-patterns/registry-object-oriented.js +285 -0
- package/core/infrastructure/language-patterns/registry-tooling.js +240 -0
- package/core/infrastructure/language-patterns/registry-web-style.js +143 -0
- package/core/infrastructure/language-patterns/registry.js +19 -0
- package/core/infrastructure/language-patterns.js +141 -0
- package/core/infrastructure/llm-provider.js +733 -0
- package/core/infrastructure/manifest.json +46 -0
- package/core/infrastructure/maxsim.wasm +0 -0
- package/core/infrastructure/model-fetcher.js +423 -0
- package/core/infrastructure/model-registry.js +214 -0
- package/core/infrastructure/native-inference.js +587 -0
- package/core/infrastructure/native-resolver.js +187 -0
- package/core/infrastructure/native-sparse-gram.js +257 -0
- package/core/infrastructure/native-tokenizer.js +160 -0
- package/core/infrastructure/onnx-mutex.js +45 -0
- package/core/infrastructure/onnx-session-utils.js +261 -0
- package/core/infrastructure/ort-pipeline.js +111 -0
- package/core/infrastructure/project-detector.js +102 -0
- package/core/infrastructure/quantization.js +410 -0
- package/core/infrastructure/simd-distance.js +502 -0
- package/core/infrastructure/simd-distance.wasm +0 -0
- package/core/infrastructure/tree-sitter-provider.js +665 -0
- package/core/infrastructure/webgpu-maxsim.js +222 -0
- package/core/query/index.js +35 -0
- package/core/query/intent-detector.js +201 -0
- package/core/query/intent-router.js +156 -0
- package/core/query/query-router-catboost.js +222 -0
- package/core/query/query-router-ml.js +266 -0
- package/core/query/query-router.js +213 -0
- package/core/ranking/cascaded-scorer.js +379 -0
- package/core/ranking/flashrank.js +810 -0
- package/core/ranking/index.js +49 -0
- package/core/ranking/late-interaction-index.js +2383 -0
- package/core/ranking/late-interaction-model.js +812 -0
- package/core/ranking/local-reranker.js +374 -0
- package/core/ranking/mmr.js +379 -0
- package/core/ranking/quality-scorer.js +363 -0
- package/core/search/context-expander.js +1167 -0
- package/core/search/dedup/sibling-expander.js +327 -0
- package/core/search/index.js +16 -0
- package/core/search/search-boost.js +259 -0
- package/core/search/search-cli.js +544 -0
- package/core/search/search-format.js +282 -0
- package/core/search/search-fusion.js +327 -0
- package/core/search/search-hybrid.js +204 -0
- package/core/search/search-pattern-chunks.js +337 -0
- package/core/search/search-pattern-planner.js +439 -0
- package/core/search/search-pattern-prefilter.js +412 -0
- package/core/search/search-pattern-ripgrep.js +663 -0
- package/core/search/search-pattern.js +463 -0
- package/core/search/search-postprocess.js +452 -0
- package/core/search/search-semantic.js +706 -0
- package/core/search/search-server.js +554 -0
- package/core/search/session-daemon-prewarm.mjs +164 -0
- package/core/search/session-warmup.js +595 -0
- package/core/search/sweet-search.js +632 -0
- package/core/search/warmup-metrics.js +532 -0
- package/core/start-server.js +6 -0
- package/core/training/query-router/features/extractor.js +762 -0
- package/core/training/query-router/features/multilingual-patterns.js +431 -0
- package/core/training/query-router/features/text-segmenter.js +303 -0
- package/core/training/query-router/features/unicode-utils.js +383 -0
- package/core/training/query-router/output/v45_router_d4.js +11521 -0
- package/core/training/query-router/output/v46_router_d4.js +11498 -0
- package/core/vector-store/binary-heap.js +227 -0
- package/core/vector-store/binary-hnsw-index.js +1004 -0
- package/core/vector-store/float-vector-store.js +234 -0
- package/core/vector-store/hnsw-index.js +580 -0
- package/core/vector-store/index.js +39 -0
- package/core/vector-store/seismic-index.js +498 -0
- package/core/vocabulary/index.js +84 -0
- package/core/vocabulary/vocab-constants.js +20 -0
- package/core/vocabulary/vocab-miner-extractors.js +375 -0
- package/core/vocabulary/vocab-miner-nl.js +404 -0
- package/core/vocabulary/vocab-miner-utils.js +146 -0
- package/core/vocabulary/vocab-miner.js +574 -0
- package/core/vocabulary/vocab-prewarm-cli.js +110 -0
- package/core/vocabulary/vocab-ranker.js +492 -0
- package/core/vocabulary/vocab-warmer.js +523 -0
- package/core/vocabulary/vocab-warmup-orchestrator.js +425 -0
- package/core/vocabulary/vocabulary-utils.js +704 -0
- package/crates/wasm-router/pkg/package.json +13 -0
- package/crates/wasm-router/pkg/query_router_wasm.d.ts +36 -0
- package/crates/wasm-router/pkg/query_router_wasm.js +271 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm +0 -0
- package/crates/wasm-router/pkg/query_router_wasm_bg.wasm.d.ts +19 -0
- package/mcp/config-gen.js +121 -0
- package/mcp/server.js +335 -0
- package/mcp/tool-handlers.js +476 -0
- package/package.json +131 -9
- package/scripts/benchmark-harness.js +794 -0
- package/scripts/init.js +1058 -0
- package/scripts/smoke-test.js +435 -0
- package/scripts/uninstall.js +478 -0
- package/scripts/verify-runtime.js +176 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text Segmenter for CJK and Thai Languages
|
|
3
|
+
*
|
|
4
|
+
* Provides proper word segmentation for languages that don't use spaces:
|
|
5
|
+
* - Chinese (zh): No spaces between characters
|
|
6
|
+
* - Japanese (ja): Mix of kanji, hiragana, katakana
|
|
7
|
+
* - Korean (ko): Uses spaces but syllable blocks can be segmented
|
|
8
|
+
* - Thai (th): No spaces between words
|
|
9
|
+
*
|
|
10
|
+
* Uses Intl.Segmenter (Node 16+) with fallback for older environments.
|
|
11
|
+
*
|
|
12
|
+
* P0 Fix: Added to support multilingual tokenization for training data
|
|
13
|
+
*
|
|
14
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Segmenter
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { normalizeNFC, detectScripts, SCRIPT_RANGES } from './unicode-utils.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if Intl.Segmenter is available
|
|
21
|
+
*/
|
|
22
|
+
const HAS_SEGMENTER = typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cache for segmenters by locale
|
|
26
|
+
*/
|
|
27
|
+
const segmenterCache = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get or create a word segmenter for a locale
|
|
31
|
+
*
|
|
32
|
+
* @param {string} locale - BCP 47 locale code (e.g., 'zh', 'ja', 'ko', 'th')
|
|
33
|
+
* @returns {Intl.Segmenter|null} - Segmenter or null if not available
|
|
34
|
+
*/
|
|
35
|
+
function getSegmenter(locale) {
|
|
36
|
+
if (!HAS_SEGMENTER) return null;
|
|
37
|
+
|
|
38
|
+
if (!segmenterCache.has(locale)) {
|
|
39
|
+
try {
|
|
40
|
+
segmenterCache.set(locale, new Intl.Segmenter(locale, { granularity: 'word' }));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// Locale not supported
|
|
43
|
+
segmenterCache.set(locale, null);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return segmenterCache.get(locale);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Segment text into words using Intl.Segmenter
|
|
52
|
+
*
|
|
53
|
+
* @param {string} text - Text to segment
|
|
54
|
+
* @param {string} locale - BCP 47 locale code
|
|
55
|
+
* @returns {string[]} - Array of word segments
|
|
56
|
+
*/
|
|
57
|
+
function segmentWithIntl(text, locale) {
|
|
58
|
+
const segmenter = getSegmenter(locale);
|
|
59
|
+
if (!segmenter) return null;
|
|
60
|
+
|
|
61
|
+
const segments = [];
|
|
62
|
+
for (const segment of segmenter.segment(text)) {
|
|
63
|
+
// Only include word-like segments (skip punctuation, whitespace)
|
|
64
|
+
if (segment.isWordLike) {
|
|
65
|
+
segments.push(segment.segment);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return segments;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Fallback segmentation for CJK when Intl.Segmenter is not available
|
|
73
|
+
*
|
|
74
|
+
* Uses character-based segmentation for CJK ideographs.
|
|
75
|
+
* This is a simple fallback - not linguistically accurate but functional.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} text - Text to segment
|
|
78
|
+
* @returns {string[]} - Array of character segments
|
|
79
|
+
*/
|
|
80
|
+
function fallbackCJKSegment(text) {
|
|
81
|
+
const segments = [];
|
|
82
|
+
let currentRun = '';
|
|
83
|
+
let currentType = null; // 'cjk', 'hangul', 'other'
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < text.length;) {
|
|
86
|
+
const codePoint = text.codePointAt(i);
|
|
87
|
+
const charLen = codePoint > 0xFFFF ? 2 : 1;
|
|
88
|
+
const char = text.substring(i, i + charLen);
|
|
89
|
+
|
|
90
|
+
let type = 'other';
|
|
91
|
+
|
|
92
|
+
// Check CJK ideographs
|
|
93
|
+
for (const [start, end] of SCRIPT_RANGES.cjk) {
|
|
94
|
+
if (codePoint >= start && codePoint <= end) {
|
|
95
|
+
type = 'cjk';
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check Hangul
|
|
101
|
+
if (type === 'other') {
|
|
102
|
+
for (const [start, end] of SCRIPT_RANGES.hangul) {
|
|
103
|
+
if (codePoint >= start && codePoint <= end) {
|
|
104
|
+
type = 'hangul';
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check hiragana/katakana
|
|
111
|
+
if (type === 'other') {
|
|
112
|
+
for (const [start, end] of SCRIPT_RANGES.hiragana || []) {
|
|
113
|
+
if (codePoint >= start && codePoint <= end) {
|
|
114
|
+
type = 'cjk'; // Treat as CJK for segmentation
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const [start, end] of SCRIPT_RANGES.katakana || []) {
|
|
119
|
+
if (codePoint >= start && codePoint <= end) {
|
|
120
|
+
type = 'cjk'; // Treat as CJK for segmentation
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Segment on type change
|
|
127
|
+
if (type !== currentType && currentRun) {
|
|
128
|
+
if (currentType === 'cjk') {
|
|
129
|
+
// Split CJK into individual characters
|
|
130
|
+
segments.push(...currentRun.split(''));
|
|
131
|
+
} else if (currentRun.trim()) {
|
|
132
|
+
segments.push(currentRun.trim());
|
|
133
|
+
}
|
|
134
|
+
currentRun = '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
currentType = type;
|
|
138
|
+
|
|
139
|
+
// Skip whitespace
|
|
140
|
+
if (/\s/.test(char)) {
|
|
141
|
+
if (currentRun.trim()) {
|
|
142
|
+
if (currentType === 'cjk') {
|
|
143
|
+
segments.push(...currentRun.split(''));
|
|
144
|
+
} else {
|
|
145
|
+
segments.push(currentRun.trim());
|
|
146
|
+
}
|
|
147
|
+
currentRun = '';
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
currentRun += char;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
i += charLen;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle remaining run
|
|
157
|
+
if (currentRun.trim()) {
|
|
158
|
+
if (currentType === 'cjk') {
|
|
159
|
+
segments.push(...currentRun.split(''));
|
|
160
|
+
} else {
|
|
161
|
+
segments.push(currentRun.trim());
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return segments.filter(s => s.length > 0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Fallback segmentation for Thai when Intl.Segmenter is not available
|
|
170
|
+
*
|
|
171
|
+
* Thai is complex - without proper dictionary-based segmentation,
|
|
172
|
+
* we can only split on spaces and punctuation.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} text - Text to segment
|
|
175
|
+
* @returns {string[]} - Array of segments (basic fallback)
|
|
176
|
+
*/
|
|
177
|
+
function fallbackThaiSegment(text) {
|
|
178
|
+
// Thai range: 0x0E00-0x0E7F
|
|
179
|
+
// Without proper dictionary, we can only do basic splitting
|
|
180
|
+
// This is a simplified fallback - production should use Intl.Segmenter
|
|
181
|
+
return text
|
|
182
|
+
.split(/[\s\u0E2F\u0E5A\u0E5B]+/) // Split on spaces and Thai punctuation
|
|
183
|
+
.filter(s => s.length > 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Segment text into words, handling CJK and Thai languages
|
|
188
|
+
*
|
|
189
|
+
* @param {string} text - Text to segment
|
|
190
|
+
* @param {string} [locale] - Optional locale hint (auto-detected if not provided)
|
|
191
|
+
* @returns {string[]} - Array of word segments
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* segmentText('用户服务') // ['用', '户', '服', '务'] (fallback) or ['用户', '服务'] (Intl)
|
|
195
|
+
* segmentText('hello world') // ['hello', 'world']
|
|
196
|
+
* segmentText('สวัสดีครับ', 'th') // Proper Thai segmentation if Intl.Segmenter available
|
|
197
|
+
*/
|
|
198
|
+
export function segmentText(text, locale) {
|
|
199
|
+
if (!text || typeof text !== 'string') return [];
|
|
200
|
+
|
|
201
|
+
// Normalize to NFC
|
|
202
|
+
const normalized = normalizeNFC(text);
|
|
203
|
+
|
|
204
|
+
// Detect scripts if locale not provided
|
|
205
|
+
if (!locale) {
|
|
206
|
+
const scripts = detectScripts(normalized);
|
|
207
|
+
|
|
208
|
+
if (scripts.has('thai')) {
|
|
209
|
+
locale = 'th';
|
|
210
|
+
} else if (scripts.has('cjk') || scripts.has('hiragana') || scripts.has('katakana')) {
|
|
211
|
+
// Prefer Japanese if we see hiragana/katakana
|
|
212
|
+
if (scripts.has('hiragana') || scripts.has('katakana')) {
|
|
213
|
+
locale = 'ja';
|
|
214
|
+
} else {
|
|
215
|
+
locale = 'zh';
|
|
216
|
+
}
|
|
217
|
+
} else if (scripts.has('hangul')) {
|
|
218
|
+
locale = 'ko';
|
|
219
|
+
} else {
|
|
220
|
+
// Default: simple whitespace split for Latin scripts
|
|
221
|
+
return normalized.split(/\s+/).filter(s => s.length > 0);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Try Intl.Segmenter first
|
|
226
|
+
const intlResult = segmentWithIntl(normalized, locale);
|
|
227
|
+
if (intlResult) {
|
|
228
|
+
return intlResult;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Fallback based on locale
|
|
232
|
+
switch (locale) {
|
|
233
|
+
case 'zh':
|
|
234
|
+
case 'ja':
|
|
235
|
+
return fallbackCJKSegment(normalized);
|
|
236
|
+
case 'th':
|
|
237
|
+
return fallbackThaiSegment(normalized);
|
|
238
|
+
case 'ko':
|
|
239
|
+
// Korean uses spaces, so simple split works
|
|
240
|
+
return normalized.split(/\s+/).filter(s => s.length > 0);
|
|
241
|
+
default:
|
|
242
|
+
return normalized.split(/\s+/).filter(s => s.length > 0);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Check if text requires special segmentation (CJK/Thai)
|
|
248
|
+
*
|
|
249
|
+
* @param {string} text - Text to check
|
|
250
|
+
* @returns {boolean} - True if text contains CJK or Thai characters
|
|
251
|
+
*/
|
|
252
|
+
export function requiresSegmentation(text) {
|
|
253
|
+
if (!text || typeof text !== 'string') return false;
|
|
254
|
+
|
|
255
|
+
const scripts = detectScripts(text);
|
|
256
|
+
return scripts.has('cjk') ||
|
|
257
|
+
scripts.has('hiragana') ||
|
|
258
|
+
scripts.has('katakana') ||
|
|
259
|
+
scripts.has('hangul') ||
|
|
260
|
+
scripts.has('thai');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get the best locale for segmentation based on text content
|
|
265
|
+
*
|
|
266
|
+
* @param {string} text - Text to analyze
|
|
267
|
+
* @returns {string|null} - Locale code or null if not CJK/Thai
|
|
268
|
+
*/
|
|
269
|
+
export function detectSegmentationLocale(text) {
|
|
270
|
+
if (!text || typeof text !== 'string') return null;
|
|
271
|
+
|
|
272
|
+
const scripts = detectScripts(text);
|
|
273
|
+
|
|
274
|
+
if (scripts.has('thai')) return 'th';
|
|
275
|
+
if (scripts.has('hiragana') || scripts.has('katakana')) return 'ja';
|
|
276
|
+
if (scripts.has('hangul')) return 'ko';
|
|
277
|
+
if (scripts.has('cjk')) return 'zh';
|
|
278
|
+
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Tokenize text for training data, properly handling all languages
|
|
284
|
+
*
|
|
285
|
+
* @param {string} text - Text to tokenize
|
|
286
|
+
* @returns {string[]} - Array of tokens
|
|
287
|
+
*/
|
|
288
|
+
export function tokenizeForTraining(text) {
|
|
289
|
+
if (!text || typeof text !== 'string') return [];
|
|
290
|
+
|
|
291
|
+
const normalized = normalizeNFC(text.toLowerCase());
|
|
292
|
+
|
|
293
|
+
if (requiresSegmentation(normalized)) {
|
|
294
|
+
return segmentText(normalized);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// For non-CJK/Thai: split on whitespace and punctuation, keep meaningful tokens
|
|
298
|
+
return normalized
|
|
299
|
+
.split(/[\s,;:!?()[\]{}"']+/)
|
|
300
|
+
.filter(s => s.length >= 2); // Skip single chars for Latin scripts
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export { HAS_SEGMENTER };
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unicode Utilities for Query Router
|
|
3
|
+
*
|
|
4
|
+
* Provides UAX #31 compliant identifier detection using @babel/helper-validator-identifier.
|
|
5
|
+
* Supports 15+ Unicode scripts for multilingual code search.
|
|
6
|
+
*
|
|
7
|
+
* P0 Fix: All string comparisons now use NFC normalization to handle:
|
|
8
|
+
* - Composed vs decomposed characters (e.g., é vs e + ́)
|
|
9
|
+
* - Visually identical but byte-different strings
|
|
10
|
+
*
|
|
11
|
+
* @see https://www.unicode.org/reports/tr31/ - Unicode Identifier and Pattern Syntax
|
|
12
|
+
* @see https://unicode.org/reports/tr15/ - Unicode Normalization Forms
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
isIdentifierStart,
|
|
17
|
+
isIdentifierChar,
|
|
18
|
+
isIdentifierName
|
|
19
|
+
} from '@babel/helper-validator-identifier';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Normalize string to NFC (Canonical Decomposition, followed by Canonical Composition)
|
|
23
|
+
* This ensures that visually identical strings compare as equal.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} s - String to normalize
|
|
26
|
+
* @returns {string} - NFC normalized string
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* normalizeNFC('café') // same output whether composed or decomposed input
|
|
30
|
+
* normalizeNFC('наслеђе') // Serbian with composed/decomposed chars
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeNFC(s) {
|
|
33
|
+
if (!s || typeof s !== 'string') return s;
|
|
34
|
+
return s.normalize('NFC');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Unicode script ranges for detection
|
|
39
|
+
* Based on Unicode 15.0 character blocks
|
|
40
|
+
*/
|
|
41
|
+
export const SCRIPT_RANGES = {
|
|
42
|
+
latin: [
|
|
43
|
+
[0x0041, 0x005A], // A-Z
|
|
44
|
+
[0x0061, 0x007A], // a-z
|
|
45
|
+
[0x00C0, 0x00FF], // Latin Extended-A (accented chars)
|
|
46
|
+
[0x0100, 0x017F], // Latin Extended-B
|
|
47
|
+
[0x0180, 0x024F], // Latin Extended-B continued
|
|
48
|
+
],
|
|
49
|
+
cyrillic: [
|
|
50
|
+
[0x0400, 0x04FF], // Cyrillic
|
|
51
|
+
[0x0500, 0x052F], // Cyrillic Supplement
|
|
52
|
+
],
|
|
53
|
+
greek: [
|
|
54
|
+
[0x0370, 0x03FF], // Greek and Coptic
|
|
55
|
+
],
|
|
56
|
+
arabic: [
|
|
57
|
+
[0x0600, 0x06FF], // Arabic
|
|
58
|
+
[0x0750, 0x077F], // Arabic Supplement
|
|
59
|
+
],
|
|
60
|
+
hebrew: [
|
|
61
|
+
[0x0590, 0x05FF], // Hebrew
|
|
62
|
+
],
|
|
63
|
+
devanagari: [
|
|
64
|
+
[0x0900, 0x097F], // Devanagari (Hindi, Sanskrit, Marathi)
|
|
65
|
+
],
|
|
66
|
+
// P1 Fix: Added missing Indian scripts (500M+ speakers)
|
|
67
|
+
bengali: [
|
|
68
|
+
[0x0980, 0x09FF], // Bengali (Bengali, Assamese)
|
|
69
|
+
],
|
|
70
|
+
tamil: [
|
|
71
|
+
[0x0B80, 0x0BFF], // Tamil
|
|
72
|
+
],
|
|
73
|
+
telugu: [
|
|
74
|
+
[0x0C00, 0x0C7F], // Telugu
|
|
75
|
+
],
|
|
76
|
+
kannada: [
|
|
77
|
+
[0x0C80, 0x0CFF], // Kannada
|
|
78
|
+
],
|
|
79
|
+
malayalam: [
|
|
80
|
+
[0x0D00, 0x0D7F], // Malayalam
|
|
81
|
+
],
|
|
82
|
+
gujarati: [
|
|
83
|
+
[0x0A80, 0x0AFF], // Gujarati
|
|
84
|
+
],
|
|
85
|
+
thai: [
|
|
86
|
+
[0x0E00, 0x0E7F], // Thai
|
|
87
|
+
],
|
|
88
|
+
hangul: [
|
|
89
|
+
[0xAC00, 0xD7AF], // Hangul Syllables
|
|
90
|
+
[0x1100, 0x11FF], // Hangul Jamo
|
|
91
|
+
[0x3130, 0x318F], // Hangul Compatibility Jamo
|
|
92
|
+
],
|
|
93
|
+
cjk: [
|
|
94
|
+
[0x4E00, 0x9FFF], // CJK Unified Ideographs
|
|
95
|
+
[0x3400, 0x4DBF], // CJK Extension A
|
|
96
|
+
[0x20000, 0x2A6DF], // CJK Extension B
|
|
97
|
+
// P1 Fix: Added CJK Extensions C-F (Unicode 13.0+)
|
|
98
|
+
[0x2A700, 0x2B73F], // CJK Extension C
|
|
99
|
+
[0x2B740, 0x2B81F], // CJK Extension D
|
|
100
|
+
[0x2B820, 0x2CEAF], // CJK Extension E
|
|
101
|
+
[0x2CEB0, 0x2EBEF], // CJK Extension F
|
|
102
|
+
[0x30000, 0x3134F], // CJK Extension G (Unicode 13.0)
|
|
103
|
+
[0x31350, 0x323AF], // CJK Extension H (Unicode 15.0)
|
|
104
|
+
],
|
|
105
|
+
hiragana: [
|
|
106
|
+
[0x3040, 0x309F], // Hiragana
|
|
107
|
+
],
|
|
108
|
+
katakana: [
|
|
109
|
+
[0x30A0, 0x30FF], // Katakana
|
|
110
|
+
[0x31F0, 0x31FF], // Katakana Phonetic Extensions
|
|
111
|
+
],
|
|
112
|
+
vietnamese: [
|
|
113
|
+
// Vietnamese uses Latin with diacritics, covered by Latin Extended
|
|
114
|
+
[0x1EA0, 0x1EF9], // Vietnamese-specific Latin Extended Additional
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the Unicode script of a code point
|
|
120
|
+
* @param {number} codePoint - The Unicode code point
|
|
121
|
+
* @returns {string|null} - Script name or null if not in known ranges
|
|
122
|
+
*/
|
|
123
|
+
export function getScript(codePoint) {
|
|
124
|
+
for (const [script, ranges] of Object.entries(SCRIPT_RANGES)) {
|
|
125
|
+
for (const [start, end] of ranges) {
|
|
126
|
+
if (codePoint >= start && codePoint <= end) {
|
|
127
|
+
return script;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if a string is a valid Unicode identifier (UAX #31 compliant)
|
|
136
|
+
*
|
|
137
|
+
* @param {string} s - String to check
|
|
138
|
+
* @returns {boolean} - True if valid identifier
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* isUnicodeIdentifier('AuthService') // true (ASCII)
|
|
142
|
+
* isUnicodeIdentifier('КорисникСервис') // true (Cyrillic)
|
|
143
|
+
* isUnicodeIdentifier('用户服务') // true (CJK)
|
|
144
|
+
* isUnicodeIdentifier('hello world') // false (contains space)
|
|
145
|
+
*/
|
|
146
|
+
export function isUnicodeIdentifier(s) {
|
|
147
|
+
if (!s || typeof s !== 'string' || s.length < 1) return false;
|
|
148
|
+
|
|
149
|
+
// P0 Fix: Normalize to NFC before validation
|
|
150
|
+
const normalized = normalizeNFC(s);
|
|
151
|
+
|
|
152
|
+
// Fast path: use Babel's isIdentifierName for full validation
|
|
153
|
+
return isIdentifierName(normalized);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check if string contains valid Unicode identifier characters (not just ASCII)
|
|
158
|
+
*
|
|
159
|
+
* @param {string} s - String to check
|
|
160
|
+
* @returns {boolean} - True if contains non-ASCII identifier characters
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* hasNonAsciiIdentifierChars('AuthService') // false
|
|
164
|
+
* hasNonAsciiIdentifierChars('КорисникСервис') // true
|
|
165
|
+
* hasNonAsciiIdentifierChars('用户服务') // true
|
|
166
|
+
* hasNonAsciiIdentifierChars('AuthСервис') // true (mixed)
|
|
167
|
+
*/
|
|
168
|
+
export function hasNonAsciiIdentifierChars(s) {
|
|
169
|
+
if (!s || typeof s !== 'string') return false;
|
|
170
|
+
|
|
171
|
+
// P0 Fix: Normalize to NFC before checking
|
|
172
|
+
const normalized = normalizeNFC(s);
|
|
173
|
+
|
|
174
|
+
for (let i = 0; i < normalized.length; ) {
|
|
175
|
+
const codePoint = normalized.codePointAt(i);
|
|
176
|
+
// Non-ASCII identifier character (above 0x7F and valid identifier char)
|
|
177
|
+
if (codePoint > 0x7F && isIdentifierChar(codePoint)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
i += codePoint > 0xFFFF ? 2 : 1;
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Detect all scripts present in a string
|
|
187
|
+
*
|
|
188
|
+
* @param {string} s - String to analyze
|
|
189
|
+
* @returns {Set<string>} - Set of script names found
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* detectScripts('AuthService') // Set(['latin'])
|
|
193
|
+
* detectScripts('КорисникСервис') // Set(['cyrillic'])
|
|
194
|
+
* detectScripts('用户Service') // Set(['cjk', 'latin'])
|
|
195
|
+
*/
|
|
196
|
+
export function detectScripts(s) {
|
|
197
|
+
if (!s || typeof s !== 'string') return new Set();
|
|
198
|
+
|
|
199
|
+
// P0 Fix: Normalize to NFC before analyzing
|
|
200
|
+
const normalized = normalizeNFC(s);
|
|
201
|
+
const scripts = new Set();
|
|
202
|
+
|
|
203
|
+
for (let i = 0; i < normalized.length; ) {
|
|
204
|
+
const codePoint = normalized.codePointAt(i);
|
|
205
|
+
const script = getScript(codePoint);
|
|
206
|
+
if (script) {
|
|
207
|
+
scripts.add(script);
|
|
208
|
+
}
|
|
209
|
+
i += codePoint > 0xFFFF ? 2 : 1;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return scripts;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Check if string contains mixed scripts (code-switching)
|
|
217
|
+
*
|
|
218
|
+
* @param {string} s - String to check
|
|
219
|
+
* @returns {boolean} - True if contains multiple scripts
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* hasMixedScript('AuthService') // false (Latin only)
|
|
223
|
+
* hasMixedScript('КорисникСервис') // false (Cyrillic only)
|
|
224
|
+
* hasMixedScript('用户Service') // true (CJK + Latin)
|
|
225
|
+
* hasMixedScript('AuthСервис') // true (Latin + Cyrillic)
|
|
226
|
+
*/
|
|
227
|
+
export function hasMixedScript(s) {
|
|
228
|
+
const scripts = detectScripts(s);
|
|
229
|
+
return scripts.size > 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if string is primarily non-Latin
|
|
234
|
+
*
|
|
235
|
+
* @param {string} s - String to check
|
|
236
|
+
* @returns {boolean} - True if majority of chars are non-Latin
|
|
237
|
+
*/
|
|
238
|
+
export function isPrimarilyNonLatin(s) {
|
|
239
|
+
if (!s || typeof s !== 'string') return false;
|
|
240
|
+
|
|
241
|
+
let latinCount = 0;
|
|
242
|
+
let nonLatinCount = 0;
|
|
243
|
+
|
|
244
|
+
for (let i = 0; i < s.length; ) {
|
|
245
|
+
const codePoint = s.codePointAt(i);
|
|
246
|
+
const script = getScript(codePoint);
|
|
247
|
+
|
|
248
|
+
if (script === 'latin') {
|
|
249
|
+
latinCount++;
|
|
250
|
+
} else if (script) {
|
|
251
|
+
nonLatinCount++;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
i += codePoint > 0xFFFF ? 2 : 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return nonLatinCount > latinCount;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Extract potential identifiers from a query string
|
|
262
|
+
*
|
|
263
|
+
* P0 Fix: Now uses proper text segmentation for CJK/Thai languages
|
|
264
|
+
* that don't use spaces between words.
|
|
265
|
+
*
|
|
266
|
+
* @param {string} query - Query string to parse
|
|
267
|
+
* @returns {string[]} - Array of potential identifiers
|
|
268
|
+
*
|
|
269
|
+
* @example
|
|
270
|
+
* extractIdentifiers('what calls AuthService') // ['AuthService']
|
|
271
|
+
* extractIdentifiers('КорисникСервис methods') // ['КорисникСервис']
|
|
272
|
+
* extractIdentifiers('用户服务 implementation') // ['用户服务']
|
|
273
|
+
*/
|
|
274
|
+
export function extractIdentifiers(query) {
|
|
275
|
+
if (!query || typeof query !== 'string') return [];
|
|
276
|
+
|
|
277
|
+
// P0 Fix: Normalize to NFC before splitting
|
|
278
|
+
const normalized = normalizeNFC(query);
|
|
279
|
+
|
|
280
|
+
// Detect if CJK/Thai - these need special segmentation
|
|
281
|
+
const scripts = detectScripts(normalized);
|
|
282
|
+
const needsSegmentation = scripts.has('cjk') ||
|
|
283
|
+
scripts.has('hiragana') ||
|
|
284
|
+
scripts.has('katakana') ||
|
|
285
|
+
scripts.has('hangul') ||
|
|
286
|
+
scripts.has('thai');
|
|
287
|
+
|
|
288
|
+
let tokens;
|
|
289
|
+
|
|
290
|
+
if (needsSegmentation) {
|
|
291
|
+
// P0 Fix: Use Intl.Segmenter for CJK/Thai if available
|
|
292
|
+
// Dynamic import would be async, so we use inline segmentation
|
|
293
|
+
const locale = scripts.has('thai') ? 'th' :
|
|
294
|
+
(scripts.has('hiragana') || scripts.has('katakana')) ? 'ja' :
|
|
295
|
+
scripts.has('hangul') ? 'ko' : 'zh';
|
|
296
|
+
|
|
297
|
+
if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
|
|
298
|
+
try {
|
|
299
|
+
const segmenter = new Intl.Segmenter(locale, { granularity: 'word' });
|
|
300
|
+
tokens = [...segmenter.segment(normalized)]
|
|
301
|
+
.filter(s => s.isWordLike)
|
|
302
|
+
.map(s => s.segment);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
// Fallback if segmenter fails
|
|
305
|
+
tokens = normalized.split(/[\s,;:!?()[\]{}'"]+/);
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
// Fallback: split on whitespace and punctuation
|
|
309
|
+
tokens = normalized.split(/[\s,;:!?()[\]{}'"]+/);
|
|
310
|
+
}
|
|
311
|
+
} else {
|
|
312
|
+
// Split on whitespace and punctuation (keeping Unicode chars)
|
|
313
|
+
tokens = normalized.split(/[\s,;:!?()[\]{}'"]+/);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return tokens.filter(token => {
|
|
317
|
+
if (!token || token.length < 2) return false;
|
|
318
|
+
return isUnicodeIdentifier(token);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Check if query contains a Unicode identifier (for feature 25)
|
|
324
|
+
*
|
|
325
|
+
* @param {string} query - Query to check
|
|
326
|
+
* @returns {boolean} - True if contains at least one identifier
|
|
327
|
+
*/
|
|
328
|
+
export function hasIdentifierInQuery(query) {
|
|
329
|
+
const identifiers = extractIdentifiers(query);
|
|
330
|
+
return identifiers.length > 0;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Check if query contains a non-ASCII Unicode identifier (for feature 25 enhancement)
|
|
335
|
+
*
|
|
336
|
+
* @param {string} query - Query to check
|
|
337
|
+
* @returns {boolean} - True if contains at least one non-ASCII identifier
|
|
338
|
+
*/
|
|
339
|
+
export function hasNonAsciiIdentifierInQuery(query) {
|
|
340
|
+
const identifiers = extractIdentifiers(query);
|
|
341
|
+
return identifiers.some(id => hasNonAsciiIdentifierChars(id));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Detect the primary language/script of a query
|
|
346
|
+
*
|
|
347
|
+
* @param {string} query - Query to analyze
|
|
348
|
+
* @returns {{script: string, confidence: number}} - Primary script and confidence
|
|
349
|
+
*/
|
|
350
|
+
export function detectPrimaryScript(query) {
|
|
351
|
+
const scripts = detectScripts(query);
|
|
352
|
+
|
|
353
|
+
if (scripts.size === 0) {
|
|
354
|
+
return { script: 'unknown', confidence: 0 };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (scripts.size === 1) {
|
|
358
|
+
return { script: [...scripts][0], confidence: 1.0 };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Count characters per script
|
|
362
|
+
const counts = {};
|
|
363
|
+
for (let i = 0; i < query.length; ) {
|
|
364
|
+
const codePoint = query.codePointAt(i);
|
|
365
|
+
const script = getScript(codePoint);
|
|
366
|
+
if (script) {
|
|
367
|
+
counts[script] = (counts[script] || 0) + 1;
|
|
368
|
+
}
|
|
369
|
+
i += codePoint > 0xFFFF ? 2 : 1;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
373
|
+
const [primaryScript, count] = Object.entries(counts)
|
|
374
|
+
.sort((a, b) => b[1] - a[1])[0];
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
script: primaryScript,
|
|
378
|
+
confidence: count / total
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Re-export Babel functions for direct access if needed
|
|
383
|
+
export { isIdentifierStart, isIdentifierChar, isIdentifierName };
|