capman 0.5.0 → 0.5.2

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 (53) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/bin/lib/cmd-demo.js +2 -2
  3. package/dist/cjs/cache.d.ts +6 -4
  4. package/dist/cjs/cache.d.ts.map +1 -1
  5. package/dist/cjs/cache.js +44 -13
  6. package/dist/cjs/cache.js.map +1 -1
  7. package/dist/cjs/engine.d.ts +23 -1
  8. package/dist/cjs/engine.d.ts.map +1 -1
  9. package/dist/cjs/engine.js +168 -165
  10. package/dist/cjs/engine.js.map +1 -1
  11. package/dist/cjs/index.d.ts +3 -2
  12. package/dist/cjs/index.d.ts.map +1 -1
  13. package/dist/cjs/index.js +3 -1
  14. package/dist/cjs/index.js.map +1 -1
  15. package/dist/cjs/learning.d.ts +2 -1
  16. package/dist/cjs/learning.d.ts.map +1 -1
  17. package/dist/cjs/learning.js +104 -20
  18. package/dist/cjs/learning.js.map +1 -1
  19. package/dist/cjs/matcher.d.ts +15 -1
  20. package/dist/cjs/matcher.d.ts.map +1 -1
  21. package/dist/cjs/matcher.js +45 -14
  22. package/dist/cjs/matcher.js.map +1 -1
  23. package/dist/cjs/parser.js +16 -2
  24. package/dist/cjs/parser.js.map +1 -1
  25. package/dist/cjs/resolver.d.ts.map +1 -1
  26. package/dist/cjs/resolver.js +32 -6
  27. package/dist/cjs/resolver.js.map +1 -1
  28. package/dist/cjs/schema.d.ts +14 -14
  29. package/dist/cjs/schema.d.ts.map +1 -1
  30. package/dist/cjs/schema.js +4 -2
  31. package/dist/cjs/schema.js.map +1 -1
  32. package/dist/cjs/types.d.ts +1 -0
  33. package/dist/cjs/types.d.ts.map +1 -1
  34. package/dist/cjs/version.d.ts +1 -1
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/esm/cache.d.ts +6 -4
  37. package/dist/esm/cache.js +44 -13
  38. package/dist/esm/engine.d.ts +23 -1
  39. package/dist/esm/engine.js +169 -166
  40. package/dist/esm/index.d.ts +3 -2
  41. package/dist/esm/index.js +1 -0
  42. package/dist/esm/learning.d.ts +2 -1
  43. package/dist/esm/learning.js +104 -20
  44. package/dist/esm/matcher.d.ts +15 -1
  45. package/dist/esm/matcher.js +43 -13
  46. package/dist/esm/parser.js +16 -2
  47. package/dist/esm/resolver.js +32 -6
  48. package/dist/esm/schema.d.ts +14 -14
  49. package/dist/esm/schema.js +4 -2
  50. package/dist/esm/types.d.ts +1 -0
  51. package/dist/esm/version.d.ts +1 -1
  52. package/dist/esm/version.js +1 -1
  53. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent, extractParams, STOPWORDS } from './matcher';
1
+ import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent, extractParams, STOPWORDS, LLMParseError } from './matcher';
2
2
  import { resolve as _resolve } from './resolver';
3
3
  import { MemoryLearningStore } from './learning';
4
4
  import { logger } from './logger';
