capman 0.6.0 → 0.6.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.
- 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/engine.d.ts +15 -0
- package/dist/cjs/engine.d.ts.map +1 -1
- package/dist/cjs/engine.js +111 -21
- 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 +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/learning.d.ts +7 -0
- package/dist/cjs/learning.d.ts.map +1 -1
- package/dist/cjs/learning.js +35 -8
- package/dist/cjs/learning.js.map +1 -1
- package/dist/cjs/matcher.d.ts +38 -1
- package/dist/cjs/matcher.d.ts.map +1 -1
- package/dist/cjs/matcher.js +106 -23
- 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 +61 -12
- package/dist/cjs/schema.js.map +1 -1
- package/dist/cjs/types.d.ts +147 -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/engine.d.ts +15 -0
- package/dist/esm/engine.js +112 -22
- package/dist/esm/generator.js +28 -6
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/learning.d.ts +7 -0
- package/dist/esm/learning.js +35 -8
- package/dist/esm/matcher.d.ts +38 -1
- package/dist/esm/matcher.js +104 -23
- 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 +61 -12
- package/dist/esm/types.d.ts +147 -9
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/package.json +1 -1
package/dist/esm/engine.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent, extractParams, LLMParseError, tokenize, buildBM25Index,
|
|
1
|
+
import { match as _match, matchWithLLM as _matchWithLLM, resolverToIntent, extractParams, LLMParseError, tokenize, buildBM25Index, sanitizeForPrompt, calibrateCeiling as _calibrateCeiling } from './matcher';
|
|
2
2
|
import { resolve as _resolve, checkPrivacy } from './resolver';
|
|
3
3
|
import { MemoryLearningStore } from './learning';
|
|
4
4
|
import { logger } from './logger';
|
|
@@ -17,6 +17,7 @@ export class CapmanEngine {
|
|
|
17
17
|
this.mode = options.mode ?? 'balanced';
|
|
18
18
|
this.llm = options.llm;
|
|
19
19
|
this.baseUrl = options.baseUrl;
|
|
20
|
+
this.environment = options.environment;
|
|
20
21
|
this.auth = options.auth;
|
|
21
22
|
this.headers = options.headers;
|
|
22
23
|
this.threshold = options.threshold ?? 50;
|
|
@@ -124,6 +125,7 @@ export class CapmanEngine {
|
|
|
124
125
|
// ── Step 2.5: Apply learning boost ───────────────────────────────────────
|
|
125
126
|
matchResult = await this.applyBoostToMatchResult(query, matchResult, resolvedVia);
|
|
126
127
|
// ── Step 3: Privacy check ────────────────────────────────────────────────
|
|
128
|
+
let privacyFailed = false;
|
|
127
129
|
if (matchResult.capability) {
|
|
128
130
|
const privacyError = checkPrivacy(matchResult.capability, this.auth);
|
|
129
131
|
steps.push({
|
|
@@ -132,13 +134,23 @@ export class CapmanEngine {
|
|
|
132
134
|
durationMs: 0,
|
|
133
135
|
detail: privacyError ?? `level: ${matchResult.capability.privacy.level}`,
|
|
134
136
|
});
|
|
137
|
+
// Warn on deprecated or sunset capabilities — never silently fail
|
|
138
|
+
this.checkCapabilityLifecycle(matchResult.capability);
|
|
139
|
+
// Log when engine mode differs from capability's preferred mode
|
|
140
|
+
this.checkMatchHint(matchResult.capability);
|
|
141
|
+
// Short-circuit: if privacy fails, skip disambiguation to avoid burning an LLM
|
|
142
|
+
// call on a request that _resolve() will block anyway. privacyFailed propagates
|
|
143
|
+
// to Step 4a so the mode guard check is clean and explicit.
|
|
144
|
+
if (privacyError)
|
|
145
|
+
privacyFailed = true;
|
|
135
146
|
}
|
|
136
147
|
// ── Step 4a: Compute verdict + optional margin-aware LLM disambiguation ──
|
|
137
148
|
let { verdict, margin } = this.computeVerdict(matchResult);
|
|
138
149
|
if (verdict === 'marginal' &&
|
|
139
150
|
this.marginAwareLLM &&
|
|
140
151
|
this.llm &&
|
|
141
|
-
|
|
152
|
+
!privacyFailed &&
|
|
153
|
+
(this.mode === 'balanced' || this.mode === 'accurate')) {
|
|
142
154
|
matchResult = await this.disambiguateLLM(query, matchResult, steps);
|
|
143
155
|
// Recompute verdict after disambiguation
|
|
144
156
|
const recomputed = this.computeVerdict(matchResult);
|
|
@@ -205,8 +217,19 @@ export class CapmanEngine {
|
|
|
205
217
|
}
|
|
206
218
|
}
|
|
207
219
|
}
|
|
208
|
-
catch {
|
|
209
|
-
|
|
220
|
+
catch (err) {
|
|
221
|
+
const isParseError = err instanceof SyntaxError;
|
|
222
|
+
if (isParseError) {
|
|
223
|
+
// JSON parse failure: refund the rate-limit slot but don't open circuit breaker
|
|
224
|
+
// The llm is reachable - the response format was just bad
|
|
225
|
+
this.llmCallsThisMinute = Math.max(0, this.llmCallsThisMinute - 1);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// Hard failure (timeout, network): refund slot and increment fail counter
|
|
229
|
+
this.recordLLMFailure();
|
|
230
|
+
}
|
|
231
|
+
logger.warn(`LLM param extraction failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
232
|
+
// fall through to missingParams below
|
|
210
233
|
}
|
|
211
234
|
}
|
|
212
235
|
}
|
|
@@ -292,6 +315,20 @@ export class CapmanEngine {
|
|
|
292
315
|
await this.cache.clear();
|
|
293
316
|
}
|
|
294
317
|
checkManifestVersion(manifest) {
|
|
318
|
+
// ── Schema version check ─────────────────────────────────────────────────
|
|
319
|
+
// schemaVersion tracks manifest format — "1" for v0.6+.
|
|
320
|
+
// Manifests without schemaVersion are pre-v0.6 — warn but allow.
|
|
321
|
+
const CURRENT_SCHEMA_VERSION = '1';
|
|
322
|
+
if (!manifest.schemaVersion) {
|
|
323
|
+
console.warn(`[capman] Manifest is missing schemaVersion — it was generated with capman < 0.6. ` +
|
|
324
|
+
`Regenerate with: npx capman generate`);
|
|
325
|
+
}
|
|
326
|
+
else if (manifest.schemaVersion !== CURRENT_SCHEMA_VERSION) {
|
|
327
|
+
console.warn(`[capman] Manifest schemaVersion "${manifest.schemaVersion}" differs from ` +
|
|
328
|
+
`engine's expected "${CURRENT_SCHEMA_VERSION}". ` +
|
|
329
|
+
`Regenerate with: npx capman generate`);
|
|
330
|
+
}
|
|
331
|
+
// ── Package version check ────────────────────────────────────────────────
|
|
295
332
|
if (!manifest.version)
|
|
296
333
|
return;
|
|
297
334
|
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
@@ -299,8 +336,8 @@ export class CapmanEngine {
|
|
|
299
336
|
const [mMaj, mMin] = manifest.version.split('.').map(Number);
|
|
300
337
|
const [eMaj, eMin] = VERSION.split('.').map(Number);
|
|
301
338
|
if (mMaj !== eMaj || mMin !== eMin) {
|
|
302
|
-
console.warn(`[capman] Manifest
|
|
303
|
-
`
|
|
339
|
+
console.warn(`[capman] Manifest was generated with capman "${manifest.version}" ` +
|
|
340
|
+
`but engine is "${VERSION}". This is usually fine across patch versions. ` +
|
|
304
341
|
`If you experience unexpected matching issues, regenerate with: npx capman generate`);
|
|
305
342
|
}
|
|
306
343
|
}
|
|
@@ -309,6 +346,42 @@ export class CapmanEngine {
|
|
|
309
346
|
`to engine version "${VERSION}" — version strings are not valid semver.`);
|
|
310
347
|
}
|
|
311
348
|
}
|
|
349
|
+
checkCapabilityLifecycle(capability) {
|
|
350
|
+
const lc = capability.lifecycle;
|
|
351
|
+
if (!lc || lc.status === 'stable' || lc.status === 'beta' || lc.status === 'experimental') {
|
|
352
|
+
if (lc?.status === 'beta') {
|
|
353
|
+
logger.warn(`Capability "${capability.id}" is in beta — behavior may change`);
|
|
354
|
+
}
|
|
355
|
+
if (lc?.status === 'experimental') {
|
|
356
|
+
logger.warn(`Capability "${capability.id}" is experimental — use with caution`);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (lc.status === 'deprecated') {
|
|
361
|
+
const sunsetPassed = lc.sunsetAt && new Date(lc.sunsetAt) < new Date();
|
|
362
|
+
if (sunsetPassed) {
|
|
363
|
+
// Sunset date has passed — strongest warning
|
|
364
|
+
console.warn(`[capman] ⚠️ Capability "${capability.id}" passed its sunset date (${lc.sunsetAt}). ` +
|
|
365
|
+
`It may be removed in a future version.` +
|
|
366
|
+
(lc.successor ? ` Use "${lc.successor}" instead.` : '') +
|
|
367
|
+
(lc.note ? ` Note: ${lc.note}` : ''));
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
logger.warn(`Capability "${capability.id}" is deprecated.` +
|
|
371
|
+
(lc.sunsetAt ? ` Sunset: ${lc.sunsetAt}.` : '') +
|
|
372
|
+
(lc.successor ? ` Use "${lc.successor}" instead.` : '') +
|
|
373
|
+
(lc.note ? ` Note: ${lc.note}` : ''));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
checkMatchHint(capability) {
|
|
378
|
+
const hint = capability.matchHint?.preferredMode;
|
|
379
|
+
if (!hint || hint === this.mode)
|
|
380
|
+
return;
|
|
381
|
+
// Advisory only — log but never enforce
|
|
382
|
+
logger.warn(`Capability "${capability.id}" prefers mode "${hint}" but engine is in "${this.mode}" mode. ` +
|
|
383
|
+
`Set mode: '${hint}' in EngineOptions to honor this hint.`);
|
|
384
|
+
}
|
|
312
385
|
/**
|
|
313
386
|
* Replaces the active manifest without creating a new engine instance.
|
|
314
387
|
* Useful for hot-reloading manifests in long-running servers without
|
|
@@ -327,6 +400,8 @@ export class CapmanEngine {
|
|
|
327
400
|
this.bm25Index = buildBM25Index(manifest.capabilities);
|
|
328
401
|
this.bm25Ceiling = this.calibrateBM25Ceiling();
|
|
329
402
|
this.adaptiveMargin = this.calibrateAdaptiveMargin();
|
|
403
|
+
// resolveBaseUrl() reads from this.manifest.servers on each call —
|
|
404
|
+
// server selection updates automatically after loadManifest()
|
|
330
405
|
await this.clearCache();
|
|
331
406
|
}
|
|
332
407
|
/**
|
|
@@ -684,7 +759,11 @@ export class CapmanEngine {
|
|
|
684
759
|
break;
|
|
685
760
|
}
|
|
686
761
|
}
|
|
687
|
-
|
|
762
|
+
if (matchResult === undefined) {
|
|
763
|
+
const exhaustive = this.mode;
|
|
764
|
+
throw new Error(`_runMatch: unhandled MatchMode "${exhaustive}"`);
|
|
765
|
+
}
|
|
766
|
+
return { matchResult, resolvedVia };
|
|
688
767
|
}
|
|
689
768
|
/**
|
|
690
769
|
* Applies learning boost to a MatchResult and returns the updated result.
|
|
@@ -769,10 +848,26 @@ export class CapmanEngine {
|
|
|
769
848
|
};
|
|
770
849
|
});
|
|
771
850
|
}
|
|
851
|
+
/**
|
|
852
|
+
* Resolves the effective baseUrl from manifest.servers[] or EngineOptions.baseUrl.
|
|
853
|
+
* Priority: environment-matched server > first server > explicit baseUrl > undefined
|
|
854
|
+
*/
|
|
855
|
+
resolveBaseUrl() {
|
|
856
|
+
const servers = this.manifest.servers;
|
|
857
|
+
if (!servers?.length)
|
|
858
|
+
return this.baseUrl;
|
|
859
|
+
if (this.environment) {
|
|
860
|
+
const match = servers.find(s => s.environment === this.environment);
|
|
861
|
+
if (match)
|
|
862
|
+
return match.url.replace(/\/$/, '');
|
|
863
|
+
}
|
|
864
|
+
// Fallback to first server
|
|
865
|
+
return servers[0].url.replace(/\/$/, '');
|
|
866
|
+
}
|
|
772
867
|
// ── Private helpers ────────────────────────────────────────────────────────
|
|
773
868
|
resolveOptions(overrides = {}) {
|
|
774
869
|
return {
|
|
775
|
-
baseUrl: this.
|
|
870
|
+
baseUrl: this.resolveBaseUrl(),
|
|
776
871
|
auth: this.auth,
|
|
777
872
|
headers: this.headers,
|
|
778
873
|
...overrides,
|
|
@@ -792,16 +887,7 @@ export class CapmanEngine {
|
|
|
792
887
|
});
|
|
793
888
|
}
|
|
794
889
|
calibrateBM25Ceiling() {
|
|
795
|
-
|
|
796
|
-
for (const cap of this.manifest.capabilities) {
|
|
797
|
-
if (!cap.examples?.length)
|
|
798
|
-
continue;
|
|
799
|
-
const selfWords = new Set(tokenize(cap.examples[0]));
|
|
800
|
-
const raw = _scoreCapability(selfWords, cap, this.bm25Index, this.bm25K1, this.bm25B);
|
|
801
|
-
if (raw > max)
|
|
802
|
-
max = raw;
|
|
803
|
-
}
|
|
804
|
-
return max > 0 ? max : 100;
|
|
890
|
+
return _calibrateCeiling(this.manifest.capabilities, this.bm25Index, this.bm25K1, this.bm25B);
|
|
805
891
|
}
|
|
806
892
|
/**
|
|
807
893
|
* Calibrates the adaptive margin threshold from the manifest's own score
|
|
@@ -829,10 +915,14 @@ export class CapmanEngine {
|
|
|
829
915
|
for (const cap of this.manifest.capabilities) {
|
|
830
916
|
if (!cap.examples?.length)
|
|
831
917
|
continue;
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
918
|
+
// Use all examples and take the maximum margin — same rationale as
|
|
919
|
+
// calibrateBM25Ceiling(): a weak first example skews the calibration.
|
|
920
|
+
for (const example of cap.examples) {
|
|
921
|
+
const result = _match(example, this.manifest, fuzzyOpts);
|
|
922
|
+
const sorted = [...result.candidates].sort((a, b) => b.score - a.score);
|
|
923
|
+
if (sorted.length >= 2) {
|
|
924
|
+
margins.push(sorted[0].score - sorted[1].score);
|
|
925
|
+
}
|
|
836
926
|
}
|
|
837
927
|
}
|
|
838
928
|
if (margins.length === 0)
|
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,11 +1,12 @@
|
|
|
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, } 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';
|
package/dist/esm/index.js
CHANGED
|
@@ -3,6 +3,7 @@ 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';
|
package/dist/esm/learning.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ 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;
|
|
9
16
|
}
|
|
10
17
|
export interface KeywordStats {
|
|
11
18
|
/** keyword → Map of capabilityId → hit count */
|
package/dist/esm/learning.js
CHANGED
|
@@ -75,6 +75,10 @@ class LearningIndex {
|
|
|
75
75
|
// more signal than a 51% borderline match. Floor of 0.1 ensures
|
|
76
76
|
// borderline matches still contribute, just proportionally less.
|
|
77
77
|
const weight = Math.max(0.1, entry.confidence / 100);
|
|
78
|
+
// Store weight on the entry so subtract() can reverse the exact amount.
|
|
79
|
+
// Without this, subtract() would have to use a hardcoded estimate (0.5)
|
|
80
|
+
// that causes index drift after pruning high-confidence entries.
|
|
81
|
+
entry.weight = weight;
|
|
78
82
|
const words = tokenize(entry.query);
|
|
79
83
|
for (const word of words) {
|
|
80
84
|
this.index[word] ??= {};
|
|
@@ -99,10 +103,12 @@ class LearningIndex {
|
|
|
99
103
|
for (const word of words) {
|
|
100
104
|
if (!this.index[word])
|
|
101
105
|
continue;
|
|
102
|
-
//
|
|
103
|
-
//
|
|
106
|
+
// Use the weight stored at record time for exact symmetric subtraction.
|
|
107
|
+
// Fallback recalculates from confidence for entries persisted before the
|
|
108
|
+
// weight field was added (backwards-compatible with older learning.json files).
|
|
109
|
+
const weight = entry.weight ?? Math.max(0.1, entry.confidence / 100);
|
|
104
110
|
this.index[word][entry.capabilityId] =
|
|
105
|
-
(this.index[word][entry.capabilityId] ??
|
|
111
|
+
(this.index[word][entry.capabilityId] ?? weight) - weight;
|
|
106
112
|
if (this.index[word][entry.capabilityId] <= 0) {
|
|
107
113
|
delete this.index[word][entry.capabilityId];
|
|
108
114
|
}
|
|
@@ -168,8 +174,10 @@ export class FileLearningStore {
|
|
|
168
174
|
fs.writeFileSync(tmp, payload);
|
|
169
175
|
fs.renameSync(tmp, this.filePath);
|
|
170
176
|
}
|
|
171
|
-
catch {
|
|
172
|
-
//
|
|
177
|
+
catch (err) {
|
|
178
|
+
// Use process.stderr.write — never console.error in an exit handler,
|
|
179
|
+
// as stdout may already be flushed or closed at this point.
|
|
180
|
+
process.stderr.write(`[capman] Failed to flush learning store to ${this.filePath}: ${err}\n`);
|
|
173
181
|
}
|
|
174
182
|
}
|
|
175
183
|
/**
|
|
@@ -202,7 +210,26 @@ export class FileLearningStore {
|
|
|
202
210
|
const raw = await fs.promises.readFile(this.filePath, 'utf-8');
|
|
203
211
|
const parsed = JSON.parse(raw);
|
|
204
212
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray(parsed.entries)) {
|
|
205
|
-
|
|
213
|
+
// Validate each entry — corrupted entries (null capability, wrong types) must
|
|
214
|
+
// not propagate into the engine where they cause runtime errors deep in matching.
|
|
215
|
+
const validEntries = [];
|
|
216
|
+
let skipped = 0;
|
|
217
|
+
for (const entry of parsed.entries) {
|
|
218
|
+
if (entry !== null && typeof entry === 'object' &&
|
|
219
|
+
typeof entry.query === 'string' &&
|
|
220
|
+
(entry.capabilityId === null || typeof entry.capabilityId === 'string') &&
|
|
221
|
+
typeof entry.confidence === 'number' &&
|
|
222
|
+
typeof entry.resolvedVia === 'string') {
|
|
223
|
+
validEntries.push(entry);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
skipped++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (skipped > 0) {
|
|
230
|
+
logger.warn(`Learning store: skipped ${skipped} invalid entries during load`);
|
|
231
|
+
}
|
|
232
|
+
this.entries = validEntries;
|
|
206
233
|
this.learningIndex.rebuild(this.entries);
|
|
207
234
|
logger.debug(`Learning store loaded: ${this.entries.length} entries`);
|
|
208
235
|
}
|
|
@@ -313,8 +340,8 @@ export class MemoryLearningStore {
|
|
|
313
340
|
if (this.entries.length > MAX_LEARNING_ENTRIES) {
|
|
314
341
|
const excess = this.entries.length - MAX_LEARNING_ENTRIES;
|
|
315
342
|
const pruned = this.entries.splice(0, excess);
|
|
316
|
-
for (const
|
|
317
|
-
this.learningIndex.subtract(
|
|
343
|
+
for (const staleEntry of pruned) {
|
|
344
|
+
this.learningIndex.subtract(staleEntry);
|
|
318
345
|
}
|
|
319
346
|
}
|
|
320
347
|
}
|
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,31 @@ 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
|
+
* Returns a sub-manifest containing only capabilities that match ALL provided tags.
|
|
64
|
+
* Capabilities without tags are excluded when tags filter is active.
|
|
65
|
+
* Enables token-efficient LLM prompts for large manifests:
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Only send order-related capabilities to LLM
|
|
69
|
+
* const orderManifest = filterByTags(manifest, ['orders'])
|
|
70
|
+
* const result = await matchWithLLM(query, orderManifest, { llm })
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Match by any of multiple tags (union) — call filterByTags per tag and merge
|
|
74
|
+
* const ordersOrPayments = [
|
|
75
|
+
* ...filterByTags(manifest, ['orders']).capabilities,
|
|
76
|
+
* ...filterByTags(manifest, ['payments']).capabilities,
|
|
77
|
+
* ]
|
|
78
|
+
*/
|
|
79
|
+
export declare function filterByTags(manifest: Manifest, tags: string[]): Manifest;
|
|
51
80
|
export declare function resolverToIntent(cap: Capability): MatchResult['intent'];
|
|
52
81
|
/**
|
|
53
82
|
* Strips characters that could break LLM prompt structure from
|
|
54
83
|
* capability field values before injection into the system prompt.
|
|
55
|
-
* Removes control characters, newlines,
|
|
84
|
+
* Removes control characters, newlines, delimiter sequences, and braces
|
|
85
|
+
* anywhere in the string (not just at line starts) to resist prompt injection
|
|
86
|
+
* from third-party OpenAPI spec content ingested via parseOpenAPI().
|
|
56
87
|
*/
|
|
57
88
|
export declare function sanitizeForPrompt(value: string, maxLen: number): string;
|
|
58
89
|
/**
|
|
@@ -78,6 +109,12 @@ export interface MatchOptions {
|
|
|
78
109
|
bm25B?: number;
|
|
79
110
|
bm25Ceiling?: number;
|
|
80
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Calibrates a BM25 normalization ceiling from the manifest.
|
|
114
|
+
* Scores each capability against all of its own examples and returns the maximum.
|
|
115
|
+
* Call once at manifest load time — O(capabilities × examples).
|
|
116
|
+
*/
|
|
117
|
+
export declare function calibrateCeiling(capabilities: Capability[], bm25Index: BM25Index, k1: number, b: number): number;
|
|
81
118
|
export declare function match(query: string, manifest: Manifest, options?: MatchOptions): MatchResult;
|
|
82
119
|
export interface LLMMatcherOptions {
|
|
83
120
|
llm: (prompt: string) => Promise<string>;
|