@cachly-dev/mcp-server 0.5.48 → 0.5.50

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 CHANGED
@@ -33,8 +33,11 @@ declare function expandCrossLingual(token: string): string[];
33
33
  * Features:
34
34
  * • CJK → character bigrams
35
35
  * • Katakana → additionally emits Hepburn romaji tokens (enables romaji queries)
36
- * • Arabic/Hebrew → word tokenization with light Arabic stemming
37
- * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE)
36
+ * • Arabic → word tokenization + iterative prefix stemming (ال/و/ب/ل/ف/ك)
37
+ * • Hebrew → word tokenization + iterative prefix stemming (ה/ו/ב/ל/מ/כ)
38
+ * • Farsi → word tokenization + suffix/prefix stemming (ها/های plural, می prefix)
39
+ * • Hindi → Devanagari word tokenization + light suffix stemming
40
+ * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE↔FA↔HI↔DE↔FR↔ES↔IT↔PT)
38
41
  */
39
42
  declare function tokenize(text: string): string[];
40
43
  /**
package/dist/index.js CHANGED
@@ -47,7 +47,7 @@ import { Redis } from 'ioredis';
47
47
  const API_URL = process.env.CACHLY_API_URL ?? 'https://api.cachly.dev';
48
48
  const JWT = process.env.CACHLY_JWT ?? '';
49
49
  const EMBED_MODEL = process.env.CACHLY_EMBED_MODEL ?? '';
50
- const CURRENT_VERSION = '0.5.48';
50
+ const CURRENT_VERSION = '0.5.50';
51
51
  /**
52
52
  * ┌──────────────────────────────────────────────────────────────────────┐
53
53
  * │ EMBEDDING PROVIDER — pluggable, client-side first │
@@ -403,12 +403,36 @@ const STOPWORDS = new Set([
403
403
  'רק', 'עוד', 'יש', 'אין', 'מה', 'זה', 'זאת', 'אלה', 'כל', 'כן',
404
404
  'לו', 'לה', 'להם', 'בו', 'בה', 'בהם', 'שם', 'כך', 'כן', 'מי',
405
405
  'אשר', 'אחרי', 'לפני', 'תחת', 'בין', 'מאז', 'עד', 'כמו', 'אז',
406
+ // ── Farsi/Persian — common function words, prepositions, pronouns ──
407
+ 'در', 'از', 'به', 'با', 'که', 'این', 'آن', 'را', 'است',
408
+ 'بود', 'باشد', 'شد', 'شده', 'می', 'نه', 'نیست', 'هم', 'هر',
409
+ 'یک', 'اما', 'و', 'یا', 'تا', 'اگر', 'برای', 'چون', 'چه',
410
+ 'کجا', 'کی', 'چطور', 'وقتی', 'بعد', 'قبل', 'مثل', 'همه', 'بعضی',
411
+ 'هیچ', 'آیا', 'خود', 'چند', 'دیگر', 'هنوز', 'همان', 'آنها', 'اینها',
412
+ 'ما', 'شما', 'آنان', 'من', 'تو', 'او', 'آنان', 'ایشان',
413
+ 'روی', 'زیر', 'بین', 'پیش', 'پس', 'طرف', 'داخل', 'خارج', 'کنار',
414
+ 'همچنین', 'مگر', 'ولی', 'وگرنه', 'چرا', 'چگونه', 'کدام',
415
+ // ── Hindi — common particles, postpositions, pronouns, auxiliary verbs ──
416
+ 'है', 'में', 'से', 'को', 'का', 'की', 'के', 'पर', 'और', 'या',
417
+ 'नहीं', 'यह', 'वह', 'एक', 'इस', 'उस', 'भी', 'हो', 'था', 'थी',
418
+ 'थे', 'हैं', 'हूँ', 'लिए', 'तक', 'साथ', 'बाद', 'पहले', 'जो',
419
+ 'जब', 'कैसे', 'क्यों', 'क्या', 'कहाँ', 'कौन', 'हम', 'आप', 'वे',
420
+ 'मैं', 'तुम', 'उन', 'इन', 'ने', 'बहुत', 'सब', 'कुछ', 'फिर',
421
+ 'अब', 'तो', 'ही', 'तरह', 'जैसे', 'करना', 'होना', 'रहा', 'रही',
422
+ 'रहे', 'गया', 'गई', 'गए', 'किया', 'किए', 'कर', 'हुआ', 'हुई',
406
423
  ]);
407
424
  // CJK Unicode ranges used for bigram extraction
408
425
  const CJK_RE = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]/;
409
426
  const SEGMENT_RE = /([\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+|[^\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uac00-\ud7af]+)/g;
410
427
  // RTL Unicode ranges: Arabic (U+0600–U+06FF + extended), Hebrew (U+0590–U+05FF)
411
428
  const RTL_RE = /[\u0590-\u05ff\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb1d-\ufb4f\ufb50-\ufdff\ufe70-\ufeff]/;
429
+ // Distinguish Arabic from Hebrew within RTL segments
430
+ const HEBREW_CHAR_RE = /[\u0590-\u05ff\ufb1d-\ufb4f]/;
431
+ const ARABIC_CHAR_RE = /[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff\ufb50-\ufdff\ufe70-\ufeff]/;
432
+ // Persian/Farsi: exclusive chars not in Arabic (U+067E=پ, U+0686=چ, U+0698=ژ, U+06AF=گ, U+06CC=ی)
433
+ const FARSI_CHAR_RE = /[\u067e\u0686\u0698\u06af\u06cc]/;
434
+ // Devanagari (Hindi, Sanskrit, Marathi …) — U+0900–U+097F + extended
435
+ const DEVANAGARI_RE = /[\u0900-\u097f]/;
412
436
  // ── Katakana → Romaji conversion table (Hepburn system) ──────────────────────
413
437
  // Digraphs must be listed before single chars so they match first.
414
438
  const KATA_ROMAJI_TABLE = [
@@ -509,7 +533,86 @@ function arabicLightStem(word) {
509
533
  }
510
534
  return result;
511
535
  }
512
- // ── Cross-lingual tech term dictionary ────────────────────────────────────────
536
+ /**
537
+ * Hebrew light stemmer — strips the definite article and common prefix particles
538
+ * that attach directly to words (no space) in Hebrew.
539
+ *
540
+ * Handles:
541
+ * ה (ha-) — definite article: הפריסה → פריסה
542
+ * ו (ve-/u-) — conjunction “and”: ופריסה → פריסה
543
+ * ב (be-/bi-) — preposition “in”: בסביבה → סביבה
544
+ * ל (le-/li-) — preposition “for”: לשרת → שרת
545
+ * מ (mi-/me-) — preposition “from”: מהשרת → השרת → שרת
546
+ * כ (ke-/ki-) — preposition “like”: כשרת → שרת
547
+ *
548
+ * Iterative (max 3 passes): מהפריסה → הפריסה → פריסה.
549
+ * Only strips when result is still ≥ 3 chars.
550
+ */
551
+ function hebrewLightStem(word) {
552
+ let result = word;
553
+ for (let i = 0; i < 3; i++) {
554
+ const prev = result;
555
+ if (result.length > 3 && 'הובלמכ'.includes(result[0]) && HEBREW_CHAR_RE.test(result[1])) {
556
+ result = result.slice(1);
557
+ }
558
+ if (result === prev)
559
+ break;
560
+ }
561
+ return result;
562
+ }
563
+ /**
564
+ * Farsi/Persian light stemmer — handles Persian morphology.
565
+ * Strips:
566
+ * \u0647\u0627 / \u0647\u0627\u06cc (ha/haye) — plural suffixes: \u0633\u0631\u0648\u0631\u0647\u0627 \u2192 \u0633\u0631\u0648\u0631
567
+ * \u0645\u06cc\u200c / \u0645\u06cc (mi-) — present-tense prefix: \u0645\u06cc\u200c\u06a9\u0646\u062f \u2192 \u06a9\u0646\u062f
568
+ * \u0646\u0645\u06cc\u200c (nami-) — negated present: \u0646\u0645\u06cc\u200c\u0634\u0648\u062f \u2192 \u0634\u0648\u062f
569
+ * Only strips when the result is still \u2265 3 chars.
570
+ */
571
+ function farsiLightStem(word) {
572
+ let result = word;
573
+ // Suffixes first (longest first)
574
+ if (result.endsWith('\u0647\u0627\u06cc') && result.length > 5)
575
+ result = result.slice(0, -3);
576
+ else if (result.endsWith('\u0647\u0627') && result.length > 4)
577
+ result = result.slice(0, -2);
578
+ // Prefixes
579
+ if (result.startsWith('\u0646\u0645\u06cc\u200c') && result.length > 5)
580
+ result = result.slice(4);
581
+ else if (result.startsWith('\u0645\u06cc\u200c') && result.length > 4)
582
+ result = result.slice(3);
583
+ else if (result.startsWith('\u0645\u06cc') && result.length > 4)
584
+ result = result.slice(2);
585
+ return result;
586
+ }
587
+ /**
588
+ * Hindi light stemmer — strips the most common inflectional suffixes.
589
+ * Handles basic verb forms and oblique plural markers.
590
+ * Avoids over-stemming loanwords (most tech terms in Hindi are English loanwords).
591
+ * Only strips when result is still \u2265 3 chars.
592
+ */
593
+ function hindiLightStem(word) {
594
+ const result = word;
595
+ // Infinitive / verb suffixes (longest first to avoid partial match)
596
+ if (result.endsWith('\u0928\u093e') && result.length > 4)
597
+ return result.slice(0, -2); // \u0928\u093e (nā) infinitive
598
+ if (result.endsWith('\u0928\u0947') && result.length > 4)
599
+ return result.slice(0, -2); // \u0928\u0947 (ne) ergative
600
+ if (result.endsWith('\u0928\u0940') && result.length > 4)
601
+ return result.slice(0, -2); // \u0928\u0940 (nī) fem infinitive
602
+ if (result.endsWith('\u0924\u093e') && result.length > 4)
603
+ return result.slice(0, -2); // \u0924\u093e (tā) m present participle
604
+ if (result.endsWith('\u0924\u0940') && result.length > 4)
605
+ return result.slice(0, -2); // \u0924\u0940 (tī) f present participle
606
+ if (result.endsWith('\u0915\u0930') && result.length > 4)
607
+ return result.slice(0, -2); // \u0915\u0930 (kar) conjunctive
608
+ if (result.endsWith('\u0913\u0902') && result.length > 4)
609
+ return result.slice(0, -2); // \u0913\u0902 (oṃ) oblique plural
610
+ if (result.endsWith('\u0907\u092f\u093e\u0902') && result.length > 5)
611
+ return result.slice(0, -4); // \u0907\u092f\u093e\u0902 (iyāṃ) f plural
612
+ if (result.endsWith('\u0940\u092f\u093e\u0902') && result.length > 5)
613
+ return result.slice(0, -4); // \u0940\u092f\u093e\u0902 (īyāṃ)
614
+ return result;
615
+ }
513
616
  // Maps tokens (English, Japanese katakana, Chinese, Korean, Arabic, Hebrew)
