capman 0.4.5 → 0.5.1

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 (46) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/CODEBASE.md +94 -156
  3. package/README.md +23 -0
  4. package/bin/lib/cmd-generate.js +20 -3
  5. package/dist/cjs/cache.d.ts +6 -4
  6. package/dist/cjs/cache.d.ts.map +1 -1
  7. package/dist/cjs/cache.js +38 -11
  8. package/dist/cjs/cache.js.map +1 -1
  9. package/dist/cjs/engine.d.ts +46 -4
  10. package/dist/cjs/engine.d.ts.map +1 -1
  11. package/dist/cjs/engine.js +157 -211
  12. package/dist/cjs/engine.js.map +1 -1
  13. package/dist/cjs/index.d.ts +1 -1
  14. package/dist/cjs/index.d.ts.map +1 -1
  15. package/dist/cjs/index.js +2 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/learning.d.ts +16 -1
  18. package/dist/cjs/learning.d.ts.map +1 -1
  19. package/dist/cjs/learning.js +161 -10
  20. package/dist/cjs/learning.js.map +1 -1
  21. package/dist/cjs/matcher.d.ts +23 -0
  22. package/dist/cjs/matcher.d.ts.map +1 -1
  23. package/dist/cjs/matcher.js +53 -18
  24. package/dist/cjs/matcher.js.map +1 -1
  25. package/dist/cjs/parser.js +15 -1
  26. package/dist/cjs/parser.js.map +1 -1
  27. package/dist/cjs/resolver.d.ts.map +1 -1
  28. package/dist/cjs/resolver.js +22 -5
  29. package/dist/cjs/resolver.js.map +1 -1
  30. package/dist/cjs/version.d.ts +1 -1
  31. package/dist/cjs/version.js +1 -1
  32. package/dist/esm/cache.d.ts +6 -4
  33. package/dist/esm/cache.js +38 -11
  34. package/dist/esm/engine.d.ts +46 -4
  35. package/dist/esm/engine.js +158 -212
  36. package/dist/esm/index.d.ts +1 -1
  37. package/dist/esm/index.js +1 -1
  38. package/dist/esm/learning.d.ts +16 -1
  39. package/dist/esm/learning.js +161 -10
  40. package/dist/esm/matcher.d.ts +23 -0
  41. package/dist/esm/matcher.js +49 -16
  42. package/dist/esm/parser.js +15 -1
  43. package/dist/esm/resolver.js +22 -5
  44. package/dist/esm/version.d.ts +1 -1
  45. package/dist/esm/version.js +1 -1
  46. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent } from './matcher';
1
+ import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent, extractParams, STOPWORDS } from './matcher';
2
2
  import { resolve as _resolve } from './resolver';
3
3
  import { MemoryLearningStore } from './learning';
4
4
  import { logger } from './logger';
@@ -7,9 +7,6 @@ import { VERSION } from './version';
7
7
  // ─── CapmanEngine ─────────────────────────────────────────────────────────────
