skyloom 1.14.6 → 1.15.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/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +74 -0
- package/CONVERSION_PLAN.md +191 -191
- package/README.md +523 -220
- package/config/default.yaml +46 -43
- package/config/models.yaml +928 -155
- package/config/providers.yaml +109 -6
- package/dist/agents/snow.d.ts +2 -0
- package/dist/agents/snow.d.ts.map +1 -1
- package/dist/agents/snow.js +36 -5
- package/dist/agents/snow.js.map +1 -1
- package/dist/cli/loom_chat.d.ts.map +1 -1
- package/dist/cli/loom_chat.js +207 -1
- package/dist/cli/loom_chat.js.map +1 -1
- package/dist/cli/main.js +190 -40
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +6 -31
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent.d.ts +6 -4
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -20
- package/dist/core/agent.js.map +1 -1
- package/dist/core/catalog.d.ts.map +1 -1
- package/dist/core/catalog.js +30 -9
- package/dist/core/catalog.js.map +1 -1
- package/dist/core/commands.d.ts +110 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +633 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/concurrency.d.ts +38 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +65 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/factory.js +16 -16
- package/dist/core/file_checkpoint.d.ts +9 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -1
- package/dist/core/file_checkpoint.js +33 -1
- package/dist/core/file_checkpoint.js.map +1 -1
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +66 -13
- package/dist/core/llm.js.map +1 -1
- package/dist/core/memory.js +51 -51
- package/dist/core/schemas.d.ts +16 -0
- package/dist/core/schemas.d.ts.map +1 -1
- package/dist/core/schemas.js +32 -0
- package/dist/core/schemas.js.map +1 -1
- package/dist/core/security.d.ts.map +1 -1
- package/dist/core/security.js +27 -0
- package/dist/core/security.js.map +1 -1
- package/dist/core/skymd.js +14 -14
- package/dist/core/trace.d.ts +105 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +213 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/tools/builtin.d.ts +2 -6
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +180 -125
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/extra.d.ts +13 -0
- package/dist/tools/extra.d.ts.map +1 -0
- package/dist/tools/extra.js +827 -0
- package/dist/tools/extra.js.map +1 -0
- package/dist/tools/guards.d.ts +12 -0
- package/dist/tools/guards.d.ts.map +1 -0
- package/dist/tools/guards.js +143 -0
- package/dist/tools/guards.js.map +1 -0
- package/dist/tools/model_tool.d.ts.map +1 -1
- package/dist/tools/model_tool.js +24 -4
- package/dist/tools/model_tool.js.map +1 -1
- package/dist/web/markdown.d.ts +32 -0
- package/dist/web/markdown.d.ts.map +1 -0
- package/dist/web/markdown.js +202 -0
- package/dist/web/markdown.js.map +1 -0
- package/dist/web/server.d.ts +4 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +14 -582
- package/dist/web/server.js.map +1 -1
- package/dist/web/ui.d.ts +31 -0
- package/dist/web/ui.d.ts.map +1 -0
- package/dist/web/ui.js +1009 -0
- package/dist/web/ui.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -152
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +68 -68
- package/src/agents/snow.ts +38 -5
- package/src/cli/commands_md.ts +112 -112
- package/src/cli/input_macros.ts +83 -83
- package/src/cli/loom.ts +1041 -1041
- package/src/cli/loom_chat.ts +772 -603
- package/src/cli/main.ts +853 -723
- package/src/cli/tui.ts +264 -289
- package/src/core/agent/guard.ts +133 -133
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +1630 -1590
- package/src/core/agent_helpers.ts +500 -500
- package/src/core/bus.ts +221 -221
- package/src/core/cache.ts +153 -153
- package/src/core/catalog.ts +199 -178
- package/src/core/circuit_breaker.ts +119 -119
- package/src/core/commands.ts +704 -0
- package/src/core/concurrency.ts +73 -0
- package/src/core/config.ts +365 -365
- package/src/core/constants.ts +95 -95
- package/src/core/factory.ts +656 -656
- package/src/core/file_checkpoint.ts +163 -136
- package/src/core/hooks.ts +126 -126
- package/src/core/llm.ts +972 -915
- package/src/core/logger.ts +143 -143
- package/src/core/mcp.ts +1001 -1001
- package/src/core/memory.ts +1201 -1201
- package/src/core/middleware.ts +350 -350
- package/src/core/model_config.ts +159 -159
- package/src/core/pipelines.ts +424 -424
- package/src/core/schemas.ts +319 -282
- package/src/core/security.ts +27 -0
- package/src/core/semantic.ts +211 -211
- package/src/core/skill.ts +384 -384
- package/src/core/skymd.ts +143 -143
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +457 -457
- package/src/core/trace.ts +236 -0
- package/src/core/verify.ts +71 -71
- package/src/plugins/loader.ts +91 -91
- package/src/skills/loader.ts +75 -75
- package/src/tools/builtin.ts +571 -493
- package/src/tools/computer.ts +279 -279
- package/src/tools/extra.ts +662 -0
- package/src/tools/guards.ts +82 -0
- package/src/tools/model_tool.ts +93 -74
- package/src/tools/todo.ts +76 -76
- package/src/web/markdown.ts +193 -0
- package/src/web/server.ts +117 -693
- package/src/web/ui.ts +949 -0
- package/tests/agent.test.ts +211 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -124
- package/tests/claude_compat.test.ts +110 -110
- package/tests/commands.test.ts +103 -0
- package/tests/concurrency.test.ts +102 -0
- package/tests/config.test.ts +41 -41
- package/tests/extra_tools.test.ts +212 -0
- package/tests/fence_plugin.test.ts +52 -52
- package/tests/guard.test.ts +75 -75
- package/tests/loom.test.ts +337 -337
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -109
- package/tests/skymd.test.ts +146 -146
- package/tests/ssrf.test.ts +38 -38
- package/tests/structured_retry.test.ts +87 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -94
- package/tests/trace.test.ts +128 -0
- package/tests/tui.test.ts +67 -67
- package/tests/web.test.ts +169 -0
- package/tsconfig.json +38 -38
package/src/core/semantic.ts
CHANGED
|
@@ -1,211 +1,211 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lightweight semantic retrieval — zero external dependencies.
|
|
3
|
-
*
|
|
4
|
-
* Why: The existing `recall_for_injection` uses SQL LIKE on tokens, which
|
|
5
|
-
* misses semantically related facts that share few literal characters (e.g.
|
|
6
|
-
* query "deploy" vs. stored fact "release_command"). This module provides
|
|
7
|
-
* a character n-gram Jaccard similarity scorer that catches those cross-
|
|
8
|
-
* lingual and synonym relationships without adding PyTorch / sentence-transformers.
|
|
9
|
-
*
|
|
10
|
-
* Design:
|
|
11
|
-
* - Character n-grams (size 2-4) naturally handle CJK, mixed-language, and
|
|
12
|
-
* code identifiers better than word-level tokenization.
|
|
13
|
-
* - Jaccard similarity on n-gram sets is fast (< 10 µs per pair) and
|
|
14
|
-
* well-correlated with human relevance judgments for short text.
|
|
15
|
-
* - Zero dependencies beyond stdlib.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Represents a candidate item for ranking
|
|
20
|
-
*/
|
|
21
|
-
export interface Candidate {
|
|
22
|
-
key?: string;
|
|
23
|
-
value?: string | Record<string, unknown>;
|
|
24
|
-
[key: string]: unknown;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Character n-gram semantic similarity scorer.
|
|
29
|
-
*
|
|
30
|
-
* Usage:
|
|
31
|
-
* ```
|
|
32
|
-
* const scorer = new SemanticScorer();
|
|
33
|
-
* const score = scorer.similarity("deploy to prod", "release_command");
|
|
34
|
-
* // score ≈ 0.15 (low but non-zero — catches partial overlap)
|
|
35
|
-
*
|
|
36
|
-
* const ranked = scorer.rank(
|
|
37
|
-
* "search query",
|
|
38
|
-
* [{value: "candidate A"}, {value: "candidate B"}]
|
|
39
|
-
* );
|
|
40
|
-
* // ranked → [[0.85, {value: "candidate A"}], [0.30, {value: "candidate B"}]]
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
export class SemanticScorer {
|
|
44
|
-
private nRange: [number, number];
|
|
45
|
-
private cache: Map<string, Set<string>>;
|
|
46
|
-
private maxCacheSize = 512;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Initialize scorer with n-gram range.
|
|
50
|
-
* @param nRange - Tuple of [minSize, maxSize] for n-grams (default [2, 4])
|
|
51
|
-
*/
|
|
52
|
-
constructor(nRange: [number, number] = [2, 4]) {
|
|
53
|
-
this.nRange = nRange;
|
|
54
|
-
this.cache = new Map();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Generate character n-gram fingerprint (cached).
|
|
59
|
-
* @param text - Input text to fingerprint
|
|
60
|
-
* @returns Set of n-grams for the text
|
|
61
|
-
*/
|
|
62
|
-
private fingerprint(text: string): Set<string> {
|
|
63
|
-
// Return cached result if available
|
|
64
|
-
if (this.cache.has(text)) {
|
|
65
|
-
return this.cache.get(text)!;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const lowered = text.toLowerCase();
|
|
69
|
-
const ngrams = new Set<string>();
|
|
70
|
-
|
|
71
|
-
// Generate n-grams for each size in range
|
|
72
|
-
for (let n = this.nRange[0]; n <= this.nRange[1]; n++) {
|
|
73
|
-
if (lowered.length < n) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
for (let i = 0; i <= lowered.length - n; i++) {
|
|
77
|
-
ngrams.add(lowered.slice(i, i + n));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Cache result if we haven't hit size limit
|
|
82
|
-
if (this.cache.size < this.maxCacheSize) {
|
|
83
|
-
this.cache.set(text, ngrams);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return ngrams;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Calculate Jaccard similarity between two texts using character n-grams.
|
|
91
|
-
* @param a - First text
|
|
92
|
-
* @param b - Second text
|
|
93
|
-
* @returns Similarity score between 0 and 1
|
|
94
|
-
*/
|
|
95
|
-
similarity(a: string, b: string): number {
|
|
96
|
-
if (!a || !b) {
|
|
97
|
-
return 0.0;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const fpA = this.fingerprint(a);
|
|
101
|
-
const fpB = this.fingerprint(b);
|
|
102
|
-
|
|
103
|
-
if (fpA.size === 0 || fpB.size === 0) {
|
|
104
|
-
return 0.0;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Calculate intersection
|
|
108
|
-
let intersection = 0;
|
|
109
|
-
for (const ngram of fpA) {
|
|
110
|
-
if (fpB.has(ngram)) {
|
|
111
|
-
intersection++;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Calculate union
|
|
116
|
-
const union = fpA.size + fpB.size - intersection;
|
|
117
|
-
|
|
118
|
-
return union > 0 ? intersection / union : 0.0;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Rank candidates by semantic similarity to the query.
|
|
123
|
-
*
|
|
124
|
-
* Each candidate is a dict. The scorer reads `candidate[keyField]`
|
|
125
|
-
* for text to compare. Also scores the "key" field if present.
|
|
126
|
-
* Returns `[score, candidate]` tuples sorted descending, filtered by `minScore`.
|
|
127
|
-
*
|
|
128
|
-
* @param query - Query string to match against
|
|
129
|
-
* @param candidates - Array of candidate objects
|
|
130
|
-
* @param keyField - Field name to extract text from (default "value")
|
|
131
|
-
* @param topK - Maximum number of results to return (default 3)
|
|
132
|
-
* @param minScore - Minimum similarity threshold (default 0.02)
|
|
133
|
-
* @returns Array of [score, candidate] tuples, sorted by score descending
|
|
134
|
-
*/
|
|
135
|
-
rank(
|
|
136
|
-
query: string,
|
|
137
|
-
candidates: Candidate[],
|
|
138
|
-
keyField: string = "value",
|
|
139
|
-
topK: number = 3,
|
|
140
|
-
minScore: number = 0.02
|
|
141
|
-
): Array<[number, Candidate]> {
|
|
142
|
-
const scored: Array<[number, Candidate]> = [];
|
|
143
|
-
|
|
144
|
-
for (const candidate of candidates) {
|
|
145
|
-
let text = candidate[keyField];
|
|
146
|
-
|
|
147
|
-
// Skip if field is missing or empty
|
|
148
|
-
if (!text) {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Convert objects to string representation
|
|
153
|
-
if (typeof text === "object") {
|
|
154
|
-
text = JSON.stringify(text);
|
|
155
|
-
} else {
|
|
156
|
-
text = String(text);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Calculate similarity with the candidate's value field
|
|
160
|
-
let score = this.similarity(query, text as string);
|
|
161
|
-
|
|
162
|
-
// Also score the "key" field if present — often more discriminative
|
|
163
|
-
const keyText = candidate.key;
|
|
164
|
-
if (keyText) {
|
|
165
|
-
const keyScore = this.similarity(query, String(keyText));
|
|
166
|
-
score = Math.max(score, keyScore);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Add to results if above threshold
|
|
170
|
-
if (score >= minScore) {
|
|
171
|
-
scored.push([score, candidate]);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Sort descending by score
|
|
176
|
-
scored.sort((a, b) => b[0] - a[0]);
|
|
177
|
-
|
|
178
|
-
// Return top K results
|
|
179
|
-
return scored.slice(0, topK);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Clear the fingerprint cache.
|
|
184
|
-
*/
|
|
185
|
-
clearCache(): void {
|
|
186
|
-
this.cache.clear();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Get current cache size.
|
|
191
|
-
*/
|
|
192
|
-
getCacheSize(): number {
|
|
193
|
-
return this.cache.size;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Module-level singleton instance (lazy initialization).
|
|
199
|
-
*/
|
|
200
|
-
let scorer: SemanticScorer | null = null;
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Get or create the module-level semantic scorer singleton.
|
|
204
|
-
* @returns SemanticScorer instance
|
|
205
|
-
*/
|
|
206
|
-
export function getScorer(): SemanticScorer {
|
|
207
|
-
if (scorer === null) {
|
|
208
|
-
scorer = new SemanticScorer();
|
|
209
|
-
}
|
|
210
|
-
return scorer;
|
|
211
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight semantic retrieval — zero external dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Why: The existing `recall_for_injection` uses SQL LIKE on tokens, which
|
|
5
|
+
* misses semantically related facts that share few literal characters (e.g.
|
|
6
|
+
* query "deploy" vs. stored fact "release_command"). This module provides
|
|
7
|
+
* a character n-gram Jaccard similarity scorer that catches those cross-
|
|
8
|
+
* lingual and synonym relationships without adding PyTorch / sentence-transformers.
|
|
9
|
+
*
|
|
10
|
+
* Design:
|
|
11
|
+
* - Character n-grams (size 2-4) naturally handle CJK, mixed-language, and
|
|
12
|
+
* code identifiers better than word-level tokenization.
|
|
13
|
+
* - Jaccard similarity on n-gram sets is fast (< 10 µs per pair) and
|
|
14
|
+
* well-correlated with human relevance judgments for short text.
|
|
15
|
+
* - Zero dependencies beyond stdlib.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a candidate item for ranking
|
|
20
|
+
*/
|
|
21
|
+
export interface Candidate {
|
|
22
|
+
key?: string;
|
|
23
|
+
value?: string | Record<string, unknown>;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Character n-gram semantic similarity scorer.
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* ```
|
|
32
|
+
* const scorer = new SemanticScorer();
|
|
33
|
+
* const score = scorer.similarity("deploy to prod", "release_command");
|
|
34
|
+
* // score ≈ 0.15 (low but non-zero — catches partial overlap)
|
|
35
|
+
*
|
|
36
|
+
* const ranked = scorer.rank(
|
|
37
|
+
* "search query",
|
|
38
|
+
* [{value: "candidate A"}, {value: "candidate B"}]
|
|
39
|
+
* );
|
|
40
|
+
* // ranked → [[0.85, {value: "candidate A"}], [0.30, {value: "candidate B"}]]
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export class SemanticScorer {
|
|
44
|
+
private nRange: [number, number];
|
|
45
|
+
private cache: Map<string, Set<string>>;
|
|
46
|
+
private maxCacheSize = 512;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Initialize scorer with n-gram range.
|
|
50
|
+
* @param nRange - Tuple of [minSize, maxSize] for n-grams (default [2, 4])
|
|
51
|
+
*/
|
|
52
|
+
constructor(nRange: [number, number] = [2, 4]) {
|
|
53
|
+
this.nRange = nRange;
|
|
54
|
+
this.cache = new Map();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Generate character n-gram fingerprint (cached).
|
|
59
|
+
* @param text - Input text to fingerprint
|
|
60
|
+
* @returns Set of n-grams for the text
|
|
61
|
+
*/
|
|
62
|
+
private fingerprint(text: string): Set<string> {
|
|
63
|
+
// Return cached result if available
|
|
64
|
+
if (this.cache.has(text)) {
|
|
65
|
+
return this.cache.get(text)!;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const lowered = text.toLowerCase();
|
|
69
|
+
const ngrams = new Set<string>();
|
|
70
|
+
|
|
71
|
+
// Generate n-grams for each size in range
|
|
72
|
+
for (let n = this.nRange[0]; n <= this.nRange[1]; n++) {
|
|
73
|
+
if (lowered.length < n) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
for (let i = 0; i <= lowered.length - n; i++) {
|
|
77
|
+
ngrams.add(lowered.slice(i, i + n));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Cache result if we haven't hit size limit
|
|
82
|
+
if (this.cache.size < this.maxCacheSize) {
|
|
83
|
+
this.cache.set(text, ngrams);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return ngrams;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Calculate Jaccard similarity between two texts using character n-grams.
|
|
91
|
+
* @param a - First text
|
|
92
|
+
* @param b - Second text
|
|
93
|
+
* @returns Similarity score between 0 and 1
|
|
94
|
+
*/
|
|
95
|
+
similarity(a: string, b: string): number {
|
|
96
|
+
if (!a || !b) {
|
|
97
|
+
return 0.0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const fpA = this.fingerprint(a);
|
|
101
|
+
const fpB = this.fingerprint(b);
|
|
102
|
+
|
|
103
|
+
if (fpA.size === 0 || fpB.size === 0) {
|
|
104
|
+
return 0.0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Calculate intersection
|
|
108
|
+
let intersection = 0;
|
|
109
|
+
for (const ngram of fpA) {
|
|
110
|
+
if (fpB.has(ngram)) {
|
|
111
|
+
intersection++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Calculate union
|
|
116
|
+
const union = fpA.size + fpB.size - intersection;
|
|
117
|
+
|
|
118
|
+
return union > 0 ? intersection / union : 0.0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Rank candidates by semantic similarity to the query.
|
|
123
|
+
*
|
|
124
|
+
* Each candidate is a dict. The scorer reads `candidate[keyField]`
|
|
125
|
+
* for text to compare. Also scores the "key" field if present.
|
|
126
|
+
* Returns `[score, candidate]` tuples sorted descending, filtered by `minScore`.
|
|
127
|
+
*
|
|
128
|
+
* @param query - Query string to match against
|
|
129
|
+
* @param candidates - Array of candidate objects
|
|
130
|
+
* @param keyField - Field name to extract text from (default "value")
|
|
131
|
+
* @param topK - Maximum number of results to return (default 3)
|
|
132
|
+
* @param minScore - Minimum similarity threshold (default 0.02)
|
|
133
|
+
* @returns Array of [score, candidate] tuples, sorted by score descending
|
|
134
|
+
*/
|
|
135
|
+
rank(
|
|
136
|
+
query: string,
|
|
137
|
+
candidates: Candidate[],
|
|
138
|
+
keyField: string = "value",
|
|
139
|
+
topK: number = 3,
|
|
140
|
+
minScore: number = 0.02
|
|
141
|
+
): Array<[number, Candidate]> {
|
|
142
|
+
const scored: Array<[number, Candidate]> = [];
|
|
143
|
+
|
|
144
|
+
for (const candidate of candidates) {
|
|
145
|
+
let text = candidate[keyField];
|
|
146
|
+
|
|
147
|
+
// Skip if field is missing or empty
|
|
148
|
+
if (!text) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Convert objects to string representation
|
|
153
|
+
if (typeof text === "object") {
|
|
154
|
+
text = JSON.stringify(text);
|
|
155
|
+
} else {
|
|
156
|
+
text = String(text);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Calculate similarity with the candidate's value field
|
|
160
|
+
let score = this.similarity(query, text as string);
|
|
161
|
+
|
|
162
|
+
// Also score the "key" field if present — often more discriminative
|
|
163
|
+
const keyText = candidate.key;
|
|
164
|
+
if (keyText) {
|
|
165
|
+
const keyScore = this.similarity(query, String(keyText));
|
|
166
|
+
score = Math.max(score, keyScore);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Add to results if above threshold
|
|
170
|
+
if (score >= minScore) {
|
|
171
|
+
scored.push([score, candidate]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Sort descending by score
|
|
176
|
+
scored.sort((a, b) => b[0] - a[0]);
|
|
177
|
+
|
|
178
|
+
// Return top K results
|
|
179
|
+
return scored.slice(0, topK);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clear the fingerprint cache.
|
|
184
|
+
*/
|
|
185
|
+
clearCache(): void {
|
|
186
|
+
this.cache.clear();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get current cache size.
|
|
191
|
+
*/
|
|
192
|
+
getCacheSize(): number {
|
|
193
|
+
return this.cache.size;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Module-level singleton instance (lazy initialization).
|
|
199
|
+
*/
|
|
200
|
+
let scorer: SemanticScorer | null = null;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get or create the module-level semantic scorer singleton.
|
|
204
|
+
* @returns SemanticScorer instance
|
|
205
|
+
*/
|
|
206
|
+
export function getScorer(): SemanticScorer {
|
|
207
|
+
if (scorer === null) {
|
|
208
|
+
scorer = new SemanticScorer();
|
|
209
|
+
}
|
|
210
|
+
return scorer;
|
|
211
|
+
}
|