514
617
  // to their equivalents in other languages. Used to expand tokens at search time
515
618
  // so "deploy" finds documents containing "デプロイ" / "部署" / "배포", and vice versa.
@@ -517,27 +620,30 @@ function arabicLightStem(word) {
517
620
  // Keys must be lowercase. CJK values will be bigram-expanded at use time.
518
621
  const CROSS_LINGUAL_MAP = new Map([
519
622
  // English → all
520
- ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação']],
521
- ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação']],
623
+ ['deploy', ['デプロイ', '部署', '배포', 'deployment', 'deploying', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellen', 'bereitstellung', 'déployer', 'déploiement', 'desplegar', 'despliegue', 'distribuire', 'distribuzione', 'implantar', 'implantação']],
624
+ ['deployment', ['デプロイ', '部署', '배포', 'deploy', 'نشر', 'פריסה', 'استقرار', 'तैनाती', 'bereitstellung', 'déploiement', 'despliegue', 'distribuzione', 'implantação']],
522
625
  ['container', ['コンテナ', '容器', '컨테이너', 'docker']],
523
- ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת']],
524
- ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'base de données', 'base de datos', 'banco de dados']],
525
- ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון']],
526
- ['error', ['エラー', '错误', '오류', 'خطأ', 'שגיאה', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro']],
527
- ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل']],
528
- ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir']],
529
- ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'bauen', 'construire', 'construir', 'costruire']],
530
- ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'testen', 'tester', 'probar', 'testare', 'testar']],
531
- ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação']],
532
- ['authentication', ['auth', 'مصادقة', 'אימות', '認証', '认证', '인증', 'login', 'oauth']],
626
+ ['server', ['サーバー', 'サーバ', '服务器', '서버', 'سيرفر', 'שרת', 'سرور', 'सर्वर']],
627
+ ['database', ['データベース', '数据库', '데이터베이스', 'db', 'قاعدة البيانات', 'datenbank', 'پایگاه داده', 'डेटाबेس', 'base de données', 'base de datos', 'banco de dados']],
628
+ ['cache', ['キャッシュ', '缓存', '캐시', 'كاش', 'מטמון', 'کش', 'कैश']],
629
+ ['error', ['エラー', '错误', '오류', 'خطأ', 'شגיאה', 'خطا', 'त्रुटि', 'गलती', 'exception', 'err', 'fehler', 'erreur', 'fallo', 'errore', 'erro']],
630
+ ['bug', ['バグ', '缺陷', '버그', 'issue', 'defect', 'خلل', 'اشکال', 'बग']],
631
+ ['fix', ['修正', '修复', '수정', 'bugfix', 'patch', 'hotfix', 'إصلاح', 'תיקון', 'رفع', 'सुधार', 'beheben', 'behoben', 'réparer', 'correction', 'arreglar', 'corrección', 'correggere', 'corrigir']],
632
+ ['build', ['ビルド', '构建', '빌드', 'بناء', 'בנייה', 'ساخت', 'निर्माण', 'bauen', 'construire', 'construir', 'costruire']],
633
+ ['test', ['テスト', '测试', '테스트', 'اختبار', 'בדיקה', 'آزمایش', 'परीक्षण', 'testen', 'tester', 'probar', 'testare', 'testar']],
634
+ ['auth', ['認証', '认证', '인증', 'authentication', 'login', 'oauth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', 'authentifizierung', 'authentification', 'autenticación', 'autenticazione', 'autenticação']],
635
+ ['authentication', ['auth', 'مصادقة', 'אימות', 'احراز هویت', 'प्रमाणीकरण', '認証', '认证', '인증', 'login', 'oauth']],
636
+ ['environment', ['環境', '环境', '환경', 'env', 'بيئة', 'סביבה', 'umgebung', 'environnement', 'entorno', 'ambiente']],
637
+ ['secret', ['key', 'token', 'password', 'jwt', 'مفتاح', 'מפתח', 'geheimnis', 'schlüssel', 'clave', 'clé', 'chiave', 'chave']],
638
+ ['problem', ['error', 'issue', 'bug', 'failure', 'مشكلة', 'بعيا', '問題', '問題', '오류', 'fehler', 'problème', 'problema']],
533
639
  ['kubernetes', ['クベルネテス', 'k8s', 'kube']],
534
- ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'netzwerk', 'réseau', 'red', 'rete', 'rede']],
640
+ ['network', ['ネットワーク', '网络', '네트워크', 'شبكة', 'רשת', 'شبکه', 'नेटवर्क', 'netzwerk', 'réseau', 'red', 'rete', 'rede']],
535
641
  ['timeout', ['タイムアウト', '超时', '타임아웃', 'مهلة']],