8
8
  export class CapmanEngine {
9
9
  constructor(options) {
10
- // ── Learning index cache ──────────────────────────────────────────────────
11
- this.cachedStats = null;
12
- this.statsInvalidated = true;
13
10
  // ── LLM rate limiting state ────────────────────────────────────────────────
14
11
  this.llmCallsThisMinute = 0;
15
12
  this.llmWindowStart = Date.now();
@@ -23,6 +20,7 @@ export class CapmanEngine {
23
20
  this.auth = options.auth;
24
21
  this.headers = options.headers;
25
22
  this.threshold = options.threshold ?? 50;
23
+ this.cacheTtlMs = options.cacheTtlMs ?? null;
26
24
  this.maxLLMCallsPerMinute = options.maxLLMCallsPerMinute ?? 60;
27
25
  this.llmCooldownMs = options.llmCooldownMs ?? 0;
28
26
  this.llmCircuitBreakerThreshold = options.llmCircuitBreakerThreshold ?? 3;
@@ -42,8 +40,11 @@ export class CapmanEngine {
42
40
  const manifestMajorMinor = options.manifest.version?.split('.').slice(0, 2).join('.');
43
41
  const engineMajorMinor = VERSION.split('.').slice(0, 2).join('.');
44
42
  if (manifestMajorMinor && manifestMajorMinor !== engineMajorMinor) {
45
- logger.warn(`Manifest version "${options.manifest.version}" differs from engine version "${VERSION}". ` +
46
- `Run: npx capman generate to regenerate your manifest.`);
43
+ // Use console.warn directly must be visible regardless of logger level
44
+ // Default log level is 'silent' so logger.warn would never be seen
45
+ console.warn(`[capman] Manifest version "${options.manifest.version}" was generated with a ` +
46
+ `different engine version than "${VERSION}". If you experience matching issues, ` +
47
+ `regenerate with: npx capman generate`);
47
48
  }
48
49
  }
49
50
  /**
@@ -60,12 +61,11 @@ export class CapmanEngine {
60
61
  async ask(query, overrides = {}) {
61
62
  const start = Date.now();
62
63
  const steps = [];
63
- let resolvedVia = 'keyword';
64
64
  // ── Step 1: Check cache ──────────────────────────────────────────────────
65
65
  const cacheStart = Date.now();
66
66
  if (this.cache) {
67
67
  const queryKey = normalizeQuery(query);
68
- const cached = await this.cache.get(queryKey);
68
+ const cached = await this.cache.get(queryKey, this.cacheTtlMs ?? undefined);
69
69
  if (cached) {
70
70
  steps.push({ type: 'cache_check', status: 'hit', durationMs: Date.now() - cacheStart, detail: 'Served from cache' });
71
71
  logger.info(`Cache hit for: "${query}"`);
@@ -94,116 +94,10 @@ export class CapmanEngine {
94
94
  steps.push({ type: 'cache_check', status: 'skip', durationMs: 0, detail: 'Cache disabled' });
95
95
  }
96
96
  // ── Step 2: Match ────────────────────────────────────────────────────────
97
- let matchResult;
98
- switch (this.mode) {
99
- case 'cheap': {
100
- const t = Date.now();
101
- matchResult = _match(query, this.manifest);
102
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
103
- break;
104
- }
105
- case 'accurate': {
106
- if (this.llm) {
107
- const skipReason = this.checkLLMAllowed();
108
- if (skipReason) {
109
- logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
110
- const t = Date.now();
111
- matchResult = _match(query, this.manifest);
112
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
113
- }
114
- else {
115
- const t = Date.now();
116
- try {
117
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
118
- this.recordLLMSuccess();
119
- resolvedVia = 'llm';
120
- steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
121
- }
122
- catch (err) {
123
- this.recordLLMFailure();
124
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
125
- const t2 = Date.now();
126
- matchResult = _match(query, this.manifest);
127
- steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
128
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
129
- }
130
- }
131
- }
132
- else {
133
- logger.warn('accurate mode requires llm — falling back to keyword');
134
- const t = Date.now();
135
- matchResult = _match(query, this.manifest);
136
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
137
- }
138
- break;
139
- }
140
- case 'balanced':
141
- default: {
142
- const t1 = Date.now();
143
- const keywordResult = _match(query, this.manifest);
144
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
145
- if (keywordResult.confidence >= this.threshold || !this.llm) {
146
- matchResult = keywordResult;
147
- }
148
- else {
149
- const skipReason = this.checkLLMAllowed();
150
- if (skipReason) {
151
- logger.warn(`LLM skipped — ${skipReason}`);
152
- steps.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
153
- matchResult = keywordResult;
154
- }
155
- else {
156
- logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
157
- const t2 = Date.now();
158
- try {
159
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
160
- this.recordLLMSuccess();
161
- resolvedVia = 'llm';
162
- steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
163
- }
164
- catch (err) {
165
- this.recordLLMFailure();
166
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
167
- steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
168
- matchResult = keywordResult;
169
- }
170
- }
171
- }
172
- break;
173
- }
174
- }
175
- const preBoostMatchResult = matchResult;
97
+ let { matchResult, resolvedVia } = await this._runMatch(query, steps);
98
+ const preBoostMatchResult = matchResult; // kept for learning recording only — prevents feedback loop
176
99
  // ── Step 2.5: Apply learning boost ───────────────────────────────────────
177
- if (matchResult.candidates.length > 0 && this.learning && this.mode !== 'cheap') {
178
- const boosted = await this.applyLearningBoost(query, matchResult.candidates);
179
- if (boosted.length > 0) {
180
- const newWinner = boosted.reduce((a, b) => a.score > b.score ? a : b);
181
- const oldWinner = matchResult.candidates.find(c => c.matched);
182
- if (newWinner.capabilityId !== oldWinner?.capabilityId && newWinner.score >= this.threshold) {
183
- // Boost changed the winner — re-extract params for the new capability
184
- const newCap = this.manifest.capabilities.find(c => c.id === newWinner.capabilityId) ?? null;
185
- const newParams = newCap ? _match(query, { ...this.manifest, capabilities: [newCap] }).extractedParams : {};
186
- matchResult = {
187
- ...matchResult,
188
- capability: newCap,
189
- confidence: newWinner.score,
190
- intent: newCap ? resolverToIntent(newCap) : 'out_of_scope',
191
- extractedParams: newParams,
192
- candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === newWinner.capabilityId })),
193
- reasoning: `Matched "${newWinner.capabilityId}" via learning boost (score: ${newWinner.score})`,
194
- };
195
- logger.info(`Learning boost changed winner: "${oldWinner?.capabilityId ?? 'none'}" → "${newWinner.capabilityId}"`);
196
- }
197
- else {
198
- // Same winner — update scores only
199
- matchResult = {
200
- ...matchResult,
201
- confidence: newWinner.score,
202
- candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === (oldWinner?.capabilityId ?? '') })),
203
- };
204
- }
205
- }
206
- }
100
+ matchResult = await this.applyBoostToMatchResult(query, matchResult);
207
101
  // ── Step 3: Privacy check ────────────────────────────────────────────────
208
102
  if (matchResult.capability) {
209
103
  const privacyLevel = matchResult.capability.privacy.level;
@@ -214,10 +108,13 @@ export class CapmanEngine {
214
108
  detail: `level: ${privacyLevel}`,
215
109
  });
216
110
  }
217
- // ── Step 4: Cache the match result ───────────────────────────────────────
218
- if (this.cache && preBoostMatchResult.capability) {
111
+ // ── Step 4: Cache the match result (public capabilities only) ─────────────
112
+ // Non-public capabilities are never cached — prevents auth bypass where
113
+ // User A's cached match is served to User B without privacy enforcement.
114
+ if (this.cache && matchResult.capability
115
+ && matchResult.capability.privacy.level === 'public') {
219
116
  const queryKey = normalizeQuery(query);
220
- await this.cache.set(queryKey, preBoostMatchResult);
117
+ await this.cache.set(queryKey, matchResult);
221
118
  }
222
119
  // ── Step 5: Resolve ──────────────────────────────────────────────────────
223
120
  const resolveStart = Date.now();
@@ -254,7 +151,10 @@ export class CapmanEngine {
254
151
  reasoning.push(matchResult.reasoning);
255
152
  }
256
153
  // ── Step 7: Record learning ──────────────────────────────────────────────
257
- await this.recordLearning(query, matchResult, resolvedVia);
154
+ // Record the pre-boost match result — not the boosted one.
155
+ // Recording the boosted winner would reinforce it further on every call,
156
+ // creating a feedback loop that permanently displaces keyword matches.
157
+ await this.recordLearning(query, preBoostMatchResult, resolvedVia);
258
158
  const trace = {
259
159
  query,
260
160
  candidates: matchResult.candidates,
@@ -314,91 +214,11 @@ export class CapmanEngine {
314
214
  */
315
215
  async explain(query) {
316
216
  const start = Date.now();
317
- // ── Match — mirrors ask() logic including rate limiting ───────────────────
318
- let matchResult;
319
- let resolvedVia = 'keyword';
320
- if (this.mode === 'accurate') {
321
- if (this.llm) {
322
- const skipReason = this.checkLLMAllowed();
323
- if (skipReason) {
324
- logger.warn(`explain(): LLM skipped — ${skipReason} — falling back to keyword`);
325
- matchResult = _match(query, this.manifest);
326
- }
327
- else {
328
- try {
329
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
330
- this.recordLLMSuccess();
331
- resolvedVia = 'llm';
332
- }
333
- catch (err) {
334
- this.recordLLMFailure();
335
- logger.warn(`explain(): LLM call failed — falling back to keyword: ${err}`);
336
- matchResult = _match(query, this.manifest);
337
- }
338
- }
339
- }
340
- else {
341
- matchResult = _match(query, this.manifest);
342
- }
343
- }
344
- else if (this.mode === 'balanced' && this.llm) {
345
- // Keyword first — escalate to LLM if low confidence (same as ask())
346
- const keywordResult = _match(query, this.manifest);
347
- if (keywordResult.confidence >= this.threshold) {
348
- matchResult = keywordResult;
349
- }
350
- else {
351
- const skipReason = this.checkLLMAllowed();
352
- if (skipReason) {
353
- logger.warn(`explain(): LLM skipped — ${skipReason}`);
354
- matchResult = keywordResult;
355
- }
356
- else {
357
- try {
358
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
359
- this.recordLLMSuccess();
360
- resolvedVia = 'llm';
361
- }
362
- catch (err) {
363
- this.recordLLMFailure();
364
- logger.warn(`explain(): LLM call failed — falling back to keyword: ${err}`);
365
- matchResult = keywordResult;
366
- }
367
- }
368
- }
369
- }
370
- else {
371
- // cheap mode or no llm — keyword only
372
- matchResult = _match(query, this.manifest);
373
- }
217
+ // ── Match — shared with ask() via _runMatch() ─────────────────────────────
218
+ let { matchResult, resolvedVia: _resolvedVia } = await this._runMatch(query);
219
+ let resolvedVia = _resolvedVia;
374
220
  // ── Apply learning boost (same as ask()) ─────────────────────────────────
375
- if (matchResult.candidates.length > 0 && this.learning && this.mode !== 'cheap') {
376
- const boosted = await this.applyLearningBoost(query, matchResult.candidates);
377
- if (boosted.length > 0) {
378
- const newWinner = boosted.reduce((a, b) => a.score > b.score ? a : b);
379
- const oldWinner = matchResult.candidates.find(c => c.matched);
380
- if (newWinner.capabilityId !== oldWinner?.capabilityId && newWinner.score >= this.threshold) {
381
- const newCap = this.manifest.capabilities.find(c => c.id === newWinner.capabilityId) ?? null;
382
- const newParams = newCap ? _match(query, { ...this.manifest, capabilities: [newCap] }).extractedParams : {};
383
- matchResult = {
384
- ...matchResult,
385
- capability: newCap,
386
- confidence: newWinner.score,
387
- intent: newCap ? resolverToIntent(newCap) : 'out_of_scope',
388
- extractedParams: newParams,
389
- candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === newWinner.capabilityId })),
390
- reasoning: `Matched "${newWinner.capabilityId}" via learning boost (score: ${newWinner.score})`,
391
- };
392
- }
393
- else {
394
- matchResult = {
395
- ...matchResult,
396
- confidence: newWinner.score,
397
- candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === (oldWinner?.capabilityId ?? '') })),
398
- };
399
- }
400
- }
401
- }
221
+ matchResult = await this.applyBoostToMatchResult(query, matchResult);
402
222
  // ── Build candidate explanations ─────────────────────────────────────────
