knolo-core 0.2.2 → 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/bin/knolo.mjs CHANGED
@@ -10,12 +10,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
10
  const require = createRequire(import.meta.url);
11
11
 
12
12
  async function tryImport(filePath) {
13
- // 1) ESM via file URL
14
13
  try {
15
14
  const url = pathToFileURL(filePath).href;
16
15
  return await import(url);
17
16
  } catch (_) {}
18
- // 2) CJS via require
19
17
  try {
20
18
  return require(filePath);
21
19
  } catch (_) {}
@@ -24,14 +22,11 @@ async function tryImport(filePath) {
24
22
 
25
23
  function getBuildPack(mod) {
26
24
  if (!mod) return undefined;
27
- // Named export (ESM)
28
25
  if (typeof mod.buildPack === "function") return mod.buildPack;
29
- // CJS default export object: { buildPack } or function
30
26
  if (mod.default) {
31
27
  if (typeof mod.default === "function") return mod.default;
32
28
  if (typeof mod.default.buildPack === "function") return mod.default.buildPack;
33
29
  }
34
- // Some CJS setups export { buildPack } directly
35
30
  if (typeof mod === "function") return mod;
36
31
  if (typeof mod.buildPack === "function") return mod.buildPack;
37
32
  return undefined;
@@ -41,7 +36,6 @@ async function loadBuildPack() {
41
36
  const candidates = [
42
37
  path.resolve(__dirname, "../dist/index.js"),
43
38
  path.resolve(__dirname, "../dist/builder.js"),
44
- // Also try .cjs just in case someone built CJS
45
39
  path.resolve(__dirname, "../dist/index.cjs"),
46
40
  path.resolve(__dirname, "../dist/builder.cjs"),
47
41
  ];
@@ -53,17 +47,130 @@ async function loadBuildPack() {
53
47
  throw new Error("Could not locate a buildPack function in dist/");
54
48
  }
55
49
 
50
+ function validateCliDocs(raw) {
51
+ if (!Array.isArray(raw)) {
52
+ throw new Error('Input JSON must be an array of docs: [{ "text": "...", "id"?: "...", "heading"?: "..." }]');
53
+ }
54
+ for (let i = 0; i < raw.length; i++) {
55
+ const doc = raw[i];
56
+ if (!doc || typeof doc !== "object") {
57
+ throw new Error(`Invalid doc at index ${i}: expected an object.`);
58
+ }
59
+ if (typeof doc.text !== "string" || !doc.text.trim()) {
60
+ throw new Error(`Invalid doc at index ${i}: "text" must be a non-empty string.`);
61
+ }
62
+ }
63
+ return raw;
64
+ }
65
+
66
+ function parseArgs(argv) {
67
+ const positional = [];
68
+ const flags = { embeddingsPath: undefined, modelId: undefined };
69
+
70
+ for (let i = 0; i < argv.length; i++) {
71
+ const arg = argv[i];
72
+ if (!arg.startsWith("--")) {
73
+ positional.push(arg);
74
+ continue;
75
+ }
76
+ if (arg === "--embeddings") {
77
+ flags.embeddingsPath = argv[++i];
78
+ continue;
79
+ }
80
+ if (arg === "--model-id") {
81
+ flags.modelId = argv[++i];
82
+ continue;
83
+ }
84
+ if (arg === "--help" || arg === "-h") {
85
+ flags.help = true;
86
+ continue;
87
+ }
88
+ throw new Error(`Unknown flag: ${arg}`);
89
+ }
90
+ return { positional, flags };
91
+ }
92
+
93
+ function loadEmbeddingsFromJson(filePath, expectedCount) {
94
+ const parsed = JSON.parse(readFileSync(filePath, "utf8"));
95
+ const vectors = Array.isArray(parsed?.embeddings) ? parsed.embeddings : parsed;
96
+ if (!Array.isArray(vectors)) {
97
+ throw new Error('Embeddings JSON must be either an array of vectors or { "embeddings": [...] }.');
98
+ }
99
+ if (vectors.length !== expectedCount) {
100
+ throw new Error(`Embeddings length mismatch: expected ${expectedCount}, got ${vectors.length}.`);
101
+ }
102
+
103
+ const first = vectors[0];
104
+ if (!Array.isArray(first) || first.length === 0) {
105
+ throw new Error("Embeddings must contain non-empty numeric vectors.");
106
+ }
107
+ const dims = first.length;
108
+
109
+ return vectors.map((entry, i) => {
110
+ if (!Array.isArray(entry)) {
111
+ throw new Error(`Embeddings[${i}] must be an array of numbers.`);
112
+ }
113
+ if (entry.length !== dims) {
114
+ throw new Error(`Embeddings[${i}] has dims ${entry.length}, expected ${dims}.`);
115
+ }
116
+ const vec = new Float32Array(dims);
117
+ for (let d = 0; d < dims; d++) {
118
+ const value = entry[d];
119
+ if (!Number.isFinite(value)) {
120
+ throw new Error(`Embeddings[${i}][${d}] must be a finite number.`);
121
+ }
122
+ vec[d] = value;
123
+ }
124
+ return vec;
125
+ });
126
+ }
127
+
128
+ function printUsage() {
129
+ console.log("Usage: knolo <input.json> [output.knolo] [--embeddings embeddings.json --model-id model-name]");
130
+ }
131
+
56
132
  const buildPack = await loadBuildPack();
133
+ const { positional, flags } = parseArgs(process.argv.slice(2));
134
+
135
+ if (flags.help) {
136
+ printUsage();
137
+ process.exit(0);
138
+ }
57
139
 
58
- const inFile = process.argv[2];
59
- const outFile = process.argv[3] || "knowledge.knolo";
140
+ const inFile = positional[0];
141
+ const outFile = positional[1] || "knowledge.knolo";
60
142
 
61
143
  if (!inFile) {
62
- console.log("Usage: knolo <input.json> [output.knolo]");
144
+ printUsage();
63
145
  process.exit(1);
64
146
  }
65
147
 
66
- const docs = JSON.parse(readFileSync(inFile, "utf8"));
67
- const bytes = await buildPack(docs);
68
- writeFileSync(outFile, Buffer.from(bytes));
69
- console.log(`wrote ${outFile}`);
148
+ try {
149
+ const rawText = readFileSync(inFile, "utf8");
150
+ const parsed = JSON.parse(rawText);
151
+ const docs = validateCliDocs(parsed);
152
+
153
+ let options;
154
+ if (flags.embeddingsPath || flags.modelId) {
155
+ if (!flags.embeddingsPath || !flags.modelId) {
156
+ throw new Error("Both --embeddings and --model-id are required when enabling semantic build output.");
157
+ }
158
+ const embeddings = loadEmbeddingsFromJson(flags.embeddingsPath, docs.length);
159
+ options = {
160
+ semantic: {
161
+ enabled: true,
162
+ modelId: flags.modelId,
163
+ embeddings,
164
+ quantization: { type: "int8_l2norm", perVectorScale: true },
165
+ },
166
+ };
167
+ }
168
+
169
+ const bytes = await buildPack(docs, options);
170
+ writeFileSync(outFile, Buffer.from(bytes));
171
+ console.log(`wrote ${outFile}`);
172
+ } catch (err) {
173
+ const message = err instanceof Error ? err.message : String(err);
174
+ console.error(`knolo: ${message}`);
175
+ process.exit(1);
176
+ }
package/dist/builder.d.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  export type BuildInputDoc = {
2
2
  id?: string;
3
3
  heading?: string;
4
+ namespace?: string;
4
5
  text: string;
5
6
  };
6
- export declare function buildPack(docs: BuildInputDoc[]): Promise<Uint8Array>;
7
+ export type BuildPackOptions = {
8
+ semantic?: {
9
+ enabled: boolean;
10
+ modelId: string;
11
+ embeddings: Float32Array[];
12
+ quantization?: {
13
+ type: 'int8_l2norm';
14
+ perVectorScale?: true;
15
+ };
16
+ };
17
+ };
18
+ export declare function buildPack(docs: BuildInputDoc[], opts?: BuildPackOptions): Promise<Uint8Array>;
package/dist/builder.js CHANGED
@@ -1,48 +1,61 @@
1
1
  /*
2
2
  * builder.ts
3
3
  *
4
- * Build `.knolo` packs from input docs. Now persists optional headings/docIds
5
- * and stores avgBlockLen in meta for faster/easier normalization at query-time.
4
+ * Build `.knolo` packs from input docs. Persists headings/docIds/token lengths
5
+ * and stores avgBlockLen in meta for stable query-time normalization.
6
6
  */
7
7
  import { buildIndex } from './indexer.js';
8
8
  import { tokenize } from './tokenize.js';
9
9
  import { getTextEncoder } from './utils/utf8.js';
10
- export async function buildPack(docs) {
10
+ import { encodeScaleF16, quantizeEmbeddingInt8L2Norm } from './semantic.js';
11
+ export async function buildPack(docs, opts = {}) {
12
+ const normalizedDocs = validateDocs(docs);
11
13
  // Prepare blocks (strip MD) and carry heading/docId for optional boosts.
12
- const blocks = docs.map((d, i) => ({
14
+ const blocks = normalizedDocs.map((d, i) => ({
13
15
  id: i,
14
16
  text: stripMd(d.text),
15
17
  heading: d.heading,
16
18
  }));
17
19
  // Build index
18
20
  const { lexicon, postings } = buildIndex(blocks);
19
- // Compute avg token length once (store in meta)
20
- const totalTokens = blocks.reduce((sum, b) => sum + tokenize(b.text).length, 0);
21
+ const blockTokenLens = blocks.map((b) => tokenize(b.text).length);
22
+ const totalTokens = blockTokenLens.reduce((sum, len) => sum + len, 0);
21
23
  const avgBlockLen = blocks.length ? totalTokens / blocks.length : 1;
22
24
  const meta = {
23
- version: 2,
25
+ version: 3,
24
26
  stats: {
25
- docs: docs.length,
27
+ docs: normalizedDocs.length,
26
28
  blocks: blocks.length,
27
29
  terms: lexicon.length,
28
30
  avgBlockLen,
29
31
  },
30
32
  };
31
- // Persist blocks as objects to optionally carry heading/docId
33
+ // Persist blocks as objects to optionally carry heading/docId/token length.
32
34
  const blocksPayload = blocks.map((b, i) => ({
33
35
  text: b.text,
34
36
  heading: b.heading ?? null,
35
- docId: docs[i]?.id ?? null,
37
+ docId: normalizedDocs[i]?.id ?? null,
38
+ namespace: normalizedDocs[i]?.namespace ?? null,
39
+ len: blockTokenLens[i] ?? 0,
36
40
  }));
37
41
  // Encode sections
38
42
  const enc = getTextEncoder();
39
43
  const metaBytes = enc.encode(JSON.stringify(meta));
40
44
  const lexBytes = enc.encode(JSON.stringify(lexicon));
41
45
  const blocksBytes = enc.encode(JSON.stringify(blocksPayload));
46
+ const semanticEnabled = Boolean(opts.semantic?.enabled);
47
+ const semanticSection = semanticEnabled && opts.semantic
48
+ ? buildSemanticSection(blocks.length, opts.semantic)
49
+ : undefined;
50
+ const semBytes = semanticSection ? enc.encode(JSON.stringify(semanticSection.semJson)) : undefined;
51
+ const semBlob = semanticSection?.semBlob;
42
52
  const totalLength = 4 + metaBytes.length +
43
53
  4 + lexBytes.length +
44
54
  4 + postings.length * 4 +
45
- 4 + blocksBytes.length;
55
+ 4 + blocksBytes.length +
56
+ (semanticEnabled && semBytes && semBlob
57
+ ? 4 + semBytes.length + 4 + semBlob.length
58
+ : 0);
46
59
  const out = new Uint8Array(totalLength);
47
60
  const dv = new DataView(out.buffer);
48
61
  let offset = 0;
@@ -67,8 +80,87 @@ export async function buildPack(docs) {
67
80
  dv.setUint32(offset, blocksBytes.length, true);
68
81
  offset += 4;
69
82
  out.set(blocksBytes, offset);
83
+ offset += blocksBytes.length;
84
+ if (semanticEnabled && semBytes && semBlob) {
85
+ dv.setUint32(offset, semBytes.length, true);
86
+ offset += 4;
87
+ out.set(semBytes, offset);
88
+ offset += semBytes.length;
89
+ dv.setUint32(offset, semBlob.length, true);
90
+ offset += 4;
91
+ out.set(semBlob, offset);
92
+ }
70
93
  return out;
71
94
  }
95
+ function buildSemanticSection(blockCount, semantic) {
96
+ const { embeddings } = semantic;
97
+ if (!Array.isArray(embeddings) || embeddings.length !== blockCount) {
98
+ throw new Error(`semantic.embeddings must be provided with one embedding per block (expected ${blockCount}).`);
99
+ }
100
+ const quantizationType = semantic.quantization?.type ?? 'int8_l2norm';
101
+ if (quantizationType !== 'int8_l2norm') {
102
+ throw new Error(`Unsupported semantic quantization type: ${quantizationType}`);
103
+ }
104
+ const dims = embeddings[0]?.length ?? 0;
105
+ if (!dims)
106
+ throw new Error('semantic.embeddings must contain vectors with non-zero dimensions.');
107
+ const vecs = new Int8Array(embeddings.length * dims);
108
+ const scales = new Uint16Array(embeddings.length);
109
+ for (let i = 0; i < embeddings.length; i++) {
110
+ const embedding = embeddings[i];
111
+ if (!(embedding instanceof Float32Array)) {
112
+ throw new Error(`semantic.embeddings[${i}] must be a Float32Array.`);
113
+ }
114
+ if (embedding.length !== dims) {
115
+ throw new Error(`semantic.embeddings[${i}] dims mismatch: expected ${dims}, got ${embedding.length}.`);
116
+ }
117
+ const { q, scale } = quantizeEmbeddingInt8L2Norm(embedding);
118
+ vecs.set(q, i * dims);
119
+ scales[i] = encodeScaleF16(scale);
120
+ }
121
+ const vecByteOffset = 0;
122
+ const vecByteLength = vecs.byteLength;
123
+ const scalesByteOffset = vecByteLength;
124
+ const scalesByteLength = scales.byteLength;
125
+ const semBlob = new Uint8Array(vecByteLength + scalesByteLength);
126
+ semBlob.set(new Uint8Array(vecs.buffer, vecs.byteOffset, vecByteLength), vecByteOffset);
127
+ semBlob.set(new Uint8Array(scales.buffer, scales.byteOffset, scalesByteLength), scalesByteOffset);
128
+ const semJson = {
129
+ version: 1,
130
+ modelId: semantic.modelId,
131
+ dims,
132
+ encoding: 'int8_l2norm',
133
+ perVectorScale: true,
134
+ blocks: {
135
+ vectors: { byteOffset: vecByteOffset, length: vecs.length },
136
+ scales: { byteOffset: scalesByteOffset, length: scales.length, encoding: 'float16' },
137
+ },
138
+ };
139
+ return { semJson, semBlob };
140
+ }
141
+ function validateDocs(docs) {
142
+ if (!Array.isArray(docs)) {
143
+ throw new Error('buildPack expects an array of docs: [{ text, id?, heading?, namespace? }, ...]');
144
+ }
145
+ return docs.map((doc, i) => {
146
+ if (!doc || typeof doc !== 'object') {
147
+ throw new Error(`Invalid doc at index ${i}: expected an object with a string "text" field.`);
148
+ }
149
+ if (typeof doc.text !== 'string' || !doc.text.trim()) {
150
+ throw new Error(`Invalid doc at index ${i}: "text" must be a non-empty string.`);
151
+ }
152
+ if (doc.id !== undefined && typeof doc.id !== 'string') {
153
+ throw new Error(`Invalid doc at index ${i}: "id" must be a string when provided.`);
154
+ }
155
+ if (doc.heading !== undefined && typeof doc.heading !== 'string') {
156
+ throw new Error(`Invalid doc at index ${i}: "heading" must be a string when provided.`);
157
+ }
158
+ if (doc.namespace !== undefined && typeof doc.namespace !== 'string') {
159
+ throw new Error(`Invalid doc at index ${i}: "namespace" must be a string when provided.`);
160
+ }
161
+ return doc;
162
+ });
163
+ }
72
164
  /** Strip Markdown syntax with lightweight regexes (no deps). */
73
165
  function stripMd(md) {
74
166
  let text = md.replace(/```[\s\S]*?```/g, ' ');
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- export { mountPack } from './pack.js';
2
- export { query } from './query.js';
1
+ export { mountPack, hasSemantic } from './pack.js';
2
+ export { query, lexConfidence, validateSemanticQueryOptions } from './query.js';
3
3
  export { makeContextPatch } from './patch.js';
4
4
  export { buildPack } from './builder.js';
5
+ export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16 } from './semantic.js';
5
6
  export type { MountOptions, PackMeta, Pack } from './pack.js';
6
7
  export type { QueryOptions, Hit } from './query.js';
7
8
  export type { ContextPatch } from './patch.js';
9
+ export type { BuildInputDoc, BuildPackOptions } from './builder.js';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // src/index.ts
2
- export { mountPack } from './pack.js';
3
- export { query } from './query.js';
2
+ export { mountPack, hasSemantic } from './pack.js';
3
+ export { query, lexConfidence, validateSemanticQueryOptions } from './query.js';
4
4
  export { makeContextPatch } from './patch.js';
5
5
  export { buildPack } from './builder.js';
6
+ export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16 } from './semantic.js';
package/dist/indexer.d.ts CHANGED
@@ -13,8 +13,9 @@ export type IndexBuildResult = {
13
13
  * sequences of blockId and positions for that term, with zeros as delimiters.
14
14
  * The structure looks like:
15
15
  *
16
- * [termId, blockId, pos, pos, 0, blockId, pos, 0, 0, termId, ...]
16
+ * [termId, blockId+1, pos, pos, 0, blockId+1, pos, 0, 0, termId, ...]
17
17
  *
18
+ * Block IDs are stored as bid+1 so that 0 can remain a sentinel delimiter.
18
19
  * Each block section ends with a 0, and each term section ends with a 0. The
19
20
  * entire array can be streamed sequentially without needing to know the sizes
20
21
  * of individual lists ahead of time.
package/dist/indexer.js CHANGED
@@ -14,8 +14,9 @@ import { tokenize } from "./tokenize.js";
14
14
  * sequences of blockId and positions for that term, with zeros as delimiters.
15
15
  * The structure looks like:
16
16
  *
17
- * [termId, blockId, pos, pos, 0, blockId, pos, 0, 0, termId, ...]
17
+ * [termId, blockId+1, pos, pos, 0, blockId+1, pos, 0, 0, termId, ...]
18
18
  *
19
+ * Block IDs are stored as bid+1 so that 0 can remain a sentinel delimiter.
19
20
  * Each block section ends with a 0, and each term section ends with a 0. The
20
21
  * entire array can be streamed sequentially without needing to know the sizes
21
22
  * of individual lists ahead of time.
@@ -60,7 +61,7 @@ export function buildIndex(blocks) {
60
61
  for (const [tid, blockMap] of termBlockPositions) {
61
62
  postings.push(tid);
62
63
  for (const [bid, positions] of blockMap) {
63
- postings.push(bid, ...positions, 0);
64
+ postings.push(bid + 1, ...positions, 0);
64
65
  }
65
66
  postings.push(0); // end of term
66
67
  }
package/dist/pack.d.ts CHANGED
@@ -17,5 +17,17 @@ export type Pack = {
17
17
  blocks: string[];
18
18
  headings?: (string | null)[];
19
19
  docIds?: (string | null)[];
20
+ namespaces?: (string | null)[];
21
+ blockTokenLens?: number[];
22
+ semantic?: {
23
+ version: 1;
24
+ modelId: string;
25
+ dims: number;
26
+ encoding: 'int8_l2norm';
27
+ perVectorScale: boolean;
28
+ vecs: Int8Array;
29
+ scales?: Uint16Array;
30
+ };
20
31
  };
32
+ export declare function hasSemantic(pack: Pack): boolean;
21
33
  export declare function mountPack(opts: MountOptions): Promise<Pack>;
package/dist/pack.js CHANGED
@@ -1,12 +1,15 @@
1
1
  /*
2
2
  * pack.ts
3
3
  *
4
- * Mount `.knolo` packs across Node, browsers, and RN/Expo. Now tolerant of:
5
- * - blocks as string[] (v1) or object[] with { text, heading?, docId? } (v2)
4
+ * Mount `.knolo` packs across Node, browsers, and RN/Expo. Tolerant of:
5
+ * - blocks as string[] (v1) or object[] with { text, heading?, docId?, namespace?, len? }
6
6
  * - meta.stats.avgBlockLen (optional)
7
7
  * Includes RN/Expo-safe TextDecoder via ponyfill.
8
8
  */
9
9
  import { getTextDecoder } from './utils/utf8.js';
10
+ export function hasSemantic(pack) {
11
+ return Boolean(pack.semantic && pack.semantic.dims > 0 && pack.semantic.vecs.length > 0);
12
+ }
10
13
  export async function mountPack(opts) {
11
14
  const buf = await resolveToBuffer(opts.src);
12
15
  const dv = new DataView(buf);
@@ -33,14 +36,17 @@ export async function mountPack(opts) {
33
36
  postings[i] = dv.getUint32(offset, true);
34
37
  offset += 4;
35
38
  }
36
- // blocks (v1: string[]; v2: {text, heading?, docId?}[])
39
+ // blocks (v1: string[]; v2/v3: {text, heading?, docId?, namespace?, len?}[])
37
40
  const blocksLen = dv.getUint32(offset, true);
38
41
  offset += 4;
39
42
  const blocksJson = dec.decode(new Uint8Array(buf, offset, blocksLen));
43
+ offset += blocksLen;
40
44
  const parsed = JSON.parse(blocksJson);
41
45
  let blocks = [];
42
46
  let headings;
43
47
  let docIds;
48
+ let namespaces;
49
+ let blockTokenLens;
44
50
  if (Array.isArray(parsed) && parsed.length && typeof parsed[0] === 'string') {
45
51
  // v1
46
52
  blocks = parsed;
@@ -49,26 +55,71 @@ export async function mountPack(opts) {
49
55
  blocks = [];
50
56
  headings = [];
51
57
  docIds = [];
58
+ namespaces = [];
59
+ blockTokenLens = [];
52
60
  for (const it of parsed) {
53
61
  if (it && typeof it === 'object') {
54
62
  blocks.push(String(it.text ?? ''));
55
63
  headings.push(it.heading ?? null);
56
64
  docIds.push(it.docId ?? null);
65
+ namespaces.push(it.namespace ?? null);
66
+ blockTokenLens.push(typeof it.len === 'number' ? it.len : 0);
57
67
  }
58
68
  else {
59
69
  blocks.push(String(it ?? ''));
60
70
  headings.push(null);
61
71
  docIds.push(null);
72
+ namespaces.push(null);
73
+ blockTokenLens.push(0);
62
74
  }
63
75
  }
64
76
  }
65
77
  else {
66
78
  blocks = [];
67
79
  }
68
- return { meta, lexicon, postings, blocks, headings, docIds };
80
+ let semantic;
81
+ if (offset < buf.byteLength) {
82
+ const semLen = dv.getUint32(offset, true);
83
+ offset += 4;
84
+ const semJson = dec.decode(new Uint8Array(buf, offset, semLen));
85
+ offset += semLen;
86
+ const sem = JSON.parse(semJson);
87
+ const semBlobLen = dv.getUint32(offset, true);
88
+ offset += 4;
89
+ const semBlob = new Uint8Array(buf, offset, semBlobLen);
90
+ semantic = parseSemanticSection(sem, semBlob);
91
+ }
92
+ return { meta, lexicon, postings, blocks, headings, docIds, namespaces, blockTokenLens, semantic };
93
+ }
94
+ function parseSemanticSection(sem, blob) {
95
+ const vectors = sem?.blocks?.vectors;
96
+ const scales = sem?.blocks?.scales;
97
+ const vecs = new Int8Array(blob.buffer, blob.byteOffset + Number(vectors?.byteOffset ?? 0), Number(vectors?.length ?? 0));
98
+ let scaleView;
99
+ if (scales) {
100
+ const scaleLen = Number(scales.length ?? 0);
101
+ const scaleOffset = Number(scales.byteOffset ?? 0);
102
+ const dv = new DataView(blob.buffer, blob.byteOffset + scaleOffset, scaleLen * 2);
103
+ scaleView = new Uint16Array(scaleLen);
104
+ for (let i = 0; i < scaleLen; i++) {
105
+ scaleView[i] = dv.getUint16(i * 2, true);
106
+ }
107
+ }
108
+ return {
109
+ version: 1,
110
+ modelId: String(sem?.modelId ?? ''),
111
+ dims: Number(sem?.dims ?? 0),
112
+ encoding: 'int8_l2norm',
113
+ perVectorScale: Boolean(sem?.perVectorScale),
114
+ vecs,
115
+ scales: scaleView,
116
+ };
69
117
  }
70
118
  async function resolveToBuffer(src) {
71
119
  if (typeof src === 'string') {
120
+ if (isNodeRuntime() && isLikelyLocalPath(src)) {
121
+ return await readLocalFileAsBuffer(src);
122
+ }
72
123
  const res = await fetch(src);
73
124
  return await res.arrayBuffer();
74
125
  }
@@ -81,3 +132,25 @@ async function resolveToBuffer(src) {
81
132
  }
82
133
  return src;
83
134
  }
135
+ function isNodeRuntime() {
136
+ return typeof process !== 'undefined' && !!process.versions?.node;
137
+ }
138
+ function isLikelyLocalPath(value) {
139
+ if (value.startsWith('file://'))
140
+ return true;
141
+ if (value.startsWith('./') || value.startsWith('../') || value.startsWith('/') || value.startsWith('~'))
142
+ return true;
143
+ if (/^[A-Za-z]:[\\/]/.test(value))
144
+ return true; // Windows absolute path
145
+ if (/^[A-Za-z][A-Za-z\d+.-]*:/.test(value))
146
+ return false; // URL scheme
147
+ return true; // plain relative path like "knowledge.knolo"
148
+ }
149
+ async function readLocalFileAsBuffer(pathOrFileUrl) {
150
+ const { readFile } = await import('node:fs/promises');
151
+ const filePath = pathOrFileUrl.startsWith('file://')
152
+ ? decodeURIComponent(new URL(pathOrFileUrl).pathname)
153
+ : pathOrFileUrl;
154
+ const data = await readFile(filePath);
155
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
156
+ }
package/dist/patch.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Hit } from "./query.js";
1
+ import type { Hit } from './query.js';
2
2
  export type ContextPatch = {
3
3
  background: string[];
4
4
  snippets: Array<{
@@ -17,13 +17,6 @@ export type ContextPatch = {
17
17
  evidence?: number[];
18
18
  }>;
19
19
  };
20
- /** Assemble a context patch from an array of hits. The `budget` determines
21
- * how many snippets and how much text to include in each snippet. Currently
22
- * three budgets are supported:
23
- * - `mini`: ~512 token contexts
24
- * - `small`: ~1k token contexts
25
- * - `full`: ~2k token contexts
26
- */
27
20
  export declare function makeContextPatch(hits: Hit[], opts?: {
28
21
  budget?: 'mini' | 'small' | 'full';
29
22
  }): ContextPatch;
package/dist/patch.js CHANGED
@@ -1,17 +1,7 @@
1
1
  /*
2
2
  * patch.ts
3
3
  *
4
- * Provides the makeContextPatch function that assembles search results into
5
- * structured context patches for consumption by language models. A context
6
- * patch includes a background summary and selected snippets. Future versions
7
- * may include definitions, facts and other structured artifacts.
8
- */
9
- /** Assemble a context patch from an array of hits. The `budget` determines
10
- * how many snippets and how much text to include in each snippet. Currently
11
- * three budgets are supported:
12
- * - `mini`: ~512 token contexts
13
- * - `small`: ~1k token contexts
14
- * - `full`: ~2k token contexts
4
+ * Produces a compact, deterministic “context patch” from ranked hits.
15
5
  */
16
6
  export function makeContextPatch(hits, opts = {}) {
17
7
  const budget = opts.budget ?? 'small';
@@ -23,6 +13,7 @@ export function makeContextPatch(hits, opts = {}) {
23
13
  const limit = limits[budget];
24
14
  const snippets = hits.slice(0, limit.snippets).map((h) => ({
25
15
  text: truncate(h.text, limit.chars),
16
+ source: h.source,
26
17
  }));
27
18
  // Build background summary from first two snippets by extracting first sentence
28
19
  const background = snippets.slice(0, 2).map((s) => firstSentence(s.text));
@@ -33,18 +24,12 @@ export function makeContextPatch(hits, opts = {}) {
33
24
  facts: [],
34
25
  };
35
26
  }
36
- /** Extract the first sentence from a block of text. If no terminal punctuation
37
- * is found, returns the first N characters up to a reasonable length.
38
- */
39
27
  function firstSentence(text) {
40
28
  const m = text.match(/^(.{10,200}?[.!?])\s/);
41
29
  if (m)
42
30
  return m[1];
43
31
  return text.slice(0, 160);
44
32
  }
45
- /** Truncate text to a maximum length and append an ellipsis if it was
46
- * truncated.
47
- */
48
33
  function truncate(text, maxChars) {
49
34
  return text.length > maxChars ? text.slice(0, maxChars) + '…' : text;
50
35
  }
package/dist/query.d.ts CHANGED
@@ -1,12 +1,40 @@
1
1
  import type { Pack } from "./pack.js";
2
2
  export type QueryOptions = {
3
3
  topK?: number;
4
+ minScore?: number;
4
5
  requirePhrases?: string[];
6
+ namespace?: string | string[];
7
+ source?: string | string[];
8
+ queryExpansion?: {
9
+ enabled?: boolean;
10
+ docs?: number;
11
+ terms?: number;
12
+ weight?: number;
13
+ minTermLength?: number;
14
+ };
15
+ semantic?: {
16
+ enabled?: boolean;
17
+ mode?: "rerank";
18
+ topN?: number;
19
+ minLexConfidence?: number;
20
+ blend?: {
21
+ enabled?: boolean;
22
+ wLex?: number;
23
+ wSem?: number;
24
+ };
25
+ queryEmbedding?: Float32Array;
26
+ force?: boolean;
27
+ };
5
28
  };
29
+ export declare function validateSemanticQueryOptions(options?: QueryOptions["semantic"]): void;
6
30
  export type Hit = {
7
31
  blockId: number;
8
32
  score: number;
9
33
  text: string;
10
34
  source?: string;
35
+ namespace?: string;
11
36
  };
12
37
  export declare function query(pack: Pack, q: string, opts?: QueryOptions): Hit[];
38
+ export declare function lexConfidence(hits: Array<{
39
+ score: number;
40
+ }>): number;