capman 0.6.1 → 0.6.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 (42) hide show
  1. package/CODEBASE.md +6 -5
  2. package/dist/cjs/concurrent.d.ts +53 -0
  3. package/dist/cjs/concurrent.d.ts.map +1 -0
  4. package/dist/cjs/concurrent.js +71 -0
  5. package/dist/cjs/concurrent.js.map +1 -0
  6. package/dist/cjs/engine.d.ts +82 -12
  7. package/dist/cjs/engine.d.ts.map +1 -1
  8. package/dist/cjs/engine.js +159 -37
  9. package/dist/cjs/engine.js.map +1 -1
  10. package/dist/cjs/index.d.ts +2 -1
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js +3 -1
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/learning.d.ts +14 -6
  15. package/dist/cjs/learning.d.ts.map +1 -1
  16. package/dist/cjs/learning.js +64 -10
  17. package/dist/cjs/learning.js.map +1 -1
  18. package/dist/cjs/matcher.d.ts +13 -1
  19. package/dist/cjs/matcher.d.ts.map +1 -1
  20. package/dist/cjs/matcher.js +67 -10
  21. package/dist/cjs/matcher.js.map +1 -1
  22. package/dist/cjs/schema.js +1 -1
  23. package/dist/cjs/schema.js.map +1 -1
  24. package/dist/cjs/types.d.ts +9 -0
  25. package/dist/cjs/types.d.ts.map +1 -1
  26. package/dist/cjs/version.d.ts +1 -1
  27. package/dist/cjs/version.js +1 -1
  28. package/dist/esm/concurrent.d.ts +52 -0
  29. package/dist/esm/concurrent.js +66 -0
  30. package/dist/esm/engine.d.ts +82 -12
  31. package/dist/esm/engine.js +159 -37
  32. package/dist/esm/index.d.ts +2 -1
  33. package/dist/esm/index.js +1 -0
  34. package/dist/esm/learning.d.ts +14 -6
  35. package/dist/esm/learning.js +64 -10
  36. package/dist/esm/matcher.d.ts +13 -1
  37. package/dist/esm/matcher.js +66 -10
  38. package/dist/esm/schema.js +1 -1
  39. package/dist/esm/types.d.ts +9 -0
  40. package/dist/esm/version.d.ts +1 -1
  41. package/dist/esm/version.js +1 -1
  42. package/package.json +1 -1
@@ -237,6 +237,20 @@ export function extractBigrams(tokens) {
237
237
  }
238
238
  return bigrams;
239
239
  }
