@futdevpro/nts-dynamo 1.15.23 → 1.15.29

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 (68) hide show
  1. package/README.md +4 -0
  2. package/__documentations/2026-05-17-oai-compatible-providers.md +229 -0
  3. package/_specifications/BACKLOG.md +28 -0
  4. package/build/_models/interfaces/compare-data-options.interface.d.ts +27 -0
  5. package/build/_models/interfaces/compare-data-options.interface.d.ts.map +1 -0
  6. package/build/_models/interfaces/compare-data-options.interface.js +3 -0
  7. package/build/_models/interfaces/compare-data-options.interface.js.map +1 -0
  8. package/build/_models/interfaces/compare-data-result.interface.d.ts +13 -0
  9. package/build/_models/interfaces/compare-data-result.interface.d.ts.map +1 -0
  10. package/build/_models/interfaces/compare-data-result.interface.js +3 -0
  11. package/build/_models/interfaces/compare-data-result.interface.js.map +1 -0
  12. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts +14 -0
  13. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.d.ts.map +1 -0
  14. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js +3 -0
  15. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.js.map +1 -0
  16. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts +50 -0
  17. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.d.ts.map +1 -0
  18. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js +3 -0
  19. package/build/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.js.map +1 -0
  20. package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.d.ts.map +1 -1
  21. package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js +32 -0
  22. package/build/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.js.map +1 -1
  23. package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.d.ts.map +1 -1
  24. package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js +20 -2
  25. package/build/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.js.map +1 -1
  26. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts +4 -1
  27. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.d.ts.map +1 -1
  28. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js +28 -1
  29. package/build/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.js.map +1 -1
  30. package/build/_modules/ai/_services/ai-provider.service-base.d.ts +21 -0
  31. package/build/_modules/ai/_services/ai-provider.service-base.d.ts.map +1 -1
  32. package/build/_modules/ai/_services/ai-provider.service-base.js +32 -0
  33. package/build/_modules/ai/_services/ai-provider.service-base.js.map +1 -1
  34. package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts +17 -1
  35. package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.d.ts.map +1 -1
  36. package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js +16 -0
  37. package/build/_modules/local-vector-search/_enums/lvs-search-mode.enum.js.map +1 -1
  38. package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts +89 -0
  39. package/build/_modules/local-vector-search/_services/lvs-bm25.util.d.ts.map +1 -0
  40. package/build/_modules/local-vector-search/_services/lvs-bm25.util.js +190 -0
  41. package/build/_modules/local-vector-search/_services/lvs-bm25.util.js.map +1 -0
  42. package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts +18 -2
  43. package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.d.ts.map +1 -1
  44. package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js +57 -3
  45. package/build/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.js.map +1 -1
  46. package/build/_services/base/data.service.d.ts +63 -0
  47. package/build/_services/base/data.service.d.ts.map +1 -1
  48. package/build/_services/base/data.service.js +189 -0
  49. package/build/_services/base/data.service.js.map +1 -1
  50. package/package.json +1 -1
  51. package/src/_models/interfaces/compare-data-options.interface.ts +27 -0
  52. package/src/_models/interfaces/compare-data-result.interface.ts +12 -0
  53. package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event-callback.interface.ts +14 -0
  54. package/src/_modules/ai/_models/interfaces/dynts-ai-cost-event.interface.ts +56 -0
  55. package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.spec.ts +92 -0
  56. package/src/_modules/ai/_modules/open-ai/_services/oai-embedding.control-service.ts +38 -4
  57. package/src/_modules/ai/_modules/open-ai/_services/oai-llm-chat.service-base.ts +24 -5
  58. package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.spec.ts +52 -0
  59. package/src/_modules/ai/_modules/open-ai/_services/oai-llm.service-base.ts +39 -10
  60. package/src/_modules/ai/_services/ai-provider.service-base.spec.ts +79 -0
  61. package/src/_modules/ai/_services/ai-provider.service-base.ts +41 -3
  62. package/src/_modules/local-vector-search/_enums/lvs-search-mode.enum.ts +16 -0
  63. package/src/_modules/local-vector-search/_services/lvs-bm25.util.spec.ts +159 -0
  64. package/src/_modules/local-vector-search/_services/lvs-bm25.util.ts +206 -0
  65. package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.spec.ts +135 -0
  66. package/src/_modules/local-vector-search/_services/lvs-local-vector-search.data-service.ts +95 -9
  67. package/src/_services/base/data.service.spec.ts +181 -0
  68. package/src/_services/base/data.service.ts +196 -2
