knolo-core 0.1.4 → 0.2.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/DOCS.md +355 -0
- package/LICENSE +20 -17
- package/README.md +196 -61
- package/dist/builder.d.ts +0 -5
- package/dist/builder.js +31 -28
- package/dist/pack.d.ts +3 -29
- package/dist/pack.js +43 -37
- package/dist/quality/diversify.d.ts +13 -0
- package/dist/quality/diversify.js +41 -0
- package/dist/quality/proximity.d.ts +2 -0
- package/dist/quality/proximity.js +31 -0
- package/dist/quality/signature.d.ts +3 -0
- package/dist/quality/signature.js +24 -0
- package/dist/quality/similarity.d.ts +3 -0
- package/dist/quality/similarity.js +27 -0
- package/dist/query.d.ts +1 -7
- package/dist/query.js +129 -70
- package/dist/rank.d.ts +7 -6
- package/dist/rank.js +6 -19
- package/dist/utils/utf8.d.ts +8 -0
- package/dist/utils/utf8.js +72 -0
- package/package.json +22 -7
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// src/quality/proximity.ts
|
|
2
|
+
// Map<termId, positions[]>
|
|
3
|
+
export function minCoverSpan(posMap) {
|
|
4
|
+
const lists = posMap ? [...posMap.values()].map(arr => arr.slice().sort((a, b) => a - b)) : [];
|
|
5
|
+
if (lists.length === 0)
|
|
6
|
+
return null;
|
|
7
|
+
const idx = new Array(lists.length).fill(0);
|
|
8
|
+
let best = null;
|
|
9
|
+
while (true) {
|
|
10
|
+
const cur = [];
|
|
11
|
+
for (let i = 0; i < lists.length; i++) {
|
|
12
|
+
const val = lists[i][idx[i]];
|
|
13
|
+
if (val === undefined)
|
|
14
|
+
return best;
|
|
15
|
+
cur.push(val);
|
|
16
|
+
}
|
|
17
|
+
const min = Math.min(...cur);
|
|
18
|
+
const max = Math.max(...cur);
|
|
19
|
+
const span = max - min;
|
|
20
|
+
if (best === null || span < best)
|
|
21
|
+
best = span;
|
|
22
|
+
// advance list with current min
|
|
23
|
+
const minList = cur.indexOf(min);
|
|
24
|
+
idx[minList]++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function proximityMultiplier(span, strength = 0.15) {
|
|
28
|
+
if (span === null)
|
|
29
|
+
return 1;
|
|
30
|
+
return 1 + strength / (1 + span); // gentle, bounded
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/quality/signature.ts
|
|
2
|
+
// "KNS" — simple, deterministic lexical numeric signature for tie-breaking.
|
|
3
|
+
const PRIMES = [257, 263, 269];
|
|
4
|
+
export function knsSignature(s) {
|
|
5
|
+
let s1 = 0, s2 = 0, s3 = 0;
|
|
6
|
+
for (let i = 0; i < s.length; i++) {
|
|
7
|
+
const code = s.charCodeAt(i);
|
|
8
|
+
s1 = (s1 + code) % PRIMES[0];
|
|
9
|
+
s2 = (s2 + code * (i + 1)) % PRIMES[1];
|
|
10
|
+
s3 = (s3 + ((code << 1) ^ (i + 7))) % PRIMES[2];
|
|
11
|
+
}
|
|
12
|
+
return [s1, s2, s3];
|
|
13
|
+
}
|
|
14
|
+
export function knsDistance(a, b) {
|
|
15
|
+
// circular distance on a mod prime, averaged & normalized to 0..1
|
|
16
|
+
let acc = 0;
|
|
17
|
+
for (let i = 0; i < PRIMES.length; i++) {
|
|
18
|
+
const p = PRIMES[i];
|
|
19
|
+
const diff = Math.abs(a[i] - b[i]);
|
|
20
|
+
const circ = Math.min(diff, p - diff) / p;
|
|
21
|
+
acc += circ;
|
|
22
|
+
}
|
|
23
|
+
return acc / PRIMES.length;
|
|
24
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/quality/similarity.ts
|
|
2
|
+
import { normalize } from '../tokenize.js';
|
|
3
|
+
export function ngramSet(s, n = 5) {
|
|
4
|
+
const t = normalize(s);
|
|
5
|
+
const out = new Set();
|
|
6
|
+
if (t.length < n) {
|
|
7
|
+
if (t)
|
|
8
|
+
out.add(t);
|
|
9
|
+
return out;
|
|
10
|
+
}
|
|
11
|
+
for (let i = 0; i <= t.length - n; i++)
|
|
12
|
+
out.add(t.slice(i, i + n));
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
export function jaccardFromSets(a, b) {
|
|
16
|
+
if (a.size === 0 && b.size === 0)
|
|
17
|
+
return 1;
|
|
18
|
+
let inter = 0;
|
|
19
|
+
for (const x of a)
|
|
20
|
+
if (b.has(x))
|
|
21
|
+
inter++;
|
|
22
|
+
const uni = a.size + b.size - inter;
|
|
23
|
+
return uni ? inter / uni : 0;
|
|
24
|
+
}
|
|
25
|
+
export function jaccard5(s1, s2) {
|
|
26
|
+
return jaccardFromSets(ngramSet(s1, 5), ngramSet(s2, 5));
|
|
27
|
+
}
|
package/dist/query.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { Pack } from
|
|
1
|
+
import type { Pack } from "./pack.js";
|
|
2
2
|
export type QueryOptions = {
|
|
3
3
|
topK?: number;
|
|
4
|
-
/** Additional phrases (unquoted) that must be present in results. */
|
|
5
4
|
requirePhrases?: string[];
|
|
6
5
|
};
|
|
7
6
|
export type Hit = {
|
|
@@ -10,9 +9,4 @@ export type Hit = {
|
|
|
10
9
|
text: string;
|
|
11
10
|
source?: string;
|
|
12
11
|
};
|
|
13
|
-
/** Execute a search against a mounted pack. The query string can contain
|
|
14
|
-
* quoted phrases; unquoted terms are treated individually. The `topK`
|
|
15
|
-
* parameter controls how many results are returned. If `requirePhrases`
|
|
16
|
-
* contains strings, those phrases must appear verbatim in candidate blocks.
|
|
17
|
-
*/
|
|
18
12
|
export declare function query(pack: Pack, q: string, opts?: QueryOptions): Hit[];
|
package/dist/query.js
CHANGED
|
@@ -1,95 +1,154 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* query.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Deterministic, embedding-free retrieval with:
|
|
5
|
+
* - REQUIRED phrase enforcement (quoted and requirePhrases)
|
|
6
|
+
* - Proximity bonus based on min cover span
|
|
7
|
+
* - Optional heading overlap boost
|
|
8
|
+
* - KNS numeric-signature tie-breaker (tiny)
|
|
9
|
+
* - Near-duplicate suppression + MMR diversity
|
|
9
10
|
*/
|
|
10
|
-
import { tokenize, parsePhrases } from "./tokenize.js";
|
|
11
|
+
import { tokenize, parsePhrases, normalize } from "./tokenize.js";
|
|
11
12
|
import { rankBM25L } from "./rank.js";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* contains strings, those phrases must appear verbatim in candidate blocks.
|
|
16
|
-
*/
|
|
13
|
+
import { minCoverSpan, proximityMultiplier } from "./quality/proximity.js";
|
|
14
|
+
import { diversifyAndDedupe } from "./quality/diversify.js";
|
|
15
|
+
import { knsSignature, knsDistance } from "./quality/signature.js";
|
|
17
16
|
export function query(pack, q, opts = {}) {
|
|
18
17
|
const topK = opts.topK ?? 10;
|
|
18
|
+
// --- Query parsing
|
|
19
19
|
const normTokens = tokenize(q).map((t) => t.term);
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// Translate tokens to term IDs. Terms not in the lexicon are skipped.
|
|
20
|
+
// 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));
|
|
23
|
+
// Normalize requirePhrases the same way
|
|
24
|
+
const extraReq = (opts.requirePhrases ?? [])
|
|
25
|
+
.map(s => tokenize(s).map(t => t.term)) // <<< normalize via tokenizer
|
|
26
|
+
.filter(arr => arr.length > 0);
|
|
27
|
+
const requiredPhrases = [...quoted, ...extraReq];
|
|
28
|
+
// --- Term ids for the free (unquoted) tokens in q
|
|
30
29
|
const termIds = normTokens
|
|
31
30
|
.map((t) => pack.lexicon.get(t))
|
|
32
31
|
.filter((id) => id !== undefined);
|
|
33
|
-
//
|
|
32
|
+
// If there are no free tokens but there ARE required phrases, we'll fill candidates from phrases later.
|
|
33
|
+
const termSet = new Set(termIds);
|
|
34
|
+
// --- Candidate map
|
|
34
35
|
const candidates = new Map();
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (relevant) {
|
|
52
|
-
let entry = candidates.get(bid);
|
|
53
|
-
if (!entry) {
|
|
54
|
-
entry = { tf: new Map() };
|
|
55
|
-
candidates.set(bid, entry);
|
|
36
|
+
// Helper to harvest postings for a given set of termIds into candidates
|
|
37
|
+
function scanForTermIds(idSet) {
|
|
38
|
+
const p = pack.postings;
|
|
39
|
+
let i = 0;
|
|
40
|
+
while (i < p.length) {
|
|
41
|
+
const tid = p[i++];
|
|
42
|
+
if (tid === 0)
|
|
43
|
+
continue;
|
|
44
|
+
const relevant = idSet.has(tid);
|
|
45
|
+
let bid = p[i++];
|
|
46
|
+
while (bid !== 0) {
|
|
47
|
+
let pos = p[i++];
|
|
48
|
+
const positions = [];
|
|
49
|
+
while (pos !== 0) {
|
|
50
|
+
positions.push(pos);
|
|
51
|
+
pos = p[i++];
|
|
56
52
|
}
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
if (relevant) {
|
|
54
|
+
let entry = candidates.get(bid);
|
|
55
|
+
if (!entry) {
|
|
56
|
+
entry = { tf: new Map(), pos: new Map() };
|
|
57
|
+
candidates.set(bid, entry);
|
|
58
|
+
}
|
|
59
|
+
entry.tf.set(tid, positions.length);
|
|
60
|
+
entry.pos.set(tid, positions);
|
|
61
|
+
}
|
|
62
|
+
bid = p[i++];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// 1) Scan using tokens from q (if any)
|
|
67
|
+
if (termSet.size > 0) {
|
|
68
|
+
scanForTermIds(termSet);
|
|
69
|
+
}
|
|
70
|
+
// 2) Phrase-first rescue:
|
|
71
|
+
// If nothing matched the free tokens, but we do have required phrases,
|
|
72
|
+
// build a fallback term set from ALL tokens that appear in those phrases and scan again.
|
|
73
|
+
if (candidates.size === 0 && requiredPhrases.length > 0) {
|
|
74
|
+
const phraseTokenIds = new Set();
|
|
75
|
+
for (const seq of requiredPhrases) {
|
|
76
|
+
for (const t of seq) {
|
|
77
|
+
const id = pack.lexicon.get(t);
|
|
78
|
+
if (id !== undefined)
|
|
79
|
+
phraseTokenIds.add(id);
|
|
59
80
|
}
|
|
60
|
-
bid = p[i++];
|
|
61
81
|
}
|
|
62
|
-
|
|
82
|
+
if (phraseTokenIds.size > 0) {
|
|
83
|
+
scanForTermIds(phraseTokenIds);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// --- Phrase enforcement (now that we have some candidates)
|
|
87
|
+
if (requiredPhrases.length > 0) {
|
|
88
|
+
for (const [bid, data] of [...candidates]) {
|
|
89
|
+
const text = pack.blocks[bid] || "";
|
|
90
|
+
const ok = requiredPhrases.every((seq) => containsPhrase(text, seq));
|
|
91
|
+
if (!ok)
|
|
92
|
+
candidates.delete(bid);
|
|
93
|
+
else
|
|
94
|
+
data.hasPhrase = true;
|
|
95
|
+
}
|
|
63
96
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
97
|
+
else if (quoted.length > 0) {
|
|
98
|
+
for (const [bid, data] of candidates) {
|
|
99
|
+
const text = pack.blocks[bid] || "";
|
|
100
|
+
data.hasPhrase = quoted.some((seq) => containsPhrase(text, seq));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// If still nothing, bail early
|
|
104
|
+
if (candidates.size === 0)
|
|
105
|
+
return [];
|
|
106
|
+
// --- Heading overlap
|
|
107
|
+
if (pack.headings?.length) {
|
|
108
|
+
const qset = new Set(normTokens);
|
|
109
|
+
const qUniqueCount = new Set(normTokens).size || 1;
|
|
110
|
+
for (const [bid, data] of candidates) {
|
|
111
|
+
const h = pack.headings[bid] ?? "";
|
|
112
|
+
const hTerms = tokenize(h || "").map((t) => t.term);
|
|
113
|
+
const overlap = new Set(hTerms.filter((t) => qset.has(t))).size;
|
|
114
|
+
data.headingScore = overlap / qUniqueCount;
|
|
115
|
+
}
|
|
69
116
|
}
|
|
70
|
-
//
|
|
71
|
-
const avgLen = pack.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
// --- Rank with proximity bonus
|
|
118
|
+
const avgLen = pack.meta?.stats?.avgBlockLen ??
|
|
119
|
+
(pack.blocks.length
|
|
120
|
+
? pack.blocks.reduce((s, b) => s + tokenize(b).length, 0) / pack.blocks.length
|
|
121
|
+
: 1);
|
|
122
|
+
const prelim = rankBM25L(candidates, avgLen, {
|
|
123
|
+
proximityBonus: (cand) => proximityMultiplier(minCoverSpan(cand.pos)),
|
|
124
|
+
});
|
|
125
|
+
if (prelim.length === 0)
|
|
126
|
+
return [];
|
|
127
|
+
// --- KNS tie-breaker + de-dup/MMR
|
|
128
|
+
const qSig = knsSignature(normalize(q));
|
|
129
|
+
const pool = prelim.slice(0, topK * 5).map((r) => {
|
|
130
|
+
const text = pack.blocks[r.blockId] || "";
|
|
131
|
+
const boost = 1 + 0.02 * (1 - knsDistance(qSig, knsSignature(text)));
|
|
132
|
+
return {
|
|
133
|
+
blockId: r.blockId,
|
|
134
|
+
score: r.score * boost,
|
|
135
|
+
text,
|
|
136
|
+
source: pack.docIds?.[r.blockId] ?? undefined,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
const finalHits = diversifyAndDedupe(pool, { k: topK });
|
|
140
|
+
return finalHits;
|
|
80
141
|
}
|
|
81
|
-
/**
|
|
82
|
-
* text. The algorithm tokenizes the text and performs a sliding window
|
|
83
|
-
* comparison. This is case‑insensitive and uses the same normalization as
|
|
84
|
-
* other parts of the system.
|
|
85
|
-
*/
|
|
142
|
+
/** Ordered phrase check using the SAME tokenizer/normalizer path as the index. */
|
|
86
143
|
function containsPhrase(text, seq) {
|
|
87
144
|
if (seq.length === 0)
|
|
88
145
|
return false;
|
|
146
|
+
// normalize seq via tokenizer to be extra safe (handles diacritics/case)
|
|
147
|
+
const seqNorm = tokenize(seq.join(" ")).map(t => t.term);
|
|
89
148
|
const toks = tokenize(text).map((t) => t.term);
|
|
90
|
-
outer: for (let i = 0; i <= toks.length -
|
|
91
|
-
for (let j = 0; j <
|
|
92
|
-
if (toks[i + j] !==
|
|
149
|
+
outer: for (let i = 0; i <= toks.length - seqNorm.length; i++) {
|
|
150
|
+
for (let j = 0; j < seqNorm.length; j++) {
|
|
151
|
+
if (toks[i + j] !== seqNorm[j])
|
|
93
152
|
continue outer;
|
|
94
153
|
}
|
|
95
154
|
return true;
|
package/dist/rank.d.ts
CHANGED
|
@@ -3,15 +3,16 @@ export type RankOptions = {
|
|
|
3
3
|
b?: number;
|
|
4
4
|
headingBoost?: number;
|
|
5
5
|
phraseBoost?: number;
|
|
6
|
+
proximityBonus?: (cand: {
|
|
7
|
+
tf: Map<number, number>;
|
|
8
|
+
pos?: Map<number, number[]>;
|
|
9
|
+
hasPhrase?: boolean;
|
|
10
|
+
headingScore?: number;
|
|
11
|
+
}) => number;
|
|
6
12
|
};
|
|
7
|
-
/**
|
|
8
|
-
* Rank a set of candidate blocks using BM25L. Each candidate carries a term
|
|
9
|
-
* frequency (tf) map keyed by termId. Additional properties may include
|
|
10
|
-
* `hasPhrase` and `headingScore` to apply multiplicative boosts. The average
|
|
11
|
-
* document length (avgLen) is required for BM25L normalization.
|
|
12
|
-
*/
|
|
13
13
|
export declare function rankBM25L(candidates: Map<number, {
|
|
14
14
|
tf: Map<number, number>;
|
|
15
|
+
pos?: Map<number, number[]>;
|
|
15
16
|
hasPhrase?: boolean;
|
|
16
17
|
headingScore?: number;
|
|
17
18
|
}>, avgLen: number, opts?: RankOptions): Array<{
|
package/dist/rank.js
CHANGED
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* rank.ts
|
|
3
|
-
*
|
|
4
|
-
* Implements a simple BM25L ranker with optional boosts for headings and
|
|
5
|
-
* phrase matches. The inputs to the ranker are a map of block IDs to term
|
|
6
|
-
* frequency maps and additional boolean flags. The outputs are sorted by
|
|
7
|
-
* descending relevance score. This ranking algorithm can be replaced or
|
|
8
|
-
* augmented in the future without impacting the public API of the query
|
|
9
|
-
* function.
|
|
10
|
-
*/
|
|
11
|
-
/**
|
|
12
|
-
* Rank a set of candidate blocks using BM25L. Each candidate carries a term
|
|
13
|
-
* frequency (tf) map keyed by termId. Additional properties may include
|
|
14
|
-
* `hasPhrase` and `headingScore` to apply multiplicative boosts. The average
|
|
15
|
-
* document length (avgLen) is required for BM25L normalization.
|
|
3
|
+
* BM25L ranker with optional heading/phrase boosts and a proximity bonus hook.
|
|
16
4
|
*/
|
|
17
5
|
export function rankBM25L(candidates, avgLen, opts = {}) {
|
|
18
6
|
const k1 = opts.k1 ?? 1.5;
|
|
@@ -24,18 +12,17 @@ export function rankBM25L(candidates, avgLen, opts = {}) {
|
|
|
24
12
|
const len = Array.from(data.tf.values()).reduce((sum, tf) => sum + tf, 0) || 1;
|
|
25
13
|
let score = 0;
|
|
26
14
|
for (const [, tf] of data.tf) {
|
|
27
|
-
const idf = 1; //
|
|
15
|
+
const idf = 1; // v0: no DF; can be extended later
|
|
28
16
|
const numer = tf * (k1 + 1);
|
|
29
17
|
const denom = tf + k1 * (1 - b + b * (len / avgLen));
|
|
30
18
|
score += idf * (numer / denom);
|
|
31
19
|
}
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
if (opts.proximityBonus)
|
|
21
|
+
score *= opts.proximityBonus(data) ?? 1;
|
|
22
|
+
if (data.hasPhrase)
|
|
34
23
|
score *= 1 + phraseBoost;
|
|
35
|
-
|
|
36
|
-
if (data.headingScore) {
|
|
24
|
+
if (data.headingScore)
|
|
37
25
|
score *= 1 + headingBoost * data.headingScore;
|
|
38
|
-
}
|
|
39
26
|
results.push({ blockId: bid, score });
|
|
40
27
|
}
|
|
41
28
|
results.sort((a, b2) => b2.score - a.score);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type TextDecoderLike = {
|
|
2
|
+
decode: (u8: Uint8Array) => string;
|
|
3
|
+
};
|
|
4
|
+
export type TextEncoderLike = {
|
|
5
|
+
encode: (s: string) => Uint8Array;
|
|
6
|
+
};
|
|
7
|
+
export declare function getTextDecoder(): TextDecoderLike;
|
|
8
|
+
export declare function getTextEncoder(): TextEncoderLike;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/utils/utf8.ts
|
|
2
|
+
// Small, dependency-free UTF-8 encoder/decoder that works in RN/Hermes.
|
|
3
|
+
export function getTextDecoder() {
|
|
4
|
+
try {
|
|
5
|
+
// eslint-disable-next-line no-new
|
|
6
|
+
const td = new TextDecoder();
|
|
7
|
+
return td;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return {
|
|
11
|
+
decode: (u8) => {
|
|
12
|
+
let out = '';
|
|
13
|
+
for (let i = 0; i < u8.length;) {
|
|
14
|
+
const a = u8[i++];
|
|
15
|
+
if (a < 0x80) {
|
|
16
|
+
out += String.fromCharCode(a);
|
|
17
|
+
}
|
|
18
|
+
else if ((a & 0xe0) === 0xc0) {
|
|
19
|
+
const b = u8[i++] & 0x3f;
|
|
20
|
+
const cp = ((a & 0x1f) << 6) | b;
|
|
21
|
+
out += String.fromCharCode(cp);
|
|
22
|
+
}
|
|
23
|
+
else if ((a & 0xf0) === 0xe0) {
|
|
24
|
+
const b = u8[i++] & 0x3f;
|
|
25
|
+
const c = u8[i++] & 0x3f;
|
|
26
|
+
const cp = ((a & 0x0f) << 12) | (b << 6) | c;
|
|
27
|
+
out += String.fromCharCode(cp);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const b = u8[i++] & 0x3f;
|
|
31
|
+
const c = u8[i++] & 0x3f;
|
|
32
|
+
const d = u8[i++] & 0x3f;
|
|
33
|
+
let cp = ((a & 0x07) << 18) | (b << 12) | (c << 6) | d;
|
|
34
|
+
cp -= 0x10000;
|
|
35
|
+
out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function getTextEncoder() {
|
|
44
|
+
try {
|
|
45
|
+
// eslint-disable-next-line no-new
|
|
46
|
+
const te = new TextEncoder();
|
|
47
|
+
return te;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return {
|
|
51
|
+
encode: (s) => {
|
|
52
|
+
const out = [];
|
|
53
|
+
for (let i = 0; i < s.length; i++) {
|
|
54
|
+
let cp = s.charCodeAt(i);
|
|
55
|
+
if (cp >= 0xd800 && cp <= 0xdbff && i + 1 < s.length) {
|
|
56
|
+
const next = s.charCodeAt(++i);
|
|
57
|
+
cp = 0x10000 + ((cp - 0xd800) << 10) + (next - 0xdc00);
|
|
58
|
+
}
|
|
59
|
+
if (cp < 0x80)
|
|
60
|
+
out.push(cp);
|
|
61
|
+
else if (cp < 0x800)
|
|
62
|
+
out.push(0xc0 | (cp >> 6), 0x80 | (cp & 0x3f));
|
|
63
|
+
else if (cp < 0x10000)
|
|
64
|
+
out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
|
|
65
|
+
else
|
|
66
|
+
out.push(0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
|
|
67
|
+
}
|
|
68
|
+
return new Uint8Array(out);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knolo-core",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local-first knowledge packs for small LLMs.",
|
|
6
|
-
"keywords": [
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
"keywords": [
|
|
7
|
+
"llm",
|
|
8
|
+
"knowledge-base",
|
|
9
|
+
"rag",
|
|
10
|
+
"local",
|
|
11
|
+
"expo"
|
|
12
|
+
],
|
|
13
|
+
"author": "Sam Paniagua",
|
|
14
|
+
"license": "Apache-2.0",
|
|
9
15
|
"main": "./dist/index.js",
|
|
10
16
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"files": [
|
|
12
|
-
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"bin",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"DOCS.md"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"knolo": "./bin/knolo.mjs"
|
|
26
|
+
},
|
|
13
27
|
"exports": {
|
|
14
28
|
".": {
|
|
15
29
|
"import": "./dist/index.js",
|
|
@@ -18,7 +32,8 @@
|
|
|
18
32
|
},
|
|
19
33
|
"scripts": {
|
|
20
34
|
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"prepublishOnly": "npm run build"
|
|
35
|
+
"prepublishOnly": "npm run build",
|
|
36
|
+
"smoke": "node scripts/smoke.mjs"
|
|
22
37
|
},
|
|
23
38
|
"devDependencies": {
|
|
24
39
|
"typescript": "^5.5.0",
|