@cachly-dev/mcp-server 0.5.36 → 0.5.37
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/dist/index.d.ts +26 -3
- package/dist/index.js +390 -5
- package/dist/index.js.map +1 -1
- package/dist/search.test.js +173 -3
- package/dist/search.test.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +382 -5
- package/src/search.test.ts +217 -3
package/src/search.test.ts
CHANGED
|
@@ -4,13 +4,16 @@
|
|
|
4
4
|
* Run: npx vitest run src/search.test.ts
|
|
5
5
|
*/
|
|
6
6
|
import { describe, it, expect } from 'vitest';
|
|
7
|
-
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS
|
|
7
|
+
import { tokenize, splitMultiQuery, levenshtein, recencyBoost, extractTimestamp, STOPWORDS,
|
|
8
|
+
katakanaToRomaji, arabicLightStem, expandCrossLingual, CROSS_LINGUAL_MAP } from './index.js';
|
|
8
9
|
|
|
9
10
|
// ── Tokenizer ─────────────────────────────────────────────────────────────────
|
|
10
11
|
|
|
11
12
|
describe('tokenize', () => {
|
|
12
13
|
it('lowercases and splits on whitespace', () => {
|
|
13
|
-
|
|
14
|
+
const result = tokenize('Deploy API');
|
|
15
|
+
expect(result).toContain('deploy');
|
|
16
|
+
expect(result).toContain('api');
|
|
14
17
|
});
|
|
15
18
|
|
|
16
19
|
it('filters English stopwords', () => {
|
|
@@ -69,7 +72,12 @@ describe('tokenize', () => {
|
|
|
69
72
|
});
|
|
70
73
|
|
|
71
74
|
it('removes single-char tokens', () => {
|
|
72
|
-
|
|
75
|
+
const result = tokenize('a b c deploy');
|
|
76
|
+
expect(result).toContain('deploy');
|
|
77
|
+
// single chars a, b, c must not appear
|
|
78
|
+
expect(result).not.toContain('a');
|
|
79
|
+
expect(result).not.toContain('b');
|
|
80
|
+
expect(result).not.toContain('c');
|
|
73
81
|
});
|
|
74
82
|
|
|
75
83
|
it('preserves colons and dots in tokens (topic slugs)', () => {
|
|
@@ -280,3 +288,209 @@ describe('extractTimestamp', () => {
|
|
|
280
288
|
expect(extractTimestamp('{"ts":"not-a-date"}')).toBeUndefined();
|
|
281
289
|
});
|
|
282
290
|
});
|
|
291
|
+
|
|
292
|
+
// ── Katakana → Romaji ──────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
describe('katakanaToRomaji', () => {
|
|
295
|
+
it('converts basic katakana vowels', () => {
|
|
296
|
+
expect(katakanaToRomaji('アイウエオ')).toBe('aiueo');
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('converts コンテナ (container)', () => {
|
|
300
|
+
expect(katakanaToRomaji('コンテナ')).toBe('kontena');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('converts デプロイ (deploy)', () => {
|
|
304
|
+
expect(katakanaToRomaji('デプロイ')).toBe('depuroi');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('converts サーバー (server) — long vowel stripped', () => {
|
|
308
|
+
const r = katakanaToRomaji('サーバー');
|
|
309
|
+
expect(r).toContain('sa');
|
|
310
|
+
expect(r).toContain('ba');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('converts digraph チェック (check)', () => {
|
|
314
|
+
const r = katakanaToRomaji('チェック');
|
|
315
|
+
expect(r).toContain('che');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('converts ファイル (file)', () => {
|
|
319
|
+
const r = katakanaToRomaji('ファイル');
|
|
320
|
+
expect(r).toContain('fa');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('handles empty string', () => {
|
|
324
|
+
expect(katakanaToRomaji('')).toBe('');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ── Arabic Light Stemmer ───────────────────────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
describe('arabicLightStem', () => {
|
|
331
|
+
it('strips definite article ال (al-)', () => {
|
|
332
|
+
// الخطأ (al-khata = the error) → خطأ (khata)
|
|
333
|
+
expect(arabicLightStem('الخطأ')).toBe('خطأ');
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('strips و (wa-) prefix from long words', () => {
|
|
337
|
+
// وخطأ → خطأ (and error → error)
|
|
338
|
+
expect(arabicLightStem('وخطأ')).toBe('خطأ');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('strips ب (bi-) prefix', () => {
|
|
342
|
+
expect(arabicLightStem('بسرعة')).toBe('سرعة');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('does not strip from short words (≤3 chars)', () => {
|
|
346
|
+
// بلد (3 chars) should not be stripped
|
|
347
|
+
expect(arabicLightStem('بلد')).toBe('بلد');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('returns unchanged for Latin input', () => {
|
|
351
|
+
expect(arabicLightStem('deploy')).toBe('deploy');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ── Cross-lingual Expansion ────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
describe('expandCrossLingual', () => {
|
|
358
|
+
it('returns Japanese katakana for "deploy"', () => {
|
|
359
|
+
const syns = expandCrossLingual('deploy');
|
|
360
|
+
expect(syns).toContain('デプロイ');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('returns Chinese for "deploy"', () => {
|
|
364
|
+
expect(expandCrossLingual('deploy')).toContain('部署');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('returns Korean for "deploy"', () => {
|
|
368
|
+
expect(expandCrossLingual('deploy')).toContain('배포');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('returns English for Japanese デプロイ', () => {
|
|
372
|
+
expect(expandCrossLingual('デプロイ')).toContain('deploy');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('returns English for Chinese 部署', () => {
|
|
376
|
+
expect(expandCrossLingual('部署')).toContain('deploy');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('returns English for Korean 배포', () => {
|
|
380
|
+
expect(expandCrossLingual('배포')).toContain('deploy');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('returns English for Arabic خطأ (error)', () => {
|
|
384
|
+
expect(expandCrossLingual('خطأ')).toContain('error');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('returns English for Hebrew שגיאה (error)', () => {
|
|
388
|
+
expect(expandCrossLingual('שגיאה')).toContain('error');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('returns empty array for unknown tokens', () => {
|
|
392
|
+
expect(expandCrossLingual('foobar')).toEqual([]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('covers all major languages for "server"', () => {
|
|
396
|
+
const syns = expandCrossLingual('server');
|
|
397
|
+
expect(syns).toContain('サーバー');
|
|
398
|
+
expect(syns).toContain('服务器');
|
|
399
|
+
expect(syns).toContain('서버');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('has at least 50 tech terms in the map', () => {
|
|
403
|
+
expect(CROSS_LINGUAL_MAP.size).toBeGreaterThanOrEqual(50);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// ── RTL Tokenization ───────────────────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
describe('tokenize — RTL (Arabic/Hebrew)', () => {
|
|
410
|
+
it('tokenizes Arabic text into words', () => {
|
|
411
|
+
// "خطأ في النظام" = error in the system
|
|
412
|
+
const tokens = tokenize('خطأ في النظام');
|
|
413
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
414
|
+
// خطأ = error, not a stopword
|
|
415
|
+
const hasError = tokens.some(t => t === 'خطأ' || t === 'error');
|
|
416
|
+
expect(hasError).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('strips Arabic definite article via light stemmer', () => {
|
|
420
|
+
// النظام → نظام (the system → system)
|
|
421
|
+
const tokens = tokenize('النظام');
|
|
422
|
+
expect(tokens).toContain('نظام');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('filters Arabic stopwords', () => {
|
|
426
|
+
const tokens = tokenize('في على من');
|
|
427
|
+
// All three are stopwords
|
|
428
|
+
expect(tokens.filter(t => RTL_RE_TEST(t)).length).toBe(0);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('tokenizes Hebrew text', () => {
|
|
432
|
+
// שגיאה = error
|
|
433
|
+
const tokens = tokenize('שגיאה בשרת');
|
|
434
|
+
expect(tokens.length).toBeGreaterThan(0);
|
|
435
|
+
const hasErr = tokens.some(t => t === 'שגיאה' || t === 'error');
|
|
436
|
+
expect(hasErr).toBe(true);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('handles mixed Arabic + English text', () => {
|
|
440
|
+
const tokens = tokenize('deploy خطأ error');
|
|
441
|
+
expect(tokens).toContain('deploy');
|
|
442
|
+
expect(tokens.some(t => t === 'خطأ' || t === 'error')).toBe(true);
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Helper: test if a string contains RTL characters
|
|
447
|
+
function RTL_RE_TEST(s: string) {
|
|
448
|
+
return /[\u0590-\u05ff\u0600-\u06ff]/.test(s);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── Cross-lingual Search Integration ──────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
describe('tokenize — cross-lingual expansion', () => {
|
|
454
|
+
it('English "deploy" expands to Japanese bigrams', () => {
|
|
455
|
+
const tokens = tokenize('deploy');
|
|
456
|
+
// デプロイ bigrams: デプ, プロ, ロイ
|
|
457
|
+
expect(tokens.some(t => t === 'デプ' || t === 'プロ' || t === 'ロイ')).toBe(true);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('Japanese デプロイ expands to include "deploy"', () => {
|
|
461
|
+
const tokens = tokenize('デプロイ');
|
|
462
|
+
expect(tokens).toContain('deploy');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('Chinese 部署 expands to "deploy"', () => {
|
|
466
|
+
const tokens = tokenize('部署');
|
|
467
|
+
expect(tokens).toContain('deploy');
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it('Korean 배포 expands to "deploy"', () => {
|
|
471
|
+
const tokens = tokenize('배포');
|
|
472
|
+
expect(tokens).toContain('deploy');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('Arabic خطأ expands to "error"', () => {
|
|
476
|
+
const tokens = tokenize('خطأ');
|
|
477
|
+
expect(tokens).toContain('error');
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('Hebrew שגיאה expands to "error"', () => {
|
|
481
|
+
const tokens = tokenize('שגיאה');
|
|
482
|
+
expect(tokens).toContain('error');
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('English "container" expands to all CJK variants', () => {
|
|
486
|
+
const tokens = tokenize('container');
|
|
487
|
+
// コンテナ bigrams should be present
|
|
488
|
+
expect(tokens.some(t => t === 'コン' || t === 'ンテ' || t === 'テナ')).toBe(true);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it('katakana コンテナ generates romaji "kontena"', () => {
|
|
492
|
+
const tokens = tokenize('コンテナ');
|
|
493
|
+
expect(tokens).toContain('kontena');
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|