536
- ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'speicher', 'mémoire', 'memoria', 'memória']],
537
- ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração']],
538
- ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'installieren', 'installer', 'instalar', 'installare']],
539
- ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar']],
540
- ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'protokoll', 'protokolle', 'journal', 'registro']],
642
+ ['memory', ['メモリ', '内存', '메모리', 'ram', 'ذاكرة', 'זיכרון', 'حافظه', 'मेमोरी', 'speicher', 'mémoire', 'memoria', 'memória']],
643
+ ['config', ['設定', '配置', '설정', 'configuration', 'settings', 'conf', 'إعدادات', 'הגדרות', 'پیکربندی', 'कॉन्फ़िग', 'konfiguration', 'einstellungen', 'configuración', 'configurazione', 'configuração']],
644
+ ['install', ['インストール', '安装', '설치', 'تثبيت', 'התקנה', 'نصب', 'इंस्टॉल', 'installieren', 'installer', 'instalar', 'installare']],
645
+ ['update', ['アップデート', '更新', '업데이트', 'upgrade', 'تحديث', 'עדכון', 'به‌روزرسانی', 'अपडेट', 'aktualisieren', 'aktualisierung', 'actualizar', 'aggiornare', 'atualizar']],
646
+ ['log', ['ログ', '日志', '로그', 'logging', 'سجل', 'יומן', 'ثبت', 'लॉग', 'protokoll', 'protokolle', 'journal', 'registro']],
541
647
  ['port', ['ポート', '端口', '포트', 'منفذ', 'פורט']],