240
+ /**
241
+ * Reciprocal Rank Fusion — fuses multiple ranked lists into a single score map.
242
+ * k=60 is the standard literature default.
243
+ */
244
+ export function rrf(rankings, k = 60) {
245
+ const scores = new Map();
246
+ for (const ranking of rankings) {
247
+ const sorted = [...ranking].sort((a, b) => b.score - a.score);
248
+ sorted.forEach((item, rank) => {
249
+ scores.set(item.id, (scores.get(item.id) ?? 0) + 1 / (k + rank + 1));
250
+ });
251
+ }
252
+ return scores;
253
+ }
240
254
  /**
241
255
  * Returns a sub-manifest containing only capabilities that match ALL provided tags.
242
256
  * Capabilities without tags are excluded when tags filter is active.
@@ -528,16 +542,57 @@ export function match(query, manifest, options = {}) {
528
542
  const b = options.bm25B ?? 0.75;
529
543
  // Calibrate ceiling — max self-score for normalization
530
544
  const ceiling = options.bm25Ceiling ?? calibrateCeiling(manifest.capabilities, bm25Index, k1, b);
531
- const allScores = [];
545
+ // Build per-source ranked lists for RRF fusion
546
+ const keywordRanking = [];
547
+ const fuzzyRanking = [];
548
+ const embeddingRanking = [];
549
+ const keywordScoreMap = new Map();
532
550
  for (const cap of manifest.capabilities) {
533
551
  const rawBM25 = scoreCapability(qWordSet, cap, bm25Index, k1, b);
534
552
  const bm25Score = Math.min(100, Math.round((rawBM25 / ceiling) * 100));
535
553
  const bonusPoints = bigramBonus(qBigrams, bm25Index.bigrams[cap.id] ?? new Set());
536
554
  const keywordScore = Math.min(100, bm25Score + bonusPoints);
537
555
  const fuzzyScore = fuzzyScoreMap.get(cap.id) ?? 0;
538
- const via = fuzzyScore > keywordScore ? 'fuzzy' : 'keyword';
539
- const score = Math.min(100, Math.round(Math.max(keywordScore, fuzzyScore)));
540
- logger.debug(` scored "${cap.id}": ${score}% (keyword: ${keywordScore}%, fuzzy: ${Math.round(fuzzyScore)}%)`);
556
+ const embScore = options.embeddingScores?.get(cap.id) ?? 0;
557
+ if (keywordScore > 0)
558
+ keywordRanking.push({ id: cap.id, score: keywordScore });
559
+ keywordScoreMap.set(cap.id, keywordScore);
560
+ if (fuzzyScore > 0)
561
+ fuzzyRanking.push({ id: cap.id, score: fuzzyScore });
562
+ if (embScore > 0)
563
+ embeddingRanking.push({ id: cap.id, score: embScore });
564
+ }
565
+ // RRF fusion. Anchor to theoretical max — a rank-1 entry in all lists scores
566
+ // rankings.length/(k+1). Using observed max instead inflates zero-overlap queries
567
+ // (all capabilities rank equally) to 100%, breaking out-of-scope rejection.
568
+ const rrfK = 60;
569
+ const rankings = [
570
+ keywordRanking,
571
+ ...(fuzzyRanking.length > 0 ? [fuzzyRanking] : []),
572
+ ...(embeddingRanking.length > 0 ? [embeddingRanking] : []),
573
+ ];
574
+ const rrfScores = rrf(rankings, rrfK);
575
+ const theoreticalMax = rankings.length / (rrfK + 1);
576
+ // Pre-compute rank maps — rank 0 = best. Used for accurate via attribution.
577
+ const rankIn = (list, id) => {
578
+ const idx = list.findIndex(e => e.id === id);
579
+ return idx === -1 ? Infinity : idx;
580
+ };
581
+ const allScores = [];
582
+ for (const cap of manifest.capabilities) {
583
+ const rrfScore = rrfScores.get(cap.id) ?? 0;
584
+ const score = Math.min(100, Math.round((rrfScore / theoreticalMax) * 100));
585
+ const keywordScore = keywordScoreMap.get(cap.id) ?? 0;
586
+ const fuzzyScore = fuzzyScoreMap.get(cap.id) ?? 0;
587
+ const embScore = options.embeddingScores?.get(cap.id) ?? 0;
588
+ // via = whichever signal ranked this capability highest (lowest rank index).
589
+ // Uses rank position rather than raw score — RRF is rank-based, not score-based.
590
+ const kRank = rankIn(keywordRanking, cap.id);
591
+ const fRank = rankIn(fuzzyRanking, cap.id);
592
+ const eRank = rankIn(embeddingRanking, cap.id);
593
+ const via = eRank < fRank && eRank < kRank ? 'embedding' :
594
+ fRank < kRank ? 'fuzzy' : 'keyword';
595
+ logger.debug(` scored "${cap.id}": ${score}% (keyword: ${keywordScore}%, fuzzy: ${Math.round(fuzzyScore)}%, emb: ${Math.round(embScore)}%, rrf: ${rrfScore.toFixed(4)})`);
541
596
  allScores.push({ cap, score, via });
542
597
  if (score > bestScore) {
543
598
  bestScore = score;
@@ -567,7 +622,8 @@ export function match(query, manifest, options = {}) {
567
622
  logger.debug(`Extracted params: ${JSON.stringify(Object.fromEntries(Object.entries(params).map(([k, v]) => [k, v != null ? '[REDACTED]' : 'null'])))}`);
568
623
  // Use the via tag tracked during scoring — avoids redundant scoreCapability call.
569
624
  const bestEntry = allScores.find(s => s.cap.id === best.id);
570
- const winner = bestEntry?.via === 'fuzzy' ? 'fuzzy match' : 'keyword scoring';
625
+ const winner = bestEntry?.via === 'embedding' ? 'embedding match' :
626
+ bestEntry?.via === 'fuzzy' ? 'fuzzy match' : 'keyword scoring';
571
627
  // Matched return:
572
628
  return {
573
629
  capability: best,
@@ -589,17 +645,17 @@ export function match(query, manifest, options = {}) {
589
645
  * wrapper that maps the prompt to a proper system message, keeping user query
590
646
  * data in the user turn only.
591
647
  */
