crbro-memory 2.10.0 → 2.11.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/LICENSE +21 -21
- package/README.md +256 -465
- package/SECURITY.md +114 -12
- package/bin/crbro.mjs +539 -300
- package/dist/build-id.json +10 -0
- package/dist/cli/common.js +211 -0
- package/dist/cli/export.js +522 -0
- package/dist/cli/import.js +683 -0
- package/dist/cli/markdown.js +313 -0
- package/dist/cli/mcpclient.js +107 -0
- package/dist/cli/serve.js +453 -0
- package/dist/cli/view.js +358 -0
- package/dist/cli/viewer-page.js +378 -0
- package/dist/daemon/daemon.js +119 -11
- package/dist/daemon/endpoint.js +222 -21
- package/dist/daemon/lines.js +102 -13
- package/dist/daemon/proxy.js +480 -125
- package/dist/daemon/surface.js +116 -0
- package/dist/daemon/trace.js +73 -0
- package/dist/engine/atrest.js +132 -0
- package/dist/engine/backup.js +139 -19
- package/dist/engine/brain.js +22 -6
- package/dist/engine/codexconfig.js +476 -0
- package/dist/engine/cortex.js +667 -232
- package/dist/engine/doctor.js +312 -14
- package/dist/engine/heat.js +81 -37
- package/dist/engine/hippocampus.js +18 -7
- package/dist/engine/hookrefresh.js +296 -0
- package/dist/engine/hookverify.js +15 -3
- package/dist/engine/installer.js +1022 -0
- package/dist/engine/installio.js +279 -0
- package/dist/engine/keychain.js +721 -162
- package/dist/engine/maintenance.js +355 -87
- package/dist/engine/modinstall.js +98 -45
- package/dist/engine/prefrontal.js +29 -6
- package/dist/engine/quarantine.js +204 -0
- package/dist/engine/secrets.js +41 -5
- package/dist/engine/shelf.js +25 -3
- package/dist/engine/synapses.js +183 -80
- package/dist/engine/triggers.js +141 -11
- package/dist/index.js +37 -3
- package/dist/mcp-surface.json +1 -0
- package/dist/miner/extractor.js +64 -17
- package/dist/miner/scheduler.js +144 -68
- package/dist/search/index.js +797 -176
- package/dist/search/semantic.js +208 -18
- package/dist/search/tokenize.js +44 -0
- package/dist/server.js +583 -373
- package/dist/sync/git.js +203 -72
- package/dist/sync/space.js +100 -25
- package/dist/sync/validate.js +204 -0
- package/dist/utils/budget.js +15 -2
- package/dist/utils/fs.js +40 -6
- package/dist/utils/ids.js +128 -14
- package/dist/utils/lock.js +94 -7
- package/hooks/crbro-codex.mjs +284 -0
- package/hooks/crbro-guard.mjs +281 -173
- package/hooks/crbro-hooks.shipped.json +20 -0
- package/hooks/crbro-lifecycle.mjs +16 -4
- package/hooks/crbro-subagent.mjs +180 -180
- package/package.json +9 -5
- package/dist/daemon/daemon.d.ts +0 -25
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js.map +0 -1
- package/dist/daemon/endpoint.d.ts +0 -66
- package/dist/daemon/endpoint.d.ts.map +0 -1
- package/dist/daemon/endpoint.js.map +0 -1
- package/dist/daemon/lines.d.ts +0 -55
- package/dist/daemon/lines.d.ts.map +0 -1
- package/dist/daemon/lines.js.map +0 -1
- package/dist/daemon/proxy.d.ts +0 -55
- package/dist/daemon/proxy.d.ts.map +0 -1
- package/dist/daemon/proxy.js.map +0 -1
- package/dist/engine/backup.d.ts +0 -68
- package/dist/engine/backup.d.ts.map +0 -1
- package/dist/engine/backup.js.map +0 -1
- package/dist/engine/brain.d.ts +0 -79
- package/dist/engine/brain.d.ts.map +0 -1
- package/dist/engine/brain.js.map +0 -1
- package/dist/engine/cortex.d.ts +0 -682
- package/dist/engine/cortex.d.ts.map +0 -1
- package/dist/engine/cortex.js.map +0 -1
- package/dist/engine/dates.d.ts +0 -28
- package/dist/engine/dates.d.ts.map +0 -1
- package/dist/engine/dates.js.map +0 -1
- package/dist/engine/doctor.d.ts +0 -93
- package/dist/engine/doctor.d.ts.map +0 -1
- package/dist/engine/doctor.js.map +0 -1
- package/dist/engine/evidence.d.ts +0 -26
- package/dist/engine/evidence.d.ts.map +0 -1
- package/dist/engine/evidence.js.map +0 -1
- package/dist/engine/heat.d.ts +0 -28
- package/dist/engine/heat.d.ts.map +0 -1
- package/dist/engine/heat.js.map +0 -1
- package/dist/engine/hippocampus.d.ts +0 -60
- package/dist/engine/hippocampus.d.ts.map +0 -1
- package/dist/engine/hippocampus.js.map +0 -1
- package/dist/engine/hookverify.d.ts +0 -42
- package/dist/engine/hookverify.d.ts.map +0 -1
- package/dist/engine/hookverify.js.map +0 -1
- package/dist/engine/keychain.d.ts +0 -32
- package/dist/engine/keychain.d.ts.map +0 -1
- package/dist/engine/keychain.js.map +0 -1
- package/dist/engine/maintenance.d.ts +0 -218
- package/dist/engine/maintenance.d.ts.map +0 -1
- package/dist/engine/maintenance.js.map +0 -1
- package/dist/engine/modinstall.d.ts +0 -215
- package/dist/engine/modinstall.d.ts.map +0 -1
- package/dist/engine/modinstall.js.map +0 -1
- package/dist/engine/postmortem.d.ts +0 -81
- package/dist/engine/postmortem.d.ts.map +0 -1
- package/dist/engine/postmortem.js.map +0 -1
- package/dist/engine/prefrontal.d.ts +0 -64
- package/dist/engine/prefrontal.d.ts.map +0 -1
- package/dist/engine/prefrontal.js.map +0 -1
- package/dist/engine/projects.d.ts +0 -20
- package/dist/engine/projects.d.ts.map +0 -1
- package/dist/engine/projects.js.map +0 -1
- package/dist/engine/secrets.d.ts +0 -30
- package/dist/engine/secrets.d.ts.map +0 -1
- package/dist/engine/secrets.js.map +0 -1
- package/dist/engine/shelf.d.ts +0 -155
- package/dist/engine/shelf.d.ts.map +0 -1
- package/dist/engine/shelf.js.map +0 -1
- package/dist/engine/source.d.ts +0 -6
- package/dist/engine/source.d.ts.map +0 -1
- package/dist/engine/source.js.map +0 -1
- package/dist/engine/synapses.d.ts +0 -82
- package/dist/engine/synapses.d.ts.map +0 -1
- package/dist/engine/synapses.js.map +0 -1
- package/dist/engine/triggers.d.ts +0 -53
- package/dist/engine/triggers.d.ts.map +0 -1
- package/dist/engine/triggers.js.map +0 -1
- package/dist/engine/usage.d.ts +0 -61
- package/dist/engine/usage.d.ts.map +0 -1
- package/dist/engine/usage.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/miner/extractor.d.ts +0 -12
- package/dist/miner/extractor.d.ts.map +0 -1
- package/dist/miner/extractor.js.map +0 -1
- package/dist/miner/index.d.ts +0 -61
- package/dist/miner/index.d.ts.map +0 -1
- package/dist/miner/index.js.map +0 -1
- package/dist/miner/scheduler.d.ts +0 -30
- package/dist/miner/scheduler.d.ts.map +0 -1
- package/dist/miner/scheduler.js.map +0 -1
- package/dist/miner/types.d.ts +0 -11
- package/dist/miner/types.d.ts.map +0 -1
- package/dist/miner/types.js.map +0 -1
- package/dist/search/index.d.ts +0 -328
- package/dist/search/index.d.ts.map +0 -1
- package/dist/search/index.js.map +0 -1
- package/dist/search/quality.d.ts +0 -18
- package/dist/search/quality.d.ts.map +0 -1
- package/dist/search/quality.js.map +0 -1
- package/dist/search/semantic.d.ts +0 -80
- package/dist/search/semantic.d.ts.map +0 -1
- package/dist/search/semantic.js.map +0 -1
- package/dist/search/synonyms.d.ts +0 -2
- package/dist/search/synonyms.d.ts.map +0 -1
- package/dist/search/synonyms.js.map +0 -1
- package/dist/search/tokenize.d.ts +0 -39
- package/dist/search/tokenize.d.ts.map +0 -1
- package/dist/search/tokenize.js.map +0 -1
- package/dist/server.d.ts +0 -41
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/sync/git.d.ts +0 -36
- package/dist/sync/git.d.ts.map +0 -1
- package/dist/sync/git.js.map +0 -1
- package/dist/sync/materialize.d.ts +0 -36
- package/dist/sync/materialize.d.ts.map +0 -1
- package/dist/sync/materialize.js.map +0 -1
- package/dist/sync/ops.d.ts +0 -147
- package/dist/sync/ops.d.ts.map +0 -1
- package/dist/sync/ops.js.map +0 -1
- package/dist/sync/space.d.ts +0 -118
- package/dist/sync/space.d.ts.map +0 -1
- package/dist/sync/space.js.map +0 -1
- package/dist/types/index.d.ts +0 -369
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -44
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/fs.d.ts +0 -59
- package/dist/utils/fs.d.ts.map +0 -1
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -20
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/ids.d.ts +0 -47
- package/dist/utils/ids.d.ts.map +0 -1
- package/dist/utils/ids.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -13
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/transcripts.d.ts +0 -63
- package/dist/utils/transcripts.d.ts.map +0 -1
- package/dist/utils/transcripts.js.map +0 -1
- package/dist/version.d.ts +0 -26
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
package/dist/search/semantic.js
CHANGED
|
@@ -32,7 +32,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
32
32
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
33
33
|
};
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
-
exports.SemanticIndex = exports.SEMANTIC_DTYPE_DEFAULT = exports.SEMANTIC_MODEL_DEFAULT = void 0;
|
|
35
|
+
exports.SemanticIndex = exports.SEMANTIC_MODEL_FILES = exports.SEMANTIC_RUNTIME = exports.SEMANTIC_DTYPE_DEFAULT = exports.SEMANTIC_MODEL_DEFAULT = void 0;
|
|
36
|
+
exports.verifyModelFiles = verifyModelFiles;
|
|
36
37
|
exports.semanticModel = semanticModel;
|
|
37
38
|
exports.semanticDtype = semanticDtype;
|
|
38
39
|
exports.semanticEnabled = semanticEnabled;
|
|
@@ -43,6 +44,7 @@ exports.semanticStatus = semanticStatus;
|
|
|
43
44
|
const node_module_1 = require("node:module");
|
|
44
45
|
const node_url_1 = require("node:url");
|
|
45
46
|
const node_fs_1 = require("node:fs");
|
|
47
|
+
const node_crypto_1 = require("node:crypto");
|
|
46
48
|
const node_path_1 = __importDefault(require("node:path"));
|
|
47
49
|
const node_os_1 = __importDefault(require("node:os"));
|
|
48
50
|
/**
|
|
@@ -54,6 +56,81 @@ const node_os_1 = __importDefault(require("node:os"));
|
|
|
54
56
|
*/
|
|
55
57
|
exports.SEMANTIC_MODEL_DEFAULT = 'Xenova/multilingual-e5-small';
|
|
56
58
|
exports.SEMANTIC_DTYPE_DEFAULT = 'q8';
|
|
59
|
+
/**
|
|
60
|
+
* The runtime `semantic install` puts in the semantic home, pinned to an
|
|
61
|
+
* exact version (2.11; it was the floating range `@3`, so every install could
|
|
62
|
+
* pull a different release of a package that runs install scripts).
|
|
63
|
+
*/
|
|
64
|
+
exports.SEMANTIC_RUNTIME = '@huggingface/transformers@3.8.1';
|
|
65
|
+
/**
|
|
66
|
+
* The default model, pinned by content (2.11). The Hub revision is a moving
|
|
67
|
+
* `main`; these are the SHA-256 of the files the benchmarks were measured
|
|
68
|
+
* with (downloaded 2026-09-03). A model whose files differ is refused, never
|
|
69
|
+
* used: the layer reports why and recall stays keyword-only. Only checked for
|
|
70
|
+
* the default model and dtype; CRBRO_SEMANTIC_MODEL users pick their own.
|
|
71
|
+
*/
|
|
72
|
+
exports.SEMANTIC_MODEL_FILES = {
|
|
73
|
+
'config.json': { sha256: 'cb99455288675345e1a4f411438d5d0adbba5fbd3a67ea4fb03c015433b996c1', bytes: 658 },
|
|
74
|
+
'tokenizer.json': { sha256: '0b44a9d7b51c3c62626640cda0e2c2f70fdacdc25bbbd68038369d14ebdf4c39', bytes: 17_082_730 },
|
|
75
|
+
'tokenizer_config.json': { sha256: 'a1d6bc8734a6f635dc158508bef000f8e2e5a759c7d92f984b2c86e5ff53425b', bytes: 443 },
|
|
76
|
+
'onnx/model_quantized.onnx': { sha256: 'f80102d3f2a1229f387d3c81909990d8945513e347b0eab049f7de3c6f98c193', bytes: 118_308_185 },
|
|
77
|
+
};
|
|
78
|
+
async function sha256Of(file) {
|
|
79
|
+
const h = (0, node_crypto_1.createHash)('sha256');
|
|
80
|
+
await new Promise((resolve, reject) => {
|
|
81
|
+
(0, node_fs_1.createReadStream)(file).on('data', d => h.update(d)).on('end', () => resolve()).on('error', reject);
|
|
82
|
+
});
|
|
83
|
+
return h.digest('hex');
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Check the cached files of the default model against SEMANTIC_MODEL_FILES.
|
|
87
|
+
* Returns null when they match (or the model is not the default one), else
|
|
88
|
+
* what is wrong.
|
|
89
|
+
*/
|
|
90
|
+
async function verifyModelFiles(home = semanticHome()) {
|
|
91
|
+
if (semanticModel() !== exports.SEMANTIC_MODEL_DEFAULT || semanticDtype() !== exports.SEMANTIC_DTYPE_DEFAULT)
|
|
92
|
+
return null;
|
|
93
|
+
const dir = node_path_1.default.join(home, 'models', ...exports.SEMANTIC_MODEL_DEFAULT.split('/'));
|
|
94
|
+
// Each file is hashed once per version of it: one whose size and date are
|
|
95
|
+
// those of the last successful check is not read again. Hashing the 135 MB
|
|
96
|
+
// cost ~140 ms at every process start, before the first semantic answer.
|
|
97
|
+
const marca = node_path_1.default.join(dir, VERIFIED_FILE);
|
|
98
|
+
let vistos = {};
|
|
99
|
+
try {
|
|
100
|
+
vistos = JSON.parse(await node_fs_1.promises.readFile(marca, 'utf8')) || {};
|
|
101
|
+
}
|
|
102
|
+
catch { /* first check */ }
|
|
103
|
+
let nuevos = false;
|
|
104
|
+
for (const [rel, want] of Object.entries(exports.SEMANTIC_MODEL_FILES)) {
|
|
105
|
+
const file = node_path_1.default.join(dir, ...rel.split('/'));
|
|
106
|
+
let st;
|
|
107
|
+
try {
|
|
108
|
+
st = await node_fs_1.promises.stat(file);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return `model file missing: ${rel} (run \`npx crbro-memory semantic install\`)`;
|
|
112
|
+
}
|
|
113
|
+
const mismatch = `model file ${rel} does not match the pinned hash; refusing to use it (delete ${dir} and run \`npx crbro-memory semantic install\`)`;
|
|
114
|
+
if (st.size !== want.bytes)
|
|
115
|
+
return mismatch;
|
|
116
|
+
const v = vistos[rel];
|
|
117
|
+
if (v && v.size === st.size && v.mtime === st.mtimeMs && v.sha256 === want.sha256)
|
|
118
|
+
continue;
|
|
119
|
+
if ((await sha256Of(file)) !== want.sha256)
|
|
120
|
+
return mismatch;
|
|
121
|
+
vistos[rel] = { size: st.size, mtime: st.mtimeMs, sha256: want.sha256 };
|
|
122
|
+
nuevos = true;
|
|
123
|
+
}
|
|
124
|
+
if (nuevos) {
|
|
125
|
+
try {
|
|
126
|
+
await node_fs_1.promises.writeFile(marca, JSON.stringify(vistos));
|
|
127
|
+
}
|
|
128
|
+
catch { /* checked again next time */ }
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/** Where verifyModelFiles remembers the files it already hashed. */
|
|
133
|
+
const VERIFIED_FILE = '.crbro-verified.json';
|
|
57
134
|
function semanticModel() { return process.env.CRBRO_SEMANTIC_MODEL || exports.SEMANTIC_MODEL_DEFAULT; }
|
|
58
135
|
function semanticDtype() { return process.env.CRBRO_SEMANTIC_DTYPE || exports.SEMANTIC_DTYPE_DEFAULT; }
|
|
59
136
|
/**
|
|
@@ -75,6 +152,10 @@ const QUERY_PREFIX = 'query: ';
|
|
|
75
152
|
const PASSAGE_PREFIX = 'passage: ';
|
|
76
153
|
/** Longer passages are truncated by the tokenizer anyway; cap the work. */
|
|
77
154
|
const MAX_CHARS = 2000;
|
|
155
|
+
/** A background pass is committed in slices of this many passages (2.11). */
|
|
156
|
+
const UPSERT_SLICE = 64;
|
|
157
|
+
/** ...and saved this often while it runs, so a process that leaves keeps what it did. */
|
|
158
|
+
const UPSERT_SAVE_MS = 30_000;
|
|
78
159
|
/** Off when CRBRO_SEMANTIC says so, forced on when it says so, else on exactly when the runtime is installed. */
|
|
79
160
|
function semanticEnabled() {
|
|
80
161
|
const mode = semanticMode();
|
|
@@ -118,6 +199,7 @@ function semanticStatus() {
|
|
|
118
199
|
}
|
|
119
200
|
class SemanticIndex {
|
|
120
201
|
dir;
|
|
202
|
+
options;
|
|
121
203
|
ids = [];
|
|
122
204
|
/** Vector width, read from the first embedding (384 for e5-small). */
|
|
123
205
|
dim = 0;
|
|
@@ -130,8 +212,48 @@ class SemanticIndex {
|
|
|
130
212
|
broken = null;
|
|
131
213
|
/** Upserts run one at a time: two overlapping ones would both extend the same array. */
|
|
132
214
|
queue = Promise.resolve();
|
|
133
|
-
|
|
215
|
+
/** Bumped on every change of the vectors, so a write knows whether it caught up. */
|
|
216
|
+
version = 0;
|
|
217
|
+
/**
|
|
218
|
+
* Queries waiting for the model (2.11). A background pass (thousands of
|
|
219
|
+
* passages after an upgrade split long entries into parts) pauses between
|
|
220
|
+
* passages while any is waiting: a recall used to queue behind it, ~2 s
|
|
221
|
+
* each, for the minutes the pass lasted.
|
|
222
|
+
*/
|
|
223
|
+
foreground = 0;
|
|
224
|
+
foregroundWaiters = [];
|
|
225
|
+
/** Passages a running pass still has to embed. */
|
|
226
|
+
left = 0;
|
|
227
|
+
/** A large pass is running: more than one slice of passages still to embed. */
|
|
228
|
+
backfilling() { return this.left > UPSERT_SLICE; }
|
|
229
|
+
/**
|
|
230
|
+
* Something in the foreground (a search, a query) is running: background
|
|
231
|
+
* passages wait until every hold is released. Returns the release.
|
|
232
|
+
*/
|
|
233
|
+
hold() {
|
|
234
|
+
this.foreground++;
|
|
235
|
+
let released = false;
|
|
236
|
+
return () => {
|
|
237
|
+
if (released)
|
|
238
|
+
return;
|
|
239
|
+
released = true;
|
|
240
|
+
this.foreground--;
|
|
241
|
+
if (this.foreground === 0) {
|
|
242
|
+
const w = this.foregroundWaiters;
|
|
243
|
+
this.foregroundWaiters = [];
|
|
244
|
+
for (const f of w)
|
|
245
|
+
f();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* allowDownload: only `semantic install` may fetch the model from the Hub
|
|
251
|
+
* (2.11). A server never downloads during a query: a missing model makes
|
|
252
|
+
* the layer report why and recall stays keyword-only.
|
|
253
|
+
*/
|
|
254
|
+
constructor(dir, options = {}) {
|
|
134
255
|
this.dir = dir;
|
|
256
|
+
this.options = options;
|
|
135
257
|
}
|
|
136
258
|
metaPath() { return node_path_1.default.join(this.dir, 'vectors.meta.json'); }
|
|
137
259
|
dataPath() { return node_path_1.default.join(this.dir, 'vectors.f32'); }
|
|
@@ -153,11 +275,14 @@ class SemanticIndex {
|
|
|
153
275
|
async load() {
|
|
154
276
|
try {
|
|
155
277
|
const meta = JSON.parse(await node_fs_1.promises.readFile(this.metaPath(), 'utf8'));
|
|
156
|
-
|
|
278
|
+
// Vectors of another model or another quantisation are not comparable.
|
|
279
|
+
if (meta.model !== semanticModel() || (meta.dtype && meta.dtype !== semanticDtype()) || !(meta.dim > 0))
|
|
157
280
|
return;
|
|
158
281
|
const buf = await node_fs_1.promises.readFile(this.dataPath());
|
|
159
282
|
const expected = meta.ids.length * meta.dim * 4;
|
|
160
|
-
|
|
283
|
+
// A torn pair (a crash between the two renames) does not match: start
|
|
284
|
+
// empty and let the index refill what is missing (2.11).
|
|
285
|
+
if (buf.byteLength !== expected || (meta.bytes !== undefined && meta.bytes !== buf.byteLength))
|
|
161
286
|
return;
|
|
162
287
|
this.vectors = new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
|
|
163
288
|
this.ids = meta.ids;
|
|
@@ -171,15 +296,20 @@ class SemanticIndex {
|
|
|
171
296
|
async persist() {
|
|
172
297
|
if (!this.dirty)
|
|
173
298
|
return;
|
|
299
|
+
// What is written is this moment's pair; a vector added while the files
|
|
300
|
+
// are written keeps the layer dirty for the next write.
|
|
301
|
+
const version = this.version;
|
|
302
|
+
const vectors = this.vectors;
|
|
303
|
+
const meta = { model: semanticModel(), dtype: semanticDtype(), dim: this.dim, ids: [...this.ids], bytes: vectors.byteLength };
|
|
174
304
|
await node_fs_1.promises.mkdir(this.dir, { recursive: true });
|
|
175
|
-
const meta = { model: semanticModel(), dtype: semanticDtype(), dim: this.dim, ids: this.ids };
|
|
176
305
|
const tmpMeta = this.metaPath() + '.tmp';
|
|
177
306
|
const tmpData = this.dataPath() + '.tmp';
|
|
178
|
-
await node_fs_1.promises.writeFile(tmpData, Buffer.from(
|
|
307
|
+
await node_fs_1.promises.writeFile(tmpData, Buffer.from(vectors.buffer, vectors.byteOffset, vectors.byteLength));
|
|
179
308
|
await node_fs_1.promises.writeFile(tmpMeta, JSON.stringify(meta));
|
|
180
309
|
await node_fs_1.promises.rename(tmpData, this.dataPath());
|
|
181
310
|
await node_fs_1.promises.rename(tmpMeta, this.metaPath());
|
|
182
|
-
this.
|
|
311
|
+
if (this.version === version)
|
|
312
|
+
this.dirty = false;
|
|
183
313
|
}
|
|
184
314
|
/** Load transformers.js from the semantic home and build the extractor. */
|
|
185
315
|
async model() {
|
|
@@ -198,8 +328,23 @@ class SemanticIndex {
|
|
|
198
328
|
const tf = mod.pipeline ? mod : mod.default;
|
|
199
329
|
tf.env.cacheDir = node_path_1.default.join(semanticHome(), 'models');
|
|
200
330
|
tf.env.allowLocalModels = true;
|
|
201
|
-
tf.env.allowRemoteModels = true;
|
|
331
|
+
tf.env.allowRemoteModels = this.options.allowDownload === true;
|
|
332
|
+
if (!this.options.allowDownload) {
|
|
333
|
+
const wrong = await verifyModelFiles();
|
|
334
|
+
if (wrong) {
|
|
335
|
+
this.broken = wrong;
|
|
336
|
+
throw new Error(wrong);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
202
339
|
this.extractor = await tf.pipeline('feature-extraction', semanticModel(), { dtype: semanticDtype() });
|
|
340
|
+
if (this.options.allowDownload) {
|
|
341
|
+
const wrong = await verifyModelFiles();
|
|
342
|
+
if (wrong) {
|
|
343
|
+
this.extractor = null;
|
|
344
|
+
this.broken = wrong;
|
|
345
|
+
throw new Error(wrong);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
203
348
|
return this.extractor;
|
|
204
349
|
})().catch(err => {
|
|
205
350
|
this.broken = this.broken || `semantic model failed to load: ${err instanceof Error ? err.message : String(err)}`;
|
|
@@ -215,6 +360,9 @@ class SemanticIndex {
|
|
|
215
360
|
const out = [];
|
|
216
361
|
const BATCH = batchSize();
|
|
217
362
|
for (let i = 0; i < texts.length; i += BATCH) {
|
|
363
|
+
// Background work gives way to a query that is waiting (2.11).
|
|
364
|
+
while (kind === 'passage' && this.foreground > 0)
|
|
365
|
+
await new Promise(r => this.foregroundWaiters.push(r));
|
|
218
366
|
const batch = texts.slice(i, i + BATCH).map(t => prefix + t.slice(0, MAX_CHARS));
|
|
219
367
|
const res = await extractor(batch, { pooling: 'mean', normalize: true });
|
|
220
368
|
// res.data is a flat typed array of batch × dim.
|
|
@@ -243,23 +391,57 @@ class SemanticIndex {
|
|
|
243
391
|
const nuevos = entries.filter(e => e.text && !this.pos.has(e.id));
|
|
244
392
|
if (nuevos.length === 0)
|
|
245
393
|
return 0;
|
|
246
|
-
|
|
394
|
+
// In slices, each one usable at once and the whole saved every half
|
|
395
|
+
// minute (2.11): a pass of thousands used to land all at the end, so a
|
|
396
|
+
// process that left halfway — a classic session — kept nothing, and the
|
|
397
|
+
// next one started over.
|
|
398
|
+
let guardado = Date.now();
|
|
399
|
+
this.left = nuevos.length;
|
|
400
|
+
try {
|
|
401
|
+
for (let i = 0; i < nuevos.length; i += UPSERT_SLICE) {
|
|
402
|
+
const tramo = nuevos.slice(i, i + UPSERT_SLICE);
|
|
403
|
+
const vecs = await this.embed(tramo.map(e => e.text), 'passage');
|
|
404
|
+
this.append(tramo, vecs);
|
|
405
|
+
this.left = Math.max(0, nuevos.length - i - tramo.length);
|
|
406
|
+
if (this.left > 0 && Date.now() - guardado > UPSERT_SAVE_MS) {
|
|
407
|
+
await this.persist().catch(() => undefined);
|
|
408
|
+
guardado = Date.now();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
413
|
+
this.left = 0;
|
|
414
|
+
}
|
|
415
|
+
return nuevos.length;
|
|
416
|
+
}
|
|
417
|
+
append(tramo, vecs) {
|
|
247
418
|
// The base offset is captured once: ids.push inside the loop would
|
|
248
419
|
// otherwise move it with every vector. Found as a RangeError, silently
|
|
249
420
|
// swallowed upstream — which is why the first benchmark run showed no
|
|
250
421
|
// change at all. Measure the effect, not the artefact.
|
|
251
422
|
const base = this.ids.length;
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
this.
|
|
423
|
+
const need = (base + tramo.length) * this.dim;
|
|
424
|
+
// Room grows by half each time (2.11): a pass that added 64 vectors at a
|
|
425
|
+
// time reallocated and copied the whole array each slice — tens of MB
|
|
426
|
+
// every couple of seconds, and the collector paid for it in pauses.
|
|
427
|
+
let store = this.vectors.buffer.byteLength / 4 >= need && this.vectors.byteOffset === 0
|
|
428
|
+
? new Float32Array(this.vectors.buffer)
|
|
429
|
+
: null;
|
|
430
|
+
if (!store) {
|
|
431
|
+
store = new Float32Array(Math.max(need, Math.ceil((this.vectors.length || 0) * 1.5), 256 * this.dim));
|
|
432
|
+
store.set(this.vectors, 0);
|
|
258
433
|
}
|
|
259
|
-
|
|
434
|
+
for (let i = 0; i < tramo.length; i++) {
|
|
435
|
+
store.set(vecs[i], (base + i) * this.dim);
|
|
436
|
+
this.pos.set(tramo[i].id, base + i);
|
|
437
|
+
this.ids.push(tramo[i].id);
|
|
438
|
+
}
|
|
439
|
+
this.vectors = store.subarray(0, need);
|
|
440
|
+
this.version++;
|
|
260
441
|
this.dirty = true;
|
|
261
|
-
return nuevos.length;
|
|
262
442
|
}
|
|
443
|
+
/** Every id holding a vector (to compare with the index: B14). */
|
|
444
|
+
idList() { return this.ids; }
|
|
263
445
|
/** Drop vectors for ids that left the index. */
|
|
264
446
|
remove(ids) {
|
|
265
447
|
const gone = new Set();
|
|
@@ -281,6 +463,7 @@ class SemanticIndex {
|
|
|
281
463
|
this.ids = keepIds;
|
|
282
464
|
this.vectors = keep;
|
|
283
465
|
this.pos = new Map(keepIds.map((id, i) => [id, i]));
|
|
466
|
+
this.version++;
|
|
284
467
|
this.dirty = true;
|
|
285
468
|
return gone.size;
|
|
286
469
|
}
|
|
@@ -288,7 +471,14 @@ class SemanticIndex {
|
|
|
288
471
|
async query(text, k) {
|
|
289
472
|
if (this.ids.length === 0)
|
|
290
473
|
return [];
|
|
291
|
-
const
|
|
474
|
+
const release = this.hold();
|
|
475
|
+
let q;
|
|
476
|
+
try {
|
|
477
|
+
[q] = await this.embed([text], 'query');
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
release();
|
|
481
|
+
}
|
|
292
482
|
const scores = new Float32Array(this.ids.length);
|
|
293
483
|
for (let i = 0; i < this.ids.length; i++) {
|
|
294
484
|
let dot = 0;
|
package/dist/search/tokenize.js
CHANGED
|
@@ -11,6 +11,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
11
11
|
exports.STOPWORDS = void 0;
|
|
12
12
|
exports.fold = fold;
|
|
13
13
|
exports.tokenize = tokenize;
|
|
14
|
+
exports.indexTokens = indexTokens;
|
|
15
|
+
exports.createIndexTokenizer = createIndexTokenizer;
|
|
14
16
|
exports.queryTerms = queryTerms;
|
|
15
17
|
exports.synonymsEnabled = synonymsEnabled;
|
|
16
18
|
exports.variants = variants;
|
|
@@ -59,6 +61,48 @@ function tokenize(text) {
|
|
|
59
61
|
.split(/[^a-z0-9_]+/)
|
|
60
62
|
.filter(Boolean);
|
|
61
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* The terms a stored text is indexed under (2.11). Exactly the query side's
|
|
66
|
+
* fold and split — until 2.10 the index used Orama's English splitter, which
|
|
67
|
+
* cuts BEFORE folding and keeps only à è é ì ò ó ù inside a word, so
|
|
68
|
+
* "página" was stored as "p" + "gina" and "diseño" as "dise" + "o" while the
|
|
69
|
+
* query asked for "pagina" and "diseno": 72 % of the entries of a real
|
|
70
|
+
* Spanish brain were unreachable by any word with á, í, ú, ñ or ü. The same
|
|
71
|
+
* splitter kept "crbro-memory" as one token, so "memory" found nothing; the
|
|
72
|
+
* hyphen now splits on both sides. A snake_case identifier is kept whole (a
|
|
73
|
+
* query keeps it whole too) and its parts are added, so "lista" also finds
|
|
74
|
+
* "ocultar_de_lista".
|
|
75
|
+
*/
|
|
76
|
+
function indexTokens(text) {
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const t of tokenize(text)) {
|
|
79
|
+
out.push(t);
|
|
80
|
+
if (t.includes('_'))
|
|
81
|
+
for (const p of t.split('_'))
|
|
82
|
+
if (p)
|
|
83
|
+
out.push(p);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* An Orama tokenizer component built on the functions above. Orama calls it
|
|
89
|
+
* with the property name when it indexes a document and without one when it
|
|
90
|
+
* tokenizes a search term: documents get indexTokens(), terms tokenize(), so
|
|
91
|
+
* both sides share one normalisation by construction. Tokens are
|
|
92
|
+
* de-duplicated per text, like Orama's own tokenizer does by default, so
|
|
93
|
+
* BM25 sees the same term frequencies it always saw.
|
|
94
|
+
*/
|
|
95
|
+
function createIndexTokenizer() {
|
|
96
|
+
return {
|
|
97
|
+
language: 'english',
|
|
98
|
+
normalizationCache: new Map(),
|
|
99
|
+
tokenize(raw, _language, prop) {
|
|
100
|
+
if (typeof raw !== 'string')
|
|
101
|
+
return [raw];
|
|
102
|
+
return [...new Set(prop ? indexTokens(raw) : tokenize(raw))];
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
62
106
|
/**
|
|
63
107
|
* Tokenize a search query: drop stopwords and 1-char noise, dedupe,
|
|
64
108
|
* and keep the original order. If everything was a stopword, fall back
|