542
648
  ['file', ['ファイル', '文件', '파일', 'ملف', 'קובץ']],
543
649
  ['image', ['イメージ', '镜像', '이미지', 'صورة', 'תמונה', 'abbild', 'imagen', 'imagem']],
@@ -550,10 +656,10 @@ const CROSS_LINGUAL_MAP = new Map([
550
656
  ['token', ['トークン', '令牌', '토큰', 'jwt', 'secret', 'رمز', 'אסימון']],
551
657
  ['health', ['ヘルス', '健康', '헬스', 'healthcheck', 'probe', 'صحة']],
552
658
  ['migration', ['マイグレーション', '迁移', '마이그레이션', 'migrate', 'هجرة']],
553
- ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança']],
554
- ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar']],
555
- ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho']],
556
- ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
659
+ ['backup', ['バックアップ', '备份', '백업', 'نسخ احتياطي', 'גיבוי', 'پشتیبان‌گیری', 'बैकअप', 'sicherung', 'sauvegarde', 'respaldo', 'cópia de segurança']],
660
+ ['monitor', ['モニター', '监控', '모니터링', 'monitoring', 'metrics', 'alert', 'مراقبة', 'ניטור', 'نظارت', 'निगरानी', 'überwachung', 'surveiller', 'monitoreo', 'monitorare', 'monitorar']],
661
+ ['performance', ['パフォーマンス', '性能', '성능', 'latency', 'throughput', 'عملکرد', 'प्रदर्शन', 'leistung', 'performances', 'rendimiento', 'prestazioni', 'desempenho']],
662
+ ['connection', ['接続', '连接', '연결', 'conn', 'socket', 'اتصال', 'חיבור', 'اتصال', 'कनेक्शन', 'verbindung', 'connexion', 'conexión', 'connessione', 'conexão']],
557
663
  ['queue', ['キュー', '队列', '큐', 'طابور', 'warteschlange', 'file d\'attente', 'cola', 'coda', 'fila']],
558
664
  ['redis', ['レディス', '레디스']],
559
665
  ['nginx', ['エンジンエックス']],
@@ -567,10 +673,10 @@ const CROSS_LINGUAL_MAP = new Map([
567
673
  ['git', ['version control', 'repo', 'repository', 'commit', 'push', 'pull']],
568
674
  ['ci', ['pipeline', 'github actions', 'gitlab', 'jenkins', 'build']],
569
675
  ['cd', ['deploy', 'deployment', 'release']],
570
- ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'debuggen', 'déboguer', 'depurar']],
571
- ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'absturz', 'panne', 'plantage', 'caída', 'falha']],
572
- ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
573
- ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão']],
676
+ ['debug', ['デバッグ', '调试', '디버그', 'debugging', 'breakpoint', 'تصحيح', 'איתור באגים', 'اشکال‌زدایی', 'डीबग', 'debuggen', 'déboguer', 'depurar']],
677
+ ['crash', ['クラッシュ', '崩溃', '크래시', 'panic', 'segfault', 'انهيار', 'קריסה', 'خرابی', 'क्रैश', 'absturz', 'panne', 'plantage', 'caída', 'falha']],
678
+ ['restart', ['再起動', '重启', '재시작', 'reboot', 'إعادة تشغيل', 'הפעלה מחדש', 'راه‌اندازی مجدد', 'पुنरारंभ', 'neustart', 'redémarrer', 'reiniciar', 'riavviare']],
679
+ ['permission', ['権限', '权限', '권한', 'access', 'acl', 'chmod', 'صلاحية', 'הרשאה', 'مجوز', 'अनुमति', 'berechtigung', 'berechtigungen', 'permiso', 'permesso', 'permissão']],
574
680
  // Japanese katakana → English
