knolo-core 0.2.3 → 3.1.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,110 @@ 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 validateQueryOptions(opts) {
18
+ if (!opts)
19
+ return;
20
+ if (opts.topK !== undefined && (!Number.isInteger(opts.topK) || opts.topK < 1)) {
21
+ throw new Error("query(...): topK must be a positive integer.");
22
+ }
23
+ if (opts.minScore !== undefined && (!Number.isFinite(opts.minScore) || opts.minScore < 0)) {
24
+ throw new Error("query(...): minScore must be a finite number >= 0.");
25
+ }
26
+ if (opts.requirePhrases !== undefined && (!Array.isArray(opts.requirePhrases) || opts.requirePhrases.some((p) => typeof p !== "string"))) {
27
+ throw new Error("query(...): requirePhrases must be an array of strings when provided.");
28
+ }
29
+ validateStringOrStringArrayOption("namespace", opts.namespace);
30
+ validateStringOrStringArrayOption("source", opts.source);
31
+ if (opts.queryExpansion) {
32
+ const qe = opts.queryExpansion;
33
+ if (qe.enabled !== undefined && typeof qe.enabled !== "boolean") {
34
+ throw new Error("query(...): queryExpansion.enabled must be a boolean when provided.");
35
+ }
36
+ if (qe.docs !== undefined && (!Number.isInteger(qe.docs) || qe.docs < 1)) {
37
+ throw new Error("query(...): queryExpansion.docs must be a positive integer.");
38
+ }
39
+ if (qe.terms !== undefined && (!Number.isInteger(qe.terms) || qe.terms < 1)) {
40
+ throw new Error("query(...): queryExpansion.terms must be a positive integer.");
41
+ }
42
+ if (qe.weight !== undefined && (!Number.isFinite(qe.weight) || qe.weight < 0)) {
43
+ throw new Error("query(...): queryExpansion.weight must be a finite number >= 0.");
44
+ }
45
+ if (qe.minTermLength !== undefined && (!Number.isInteger(qe.minTermLength) || qe.minTermLength < 1)) {
46
+ throw new Error("query(...): queryExpansion.minTermLength must be a positive integer.");
47
+ }
48
+ }
49
+ validateSemanticQueryOptions(opts.semantic);
50
+ }
51
+ export function validateSemanticQueryOptions(options) {
52
+ if (!options)
53
+ return;
54
+ if (options.enabled !== undefined && typeof options.enabled !== "boolean") {
55
+ throw new Error("query(...): semantic.enabled must be a boolean when provided.");
56
+ }
57
+ if (options.mode !== undefined && options.mode !== "rerank") {
58
+ throw new Error('query(...): semantic.mode currently only supports "rerank".');
59
+ }
60
+ if (options.topN !== undefined && (!Number.isInteger(options.topN) || options.topN < 1)) {
61
+ throw new Error("query(...): semantic.topN must be a positive integer.");
62
+ }
63
+ if (options.minLexConfidence !== undefined && (!Number.isFinite(options.minLexConfidence) || options.minLexConfidence < 0 || options.minLexConfidence > 1)) {
64
+ throw new Error("query(...): semantic.minLexConfidence must be a finite number between 0 and 1.");
65
+ }
66
+ if (options.queryEmbedding !== undefined && !(options.queryEmbedding instanceof Float32Array)) {
67
+ throw new Error("query(...): semantic.queryEmbedding must be a Float32Array.");
68
+ }
69
+ if (options.blend) {
70
+ if (options.blend.enabled !== undefined && typeof options.blend.enabled !== "boolean") {
71
+ throw new Error("query(...): semantic.blend.enabled must be a boolean when provided.");
72
+ }
73
+ if (options.blend.wLex !== undefined && (!Number.isFinite(options.blend.wLex) || options.blend.wLex < 0)) {
74
+ throw new Error("query(...): semantic.blend.wLex must be a finite number >= 0.");
75
+ }
76
+ if (options.blend.wSem !== undefined && (!Number.isFinite(options.blend.wSem) || options.blend.wSem < 0)) {
77
+ throw new Error("query(...): semantic.blend.wSem must be a finite number >= 0.");
78
+ }
79
+ }
80
+ if (options.force !== undefined && typeof options.force !== "boolean") {
81
+ throw new Error("query(...): semantic.force must be a boolean when provided.");
82
+ }
83
+ }
16
84
  export function query(pack, q, opts = {}) {
85
+ validateQueryOptions(opts);
17
86
  const topK = opts.topK ?? 10;
87
+ const minScore = Number.isFinite(opts.minScore) ? Math.max(0, opts.minScore) : 0;
88
+ const expansionOpts = {
89
+ enabled: opts.queryExpansion?.enabled ?? true,
90
+ docs: Math.max(1, opts.queryExpansion?.docs ?? 3),
91
+ terms: Math.max(1, opts.queryExpansion?.terms ?? 4),
92
+ weight: Math.max(0, opts.queryExpansion?.weight ?? 0.35),
93
+ minTermLength: Math.max(2, opts.queryExpansion?.minTermLength ?? 3),
94
+ };
95
+ const semanticOpts = {
96
+ enabled: opts.semantic?.enabled ?? false,
97
+ mode: opts.semantic?.mode ?? "rerank",
98
+ topN: Math.max(1, opts.semantic?.topN ?? 50),
99
+ minLexConfidence: clamp01(opts.semantic?.minLexConfidence ?? 0.35),
100
+ blend: {
101
+ enabled: opts.semantic?.blend?.enabled ?? true,
102
+ wLex: Math.max(0, opts.semantic?.blend?.wLex ?? 0.75),
103
+ wSem: Math.max(0, opts.semantic?.blend?.wSem ?? 0.25),
104
+ },
105
+ queryEmbedding: opts.semantic?.queryEmbedding,
106
+ force: opts.semantic?.force ?? false,
107
+ };
18
108
  // --- Query parsing
19
109
  const normTokens = tokenize(q).map((t) => t.term);
20
110
  // 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));
111
+ const quotedRaw = parsePhrases(q);
112
+ const quoted = quotedRaw.map((seq) => seq.map((t) => normalize(t)).flatMap((s) => s.split(/\s+/)).filter(Boolean));
23
113
  // Normalize requirePhrases the same way
24
114
  const extraReq = (opts.requirePhrases ?? [])
25
- .map(s => tokenize(s).map(t => t.term)) // <<< normalize via tokenizer
26
- .filter(arr => arr.length > 0);
115
+ .map((s) => tokenize(s).map((t) => t.term))
116
+ .filter((arr) => arr.length > 0);
27
117
  const requiredPhrases = [...quoted, ...extraReq];
118
+ const namespaceFilter = normalizeNamespaceFilter(opts.namespace);
119
+ const sourceFilter = normalizeSourceFilter(opts.source);
28
120
  // --- Term ids for the free (unquoted) tokens in q
29
121
  const termIds = normTokens
30
122
  .map((t) => pack.lexicon.get(t))
@@ -33,39 +125,53 @@ export function query(pack, q, opts = {}) {
33
125
  const termSet = new Set(termIds);
34
126
  // --- Candidate map
35
127
  const candidates = new Map();
128
+ // Query-time document frequency collection for BM25 IDF.
129
+ const dfs = new Map();
130
+ const usesOffsetBlockIds = (pack.meta?.version ?? 1) >= 3;
36
131
  // Helper to harvest postings for a given set of termIds into candidates
37
- function scanForTermIds(idSet) {
132
+ function scanForTermIds(idWeights, cfg = { collectPositions: true, createCandidates: true }) {
38
133
  const p = pack.postings;
39
134
  let i = 0;
40
135
  while (i < p.length) {
41
136
  const tid = p[i++];
42
137
  if (tid === 0)
43
138
  continue;
44
- const relevant = idSet.has(tid);
45
- let bid = p[i++];
46
- while (bid !== 0) {
139
+ const weight = idWeights.get(tid) ?? 0;
140
+ const relevant = weight > 0;
141
+ let termDf = 0;
142
+ let encodedBid = p[i++];
143
+ while (encodedBid !== 0) {
144
+ const bid = usesOffsetBlockIds ? encodedBid - 1 : encodedBid;
47
145
  let pos = p[i++];
48
146
  const positions = [];
49
147
  while (pos !== 0) {
50
148
  positions.push(pos);
51
149
  pos = p[i++];
52
150
  }
53
- if (relevant) {
151
+ termDf++;
152
+ if (relevant && bid >= 0) {
54
153
  let entry = candidates.get(bid);
55
- if (!entry) {
154
+ if (!entry && cfg.createCandidates !== false) {
56
155
  entry = { tf: new Map(), pos: new Map() };
57
156
  candidates.set(bid, entry);
58
157
  }
59
- entry.tf.set(tid, positions.length);
60
- entry.pos.set(tid, positions);
158
+ if (entry) {
159
+ const prevTf = entry.tf.get(tid) ?? 0;
160
+ entry.tf.set(tid, prevTf + positions.length * weight);
161
+ if (cfg.collectPositions !== false) {
162
+ entry.pos.set(tid, positions);
163
+ }
164
+ }
61
165
  }
62
- bid = p[i++];
166
+ encodedBid = p[i++];
63
167
  }
168
+ if (relevant)
169
+ dfs.set(tid, termDf);
64
170
  }
65
171
  }
66
172
  // 1) Scan using tokens from q (if any)
67
173
  if (termSet.size > 0) {
68
- scanForTermIds(termSet);
174
+ scanForTermIds(new Map(Array.from(termSet.values(), (tid) => [tid, 1])));
69
175
  }
70
176
  // 2) Phrase-first rescue:
71
177
  // If nothing matched the free tokens, but we do have required phrases,
@@ -80,7 +186,27 @@ export function query(pack, q, opts = {}) {
80
186
  }
81
187
  }
82
188
  if (phraseTokenIds.size > 0) {
83
- scanForTermIds(phraseTokenIds);
189
+ scanForTermIds(new Map(Array.from(phraseTokenIds.values(), (tid) => [tid, 1])));
190
+ }
191
+ }
192
+ // --- Namespace filtering
193
+ if (namespaceFilter.size > 0) {
194
+ for (const bid of [...candidates.keys()]) {
195
+ const ns = pack.namespaces?.[bid];
196
+ const normalizedNs = typeof ns === "string" ? normalize(ns) : "";
197
+ if (!normalizedNs || !namespaceFilter.has(normalizedNs)) {
198
+ candidates.delete(bid);
199
+ }
200
+ }
201
+ }
202
+ // --- Source/docId filtering
203
+ if (sourceFilter.size > 0) {
204
+ for (const bid of [...candidates.keys()]) {
205
+ const source = pack.docIds?.[bid];
206
+ const normalizedSource = typeof source === "string" ? normalize(source) : "";
207
+ if (!normalizedSource || !sourceFilter.has(normalizedSource)) {
208
+ candidates.delete(bid);
209
+ }
84
210
  }
85
211
  }
86
212
  // --- Phrase enforcement (now that we have some candidates)
@@ -119,11 +245,30 @@ export function query(pack, q, opts = {}) {
119
245
  (pack.blocks.length
120
246
  ? pack.blocks.reduce((s, b) => s + tokenize(b).length, 0) / pack.blocks.length
121
247
  : 1);
122
- const prelim = rankBM25L(candidates, avgLen, {
248
+ const docCount = pack.meta?.stats?.blocks ?? pack.blocks.length;
249
+ let prelim = rankBM25L(candidates, avgLen, docCount, dfs, pack.blockTokenLens, {
123
250
  proximityBonus: (cand) => proximityMultiplier(minCoverSpan(cand.pos)),
124
251
  });
252
+ if (expansionOpts.enabled && prelim.length > 0) {
253
+ const expansionWeights = deriveExpansionTerms(pack, prelim, termSet, requiredPhrases, expansionOpts);
254
+ if (expansionWeights.size > 0) {
255
+ scanForTermIds(expansionWeights, { collectPositions: false, createCandidates: true });
256
+ prelim = rankBM25L(candidates, avgLen, docCount, dfs, pack.blockTokenLens, {
257
+ proximityBonus: (cand) => proximityMultiplier(minCoverSpan(cand.pos)),
258
+ });
259
+ }
260
+ }
125
261
  if (prelim.length === 0)
126
262
  return [];
263
+ if (minScore > 0) {
264
+ prelim = prelim.filter((item) => item.score >= minScore);
265
+ if (prelim.length === 0)
266
+ return [];
267
+ }
268
+ const confidence = lexConfidence(prelim);
269
+ if (shouldRerankWithSemantic(pack, semanticOpts, confidence)) {
270
+ prelim = rerankLexicalHitsWithSemantic(pack, prelim, semanticOpts);
271
+ }
127
272
  // --- KNS tie-breaker + de-dup/MMR
128
273
  const qSig = knsSignature(normalize(q));
129
274
  const pool = prelim.slice(0, topK * 5).map((r) => {
@@ -134,17 +279,158 @@ export function query(pack, q, opts = {}) {
134
279
  score: r.score * boost,
135
280
  text,
136
281
  source: pack.docIds?.[r.blockId] ?? undefined,
282
+ namespace: pack.namespaces?.[r.blockId] ?? undefined,
137
283
  };
138
284
  });
139
285
  const finalHits = diversifyAndDedupe(pool, { k: topK });
140
286
  return finalHits;
141
287
  }
288
+ export function lexConfidence(hits) {
289
+ if (hits.length === 0)
290
+ return 0;
291
+ const top1 = Math.max(0, hits[0]?.score ?? 0);
292
+ const top2 = Math.max(0, hits[1]?.score ?? 0);
293
+ const gapRatio = top1 > 0 ? clamp01((top1 - top2) / top1) : 0;
294
+ const strength = top1 > 0 ? top1 / (top1 + 1) : 0;
295
+ return clamp01(0.65 * gapRatio + 0.35 * strength);
296
+ }
297
+ function shouldRerankWithSemantic(pack, opts, confidence) {
298
+ if (!opts.enabled || opts.mode !== "rerank")
299
+ return false;
300
+ if (!pack.semantic)
301
+ return false;
302
+ if (!opts.queryEmbedding) {
303
+ throw new Error("query(...): semantic.queryEmbedding (Float32Array) is required when semantic.enabled=true.");
304
+ }
305
+ return opts.force || confidence < opts.minLexConfidence;
306
+ }
307
+ function rerankLexicalHitsWithSemantic(pack, prelim, opts) {
308
+ const sem = pack.semantic;
309
+ if (!sem || !opts.queryEmbedding)
310
+ return prelim;
311
+ if (sem.dims <= 0 || sem.vecs.length === 0 || sem.dims !== opts.queryEmbedding.length)
312
+ return prelim;
313
+ const topN = Math.min(opts.topN, prelim.length);
314
+ const rerankSlice = prelim.slice(0, topN);
315
+ const tail = prelim.slice(topN);
316
+ const lexScores = new Float64Array(topN);
317
+ for (let i = 0; i < topN; i++)
318
+ lexScores[i] = rerankSlice[i].score;
319
+ const normLex = minMaxNormalizeTyped(lexScores);
320
+ const quantizedQuery = quantizeEmbeddingInt8L2Norm(opts.queryEmbedding);
321
+ const semScores = scoreSemanticInt8(quantizedQuery.q, quantizedQuery.scale, sem, rerankSlice);
322
+ const normSem = minMaxNormalizeTyped(semScores);
323
+ const denom = opts.blend.wLex + opts.blend.wSem;
324
+ const wLex = denom > 0 ? opts.blend.wLex / denom : 0.5;
325
+ const wSem = denom > 0 ? opts.blend.wSem / denom : 0.5;
326
+ const reranked = new Array(topN);
327
+ for (let i = 0; i < topN; i++) {
328
+ const hit = rerankSlice[i];
329
+ reranked[i] = {
330
+ blockId: hit.blockId,
331
+ score: opts.blend.enabled ? wLex * normLex[i] + wSem * normSem[i] : semScores[i],
332
+ };
333
+ }
334
+ reranked.sort((a, b) => b.score - a.score || a.blockId - b.blockId);
335
+ return [...reranked, ...tail];
336
+ }
337
+ function scoreSemanticInt8(queryQ, queryScale, semantic, hits) {
338
+ const scores = new Float64Array(hits.length);
339
+ const dims = semantic.dims;
340
+ if (queryQ.length !== dims || queryScale === 0)
341
+ return scores;
342
+ const vecs = semantic.vecs;
343
+ const scales = semantic.scales;
344
+ for (let h = 0; h < hits.length; h++) {
345
+ const blockId = hits[h].blockId;
346
+ if (blockId < 0)
347
+ continue;
348
+ const base = blockId * dims;
349
+ if (base + dims > vecs.length)
350
+ continue;
351
+ let dot = 0;
352
+ for (let i = 0; i < dims; i++) {
353
+ dot += queryQ[i] * vecs[base + i];
354
+ }
355
+ const blockScale = scales?.[blockId] !== undefined ? decodeScaleF16(scales[blockId]) : (1 / 127);
356
+ scores[h] = dot * queryScale * blockScale;
357
+ }
358
+ return scores;
359
+ }
360
+ function minMaxNormalizeTyped(values) {
361
+ if (values.length === 0)
362
+ return values;
363
+ let min = Infinity;
364
+ let max = -Infinity;
365
+ for (let i = 0; i < values.length; i++) {
366
+ const v = values[i];
367
+ if (v < min)
368
+ min = v;
369
+ if (v > max)
370
+ max = v;
371
+ }
372
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
373
+ const out = new Float64Array(values.length);
374
+ out.fill(1);
375
+ return out;
376
+ }
377
+ const out = new Float64Array(values.length);
378
+ const span = max - min;
379
+ for (let i = 0; i < values.length; i++) {
380
+ out[i] = clamp01((values[i] - min) / span);
381
+ }
382
+ return out;
383
+ }
384
+ function clamp01(v) {
385
+ if (!Number.isFinite(v))
386
+ return 0;
387
+ if (v < 0)
388
+ return 0;
389
+ if (v > 1)
390
+ return 1;
391
+ return v;
392
+ }
393
+ function deriveExpansionTerms(pack, prelim, baseTermSet, requiredPhrases, opts) {
394
+ if (prelim.length === 0 || opts.weight <= 0)
395
+ return new Map();
396
+ const forbidden = new Set(baseTermSet);
397
+ for (const seq of requiredPhrases) {
398
+ for (const term of seq) {
399
+ const tid = pack.lexicon.get(term);
400
+ if (tid !== undefined)
401
+ forbidden.add(tid);
402
+ }
403
+ }
404
+ const cap = Math.min(opts.docs, prelim.length);
405
+ const bestScore = Math.max(prelim[0]?.score ?? 0, 1e-6);
406
+ const termScores = new Map();
407
+ for (let i = 0; i < cap; i++) {
408
+ const item = prelim[i];
409
+ const text = pack.blocks[item.blockId] ?? "";
410
+ const docWeight = Math.max(item.score / bestScore, 0.2);
411
+ const localTfs = new Map();
412
+ for (const tok of tokenize(text)) {
413
+ if (tok.term.length < opts.minTermLength)
414
+ continue;
415
+ const tid = pack.lexicon.get(tok.term);
416
+ if (tid === undefined || forbidden.has(tid))
417
+ continue;
418
+ localTfs.set(tid, (localTfs.get(tid) ?? 0) + 1);
419
+ }
420
+ for (const [tid, tf] of localTfs) {
421
+ termScores.set(tid, (termScores.get(tid) ?? 0) + tf * docWeight);
422
+ }
423
+ }
424
+ const selected = [...termScores.entries()]
425
+ .sort((a, b) => b[1] - a[1])
426
+ .slice(0, opts.terms);
427
+ return new Map(selected.map(([tid, score]) => [tid, opts.weight * Math.max(0.5, Math.min(1.5, score))]));
428
+ }
142
429
  /** Ordered phrase check using the SAME tokenizer/normalizer path as the index. */
143
430
  function containsPhrase(text, seq) {
144
431
  if (seq.length === 0)
145
432
  return false;
146
- // normalize seq via tokenizer to be extra safe (handles diacritics/case)
147
- const seqNorm = tokenize(seq.join(" ")).map(t => t.term);
433
+ const seqNorm = tokenize(seq.join(" ")).map((t) => t.term);
148
434
  const toks = tokenize(text).map((t) => t.term);
149
435
  outer: for (let i = 0; i <= toks.length - seqNorm.length; i++) {
150
436
  for (let j = 0; j < seqNorm.length; j++) {
@@ -155,3 +441,23 @@ function containsPhrase(text, seq) {
155
441
  }
156
442
  return false;
157
443
  }
444
+ function normalizeNamespaceFilter(input) {
445
+ if (input === undefined)
446
+ return new Set();
447
+ const values = Array.isArray(input) ? input : [input];
448
+ return new Set(values.map((v) => normalize(v)).filter(Boolean));
449
+ }
450
+ function normalizeSourceFilter(input) {
451
+ if (input === undefined)
452
+ return new Set();
453
+ const values = Array.isArray(input) ? input : [input];
454
+ return new Set(values.map((v) => normalize(v)).filter(Boolean));
455
+ }
456
+ function validateStringOrStringArrayOption(name, value) {
457
+ if (value === undefined)
458
+ return;
459
+ const valid = typeof value === "string" || (Array.isArray(value) && value.every((entry) => typeof entry === "string"));
460
+ if (!valid) {
461
+ throw new Error(`query(...): ${name} must be a string or an array of strings when provided.`);
462
+ }
463
+ }
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": "3.1.0",
4
4
  "type": "module",
5
5
  "description": "Local-first knowledge packs for small LLMs.",
6
6
  "keywords": [
@@ -33,7 +33,10 @@
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",
38
+ "format": "prettier --write src/agent.ts src/pack.ts src/builder.ts src/index.ts scripts/test.mjs README.md",
39
+ "format:check": "prettier --check src/agent.ts src/pack.ts src/builder.ts src/index.ts scripts/test.mjs README.md"
37
40
  },
38
41
  "devDependencies": {
39
42
  "typescript": "^5.5.0",