403
223
  const candidates = matchResult.candidates
404
224
  .sort((a, b) => b.score - a.score)
@@ -582,6 +402,137 @@ export class CapmanEngine {
582
402
  logger.warn(`LLM circuit breaker opened after ${this.llmConsecutiveFails} consecutive failures — pausing for ${this.llmCircuitBreakerResetMs / 1000}s`);
583
403
  }
584
404
  }
405
+ /**
406
+ * Runs the matching pipeline for a query — shared by ask() and explain().
407
+ * Handles cheap / balanced / accurate mode dispatch and LLM rate limiting.
408
+ * Returns the match result and which resolver was used.
409
+ */
410
+ async _runMatch(query, steps) {
411
+ let matchResult;
412
+ let resolvedVia = 'keyword';
413
+ switch (this.mode) {
414
+ case 'cheap': {
415
+ const t = Date.now();
416
+ matchResult = _match(query, this.manifest);
417
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
418
+ break;
419
+ }
420
+ case 'accurate': {
421
+ if (this.llm) {
422
+ const skipReason = this.checkLLMAllowed();
423
+ if (skipReason) {
424
+ logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
425
+ const t = Date.now();
426
+ matchResult = _match(query, this.manifest);
427
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
428
+ }
429
+ else {
430
+ const t = Date.now();
431
+ try {
432
+ matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
433
+ this.recordLLMSuccess();
434
+ resolvedVia = 'llm';
435
+ steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
436
+ }
437
+ catch (err) {
438
+ const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
439
+ if (!isParseError)
440
+ this.recordLLMFailure();
441
+ logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
442
+ const t2 = Date.now();
443
+ matchResult = _match(query, this.manifest);
444
+ steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
445
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
446
+ }
447
+ }
448
+ }
449
+ else {
450
+ logger.warn('accurate mode requires llm — falling back to keyword');
451
+ const t = Date.now();
452
+ matchResult = _match(query, this.manifest);
453
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
454
+ }
455
+ break;
456
+ }
457
+ case 'balanced':
458
+ default: {
459
+ const t1 = Date.now();
460
+ const keywordResult = _match(query, this.manifest);
461
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
462
+ if (keywordResult.confidence >= this.threshold || !this.llm) {
463
+ matchResult = keywordResult;
464
+ }
465
+ else {
466
+ const skipReason = this.checkLLMAllowed();
467
+ if (skipReason) {
468
+ logger.warn(`LLM skipped — ${skipReason}`);
469
+ steps?.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
470
+ matchResult = keywordResult;
471
+ }
472
+ else {
473
+ logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
474
+ const t2 = Date.now();
475
+ try {
476
+ matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
477
+ this.recordLLMSuccess();
478
+ resolvedVia = 'llm';
479
+ steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
480
+ }
481
+ catch (err) {
482
+ const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
483
+ if (!isParseError)
484
+ this.recordLLMFailure();
485
+ logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
486
+ steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
487
+ matchResult = keywordResult;
488
+ }
489
+ }
490
+ }
491
+ break;
492
+ }
493
+ }
494
+ return { matchResult: matchResult, resolvedVia };
495
+ }
496
+ /**
497
+ * Applies learning boost to a MatchResult and returns the updated result.
498
+ * Shared by ask() and explain() to avoid logic divergence.
499
+ */
500
+ async applyBoostToMatchResult(query, matchResult) {
501
+ const hasKeywordSignal = matchResult.candidates.some(c => c.score > 0);
502
+ if (!hasKeywordSignal || matchResult.candidates.length === 0 || !this.learning || this.mode === 'cheap') {
503
+ return matchResult;
504
+ }
505
+ const boosted = await this.applyLearningBoost(query, matchResult.candidates);
506
+ if (boosted.length === 0)
507
+ return matchResult;
508
+ const newWinner = boosted.reduce((a, b) => {
509
+ if (b.score > a.score)
510
+ return b;
511
+ if (b.score === a.score && b.matched)
512
+ return b; // original winner wins ties
513
+ return a;
514
+ });
515
+ const oldWinner = matchResult.candidates.find(c => c.matched);
516
+ if (newWinner.capabilityId !== oldWinner?.capabilityId && newWinner.score >= this.threshold) {
517
+ const newCap = this.manifest.capabilities.find(c => c.id === newWinner.capabilityId) ?? null;
518
+ const newParams = newCap ? extractParams(query, newCap) : {};
519
+ logger.info(`Learning boost changed winner: "${oldWinner?.capabilityId ?? 'none'}" → "${newWinner.capabilityId}"`);
520
+ return {
521
+ ...matchResult,
522
+ capability: newCap,
523
+ confidence: newWinner.score,
524
+ intent: newCap ? resolverToIntent(newCap) : 'out_of_scope',
525
+ extractedParams: newParams,
526
+ candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === newWinner.capabilityId })),
527
+ reasoning: `Matched "${newWinner.capabilityId}" via learning boost (score: ${newWinner.score})`,
528
+ };
529
+ }
530
+ return {
531
+ ...matchResult,
532
+ confidence: newWinner.score,
533
+ candidates: boosted.map(c => ({ ...c, matched: c.capabilityId === (oldWinner?.capabilityId ?? '') })),
534
+ };
535
+ }
585
536
  /**
586
537
  * Applies learning boost to match candidates based on historical usage.
587
538
  * Capabilities that have previously matched similar keywords get a small
@@ -591,14 +542,10 @@ export class CapmanEngine {
591
542
  if (!this.learning)
592
543
  return candidates;
593
544
  // Use cached stats — rebuilt only when new entries recorded
594
- if (this.statsInvalidated) {
595
- this.cachedStats = await this.learning.getStats();
596
- this.statsInvalidated = false;
597
- }
598
- const stats = this.cachedStats;
545
+ const stats = await this.learning.getStats();
599
546
  if (!stats || Object.keys(stats.index).length === 0)
600
547
  return candidates;
601
- const qWords = query.toLowerCase().split(/\W+/).filter(w => w.length > 2);
548
+ const qWords = query.toLowerCase().split(/\W+/).filter(w => w.length > 2 && !STOPWORDS.has(w));
602
549
  if (qWords.length === 0)
603
550
  return candidates;
604
551
  return candidates.map(candidate => {
@@ -645,6 +592,5 @@ export class CapmanEngine {
645
592
  resolvedVia,
646
593
  timestamp: new Date().toISOString(),
647
594
  });
648
- this.statsInvalidated = true;
649
595
  }
650
596
  }
@@ -2,7 +2,7 @@ export { setLogLevel } from './logger';
2
2
  export type { LogLevel } from './logger';
3
3
  export type { Capability, CapabilityParam, CapmanConfig, Manifest, MatchResult, ExecutionTrace, TraceStep, MatchCandidate, ResolveResult, ApiCallResult, ValidationResult, Resolver, ApiResolver, NavResolver, HybridResolver, PrivacyScope, ResolverType, HttpMethod, ExplainResult, ExplainCandidate, } from './types';
4
4
  export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
5
- export { match, matchWithLLM, } from './matcher';
5
+ export { match, matchWithLLM, extractParams, } from './matcher';
6
6
  export type { LLMMatcherOptions } from './matcher';
7
7
  export { resolve } from './resolver';
8
8
  export type { ResolveOptions, AuthContext } from './resolver';
package/dist/esm/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { setLogLevel } from './logger';
2
2
  export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
3
- export { match, matchWithLLM, } from './matcher';
3
+ export { match, matchWithLLM, extractParams, } from './matcher';
4
4
  export { resolve } from './resolver';
5
5
  // ─── Engine (recommended API) ─────────────────────────────────────────────────
6
6
  export { CapmanEngine } from './engine';
@@ -26,17 +26,26 @@ export interface LearningStore {
26
26
  id: string;
27
27
  hits: number;
28
28
  }>>;
29
- clear(): Promise<void>;
29
+ /** Returns the live keyword index without rebuilding — O(1) */
30
+ getIndex(): Promise<Record<string, Record<string, number>>>;
30
31
  }
