capman 0.6.0 → 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.
- package/CODEBASE.md +6 -5
- package/dist/cjs/cache.d.ts +9 -0
- package/dist/cjs/cache.d.ts.map +1 -1
- package/dist/cjs/cache.js +37 -7
- package/dist/cjs/cache.js.map +1 -1
- package/dist/cjs/concurrent.d.ts +53 -0
- package/dist/cjs/concurrent.d.ts.map +1 -0
- package/dist/cjs/concurrent.js +71 -0
- package/dist/cjs/concurrent.js.map +1 -0
- package/dist/cjs/engine.d.ts +92 -7
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +269 -57
- package/dist/cjs/engine.js.map +1 -1
- package/dist/cjs/generator.d.ts.map +1 -1
- package/dist/cjs/generator.js +28 -6
- package/dist/cjs/generator.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/learning.d.ts +16 -1
- package/dist/cjs/learning.d.ts.map +1 -1
- package/dist/cjs/learning.js +95 -14
- package/dist/cjs/learning.js.map +1 -1
- package/dist/cjs/matcher.d.ts +51 -2
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +173 -33
- package/dist/cjs/matcher.js.map +1 -1
- package/dist/cjs/parser.js +27 -9
- package/dist/cjs/parser.js.map +1 -1
- package/dist/cjs/resolver.d.ts +2 -2
- package/dist/cjs/resolver.d.ts.map +1 -1
- package/dist/cjs/resolver.js +66 -26
- package/dist/cjs/resolver.js.map +1 -1
- package/dist/cjs/schema.d.ts +821 -68
- package/dist/cjs/schema.d.ts.map +1 -1
- package/dist/cjs/schema.js +62 -13
- package/dist/cjs/schema.js.map +1 -1
- package/dist/cjs/types.d.ts +156 -9
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/cache.d.ts +9 -0
- package/dist/esm/cache.js +37 -7
- package/dist/esm/concurrent.d.ts +52 -0
- package/dist/esm/concurrent.js +66 -0
- package/dist/esm/engine.d.ts +92 -7
- package/dist/esm/engine.js +270 -58
- package/dist/esm/generator.js +28 -6
- package/dist/esm/index.d.ts +3 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/learning.d.ts +16 -1
- package/dist/esm/learning.js +95 -14
- package/dist/esm/matcher.d.ts +51 -2
- package/dist/esm/matcher.js +170 -33
- package/dist/esm/parser.js +27 -9
- package/dist/esm/resolver.d.ts +2 -2
- package/dist/esm/resolver.js +66 -26
- package/dist/esm/schema.d.ts +821 -68
- package/dist/esm/schema.js +62 -13
- package/dist/esm/types.d.ts +156 -9
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/generator.js
CHANGED
|
@@ -5,10 +5,14 @@ import { validateConfig, validateManifest } from './schema';
|
|
|
5
5
|
import { logger } from './logger';
|
|
6
6
|
export function generate(config) {
|
|
7
7
|
return {
|
|
8
|
+
schemaVersion: '1',
|
|
8
9
|
version: VERSION,
|
|
9
10
|
app: config.app,
|
|
10
11
|
generatedAt: new Date().toISOString(),
|
|
11
|
-
capabilities: config.capabilities.map(cap => ({ ...cap
|
|
12
|
+
capabilities: config.capabilities.map(cap => ({ ...cap })),
|
|
13
|
+
...(config.info ? { info: config.info } : {}),
|
|
14
|
+
...(config.tagRegistry ? { tagRegistry: config.tagRegistry } : {}),
|
|
15
|
+
...(config.servers ? { servers: config.servers } : {}),
|
|
12
16
|
};
|
|
13
17
|
}
|
|
14
18
|
export function loadConfig(configPath) {
|
|
@@ -33,6 +37,10 @@ export function loadConfig(configPath) {
|
|
|
33
37
|
// Use a CJS config file or convert with: module.exports = { ... }
|
|
34
38
|
// Full ESM config support is planned for v0.5.
|
|
35
39
|
try {
|
|
40
|
+
// Bust the module cache before loading — require() caches by resolved path,
|
|
41
|
+
// so a second call without this returns the stale version from the first call.
|
|
42
|
+
// This matters in watch mode and test suites that change config between calls.
|
|
43
|
+
delete require.cache[require.resolve(resolved)];
|
|
36
44
|
const mod = require(resolved);
|
|
37
45
|
raw = mod.default ?? mod;
|
|
38
46
|
}
|
|
@@ -80,7 +88,12 @@ export function writeManifest(manifest, outputPath = 'manifest.json') {
|
|
|
80
88
|
throw new Error(`writeManifest: output path "${outputPath}" resolves outside the working directory.\n` +
|
|
81
89
|
`Resolved: ${resolved}\nAllowed: ${cwd}`);
|
|
82
90
|
}
|
|
83
|
-
|
|
91
|
+
// Write atomically via tmp → rename — same pattern used by FileCache and
|
|
92
|
+
// FileLearningStore. A crash or SIGKILL mid-write leaves the .tmp file, not
|
|
93
|
+
// a truncated manifest.json, so the next readManifest() can still parse it.
|
|
94
|
+
const tmp = `${resolved}.tmp`;
|
|
95
|
+
fs.writeFileSync(tmp, JSON.stringify(manifest, null, 2));
|
|
96
|
+
fs.renameSync(tmp, resolved);
|
|
84
97
|
return resolved;
|
|
85
98
|
}
|
|
86
99
|
export function readManifest(manifestPath = 'manifest.json') {
|
|
@@ -122,14 +135,23 @@ export function validate(manifest) {
|
|
|
122
135
|
return { valid: errors.length === 0, errors, warnings };
|
|
123
136
|
}
|
|
124
137
|
export function generateStarterConfig() {
|
|
125
|
-
return `// capman.config.js
|
|
126
|
-
//
|
|
127
|
-
// Replace the examples below with your own app's capabilities.
|
|
138
|
+
return `// capman.config.js
|
|
139
|
+
// Auto-generated starter config — edit before use
|
|
128
140
|
|
|
129
141
|
module.exports = {
|
|
130
|
-
app: '
|
|
142
|
+
app: 'my-app',
|
|
131
143
|
baseUrl: 'https://api.your-app.com',
|
|
132
144
|
|
|
145
|
+
// Optional metadata block — used for documentation and provenance
|
|
146
|
+
info: {
|
|
147
|
+
title: 'My App',
|
|
148
|
+
description: 'Brief description of what this app does',
|
|
149
|
+
version: '1.0.0',
|
|
150
|
+
homepage: 'https://your-app.com',
|
|
151
|
+
contact: { name: 'Your Name', email: 'you@your-app.com' },
|
|
152
|
+
license: { name: 'MIT' },
|
|
153
|
+
},
|
|
154
|
+
|
|
133
155
|
capabilities: [
|
|
134
156
|
{
|
|
135
157
|
id: 'get_resource',
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
export { setLogLevel } from './logger';
|
|
2
2
|
export type { LogLevel } from './logger';
|
|
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';
|
|
3
|
+
export type { Capability, CapabilityParam, CapmanConfig, Manifest, MatchResult, ExecutionTrace, TraceStep, MatchCandidate, ResolveResult, ApiCallResult, ValidationResult, Resolver, ApiResolver, NavResolver, HybridResolver, PrivacyScope, ResolverType, HttpMethod, ExplainResult, ExplainCandidate, ManifestInfo, Server, LifecycleInfo, LifecycleStatus, CapabilityError, Endpoint, ParamType, MatchHint, EmbeddingProvider, } from './types';
|
|
4
4
|
export { generate, loadConfig, writeManifest, readManifest, validate, generateStarterConfig, } from './generator';
|
|
5
5
|
export { match, matchWithLLM, extractParams, } from './matcher';
|
|
6
6
|
export { LLMParseError } from './matcher';
|
|
7
7
|
export type { LLMMatcherOptions } from './matcher';
|
|
8
8
|
export { TYPE_PATTERNS } from './matcher';
|
|
9
|
+
export { filterByTags } from './matcher';
|
|
9
10
|
export { resolve } from './resolver';
|
|
10
11
|
export type { ResolveOptions, AuthContext } from './resolver';
|
|
11
12
|
export { CapmanEngine } from './engine';
|
|
13
|
+
export { ConcurrentCapmanEngine } from './concurrent';
|
|
12
14
|
export type { EngineOptions, EngineResult } from './engine';
|
|
13
15
|
export { MemoryCache, FileCache, ComboCache, buildCacheKey, normalizeQuery } from './cache';
|
|
14
16
|
export type { CacheStore, CacheEntry } from './cache';
|
package/dist/esm/index.js
CHANGED
|
@@ -3,9 +3,11 @@ export { generate, loadConfig, writeManifest, readManifest, validate, generateSt
|
|
|
3
3
|
export { match, matchWithLLM, extractParams, } from './matcher';
|
|
4
4
|
export { LLMParseError } from './matcher';
|
|
5
5
|
export { TYPE_PATTERNS } from './matcher';
|
|
6
|
+
export { filterByTags } from './matcher';
|
|
6
7
|
export { resolve } from './resolver';
|
|
7
8
|
// ─── Engine (recommended API) ─────────────────────────────────────────────────
|
|
8
9
|
export { CapmanEngine } from './engine';
|
|
10
|
+
export { ConcurrentCapmanEngine } from './concurrent';
|
|
9
11
|
// ─── Cache ────────────────────────────────────────────────────────────────────
|
|
10
12
|
export { MemoryCache, FileCache, ComboCache, buildCacheKey, normalizeQuery } from './cache';
|
|
11
13
|
// ─── Learning ─────────────────────────────────────────────────────────────────
|
package/dist/esm/learning.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ export interface LearningEntry {
|
|
|
6
6
|
extractedParams: Record<string, string | null>;
|
|
7
7
|
resolvedVia: 'keyword' | 'llm' | 'cache';
|
|
8
8
|
timestamp: string;
|
|
9
|
+
/**
|
|
10
|
+
* Confidence-derived weight stored at record time (confidence / 100, floor 0.1).
|
|
11
|
+
* Used by subtract() to reverse the exact contribution made by update(),
|
|
12
|
+
* preventing index drift when high-confidence entries are pruned.
|
|
13
|
+
* Optional for backwards-compatibility with persisted entries written before v0.5.5.
|
|
14
|
+
*/
|
|
15
|
+
weight?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Unix timestamp (ms) when this entry was last updated.
|
|
18
|
+
* Used for time-decay — older entries contribute less learning signal.
|
|
19
|
+
* Optional for backwards-compatibility with persisted entries written before v0.7.0.
|
|
20
|
+
* Migration: FileLearningStore falls back to file mtime for entries missing this field.
|
|
21
|
+
*/
|
|
22
|
+
lastUpdated?: number;
|
|
9
23
|
}
|
|
10
24
|
export interface KeywordStats {
|
|
11
25
|
/** keyword → Map of capabilityId → hit count */
|
|
@@ -43,7 +57,7 @@ export declare class FileLearningStore implements LearningStore {
|
|
|
43
57
|
private learningIndex;
|
|
44
58
|
private dirty;
|
|
45
59
|
private saveTimer;
|
|
46
|
-
constructor(filePath?: string);
|
|
60
|
+
constructor(filePath?: string, halfLifeDays?: number);
|
|
47
61
|
flushSync(): void;
|
|
48
62
|
/**
|
|
49
63
|
* Removes this store from the exit flush registry and cancels any pending save timer.
|
|
@@ -67,6 +81,7 @@ export declare class FileLearningStore implements LearningStore {
|
|
|
67
81
|
export declare class MemoryLearningStore implements LearningStore {
|
|
68
82
|
private entries;
|
|
69
83
|
private learningIndex;
|
|
84
|
+
constructor(halfLifeDays?: number);
|
|
70
85
|
record(entry: LearningEntry): Promise<void>;
|
|
71
86
|
getStats(): Promise<KeywordStats>;
|
|
72
87
|
getIndex(): Promise<Record<string, Record<string, number>>>;
|
package/dist/esm/learning.js
CHANGED
|
@@ -3,6 +3,15 @@ import * as path from 'path';
|
|
|
3
3
|
import { logger } from './logger';
|
|
4
4
|
const MAX_LEARNING_ENTRIES = 10_000;
|
|
5
5
|
import { tokenize } from './matcher';
|
|
6
|
+
/**
|
|
7
|
+
* Exponential decay — older entries contribute less signal.
|
|
8
|
+
* At exactly halfLifeDays old, a weight of 1.0 decays to 0.5.
|
|
9
|
+
* At 2× halfLifeDays, it decays to 0.25. And so on.
|
|
10
|
+
*/
|
|
11
|
+
function decayedWeight(weight, lastUpdated, halfLifeDays) {
|
|
12
|
+
const ageDays = (Date.now() - lastUpdated) / (1000 * 60 * 60 * 24);
|
|
13
|
+
return weight * Math.pow(0.5, ageDays / halfLifeDays);
|
|
14
|
+
}
|
|
6
15
|
// Module-level registry — tracks all active FileLearningStore instances
|
|
7
16
|
// for process exit flushing. Handlers registered once to avoid accumulation.
|
|
8
17
|
const activeStores = new Set();
|
|
@@ -56,11 +65,18 @@ function computeTopCapabilities(entries, limit) {
|
|
|
56
65
|
// Both FileLearningStore and MemoryLearningStore compose this instead of
|
|
57
66
|
// duplicating the same ~80 lines of index management logic.
|
|
58
67
|
class LearningIndex {
|
|
59
|
-
constructor() {
|
|
68
|
+
constructor(halfLifeDays = 30) {
|
|
60
69
|
this.index = {};
|
|
70
|
+
/** Tracks when each (word, capabilityId) cell was last reinforced — used for decay */
|
|
71
|
+
this.lastUpdatedIndex = {};
|
|
61
72
|
this.statsCounter = {
|
|
62
73
|
totalQueries: 0, llmQueries: 0, cacheHits: 0, outOfScope: 0,
|
|
63
74
|
};
|
|
75
|
+
if (halfLifeDays <= 0) {
|
|
76
|
+
throw new RangeError(`halfLifeDays must be a positive number — got ${halfLifeDays}. ` +
|
|
77
|
+
`Use a value in days e.g. 30 (1 month), 7 (1 week).`);
|
|
78
|
+
}
|
|
79
|
+
this.halfLifeDays = halfLifeDays;
|
|
64
80
|
}
|
|
65
81
|
update(entry) {
|
|
66
82
|
this.statsCounter.totalQueries++;
|
|
@@ -75,11 +91,21 @@ class LearningIndex {
|
|
|
75
91
|
// more signal than a 51% borderline match. Floor of 0.1 ensures
|
|
76
92
|
// borderline matches still contribute, just proportionally less.
|
|
77
93
|
const weight = Math.max(0.1, entry.confidence / 100);
|
|
94
|
+
// Respect a caller-supplied timestamp (historical replay, rebuild()).
|
|
95
|
+
// For brand-new real-time entries lastUpdated is undefined — default to now.
|
|
96
|
+
const now = entry.lastUpdated ?? Date.now();
|
|
97
|
+
// Store weight and timestamp on the entry so subtract() can reverse the
|
|
98
|
+
// exact amount and migration has an accurate record time.
|
|
99
|
+
entry.weight = weight;
|
|
100
|
+
entry.lastUpdated = now;
|
|
78
101
|
const words = tokenize(entry.query);
|
|
79
102
|
for (const word of words) {
|
|
80
103
|
this.index[word] ??= {};
|
|
81
104
|
this.index[word][entry.capabilityId] =
|
|
82
105
|
(this.index[word][entry.capabilityId] ?? 0) + weight;
|
|
106
|
+
// Track when this (word, cap) cell was last reinforced for decay
|
|
107
|
+
this.lastUpdatedIndex[word] ??= {};
|
|
108
|
+
this.lastUpdatedIndex[word][entry.capabilityId] = now;
|
|
83
109
|
}
|
|
84
110
|
}
|
|
85
111
|
}
|
|
@@ -99,15 +125,19 @@ class LearningIndex {
|
|
|
99
125
|
for (const word of words) {
|
|
100
126
|
if (!this.index[word])
|
|
101
127
|
continue;
|
|
102
|
-
//
|
|
103
|
-
//
|
|
128
|
+
// Use the weight stored at record time for exact symmetric subtraction.
|
|
129
|
+
// Fallback recalculates from confidence for entries persisted before the
|
|
130
|
+
// weight field was added (backwards-compatible with older learning.json files).
|
|
131
|
+
const weight = entry.weight ?? Math.max(0.1, entry.confidence / 100);
|
|
104
132
|
this.index[word][entry.capabilityId] =
|
|
105
|
-
(this.index[word][entry.capabilityId] ??
|
|
133
|
+
(this.index[word][entry.capabilityId] ?? weight) - weight;
|
|
106
134
|
if (this.index[word][entry.capabilityId] <= 0) {
|
|
107
135
|
delete this.index[word][entry.capabilityId];
|
|
136
|
+
delete this.lastUpdatedIndex[word]?.[entry.capabilityId];
|
|
108
137
|
}
|
|
109
138
|
if (Object.keys(this.index[word]).length === 0) {
|
|
110
139
|
delete this.index[word];
|
|
140
|
+
delete this.lastUpdatedIndex[word];
|
|
111
141
|
}
|
|
112
142
|
}
|
|
113
143
|
}
|
|
@@ -120,10 +150,25 @@ class LearningIndex {
|
|
|
120
150
|
}
|
|
121
151
|
reset() {
|
|
122
152
|
this.index = {};
|
|
153
|
+
this.lastUpdatedIndex = {};
|
|
123
154
|
this.statsCounter = { totalQueries: 0, llmQueries: 0, cacheHits: 0, outOfScope: 0 };
|
|
124
155
|
}
|
|
125
156
|
getStats() {
|
|
126
|
-
|
|
157
|
+
// Apply time-decay lazily on read. The index stores accumulated weights;
|
|
158
|
+
// each (word, capId) cell is decayed by how long ago it was last reinforced.
|
|
159
|
+
// This means recently-used capabilities retain full signal while stale ones fade.
|
|
160
|
+
const decayed = {};
|
|
161
|
+
for (const [word, capMap] of Object.entries(this.index)) {
|
|
162
|
+
for (const [capId, weight] of Object.entries(capMap)) {
|
|
163
|
+
const lastUpdated = this.lastUpdatedIndex[word]?.[capId] ?? Date.now();
|
|
164
|
+
const dw = decayedWeight(weight, lastUpdated, this.halfLifeDays);
|
|
165
|
+
if (dw > 0.001) { // drop negligible signal — avoids ghost entries
|
|
166
|
+
decayed[word] ??= {};
|
|
167
|
+
decayed[word][capId] = dw;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { ...this.statsCounter, index: decayed };
|
|
127
172
|
}
|
|
128
173
|
getIndex() {
|
|
129
174
|
return structuredClone(this.index);
|
|
@@ -131,13 +176,13 @@ class LearningIndex {
|
|
|
131
176
|
}
|
|
132
177
|
// ─── File Learning Store ──────────────────────────────────────────────────────
|
|
133
178
|
export class FileLearningStore {
|
|
134
|
-
constructor(filePath = '.capman/learning.json') {
|
|
179
|
+
constructor(filePath = '.capman/learning.json', halfLifeDays = 30) {
|
|
135
180
|
this.entries = [];
|
|
136
181
|
this.loadPromise = null;
|
|
137
182
|
this.saveQueue = Promise.resolve();
|
|
138
|
-
this.learningIndex = new LearningIndex();
|
|
139
183
|
this.dirty = false;
|
|
140
184
|
this.saveTimer = null;
|
|
185
|
+
this.learningIndex = new LearningIndex(halfLifeDays);
|
|
141
186
|
const cwd = process.cwd();
|
|
142
187
|
const resolved = path.resolve(cwd, filePath);
|
|
143
188
|
const allowedPrefix = cwd === '/' ? '/' : cwd + path.sep;
|
|
@@ -168,8 +213,10 @@ export class FileLearningStore {
|
|
|
168
213
|
fs.writeFileSync(tmp, payload);
|
|
169
214
|
fs.renameSync(tmp, this.filePath);
|
|
170
215
|
}
|
|
171
|
-
catch {
|
|
172
|
-
//
|
|
216
|
+
catch (err) {
|
|
217
|
+
// Use process.stderr.write — never console.error in an exit handler,
|
|
218
|
+
// as stdout may already be flushed or closed at this point.
|
|
219
|
+
process.stderr.write(`[capman] Failed to flush learning store to ${this.filePath}: ${err}\n`);
|
|
173
220
|
}
|
|
174
221
|
}
|
|
175
222
|
/**
|
|
@@ -199,10 +246,44 @@ export class FileLearningStore {
|
|
|
199
246
|
}
|
|
200
247
|
async _doLoad() {
|
|
201
248
|
try {
|
|
249
|
+
// Fetch mtime once — used as lastUpdated fallback for pre-v0.7.0 entries.
|
|
250
|
+
// Conservative: treats all old entries as "last updated when file was written"
|
|
251
|
+
// rather than "infinitely old", preventing a cliff-edge decay on first upgrade.
|
|
252
|
+
let fileMtimeMs = Date.now();
|
|
253
|
+
try {
|
|
254
|
+
const stat = await fs.promises.stat(this.filePath);
|
|
255
|
+
fileMtimeMs = stat.mtimeMs;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// File doesn't exist yet or stat failed — Date.now() fallback is safe
|
|
259
|
+
}
|
|
202
260
|
const raw = await fs.promises.readFile(this.filePath, 'utf-8');
|
|
203
261
|
const parsed = JSON.parse(raw);
|
|
204
262
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray(parsed.entries)) {
|
|
205
|
-
|
|
263
|
+
// Validate each entry — corrupted entries (null capability, wrong types) must
|
|
264
|
+
// not propagate into the engine where they cause runtime errors deep in matching.
|
|
265
|
+
const validEntries = [];
|
|
266
|
+
let skipped = 0;
|
|
267
|
+
for (const entry of parsed.entries) {
|
|
268
|
+
if (entry !== null && typeof entry === 'object' &&
|
|
269
|
+
typeof entry.query === 'string' &&
|
|
270
|
+
(entry.capabilityId === null || typeof entry.capabilityId === 'string') &&
|
|
271
|
+
typeof entry.confidence === 'number' &&
|
|
272
|
+
typeof entry.resolvedVia === 'string') {
|
|
273
|
+
// Migration guard: backfill lastUpdated for pre-v0.7.0 entries
|
|
274
|
+
validEntries.push({
|
|
275
|
+
...entry,
|
|
276
|
+
lastUpdated: entry.lastUpdated ?? fileMtimeMs,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
skipped++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (skipped > 0) {
|
|
284
|
+
logger.warn(`Learning store: skipped ${skipped} invalid entries during load`);
|
|
285
|
+
}
|
|
286
|
+
this.entries = validEntries;
|
|
206
287
|
this.learningIndex.rebuild(this.entries);
|
|
207
288
|
logger.debug(`Learning store loaded: ${this.entries.length} entries`);
|
|
208
289
|
}
|
|
@@ -299,9 +380,9 @@ export class FileLearningStore {
|
|
|
299
380
|
}
|
|
300
381
|
// ─── Memory Learning Store (for testing) ─────────────────────────────────────
|
|
301
382
|
export class MemoryLearningStore {
|
|
302
|
-
constructor() {
|
|
383
|
+
constructor(halfLifeDays = 30) {
|
|
303
384
|
this.entries = [];
|
|
304
|
-
this.learningIndex = new LearningIndex();
|
|
385
|
+
this.learningIndex = new LearningIndex(halfLifeDays);
|
|
305
386
|
}
|
|
306
387
|
async record(entry) {
|
|
307
388
|
const sanitized = {
|
|
@@ -313,8 +394,8 @@ export class MemoryLearningStore {
|
|
|
313
394
|
if (this.entries.length > MAX_LEARNING_ENTRIES) {
|
|
314
395
|
const excess = this.entries.length - MAX_LEARNING_ENTRIES;
|
|
315
396
|
const pruned = this.entries.splice(0, excess);
|
|
316
|
-
for (const
|
|
317
|
-
this.learningIndex.subtract(
|
|
397
|
+
for (const staleEntry of pruned) {
|
|
398
|
+
this.learningIndex.subtract(staleEntry);
|
|
318
399
|
}
|
|
319
400
|
}
|
|
320
401
|
}
|
package/dist/esm/matcher.d.ts
CHANGED
|
@@ -34,6 +34,17 @@ export interface BM25Index {
|
|
|
34
34
|
N: number;
|
|
35
35
|
/** Bigram sets per capability — post-stopword, post-stem, examples only */
|
|
36
36
|
bigrams: Record<string, Set<string>>;
|
|
37
|
+
/**
|
|
38
|
+
* Pre-computed token arrays per capability, per field.
|
|
39
|
+
* Avoids re-tokenizing capability text on every scoreCapability() call.
|
|
40
|
+
* At 50 capabilities × 100 req/s, that is 5,000 redundant tokenization
|
|
41
|
+
* calls per second — each involving stem() and split/filter chains.
|
|
42
|
+
*/
|
|
43
|
+
capTokens: Record<string, {
|
|
44
|
+
examples: string[];
|
|
45
|
+
description: string[];
|
|
46
|
+
name: string[];
|
|
47
|
+
}>;
|
|
37
48
|
}
|
|
38
49
|
/** Build a BM25 index over all capabilities. Call once at manifest load. */
|
|
39
50
|
export declare function buildBM25Index(capabilities: Capability[]): BM25Index;
|
|
@@ -48,11 +59,39 @@ export declare function scoreCapability(qWordSet: Set<string>, cap: Capability,
|
|
|
48
59
|
* Input must already be post-stopword and post-stem (use tokenize() first).
|
|
49
60
|
*/
|
|
50
61
|
export declare function extractBigrams(tokens: string[]): Set<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Reciprocal Rank Fusion — fuses multiple ranked lists into a single score map.
|
|
64
|
+
* k=60 is the standard literature default.
|
|
65
|
+
*/
|
|
66
|
+
export declare function rrf(rankings: Array<Array<{
|
|
67
|
+
id: string;
|
|
68
|
+
score: number;
|
|
69
|
+
}>>, k?: number): Map<string, number>;
|
|
70
|
+
/**
|
|
71
|
+
* Returns a sub-manifest containing only capabilities that match ALL provided tags.
|
|
72
|
+
* Capabilities without tags are excluded when tags filter is active.
|
|
73
|
+
* Enables token-efficient LLM prompts for large manifests:
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Only send order-related capabilities to LLM
|
|
77
|
+
* const orderManifest = filterByTags(manifest, ['orders'])
|
|
78
|
+
* const result = await matchWithLLM(query, orderManifest, { llm })
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // Match by any of multiple tags (union) — call filterByTags per tag and merge
|
|
82
|
+
* const ordersOrPayments = [
|
|
83
|
+
* ...filterByTags(manifest, ['orders']).capabilities,
|
|
84
|
+
* ...filterByTags(manifest, ['payments']).capabilities,
|
|
85
|
+
* ]
|
|
86
|
+
*/
|
|
87
|
+
export declare function filterByTags(manifest: Manifest, tags: string[]): Manifest;
|
|
51
88
|
export declare function resolverToIntent(cap: Capability): MatchResult['intent'];
|
|
52
89
|
/**
|
|
53
90
|
* Strips characters that could break LLM prompt structure from
|
|
54
91
|
* capability field values before injection into the system prompt.
|
|
55
|
-
* Removes control characters, newlines,
|
|
92
|
+
* Removes control characters, newlines, delimiter sequences, and braces
|
|
93
|
+
* anywhere in the string (not just at line starts) to resist prompt injection
|
|
94
|
+
* from third-party OpenAPI spec content ingested via parseOpenAPI().
|
|
56
95
|
*/
|
|
57
96
|
export declare function sanitizeForPrompt(value: string, maxLen: number): string;
|
|
58
97
|
/**
|
|
@@ -77,9 +116,19 @@ export interface MatchOptions {
|
|
|
77
116
|
bm25K1?: number;
|
|
78
117
|
bm25B?: number;
|
|
79
118
|
bm25Ceiling?: number;
|
|
119
|
+
/** Pre-computed cosine similarity scores keyed by capability ID (0–100). Engine passes these when an EmbeddingProvider is configured. */
|
|
120
|
+
embeddingScores?: Map<string, number>;
|
|
80
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Calibrates a BM25 normalization ceiling from the manifest.
|
|
124
|
+
* Scores each capability against all of its own examples and returns the maximum.
|
|
125
|
+
* Call once at manifest load time — O(capabilities × examples).
|
|
126
|
+
*/
|
|
127
|
+
export declare function calibrateCeiling(capabilities: Capability[], bm25Index: BM25Index, k1: number, b: number): number;
|
|
81
128
|
export declare function match(query: string, manifest: Manifest, options?: MatchOptions): MatchResult;
|
|
82
129
|
export interface LLMMatcherOptions {
|
|
130
|
+
/** App name for prompt context — passed from engine, optional for direct callers */
|
|
131
|
+
app?: string;
|
|
83
132
|
llm: (prompt: string) => Promise<string>;
|
|
84
133
|
}
|
|
85
134
|
/**
|
|
@@ -93,4 +142,4 @@ export interface LLMMatcherOptions {
|
|
|
93
142
|
* wrapper that maps the prompt to a proper system message, keeping user query
|
|
94
143
|
* data in the user turn only.
|
|
95
144
|
*/
|
|
96
|
-
export declare function matchWithLLM(query: string,
|
|
145
|
+
export declare function matchWithLLM(query: string, topCandidates: Capability[], options: LLMMatcherOptions): Promise<MatchResult>;
|