@@ -343,6 +343,58 @@ describe('| DyNTS_OAI_LLM_ServiceBase', () => {
343
343
  });
344
344
  });
345
345
 
346
+ describe('| FR-002 cost-event emit', () => {
347
+
348
+ it('| should emit cost-event with llm-completion callType for single user message', async () => {
349
+ const received: any[] = [];
350
+ (service as any).onCostEvent = (e: any) => { received.push(e); };
351
+
352
+ await service.resolveMessage({
353
+ conversation: [{ role: DyFM_AI_MessageRole.user, content: 'Q?' }],
354
+ issuer: 'llm-issuer-1',
355
+ });
356
+
357
+ expect(received.length).toBe(1);
358
+ expect(received[0].callType).toBe('llm-completion');
359
+ expect(received[0].provider).toBe('open-ai');
360
+ expect(received[0].model).toBe(DyFM_OAI_Model.gpt4o_mini);
361
+ expect(received[0].tokensUsed.input).toBe(10);
362
+ expect(received[0].tokensUsed.output).toBe(5);
363
+ expect(received[0].tokensUsed.total).toBe(15);
364
+ expect(received[0].issuer).toBe('llm-issuer-1');
365
+ expect(received[0].timestamp).toBeInstanceOf(Date);
366
+ expect(received[0].durationMs).toBeGreaterThanOrEqual(0);
367
+ expect(received[0].estimatedCostUsd).toBeUndefined();
368
+ });
369
+
370
+ it('| should emit cost-event with llm-chat callType for multi-message conversation', async () => {
371
+ const received: any[] = [];
372
+ (service as any).onCostEvent = (e: any) => { received.push(e); };
373
+
374
+ await service.resolveMessage({
375
+ conversation: [
376
+ { role: DyFM_AI_MessageRole.user, content: 'Q1?' },
377
+ { role: DyFM_AI_MessageRole.assistant, content: 'A1.' },
378
+ { role: DyFM_AI_MessageRole.user, content: 'Q2?' },
379
+ ],
380
+ issuer: 'llm-issuer-chat',
381
+ });
382
+
383
+ expect(received.length).toBe(1);
384
+ expect(received[0].callType).toBe('llm-chat');
385
+ });
386
+
387
+ it('| constructor accepts onCostEvent option (non-breaking additive)', () => {
388
+ const cb: any = jasmine.createSpy('onCostEvent');
389
+ const s: DyNTS_OAI_LLM_ServiceBase = new DyNTS_OAI_LLM_ServiceBase({
390
+ ...mockSettings,
391
+ onCostEvent: cb,
392
+ });
393
+
394
+ expect((s as any).onCostEvent).toBe(cb);
395
+ });
396
+ });
397
+
346
398
  describe('| getMessageCreateInput', () => {
347
399
  it('| should create input with default settings', () => {
348
400
  const conversation: DyFM_AI_Message[] = [
@@ -15,6 +15,7 @@ import {
15
15
  import { DyNTS_global_settings } from '../../../../../_collections/global-settings.const';
16
16
  import { ChatCompletion } from 'openai/resources';
17
17
  import { ChatCompletionCreateParamsBase, ChatCompletionMessageParam } from 'openai/resources/chat/completions';
18
+ import { DyNTS_AI_CostEventCallback } from '../../../_models/interfaces/dynts-ai-cost-event-callback.interface';
18
19
  import { DyNTS_OAI_LLM_Predefined_Requests } from '../_models/interfaces/oai-llm-predefined-requests.interface';
19
20
  import { DyNTS_OAI_global_settings } from '../_collections/oai-global-settings.const';
20
21
  import { DyNTS_OAI_LLMDefaultPredefined_Requests } from '../_collections/oai-llm-predefined-requests.conts';
@@ -82,14 +83,14 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
82
83
  predefinedRequests: DyNTS_OAI_LLM_Predefined_Requests = DyNTS_OAI_LLMDefaultPredefined_Requests;
83
84
 
84
85
  constructor(
85
- set?: DyFM_OAI_Settings
86
+ set?: DyFM_OAI_Settings & { onCostEvent?: DyNTS_AI_CostEventCallback }
86
87
  ) {
87
88
  super();
88
-
89
+
89
90
  DyNTS_global_settings.openAi_settings ??= DyNTS_OAI_global_settings;
90
-
91
+
91
92
  this.openai = new OpenAI(
92
- set?.config ??
93
+ set?.config ??
93
94
  {
94
95
  organization: DyNTS_global_settings.env_settings.openAi.organization,
95
96
  apiKey: DyNTS_global_settings.env_settings.openAi.apiKey,
@@ -97,9 +98,14 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
97
98
  }
98
99
  );
99
100
 
100
- this.defaultSettings = set?.defaultSettings ??
101
- DyNTS_OAI_global_settings.defaultSettings ??
101
+ this.defaultSettings = set?.defaultSettings ??
102
+ DyNTS_OAI_global_settings.defaultSettings ??
102
103
  new DyFM_OAI_CallSettings();
104
+
105
+ // FR-002 — opcionális per-call cost-event callback átadás a base-osztálynak
106
+ if (set?.onCostEvent) {
107
+ this.onCostEvent = set.onCostEvent;
108
+ }
103
109
  }
104
110
 
105
111
  /**
@@ -445,13 +451,36 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
445
451
  } */
446
452
  ): Promise<string | ChatCompletion> {
447
453
  try {
448
- set.conversation.unshift(this.getDefaultSystemMessage(set.settings));
449
-
454
+ // FR-002 — callType meghatározás a unshift ELŐTT (a system-message hozzáadás
455
+ // után már 2+ message lenne, ami a "chat" kategóriát mindig kiváltja).
456
+ // 1 user-message → 'llm-completion'; több → 'llm-chat'. A 'llm-tool-use'
457
+ // categoria a tools paraméterek bevezetésekor kerül használatba (jövőbeli FR).
458
+ const userMessageCount: number = set.conversation.length;
450
459
 
460
+ set.conversation.unshift(this.getDefaultSystemMessage(set.settings));
451
461
 
462
+ // FR-002 — per-call cost-event timing: a tényleges API-call latency mérve
463
+ const input: ChatCompletionCreateParamsBase = this.getMessageCreateInput(set);
464
+ const start: number = Date.now();
452
465
  const result: ChatCompletion = await this.openai.chat.completions.create(
453
- this.getMessageCreateInput(set)
466
+ input,
454
467
  ) as ChatCompletion;
468
+ const durationMs: number = Date.now() - start;
469
+
470
+ // FR-002 — cost-event emit (safe, no-op ha nincs onCostEvent callback)
471
+ this.emitCostEvent({
472
+ callType: userMessageCount === 1 ? 'llm-completion' : 'llm-chat',
473
+ provider: this.aiProvider,
474
+ model: String(input.model ?? this.defaultModel),
475
+ tokensUsed: {
476
+ input: result.usage?.prompt_tokens ?? 0,
477
+ output: result.usage?.completion_tokens ?? 0,
478
+ total: result.usage?.total_tokens ?? 0,
479
+ },
480
+ durationMs: durationMs,
481
+ issuer: set.issuer,
482
+ timestamp: new Date(),
483
+ });
455
484
 
456
485
  if (set.getFullResponse) {
457
486
  return result;
@@ -461,7 +490,7 @@ export class DyNTS_OAI_LLM_ServiceBase extends DyNTS_AI_LLM_ServiceBase {
461
490
  } catch (error) {
462
491
  throw new DyFM_Error({
463
492
  ...this.getDefaultErrorSettings('resolveConversation', error, set.issuer),
464
-
493
+
465
494
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-OLSB-RM0`,
466
495
  });
467
496
  }
@@ -1,4 +1,6 @@
1
1
 
2
+ import { DyNTS_AI_CostEvent } from '../_models/interfaces/dynts-ai-cost-event.interface';
3
+ import { DyNTS_AI_CostEventCallback } from '../_models/interfaces/dynts-ai-cost-event-callback.interface';
2
4
  import { DyNTS_AI_Provider_ServiceBase } from './ai-provider.service-base';
3
5
  import { DyFM_AI_Provider, DyFM_AI_ProviderCapabilities, DyFM_AI_Config } from '@futdevpro/fsm-dynamo/ai';
4
6
 
@@ -25,11 +27,35 @@ class TestProviderService extends DyNTS_AI_Provider_ServiceBase {
25
27
  return true;
26
28
  };
27
29
 
30
+ /** Test-only helper: ráad egy callback-et a protected onCostEvent fielden át. */
31
+ public setOnCostEventForTest(cb: DyNTS_AI_CostEventCallback | undefined): void {
32
+ this.onCostEvent = cb;
33
+ }
34
+
35
+ /** Test-only helper: meghívja a protected emitCostEvent-et (a sub-class amúgy is hívná). */
36
+ public emitForTest(event: DyNTS_AI_CostEvent): void {
37
+ this.emitCostEvent(event);
38
+ }
39
+
28
40
  static getInstance(): TestProviderService {
29
41
  return TestProviderService.getSingletonInstance();
30
42
  }
31
43
  }
32
44
 
45
+ /** Helper: minimal valid DyNTS_AI_CostEvent. */
46
+ function makeCostEvent(overrides: Partial<DyNTS_AI_CostEvent> = {}): DyNTS_AI_CostEvent {
47
+ return {
48
+ callType: 'embedding-single',
49
+ provider: 'openai',
50
+ model: 'text-embedding-3-small',
51
+ tokensUsed: { input: 10, total: 10 },
52
+ durationMs: 42,
53
+ issuer: 'test-issuer',
54
+ timestamp: new Date(),
55
+ ...overrides,
56
+ };
57
+ }
58
+
33
59
  describe('| DyNTS_AI_Provider_ServiceBase', () => {
34
60
  let service: TestProviderService;
35
61
 
@@ -75,5 +101,58 @@ describe('| DyNTS_AI_Provider_ServiceBase', () => {
75
101
  expect(result).toBe(true);
76
102
  });
77
103
  });
104
+
105
+ describe('| emitCostEvent (FR-002 cost-event hook)', () => {
106
+
107
+ afterEach(() => {
108
+ // Reset callback hogy ne szivárogjon át teszt-cases között (singleton service)
109
+ service.setOnCostEventForTest(undefined);
110
+ });
111
+
112
+ it('| should be no-op when onCostEvent is undefined', () => {
113
+ service.setOnCostEventForTest(undefined);
114
+
115
+ expect(() => {
116
+ service.emitForTest(makeCostEvent());
117
+ }).not.toThrow();
118
+ });
119
+
120
+ it('| should invoke the registered callback with the event payload', () => {
121
+ const received: DyNTS_AI_CostEvent[] = [];
122
+ service.setOnCostEventForTest((e: DyNTS_AI_CostEvent) => {
123
+ received.push(e);
124
+ });
125
+
126
+ const event: DyNTS_AI_CostEvent = makeCostEvent({
127
+ model: 'gpt-4o-mini',
128
+ callType: 'llm-completion',
129
+ tokensUsed: { input: 100, output: 50, total: 150 },
130
+ });
131
+ service.emitForTest(event);
132
+
133
+ expect(received.length).toBe(1);
134
+ expect(received[0]).toEqual(event);
135
+ expect(received[0].tokensUsed.total).toBe(150);
136
+ });
137
+
138
+ it('| should NOT throw when the callback itself throws (safe emit)', () => {
139
+ service.setOnCostEventForTest(() => {
140
+ throw new Error('consumer callback failure');
141
+ });
142
+
143
+ expect(() => {
144
+ service.emitForTest(makeCostEvent());
145
+ }).not.toThrow();
146
+ });
147
+
148
+ it('| should leave estimatedCostUsd undefined by default (Dynamo has no pricing registry)', () => {
149
+ const received: DyNTS_AI_CostEvent[] = [];
150
+ service.setOnCostEventForTest((e: DyNTS_AI_CostEvent) => { received.push(e); });
151
+
152
+ service.emitForTest(makeCostEvent());
153
+
154
+ expect(received[0].estimatedCostUsd).toBeUndefined();
155
+ });
156
+ });
78
157
  });
79
158
 
@@ -1,29 +1,67 @@
1
1
  import { DyFM_AI_Provider } from '@futdevpro/fsm-dynamo/ai';
2
2
  import { DyFM_AI_ProviderCapabilities } from '@futdevpro/fsm-dynamo/ai';
3
3
  import { DyFM_AI_Config } from '@futdevpro/fsm-dynamo/ai';
4
+ import { DyFM_Log } from '@futdevpro/fsm-dynamo';
5
+
6
+ import { DyNTS_AI_CostEvent } from '../_models/interfaces/dynts-ai-cost-event.interface';
7
+ import { DyNTS_AI_CostEventCallback } from '../_models/interfaces/dynts-ai-cost-event-callback.interface';
4
8
  import { DyNTS_SingletonService } from '../../../_services/base/singleton.service';
5
9
 
6
10
  /**
7
11
  * Abstract base class for all AI providers
8
12
  * Defines the common interface that all AI providers must implement
13
+ *
14
+ * **Cost-event hook (FR-002)**: a leszármazott concrete service-ek (OAI
15
+ * Embedding / LLM / Chat) a constructor-ban opcionálisan kapnak egy
16
+ * `onCostEvent` callback-et. Minden sikeres AI-call után `emitCostEvent`-tel
17
+ * hívják, ami safe (try/catch) — a callback hibája NEM akasztja meg az
18
+ * AI-call-t. Lásd `DyNTS_AI_CostEvent` interface.
9
19
  */
10
20
  export abstract class DyNTS_AI_Provider_ServiceBase extends DyNTS_SingletonService {
11
21
  /** The AI provider this service represents */
12
22
  abstract readonly aiProvider: DyFM_AI_Provider;
13
-
23
+
14
24
  /** The capabilities supported by this provider */
15
25
  abstract readonly capabilities: DyFM_AI_ProviderCapabilities;
16
-
26
+
27
+ /**
28
+ * Optional per-call cost-event callback (FR-002).
29
+ * A leszármazott service-ek minden sikeres AI-call után meghívják az
30
+ * `emitCostEvent` helper-en keresztül. Non-breaking: ha undefined, nincs emit.
31
+ */
32
+ protected onCostEvent?: DyNTS_AI_CostEventCallback;
33
+
17
34
  /**
18
35
  * Initialize the provider with configuration
19
36
  * @param config - Provider-specific configuration
20
37
  */
21
38
  abstract setup(config: DyFM_AI_Config): void;
22
-
39
+
23
40
  /**
24
41
  * Test the connection to the AI provider
25
42
  * @param issuer - The issuer making the request
26
43
  * @returns Promise<boolean> - True if connection is successful
27
44
  */
28
45
  abstract testConnection(issuer: string): Promise<boolean>;
46
+
47
+ /**
48
+ * Safe cost-event emit: ha van regisztrált `onCostEvent` callback, meghívja.
49
+ * A callback hibája NEM propagálódik — try/catch-csel safen hívjuk és
50
+ * `DyFM_Log.warn`-nal jelezzük. Ezzel a consumer-oldali integráció
51
+ * (CCAP `CT_`, telemetry, stb.) hibái nem akasztják meg az AI-call-t.
52
+ */
53
+ protected emitCostEvent(event: DyNTS_AI_CostEvent): void {
54
+ if (!this.onCostEvent) {
55
+ return;
56
+ }
57
+ try {
58
+ this.onCostEvent(event);
59
+ } catch (err: unknown) {
60
+ const msg: string = err instanceof Error ? err.message : String(err);
61
+ DyFM_Log.warn(
62
+ `DyNTS AI | onCostEvent callback threw (call-type: ${event.callType},`
63
+ + ` model: ${event.model}, issuer: ${event.issuer}): ${msg}`,
64
+ );
65
+ }
66
+ }
29
67
  }
@@ -15,5 +15,21 @@ export enum LVS_Search_Mode {
15
15
  * Eredmények növekvő sorrendben (legkisebb távolság először)
16
16
  */
17
17
  l2Distance = 'l2-distance',
18
+ /**
19
+ * Hybrid search: cosine similarity (vektor-fele) + BM25 (text-fele) score-merge.
20
+ *
21
+ * Hasznalat:
22
+ * - `searchMode: LVS_Search_Mode.hybrid`
23
+ * - `textSearchKey: keyof T` — KOTELEZO; melyik string property-n fut a BM25
24
+ * - `hybridWeight?: { vector: number; text: number }` — default { vector: 0.5, text: 0.5 }
25
+ *
26
+ * BM25 min-max normalizalva [0,1] a candidate-szettre (cosine mar 0..1), igy
27
+ * a `weight` direkt linearis kombinacio. Ha minden BM25 score 0 (egyetlen
28
+ * query term sem matchel), a hybrid effektivan cosine-only-re degradalodik.
29
+ *
30
+ * L2+hybrid kombinacio NEM tamogatott — a hybrid mindig cosine-alapu
31
+ * vector-half-fel mukodik.
32
+ */
33
+ hybrid = 'hybrid',
18
34
  }
19
35
 
@@ -0,0 +1,159 @@
1
+ import {
2
+ DyNTS_LVS_BM25_Corpus,
3
+ DyNTS_LVS_BM25_DocScore,
4
+ dyNTS_LVS_BM25_minMaxNormalize,
5
+ } from './lvs-bm25.util';
6
+
7
+
8
+ describe('| DyNTS_LVS_BM25_Corpus.tokenize', (): void => {
9
+ it('| lowercase + split on \\w+ boundaries', (): void => {
10
+ expect(DyNTS_LVS_BM25_Corpus.tokenize('Hello, World!')).toEqual(['hello', 'world']);
11
+ });
12
+
13
+ it('| identifier marad egy tokenkent (PascalCase)', (): void => {
14
+ expect(DyNTS_LVS_BM25_Corpus.tokenize('UserController')).toEqual(['usercontroller']);
15
+ });
16
+
17
+ it('| hyphenated nev ket tokenre esik', (): void => {
18
+ expect(DyNTS_LVS_BM25_Corpus.tokenize('auth-flow')).toEqual(['auth', 'flow']);
19
+ });
20
+
21
+ it('| ures string → ures array', (): void => {
22
+ expect(DyNTS_LVS_BM25_Corpus.tokenize('')).toEqual([]);
23
+ });
24
+
25
+ it('| csak whitespace/punctuation → ures array', (): void => {
26
+ expect(DyNTS_LVS_BM25_Corpus.tokenize(' ,.! ')).toEqual([]);
27
+ });
28
+
29
+ it('| underscore megmarad (snake_case is egy token)', (): void => {
30
+ expect(DyNTS_LVS_BM25_Corpus.tokenize('user_controller')).toEqual(['user_controller']);
31
+ });
32
+ });
33
+
34
+
35
+ describe('| DyNTS_LVS_BM25_Corpus.score', (): void => {
36
+ it('| ures corpus → ures array', (): void => {
37
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([]);
38
+ expect(corpus.size()).toBe(0);
39
+ expect(corpus.score('anything')).toEqual([]);
40
+ });
41
+
42
+ it('| ures query → minden doc 0 score-t kap', (): void => {
43
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
44
+ { id: 'a', text: 'foo bar' },
45
+ { id: 'b', text: 'baz' },
46
+ ]);
47
+ const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('');
48
+ expect(out.length).toBe(2);
49
+ expect(out.every((s: DyNTS_LVS_BM25_DocScore) => s.score === 0)).toBe(true);
50
+ });
51
+
52
+ it('| query NEM matchelo termmel → minden score 0', (): void => {
53
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
54
+ { id: 'a', text: 'foo bar' },
55
+ { id: 'b', text: 'baz' },
56
+ ]);
57
+ const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('xyz');
58
+ expect(out.every((s: DyNTS_LVS_BM25_DocScore) => s.score === 0)).toBe(true);
59
+ });
60
+
61
+ it('| relevant doc magasabb score-t kap mint nem-relevant', (): void => {
62
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
63
+ { id: 'a', text: 'the UserController handles authentication' },
64
+ { id: 'b', text: 'cooking recipes for desserts' },
65
+ { id: 'c', text: 'database setup guide' },
66
+ ]);
67
+ const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('UserController');
68
+ const a: DyNTS_LVS_BM25_DocScore = out.find((s) => s.id === 'a')!;
69
+ const b: DyNTS_LVS_BM25_DocScore = out.find((s) => s.id === 'b')!;
70
+ expect(a.score).toBeGreaterThan(0);
71
+ expect(b.score).toBe(0);
72
+ expect(a.score).toBeGreaterThan(b.score);
73
+ });
74
+
75
+ it('| rarer term (alacsonyabb df) magasabb IDF-et ad → magasabb score', (): void => {
76
+ // 'common' minden docban → alacsony IDF; 'rare' csak az 'a' docban → magas IDF
77
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
78
+ { id: 'a', text: 'common rare' },
79
+ { id: 'b', text: 'common' },
80
+ { id: 'c', text: 'common' },
81
+ { id: 'd', text: 'common' },
82
+ ]);
83
+ const commonOut: DyNTS_LVS_BM25_DocScore[] = corpus.score('common');
84
+ const rareOut: DyNTS_LVS_BM25_DocScore[] = corpus.score('rare');
85
+ const aCommon: number = commonOut.find((s) => s.id === 'a')!.score;
86
+ const aRare: number = rareOut.find((s) => s.id === 'a')!.score;
87
+ expect(aRare).toBeGreaterThan(aCommon);
88
+ });
89
+
90
+ it('| query-term ismetles NEM no monotonan a score-on (term saturation k1)', (): void => {
91
+ // 'a' egy 'foo'-t tartalmaz, 'b' tobbet
92
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
93
+ { id: 'a', text: 'foo bar baz qux' },
94
+ { id: 'b', text: 'foo foo foo foo bar baz qux' },
95
+ ]);
96
+ const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('foo');
97
+ const a: number = out.find((s) => s.id === 'a')!.score;
98
+ const b: number = out.find((s) => s.id === 'b')!.score;
99
+ // b > a, de NEM 4x a-szor (saturation)
100
+ expect(b).toBeGreaterThan(a);
101
+ expect(b).toBeLessThan(4 * a);
102
+ });
103
+
104
+ it('| case-insensitive — uppercase query matchel lowercase doc-ot', (): void => {
105
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
106
+ { id: 'a', text: 'usercontroller is great' },
107
+ ]);
108
+ const out: DyNTS_LVS_BM25_DocScore[] = corpus.score('USERCONTROLLER');
109
+ expect(out[0].score).toBeGreaterThan(0);
110
+ });
111
+
112
+ it('| ket query term aggregalja az IDF-eket additivan', (): void => {
113
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
114
+ { id: 'a', text: 'auth flow handler' },
115
+ { id: 'b', text: 'auth only' },
116
+ { id: 'c', text: 'flow only' },
117
+ ]);
118
+ const both: number = corpus.score('auth flow').find((s) => s.id === 'a')!.score;
119
+ const authOnly: number = corpus.score('auth').find((s) => s.id === 'a')!.score;
120
+ expect(both).toBeGreaterThan(authOnly);
121
+ });
122
+
123
+ it('| invalid input doc (null/missing id/text) silently skipd', (): void => {
124
+ const corpus: DyNTS_LVS_BM25_Corpus = new DyNTS_LVS_BM25_Corpus([
125
+ { id: 'a', text: 'hello' },
126
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
127
+ null as any,
128
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
129
+ { id: 'b' } as any,
130
+ ]);
131
+ expect(corpus.size()).toBe(1);
132
+ });
133
+ });
134
+
135
+
136
+ describe('| dyNTS_LVS_BM25_minMaxNormalize', (): void => {
137
+ it('| ures input → ures array', (): void => {
138
+ expect(dyNTS_LVS_BM25_minMaxNormalize([])).toEqual([]);
139
+ });
140
+
141
+ it('| min 0, max 10 → normalizalva [0..1]', (): void => {
142
+ const out: DyNTS_LVS_BM25_DocScore[] = dyNTS_LVS_BM25_minMaxNormalize([
143
+ { id: 'a', score: 0 },
144
+ { id: 'b', score: 5 },
145
+ { id: 'c', score: 10 },
146
+ ]);
147
+ expect(out.find((s) => s.id === 'a')!.score).toBeCloseTo(0, 5);
148
+ expect(out.find((s) => s.id === 'b')!.score).toBeCloseTo(0.5, 5);
149
+ expect(out.find((s) => s.id === 'c')!.score).toBeCloseTo(1, 5);
150
+ });
151
+
152
+ it('| azonos score → minden 0 (NEM 0.5, nincs jel diszkriminaciora)', (): void => {
153
+ const out: DyNTS_LVS_BM25_DocScore[] = dyNTS_LVS_BM25_minMaxNormalize([
154
+ { id: 'a', score: 3 },
155
+ { id: 'b', score: 3 },
156
+ ]);
157
+ expect(out.every((s) => s.score === 0)).toBe(true);
158
+ });
159
+ });