@adityanair98/api-oracle 0.5.0 → 0.6.0
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/README.md +78 -26
- package/dist/cli.js +24 -11
- package/dist/dashboard/routes/api.js +1 -1
- package/dist/dashboard/server.d.ts +7 -1
- package/dist/dashboard/server.js +48 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -5
- package/dist/knowledge/db.d.ts +10 -0
- package/dist/knowledge/db.js +38 -2
- package/dist/knowledge/loader.d.ts +56 -0
- package/dist/knowledge/loader.js +116 -0
- package/dist/knowledge/relevance.d.ts +66 -0
- package/dist/knowledge/relevance.js +267 -0
- package/dist/knowledge/scorer.d.ts +41 -15
- package/dist/knowledge/scorer.js +151 -156
- package/dist/knowledge/search.d.ts +37 -12
- package/dist/knowledge/search.js +74 -71
- package/dist/knowledge/synonyms.d.ts +9 -0
- package/dist/knowledge/synonyms.js +23 -2
- package/dist/server.d.ts +3 -1
- package/dist/server.js +8 -2
- package/dist/tools/check-freshness.js +1 -1
- package/dist/tools/find-api.d.ts +18 -1
- package/dist/tools/find-api.js +109 -63
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.js +4 -1
- package/dist/utils/logger.d.ts +8 -1
- package/dist/utils/logger.js +13 -1
- package/package.json +4 -3
- package/dist/dashboard/public/public/app.js +0 -1004
- package/dist/dashboard/public/public/index.html +0 -142
- package/dist/dashboard/public/public/styles.css +0 -1464
package/dist/knowledge/scorer.js
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Scoring/ranking engine — ranks API entries by relevance to a task description.
|
|
3
|
-
* Uses weighted multi-factor scoring across 6 dimensions.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Relevance gates the score. Query-independent factors (quality, developer
|
|
5
|
+
* experience, pricing, recency) can only ADJUST an entry that is already
|
|
6
|
+
* relevant — they can never make an unrelated API rank:
|
|
7
|
+
*
|
|
8
|
+
* relevance = min(coverage + keyword bonus, 1) × categoryFit ∈ [0, 1]
|
|
9
|
+
* prior = weighted mean of quality, DX, pricing, recency ∈ [0, 1]
|
|
10
|
+
* score = relevance × (1 − PRIOR_INFLUENCE + PRIOR_INFLUENCE × prior)
|
|
11
|
+
* + keywordRelevance × KEYWORD_RANK_WEIGHT × categoryFit
|
|
12
|
+
*
|
|
13
|
+
* With PRIOR_INFLUENCE = 0.25 the prior scales a score between 0.75× and 1.0×
|
|
14
|
+
* of its relevance, so a high-quality but irrelevant API (relevance≈0) scores
|
|
15
|
+
* ≈0. The keyword term (the user named the API or its subcategory) sits
|
|
16
|
+
* outside the clamp so a named API wins ties against entries that merely
|
|
17
|
+
* mention it ("firebase" → Firebase, not "the open-source Firebase
|
|
18
|
+
* alternative"). search.ts additionally drops candidates below a relevance
|
|
19
|
+
* threshold and those in a clearly incompatible category.
|
|
20
|
+
*
|
|
21
|
+
* Exports: SCORING_WEIGHTS, DEFAULT_WEIGHTS, ScoreWeights, PRIOR_INFLUENCE, KEYWORD_RANK_WEIGHT,
|
|
22
|
+
* scoreEntry, rankEntries, tokenize, scoreDevExperience, categoryFit,
|
|
23
|
+
* computeConfidence, confidenceLabel, ScoringContext
|
|
7
24
|
*/
|
|
25
|
+
import { buildRelevanceModel, entryRelevance } from "./relevance.js";
|
|
8
26
|
/** Default weights — must sum to exactly 1.0 */
|
|
9
27
|
export const SCORING_WEIGHTS = {
|
|
10
28
|
useCaseFit: 0.35,
|
|
@@ -16,7 +34,14 @@ export const SCORING_WEIGHTS = {
|
|
|
16
34
|
};
|
|
17
35
|
/** Alias for backwards compatibility */
|
|
18
36
|
export const DEFAULT_WEIGHTS = SCORING_WEIGHTS;
|
|
19
|
-
|
|
37
|
+
/** How much the query-independent prior can move a relevant entry's score */
|
|
38
|
+
export const PRIOR_INFLUENCE = 0.25;
|
|
39
|
+
/**
|
|
40
|
+
* Ranking weight of keywordRelevance outside the relevance clamp
|
|
41
|
+
* (= SCORING_WEIGHTS.keywordRelevance by default; see scoreEntry).
|
|
42
|
+
*/
|
|
43
|
+
export const KEYWORD_RANK_WEIGHT = SCORING_WEIGHTS.keywordRelevance;
|
|
44
|
+
// ─── Tokenizer (used by the TF-IDF index) ─────────────────────────────────────
|
|
20
45
|
// Stop words that add noise without semantic value in the scoring context
|
|
21
46
|
const STOP_WORDS = new Set([
|
|
22
47
|
"the", "and", "for", "are", "but", "not", "you", "all",
|
|
@@ -38,7 +63,7 @@ export function tokenize(text) {
|
|
|
38
63
|
.filter((t) => t.length >= 2 && !STOP_WORDS.has(t));
|
|
39
64
|
return new Set(tokens);
|
|
40
65
|
}
|
|
41
|
-
/**
|
|
66
|
+
/** Overlap score between two token sets, normalised by the larger set */
|
|
42
67
|
function tokenOverlap(a, b) {
|
|
43
68
|
if (a.size === 0 || b.size === 0)
|
|
44
69
|
return 0;
|
|
@@ -49,75 +74,53 @@ function tokenOverlap(a, b) {
|
|
|
49
74
|
}
|
|
50
75
|
return overlap / Math.max(a.size, b.size);
|
|
51
76
|
}
|
|
52
|
-
// ─── Category
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
media: ["
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
perfect: 1.0,
|
|
83
|
-
good: 0.7,
|
|
84
|
-
partial: 0.4,
|
|
77
|
+
// ─── Category fit ─────────────────────────────────────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* Categories that plausibly serve the same task. A query detected as "storage"
|
|
80
|
+
* can be answered by infrastructure (S3/R2) or media (imgix) entries, but not
|
|
81
|
+
* by payments.
|
|
82
|
+
*/
|
|
83
|
+
const CATEGORY_AFFINITY = {
|
|
84
|
+
email: ["notifications"],
|
|
85
|
+
payments: ["commerce"],
|
|
86
|
+
commerce: ["payments", "cms"],
|
|
87
|
+
ai: ["media"],
|
|
88
|
+
media: ["ai", "storage", "infrastructure"],
|
|
89
|
+
storage: ["infrastructure", "media", "database"],
|
|
90
|
+
infrastructure: ["storage", "devops", "media"],
|
|
91
|
+
devops: ["infrastructure", "database"],
|
|
92
|
+
database: ["storage", "auth", "infrastructure", "devops"],
|
|
93
|
+
auth: ["database", "security"],
|
|
94
|
+
search: ["database"],
|
|
95
|
+
messaging: ["communication", "notifications"],
|
|
96
|
+
communication: ["messaging", "notifications"],
|
|
97
|
+
notifications: ["messaging", "communication", "email"],
|
|
98
|
+
analytics: ["testing"],
|
|
99
|
+
testing: ["analytics"],
|
|
100
|
+
security: ["auth", "database", "infrastructure"],
|
|
101
|
+
workflow: ["devops", "integration", "infrastructure"],
|
|
102
|
+
integration: ["workflow", "forms"],
|
|
103
|
+
forms: ["integration"],
|
|
104
|
+
cms: ["commerce"],
|
|
105
|
+
scheduling: [],
|
|
106
|
+
maps: [],
|
|
85
107
|
};
|
|
108
|
+
/** Minimum detection confidence before category is used at all */
|
|
109
|
+
const CATEGORY_MIN_CONFIDENCE = 0.4;
|
|
86
110
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* - bestFor (25%) — concise purpose statement
|
|
90
|
-
* - Description (20%) — first 200 chars (signal-dense)
|
|
91
|
-
* - Category signals (15%) — broad category alignment
|
|
111
|
+
* Relevance multiplier from category alignment with the detected task category:
|
|
112
|
+
* same 1.0, related 0.9, unrelated 0.7. No detection → 1.0 (neutral).
|
|
92
113
|
*/
|
|
93
|
-
export function
|
|
94
|
-
if (
|
|
95
|
-
return 0;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const score = overlap * FIT_WEIGHTS[useCase.fit];
|
|
102
|
-
if (score > bestUseCaseScore)
|
|
103
|
-
bestUseCaseScore = score;
|
|
104
|
-
}
|
|
105
|
-
// bestFor
|
|
106
|
-
const bestForScore = tokenOverlap(queryTokens, tokenize(entry.bestFor));
|
|
107
|
-
// Description (first 200 chars)
|
|
108
|
-
const descScore = tokenOverlap(queryTokens, tokenize(entry.description.slice(0, 200)));
|
|
109
|
-
// Category match
|
|
110
|
-
const categoryScore = scoreCategoryMatch(queryTokens, entry.category);
|
|
111
|
-
return Math.min(bestUseCaseScore * 0.40 +
|
|
112
|
-
bestForScore * 0.25 +
|
|
113
|
-
descScore * 0.20 +
|
|
114
|
-
categoryScore * 0.15, 1.0);
|
|
114
|
+
export function categoryFit(entryCategory, detectedCategory, detectedConfidence) {
|
|
115
|
+
if (!detectedCategory || detectedConfidence < CATEGORY_MIN_CONFIDENCE)
|
|
116
|
+
return 1.0;
|
|
117
|
+
if (entryCategory === detectedCategory)
|
|
118
|
+
return 1.0;
|
|
119
|
+
if (CATEGORY_AFFINITY[detectedCategory]?.includes(entryCategory))
|
|
120
|
+
return 0.9;
|
|
121
|
+
return 0.7;
|
|
115
122
|
}
|
|
116
|
-
// ───
|
|
117
|
-
function scoreQuality(entry) {
|
|
118
|
-
return (entry.qualityScore - 1) / 9;
|
|
119
|
-
}
|
|
120
|
-
// ─── 3. Developer Experience (sub-scored) ─────────────────────────────────────
|
|
123
|
+
// ─── Developer Experience (sub-scored) ────────────────────────────────────────
|
|
121
124
|
/**
|
|
122
125
|
* DX sub-scoring with measurable, explicit factors:
|
|
123
126
|
* - TypeScript as primary SDK: 3 pts
|
|
@@ -132,33 +135,27 @@ function scoreQuality(entry) {
|
|
|
132
135
|
export function scoreDevExperience(entry, constraints) {
|
|
133
136
|
let score = 0;
|
|
134
137
|
let maxScore = 8;
|
|
135
|
-
// TypeScript primary SDK (+3)
|
|
136
138
|
if (entry.sdk.primaryLanguage.toLowerCase() === "typescript") {
|
|
137
139
|
score += 3;
|
|
138
140
|
}
|
|
139
141
|
else if (entry.sdk.otherLanguages.some((l) => l.toLowerCase() === "typescript")) {
|
|
140
142
|
score += 1.5;
|
|
141
143
|
}
|
|
142
|
-
// 2+ code examples (+2)
|
|
143
144
|
if (entry.codeExamples.length >= 2) {
|
|
144
145
|
score += 2;
|
|
145
146
|
}
|
|
146
147
|
else if (entry.codeExamples.length === 1) {
|
|
147
148
|
score += 1;
|
|
148
149
|
}
|
|
149
|
-
// envVarName defined — clear auth setup (+1)
|
|
150
150
|
if (entry.auth.envVarName && entry.auth.envVarName.length > 0) {
|
|
151
151
|
score += 1;
|
|
152
152
|
}
|
|
153
|
-
// Single npm install command (+1)
|
|
154
153
|
if (entry.sdk.installCommand.includes("npm install")) {
|
|
155
154
|
score += 1;
|
|
156
155
|
}
|
|
157
|
-
// Multiple language SDKs (3+) (+1)
|
|
158
156
|
if (entry.sdk.otherLanguages.length >= 3) {
|
|
159
157
|
score += 1;
|
|
160
158
|
}
|
|
161
|
-
// Required language match (only when constraint specified; +2)
|
|
162
159
|
if (constraints?.requiredLanguage) {
|
|
163
160
|
maxScore += 2;
|
|
164
161
|
const required = constraints.requiredLanguage.toLowerCase();
|
|
@@ -171,10 +168,12 @@ export function scoreDevExperience(entry, constraints) {
|
|
|
171
168
|
}
|
|
172
169
|
return score / maxScore;
|
|
173
170
|
}
|
|
174
|
-
// ───
|
|
171
|
+
// ─── Quality / Pricing / Recency (prior components) ───────────────────────────
|
|
172
|
+
function scoreQuality(entry) {
|
|
173
|
+
return (entry.qualityScore - 1) / 9;
|
|
174
|
+
}
|
|
175
175
|
function scorePricingFit(entry, constraints) {
|
|
176
176
|
if (constraints?.preferFree) {
|
|
177
|
-
// preferFree: generous free tier or open source scores highest
|
|
178
177
|
if (entry.pricing.model === "free" || entry.pricing.model === "open_source") {
|
|
179
178
|
return 1.0;
|
|
180
179
|
}
|
|
@@ -198,74 +197,47 @@ function scorePricingFit(entry, constraints) {
|
|
|
198
197
|
}
|
|
199
198
|
return 0.50; // Paid — neutral
|
|
200
199
|
}
|
|
201
|
-
// ─── 5. Keyword Relevance ─────────────────────────────────────────────────────
|
|
202
|
-
/**
|
|
203
|
-
* Direct keyword relevance: checks if query tokens appear in name/slug/subcategory.
|
|
204
|
-
* This rewards exact API name matches ("algolia" for search queries, "stripe" for payments).
|
|
205
|
-
*/
|
|
206
|
-
function scoreKeywordRelevance(entry, queryTokens) {
|
|
207
|
-
const nameTokens = tokenize(entry.name);
|
|
208
|
-
const slugTokens = tokenize(entry.slug.replace(/-/g, " "));
|
|
209
|
-
const subCatTokens = tokenize(entry.subcategory.replace(/-/g, " "));
|
|
210
|
-
// Boost when API name or slug appears in query (user named the tool directly)
|
|
211
|
-
const nameMatch = tokenOverlap(queryTokens, nameTokens);
|
|
212
|
-
const slugMatch = tokenOverlap(queryTokens, slugTokens);
|
|
213
|
-
const subCatMatch = tokenOverlap(queryTokens, subCatTokens);
|
|
214
|
-
// Name match is strongest signal (user asked for "stripe" or "algolia" by name)
|
|
215
|
-
return Math.min(nameMatch * 0.5 + slugMatch * 0.3 + subCatMatch * 0.2, 1.0);
|
|
216
|
-
}
|
|
217
|
-
// ─── 6. Recency Bonus ─────────────────────────────────────────────────────────
|
|
218
|
-
const NOW_MS = Date.now();
|
|
219
200
|
const MS_PER_DAY = 86_400_000;
|
|
220
201
|
/**
|
|
221
|
-
* Recency
|
|
222
|
-
*
|
|
202
|
+
* Recency: verified within 30 days → 1.0, linearly to 0 at 365 days.
|
|
203
|
+
* Computed at call time (not module load) so long-running servers stay correct.
|
|
223
204
|
*/
|
|
224
|
-
function scoreRecency(entry) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const daysOld = (NOW_MS - verifiedMs) / MS_PER_DAY;
|
|
228
|
-
if (daysOld <= 30)
|
|
229
|
-
return 1.0;
|
|
230
|
-
if (daysOld <= 365)
|
|
231
|
-
return 1.0 - (daysOld - 30) / 335;
|
|
232
|
-
return 0;
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
205
|
+
function scoreRecency(entry, nowMs = Date.now()) {
|
|
206
|
+
const verifiedMs = new Date(entry.lastVerified).getTime();
|
|
207
|
+
if (Number.isNaN(verifiedMs))
|
|
235
208
|
return 0;
|
|
236
|
-
|
|
209
|
+
const daysOld = (nowMs - verifiedMs) / MS_PER_DAY;
|
|
210
|
+
if (daysOld <= 30)
|
|
211
|
+
return 1.0;
|
|
212
|
+
if (daysOld <= 365)
|
|
213
|
+
return 1.0 - (daysOld - 30) / 335;
|
|
214
|
+
return 0;
|
|
237
215
|
}
|
|
238
|
-
// ───
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
216
|
+
// ─── Keyword Relevance ────────────────────────────────────────────────────────
|
|
217
|
+
/**
|
|
218
|
+
* Direct keyword relevance on the ORIGINAL query (not the synonym-expanded
|
|
219
|
+
* one): rewards the user naming the API ("algolia", "stripe") or its subcategory.
|
|
220
|
+
*/
|
|
221
|
+
function scoreKeywordRelevance(entry, queryTokens) {
|
|
222
|
+
const nameMatch = tokenOverlap(queryTokens, tokenize(entry.name));
|
|
223
|
+
const slugMatch = tokenOverlap(queryTokens, tokenize(entry.slug.replace(/-/g, " ")));
|
|
224
|
+
const subCatMatch = tokenOverlap(queryTokens, tokenize(entry.subcategory.replace(/-/g, " ")));
|
|
225
|
+
return Math.min(nameMatch * 0.5 + slugMatch * 0.3 + subCatMatch * 0.2, 1.0);
|
|
247
226
|
}
|
|
248
227
|
// ─── Confidence Score ─────────────────────────────────────────────────────────
|
|
249
228
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* High confidence (>0.8): strong category match + high useCaseFit score
|
|
253
|
-
* Medium confidence (0.5-0.8): partial signals
|
|
254
|
-
* Low confidence (<0.5): weak query, no clear category
|
|
229
|
+
* Confidence (0-1) for the top result: how relevant it is, how clear the
|
|
230
|
+
* category is, and how far it is ahead of the runner-up.
|
|
255
231
|
*/
|
|
256
232
|
export function computeConfidence(topResult, detectedCategory, detectedConfidence, allResults) {
|
|
257
|
-
const
|
|
233
|
+
const relevance = topResult.scoreBreakdown["relevance"] ?? 0;
|
|
258
234
|
const totalScore = topResult.score;
|
|
259
|
-
|
|
260
|
-
const fitSignal = Math.min(useCaseFit / 0.5, 1.0); // 0.5+ useCaseFit → full signal
|
|
261
|
-
// Factor 2: category detection confidence
|
|
235
|
+
const fitSignal = Math.min(relevance / 0.7, 1.0);
|
|
262
236
|
const catSignal = detectedCategory ? detectedConfidence : 0;
|
|
263
|
-
// Factor 3: score separation (top result vs runner-up)
|
|
264
237
|
const separation = allResults.length >= 2
|
|
265
|
-
? Math.min((totalScore - allResults[1].score) *
|
|
238
|
+
? Math.min(((totalScore - allResults[1].score) / Math.max(totalScore, 0.01)) * 4, 1.0)
|
|
266
239
|
: 0.5;
|
|
267
|
-
|
|
268
|
-
const raw = fitSignal * 0.40 + catSignal * 0.35 + separation * 0.25;
|
|
240
|
+
const raw = fitSignal * 0.5 + catSignal * 0.25 + separation * 0.25;
|
|
269
241
|
return Math.round(Math.min(raw, 1.0) * 100) / 100;
|
|
270
242
|
}
|
|
271
243
|
/** Human-readable confidence label */
|
|
@@ -277,38 +249,61 @@ export function confidenceLabel(confidence) {
|
|
|
277
249
|
return "low";
|
|
278
250
|
}
|
|
279
251
|
// ─── Main Scoring Function ────────────────────────────────────────────────────
|
|
252
|
+
function round2(n) {
|
|
253
|
+
return Math.round(n * 100) / 100;
|
|
254
|
+
}
|
|
280
255
|
export function scoreEntry(entry, query, constraints, weights = SCORING_WEIGHTS, context) {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
256
|
+
const model = context?.relevanceModel ?? buildRelevanceModel(query, [entry]);
|
|
257
|
+
// Relevance (query-dependent)
|
|
258
|
+
const coverage = entryRelevance(model, entry.slug);
|
|
259
|
+
const keyword = scoreKeywordRelevance(entry, tokenize(query));
|
|
260
|
+
const keywordBonus = weights.useCaseFit > 0 ? weights.keywordRelevance / weights.useCaseFit : 0;
|
|
261
|
+
const catFit = categoryFit(entry.category, context?.detectedCategory ?? null, context?.detectedConfidence ?? 0);
|
|
262
|
+
const relevance = Math.min(coverage + keyword * keywordBonus, 1.0) * catFit;
|
|
263
|
+
// Prior (query-independent) — can only adjust a relevant entry
|
|
264
|
+
const quality = scoreQuality(entry);
|
|
265
|
+
const dx = scoreDevExperience(entry, constraints);
|
|
266
|
+
const pricing = scorePricingFit(entry, constraints);
|
|
267
|
+
const recency = scoreRecency(entry);
|
|
268
|
+
const priorWeight = weights.qualityScore + weights.developerExperience + weights.pricingFit + weights.recencyBonus;
|
|
269
|
+
const prior = priorWeight > 0
|
|
270
|
+
? (quality * weights.qualityScore +
|
|
271
|
+
dx * weights.developerExperience +
|
|
272
|
+
pricing * weights.pricingFit +
|
|
273
|
+
recency * weights.recencyBonus) /
|
|
274
|
+
priorWeight
|
|
275
|
+
: 0;
|
|
276
|
+
const totalScore = relevance * (1 - PRIOR_INFLUENCE + PRIOR_INFLUENCE * prior) +
|
|
277
|
+
keyword * weights.keywordRelevance * catFit;
|
|
296
278
|
const scoreBreakdown = {
|
|
297
|
-
useCaseFit:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
279
|
+
useCaseFit: round2(coverage),
|
|
280
|
+
keywordRelevance: round2(keyword),
|
|
281
|
+
categoryFit: round2(catFit),
|
|
282
|
+
relevance: round2(relevance),
|
|
283
|
+
qualityScore: round2(quality),
|
|
284
|
+
developerExperience: round2(dx),
|
|
285
|
+
pricingFit: round2(pricing),
|
|
286
|
+
recencyBonus: round2(recency),
|
|
287
|
+
prior: round2(prior),
|
|
288
|
+
total: round2(totalScore),
|
|
304
289
|
};
|
|
305
|
-
return { entry, score: totalScore, scoreBreakdown };
|
|
290
|
+
return { entry, score: totalScore, relevance, scoreBreakdown };
|
|
306
291
|
}
|
|
307
292
|
// ─── Ranking Function ─────────────────────────────────────────────────────────
|
|
293
|
+
/**
|
|
294
|
+
* Score and sort entries (no filtering — see findApis for relevance gating).
|
|
295
|
+
* The relevance model is built over `entries`, so IDF reflects the corpus.
|
|
296
|
+
*/
|
|
308
297
|
export function rankEntries(entries, query, constraints, topN = 3, weights = SCORING_WEIGHTS, context) {
|
|
309
298
|
if (entries.length === 0)
|
|
310
299
|
return [];
|
|
311
|
-
const
|
|
300
|
+
const fullContext = {
|
|
301
|
+
detectedCategory: context?.detectedCategory ?? null,
|
|
302
|
+
detectedConfidence: context?.detectedConfidence ?? 0,
|
|
303
|
+
expandedQuery: context?.expandedQuery,
|
|
304
|
+
relevanceModel: context?.relevanceModel ?? buildRelevanceModel(query, entries),
|
|
305
|
+
};
|
|
306
|
+
const scored = entries.map((entry) => scoreEntry(entry, query, constraints, weights, fullContext));
|
|
312
307
|
scored.sort((a, b) => b.score - a.score);
|
|
313
308
|
return scored.slice(0, topN);
|
|
314
309
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Search layer —
|
|
3
|
-
*
|
|
2
|
+
* Search layer — scores the knowledge base against a task and keeps only
|
|
3
|
+
* candidates that are actually relevant to it.
|
|
4
4
|
*
|
|
5
|
-
* Exports: SearchConstraints, RankedResult,
|
|
5
|
+
* Exports: SearchConstraints, RankedResult, SearchOutcome, RELEVANCE_GATE,
|
|
6
|
+
* findApis, findApisDetailed, getApiBySlug, getApisByCategory
|
|
6
7
|
*/
|
|
7
8
|
import type { ApiEntry } from "./schema.js";
|
|
8
9
|
export interface SearchConstraints {
|
|
@@ -13,23 +14,47 @@ export interface SearchConstraints {
|
|
|
13
14
|
export interface RankedResult {
|
|
14
15
|
entry: ApiEntry;
|
|
15
16
|
score: number;
|
|
17
|
+
/** Query relevance in [0,1] before the quality/DX/pricing/recency prior */
|
|
18
|
+
relevance?: number;
|
|
16
19
|
scoreBreakdown: Record<string, number>;
|
|
17
20
|
/** Confidence in this recommendation (0-1); only set on the top result */
|
|
18
21
|
confidence?: number;
|
|
19
22
|
/** Human-readable confidence label: "high" | "medium" | "low" */
|
|
20
23
|
confidenceLabel?: "high" | "medium" | "low";
|
|
21
24
|
}
|
|
25
|
+
export interface SearchOutcome {
|
|
26
|
+
/** Top `topN` relevant results, best first */
|
|
27
|
+
results: RankedResult[];
|
|
28
|
+
/** All candidates that passed the relevance gate, best first */
|
|
29
|
+
relevant: RankedResult[];
|
|
30
|
+
/** How many entries were scored */
|
|
31
|
+
evaluated: number;
|
|
32
|
+
detectedCategory: string | null;
|
|
33
|
+
}
|
|
22
34
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
* Relevance gate. A candidate is kept only if:
|
|
36
|
+
* - relevance ≥ minAbsolute, and
|
|
37
|
+
* - relevance ≥ minRelativeToBest × the best candidate's relevance, and
|
|
38
|
+
* - it is not in a clearly unrelated category (categoryFit at its floor while
|
|
39
|
+
* the category was detected with ≥ strongCategoryConfidence) unless its own
|
|
40
|
+
* relevance is ≥ categoryOverride.
|
|
41
|
+
*/
|
|
42
|
+
export declare const RELEVANCE_GATE: {
|
|
43
|
+
readonly minAbsolute: 0.2;
|
|
44
|
+
readonly minRelativeToBest: 0.6;
|
|
45
|
+
readonly strongCategoryConfidence: 0.5;
|
|
46
|
+
readonly categoryOverride: 0.6;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Find and rank APIs for a task, with stats. Pipeline:
|
|
50
|
+
* 1. Phrase detection (e.g. "free" → preferFree) merged with constraints
|
|
51
|
+
* 2. Category detection
|
|
52
|
+
* 3. Candidate pool: every entry (small KB) or TF-IDF + detected category
|
|
53
|
+
* 4. Score: relevance × bounded quality/DX/pricing/recency prior
|
|
54
|
+
* 5. Gate: drop candidates that are not relevant enough (RELEVANCE_GATE)
|
|
32
55
|
*/
|
|
56
|
+
export declare function findApisDetailed(query: string, constraints?: SearchConstraints, topN?: number): SearchOutcome;
|
|
57
|
+
/** Find and rank APIs for a task — relevant results only, best first. */
|
|
33
58
|
export declare function findApis(query: string, constraints?: SearchConstraints, topN?: number): RankedResult[];
|
|
34
59
|
/** Get a single API entry by its slug */
|
|
35
60
|
export declare function getApiBySlug(slug: string): ApiEntry | null;
|