@@ -20,10 +20,11 @@ export class CapmanEngine {
20
20
  this.auth = options.auth;
21
21
  this.headers = options.headers;
22
22
  this.threshold = options.threshold ?? 50;
23
+ this.cacheTtlMs = options.cacheTtlMs ?? null;
23
24
  this.maxLLMCallsPerMinute = options.maxLLMCallsPerMinute ?? 60;
24
25
  this.llmCooldownMs = options.llmCooldownMs ?? 0;
25
26
  this.llmCircuitBreakerThreshold = options.llmCircuitBreakerThreshold ?? 3;
26
- this.llmCircuitBreakerResetMs = options.llmCircuitBreakerResetMs ?? 60000;
27
+ this.llmCircuitBreakerResetMs = options.llmCircuitBreakerResetMs ?? 60_000;
27
28
  // Cache — default MemoryCache (no filesystem writes), or disabled with false
28
29
  // Use FileCache or ComboCache explicitly for persistence across restarts
29
30
  this.cache = options.cache === false
@@ -36,14 +37,14 @@ export class CapmanEngine {
36
37
  : (options.learning ?? new MemoryLearningStore());
37
38
  logger.info(`CapmanEngine initialized — mode: ${this.mode}, cache: ${this.cache ? 'enabled' : 'disabled'}, learning: ${this.learning ? 'enabled' : 'disabled'}`);
38
39
  // ── Manifest version compatibility check ─────────────────────────────────
39
- const manifestMajorMinor = options.manifest.version?.split('.').slice(0, 2).join('.');
40
- const engineMajorMinor = VERSION.split('.').slice(0, 2).join('.');
41
- if (manifestMajorMinor && manifestMajorMinor !== engineMajorMinor) {
42
- // Use console.warn directly must be visible regardless of logger level
43
- // Default log level is 'silent' so logger.warn would never be seen
44
- console.warn(`[capman] Manifest version "${options.manifest.version}" was generated with a ` +
45
- `different engine version than "${VERSION}". If you experience matching issues, ` +
46
- `regenerate with: npx capman generate`);
40
+ if (options.manifest.version) {
41
+ const [mMaj, mMin] = options.manifest.version.split('.').map(Number);
42
+ const [eMaj, eMin] = VERSION.split('.').map(Number);
43
+ if (mMaj !== eMaj || mMin !== eMin) {
44
+ console.warn(`[capman] Manifest version "${options.manifest.version}" was generated with a ` +
45
+ `different engine version than "${VERSION}". This is usually fine across patch versions. ` +
46
+ `If you experience unexpected matching issues, regenerate with: npx capman generate`);
47
+ }
47
48
  }
48
49
  }
49
50
  /**
@@ -58,18 +59,34 @@ export class CapmanEngine {
58
59
  * console.log(result.resolvedVia) // 'keyword' | 'llm' | 'cache'
59
60
  */
60
61
  async ask(query, overrides = {}) {
62
+ if (!query || typeof query !== 'string') {
63
+ throw new TypeError('query must be a non-empty string');
64
+ }
65
+ if (query.length > CapmanEngine.MAX_QUERY_LENGTH) {
66
+ throw new RangeError(`query exceeds maximum length of ${CapmanEngine.MAX_QUERY_LENGTH} characters`);
67
+ }
61
68
  const start = Date.now();
62
69
  const steps = [];
63
- let resolvedVia = 'keyword';
64
70
  // ── Step 1: Check cache ──────────────────────────────────────────────────
65
71
  const cacheStart = Date.now();
66
72
  if (this.cache) {
67
73
  const queryKey = normalizeQuery(query);
68
- const cached = await this.cache.get(queryKey);
74
+ const cached = await this.cache.get(queryKey, this.cacheTtlMs ?? undefined);
69
75
  if (cached) {
70
76
  steps.push({ type: 'cache_check', status: 'hit', durationMs: Date.now() - cacheStart, detail: 'Served from cache' });
71
77
  logger.info(`Cache hit for: "${query}"`);
72
- const resolution = await _resolve(cached.result, cached.result.extractedParams, this.resolveOptions(overrides));
78
+ // Re-extract params from the current query — never re-use cached params.
79
+ // Cached params belong to the original query (potentially from a different user).
80
+ // e.g. User A: "show orders for john" → cached with { customer: 'john' }
81
+ // User B: "show orders for jane" → must get { customer: 'jane' }, not john's
82
+ const freshParams = cached.result.capability
83
+ ? extractParams(query, cached.result.capability)
84
+ : {};
85
+ const matchWithFreshParams = {
86
+ ...cached.result,
87
+ extractedParams: freshParams,
88
+ };
89
+ const resolution = await _resolve(matchWithFreshParams, freshParams, this.resolveOptions(overrides));
73
90
  const trace = {
74
91
  query,
75
92
  candidates: cached.result.candidates,
@@ -79,13 +96,13 @@ export class CapmanEngine {
79
96
  totalMs: Date.now() - start,
80
97
  };
81
98
  const result = {
82
- match: cached.result,
99
+ match: matchWithFreshParams,
83
100
  resolution,
84
101
  resolvedVia: 'cache',
85
102
  durationMs: Date.now() - start,
86
103
  trace,
87
104
  };
88
- await this.recordLearning(query, cached.result, 'cache');
105
+ await this.recordLearning(query, matchWithFreshParams, 'cache');
89
106
  return result;
90
107
  }
91
108
  steps.push({ type: 'cache_check', status: 'miss', durationMs: Date.now() - cacheStart });
@@ -94,88 +111,7 @@ export class CapmanEngine {
94
111
  steps.push({ type: 'cache_check', status: 'skip', durationMs: 0, detail: 'Cache disabled' });
95
112
  }
96
113
  // ── 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
- const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
124
- if (!isParseError)
125
- this.recordLLMFailure();
126
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
127
- const t2 = Date.now();
128
- matchResult = _match(query, this.manifest);
129
- steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
130
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
131
- }
132
- }
133
- }
134
- else {
135
- logger.warn('accurate mode requires llm — falling back to keyword');
136
- const t = Date.now();
137
- matchResult = _match(query, this.manifest);
138
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
139
- }
140
- break;
141
- }
142
- case 'balanced':
143
- default: {
144
- const t1 = Date.now();
145
- const keywordResult = _match(query, this.manifest);
146
- steps.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
147
- if (keywordResult.confidence >= this.threshold || !this.llm) {
148
- matchResult = keywordResult;
149
- }
150
- else {
151
- const skipReason = this.checkLLMAllowed();
152
- if (skipReason) {
153
- logger.warn(`LLM skipped — ${skipReason}`);
154
- steps.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
155
- matchResult = keywordResult;
156
- }
157
- else {
158
- logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
159
- const t2 = Date.now();
160
- try {
161
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
162
- this.recordLLMSuccess();
163
- resolvedVia = 'llm';
164
- steps.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
165
- }
166
- catch (err) {
167
- const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
168
- if (!isParseError)
169
- this.recordLLMFailure();
170
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
171
- steps.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
172
- matchResult = keywordResult;
173
- }
174
- }
175
- }
176
- break;
177
- }
178
- }
114
+ let { matchResult, resolvedVia } = await this._runMatch(query, steps);
179
115
  const preBoostMatchResult = matchResult; // kept for learning recording only — prevents feedback loop
