knolo-core 0.2.3 → 0.3.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/dist/query.js CHANGED
@@ -13,18 +13,76 @@ import { rankBM25L } from "./rank.js";
13
13
  import { minCoverSpan, proximityMultiplier } from "./quality/proximity.js";
14
14
  import { diversifyAndDedupe } from "./quality/diversify.js";
15
15
  import { knsSignature, knsDistance } from "./quality/signature.js";
16
+ import { decodeScaleF16, quantizeEmbeddingInt8L2Norm } from "./semantic.js";
17
+ export function validateSemanticQueryOptions(options) {
18
+ if (!options)
19
+ return;
20
+ if (options.enabled !== undefined && typeof options.enabled !== "boolean") {
21
+ throw new Error("query(...): semantic.enabled must be a boolean when provided.");
22
+ }
23
+ if (options.mode !== undefined && options.mode !== "rerank") {
24
+ throw new Error('query(...): semantic.mode currently only supports "rerank".');
25
+ }
26
+ if (options.topN !== undefined && (!Number.isInteger(options.topN) || options.topN < 1)) {
27
+ throw new Error("query(...): semantic.topN must be a positive integer.");
28
+ }
29
+ if (options.minLexConfidence !== undefined && (!Number.isFinite(options.minLexConfidence) || options.minLexConfidence < 0 || options.minLexConfidence > 1)) {
30
+ throw new Error("query(...): semantic.minLexConfidence must be a finite number between 0 and 1.");
31
+ }
32
+ if (options.queryEmbedding !== undefined && !(options.queryEmbedding instanceof Float32Array)) {
33
+ throw new Error("query(...): semantic.queryEmbedding must be a Float32Array.");
34
+ }
35
+ if (options.blend) {
36
+ if (options.blend.enabled !== undefined && typeof options.blend.enabled !== "boolean") {
37
+ throw new Error("query(...): semantic.blend.enabled must be a boolean when provided.");
38
+ }
39
+ if (options.blend.wLex !== undefined && (!Number.isFinite(options.blend.wLex) || options.blend.wLex < 0)) {
40
+ throw new Error("query(...): semantic.blend.wLex must be a finite number >= 0.");
41
+ }
42
+ if (options.blend.wSem !== undefined && (!Number.isFinite(options.blend.wSem) || options.blend.wSem < 0)) {
43
+ throw new Error("query(...): semantic.blend.wSem must be a finite number >= 0.");
44
+ }
45
+ }
46
+ if (options.force !== undefined && typeof options.force !== "boolean") {
47
+ throw new Error("query(...): semantic.force must be a boolean when provided.");
48
+ }
49
+ }
16
50
  export function query(pack, q, opts = {}) {
51
+ validateSemanticQueryOptions(opts.semantic);
17
52
  const topK = opts.topK ?? 10;
53
+ const minScore = Number.isFinite(opts.minScore) ? Math.max(0, opts.minScore) : 0;
54
+ const expansionOpts = {
55
+ enabled: opts.queryExpansion?.enabled ?? true,
56
+ docs: Math.max(1, opts.queryExpansion?.docs ?? 3),
57
+ terms: Math.max(1, opts.queryExpansion?.terms ?? 4),
58
+ weight: Math.max(0, opts.queryExpansion?.weight ?? 0.35),
59
+ minTermLength: Math.max(2, opts.queryExpansion?.minTermLength ?? 3),
60
+ };
61
+ const semanticOpts = {
62
+ enabled: opts.semantic?.enabled ?? false,
63
+ mode: opts.semantic?.mode ?? "rerank",
64
+ topN: Math.max(1, opts.semantic?.topN ?? 50),
65
+ minLexConfidence: clamp01(opts.semantic?.minLexConfidence ?? 0.35),
66
+ blend: {
67
+ enabled: opts.semantic?.blend?.enabled ?? true,
68
+ wLex: Math.max(0, opts.semantic?.blend?.wLex ?? 0.75),
69
+ wSem: Math.max(0, opts.semantic?.blend?.wSem ?? 0.25),
70
+ },
71
+ queryEmbedding: opts.semantic?.queryEmbedding,
72
+ force: opts.semantic?.force ?? false,
73
+ };
18
74
  // --- Query parsing
19
75
  const normTokens = tokenize(q).map((t) => t.term);
20
76
  // Normalize quoted phrases from q
21
- const quotedRaw = parsePhrases(q); // arrays of raw terms
22
- const quoted = quotedRaw.map(seq => seq.map(t => normalize(t)).flatMap(s => s.split(/\s+/)).filter(Boolean));
77
+ const quotedRaw = parsePhrases(q);
78
+ const quoted = quotedRaw.map((seq) => seq.map((t) => normalize(t)).flatMap((s) => s.split(/\s+/)).filter(Boolean));
23
79
  // Normalize requirePhrases the same way
24
80
  const extraReq = (opts.requirePhrases ?? [])
25
- .map(s => tokenize(s).map(t => t.term)) // <<< normalize via tokenizer
26
- .filter(arr => arr.length > 0);
81
+ .map((s) => tokenize(s).map((t) => t.term))
82
+ .filter((arr) => arr.length > 0);
27
83
  const requiredPhrases = [...quoted, ...extraReq];
84
+ const namespaceFilter = normalizeNamespaceFilter(opts.namespace);
85
+ const sourceFilter = normalizeSourceFilter(opts.source);
28
86
  // --- Term ids for the free (unquoted) tokens in q
29
87
  const termIds = normTokens
30
88
  .map((t) => pack.lexicon.get(t))
@@ -33,39 +91,53 @@ export function query(pack, q, opts = {}) {
33
91
  const termSet = new Set(termIds);
34
92
  // --- Candidate map
35
93
  const candidates = new Map();
94
+ // Query-time document frequency collection for BM25 IDF.
95
+ const dfs = new Map();
96
+ const usesOffsetBlockIds = (pack.meta?.version ?? 1) >= 3;
36
97
  // Helper to harvest postings for a given set of termIds into candidates
37
- function scanForTermIds(idSet) {
98
+ function scanForTermIds(idWeights, cfg = { collectPositions: true, createCandidates: true }) {
38
99
  const p = pack.postings;
39
100
  let i = 0;
40
101
  while (i < p.length) {
41
102
  const tid = p[i++];
42
103
  if (tid === 0)
43
104
  continue;
44
- const relevant = idSet.has(tid);
45
- let bid = p[i++];
46
- while (bid !== 0) {
105
+ const weight = idWeights.get(tid) ?? 0;
106
+ const relevant = weight > 0;
107
+ let termDf = 0;
108
+ let encodedBid = p[i++];
109
+ while (encodedBid !== 0) {
110
+ const bid = usesOffsetBlockIds ? encodedBid - 1 : encodedBid;
47
111
  let pos = p[i++];
48
112
  const positions = [];
49
113
  while (pos !== 0) {
50
114
  positions.push(pos);
51
115
  pos = p[i++];
52
116
  }
53
- if (relevant) {
117
+ termDf++;
118
+ if (relevant && bid >= 0) {
54
119
  let entry = candidates.get(bid);
55
- if (!entry) {
120
+ if (!entry && cfg.createCandidates !== false) {
56
121
  entry = { tf: new Map(), pos: new Map() };
57
122
  candidates.set(bid, entry);
58
123
  }
59
- entry.tf.set(tid, positions.length);
60
- entry.pos.set(tid, positions);
124
+ if (entry) {
125
+ const prevTf = entry.tf.get(tid) ?? 0;
126
+ entry.tf.set(tid, prevTf + positions.length * weight);
127
+ if (cfg.collectPositions !== false) {
128
+ entry.pos.set(tid, positions);
129
+ }
130
+ }
61
131
  }
62
- bid = p[i++];
132
+ encodedBid = p[i++];
63
133
  }
134
+ if (relevant)
135
+ dfs.set(tid, termDf);
64
136
  }
65
137
  }
66
138
  // 1) Scan using tokens from q (if any)
67
139
  if (termSet.size > 0) {
68
- scanForTermIds(termSet);
140
+ scanForTermIds(new Map(Array.from(termSet.values(), (tid) => [tid, 1])));
69
141
  }
70
142
  // 2) Phrase-first rescue:
71
143
  // If nothing matched the free tokens, but we do have required phrases,
@@ -80,7 +152,27 @@ export function query(pack, q, opts = {}) {
80
152
  }
81
153
  }
82
154
  if (phraseTokenIds.size > 0) {
83
- scanForTermIds(phraseTokenIds);
155
+ scanForTermIds(new Map(Array.from(phraseTokenIds.values(), (tid) => [tid, 1])));
156
+ }
157
+ }
158
+ // --- Namespace filtering
159
+ if (namespaceFilter.size > 0) {
160
+ for (const bid of [...candidates.keys()]) {
161
+ const ns = pack.namespaces?.[bid];
162
+ const normalizedNs = typeof ns === "string" ? normalize(ns) : "";
163
+ if (!normalizedNs || !namespaceFilter.has(normalizedNs)) {
164
+ candidates.delete(bid);
165
+ }
166
+ }
167
+ }
168
+ // --- Source/docId filtering
169
+ if (sourceFilter.size > 0) {
170
+ for (const bid of [...candidates.keys()]) {
171
+ const source = pack.docIds?.[bid];
172
+ const normalizedSource = typeof source === "string" ? normalize(source) : "";
173
+ if (!normalizedSource || !sourceFilter.has(normalizedSource)) {
174
+ candidates.delete(bid);
175
+ }
84
176
  }
85
177
  }
86
178
  // --- Phrase enforcement (now that we have some candidates)
@@ -119,11 +211,30 @@ export function query(pack, q, opts = {}) {
119
211
  (pack.blocks.length
120
212
  ? pack.blocks.reduce((s, b) => s + tokenize(b).length, 0) / pack.blocks.length
121
213
  : 1);
122
- const prelim = rankBM25L(candidates, avgLen, {
214
+ const docCount = pack.meta?.stats?.blocks ?? pack.blocks.length;
215
+ let prelim = rankBM25L(candidates, avgLen, docCount, dfs, pack.blockTokenLens, {
123
216
  proximityBonus: (cand) => proximityMultiplier(minCoverSpan(cand.pos)),
124
217
  });
218
+ if (expansionOpts.enabled && prelim.length > 0) {
219
+ const expansionWeights = deriveExpansionTerms(pack, prelim, termSet, requiredPhrases, expansionOpts);
220
+ if (expansionWeights.size > 0) {
221
+ scanForTermIds(expansionWeights, { collectPositions: false, createCandidates: true });
222
+ prelim = rankBM25L(candidates, avgLen, docCount, dfs, pack.blockTokenLens, {
223
+ proximityBonus: (cand) => proximityMultiplier(minCoverSpan(cand.pos)),
224
+ });
225
+ }
226
+ }
125
227
  if (prelim.length === 0)
126
228
  return [];
229
+ if (minScore > 0) {
230
+ prelim = prelim.filter((item) => item.score >= minScore);
231
+ if (prelim.length === 0)
232
+ return [];
233
+ }
234
+ const confidence = lexConfidence(prelim);
235
+ if (shouldRerankWithSemantic(pack, semanticOpts, confidence)) {
236
+ prelim = rerankLexicalHitsWithSemantic(pack, prelim, semanticOpts);
237
+ }
127
238
  // --- KNS tie-breaker + de-dup/MMR
128
239
  const qSig = knsSignature(normalize(q));
129
240
  const pool = prelim.slice(0, topK * 5).map((r) => {
@@ -134,17 +245,158 @@ export function query(pack, q, opts = {}) {
134
245
  score: r.score * boost,
135
246
  text,
136
247
  source: pack.docIds?.[r.blockId] ?? undefined,
248
+ namespace: pack.namespaces?.[r.blockId] ?? undefined,
137
249
  };
138
250
  });
139
251
  const finalHits = diversifyAndDedupe(pool, { k: topK });
140
252
  return finalHits;
141
253
  }
254
+ export function lexConfidence(hits) {
255
+ if (hits.length === 0)
256
+ return 0;
257
+ const top1 = Math.max(0, hits[0]?.score ?? 0);
258
+ const top2 = Math.max(0, hits[1]?.score ?? 0);
259
+ const gapRatio = top1 > 0 ? clamp01((top1 - top2) / top1) : 0;
260
+ const strength = top1 > 0 ? top1 / (top1 + 1) : 0;
261
+ return clamp01(0.65 * gapRatio + 0.35 * strength);
262
+ }
263
+ function shouldRerankWithSemantic(pack, opts, confidence) {
264
+ if (!opts.enabled || opts.mode !== "rerank")
265
+ return false;
266
+ if (!pack.semantic)
267
+ return false;
268
+ if (!opts.queryEmbedding) {
269
+ throw new Error("query(...): semantic.queryEmbedding (Float32Array) is required when semantic.enabled=true.");
270
+ }
271
+ return opts.force || confidence < opts.minLexConfidence;
272
+ }
273
+ function rerankLexicalHitsWithSemantic(pack, prelim, opts) {
274
+ const sem = pack.semantic;
275
+ if (!sem || !opts.queryEmbedding)
276
+ return prelim;
277
+ if (sem.dims <= 0 || sem.vecs.length === 0 || sem.dims !== opts.queryEmbedding.length)
278
+ return prelim;
279
+ const topN = Math.min(opts.topN, prelim.length);
280
+ const rerankSlice = prelim.slice(0, topN);
281
+ const tail = prelim.slice(topN);
282
+ const lexScores = new Float64Array(topN);
283
+ for (let i = 0; i < topN; i++)
284
+ lexScores[i] = rerankSlice[i].score;
285
+ const normLex = minMaxNormalizeTyped(lexScores);
286
+ const quantizedQuery = quantizeEmbeddingInt8L2Norm(opts.queryEmbedding);
287
+ const semScores = scoreSemanticInt8(quantizedQuery.q, quantizedQuery.scale, sem, rerankSlice);
288
+ const normSem = minMaxNormalizeTyped(semScores);
289
+ const denom = opts.blend.wLex + opts.blend.wSem;
290
+ const wLex = denom > 0 ? opts.blend.wLex / denom : 0.5;
291
+ const wSem = denom > 0 ? opts.blend.wSem / denom : 0.5;
292
+ const reranked = new Array(topN);
293
+ for (let i = 0; i < topN; i++) {
294
+ const hit = rerankSlice[i];
295
+ reranked[i] = {
296
+ blockId: hit.blockId,
297
+ score: opts.blend.enabled ? wLex * normLex[i] + wSem * normSem[i] : semScores[i],
298
+ };
299
+ }
300
+ reranked.sort((a, b) => b.score - a.score || a.blockId - b.blockId);
301
+ return [...reranked, ...tail];
302
+ }
303
+ function scoreSemanticInt8(queryQ, queryScale, semantic, hits) {
304
+ const scores = new Float64Array(hits.length);
305
+ const dims = semantic.dims;
306
+ if (queryQ.length !== dims || queryScale === 0)
307
+ return scores;
308
+ const vecs = semantic.vecs;
309
+ const scales = semantic.scales;
310
+ for (let h = 0; h < hits.length; h++) {
311
+ const blockId = hits[h].blockId;
312
+ if (blockId < 0)
313
+ continue;
314
+ const base = blockId * dims;
315
+ if (base + dims > vecs.length)
316
+ continue;
317
+ let dot = 0;
318
+ for (let i = 0; i < dims; i++) {
319
+ dot += queryQ[i] * vecs[base + i];
320
+ }
321
+ const blockScale = scales?.[blockId] !== undefined ? decodeScaleF16(scales[blockId]) : (1 / 127);
322
+ scores[h] = dot * queryScale * blockScale;
323
+ }
324
+ return scores;
325
+ }
326
+ function minMaxNormalizeTyped(values) {
327
+ if (values.length === 0)
328
+ return values;
329
+ let min = Infinity;
330
+ let max = -Infinity;
331
+ for (let i = 0; i < values.length; i++) {
332
+ const v = values[i];
333
+ if (v < min)
334
+ min = v;
335
+ if (v > max)
336
+ max = v;
337
+ }
338
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
339
+ const out = new Float64Array(values.length);
340
+ out.fill(1);
341
+ return out;
342
+ }
343
+ const out = new Float64Array(values.length);
344
+ const span = max - min;
345
+ for (let i = 0; i < values.length; i++) {
346
+ out[i] = clamp01((values[i] - min) / span);
347
+ }
348
+ return out;
349
+ }
350
+ function clamp01(v) {
351
+ if (!Number.isFinite(v))
352
+ return 0;
353
+ if (v < 0)
354
+ return 0;
355
+ if (v > 1)
356
+ return 1;
357
+ return v;
358
+ }
359
+ function deriveExpansionTerms(pack, prelim, baseTermSet, requiredPhrases, opts) {
360
+ if (prelim.length === 0 || opts.weight <= 0)
361
+ return new Map();
362
+ const forbidden = new Set(baseTermSet);
363
+ for (const seq of requiredPhrases) {
364
+ for (const term of seq) {
365
+ const tid = pack.lexicon.get(term);
366
+ if (tid !== undefined)
367
+ forbidden.add(tid);
368
+ }
369
+ }
370
+ const cap = Math.min(opts.docs, prelim.length);
371
+ const bestScore = Math.max(prelim[0]?.score ?? 0, 1e-6);
372
+ const termScores = new Map();
373
+ for (let i = 0; i < cap; i++) {
374
+ const item = prelim[i];
375
+ const text = pack.blocks[item.blockId] ?? "";
376
+ const docWeight = Math.max(item.score / bestScore, 0.2);
377
+ const localTfs = new Map();
378
+ for (const tok of tokenize(text)) {
379
+ if (tok.term.length < opts.minTermLength)
380
+ continue;
381
+ const tid = pack.lexicon.get(tok.term);
382
+ if (tid === undefined || forbidden.has(tid))
383
+ continue;
384
+ localTfs.set(tid, (localTfs.get(tid) ?? 0) + 1);
385
+ }
386
+ for (const [tid, tf] of localTfs) {
387
+ termScores.set(tid, (termScores.get(tid) ?? 0) + tf * docWeight);
388
+ }
389
+ }
390
+ const selected = [...termScores.entries()]
391
+ .sort((a, b) => b[1] - a[1])
392
+ .slice(0, opts.terms);
393
+ return new Map(selected.map(([tid, score]) => [tid, opts.weight * Math.max(0.5, Math.min(1.5, score))]));
394
+ }
142
395
  /** Ordered phrase check using the SAME tokenizer/normalizer path as the index. */
143
396
  function containsPhrase(text, seq) {
144
397
  if (seq.length === 0)
145
398
  return false;
146
- // normalize seq via tokenizer to be extra safe (handles diacritics/case)
147
- const seqNorm = tokenize(seq.join(" ")).map(t => t.term);
399
+ const seqNorm = tokenize(seq.join(" ")).map((t) => t.term);
148
400
  const toks = tokenize(text).map((t) => t.term);
149
401
  outer: for (let i = 0; i <= toks.length - seqNorm.length; i++) {
150
402
  for (let j = 0; j < seqNorm.length; j++) {
@@ -155,3 +407,15 @@ function containsPhrase(text, seq) {
155
407
  }
156
408
  return false;
157
409
  }
410
+ function normalizeNamespaceFilter(input) {
411
+ if (input === undefined)
412
+ return new Set();
413
+ const values = Array.isArray(input) ? input : [input];
414
+ return new Set(values.map((v) => normalize(v)).filter(Boolean));
415
+ }
416
+ function normalizeSourceFilter(input) {
417
+ if (input === undefined)
418
+ return new Set();
419
+ const values = Array.isArray(input) ? input : [input];
420
+ return new Set(values.map((v) => normalize(v)).filter(Boolean));
421
+ }
package/dist/rank.d.ts CHANGED
@@ -15,7 +15,7 @@ export declare function rankBM25L(candidates: Map<number, {
15
15
  pos?: Map<number, number[]>;
16
16
  hasPhrase?: boolean;
17
17
  headingScore?: number;
18
- }>, avgLen: number, opts?: RankOptions): Array<{
18
+ }>, avgLen: number, docCount: number, dfs: Map<number, number>, blockTokenLens?: number[], opts?: RankOptions): Array<{
19
19
  blockId: number;
20
20
  score: number;
21
21
  }>;
package/dist/rank.js CHANGED
@@ -2,17 +2,18 @@
2
2
  * rank.ts
3
3
  * BM25L ranker with optional heading/phrase boosts and a proximity bonus hook.
4
4
  */
5
- export function rankBM25L(candidates, avgLen, opts = {}) {
5
+ export function rankBM25L(candidates, avgLen, docCount, dfs, blockTokenLens, opts = {}) {
6
6
  const k1 = opts.k1 ?? 1.5;
7
7
  const b = opts.b ?? 0.75;
8
8
  const headingBoost = opts.headingBoost ?? 0.3;
9
9
  const phraseBoost = opts.phraseBoost ?? 0.6;
10
10
  const results = [];
11
11
  for (const [bid, data] of candidates) {
12
- const len = Array.from(data.tf.values()).reduce((sum, tf) => sum + tf, 0) || 1;
12
+ const len = blockTokenLens?.[bid] ?? (Array.from(data.tf.values()).reduce((sum, tf) => sum + tf, 0) || 1);
13
13
  let score = 0;
14
- for (const [, tf] of data.tf) {
15
- const idf = 1; // v0: no DF; can be extended later
14
+ for (const [tid, tf] of data.tf) {
15
+ const df = dfs.get(tid) ?? 0;
16
+ const idf = Math.log(1 + (docCount - df + 0.5) / (df + 0.5));
16
17
  const numer = tf * (k1 + 1);
17
18
  const denom = tf + k1 * (1 - b + b * (len / avgLen));
18
19
  score += idf * (numer / denom);
@@ -0,0 +1,7 @@
1
+ export type QuantizedVector = {
2
+ q: Int8Array;
3
+ scale: number;
4
+ };
5
+ export declare function quantizeEmbeddingInt8L2Norm(embedding: Float32Array): QuantizedVector;
6
+ export declare function encodeScaleF16(scale: number): number;
7
+ export declare function decodeScaleF16(encoded: number): number;
@@ -0,0 +1,98 @@
1
+ export function quantizeEmbeddingInt8L2Norm(embedding) {
2
+ const dims = embedding.length;
3
+ const normalized = new Float32Array(dims);
4
+ let normSq = 0;
5
+ for (let i = 0; i < dims; i++)
6
+ normSq += embedding[i] * embedding[i];
7
+ const norm = Math.sqrt(normSq);
8
+ if (norm === 0) {
9
+ return { q: new Int8Array(dims), scale: 0 };
10
+ }
11
+ let maxAbs = 0;
12
+ for (let i = 0; i < dims; i++) {
13
+ const value = embedding[i] / norm;
14
+ normalized[i] = value;
15
+ const abs = Math.abs(value);
16
+ if (abs > maxAbs)
17
+ maxAbs = abs;
18
+ }
19
+ const scale = maxAbs / 127;
20
+ if (scale === 0) {
21
+ return { q: new Int8Array(dims), scale: 0 };
22
+ }
23
+ const q = new Int8Array(dims);
24
+ for (let i = 0; i < dims; i++) {
25
+ const quantized = Math.round(normalized[i] / scale);
26
+ q[i] = clampInt8(quantized);
27
+ }
28
+ return { q, scale };
29
+ }
30
+ export function encodeScaleF16(scale) {
31
+ return float32ToFloat16(scale);
32
+ }
33
+ export function decodeScaleF16(encoded) {
34
+ return float16ToFloat32(encoded);
35
+ }
36
+ function clampInt8(value) {
37
+ if (value > 127)
38
+ return 127;
39
+ if (value < -127)
40
+ return -127;
41
+ return value;
42
+ }
43
+ function float32ToFloat16(value) {
44
+ if (Number.isNaN(value))
45
+ return 0x7e00;
46
+ if (value === Infinity)
47
+ return 0x7c00;
48
+ if (value === -Infinity)
49
+ return 0xfc00;
50
+ const f32 = new Float32Array(1);
51
+ const u32 = new Uint32Array(f32.buffer);
52
+ f32[0] = value;
53
+ const bits = u32[0];
54
+ const sign = (bits >>> 16) & 0x8000;
55
+ let exp = (bits >>> 23) & 0xff;
56
+ let mantissa = bits & 0x7fffff;
57
+ if (exp === 0xff) {
58
+ return sign | (mantissa ? 0x7e00 : 0x7c00);
59
+ }
60
+ const halfExp = exp - 127 + 15;
61
+ if (halfExp >= 0x1f) {
62
+ return sign | 0x7c00;
63
+ }
64
+ if (halfExp <= 0) {
65
+ if (halfExp < -10)
66
+ return sign;
67
+ mantissa = (mantissa | 0x800000) >>> (1 - halfExp);
68
+ if (mantissa & 0x1000)
69
+ mantissa += 0x2000;
70
+ return sign | (mantissa >>> 13);
71
+ }
72
+ if (mantissa & 0x1000) {
73
+ mantissa += 0x2000;
74
+ if (mantissa & 0x800000) {
75
+ mantissa = 0;
76
+ exp += 1;
77
+ if (exp > 142)
78
+ return sign | 0x7c00;
79
+ }
80
+ }
81
+ return sign | (halfExp << 10) | (mantissa >>> 13);
82
+ }
83
+ function float16ToFloat32(bits) {
84
+ const sign = (bits & 0x8000) ? -1 : 1;
85
+ const exp = (bits >>> 10) & 0x1f;
86
+ const frac = bits & 0x03ff;
87
+ if (exp === 0) {
88
+ if (frac === 0)
89
+ return sign * 0;
90
+ return sign * Math.pow(2, -14) * (frac / 1024);
91
+ }
92
+ if (exp === 0x1f) {
93
+ if (frac === 0)
94
+ return sign * Infinity;
95
+ return NaN;
96
+ }
97
+ return sign * Math.pow(2, exp - 15) * (1 + frac / 1024);
98
+ }
package/dist/tokenize.js CHANGED
@@ -42,7 +42,7 @@ export function tokenize(text) {
42
42
  */
43
43
  export function parsePhrases(q) {
44
44
  const parts = [];
45
- const regex = /"([^\"]+)"/g;
45
+ const regex = /["“”]([^"“”]+)["“”]/g;
46
46
  let match;
47
47
  while ((match = regex.exec(q)) !== null) {
48
48
  const phrase = match[1].trim().split(/\s+/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "knolo-core",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Local-first knowledge packs for small LLMs.",
6
6
  "keywords": [
@@ -33,7 +33,8 @@
33
33
  "scripts": {
34
34
  "build": "tsc -p tsconfig.json",
35
35
  "prepublishOnly": "npm run build",
36
- "smoke": "node scripts/smoke.mjs"
36
+ "smoke": "node scripts/smoke.mjs",
37
+ "test": "npm run build && node scripts/test.mjs"
37
38
  },
38
39
  "devDependencies": {
39
40
  "typescript": "^5.5.0",