@knolo/core 3.2.1 → 3.2.2

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/index.d.ts CHANGED
@@ -3,6 +3,10 @@ export { query, lexConfidence, validateQueryOptions, validateSemanticQueryOption
3
3
  export { makeContextPatch } from './patch.js';
4
4
  export { buildPack } from './builder.js';
5
5
  export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16, } from './semantic.js';
6
+ export { cosineSimilarity, normalizeVector } from './semantic/cosine.js';
7
+ export { createPackFingerprint, serializeSidecar, parseSidecar, validateSidecarForPack, } from './semantic/sidecar.js';
8
+ export { rerankCandidates } from './semantic/rerank.js';
9
+ export { assertProviderCompatible, ensureProviderModelId } from './semantic/provider.js';
6
10
  export { listAgents, getAgent, resolveAgent, buildSystemPrompt, isToolAllowed, assertToolAllowed, validateAgentRegistry, validateAgentDefinition, } from './agent.js';
7
11
  export { getClaimGraph, validateClaimGraph, } from './graph/claim_graph.js';
8
12
  export { buildClaimGraph } from './graph/build_claim_graph.js';
@@ -10,6 +14,7 @@ export { createGraphLog, appendOp, applyClaimGraphLog, mergeClaimGraphLogs, seri
10
14
  export { expandQueryWithGraph } from './graph/query_expand.js';
11
15
  export type { MountOptions, PackMeta, Pack } from './pack.runtime.js';
12
16
  export type { QueryOptions, Hit } from './query.js';
17
+ export type { EmbeddingProvider, SemanticSidecar, SemanticQueryOptions, RetrievalEvidence } from './semantic/types.js';
13
18
  export type { ContextPatch } from './patch.js';
14
19
  export type { BuildInputDoc, BuildPackOptions } from './builder.js';
15
20
  export type { AgentPromptTemplate, AgentToolPolicy, AgentRetrievalDefaults, AgentDefinitionV1, AgentRegistry, ResolveAgentInput, ResolvedAgent, } from './agent.js';
package/dist/index.js CHANGED
@@ -4,6 +4,10 @@ export { query, lexConfidence, validateQueryOptions, validateSemanticQueryOption
4
4
  export { makeContextPatch } from './patch.js';
5
5
  export { buildPack } from './builder.js';
6
6
  export { quantizeEmbeddingInt8L2Norm, encodeScaleF16, decodeScaleF16, } from './semantic.js';
7
+ export { cosineSimilarity, normalizeVector } from './semantic/cosine.js';
8
+ export { createPackFingerprint, serializeSidecar, parseSidecar, validateSidecarForPack, } from './semantic/sidecar.js';
9
+ export { rerankCandidates } from './semantic/rerank.js';
10
+ export { assertProviderCompatible, ensureProviderModelId } from './semantic/provider.js';
7
11
  export { listAgents, getAgent, resolveAgent, buildSystemPrompt, isToolAllowed, assertToolAllowed, validateAgentRegistry, validateAgentDefinition, } from './agent.js';
8
12
  export { getClaimGraph, validateClaimGraph, } from './graph/claim_graph.js';
9
13
  export { buildClaimGraph } from './graph/build_claim_graph.js';
package/dist/query.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Pack } from "./pack.js";
2
+ import type { RetrievalEvidence, SemanticSidecar } from "./semantic/types.js";
2
3
  export type QueryOptions = {
3
4
  topK?: number;
4
5
  minScore?: number;
@@ -28,6 +29,14 @@ export type QueryOptions = {
28
29
  wSem?: number;
29
30
  };
30
31
  queryEmbedding?: Float32Array;
32
+ sidecar?: SemanticSidecar;
33
+ provider?: {
34
+ type: "ollama";
35
+ modelId: string;
36
+ endpoint?: string;
37
+ };
38
+ sidecarPath?: string;
39
+ minSemanticScore?: number;
31
40
  force?: boolean;
32
41
  };
33
42
  };
