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,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 };