575
681
  ['デプロイ', ['deploy', 'deployment']],
576
682
  ['コンテナ', ['container', 'docker']],
@@ -707,6 +813,13 @@ const CROSS_LINGUAL_MAP = new Map([
707
813
  ['قرص', ['disk', 'storage']],
708
814
  ['خادم', ['server']],
709
815
  ['برنامج', ['application', 'software', 'app']],
816
+ // Arabic terms from blog examples
817
+ ['مشكلة', ['error', 'issue', 'bug', 'problem', 'failure']],
818
+ ['بيئة', ['environment', 'env']],
819
+ ['متغيرات', ['variables', 'env', 'environment']],
820
+ ['إنتاج', ['production', 'prod']],
821
+ ['سري', ['secret', 'key', 'token']],
822
+ ['مفتاح سري', ['secret', 'key']],
710
823
  // Hebrew → English
711
824
  ['שגיאה', ['error']],
712
825
  ['תיקון', ['fix']],
@@ -742,6 +855,74 @@ const CROSS_LINGUAL_MAP = new Map([
742
855
  ['ממשק תכנות', ['api', 'interface']],
743
856
  ['ביצועים', ['performance']],
744
857
  ['אחסון', ['storage', 'disk']],
858
+ // Hebrew terms from blog examples
859
+ ['פריסה', ['deploy', 'deployment']],
860
+ ['בעיה', ['error', 'issue', 'bug', 'problem']],
861
+ ['סביבה', ['environment', 'env']],
862
+ ['ייצור', ['production', 'prod']],
863
+ ['סודי', ['secret', 'key', 'token']],
864
+ ['מפתח סודי', ['secret', 'key']],
865
+ // ── Farsi/Persian → English ───────────────────────────────────────────────
866
+ ['استقرار', ['deploy', 'deployment']],
867
+ ['خطا', ['error', 'err']], // Farsi: خطا vs Arabic: خطأ
868
+ ['اشکال', ['bug', 'issue', 'error']],
869
+ ['رفع اشکال', ['fix', 'debug', 'debugging']],
870
+ ['ساخت', ['build']],
871
+ ['آزمایش', ['test']],
872
+ ['احراز هویت', ['auth', 'authentication']],
873
+ ['پیکربندی', ['config', 'configuration']],
874
+ ['نصب', ['install']],
875
+ ['به‌روزرسانی', ['update', 'upgrade']],
876
+ ['ثبت', ['log']],
877
+ ['سرور', ['server']],
878
+ ['پایگاه داده', ['database', 'db']],
879
+ ['شبکه', ['network']], // Farsi شبکه vs Arabic شبكة
880
+ ['حافظه', ['memory', 'ram']], // Farsi حافظه vs Arabic ذاكرة
881
+ ['کش', ['cache']],
882
+ ['اشکال‌زدایی', ['debug', 'debugging']],
883
+ ['خرابی', ['crash', 'failure', 'error']],
884
+ ['راه‌اندازی مجدد', ['restart', 'reboot']],
885
+ ['مجوز', ['permission', 'access']],
886
+ ['پشتیبان‌گیری', ['backup']],
887
+ ['عملکرد', ['performance']],
888
+ ['مشکل', ['problem', 'issue', 'error']],
889
+ ['سرویس', ['service']],
890
+ ['کلید', ['key', 'token', 'secret']],
891
+ ['محیط', ['environment', 'env']],
892
+ ['تولید', ['production', 'prod']],
893
+ ['رمز', ['secret', 'key', 'token']],
894
+ ['اتصال', ['connection', 'conn']],
895
+ // ── Hindi (Devanagari) → English ──────────────────────────────────────────
896
+ ['तैनाती', ['deploy', 'deployment']],
897
+ ['त्रुटि', ['error', 'err']],
898
+ ['गलती', ['error', 'bug', 'issue']],
899
+ ['सुधार', ['fix', 'bugfix']],
900
+ ['निर्माण', ['build']],
901
+ ['परीक्षण', ['test']],
902
+ ['प्रमाणीकरण', ['auth', 'authentication']],
903
+ ['विन्यास', ['config', 'configuration']],
904
+ ['कॉन्फ़िगरेशन', ['config', 'configuration']],
905
+ ['इंस्टॉल', ['install']],
906
+ ['अपडेट', ['update', 'upgrade']],
907
+ ['लॉग', ['log']],
908
+ ['सर्वर', ['server']],
909
+ ['डेटाबेस', ['database', 'db']],
910
+ ['नेटवर्क', ['network']],
911
+ ['मेमोरी', ['memory', 'ram']],
912
+ ['कैश', ['cache']],
913
+ ['डीबग', ['debug', 'debugging']],
914
+ ['क्रैश', ['crash', 'panic']],
915
+ ['पुनरारंभ', ['restart', 'reboot']],
916
+ ['अनुमति', ['permission', 'access']],
917
+ ['बैकअप', ['backup']],
918
+ ['प्रदर्शन', ['performance']],
919
+ ['समस्या', ['problem', 'issue', 'error']],
920
+ ['सेवा', ['service']],
921
+ ['कनेक्शन', ['connection', 'conn']],
922
+ ['पासवर्ड', ['password']],
923
+ ['टोकन', ['token', 'secret']],
924
+ ['वातावरण', ['environment', 'env']],
925
+ ['उत्पादन', ['production', 'prod']],
745
926
  // ── European languages → English ─────────────────────────────────────────
746
927
  // German (Deutsch) → English
747
928
  ['fehler', ['error', 'err']],
@@ -939,8 +1120,11 @@ function expandCrossLingual(token) {
939
1120
  * Features:
940
1121
  * • CJK → character bigrams
941
1122
  * • Katakana → additionally emits Hepburn romaji tokens (enables romaji queries)
942
- * • Arabic/Hebrew → word tokenization with light Arabic stemming
943
- * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE)
1123
+ * • Arabic → word tokenization + iterative prefix stemming (ال/و/ب/ل/ف/ك)
1124
+ * • Hebrew → word tokenization + iterative prefix stemming (ה/ו/ב/ל/מ/כ)
1125
+ * • Farsi → word tokenization + suffix/prefix stemming (ها/های plural, می prefix)
1126
+ * • Hindi → Devanagari word tokenization + light suffix stemming
1127
+ * • All scripts → cross-lingual synonym expansion (EN↔JA↔ZH↔KO↔AR↔HE↔FA↔HI↔DE↔FR↔ES↔IT↔PT)
944
1128
  */
945
1129
  function tokenize(text) {
946
1130
  const tokens = [];
@@ -979,12 +1163,33 @@ function tokenize(text) {
979
1163
  }
980
1164
  }
981
1165
  else if (RTL_RE.test(seg)) {
982
- // RTL segment (Arabic / Hebrew) — space-separated words with light stemming
1166
+ // RTL segment — 3-way branch: Farsi / Hebrew / Arabic
983
1167
  const words = seg.trim().split(/\s+/);
1168
+ let stemFn;
1169
+ if (FARSI_CHAR_RE.test(seg))
1170
+ stemFn = farsiLightStem;
1171
+ else if (HEBREW_CHAR_RE.test(seg))
1172
+ stemFn = hebrewLightStem;
1173
+ else
1174
+ stemFn = arabicLightStem;
984
1175
  for (const w of words) {
985
1176
  if (w.length <= 1 || STOPWORDS.has(w))
986
1177
  continue;
987
- const stemmed = arabicLightStem(w);
1178
+ const stemmed = stemFn(w);
1179
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1180
+ tokens.push(stemmed);
1181
+ }
1182
+ }
1183
+ else if (DEVANAGARI_RE.test(seg)) {
1184
+ // Devanagari segment (Hindi / Marathi) — naturally space-separated
1185
+ // Words don't need bigram expansion; stems reduce inflected forms.
1186
+ const words = seg.trim().split(/[\s\u200c\u200d]+/); // handle zero-width joiners
1187
+ for (const w of words) {
1188
+ // Keep Devanagari chars + digits; discard pure Latin portions (e.g. embedded numbers)
1189
+ const deva = w.replace(/[^\u0900-\u097f0-9]/g, '');
1190
+ if (deva.length <= 1 || STOPWORDS.has(deva))
1191
+ continue;
1192
+ const stemmed = hindiLightStem(deva);
988
1193
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
989
1194
  tokens.push(stemmed);
990
1195
  }
@@ -1023,8 +1228,17 @@ function tokenize(text) {
1023
1228
  }
1024
1229
  }
1025
1230
  else if (RTL_RE.test(syn)) {
1026
- // RTL synonym
1027
- const stemmed = arabicLightStem(syn);
1231
+ // RTL synonym — apply correct stemmer per language
1232
+ const rtlStem = FARSI_CHAR_RE.test(syn) ? farsiLightStem
1233
+ : HEBREW_CHAR_RE.test(syn) ? hebrewLightStem
1234
+ : arabicLightStem;
1235
+ const stemmed = rtlStem(syn);
1236
+ if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1237
+ expansions.push(stemmed);
1238
+ }
1239
+ else if (DEVANAGARI_RE.test(syn)) {
1240
+ // Devanagari synonym (Hindi)
1241
+ const stemmed = hindiLightStem(syn);
1028
1242
  if (stemmed.length > 1 && !STOPWORDS.has(stemmed))
1029
1243
  expansions.push(stemmed);
1030
1244
  }