@@ -39,6 +48,7 @@ export type Hit = {
39
48
  text: string;
40
49
  source?: string;
41
50
  namespace?: string;
51
+ evidence?: RetrievalEvidence;
42
52
  };
43
53
  export declare function query(pack: Pack, q: string, opts?: QueryOptions): Hit[];
44
54
  export declare function lexConfidence(hits: Array<{
package/dist/query.js CHANGED
@@ -15,6 +15,8 @@ import { diversifyAndDedupe } from "./quality/diversify.js";
15
15
  import { knsSignature, knsDistance } from "./quality/signature.js";
16
16
  import { decodeScaleF16, quantizeEmbeddingInt8L2Norm } from "./semantic.js";
17
17
  import { expandQueryWithGraph } from "./graph/query_expand.js";
18
+ import { rerankCandidates } from "./semantic/rerank.js";
19
+ import { parseSidecar } from "./semantic/sidecar.js";
18
20
  export function validateQueryOptions(opts) {
19
21
  if (!opts)
20
22
  return;
@@ -78,6 +80,19 @@ export function validateSemanticQueryOptions(options) {
78
80
  if (options.queryEmbedding !== undefined && !(options.queryEmbedding instanceof Float32Array)) {
79
81
  throw new Error("query(...): semantic.queryEmbedding must be a Float32Array.");
80
82
  }
83
+ if (options.sidecarPath !== undefined && typeof options.sidecarPath !== "string") {
84
+ throw new Error("query(...): semantic.sidecarPath must be a string when provided.");
85
+ }
86
+ if (options.minSemanticScore !== undefined && (!Number.isFinite(options.minSemanticScore) || options.minSemanticScore < 0 || options.minSemanticScore > 1)) {
87
+ throw new Error("query(...): semantic.minSemanticScore must be a finite number between 0 and 1.");
88
+ }
89
+ if (options.provider) {
90
+ if (options.provider.type !== "ollama")
91
+ throw new Error('query(...): semantic.provider.type must be "ollama".');
92
+ if (typeof options.provider.modelId !== "string" || !options.provider.modelId.trim()) {
93
+ throw new Error("query(...): semantic.provider.modelId must be a non-empty string.");
94
+ }
95
+ }
81
96
  if (options.blend) {
82
97
  if (options.blend.enabled !== undefined && typeof options.blend.enabled !== "boolean") {
83
98
  throw new Error("query(...): semantic.blend.enabled must be a boolean when provided.");
@@ -115,6 +130,9 @@ export function query(pack, q, opts = {}) {
115
130
  wSem: Math.max(0, opts.semantic?.blend?.wSem ?? 0.25),
116
131
  },
117
132
  queryEmbedding: opts.semantic?.queryEmbedding,
133
+ sidecar: resolveSemanticSidecar(opts.semantic?.sidecar, opts.semantic?.sidecarPath),
134
+ provider: opts.semantic?.provider,
135
+ minSemanticScore: opts.semantic?.minSemanticScore,
118
136
  force: opts.semantic?.force ?? false,
119
137
  };
120
138
  const graphQuery = opts.graph?.expand === true
@@ -284,9 +302,16 @@ export function query(pack, q, opts = {}) {
284
302
  return [];
285
303
  }
286
304
  const confidence = lexConfidence(prelim);
305
+ let semanticScores;
306
+ let blendedScores;
307
+ const originalLexicalScores = new Map(prelim.map((item) => [item.blockId, item.score]));
287
308
  if (shouldRerankWithSemantic(pack, semanticOpts, confidence)) {
288
- prelim = rerankLexicalHitsWithSemantic(pack, prelim, semanticOpts);
309
+ const semanticResult = rerankLexicalHitsWithSemantic(pack, prelim, semanticOpts);
310
+ prelim = semanticResult.hits;
311
+ semanticScores = semanticResult.semanticScores;
312
+ blendedScores = semanticResult.blendedScores;
289
313
  }
314
+ const retrievalMode = semanticScores ? "hybrid" : "lexical";
290
315
  // --- KNS tie-breaker + de-dup/MMR
291
316
  const qSig = knsSignature(normalize(q));
292
317
  const pool = prelim.slice(0, topK * 5).map((r) => {
@@ -298,6 +323,13 @@ export function query(pack, q, opts = {}) {
298
323
  text,
299
324
  source: pack.docIds?.[r.blockId] ?? undefined,
300
325
  namespace: pack.namespaces?.[r.blockId] ?? undefined,
326
+ evidence: {
327
+ retrieval: retrievalMode,
328
+ lexicalScore: originalLexicalScores.get(r.blockId) ?? r.score,
329
+ semanticScore: semanticScores?.get(r.blockId),
330
+ blendedScore: blendedScores?.get(r.blockId),
331
+ modelId: semanticOpts.provider?.modelId ?? semanticOpts.sidecar?.modelId,
332
+ },
301
333
  };
302
334
  });
303
335
  const finalHits = diversifyAndDedupe(pool, { k: topK });
@@ -315,19 +347,66 @@ export function lexConfidence(hits) {
315
347
  function shouldRerankWithSemantic(pack, opts, confidence) {
316
348
  if (!opts.enabled || opts.mode !== "rerank")
317
349
  return false;
318
- if (!pack.semantic)
350
+ if (!pack.semantic && !opts.sidecar)
319
351
  return false;
320
352
  if (!opts.queryEmbedding) {
321
353
  throw new Error("query(...): semantic.queryEmbedding (Float32Array) is required when semantic.enabled=true.");
322
354
  }
323
355
  return opts.force || confidence < opts.minLexConfidence;
324
356
  }
357
+ function resolveSemanticSidecar(sidecar, sidecarPath) {
358
+ if (sidecar)
359
+ return sidecar;
360
+ if (!sidecarPath)
361
+ return undefined;
362
+ const raw = sidecarPath.trim();
363
+ if (!raw)
364
+ return undefined;
365
+ if (raw.startsWith("{")) {
366
+ return parseSidecar(raw);
367
+ }
368
+ if (raw.startsWith("data:")) {
369
+ const comma = raw.indexOf(",");
370
+ if (comma <= 0)
371
+ return undefined;
372
+ const meta = raw.slice(5, comma).toLowerCase();
373
+ const payload = raw.slice(comma + 1);
374
+ const decoded = meta.includes(";base64")
375
+ ? decodeBase64(payload)
376
+ : decodeURIComponent(payload);
377
+ if (!decoded.trim())
378
+ return undefined;
379
+ return parseSidecar(decoded);
380
+ }
381
+ return undefined;
382
+ }
383
+ function decodeBase64(input) {
384
+ const normalized = input.replace(/\s+/g, "");
385
+ const atobFn = globalThis.atob;
386
+ if (typeof atobFn === "function")
387
+ return atobFn(normalized);
388
+ const maybeBufferCtor = globalThis.Buffer;
389
+ if (maybeBufferCtor?.from)
390
+ return maybeBufferCtor.from(normalized, "base64").toString("utf8");
391
+ throw new Error("query(...): Unable to decode semantic.sidecarPath base64 payload in this runtime.");
392
+ }
325
393
  function rerankLexicalHitsWithSemantic(pack, prelim, opts) {
394
+ if (opts.sidecar && opts.queryEmbedding) {
395
+ const sidecarResult = rerankCandidates({
396
+ lexical: prelim,
397
+ sidecar: opts.sidecar,
398
+ queryEmbedding: opts.queryEmbedding,
399
+ topN: opts.topN,
400
+ blend: opts.blend,
401
+ minSemanticScore: opts.minSemanticScore,
402
+ });
403
+ return { hits: sidecarResult.reranked, semanticScores: sidecarResult.semanticScores, blendedScores: sidecarResult.blendedScores };
404
+ }
326
405
  const sem = pack.semantic;
327
406
  if (!sem || !opts.queryEmbedding)
328
- return prelim;
407
+ return { hits: prelim };
329
408
  if (sem.dims <= 0 || sem.vecs.length === 0 || sem.dims !== opts.queryEmbedding.length)
330
- return prelim;
409
+ return { hits: prelim };
331
410
  const topN = Math.min(opts.topN, prelim.length);
332
411
  const rerankSlice = prelim.slice(0, topN);
333
412
  const tail = prelim.slice(topN);
@@ -342,15 +421,19 @@ function rerankLexicalHitsWithSemantic(pack, prelim, opts) {
342
421
  const wLex = denom > 0 ? opts.blend.wLex / denom : 0.5;
343
422
  const wSem = denom > 0 ? opts.blend.wSem / denom : 0.5;
344
423
  const reranked = new Array(topN);
424
+ const semanticScores = new Map();
425
+ const blendedScores = new Map();
345
426
  for (let i = 0; i < topN; i++) {
346
427
  const hit = rerankSlice[i];
428
+ semanticScores.set(hit.blockId, normSem[i]);
429
+ blendedScores.set(hit.blockId, opts.blend.enabled ? wLex * normLex[i] + wSem * normSem[i] : semScores[i]);
347
430
  reranked[i] = {
348
431
  blockId: hit.blockId,
349
- score: opts.blend.enabled ? wLex * normLex[i] + wSem * normSem[i] : semScores[i],
432
+ score: blendedScores.get(hit.blockId) ?? hit.score,
350
433
  };
351
434
  }
352
435
  reranked.sort((a, b) => b.score - a.score || a.blockId - b.blockId);
353
- return [...reranked, ...tail];
436
+ return { hits: [...reranked, ...tail], semanticScores, blendedScores };
354
437
  }
355
438
  function scoreSemanticInt8(queryQ, queryScale, semantic, hits) {
356
439
  const scores = new Float64Array(hits.length);
@@ -0,0 +1,2 @@
1
+ export declare function normalizeVector(vector: Float32Array): Float32Array;
2
+ export declare function cosineSimilarity(a: Float32Array, b: Float32Array): number;
@@ -0,0 +1,20 @@
1
+ export function normalizeVector(vector) {
2
+ let normSq = 0;
3
+ for (let i = 0; i < vector.length; i++)
4
+ normSq += vector[i] * vector[i];
5
+ const norm = Math.sqrt(normSq);
6
+ if (!norm)
7
+ return new Float32Array(vector.length);
8
+ const out = new Float32Array(vector.length);
9
+ for (let i = 0; i < vector.length; i++)
10
+ out[i] = vector[i] / norm;
11
+ return out;
12
+ }
13
+ export function cosineSimilarity(a, b) {
14
+ if (a.length !== b.length || a.length === 0)
15
+ return 0;
16
+ let dot = 0;
17
+ for (let i = 0; i < a.length; i++)
18
+ dot += a[i] * b[i];
19
+ return dot;
20
+ }
@@ -0,0 +1,3 @@
1
+ import type { EmbeddingProvider, SemanticQueryOptions } from './types.js';
2
+ export declare function ensureProviderModelId(options?: SemanticQueryOptions): string | undefined;
3
+ export declare function assertProviderCompatible(options?: SemanticQueryOptions, provider?: EmbeddingProvider): void;
@@ -0,0 +1,13 @@
1
+ export function ensureProviderModelId(options) {
2
+ return options?.provider?.modelId;
3
+ }
4
+ export function assertProviderCompatible(options, provider) {
5
+ if (!options?.enabled)
6
+ return;
7
+ if (!provider && !options.queryEmbedding) {
8
+ throw new Error('semantic.enabled=true requires either semantic.queryEmbedding or an EmbeddingProvider.');
9
+ }
10
+ if (provider && options.provider?.modelId && options.provider.modelId !== provider.modelId) {
11
+ throw new Error(`Semantic provider model mismatch: options requested ${options.provider.modelId}, provider exposes ${provider.modelId}.`);
12
+ }
13
+ }
@@ -0,0 +1,23 @@
1
+ import type { SemanticSidecar } from './types.js';
2
+ export declare function rerankCandidates(params: {
3
+ lexical: Array<{
4
+ blockId: number;
5
+ score: number;
6
+ }>;
7
+ sidecar: SemanticSidecar;
8
+ queryEmbedding: Float32Array;
9
+ topN: number;
10
+ blend: {
11
+ enabled: boolean;
12
+ wLex: number;
13
+ wSem: number;
14
+ };
15
+ minSemanticScore?: number;
16
+ }): {
17
+ reranked: Array<{
18
+ blockId: number;
19
+ score: number;
20
+ }>;
21
+ semanticScores: Map<number, number>;
22
+ blendedScores: Map<number, number>;
23
+ };
@@ -0,0 +1,42 @@
1
+ import { cosineSimilarity, normalizeVector } from './cosine.js';
2
+ export function rerankCandidates(params) {
3
+ const topN = Math.min(params.topN, params.lexical.length);
4
+ const head = params.lexical.slice(0, topN);
5
+ const tail = params.lexical.slice(topN);
6
+ const q = normalizeVector(params.queryEmbedding);
7
+ const semanticScores = new Map();
8
+ const blendedScores = new Map();
9
+ const lexNorm = minMax(head.map((h) => h.score));
10
+ const semRaw = [];
11
+ for (const item of head) {
12
+ const rec = params.sidecar.blocks.find((b) => b.blockId === item.blockId);
13
+ const vec = rec ? Float32Array.from(rec.vector) : new Float32Array(q.length);
14
+ semRaw.push(cosineSimilarity(q, vec));
15
+ }
16
+ const semNorm = minMax(semRaw);
17
+ const denom = params.blend.wLex + params.blend.wSem;
18
+ const wLex = denom > 0 ? params.blend.wLex / denom : 0.7;
19
+ const wSem = denom > 0 ? params.blend.wSem / denom : 0.3;
20
+ const reranked = head.map((item, idx) => {
21
+ const sem = semNorm[idx];
22
+ semanticScores.set(item.blockId, sem);
23
+ if ((params.minSemanticScore ?? 0) > sem) {
24
+ blendedScores.set(item.blockId, lexNorm[idx]);
25
+ return { blockId: item.blockId, score: lexNorm[idx] };
26
+ }
27
+ const blended = params.blend.enabled ? wLex * lexNorm[idx] + wSem * sem : sem;
28
+ blendedScores.set(item.blockId, blended);
29
+ return { blockId: item.blockId, score: blended };
30
+ });
31
+ reranked.sort((a, b) => b.score - a.score || a.blockId - b.blockId);
32
+ return { reranked: [...reranked, ...tail], semanticScores, blendedScores };
33
+ }
34
+ function minMax(values) {
35
+ if (values.length === 0)
36
+ return values;
37
+ const min = Math.min(...values);
38
+ const max = Math.max(...values);
39
+ if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min)
40
+ return values.map(() => 1);
41
+ return values.map((v) => Math.min(1, Math.max(0, (v - min) / (max - min))));
42
+ }
@@ -0,0 +1,10 @@
1
+ import type { Pack } from '../pack.runtime.js';
2
+ import type { SemanticSidecar } from './types.js';
3
+ export declare function createPackFingerprint(pack: Pick<Pack, 'blocks' | 'docIds' | 'meta'>): string;
4
+ export declare function serializeSidecar(sidecar: SemanticSidecar): string;
5
+ export declare function parseSidecar(raw: string): SemanticSidecar;
6
+ export declare function validateSidecarForPack(input: {
7
+ sidecar: SemanticSidecar;
8
+ pack: Pick<Pack, 'blocks' | 'docIds' | 'meta'>;
9
+ modelId: string;
10
+ }): void;
@@ -0,0 +1,32 @@
1
+ export function createPackFingerprint(pack) {
2
+ let hash = 2166136261;
3
+ const parts = [String(pack.meta?.version ?? 0), ...(pack.docIds ?? []), ...pack.blocks];
4
+ for (const part of parts) {
5
+ const text = String(part ?? '');
6
+ for (let i = 0; i < text.length; i++) {
7
+ hash ^= text.charCodeAt(i);
8
+ hash = Math.imul(hash, 16777619);
9
+ }
10
+ }
11
+ return `fnv1a-${(hash >>> 0).toString(16).padStart(8, '0')}`;
12
+ }
13
+ export function serializeSidecar(sidecar) {
14
+ return `${JSON.stringify(sidecar, null, 2)}\n`;
15
+ }
16
+ export function parseSidecar(raw) {
17
+ const parsed = JSON.parse(raw);
18
+ if (parsed.version !== 1)
19
+ throw new Error(`Unsupported semantic sidecar version: ${parsed.version}`);
20
+ if (parsed.metric !== 'cosine')
21
+ throw new Error(`Unsupported semantic metric: ${parsed.metric}`);
22
+ return parsed;
23
+ }
24
+ export function validateSidecarForPack(input) {
25
+ const expectedFingerprint = createPackFingerprint(input.pack);
26
+ if (input.sidecar.packFingerprint !== expectedFingerprint) {
27
+ throw new Error(`Semantic sidecar pack fingerprint mismatch: expected ${expectedFingerprint}, got ${input.sidecar.packFingerprint}. Regenerate the sidecar for this pack.`);
28
+ }
29
+ if (input.sidecar.modelId !== input.modelId) {
30
+ throw new Error(`Semantic model mismatch: sidecar model is ${input.sidecar.modelId}, but query provider is ${input.modelId}. Use the same embedding model or regenerate the sidecar.`);
31
+ }
32
+ }
@@ -0,0 +1,44 @@
1
+ export interface EmbeddingProvider {
2
+ readonly modelId: string;
3
+ embedQuery(text: string): Promise<Float32Array>;
4
+ embedTexts(texts: string[]): Promise<Float32Array[]>;
5
+ }
6
+ export interface SemanticSidecar {
7
+ version: 1;
8
+ packFingerprint: string;
9
+ modelId: string;
10
+ dimension: number;
11
+ metric: 'cosine';
12
+ createdAt: string;
13
+ blocks: Array<{
14
+ blockId: number;
15
+ vector: number[];
16
+ }>;
17
+ }
18
+ export type SemanticQueryOptions = {
19
+ enabled?: boolean;
20
+ mode?: 'rerank';
21
+ topN?: number;
22
+ minLexConfidence?: number;
23
+ minSemanticScore?: number;
24
+ blend?: {
25
+ enabled?: boolean;
26
+ wLex?: number;
27
+ wSem?: number;
28
+ };
29
+ provider?: {
30
+ type: 'ollama';
31
+ modelId: string;
32
+ endpoint?: string;
33
+ };
34
+ sidecarPath?: string;
35
+ queryEmbedding?: Float32Array;
36
+ force?: boolean;
37
+ };
38
+ export type RetrievalEvidence = {
39
+ retrieval: 'lexical' | 'hybrid';
40
+ lexicalScore?: number;
41
+ semanticScore?: number;
42
+ blendedScore?: number;
43
+ modelId?: string;
44
+ };
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knolo/core",
3
- "version": "3.2.1",
3
+ "version": "3.2.2",
4
4
  "type": "module",
5
5
  "description": "Local-first knowledge packs for small LLMs.",
6
6
  "keywords": [