180
116
  // ── Step 2.5: Apply learning boost ───────────────────────────────────────
181
117
  matchResult = await this.applyBoostToMatchResult(query, matchResult);
@@ -294,68 +230,16 @@ export class CapmanEngine {
294
230
  * console.log(explanation.candidates)
295
231
  */
296
232
  async explain(query) {
297
- const start = Date.now();
298
- // ── Match — mirrors ask() logic including rate limiting ───────────────────
299
- let matchResult;
300
- let resolvedVia = 'keyword';
301
- if (this.mode === 'accurate') {
302
- if (this.llm) {
303
- const skipReason = this.checkLLMAllowed();
304
- if (skipReason) {
305
- logger.warn(`explain(): LLM skipped — ${skipReason} — falling back to keyword`);
306
- matchResult = _match(query, this.manifest);
307
- }
308
- else {
309
- try {
310
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
311
- this.recordLLMSuccess();
312
- resolvedVia = 'llm';
313
- }
314
- catch (err) {
315
- const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
316
- if (!isParseError)
317
- this.recordLLMFailure();
318
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
319
- matchResult = _match(query, this.manifest);
320
- }
321
- }
322
- }
323
- else {
324
- matchResult = _match(query, this.manifest);
325
- }
233
+ if (!query || typeof query !== 'string') {
234
+ throw new TypeError('query must be a non-empty string');
326
235
  }
327
- else if (this.mode === 'balanced' && this.llm) {
328
- // Keyword first escalate to LLM if low confidence (same as ask())
329
- const keywordResult = _match(query, this.manifest);
330
- if (keywordResult.confidence >= this.threshold) {
331
- matchResult = keywordResult;
332
- }
333
- else {
334
- const skipReason = this.checkLLMAllowed();
335
- if (skipReason) {
336
- logger.warn(`explain(): LLM skipped — ${skipReason}`);
337
- matchResult = keywordResult;
338
- }
339
- else {
340
- try {
341
- matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
342
- this.recordLLMSuccess();
343
- resolvedVia = 'llm';
344
- }
345
- catch (err) {
346
- const isParseError = String(err).startsWith('LLM_PARSE_ERROR');
347
- if (!isParseError)
348
- this.recordLLMFailure();
349
- logger.warn(`LLM call failed — falling back to keyword: ${err}`);
350
- matchResult = keywordResult;
351
- }
352
- }
353
- }
354
- }
355
- else {
356
- // cheap mode or no llm — keyword only
357
- matchResult = _match(query, this.manifest);
236
+ if (query.length > CapmanEngine.MAX_QUERY_LENGTH) {
237
+ throw new RangeError(`query exceeds maximum length of ${CapmanEngine.MAX_QUERY_LENGTH} characters`);
358
238
  }
239
+ const start = Date.now();
240
+ // ── Match — shared with ask() via _runMatch() ─────────────────────────────
241
+ let { matchResult, resolvedVia: _resolvedVia } = await this._runMatch(query);
242
+ let resolvedVia = _resolvedVia;
359
243
  // ── Apply learning boost (same as ask()) ─────────────────────────────────
360
244
  matchResult = await this.applyBoostToMatchResult(query, matchResult);
361
245
  // ── Build candidate explanations ─────────────────────────────────────────
@@ -400,12 +284,17 @@ export class CapmanEngine {
400
284
  }
401
285
  reasoning.push(`Resolved via: ${resolvedVia}`);
402
286
  if (matchResult.extractedParams && Object.keys(matchResult.extractedParams).length) {
403
- const params = Object.entries(matchResult.extractedParams)
287
+ const extracted = Object.entries(matchResult.extractedParams)
404
288
  .filter(([, v]) => v !== null)
405
289
  .map(([k, v]) => `${k}=${v}`)
406
290
  .join(', ');
407
- if (params)
408
- reasoning.push(`Would extract params: ${params}`);
291
+ const session = matchResult.capability?.params
292
+ .filter(p => p.source === 'session')
293
+ .map(p => `${p.name}=[from auth]`)
294
+ .join(', ');
295
+ const parts = [extracted, session].filter(Boolean).join(', ');
296
+ if (parts)
297
+ reasoning.push(`Would extract params: ${parts}`);
409
298
  }
410
299
  // ── Build wouldExecute ───────────────────────────────────────────────────
411
300
  const cap = matchResult.capability;
@@ -438,7 +327,7 @@ export class CapmanEngine {
438
327
  let path = endpoint.path;
439
328
  for (const [k, v] of Object.entries(params)) {
440
329
  if (v)
441
- path = path.replace(`{${k}}`, v);
330
+ path = path.replaceAll(`{${k}}`, v);
442
331
  }
443
332
  const base = this.baseUrl ?? '';
444
333
  action = `${endpoint.method} ${base}${path}`;
@@ -447,7 +336,7 @@ export class CapmanEngine {
447
336
  let dest = cap.resolver.destination;
448
337
  for (const [k, v] of Object.entries(params)) {
449
338
  if (v)
450
- dest = dest.replace(`{${k}}`, v);
339
+ dest = dest.replaceAll(`{${k}}`, v);
451
340
  }
452
341
  action = `navigate → ${dest}`;
453
342
  }
@@ -457,12 +346,12 @@ export class CapmanEngine {
457
346
  let path = endpoint.path;
458
347
  for (const [k, v] of Object.entries(params)) {
459
348
  if (v)
460
- path = path.replace(`{${k}}`, v);
349
+ path = path.replaceAll(`{${k}}`, v);
461
350
  }
462
351
  let dest = hybrid.nav.destination;
463
352
  for (const [k, v] of Object.entries(params)) {
464
353
  if (v)
465
- dest = dest.replace(`{${k}}`, v);
354
+ dest = dest.replaceAll(`{${k}}`, v);
466
355
  }
467
356
  const base = this.baseUrl ?? '';
468
357
  action = `${endpoint.method} ${base}${path} + navigate → ${dest}`;
@@ -511,13 +400,13 @@ export class CapmanEngine {
511
400
  }
512
401
  // ── Per-minute rate limit ────────────────────────────────────────────────
513
402
  const windowElapsed = now - this.llmWindowStart;
514
- if (windowElapsed >= 60000) {
403
+ if (windowElapsed >= 60_000) {
515
404
  this.llmCallsThisMinute = 0;
516
405
  this.llmWindowStart = now;
517
406
  }
518
407
  if (this.llmCallsThisMinute >= this.maxLLMCallsPerMinute) {
519
408
  // Recalculate elapsed after possible window reset above
520
- const resetIn = Math.ceil((60000 - (now - this.llmWindowStart)) / 1000);
409
+ const resetIn = Math.ceil((60_000 - (now - this.llmWindowStart)) / 1000);
521
410
  return `rate limit reached (${this.maxLLMCallsPerMinute}/min) — resets in ${Math.max(0, resetIn)}s`;
522
411
  }
523
412
  // Reserve the slot atomically before the call happens
@@ -541,6 +430,118 @@ export class CapmanEngine {
541
430
  logger.warn(`LLM circuit breaker opened after ${this.llmConsecutiveFails} consecutive failures — pausing for ${this.llmCircuitBreakerResetMs / 1000}s`);
542
431
  }
543
432
  }
433
+ /**
434
+ * Runs the matching pipeline for a query — shared by ask() and explain().
435
+ * Handles cheap / balanced / accurate mode dispatch and LLM rate limiting.
436
+ * Returns the match result and which resolver was used.
437
+ */
438
+ async _runMatch(query, steps) {
439
+ let matchResult;
440
+ let resolvedVia = 'keyword';
441
+ switch (this.mode) {
442
+ case 'cheap': {
443
+ const t = Date.now();
444
+ matchResult = _match(query, this.manifest);
445
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
446
+ break;
447
+ }
448
+ case 'accurate': {
449
+ if (this.llm) {
450
+ const skipReason = this.checkLLMAllowed();
451
+ if (skipReason) {
452
+ logger.warn(`LLM skipped — ${skipReason} — falling back to keyword`);
453
+ const t = Date.now();
454
+ matchResult = _match(query, this.manifest);
455
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: `llm skipped: ${skipReason}` });
456
+ }
457
+ else {
458
+ const t = Date.now();
459
+ try {
460
+ matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
461
+ this.recordLLMSuccess();
462
+ resolvedVia = 'llm';
463
+ // Merge keyword scores into LLM candidates so boost has real signal for alternatives
464
+ const kwResult = _match(query, this.manifest);
465
+ matchResult = {
466
+ ...matchResult,
467
+ candidates: matchResult.candidates.map(c => ({
468
+ ...c,
469
+ score: c.matched
470
+ ? c.score // keep LLM confidence for winner
471
+ : (kwResult.candidates.find(kc => kc.capabilityId === c.capabilityId)?.score ?? 0),
472
+ })),
473
+ };
474
+ steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t, detail: `confidence: ${matchResult.confidence}%` });
475
+ }
476
+ catch (err) {
477
+ const isParseError = err instanceof LLMParseError;
478
+ if (!isParseError)
479
+ this.recordLLMFailure();
480
+ logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
481
+ const t2 = Date.now();
482
+ matchResult = _match(query, this.manifest);
483
+ steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t, detail: String(err) });
484
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t2, detail: 'fallback after llm failure' });
485
+ }
486
+ }
487
+ }
488
+ else {
489
+ logger.warn('accurate mode requires llm — falling back to keyword');
490
+ const t = Date.now();
491
+ matchResult = _match(query, this.manifest);
492
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t, detail: 'llm not provided, used keyword' });
493
+ }
494
+ break;
495
+ }
496
+ case 'balanced':
497
+ default: {
498
+ const t1 = Date.now();
499
+ const keywordResult = _match(query, this.manifest);
500
+ steps?.push({ type: 'keyword_match', status: 'pass', durationMs: Date.now() - t1, detail: `confidence: ${keywordResult.confidence}%` });
501
+ if (keywordResult.confidence >= this.threshold || !this.llm) {
502
+ matchResult = keywordResult;
503
+ }
504
+ else {
505
+ const skipReason = this.checkLLMAllowed();
506
+ if (skipReason) {
507
+ logger.warn(`LLM skipped — ${skipReason}`);
508
+ steps?.push({ type: 'llm_match', status: 'skip', durationMs: 0, detail: skipReason });
509
+ matchResult = keywordResult;
510
+ }
511
+ else {
512
+ logger.info(`Low confidence (${keywordResult.confidence}%) — escalating to LLM`);
513
+ const t2 = Date.now();
514
+ try {
515
+ matchResult = await _matchWithLLM(query, this.manifest, { llm: this.llm });
516
+ this.recordLLMSuccess();
517
+ resolvedVia = 'llm';
518
+ // keywordResult already computed above in balanced mode — merge scores
519
+ matchResult = {
520
+ ...matchResult,
521
+ candidates: matchResult.candidates.map(c => ({
522
+ ...c,
523
+ score: c.matched
524
+ ? c.score
525
+ : (keywordResult.candidates.find(kc => kc.capabilityId === c.capabilityId)?.score ?? 0),
526
+ })),
527
+ };
528
+ steps?.push({ type: 'llm_match', status: 'pass', durationMs: Date.now() - t2, detail: `confidence: ${matchResult.confidence}%` });
529
+ }
530
+ catch (err) {
531
+ const isParseError = err instanceof LLMParseError;
532
+ if (!isParseError)
533
+ this.recordLLMFailure();
534
+ logger.warn(`LLM call failed — falling back to keyword: ${err instanceof Error ? err.message : String(err)}`);
535
+ steps?.push({ type: 'llm_match', status: 'fail', durationMs: Date.now() - t2, detail: String(err) });
536
+ matchResult = keywordResult;
537
+ }
538
+ }
539
+ }
540
+ break;
541
+ }
542
+ }
543
+ return { matchResult: matchResult, resolvedVia };
544
+ }
544
545
  /**
545
546
  * Applies learning boost to a MatchResult and returns the updated result.
546
547
  * Shared by ask() and explain() to avoid logic divergence.
@@ -642,3 +643,5 @@ export class CapmanEngine {
642
643
  });
643
644
  }
644
645
  }
646
+ /** Maximum allowed query length in characters. Queries exceeding this throw RangeError. */
647
+ CapmanEngine.MAX_QUERY_LENGTH = 1000;
@@ -3,6 +3,7 @@ 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
5
  export { match, matchWithLLM, extractParams, } from './matcher';