592
- export async function matchWithLLM(query, manifest, options) {
648
+ export async function matchWithLLM(query, topCandidates, options) {
593
649
  // Truncate description and examples — prevents context window overflow and
594
650
  // reduces prompt injection surface from third-party OpenAPI spec content.
595
651
  const MAX_DESC_LEN = 200;
596
652
  const MAX_EXAMPLE_LEN = 100;
597
- const manifestSummary = manifest.capabilities.map(c => `- ${c.id} (${c.resolver.type}): ${sanitizeForPrompt(c.description, MAX_DESC_LEN)}${c.examples?.length
653
+ const manifestSummary = topCandidates.map(c => `- ${c.id} (${c.resolver.type}): ${sanitizeForPrompt(c.description, MAX_DESC_LEN)}${c.examples?.length
598
654
  ? `\n examples: ${c.examples.slice(0, 2).map(e => sanitizeForPrompt(e, MAX_EXAMPLE_LEN)).join(', ')}`
599
655
  : ''}`).join('\n');
600
656
  // Sanitize app name — strip newlines and control characters that could
601
657
  // break the prompt structure or inject additional instructions.
602
- const safeApp = sanitizeForPrompt(manifest.app, 100);
658
+ const safeApp = sanitizeForPrompt(options.app ?? 'the application', 100);
603
659
  const prompt = `You are an intent matcher for an AI agent system.
604
660
 
605
661
  App: ${safeApp}
@@ -641,7 +697,7 @@ ${JSON.stringify({ user_query: query })}
641
697
  const isOOS = parsed.matched_capability === 'OUT_OF_SCOPE';
642
698
  const capability = isOOS
643
699
  ? null
644
- : manifest.capabilities.find(c => c.id === parsed.matched_capability) ?? null;
700
+ : topCandidates.find(c => c.id === parsed.matched_capability) ?? null;
645
701
  // If LLM returned an unknown capability ID, treat as out of scope
646
702
  const effectivelyOOS = isOOS || capability === null;
647
703
  if (!isOOS && capability === null) {
@@ -657,7 +713,7 @@ ${JSON.stringify({ user_query: query })}
657
713
  const llmConfidence = effectivelyOOS
658
714
  ? 0
659
715
  : Math.min(100, Math.max(0, Math.round(parsed.confidence)));
660
- const allCandidates = manifest.capabilities.map(c => ({
716
+ const allCandidates = topCandidates.map(c => ({
661
717
  capabilityId: c.id,
662
718
  score: c.id === capability?.id ? llmConfidence : 0,
663
719
  matched: c.id === capability?.id,
@@ -114,7 +114,7 @@ export const CapmanConfigSchema = z.object({
114
114
  .refine(caps => new Set(caps.map(c => c.id)).size === caps.length, 'capability ids must be unique'),
115
115
  }).refine(cfg => {
116
116
  const needsBaseUrl = cfg.capabilities.some(c => c.resolver.type === 'api' || c.resolver.type === 'hybrid');
117
- return !needsBaseUrl || !!cfg.baseUrl;
117
+ return !needsBaseUrl || !!cfg.baseUrl || (cfg.servers?.length ?? 0) > 0;
118
118
  }, { message: 'baseUrl is required when any capability uses an api or hybrid resolver' });
119
119
  // ─── Manifest Schema ──────────────────────────────────────────────────────────
120
120
  export const ManifestSchema = z.object({
@@ -81,6 +81,15 @@ export interface LifecycleInfo {
81
81
  /** Human-readable note for consumers */
82
82
  note?: string;
83
83
  }
84
+ /**
85
+ * Optional embedding provider for semantic similarity matching.
86
+ * Zero mandatory dependency — only used when passed to EngineOptions.
87
+ * Implement with any model: OpenAI, local ONNX, Transformers.js, etc.
88
+ */
89
+ export interface EmbeddingProvider {
90
+ /** Encode a batch of texts into fixed-length float vectors. */
91
+ encode(texts: string[]): Promise<number[][]>;
92
+ }
84
93
  export interface MatchHint {
85
94
  /**
86
95
  * Advisory preferred matching mode for this capability.
@@ -1 +1 @@
1
- export declare const VERSION = "0.6.0";
1
+ export declare const VERSION = "0.6.1";
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by scripts/version.js — do not edit manually
2
- export const VERSION = '0.6.0';
2
+ export const VERSION = '0.6.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capman",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "Capability Manifest Engine — let AI agents interact with your app without navigating the UI",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",