31
32
  export declare class FileLearningStore implements LearningStore {
32
33
  private filePath;
33
34
  private entries;
34
35
  private loaded;
36
+ private saveQueue;
37
+ private index;
38
+ private statsCounter;
35
39
  constructor(filePath?: string);
40
+ private updateIndex;
41
+ private subtractFromIndex;
42
+ private rebuildIndex;
36
43
  private load;
37
44
  private save;
45
+ private _doSave;
38
46
  record(entry: LearningEntry): Promise<void>;
39
47
  getStats(): Promise<KeywordStats>;
48
+ getIndex(): Promise<Record<string, Record<string, number>>>;
40
49
  getTopCapabilities(limit?: number): Promise<Array<{
41
50
  id: string;
42
51
  hits: number;
@@ -45,8 +54,14 @@ export declare class FileLearningStore implements LearningStore {
45
54
  }
46
55
  export declare class MemoryLearningStore implements LearningStore {
47
56
  private entries;
57
+ private index;
58
+ private statsCounter;
48
59
  record(entry: LearningEntry): Promise<void>;
49
60
  getStats(): Promise<KeywordStats>;
61
+ getIndex(): Promise<Record<string, Record<string, number>>>;
62
+ private updateIndex;
63
+ private subtractFromIndex;
64
+ private rebuildIndex;
50
65
  getTopCapabilities(limit?: number): Promise<Array<{
51
66
  id: string;
52
67
  hits: number;