6
+ export { LLMParseError } from './matcher';
6
7
  export type { LLMMatcherOptions } from './matcher';
7
8
  export { resolve } from './resolver';
8
9
  export type { ResolveOptions, AuthContext } from './resolver';
@@ -14,10 +15,10 @@ export { FileLearningStore, MemoryLearningStore } from './learning';
14
15
  export type { LearningStore, LearningEntry, KeywordStats } from './learning';
15
16
  export { parseOpenAPI } from './parser';
16
17
  export type { ParseResult } from './parser';
17
- import type { Manifest, MatchResult, ResolveResult } from './types';
18
+ import type { Manifest, MatchResult, ResolveResult, MatchMode } from './types';
18
19
  import type { LLMMatcherOptions } from './matcher';
19
20
  import type { ResolveOptions } from './resolver';
20
- export type MatchMode = 'cheap' | 'balanced' | 'accurate';
21
+ export type { MatchMode } from './types';
21
22
  export interface AskOptions extends ResolveOptions {
22
23
  llm?: LLMMatcherOptions['llm'];
23
24
  /**
package/dist/esm/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { setLogLevel } from './logger';
2
2
  export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
3
3
  export { match, matchWithLLM, extractParams, } from './matcher';
4
+ export { LLMParseError } from './matcher';
4
5
  export { resolve } from './resolver';
5
6
  // ─── Engine (recommended API) ─────────────────────────────────────────────────
6
7
  export { CapmanEngine } from './engine';
@@ -38,6 +38,7 @@ export declare class FileLearningStore implements LearningStore {
38
38
  private statsCounter;
39
39
  constructor(filePath?: string);
40
40
  private updateIndex;
41
+ private subtractFromIndex;
41
42
  private rebuildIndex;
42
43
  private load;
43
44
  private save;
@@ -59,7 +60,7 @@ export declare class MemoryLearningStore implements LearningStore {
59
60
  getStats(): Promise<KeywordStats>;
60
61
  getIndex(): Promise<Record<string, Record<string, number>>>;
61
62
  private updateIndex;
62
- private rebuildIndex;
63
+ private subtractFromIndex;
63
64
  getTopCapabilities(limit?: number): Promise<Array<{
64
65
  id: string;
65
66
  hits: number;