sensemaking 0.15.4 → 0.16.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/README.md +33 -5
- package/dist/cjs/cli/download.js +12 -11
- package/dist/cjs/cli/download.js.map +1 -1
- package/dist/cjs/cli/index.d.cts +1 -1
- package/dist/cjs/cli/index.d.ts +1 -1
- package/dist/cjs/cli/index.js +1 -1
- package/dist/cjs/cli/index.js.map +1 -1
- package/dist/cjs/cli/init.js +31 -5
- package/dist/cjs/cli/init.js.map +1 -1
- package/dist/cjs/cli/status.js +343 -99
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/commands/related.js +17 -13
- package/dist/cjs/commands/related.js.map +1 -1
- package/dist/cjs/commands/search.js +25 -96
- package/dist/cjs/commands/search.js.map +1 -1
- package/dist/cjs/commands/signals.d.cts +18 -0
- package/dist/cjs/commands/signals.d.ts +18 -0
- package/dist/cjs/commands/signals.js +299 -0
- package/dist/cjs/commands/signals.js.map +1 -0
- package/dist/cjs/commands/status.d.cts +2 -2
- package/dist/cjs/commands/status.d.ts +2 -2
- package/dist/cjs/commands/status.js +1 -1
- package/dist/cjs/commands/status.js.map +1 -1
- package/dist/cjs/config/access.d.cts +5 -2
- package/dist/cjs/config/access.d.ts +5 -2
- package/dist/cjs/config/access.js +30 -12
- package/dist/cjs/config/access.js.map +1 -1
- package/dist/cjs/config/index.d.cts +4 -1
- package/dist/cjs/config/index.d.ts +4 -1
- package/dist/cjs/config/index.js +12 -2
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/config/load.d.cts +6 -1
- package/dist/cjs/config/load.d.ts +6 -1
- package/dist/cjs/config/load.js +83 -8
- package/dist/cjs/config/load.js.map +1 -1
- package/dist/cjs/config/resolve.js +1 -1
- package/dist/cjs/config/resolve.js.map +1 -1
- package/dist/cjs/config/signals.d.cts +4 -0
- package/dist/cjs/config/signals.d.ts +4 -0
- package/dist/cjs/config/signals.js +30 -0
- package/dist/cjs/config/signals.js.map +1 -0
- package/dist/cjs/config/types.d.cts +6 -4
- package/dist/cjs/config/types.d.ts +6 -4
- package/dist/cjs/config/types.js +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/config/validate.js +120 -11
- package/dist/cjs/config/validate.js.map +1 -1
- package/dist/cjs/db/open.d.cts +1 -1
- package/dist/cjs/db/open.d.ts +1 -1
- package/dist/cjs/db/open.js +6 -1
- package/dist/cjs/db/open.js.map +1 -1
- package/dist/cjs/db/reconcile.d.cts +1 -0
- package/dist/cjs/db/reconcile.d.ts +1 -0
- package/dist/cjs/db/reconcile.js +15 -0
- package/dist/cjs/db/reconcile.js.map +1 -1
- package/dist/cjs/embed/cohere.d.cts +2 -0
- package/dist/cjs/embed/cohere.d.ts +2 -0
- package/dist/cjs/embed/cohere.js +238 -0
- package/dist/cjs/embed/cohere.js.map +1 -0
- package/dist/cjs/embed/http.d.cts +2 -0
- package/dist/cjs/embed/http.d.ts +2 -0
- package/dist/cjs/embed/http.js +258 -0
- package/dist/cjs/embed/http.js.map +1 -0
- package/dist/cjs/embed/identity.d.cts +13 -0
- package/dist/cjs/embed/identity.d.ts +13 -0
- package/dist/cjs/embed/identity.js +194 -0
- package/dist/cjs/embed/identity.js.map +1 -0
- package/dist/cjs/embed/langfit.d.cts +6 -0
- package/dist/cjs/embed/langfit.d.ts +6 -0
- package/dist/cjs/embed/langfit.js +326 -0
- package/dist/cjs/embed/langfit.js.map +1 -0
- package/dist/cjs/embed/languages.d.cts +3 -0
- package/dist/cjs/embed/languages.d.ts +3 -0
- package/dist/cjs/embed/languages.js +98 -0
- package/dist/cjs/embed/languages.js.map +1 -0
- package/dist/cjs/embed/openai.d.cts +2 -0
- package/dist/cjs/embed/openai.d.ts +2 -0
- package/dist/cjs/embed/openai.js +268 -0
- package/dist/cjs/embed/openai.js.map +1 -0
- package/dist/cjs/embed/query.d.cts +21 -0
- package/dist/cjs/embed/query.d.ts +21 -0
- package/dist/cjs/embed/query.js +610 -0
- package/dist/cjs/embed/query.js.map +1 -0
- package/dist/cjs/embed/registry.d.cts +3 -0
- package/dist/cjs/embed/registry.d.ts +3 -0
- package/dist/cjs/embed/registry.js +114 -0
- package/dist/cjs/embed/registry.js.map +1 -0
- package/dist/cjs/embed/static.d.cts +2 -0
- package/dist/cjs/embed/static.d.ts +2 -0
- package/dist/cjs/embed/static.js +367 -0
- package/dist/cjs/embed/static.js.map +1 -0
- package/dist/cjs/embed/store.d.cts +8 -0
- package/dist/cjs/embed/store.d.ts +8 -0
- package/dist/cjs/embed/store.js +498 -0
- package/dist/cjs/embed/store.js.map +1 -0
- package/dist/cjs/embed/types.d.cts +8 -0
- package/dist/cjs/embed/types.d.ts +8 -0
- package/dist/cjs/embed/types.js +7 -0
- package/dist/cjs/embed/types.js.map +1 -0
- package/dist/cjs/errors.d.cts +1 -1
- package/dist/cjs/errors.d.ts +1 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/features/embed.d.cts +4 -27
- package/dist/cjs/features/embed.d.ts +4 -27
- package/dist/cjs/features/embed.js +3 -969
- package/dist/cjs/features/embed.js.map +1 -1
- package/dist/cjs/index.d.cts +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/output.d.cts +2 -2
- package/dist/cjs/output.d.ts +2 -2
- package/dist/cjs/output.js +9 -2
- package/dist/cjs/output.js.map +1 -1
- package/dist/cjs/scan.js +8 -5
- package/dist/cjs/scan.js.map +1 -1
- package/dist/cjs/segment.js +67 -14
- package/dist/cjs/segment.js.map +1 -1
- package/dist/esm/cli/download.js +9 -8
- package/dist/esm/cli/download.js.map +1 -1
- package/dist/esm/cli/index.d.ts +1 -1
- package/dist/esm/cli/index.js +1 -1
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/cli/init.js +32 -6
- package/dist/esm/cli/init.js.map +1 -1
- package/dist/esm/cli/status.js +36 -8
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/commands/related.js +14 -10
- package/dist/esm/commands/related.js.map +1 -1
- package/dist/esm/commands/search.js +27 -78
- package/dist/esm/commands/search.js.map +1 -1
- package/dist/esm/commands/signals.d.ts +18 -0
- package/dist/esm/commands/signals.js +78 -0
- package/dist/esm/commands/signals.js.map +1 -0
- package/dist/esm/commands/status.d.ts +2 -2
- package/dist/esm/commands/status.js +2 -2
- package/dist/esm/commands/status.js.map +1 -1
- package/dist/esm/config/access.d.ts +5 -2
- package/dist/esm/config/access.js +31 -12
- package/dist/esm/config/access.js.map +1 -1
- package/dist/esm/config/index.d.ts +4 -1
- package/dist/esm/config/index.js +2 -1
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/config/load.d.ts +6 -1
- package/dist/esm/config/load.js +67 -9
- package/dist/esm/config/load.js.map +1 -1
- package/dist/esm/config/resolve.js +2 -2
- package/dist/esm/config/resolve.js.map +1 -1
- package/dist/esm/config/signals.d.ts +4 -0
- package/dist/esm/config/signals.js +14 -0
- package/dist/esm/config/signals.js.map +1 -0
- package/dist/esm/config/types.d.ts +6 -4
- package/dist/esm/config/types.js +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/config/validate.js +58 -11
- package/dist/esm/config/validate.js.map +1 -1
- package/dist/esm/db/open.d.ts +1 -1
- package/dist/esm/db/open.js +7 -2
- package/dist/esm/db/open.js.map +1 -1
- package/dist/esm/db/reconcile.d.ts +1 -0
- package/dist/esm/db/reconcile.js +10 -0
- package/dist/esm/db/reconcile.js.map +1 -1
- package/dist/esm/embed/cohere.d.ts +2 -0
- package/dist/esm/embed/cohere.js +46 -0
- package/dist/esm/embed/cohere.js.map +1 -0
- package/dist/esm/embed/http.d.ts +2 -0
- package/dist/esm/embed/http.js +33 -0
- package/dist/esm/embed/http.js.map +1 -0
- package/dist/esm/embed/identity.d.ts +13 -0
- package/dist/esm/embed/identity.js +92 -0
- package/dist/esm/embed/identity.js.map +1 -0
- package/dist/esm/embed/langfit.d.ts +6 -0
- package/dist/esm/embed/langfit.js +44 -0
- package/dist/esm/embed/langfit.js.map +1 -0
- package/dist/esm/embed/languages.d.ts +3 -0
- package/dist/esm/embed/languages.js +80 -0
- package/dist/esm/embed/languages.js.map +1 -0
- package/dist/esm/embed/openai.d.ts +2 -0
- package/dist/esm/embed/openai.js +49 -0
- package/dist/esm/embed/openai.js.map +1 -0
- package/dist/esm/embed/query.d.ts +21 -0
- package/dist/esm/embed/query.js +181 -0
- package/dist/esm/embed/query.js.map +1 -0
- package/dist/esm/embed/registry.d.ts +3 -0
- package/dist/esm/embed/registry.js +45 -0
- package/dist/esm/embed/registry.js.map +1 -0
- package/dist/esm/embed/static.d.ts +2 -0
- package/dist/esm/embed/static.js +72 -0
- package/dist/esm/embed/static.js.map +1 -0
- package/dist/esm/embed/store.d.ts +8 -0
- package/dist/esm/embed/store.js +123 -0
- package/dist/esm/embed/store.js.map +1 -0
- package/dist/esm/embed/types.d.ts +8 -0
- package/dist/esm/embed/types.js +3 -0
- package/dist/esm/embed/types.js.map +1 -0
- package/dist/esm/errors.d.ts +1 -1
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/features/embed.d.ts +4 -27
- package/dist/esm/features/embed.js +0 -346
- package/dist/esm/features/embed.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/output.d.ts +2 -2
- package/dist/esm/output.js +6 -2
- package/dist/esm/output.js.map +1 -1
- package/dist/esm/scan.js +7 -4
- package/dist/esm/scan.js.map +1 -1
- package/dist/esm/segment.js +49 -19
- package/dist/esm/segment.js.map +1 -1
- package/package.json +9 -1
- package/schema.json +14 -7
- package/skills/sense/SKILL.md +21 -5
- package/skills/sense-setup/EXAMPLES.md +6 -6
- package/skills/sense-setup/SKILL.md +6 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/static.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { SenseError } from '../errors.ts';\nimport { downloadModel, isDownloadable, MODEL_FILENAMES, MODEL_FILES, modelDir, readLanguages } from './store.ts';\nimport type { EmbedProvider } from './types.ts';\n\nconst BATCH_CAP = 64;\n\n// Model2Vec safetensors + pure-JS tokenizer. Encode convention from model2vec/model.py: no\n// special tokens, drop unk ids, mean-pool, L2-normalize.\nexport async function staticProvider(model: string): Promise<EmbedProvider> {\n let dir: string;\n let languages: string[] | undefined;\n if (isDownloadable(model)) {\n // Naming a HF id is consent: fetch whatever is missing now, announcing it on stderr.\n dir = await downloadModel(model, (file, into) => console.error(`sense: fetching ${model}/${file} into ${into}`));\n // Cached beside the repo; a local model directory has no card to read, so it has none.\n const cached = readLanguages(model);\n languages = cached && cached.length > 0 ? cached : undefined;\n } else {\n dir = modelDir(model);\n for (const file of MODEL_FILES) {\n if (!existsSync(join(dir, file))) {\n throw new SenseError('EMBED_MODEL_MISSING', `embed model ${model} is not available (looked in ${dir}); expected ${MODEL_FILENAMES} in that directory`);\n }\n }\n }\n\n const raw = readFileSync(join(dir, 'model.safetensors'));\n const headerLen = Number(raw.readBigUInt64LE(0));\n const header = JSON.parse(raw.subarray(8, 8 + headerLen).toString('utf8')) as Record<string, { dtype: string; shape: number[]; data_offsets: number[] }>;\n const entry = Object.entries(header).find(([k]) => k !== '__metadata__');\n if (!entry || entry[1].dtype !== 'F32') throw new SenseError('EMBED_MODEL', `${model}: expected an F32 safetensors matrix`);\n const spec = entry[1];\n const dims = spec.shape[1];\n const dataStart = raw.byteOffset + 8 + headerLen + spec.data_offsets[0];\n const matrix = dataStart % 4 === 0 ? new Float32Array(raw.buffer, dataStart, spec.shape[0] * dims) : new Float32Array(raw.buffer.slice(dataStart, dataStart + spec.shape[0] * dims * 4));\n\n const tokenizerJson = JSON.parse(readFileSync(join(dir, 'tokenizer.json'), 'utf8'));\n // Lazy import: the tokenizer loads only on the semantic path, never at CLI startup.\n const { Tokenizer } = await import('@huggingface/tokenizers');\n const tok = new Tokenizer(tokenizerJson, {});\n const unkId = tokenizerJson.model?.vocab?.[tokenizerJson.model?.unk_token] ?? -1;\n\n function one(text: string): Float32Array {\n // The tokenizer yields undefined (not the unk id) for tokens outside the vocab; an\n // undefined id would index the matrix at NaN and poison the whole mean-pool -- and the\n // int8 conversion then stores the NaN vector as all zeros, silently. Keep integers only.\n const ids = (tok.encode(text, { add_special_tokens: false }).ids as number[]).filter((id) => Number.isInteger(id) && id !== unkId);\n const v = new Float32Array(dims);\n if (ids.length === 0) return v;\n for (const id of ids) {\n const off = id * dims;\n for (let d = 0; d < dims; d++) v[d] += matrix[off + d];\n }\n let norm = 0;\n for (let d = 0; d < dims; d++) {\n v[d] /= ids.length;\n norm += v[d] * v[d];\n }\n norm = Math.sqrt(norm) + 1e-32;\n for (let d = 0; d < dims; d++) v[d] /= norm;\n return v;\n }\n\n // Symmetric model: document and query embedding are the same call.\n return { id: `static:${model}`, dims, batchCap: BATCH_CAP, languages, embedDocuments: async (texts) => texts.map(one), embedQuery: async (text) => one(text) };\n}\n"],"names":["existsSync","readFileSync","join","SenseError","downloadModel","isDownloadable","MODEL_FILENAMES","MODEL_FILES","modelDir","readLanguages","BATCH_CAP","staticProvider","model","tokenizerJson","dir","languages","file","into","console","error","cached","length","undefined","raw","headerLen","Number","readBigUInt64LE","header","JSON","parse","subarray","toString","entry","Object","entries","find","k","dtype","spec","dims","shape","dataStart","byteOffset","data_offsets","matrix","Float32Array","buffer","slice","Tokenizer","tok","unkId","vocab","unk_token","one","text","ids","encode","add_special_tokens","filter","id","isInteger","v","off","d","norm","Math","sqrt","batchCap","embedDocuments","texts","map","embedQuery"],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Config } from '../config/index.js';
|
|
2
|
+
import { embedConfig } from '../config/index.js';
|
|
3
|
+
export { hasModelFiles, isDownloadable, MODEL_FILENAMES, MODEL_FILES, modelDir, readLanguages, refsMainPath, snapshotDir, writeLanguages, writeRef } from './identity.js';
|
|
4
|
+
export declare function modelPresent(cfg: Config): boolean;
|
|
5
|
+
type ResolvedEmbedConfig = NonNullable<ReturnType<typeof embedConfig>>;
|
|
6
|
+
export declare function localModelMissing(e: ResolvedEmbedConfig | null): e is ResolvedEmbedConfig;
|
|
7
|
+
export declare function downloadModelRevision(model: string, sha: string, onFile?: (file: string, dir: string) => void): Promise<string>;
|
|
8
|
+
export declare function downloadModel(model: string, onFile?: (file: string, dir: string) => void): Promise<string>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { embedConfig } from '../config/index.js';
|
|
4
|
+
import { SenseError } from '../errors.js';
|
|
5
|
+
import { fetchWithRetry } from './http.js';
|
|
6
|
+
import { hasModelFiles, isDownloadable, languagesPath, MODEL_FILES, readRef, snapshotDir, writeLanguages, writeRef } from './identity.js';
|
|
7
|
+
import { isKnownLanguageTag, MEASURED_MODEL_LANGUAGES } from './languages.js';
|
|
8
|
+
// identity.ts holds paths and cache identity, no network; store.ts holds the network fetches
|
|
9
|
+
// (downloads, sha resolution, language metadata). Re-exported here for callers that otherwise
|
|
10
|
+
// only need store.ts's network surface.
|
|
11
|
+
export { hasModelFiles, isDownloadable, MODEL_FILENAMES, MODEL_FILES, modelDir, readLanguages, refsMainPath, snapshotDir, writeLanguages, writeRef } from './identity.js';
|
|
12
|
+
// A non-static provider has nothing to download here; an unreachable endpoint surfaces as an
|
|
13
|
+
// EMBED_MODEL error at call time instead.
|
|
14
|
+
export function modelPresent(cfg) {
|
|
15
|
+
const e = embedConfig(cfg);
|
|
16
|
+
if (!e) return false;
|
|
17
|
+
return e.provider !== 'static' || hasModelFiles(e.model);
|
|
18
|
+
}
|
|
19
|
+
// True when embed.model names a local path (not a Hugging Face id) that is missing its model
|
|
20
|
+
// files -- nothing will ever fetch it for itself, unlike a downloadable id.
|
|
21
|
+
export function localModelMissing(e) {
|
|
22
|
+
return e !== null && e.provider === 'static' && !isDownloadable(e.model) && !hasModelFiles(e.model);
|
|
23
|
+
}
|
|
24
|
+
// `main` resolves to a sha once per model: a recorded ref pins until its snapshot is
|
|
25
|
+
// removed, so only the first need for an id ever costs this HEAD request.
|
|
26
|
+
async function resolveSha(model) {
|
|
27
|
+
const existing = readRef(model);
|
|
28
|
+
if (existing) return existing;
|
|
29
|
+
const url = `https://huggingface.co/${model}/resolve/main/model.safetensors`;
|
|
30
|
+
// redirect: manual -- the header rides the Hub's own response; following the LFS/CDN
|
|
31
|
+
// redirect would read the CDN's headers instead, which do not carry it.
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetchWithRetry(url, {
|
|
35
|
+
method: 'HEAD',
|
|
36
|
+
redirect: 'manual'
|
|
37
|
+
});
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
const sha = res.headers.get('x-repo-commit');
|
|
42
|
+
if (!sha) throw new SenseError('EMBED_MODEL', `${url}: response carried no x-repo-commit header`);
|
|
43
|
+
writeRef(model, sha);
|
|
44
|
+
return sha;
|
|
45
|
+
}
|
|
46
|
+
// cardData.language is the structured field (verified live: minishlab/potion-retrieval-32M has
|
|
47
|
+
// neither this nor any language tag, so it stays silent; potion-multilingual-128M carries an
|
|
48
|
+
// array here). Tags are a fallback for cards that only tag languages, filtered to recognized
|
|
49
|
+
// codes so an unrelated short tag (format markers etc.) is never mistaken for one.
|
|
50
|
+
function languagesFromCard(info) {
|
|
51
|
+
var _info_tags;
|
|
52
|
+
var _info_cardData;
|
|
53
|
+
const lang = (_info_cardData = info.cardData) === null || _info_cardData === void 0 ? void 0 : _info_cardData.language;
|
|
54
|
+
if (typeof lang === 'string') return [
|
|
55
|
+
lang
|
|
56
|
+
];
|
|
57
|
+
if (Array.isArray(lang) && lang.length > 0) return lang;
|
|
58
|
+
const tagLangs = ((_info_tags = info.tags) !== null && _info_tags !== void 0 ? _info_tags : []).filter(isKnownLanguageTag);
|
|
59
|
+
return tagLangs.length > 0 ? tagLangs : undefined;
|
|
60
|
+
}
|
|
61
|
+
// Best-effort, cached forever once resolved (even to "none"): unlike model weights, missing or
|
|
62
|
+
// unreachable metadata is not an error -- it just means the language-fit check stays off.
|
|
63
|
+
async function ensureLanguages(model) {
|
|
64
|
+
var _languagesFromCard;
|
|
65
|
+
if (existsSync(languagesPath(model))) return;
|
|
66
|
+
// A measured entry needs no card and no network, and stays deterministic offline.
|
|
67
|
+
const measured = MEASURED_MODEL_LANGUAGES[model];
|
|
68
|
+
if (measured) {
|
|
69
|
+
writeLanguages(model, measured);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let res;
|
|
73
|
+
try {
|
|
74
|
+
res = await fetchWithRetry(`https://huggingface.co/api/models/${model}`, {});
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!res.ok) return;
|
|
79
|
+
let info;
|
|
80
|
+
try {
|
|
81
|
+
info = await res.json();
|
|
82
|
+
} catch {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
writeLanguages(model, (_languagesFromCard = languagesFromCard(info)) !== null && _languagesFromCard !== void 0 ? _languagesFromCard : []);
|
|
86
|
+
}
|
|
87
|
+
async function fetchToFile(url, dest) {
|
|
88
|
+
let res;
|
|
89
|
+
try {
|
|
90
|
+
res = await fetchWithRetry(url, {});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
if (!res.ok) throw new SenseError('EMBED_MODEL', `${url} -> HTTP ${res.status}`);
|
|
95
|
+
writeFileSync(`${dest}.part`, Buffer.from(await res.arrayBuffer()));
|
|
96
|
+
renameSync(`${dest}.part`, dest);
|
|
97
|
+
}
|
|
98
|
+
// Fetches an already-known sha straight into its snapshot dir, without touching refs/main --
|
|
99
|
+
// for downloadModel below, and for callers pinning an exact revision (the parity suite).
|
|
100
|
+
export async function downloadModelRevision(model, sha, onFile) {
|
|
101
|
+
const dir = snapshotDir(model, sha);
|
|
102
|
+
mkdirSync(dir, {
|
|
103
|
+
recursive: true
|
|
104
|
+
});
|
|
105
|
+
for (const file of MODEL_FILES){
|
|
106
|
+
if (existsSync(join(dir, file))) continue;
|
|
107
|
+
onFile === null || onFile === void 0 ? void 0 : onFile(file, dir);
|
|
108
|
+
await fetchToFile(`https://huggingface.co/${model}/resolve/${sha}/${file}`, join(dir, file));
|
|
109
|
+
}
|
|
110
|
+
return dir;
|
|
111
|
+
}
|
|
112
|
+
// The only code path that resolves `main`: a static provider's first construction calls it
|
|
113
|
+
// lazily, `sense download` as an explicit prefetch. Idempotent either way.
|
|
114
|
+
export async function downloadModel(model, onFile) {
|
|
115
|
+
if (!isDownloadable(model)) {
|
|
116
|
+
throw new SenseError('EMBED_MODEL', `embed.model "${model}" is a local path, not a Hugging Face model id, so there is nothing to download; put model.safetensors and tokenizer.json in that directory, or name a model id like "minishlab/potion-retrieval-32M"`);
|
|
117
|
+
}
|
|
118
|
+
const sha = await resolveSha(model);
|
|
119
|
+
// Independent of the weights above: a fresh HEAD/GET pair either way, so a lazy first
|
|
120
|
+
// construction and an explicit prefetch both end up with a cached language verdict.
|
|
121
|
+
await ensureLanguages(model);
|
|
122
|
+
return downloadModelRevision(model, sha, onFile);
|
|
123
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/store.ts"],"sourcesContent":["import { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Config } from '../config/index.ts';\nimport { embedConfig } from '../config/index.ts';\nimport { SenseError } from '../errors.ts';\nimport { fetchWithRetry } from './http.ts';\nimport { hasModelFiles, isDownloadable, languagesPath, MODEL_FILES, readRef, snapshotDir, writeLanguages, writeRef } from './identity.ts';\nimport { isKnownLanguageTag, MEASURED_MODEL_LANGUAGES } from './languages.ts';\n\n// identity.ts holds paths and cache identity, no network; store.ts holds the network fetches\n// (downloads, sha resolution, language metadata). Re-exported here for callers that otherwise\n// only need store.ts's network surface.\nexport { hasModelFiles, isDownloadable, MODEL_FILENAMES, MODEL_FILES, modelDir, readLanguages, refsMainPath, snapshotDir, writeLanguages, writeRef } from './identity.ts';\n\n// A non-static provider has nothing to download here; an unreachable endpoint surfaces as an\n// EMBED_MODEL error at call time instead.\nexport function modelPresent(cfg: Config): boolean {\n const e = embedConfig(cfg);\n if (!e) return false;\n return e.provider !== 'static' || hasModelFiles(e.model);\n}\n\ntype ResolvedEmbedConfig = NonNullable<ReturnType<typeof embedConfig>>;\n\n// True when embed.model names a local path (not a Hugging Face id) that is missing its model\n// files -- nothing will ever fetch it for itself, unlike a downloadable id.\nexport function localModelMissing(e: ResolvedEmbedConfig | null): e is ResolvedEmbedConfig {\n return e !== null && e.provider === 'static' && !isDownloadable(e.model) && !hasModelFiles(e.model);\n}\n\n// `main` resolves to a sha once per model: a recorded ref pins until its snapshot is\n// removed, so only the first need for an id ever costs this HEAD request.\nasync function resolveSha(model: string): Promise<string> {\n const existing = readRef(model);\n if (existing) return existing;\n const url = `https://huggingface.co/${model}/resolve/main/model.safetensors`;\n // redirect: manual -- the header rides the Hub's own response; following the LFS/CDN\n // redirect would read the CDN's headers instead, which do not carry it.\n let res: Response;\n try {\n res = await fetchWithRetry(url, { method: 'HEAD', redirect: 'manual' });\n } catch (err) {\n throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${(err as Error).message}`);\n }\n const sha = res.headers.get('x-repo-commit');\n if (!sha) throw new SenseError('EMBED_MODEL', `${url}: response carried no x-repo-commit header`);\n writeRef(model, sha);\n return sha;\n}\n\ninterface HfModelInfo {\n cardData?: { language?: string | string[] };\n tags?: string[];\n}\n\n// cardData.language is the structured field (verified live: minishlab/potion-retrieval-32M has\n// neither this nor any language tag, so it stays silent; potion-multilingual-128M carries an\n// array here). Tags are a fallback for cards that only tag languages, filtered to recognized\n// codes so an unrelated short tag (format markers etc.) is never mistaken for one.\nfunction languagesFromCard(info: HfModelInfo): string[] | undefined {\n const lang = info.cardData?.language;\n if (typeof lang === 'string') return [lang];\n if (Array.isArray(lang) && lang.length > 0) return lang;\n const tagLangs = (info.tags ?? []).filter(isKnownLanguageTag);\n return tagLangs.length > 0 ? tagLangs : undefined;\n}\n\n// Best-effort, cached forever once resolved (even to \"none\"): unlike model weights, missing or\n// unreachable metadata is not an error -- it just means the language-fit check stays off.\nasync function ensureLanguages(model: string): Promise<void> {\n if (existsSync(languagesPath(model))) return;\n // A measured entry needs no card and no network, and stays deterministic offline.\n const measured = MEASURED_MODEL_LANGUAGES[model];\n if (measured) {\n writeLanguages(model, measured);\n return;\n }\n let res: Response;\n try {\n res = await fetchWithRetry(`https://huggingface.co/api/models/${model}`, {});\n } catch {\n return;\n }\n if (!res.ok) return;\n let info: HfModelInfo;\n try {\n info = (await res.json()) as HfModelInfo;\n } catch {\n return;\n }\n writeLanguages(model, languagesFromCard(info) ?? []);\n}\n\nasync function fetchToFile(url: string, dest: string): Promise<void> {\n let res: Response;\n try {\n res = await fetchWithRetry(url, {});\n } catch (err) {\n throw new SenseError('EMBED_MODEL', `could not reach ${url}: ${(err as Error).message}`);\n }\n if (!res.ok) throw new SenseError('EMBED_MODEL', `${url} -> HTTP ${res.status}`);\n writeFileSync(`${dest}.part`, Buffer.from(await res.arrayBuffer()));\n renameSync(`${dest}.part`, dest);\n}\n\n// Fetches an already-known sha straight into its snapshot dir, without touching refs/main --\n// for downloadModel below, and for callers pinning an exact revision (the parity suite).\nexport async function downloadModelRevision(model: string, sha: string, onFile?: (file: string, dir: string) => void): Promise<string> {\n const dir = snapshotDir(model, sha);\n mkdirSync(dir, { recursive: true });\n for (const file of MODEL_FILES) {\n if (existsSync(join(dir, file))) continue;\n onFile?.(file, dir);\n await fetchToFile(`https://huggingface.co/${model}/resolve/${sha}/${file}`, join(dir, file));\n }\n return dir;\n}\n\n// The only code path that resolves `main`: a static provider's first construction calls it\n// lazily, `sense download` as an explicit prefetch. Idempotent either way.\nexport async function downloadModel(model: string, onFile?: (file: string, dir: string) => void): Promise<string> {\n if (!isDownloadable(model)) {\n throw new SenseError('EMBED_MODEL', `embed.model \"${model}\" is a local path, not a Hugging Face model id, so there is nothing to download; put model.safetensors and tokenizer.json in that directory, or name a model id like \"minishlab/potion-retrieval-32M\"`);\n }\n const sha = await resolveSha(model);\n // Independent of the weights above: a fresh HEAD/GET pair either way, so a lazy first\n // construction and an explicit prefetch both end up with a cached language verdict.\n await ensureLanguages(model);\n return downloadModelRevision(model, sha, onFile);\n}\n"],"names":["existsSync","mkdirSync","renameSync","writeFileSync","join","embedConfig","SenseError","fetchWithRetry","hasModelFiles","isDownloadable","languagesPath","MODEL_FILES","readRef","snapshotDir","writeLanguages","writeRef","isKnownLanguageTag","MEASURED_MODEL_LANGUAGES","MODEL_FILENAMES","modelDir","readLanguages","refsMainPath","modelPresent","cfg","e","provider","model","localModelMissing","resolveSha","existing","url","res","method","redirect","err","message","sha","headers","get","languagesFromCard","info","lang","cardData","language","Array","isArray","length","tagLangs","tags","filter","undefined","ensureLanguages","measured","ok","json","fetchToFile","dest","status","Buffer","from","arrayBuffer","downloadModelRevision","onFile","dir","recursive","file","downloadModel"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/embed/types.ts"],"sourcesContent":["// The provider contract every wire protocol implements: a Liskov-substitutable\n// surface so query/search code never branches on provider identity.\nexport interface EmbedProvider {\n id: string; // model identity; participates in the cache key, change -> re-embed\n dims: number;\n batchCap: number; // max texts per embedDocuments call\n languages?: string[]; // declared model languages, for the language-fit check\n embedDocuments(texts: string[]): Promise<Float32Array[]>;\n embedQuery(text: string): Promise<Float32Array>;\n}\n"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,oEAAoE;AACpE,WAOC"}
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_INVALID' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE' | 'NOTE_NOT_FOUND' | 'NOTE_AMBIGUOUS' | 'EMBED_DISABLED' | 'EMBED_MODEL' | 'EMBED_MODEL_MISSING' | 'SEARCH_SYNTAX' | 'COLUMN_LIMIT' | 'PRESET_UNKNOWN' | 'PRESET_NOT_SEMANTIC' | 'NOTE_NOT_EMBEDDED';
|
|
1
|
+
export type SenseErrorCode = 'CONFIG_NOT_FOUND' | 'CONFIG_EXISTS' | 'CONFIG_INVALID' | 'CONFIG_VERSION_UNSUPPORTED' | 'WATCH_ACTIVE' | 'NOTE_NOT_FOUND' | 'NOTE_AMBIGUOUS' | 'EMBED_DISABLED' | 'EMBED_MODEL' | 'EMBED_MODEL_MISSING' | 'EMBED_MODEL_MISMATCH' | 'SEARCH_SYNTAX' | 'COLUMN_LIMIT' | 'PRESET_UNKNOWN' | 'PRESET_NOT_SEMANTIC' | 'NOTE_NOT_EMBEDDED';
|
|
2
2
|
export declare class SenseError extends Error {
|
|
3
3
|
code: SenseErrorCode;
|
|
4
4
|
constructor(code: SenseErrorCode, message: string);
|
package/dist/esm/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Library code throws; only cli.ts prints and exits.\nexport type SenseErrorCode =\n | 'CONFIG_NOT_FOUND'\n | 'CONFIG_EXISTS'\n | 'CONFIG_INVALID'\n | 'CONFIG_VERSION_UNSUPPORTED'\n | 'WATCH_ACTIVE'\n | 'NOTE_NOT_FOUND'\n | 'NOTE_AMBIGUOUS'\n | 'EMBED_DISABLED'\n | 'EMBED_MODEL'\n | 'EMBED_MODEL_MISSING'\n | 'SEARCH_SYNTAX'\n | 'COLUMN_LIMIT'\n | 'PRESET_UNKNOWN'\n | 'PRESET_NOT_SEMANTIC'\n | 'NOTE_NOT_EMBEDDED';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","Error","code","message","name"],"mappings":"AAAA,qDAAqD;
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/errors.ts"],"sourcesContent":["// Library code throws; only cli.ts prints and exits.\nexport type SenseErrorCode =\n | 'CONFIG_NOT_FOUND'\n | 'CONFIG_EXISTS'\n | 'CONFIG_INVALID'\n | 'CONFIG_VERSION_UNSUPPORTED'\n | 'WATCH_ACTIVE'\n | 'NOTE_NOT_FOUND'\n | 'NOTE_AMBIGUOUS'\n | 'EMBED_DISABLED'\n | 'EMBED_MODEL'\n | 'EMBED_MODEL_MISSING'\n | 'EMBED_MODEL_MISMATCH'\n | 'SEARCH_SYNTAX'\n | 'COLUMN_LIMIT'\n | 'PRESET_UNKNOWN'\n | 'PRESET_NOT_SEMANTIC'\n | 'NOTE_NOT_EMBEDDED';\n\nexport class SenseError extends Error {\n code: SenseErrorCode;\n\n constructor(code: SenseErrorCode, message: string) {\n super(message);\n this.name = 'SenseError';\n this.code = code;\n }\n}\n"],"names":["SenseError","Error","code","message","name"],"mappings":"AAAA,qDAAqD;AAmBrD,OAAO,MAAMA,mBAAmBC;IAG9B,YAAYC,IAAoB,EAAEC,OAAe,CAAE;QACjD,KAAK,CAACA;QACN,IAAI,CAACC,IAAI,GAAG;QACZ,IAAI,CAACF,IAAI,GAAGA;IACd;AACF"}
|
|
@@ -1,30 +1,7 @@
|
|
|
1
|
-
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
import type { Config } from '../config/index.js';
|
|
3
1
|
import type { Feature } from './types.js';
|
|
4
|
-
export interface
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
export interface Chunk {
|
|
3
|
+
startLine: number;
|
|
4
|
+
endLine: number;
|
|
5
|
+
text: string;
|
|
8
6
|
}
|
|
9
7
|
export declare const embed: Feature;
|
|
10
|
-
export declare const MODEL_FILENAMES: string;
|
|
11
|
-
export declare function isDownloadable(model: string): boolean;
|
|
12
|
-
export declare function modelDir(model: string): string;
|
|
13
|
-
export declare function hasModelFiles(model: string): boolean;
|
|
14
|
-
export declare function modelPresent(cfg: Config): boolean;
|
|
15
|
-
export declare function downloadModel(model: string, onFile?: (file: string, dir: string) => void): Promise<string>;
|
|
16
|
-
export declare function embedPending(db: DatabaseSync, cfg: Config, baseDir: string): Promise<void>;
|
|
17
|
-
export declare function semanticCandidates(db: DatabaseSync, cfg: Config, terms: string, fetch: number, allowed?: Set<string>): Promise<Array<{
|
|
18
|
-
path: string;
|
|
19
|
-
lines: string;
|
|
20
|
-
similarity: number;
|
|
21
|
-
}>>;
|
|
22
|
-
export declare function hasEmbedding(db: DatabaseSync, path: string): boolean;
|
|
23
|
-
export declare function similarNotes(db: DatabaseSync, _cfg: Config, path: string, opts: {
|
|
24
|
-
exclude: Set<string>;
|
|
25
|
-
allowed?: Set<string>;
|
|
26
|
-
k: number;
|
|
27
|
-
}): Array<{
|
|
28
|
-
path: string;
|
|
29
|
-
similarity: number;
|
|
30
|
-
}>;
|
|
@@ -1,19 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { embedConfig } from '../config/index.js';
|
|
5
|
-
import { SenseError } from '../errors.js';
|
|
6
1
|
import { fenceTracker } from '../fences.js';
|
|
7
|
-
import { progress } from '../progress.js';
|
|
8
|
-
import { parseFile } from '../scan.js';
|
|
9
|
-
// Heading-based chunks, int8 vectors with a per-vector scale, NULL vector = not yet embedded.
|
|
10
|
-
// Reconcile writes dirty rows; embedding tops up on the next search, so staleness costs recall.
|
|
11
|
-
// Storage lever fixed by the bake-off (BENCHMARKING.md): int8 at 256 dims is
|
|
12
|
-
// quality-free vs f32-512 when fused. Queries stay f32 at the same dims.
|
|
13
|
-
const STORE_DIMS = 256;
|
|
14
|
-
// Seed chunks that participate in a `related` scan; see similarNotes for the measurement.
|
|
15
|
-
const TARGET_CHUNK_CAP = 16;
|
|
16
|
-
const BATCH = 64;
|
|
17
2
|
// Deterministic, so embed time can re-derive text from stored line ranges. Heading-delimited,
|
|
18
3
|
// preamble kept, whole body when no headings; the title/summary prefix mirrors bm25 weighting.
|
|
19
4
|
//
|
|
@@ -77,334 +62,3 @@ export const embed = {
|
|
|
77
62
|
return file.embed;
|
|
78
63
|
}
|
|
79
64
|
};
|
|
80
|
-
// --- static type: Model2Vec safetensors + pure-JS tokenizer, model cached in ~/.cache ---
|
|
81
|
-
// Encode convention from model2vec/model.py: no special tokens, drop unk ids, mean-pool,
|
|
82
|
-
// L2-normalize.
|
|
83
|
-
const MODEL_FILES = [
|
|
84
|
-
'model.safetensors',
|
|
85
|
-
'tokenizer.json'
|
|
86
|
-
];
|
|
87
|
-
// The pair, for messages that name what a local model directory must contain.
|
|
88
|
-
export const MODEL_FILENAMES = MODEL_FILES.join(' and ');
|
|
89
|
-
// A Hugging Face repo id: one slash, HF's charset. Anything else is a path, used as one rather
|
|
90
|
-
// than flattened into a cache key Windows would reject.
|
|
91
|
-
const HF_MODEL_ID = /^[A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*$/;
|
|
92
|
-
// Downloadable iff it names a Hugging Face repo; a local path is the caller's to populate.
|
|
93
|
-
export function isDownloadable(model) {
|
|
94
|
-
return HF_MODEL_ID.test(model);
|
|
95
|
-
}
|
|
96
|
-
// Machine-wide, not per-tree: `.sense/` is the index a rebuild throws away, while a 124 MB
|
|
97
|
-
// model is shared by every tree on the machine. Honors XDG_CACHE_HOME where it is set;
|
|
98
|
-
// elsewhere ~/.cache, which is also where Hugging Face's own libraries keep theirs.
|
|
99
|
-
function cacheRoot() {
|
|
100
|
-
const xdg = process.env.XDG_CACHE_HOME;
|
|
101
|
-
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), '.cache'), 'sensemaking', 'models');
|
|
102
|
-
}
|
|
103
|
-
// A model is either a local directory the caller pointed at, or the cache `sense download`
|
|
104
|
-
// fills. Nothing here fetches: an absent model degrades search to its other signals rather
|
|
105
|
-
// than pulling 124 MB out of a command that reads like a query.
|
|
106
|
-
export function modelDir(model) {
|
|
107
|
-
if (!HF_MODEL_ID.test(model)) return model;
|
|
108
|
-
return join(cacheRoot(), model.replace(/\//g, '--'));
|
|
109
|
-
}
|
|
110
|
-
// Both files, so an interrupted download reads as absent and the next one resumes it.
|
|
111
|
-
export function hasModelFiles(model) {
|
|
112
|
-
const dir = modelDir(model);
|
|
113
|
-
return MODEL_FILES.every((file)=>existsSync(join(dir, file)));
|
|
114
|
-
}
|
|
115
|
-
// api providers have nothing to download, so they are never "missing" here; an unreachable
|
|
116
|
-
// endpoint surfaces as an EMBED_MODEL error at call time instead.
|
|
117
|
-
export function modelPresent(cfg) {
|
|
118
|
-
const e = embedConfig(cfg);
|
|
119
|
-
if (!e) return false;
|
|
120
|
-
return e.type === 'api' || hasModelFiles(e.model);
|
|
121
|
-
}
|
|
122
|
-
function fetchToFile(url, dest) {
|
|
123
|
-
return fetch(url).then(async (res)=>{
|
|
124
|
-
if (!res.ok) throw new SenseError('EMBED_MODEL', `model download failed: ${url} -> HTTP ${res.status}`);
|
|
125
|
-
writeFileSync(`${dest}.part`, Buffer.from(await res.arrayBuffer()));
|
|
126
|
-
renameSync(`${dest}.part`, dest);
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
// The only code path that touches the network for weights. `sense download` calls it; no
|
|
130
|
-
// query ever does. Idempotent: a file already on disk is left alone.
|
|
131
|
-
export async function downloadModel(model, onFile) {
|
|
132
|
-
if (!isDownloadable(model)) {
|
|
133
|
-
throw new SenseError('EMBED_MODEL', `embed.model "${model}" is a local path, not a Hugging Face model id, so there is nothing to download; put model.safetensors and tokenizer.json in that directory, or name a model id like "minishlab/potion-retrieval-32M"`);
|
|
134
|
-
}
|
|
135
|
-
const dir = modelDir(model);
|
|
136
|
-
mkdirSync(dir, {
|
|
137
|
-
recursive: true
|
|
138
|
-
});
|
|
139
|
-
for (const file of MODEL_FILES){
|
|
140
|
-
if (existsSync(join(dir, file))) continue;
|
|
141
|
-
onFile === null || onFile === void 0 ? void 0 : onFile(file, dir);
|
|
142
|
-
await fetchToFile(`https://huggingface.co/${model}/resolve/main/${file}`, join(dir, file));
|
|
143
|
-
}
|
|
144
|
-
return dir;
|
|
145
|
-
}
|
|
146
|
-
async function staticProvider(model) {
|
|
147
|
-
var _ref;
|
|
148
|
-
var _tokenizerJson_model, _tokenizerJson_model_vocab, _tokenizerJson_model1;
|
|
149
|
-
const dir = modelDir(model);
|
|
150
|
-
for (const file of MODEL_FILES){
|
|
151
|
-
if (!existsSync(join(dir, file))) {
|
|
152
|
-
// A local path the caller controls gets told what is missing where; only a repo id can
|
|
153
|
-
// be fixed by downloading.
|
|
154
|
-
const fix = isDownloadable(model) ? 'run `sense download`' : `expected ${MODEL_FILENAMES} in that directory`;
|
|
155
|
-
throw new SenseError('EMBED_MODEL_MISSING', `embed model ${model} is not available (looked in ${dir}); ${fix}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
const raw = readFileSync(join(dir, 'model.safetensors'));
|
|
159
|
-
const headerLen = Number(raw.readBigUInt64LE(0));
|
|
160
|
-
const header = JSON.parse(raw.subarray(8, 8 + headerLen).toString('utf8'));
|
|
161
|
-
const entry = Object.entries(header).find(([k])=>k !== '__metadata__');
|
|
162
|
-
if (!entry || entry[1].dtype !== 'F32') throw new SenseError('EMBED_MODEL', `${model}: expected an F32 safetensors matrix`);
|
|
163
|
-
const spec = entry[1];
|
|
164
|
-
const dims = spec.shape[1];
|
|
165
|
-
const dataStart = raw.byteOffset + 8 + headerLen + spec.data_offsets[0];
|
|
166
|
-
const matrix = dataStart % 4 === 0 ? new Float32Array(raw.buffer, dataStart, spec.shape[0] * dims) : new Float32Array(raw.buffer.slice(dataStart, dataStart + spec.shape[0] * dims * 4));
|
|
167
|
-
const tokenizerJson = JSON.parse(readFileSync(join(dir, 'tokenizer.json'), 'utf8'));
|
|
168
|
-
// Lazy import: the tokenizer loads only on the semantic path, never at CLI startup.
|
|
169
|
-
const { Tokenizer } = await import('@huggingface/tokenizers');
|
|
170
|
-
const tok = new Tokenizer(tokenizerJson, {});
|
|
171
|
-
const unkId = (_ref = (_tokenizerJson_model1 = tokenizerJson.model) === null || _tokenizerJson_model1 === void 0 ? void 0 : (_tokenizerJson_model_vocab = _tokenizerJson_model1.vocab) === null || _tokenizerJson_model_vocab === void 0 ? void 0 : _tokenizerJson_model_vocab[(_tokenizerJson_model = tokenizerJson.model) === null || _tokenizerJson_model === void 0 ? void 0 : _tokenizerJson_model.unk_token]) !== null && _ref !== void 0 ? _ref : -1;
|
|
172
|
-
function one(text) {
|
|
173
|
-
// The tokenizer yields undefined (not the unk id) for tokens outside the vocab; an
|
|
174
|
-
// undefined id would index the matrix at NaN and poison the whole mean-pool -- and the
|
|
175
|
-
// int8 conversion then stores the NaN vector as all zeros, silently. Keep integers only.
|
|
176
|
-
const ids = tok.encode(text, {
|
|
177
|
-
add_special_tokens: false
|
|
178
|
-
}).ids.filter((id)=>Number.isInteger(id) && id !== unkId);
|
|
179
|
-
const v = new Float32Array(dims);
|
|
180
|
-
if (ids.length === 0) return v;
|
|
181
|
-
for (const id of ids){
|
|
182
|
-
const off = id * dims;
|
|
183
|
-
for(let d = 0; d < dims; d++)v[d] += matrix[off + d];
|
|
184
|
-
}
|
|
185
|
-
let norm = 0;
|
|
186
|
-
for(let d = 0; d < dims; d++){
|
|
187
|
-
v[d] /= ids.length;
|
|
188
|
-
norm += v[d] * v[d];
|
|
189
|
-
}
|
|
190
|
-
norm = Math.sqrt(norm) + 1e-32;
|
|
191
|
-
for(let d = 0; d < dims; d++)v[d] /= norm;
|
|
192
|
-
return v;
|
|
193
|
-
}
|
|
194
|
-
return {
|
|
195
|
-
id: `static:${model}`,
|
|
196
|
-
dims,
|
|
197
|
-
embed: async (texts)=>texts.map(one)
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
// --- api type: one POST against any OpenAI-compatible /embeddings endpoint ---
|
|
201
|
-
async function apiProvider(model, url, keyEnv) {
|
|
202
|
-
if (!url) throw new SenseError('EMBED_MODEL', 'features.embed.type "api" requires a url');
|
|
203
|
-
const base = url.replace(/\/+$/, '');
|
|
204
|
-
const headers = {
|
|
205
|
-
'content-type': 'application/json'
|
|
206
|
-
};
|
|
207
|
-
const key = keyEnv ? process.env[keyEnv] : undefined;
|
|
208
|
-
if (key) headers.authorization = `Bearer ${key}`;
|
|
209
|
-
async function post(texts) {
|
|
210
|
-
const res = await fetch(`${base}/embeddings`, {
|
|
211
|
-
method: 'POST',
|
|
212
|
-
headers,
|
|
213
|
-
body: JSON.stringify({
|
|
214
|
-
model,
|
|
215
|
-
input: texts
|
|
216
|
-
})
|
|
217
|
-
});
|
|
218
|
-
if (!res.ok) throw new SenseError('EMBED_MODEL', `${base}/embeddings -> HTTP ${res.status}`);
|
|
219
|
-
const body = await res.json();
|
|
220
|
-
return body.data.map((d)=>Float32Array.from(d.embedding));
|
|
221
|
-
}
|
|
222
|
-
const dims = (await post([
|
|
223
|
-
'dimension probe'
|
|
224
|
-
]))[0].length;
|
|
225
|
-
return {
|
|
226
|
-
id: `api:${base}:${model}`,
|
|
227
|
-
dims,
|
|
228
|
-
embed: post
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
const providers = new Map();
|
|
232
|
-
function getProvider(cfg) {
|
|
233
|
-
var _e_url;
|
|
234
|
-
const e = embedConfig(cfg);
|
|
235
|
-
if (!e) throw new SenseError('EMBED_DISABLED', 'this tree has no embedding model: add an "embed" block naming one to sense.config.json, then run `sense download`');
|
|
236
|
-
const sig = `${e.type}:${e.model}:${(_e_url = e.url) !== null && _e_url !== void 0 ? _e_url : ''}`;
|
|
237
|
-
let p = providers.get(sig);
|
|
238
|
-
if (!p) {
|
|
239
|
-
p = e.type === 'api' ? apiProvider(e.model, e.url, e.key) : staticProvider(e.model);
|
|
240
|
-
providers.set(sig, p);
|
|
241
|
-
}
|
|
242
|
-
return p;
|
|
243
|
-
}
|
|
244
|
-
// Slice + re-normalize (Matryoshka); optionally round through int8 storage.
|
|
245
|
-
function toStore(full, dims, int8) {
|
|
246
|
-
const v = new Float32Array(dims);
|
|
247
|
-
let norm = 0;
|
|
248
|
-
for(let d = 0; d < dims; d++)norm += full[d] * full[d];
|
|
249
|
-
norm = Math.sqrt(norm) + 1e-32;
|
|
250
|
-
for(let d = 0; d < dims; d++)v[d] = full[d] / norm;
|
|
251
|
-
if (!int8) return {
|
|
252
|
-
v,
|
|
253
|
-
scale: 1
|
|
254
|
-
};
|
|
255
|
-
let max = 0;
|
|
256
|
-
for(let d = 0; d < dims; d++)max = Math.max(max, Math.abs(v[d]));
|
|
257
|
-
return {
|
|
258
|
-
v,
|
|
259
|
-
scale: max / 127 || 1
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
// Embed rows whose vector is NULL, re-deriving chunk text from the files through the
|
|
263
|
-
// same parse + chunker that stored the rows.
|
|
264
|
-
export async function embedPending(db, cfg, baseDir) {
|
|
265
|
-
const provider = await getProvider(cfg); // throws EMBED_DISABLED before touching the table
|
|
266
|
-
const dirty = db.prepare('SELECT "path", chunk FROM embeddings WHERE vector IS NULL ORDER BY "path", chunk').all();
|
|
267
|
-
if (dirty.length === 0) return;
|
|
268
|
-
const storeDims = Math.min(STORE_DIMS, provider.dims);
|
|
269
|
-
const byPath = new Map();
|
|
270
|
-
for (const row of dirty){
|
|
271
|
-
var _byPath_get;
|
|
272
|
-
const list = (_byPath_get = byPath.get(row.path)) !== null && _byPath_get !== void 0 ? _byPath_get : [];
|
|
273
|
-
list.push(row.chunk);
|
|
274
|
-
byPath.set(row.path, list);
|
|
275
|
-
}
|
|
276
|
-
const jobs = [];
|
|
277
|
-
for (const [path, chunkIdxs] of byPath){
|
|
278
|
-
let chunks;
|
|
279
|
-
try {
|
|
280
|
-
// presets/embed are irrelevant here -- re-deriving chunk text for a doc that already
|
|
281
|
-
// has embeddings rows means the tree had an embedding model at reconcile time.
|
|
282
|
-
chunks = parseFile({
|
|
283
|
-
relPath: path,
|
|
284
|
-
absPath: join(baseDir, path),
|
|
285
|
-
mtimeMs: 0,
|
|
286
|
-
ctimeMs: 0,
|
|
287
|
-
size: 0,
|
|
288
|
-
presets: [],
|
|
289
|
-
embed: true
|
|
290
|
-
}, [
|
|
291
|
-
embed
|
|
292
|
-
]).doc.extracted.embed;
|
|
293
|
-
} catch {
|
|
294
|
-
continue; // vanished since reconcile; the next reconcile removes its rows
|
|
295
|
-
}
|
|
296
|
-
for (const idx of chunkIdxs)if (chunks[idx]) jobs.push({
|
|
297
|
-
path,
|
|
298
|
-
chunk: idx,
|
|
299
|
-
text: chunks[idx].text
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
const update = db.prepare('UPDATE embeddings SET scale = ?, vector = ? WHERE "path" = ? AND chunk = ?');
|
|
303
|
-
// The lazy build is the one long silence a first search hits (measured 23s at
|
|
304
|
-
// 26k notes); progress makes it distinguishable from a hang.
|
|
305
|
-
const report = progress('embedding chunks', jobs.length);
|
|
306
|
-
for(let i = 0; i < jobs.length; i += BATCH){
|
|
307
|
-
const batch = jobs.slice(i, i + BATCH);
|
|
308
|
-
const vectors = await provider.embed(batch.map((j)=>j.text));
|
|
309
|
-
db.exec('BEGIN');
|
|
310
|
-
try {
|
|
311
|
-
batch.forEach((job, j)=>{
|
|
312
|
-
const { v, scale } = toStore(vectors[j], storeDims, true);
|
|
313
|
-
const q = new Int8Array(storeDims);
|
|
314
|
-
for(let d = 0; d < storeDims; d++)q[d] = Math.round(v[d] / scale);
|
|
315
|
-
update.run(scale, Buffer.from(q.buffer), job.path, job.chunk);
|
|
316
|
-
});
|
|
317
|
-
db.exec('COMMIT');
|
|
318
|
-
} catch (err) {
|
|
319
|
-
db.exec('ROLLBACK');
|
|
320
|
-
throw err;
|
|
321
|
-
}
|
|
322
|
-
report.tick(Math.min(i + BATCH, jobs.length));
|
|
323
|
-
}
|
|
324
|
-
report.finish();
|
|
325
|
-
}
|
|
326
|
-
// Dequantised int8 dot products land a little either side of a true cosine, so an identical
|
|
327
|
-
// pair prints 1.001 and undermines a column whose whole job is being a bounded number.
|
|
328
|
-
function asCosine(score) {
|
|
329
|
-
return Math.round(Math.min(1, Math.max(-1, score)) * 1000) / 1000;
|
|
330
|
-
}
|
|
331
|
-
// Best chunk per file by cosine, its line range riding along; FTS5 operators are stripped as
|
|
332
|
-
// lexical syntax. Similarity comes back because the fused score cannot express match quality,
|
|
333
|
-
// and nearest-neighbour search always returns a neighbour however far away.
|
|
334
|
-
export async function semanticCandidates(db, cfg, terms, fetch1, allowed) {
|
|
335
|
-
var _terms_match;
|
|
336
|
-
const baseDir = cfg.baseDir;
|
|
337
|
-
if (!baseDir) throw new SenseError('EMBED_MODEL', 'semantic expansion needs a config with baseDir (use loadConfig/open)');
|
|
338
|
-
await embedPending(db, cfg, baseDir);
|
|
339
|
-
const provider = await getProvider(cfg);
|
|
340
|
-
const storeDims = Math.min(STORE_DIMS, provider.dims);
|
|
341
|
-
const text = ((_terms_match = terms.match(/[\p{L}\p{N}]+/gu)) !== null && _terms_match !== void 0 ? _terms_match : []).filter((t)=>![
|
|
342
|
-
'AND',
|
|
343
|
-
'OR',
|
|
344
|
-
'NOT',
|
|
345
|
-
'NEAR'
|
|
346
|
-
].includes(t)).join(' ');
|
|
347
|
-
const { v: qv } = toStore((await provider.embed([
|
|
348
|
-
text
|
|
349
|
-
]))[0], storeDims, false);
|
|
350
|
-
const rows = db.prepare('SELECT "path", start_line, end_line, scale, vector FROM embeddings WHERE vector IS NOT NULL').all();
|
|
351
|
-
const best = new Map();
|
|
352
|
-
for (const row of rows){
|
|
353
|
-
if (allowed && !allowed.has(row.path)) continue;
|
|
354
|
-
const q = new Int8Array(row.vector.buffer, row.vector.byteOffset, Math.min(storeDims, row.vector.byteLength));
|
|
355
|
-
let dot = 0;
|
|
356
|
-
for(let d = 0; d < q.length; d++)dot += q[d] * qv[d];
|
|
357
|
-
const score = dot * row.scale;
|
|
358
|
-
const existing = best.get(row.path);
|
|
359
|
-
if (!existing || score > existing.score) best.set(row.path, {
|
|
360
|
-
score,
|
|
361
|
-
lines: `L${row.start_line}-${row.end_line}`
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
return [
|
|
365
|
-
...best.entries()
|
|
366
|
-
].sort((a, b)=>b[1].score - a[1].score).slice(0, fetch1).map(([path, b])=>({
|
|
367
|
-
path,
|
|
368
|
-
lines: b.lines,
|
|
369
|
-
similarity: asCosine(b.score)
|
|
370
|
-
}));
|
|
371
|
-
}
|
|
372
|
-
// Whether a note has any chunk with a vector. Distinguishes "nothing is near this note" from
|
|
373
|
-
// "this note has no text", which look the same in an empty result.
|
|
374
|
-
export function hasEmbedding(db, path) {
|
|
375
|
-
const row = db.prepare('SELECT 1 AS ok FROM embeddings WHERE "path" = ? AND vector IS NOT NULL LIMIT 1').get(path);
|
|
376
|
-
return row !== undefined;
|
|
377
|
-
}
|
|
378
|
-
// Note-to-note similarity is the max cosine over (target chunk, other chunk) pairs, one linear
|
|
379
|
-
// scan of stored vectors. Reads only what is stored, so it stays sync.
|
|
380
|
-
export function similarNotes(db, _cfg, path, opts) {
|
|
381
|
-
// Cost is target_chunks x stored_chunks, so a heading-dense seed multiplies a full-corpus
|
|
382
|
-
// scan (12.7s at 201 chunks/note). Sample evenly, so late sections still get a vote.
|
|
383
|
-
const targetRows = db.prepare('SELECT scale, vector FROM embeddings WHERE "path" = ? AND vector IS NOT NULL ORDER BY chunk').all(path);
|
|
384
|
-
if (targetRows.length === 0) return [];
|
|
385
|
-
const step = Math.max(1, Math.ceil(targetRows.length / TARGET_CHUNK_CAP));
|
|
386
|
-
const target = targetRows.filter((_, i)=>i % step === 0).map((row)=>({
|
|
387
|
-
v: new Int8Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength),
|
|
388
|
-
scale: row.scale
|
|
389
|
-
}));
|
|
390
|
-
const rows = db.prepare('SELECT "path", scale, vector FROM embeddings WHERE vector IS NOT NULL').all();
|
|
391
|
-
const best = new Map();
|
|
392
|
-
for (const row of rows){
|
|
393
|
-
if (row.path === path || opts.exclude.has(row.path) || opts.allowed && !opts.allowed.has(row.path)) continue;
|
|
394
|
-
const other = new Int8Array(row.vector.buffer, row.vector.byteOffset, row.vector.byteLength);
|
|
395
|
-
for (const t of target){
|
|
396
|
-
let dot = 0;
|
|
397
|
-
const len = Math.min(t.v.length, other.length);
|
|
398
|
-
for(let d = 0; d < len; d++)dot += t.v[d] * other[d];
|
|
399
|
-
const score = dot * t.scale * row.scale;
|
|
400
|
-
const existing = best.get(row.path);
|
|
401
|
-
if (existing === undefined || score > existing) best.set(row.path, score);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return [
|
|
405
|
-
...best.entries()
|
|
406
|
-
].sort((a, b)=>b[1] - a[1]).slice(0, opts.k).map(([p, score])=>({
|
|
407
|
-
path: p,
|
|
408
|
-
similarity: asCosine(score)
|
|
409
|
-
}));
|
|
410
|
-
}
|