ruvnet-brain 4.3.20 → 4.3.25
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 +5 -5
- package/bin/install.mjs +383 -78
- package/console/app.js +141 -9
- package/console/index.html +51 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/host-shell-boundary.mjs +43 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/scripts/update-apply.mjs +2 -32
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +319 -27
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +23 -6
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
|
@@ -1,17 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// rvf-index-audit.mjs — the C2 ("stores them correctly") gate for shipped RVF stores.
|
|
3
|
+
//
|
|
4
|
+
// Two layers:
|
|
5
|
+
// 1. Legacy presence audit (inspectRvfIndex / auditRvfIndexes / repairRvfIndex): does an eligible
|
|
6
|
+
// store carry an INDEX_SEG at all. Kept verbatim for build-bundle.mjs and ci.yml, but segment
|
|
7
|
+
// presence proves no recall figure — rUv's rvf-index has three progressive layers (~0.70 / ~0.85 /
|
|
8
|
+
// >=0.95 recall; ruvector/crates/rvf/rvf-index/src/lib.rs:5-7) and `segments()` reports segType only.
|
|
9
|
+
// 2. Deep store audit (auditCorpusStores): reopen each persisted store read-only, execute the SDK's
|
|
10
|
+
// k-NN query against sampled STORED vectors, compare with exact neighbours brute-forced over the
|
|
11
|
+
// store's own VEC_SEG bytes (scripts/rvf-wire.mjs), verify every segment's content hash, and prove
|
|
12
|
+
// id-map <-> vector <-> passage <-> source-mapping correspondence. Stores under the engine's HNSW
|
|
13
|
+
// threshold are reported as 'PASS-SMALL-STORE' — never folded into 'PASS'.
|
|
14
|
+
//
|
|
15
|
+
// Installed engine facts this gate relies on (all verified 2026-09-13 against the installed
|
|
16
|
+
// @ruvector/rvf 0.3.4 / @ruvector/rvf-node 0.2.3 and rUv's rvf-runtime source):
|
|
17
|
+
// - openReadonly / query(vec, k, {efSearch}) / status() / segments(): @ruvector/rvf dist/database.d.ts:37,91,99,133;
|
|
18
|
+
// dist/types.d.ts:106-113 (efSearch), :163-180 (status shape), :216-225 (segments shape — no layer/tier field).
|
|
19
|
+
// - @ruvector/rvf-node index.d.ts:263 indexStats(), :280 metric() exist on the NATIVE class only; the SDK class
|
|
20
|
+
// does not re-export them (dist/database.d.ts has neither). Neither class reads a vector back by id.
|
|
21
|
+
// - Routing: stores with < INDEX_MIN_VECTORS (1024) vectors, or > INDEX_MAX_DELETED_FRACTION (0.25) soft-deleted,
|
|
22
|
+
// or filtered queries are served by the exact scan; otherwise by HNSW (store.rs:415-478, :566-579;
|
|
23
|
+
// index_path.rs:37,41). ef_search is floored at INDEX_MIN_EF_SEARCH = 256 (index_path.rs:48; store.rs:793-797),
|
|
24
|
+
// so the SDK's efSearch knob cannot push recall below the engine's own >= 0.95 contract.
|
|
25
|
+
// - A missing/corrupt INDEX_SEG is silently rebuilt in memory on the first query (store.rs:761-780), so recall
|
|
26
|
+
// alone cannot detect a damaged persisted index — the content-hash check in rvf-wire.mjs can.
|
|
2
27
|
import fs from 'node:fs';
|
|
3
28
|
import path from 'node:path';
|
|
29
|
+
import { createRequire } from 'node:module';
|
|
4
30
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { loadRvf, closeReadonlyRvf } from '../kb/resolve-deps.mjs';
|
|
31
|
+
import { loadRvf, closeReadonlyRvf, loadTransformers, chooseModelCache } from '../kb/resolve-deps.mjs';
|
|
32
|
+
import { materializeModelRevision, modelCacheReady } from '../kb/model-requirements.mjs';
|
|
6
33
|
import { persistAndVerifyRvfIndex } from '../kb/rvf-index.mjs';
|
|
7
34
|
import {
|
|
8
35
|
readRvfGenerations,
|
|
9
36
|
sha256File,
|
|
10
37
|
writeRvfGeneration,
|
|
11
38
|
} from './rvf-generation.mjs';
|
|
39
|
+
import { metricFromId, readRvfStoredVectors } from './rvf-wire.mjs';
|
|
12
40
|
|
|
13
41
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
42
|
export const RVF_HNSW_THRESHOLD = 1024;
|
|
43
|
+
export const RVF_RECALL_THRESHOLD = 0.95;
|
|
44
|
+
export const RVF_RECALL_K = 10;
|
|
45
|
+
export const RVF_RECALL_SAMPLE = 50;
|
|
46
|
+
export const RVF_PROBE_SAMPLE = 5;
|
|
47
|
+
// The passage<->vector probe accepts a sampled passage when its re-embedding ranks the vector stored
|
|
48
|
+
// under its own id first. See probePassageEmbeddings for why this is a rank and not a cosine cutoff.
|
|
49
|
+
export const RVF_PROBE_MAX_RANK = 1;
|
|
50
|
+
const MAX_LISTED_FAILURES = 20;
|
|
51
|
+
|
|
52
|
+
export function isPassingState(state) {
|
|
53
|
+
return state === 'PASS' || state === 'PASS-SMALL-STORE';
|
|
54
|
+
}
|
|
15
55
|
|
|
16
56
|
function dimensionsFor(rvfPath) {
|
|
17
57
|
const embedPath = `${rvfPath}.embed.json`;
|
|
@@ -70,7 +110,7 @@ export function restampChangedRvfGenerations(kbDir, results) {
|
|
|
70
110
|
const ledger = readRvfGenerations(kbDir);
|
|
71
111
|
let stamped = 0;
|
|
72
112
|
for (const result of results) {
|
|
73
|
-
if (result.state
|
|
113
|
+
if (!isPassingState(result.state) || !result.path.endsWith('.big.rvf')) continue;
|
|
74
114
|
const store = path.basename(result.path, '.big.rvf');
|
|
75
115
|
const prior = ledger.stores?.[store];
|
|
76
116
|
if (!prior || prior.sha256 === sha256File(result.path)) continue;
|
|
@@ -88,20 +128,525 @@ export function restampChangedRvfGenerations(kbDir, results) {
|
|
|
88
128
|
return stamped;
|
|
89
129
|
}
|
|
90
130
|
|
|
131
|
+
// ---------------------------------------------------------------------------------------------
|
|
132
|
+
// Deep store audit
|
|
133
|
+
// ---------------------------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
const storeBase = (rvfPath) => rvfPath.replace(/\.big\.rvf$/, '').replace(/\.rvf$/, '');
|
|
136
|
+
const storeName = (rvfPath) => path.basename(storeBase(rvfPath));
|
|
137
|
+
|
|
138
|
+
/** Resolve the native @ruvector/rvf-node binding the SDK itself uses (for metric()/indexStats()). */
|
|
139
|
+
export function loadRvfNative() {
|
|
140
|
+
try {
|
|
141
|
+
const kbRequire = createRequire(new URL('../kb/package.json', import.meta.url));
|
|
142
|
+
const sdkMain = kbRequire.resolve('@ruvector/rvf');
|
|
143
|
+
const sdkRequire = createRequire(sdkMain);
|
|
144
|
+
const nativeMain = sdkRequire.resolve('@ruvector/rvf-node');
|
|
145
|
+
const version = (file) => JSON.parse(fs.readFileSync(file, 'utf8')).version;
|
|
146
|
+
return {
|
|
147
|
+
native: sdkRequire('@ruvector/rvf-node'),
|
|
148
|
+
versions: {
|
|
149
|
+
'@ruvector/rvf': version(path.join(path.dirname(sdkMain), '..', 'package.json')),
|
|
150
|
+
'@ruvector/rvf-node': version(path.join(path.dirname(nativeMain), 'package.json')),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return { native: null, versions: null, error: error.message };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readJsonOrNull(file) {
|
|
159
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The SDK's own string-id <-> numeric-label sidecar (`<store>.big.rvf.idmap.json`, written by
|
|
164
|
+
* @ruvector/rvf dist/backend.js:491-512 and validated at :576-584). Read and validate it HERE, before
|
|
165
|
+
* the SDK opens the store: a malformed sidecar makes the SDK quarantine (rename) it, which must never
|
|
166
|
+
* happen inside an archive under verification.
|
|
167
|
+
*/
|
|
168
|
+
export function readRvfIdMap(rvfPath) {
|
|
169
|
+
const file = `${rvfPath}.idmap.json`;
|
|
170
|
+
const failures = [];
|
|
171
|
+
const idToLabel = new Map();
|
|
172
|
+
const labelToId = new Map();
|
|
173
|
+
if (!fs.existsSync(file)) return { file, idToLabel, labelToId, nextLabel: null, failures: [{ kind: 'idmap-missing', detail: path.basename(file) }] };
|
|
174
|
+
const raw = readJsonOrNull(file);
|
|
175
|
+
if (!raw || typeof raw.idToLabel !== 'object' || Array.isArray(raw.idToLabel) || !raw.idToLabel
|
|
176
|
+
|| typeof raw.labelToId !== 'object' || Array.isArray(raw.labelToId) || !raw.labelToId
|
|
177
|
+
|| !Number.isSafeInteger(raw.nextLabel) || raw.nextLabel < 1) {
|
|
178
|
+
return { file, idToLabel, labelToId, nextLabel: null, failures: [{ kind: 'idmap-malformed', detail: 'expected { idToLabel, labelToId, nextLabel >= 1 }' }] };
|
|
179
|
+
}
|
|
180
|
+
for (const [id, label] of Object.entries(raw.idToLabel)) {
|
|
181
|
+
if (!Number.isSafeInteger(label) || label < 1 || label >= raw.nextLabel) failures.push({ kind: 'idmap-label-out-of-range', id, detail: String(label) });
|
|
182
|
+
idToLabel.set(id, label);
|
|
183
|
+
}
|
|
184
|
+
for (const [labelText, id] of Object.entries(raw.labelToId)) labelToId.set(Number(labelText), id);
|
|
185
|
+
let inconsistent = 0;
|
|
186
|
+
for (const [id, label] of idToLabel) if (labelToId.get(label) !== id) inconsistent++;
|
|
187
|
+
for (const [label, id] of labelToId) if (idToLabel.get(id) !== label) inconsistent++;
|
|
188
|
+
if (inconsistent) failures.push({ kind: 'idmap-not-bijective', detail: `${inconsistent} id/label pairs disagree between idToLabel and labelToId` });
|
|
189
|
+
return { file, idToLabel, labelToId, nextLabel: raw.nextLabel, failures };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function checkPrivateExclusion({ dir, privateStores = null, rvfFiles = null }) {
|
|
193
|
+
let fence = privateStores;
|
|
194
|
+
if (!fence) {
|
|
195
|
+
const file = path.join(dir, 'PRIVATE-STORES.json');
|
|
196
|
+
const raw = readJsonOrNull(file);
|
|
197
|
+
if (!raw || !Array.isArray(raw.privateStores)) throw new Error(`private-store fence missing or malformed (${file}); refusing to audit without one`);
|
|
198
|
+
fence = raw.privateStores;
|
|
199
|
+
}
|
|
200
|
+
const folded = new Set([...fence].map((name) => String(name).toLowerCase()));
|
|
201
|
+
const files = rvfFiles || fs.readdirSync(dir).filter((name) => name.endsWith('.big.rvf'));
|
|
202
|
+
const leaks = files.map((file) => path.basename(file).replace(/\.big\.rvf$/, ''))
|
|
203
|
+
.filter((name) => folded.has(name.toLowerCase())).sort();
|
|
204
|
+
return { state: leaks.length ? 'FAIL' : 'PASS', fenceSize: folded.size, checked: files.length, leaks };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const SOURCE_PATH_OK = (value) => typeof value === 'string' && value.length > 0 && value.length < 4096
|
|
208
|
+
&& !value.startsWith('/') && !value.includes('\\') && !value.includes('\0')
|
|
209
|
+
&& !value.split(/[#?]/)[0].split('/').some((segment) => segment === '..' || segment === '');
|
|
210
|
+
|
|
211
|
+
function capFailures(failures) {
|
|
212
|
+
if (failures.length <= MAX_LISTED_FAILURES) return { failures, omittedFailures: 0 };
|
|
213
|
+
return { failures: failures.slice(0, MAX_LISTED_FAILURES), omittedFailures: failures.length - MAX_LISTED_FAILURES };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* (a) id-map <-> stored vectors, (b) id -> passage row, (c) passage -> source mapping.
|
|
218
|
+
* Source mapping follows the two shapes the corpus actually ships: repository stores bind each
|
|
219
|
+
* passage's `path` in `<store>.meta.json#incremental.files[path].chunkIds` (kb/forge-corpus.mjs
|
|
220
|
+
* incremental schema 2); aggregate stores (ruv-gists, concepts) bind `meta.entries[id].path`.
|
|
221
|
+
* A store with neither is a source-map failure, not a pass.
|
|
222
|
+
*/
|
|
223
|
+
export function checkStoreCorrespondence({ rvfPath, stored, idmap, totalVectors }) {
|
|
224
|
+
const failures = [];
|
|
225
|
+
const base = storeBase(rvfPath);
|
|
226
|
+
if (idmap.labelToId.size !== totalVectors) failures.push({ kind: 'idmap-count-mismatch', detail: `idmap has ${idmap.labelToId.size} labels, engine reports ${totalVectors} live vectors` });
|
|
227
|
+
if (stored.vectors.size !== totalVectors) failures.push({ kind: 'vector-count-mismatch', detail: `${stored.vectors.size} live stored vectors (${stored.entries} entries, ${stored.deleted.size} deleted, ${stored.duplicateLabels} duplicate labels) vs engine ${totalVectors}` });
|
|
228
|
+
for (const label of idmap.labelToId.keys()) if (!stored.vectors.has(label)) failures.push({ kind: 'idmap-label-without-vector', id: idmap.labelToId.get(label), detail: `label ${label}` });
|
|
229
|
+
for (const label of stored.vectors.keys()) if (!idmap.labelToId.has(label)) failures.push({ kind: 'vector-without-idmap-label', detail: `label ${label}` });
|
|
230
|
+
|
|
231
|
+
const passages = new Map();
|
|
232
|
+
const passagesFile = `${base}.passages.jsonl`;
|
|
233
|
+
if (!fs.existsSync(passagesFile)) failures.push({ kind: 'passages-missing', detail: path.basename(passagesFile) });
|
|
234
|
+
else {
|
|
235
|
+
const lines = fs.readFileSync(passagesFile, 'utf8').split('\n');
|
|
236
|
+
lines.forEach((line, index) => {
|
|
237
|
+
if (!line.trim()) return;
|
|
238
|
+
let row;
|
|
239
|
+
try { row = JSON.parse(line); } catch { failures.push({ kind: 'passage-malformed', detail: `line ${index + 1} is not JSON` }); return; }
|
|
240
|
+
if (!row || typeof row.id !== 'string' || !row.id) { failures.push({ kind: 'passage-malformed', detail: `line ${index + 1} has no string id` }); return; }
|
|
241
|
+
if (passages.has(row.id)) failures.push({ kind: 'passage-duplicate-id', id: row.id });
|
|
242
|
+
passages.set(row.id, { path: row.path, text: typeof row.text === 'string' ? row.text : null, line: index + 1 });
|
|
243
|
+
});
|
|
244
|
+
for (const id of idmap.idToLabel.keys()) if (!passages.has(id)) failures.push({ kind: 'passage-missing', id, detail: 'vector id has no row in passages.jsonl' });
|
|
245
|
+
for (const id of passages.keys()) if (!idmap.idToLabel.has(id)) failures.push({ kind: 'passage-orphan', id, detail: 'passage row has no stored vector' });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const meta = readJsonOrNull(`${base}.meta.json`);
|
|
249
|
+
let metaFiles = null;
|
|
250
|
+
if (!meta) failures.push({ kind: 'meta-missing', detail: path.basename(`${base}.meta.json`) });
|
|
251
|
+
else if (meta.incremental && meta.incremental.files && typeof meta.incremental.files === 'object') {
|
|
252
|
+
metaFiles = Object.keys(meta.incremental.files).length;
|
|
253
|
+
const chunkOwner = new Map();
|
|
254
|
+
for (const [filePath, entry] of Object.entries(meta.incremental.files)) {
|
|
255
|
+
for (const id of entry?.chunkIds || []) chunkOwner.set(id, filePath);
|
|
256
|
+
}
|
|
257
|
+
for (const [id, row] of passages) {
|
|
258
|
+
if (!SOURCE_PATH_OK(row.path)) { failures.push({ kind: 'source-path-malformed', id, path: row.path ?? null }); continue; }
|
|
259
|
+
if (chunkOwner.get(id) !== row.path) failures.push({ kind: 'source-map-mismatch', id, path: row.path, detail: `meta.incremental.files binds this id to ${chunkOwner.has(id) ? JSON.stringify(chunkOwner.get(id)) : 'nothing'}` });
|
|
260
|
+
}
|
|
261
|
+
for (const [id, filePath] of chunkOwner) if (!passages.has(id)) failures.push({ kind: 'source-map-orphan-chunk', id, path: filePath, detail: 'meta chunkId has no passage row' });
|
|
262
|
+
} else if (meta.entries && typeof meta.entries === 'object') {
|
|
263
|
+
metaFiles = Object.keys(meta.entries).length;
|
|
264
|
+
for (const [id, row] of passages) {
|
|
265
|
+
if (!SOURCE_PATH_OK(row.path)) { failures.push({ kind: 'source-path-malformed', id, path: row.path ?? null }); continue; }
|
|
266
|
+
const entry = meta.entries[id];
|
|
267
|
+
if (!entry || entry.path !== row.path) failures.push({ kind: 'source-map-mismatch', id, path: row.path, detail: `meta.entries binds this id to ${entry ? JSON.stringify(entry.path) : 'nothing'}` });
|
|
268
|
+
}
|
|
269
|
+
} else failures.push({ kind: 'source-map-missing', detail: 'meta.json has neither incremental.files nor entries' });
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
result: { state: failures.length ? 'FAIL' : 'PASS', counts: { idmapIds: idmap.idToLabel.size, storedVectors: stored.vectors.size, passages: passages.size, metaFiles }, ...capFailures(failures) },
|
|
273
|
+
passages,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- recall -----------------------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
function seededRandom(text) {
|
|
280
|
+
let h = 0x811c9dc5;
|
|
281
|
+
for (let i = 0; i < text.length; i++) { h ^= text.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; }
|
|
282
|
+
let s = h || 1;
|
|
283
|
+
return () => { s = (s + 0x6d2b79f5) >>> 0; let t = s; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function sampleDistinct(items, count, random) {
|
|
287
|
+
if (count >= items.length) return [...items];
|
|
288
|
+
const picked = new Set();
|
|
289
|
+
while (picked.size < count) picked.add(items[Math.floor(random() * items.length)]);
|
|
290
|
+
return [...picked];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const DISTANCE = {
|
|
294
|
+
l2: (a, b) => { let s = 0; for (let i = 0; i < a.length; i++) { const d = a[i] - b[i]; s += d * d; } return s; },
|
|
295
|
+
cosine: (a, b) => { let d = 0, na = 0, nb = 0; for (let i = 0; i < a.length; i++) { d += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i]; } return 1 - d / (Math.sqrt(na * nb) || 1); },
|
|
296
|
+
dotproduct: (a, b) => { let d = 0; for (let i = 0; i < a.length; i++) d += a[i] * b[i]; return -d; },
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/** Exact top-k under `metric` with tie tolerance: every label whose distance ties the k-th best counts. */
|
|
300
|
+
function exactNeighbourLabels(query, vectors, k, metric) {
|
|
301
|
+
const distance = DISTANCE[metric] || DISTANCE.l2;
|
|
302
|
+
const scored = [];
|
|
303
|
+
for (const [label, vector] of vectors) scored.push([distance(query, vector), label]);
|
|
304
|
+
scored.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
305
|
+
const kth = scored[Math.min(k, scored.length) - 1][0];
|
|
306
|
+
const tolerance = 1e-6 * Math.max(1, Math.abs(kth));
|
|
307
|
+
return new Set(scored.filter(([d]) => d <= kth + tolerance).map(([, label]) => label));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function norm(vector) { let s = 0; for (let i = 0; i < vector.length; i++) s += vector[i] * vector[i]; return Math.sqrt(s); }
|
|
311
|
+
function percentile(sorted, p) { return sorted.length ? sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * p))] : null; }
|
|
312
|
+
|
|
313
|
+
async function openStore(rvfPath, RvfDatabase) {
|
|
314
|
+
const db = await RvfDatabase.openReadonly(rvfPath);
|
|
315
|
+
try {
|
|
316
|
+
const [status, segments, dimension] = await Promise.all([db.status(), db.segments(), db.dimension()]);
|
|
317
|
+
return { db, status, segments, dimension };
|
|
318
|
+
} catch (error) {
|
|
319
|
+
await closeReadonlyRvf(db).catch(() => {});
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Reopen a persisted store read-only, run the SDK's k-NN query for a deterministic sample of its own
|
|
326
|
+
* stored vectors, and compare with exact neighbours brute-forced over the store's VEC_SEG bytes.
|
|
327
|
+
* recall@k = |approx ∩ exact| / min(k, liveVectors), averaged over the sample. Cost: O(sample × N × dim)
|
|
328
|
+
* — for the corpus' largest store (ruvector, 29,872 × 768) 50 queries take ~2.5 s wall including parsing.
|
|
329
|
+
*/
|
|
330
|
+
export async function measureRecall({
|
|
331
|
+
rvfPath, RvfDatabase, sampleSize = RVF_RECALL_SAMPLE, k = RVF_RECALL_K, efSearch = null, seed = 0,
|
|
332
|
+
recallThreshold = RVF_RECALL_THRESHOLD, preloaded = null, native = undefined,
|
|
333
|
+
}) {
|
|
334
|
+
if (!RvfDatabase) ({ mod: { RvfDatabase } } = loadRvf());
|
|
335
|
+
const nativeBinding = native === undefined ? loadRvfNative().native : native;
|
|
336
|
+
const idmap = preloaded?.idmap || readRvfIdMap(rvfPath);
|
|
337
|
+
const failures = [...idmap.failures];
|
|
338
|
+
const opened = preloaded?.db ? preloaded : await openStore(rvfPath, RvfDatabase);
|
|
339
|
+
const ownsHandle = !preloaded?.db;
|
|
340
|
+
try {
|
|
341
|
+
const { db, status, segments, dimension } = opened;
|
|
342
|
+
const stored = preloaded?.stored || readRvfStoredVectors(rvfPath, segments);
|
|
343
|
+
failures.push(...stored.failures);
|
|
344
|
+
const hasIndex = segments.some(({ segType }) => segType === 'index');
|
|
345
|
+
const indexRequired = status.totalVectors >= RVF_HNSW_THRESHOLD;
|
|
346
|
+
if (indexRequired && !hasIndex) failures.push({ kind: 'index-missing', detail: `${status.totalVectors} vectors >= ${RVF_HNSW_THRESHOLD} but no INDEX_SEG is persisted` });
|
|
347
|
+
if (stored.dimension !== null && stored.dimension !== dimension) failures.push({ kind: 'dimension-mismatch', detail: `VEC_SEG dimension ${stored.dimension} != engine ${dimension}` });
|
|
348
|
+
|
|
349
|
+
let engineMetric = stored.manifest ? stored.manifest.metric : null;
|
|
350
|
+
let indexStats = null;
|
|
351
|
+
if (nativeBinding) {
|
|
352
|
+
const handle = nativeBinding.RvfDatabase.openReadonly(rvfPath);
|
|
353
|
+
try {
|
|
354
|
+
const reported = String(handle.metric()).replace('inner_product', 'dotproduct');
|
|
355
|
+
indexStats = handle.indexStats();
|
|
356
|
+
if (engineMetric && reported !== engineMetric) failures.push({ kind: 'engine-metric-disagreement', detail: `native metric() ${reported} vs manifest byte ${engineMetric}` });
|
|
357
|
+
engineMetric = reported;
|
|
358
|
+
} finally { handle.close(); }
|
|
359
|
+
}
|
|
360
|
+
if (!engineMetric) failures.push({ kind: 'metric-unknown', detail: 'neither the native binding nor the manifest reported a distance metric' });
|
|
361
|
+
const embedConfig = readJsonOrNull(`${rvfPath}.embed.json`);
|
|
362
|
+
const declaredMetric = embedConfig?.metric ?? null;
|
|
363
|
+
const metricMismatch = Boolean(declaredMetric && engineMetric && declaredMetric !== engineMetric);
|
|
364
|
+
|
|
365
|
+
const random = seededRandom(`${storeName(rvfPath)}:${seed}`);
|
|
366
|
+
const labels = [...stored.vectors.keys()].sort((a, b) => a - b);
|
|
367
|
+
const sample = sampleDistinct(labels, sampleSize, random);
|
|
368
|
+
const kEffective = Math.min(k, stored.vectors.size);
|
|
369
|
+
const latencies = [];
|
|
370
|
+
let recallSum = 0;
|
|
371
|
+
let minRecall = 1;
|
|
372
|
+
let unmappedResultIds = 0;
|
|
373
|
+
let maxNormDeviation = 0;
|
|
374
|
+
if (!sample.length) failures.push({ kind: 'empty-store', detail: 'no live stored vectors to sample' });
|
|
375
|
+
for (const label of sample) {
|
|
376
|
+
const query = stored.vectors.get(label);
|
|
377
|
+
if (metricMismatch) maxNormDeviation = Math.max(maxNormDeviation, Math.abs(norm(query) - 1));
|
|
378
|
+
const started = process.hrtime.bigint();
|
|
379
|
+
let results;
|
|
380
|
+
try {
|
|
381
|
+
results = efSearch === null ? await db.query(query, k) : await db.query(query, k, { efSearch });
|
|
382
|
+
} catch (error) {
|
|
383
|
+
failures.push({ kind: 'query-failed', id: idmap.labelToId.get(label) ?? null, detail: error.message });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
latencies.push(Number(process.hrtime.bigint() - started) / 1e6);
|
|
387
|
+
const exact = exactNeighbourLabels(query, stored.vectors, kEffective, engineMetric || 'l2');
|
|
388
|
+
const hits = new Set();
|
|
389
|
+
for (const { id } of results) {
|
|
390
|
+
const resultLabel = idmap.idToLabel.get(String(id));
|
|
391
|
+
if (resultLabel === undefined) { unmappedResultIds++; continue; }
|
|
392
|
+
if (exact.has(resultLabel)) hits.add(resultLabel);
|
|
393
|
+
}
|
|
394
|
+
const recall = Math.min(1, hits.size / kEffective);
|
|
395
|
+
recallSum += recall;
|
|
396
|
+
minRecall = Math.min(minRecall, recall);
|
|
397
|
+
}
|
|
398
|
+
const queries = latencies.length;
|
|
399
|
+
const recallAtK = queries ? recallSum / queries : null;
|
|
400
|
+
if (unmappedResultIds) failures.push({ kind: 'query-id-unmapped', detail: `${unmappedResultIds} query result ids are not in the id map` });
|
|
401
|
+
if (recallAtK !== null && recallAtK < recallThreshold) failures.push({ kind: 'recall-below-threshold', detail: `measured recall@${kEffective} ${recallAtK.toFixed(4)} < ${recallThreshold} over ${queries} persisted queries` });
|
|
402
|
+
if (metricMismatch && maxNormDeviation > 1e-3) failures.push({ kind: 'metric-mismatch-unnormalized', detail: `engine ranks by ${engineMetric}, embed.json declares ${declaredMetric}, and sampled vectors deviate from unit norm by up to ${maxNormDeviation.toExponential(2)} — rankings are not equivalent` });
|
|
403
|
+
latencies.sort((a, b) => a - b);
|
|
404
|
+
const state = failures.length ? 'FAIL' : indexRequired ? 'PASS' : 'PASS-SMALL-STORE';
|
|
405
|
+
return {
|
|
406
|
+
path: rvfPath,
|
|
407
|
+
store: storeName(rvfPath),
|
|
408
|
+
state,
|
|
409
|
+
totalVectors: status.totalVectors,
|
|
410
|
+
storedVectors: stored.vectors.size,
|
|
411
|
+
storedEntries: stored.entries,
|
|
412
|
+
deletedVectors: stored.deleted.size,
|
|
413
|
+
duplicateLabels: stored.duplicateLabels,
|
|
414
|
+
dimension,
|
|
415
|
+
indexRequired,
|
|
416
|
+
hasIndex,
|
|
417
|
+
segmentsHashVerified: stored.hashesVerified,
|
|
418
|
+
segmentCount: stored.segmentCount,
|
|
419
|
+
supersededSegments: stored.supersededSegments,
|
|
420
|
+
engineMetric,
|
|
421
|
+
declaredMetric,
|
|
422
|
+
metricMismatch,
|
|
423
|
+
unitNorm: metricMismatch ? { checked: sample.length, maxDeviation: maxNormDeviation } : null,
|
|
424
|
+
indexStats,
|
|
425
|
+
sampleSize: sample.length,
|
|
426
|
+
queries,
|
|
427
|
+
k,
|
|
428
|
+
kEffective,
|
|
429
|
+
efSearch: efSearch === null ? 'sdk-default' : efSearch,
|
|
430
|
+
recallThreshold,
|
|
431
|
+
recallAtK,
|
|
432
|
+
minRecall: queries ? minRecall : null,
|
|
433
|
+
p50Ms: percentile(latencies, 0.5),
|
|
434
|
+
p95Ms: percentile(latencies, 0.95),
|
|
435
|
+
...capFailures(failures),
|
|
436
|
+
};
|
|
437
|
+
} finally {
|
|
438
|
+
if (ownsHandle) await closeReadonlyRvf(opened.db);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// --- passage -> vector spot check ---------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Build the store's own embedder from `<store>.big.rvf.embed.json` exactly the way kb/forge-big.mjs:80-96
|
|
446
|
+
* builds it (pinned model + revision, pooling, normalize, quantized, passages embedded WITHOUT the query
|
|
447
|
+
* prefix), so a re-embedded passage is comparable to the stored vector.
|
|
448
|
+
*/
|
|
449
|
+
export async function createPassageEmbedder(embedConfig) {
|
|
450
|
+
if (!embedConfig?.model) throw new Error('embed.json has no model');
|
|
451
|
+
const { T } = await loadTransformers();
|
|
452
|
+
const cache = chooseModelCache();
|
|
453
|
+
if (embedConfig.revision) materializeModelRevision(cache, embedConfig.model, embedConfig.revision);
|
|
454
|
+
T.env.localModelPath = cache;
|
|
455
|
+
T.env.cacheDir = cache;
|
|
456
|
+
T.env.allowRemoteModels = !modelCacheReady(cache, embedConfig.model);
|
|
457
|
+
const pipe = await T.pipeline('feature-extraction', embedConfig.model, { quantized: true, ...(embedConfig.revision ? { revision: embedConfig.revision } : {}) });
|
|
458
|
+
const pooling = embedConfig.pooling || 'mean';
|
|
459
|
+
const normalize = embedConfig.normalize !== false;
|
|
460
|
+
return async (texts) => {
|
|
461
|
+
const out = await pipe(texts, { pooling, normalize });
|
|
462
|
+
const dim = out.dims[1];
|
|
463
|
+
return texts.map((_, i) => Float32Array.from(out.data.slice(i * dim, (i + 1) * dim)));
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* A CONSISTENT id-map swap (idToLabel and labelToId swapped together) is invisible to label-level
|
|
469
|
+
* recall, so vector<->passage correspondence is proven by re-embedding sampled passages and checking
|
|
470
|
+
* them against the vector stored under their id. Without an embedder the probe reports NOT-RUN —
|
|
471
|
+
* visibly, never as a pass.
|
|
472
|
+
*
|
|
473
|
+
* The criterion is RANK, not an absolute cosine, and that is a measured decision rather than a
|
|
474
|
+
* preference. Measured 2026-09-14 over three shipped stores (12 + 12 + 3 samples), correctly paired
|
|
475
|
+
* re-embeddings scored as low as 0.9326 — those stores predate the model-revision pin, so rebuilding
|
|
476
|
+
* today drifts slightly — while a deliberately MIS-paired vector scored as high as 0.9462, because
|
|
477
|
+
* adjacent chunks of one document are genuinely near-duplicate text. Those ranges overlap (one store
|
|
478
|
+
* separated by -0.0136), so no fixed cutoff can hold. Ranking does: across 39 samples in stores of 3
|
|
479
|
+
* to 3,641 vectors, every passage's re-embedding ranked its OWN stored vector first. A consistent
|
|
480
|
+
* swap still fails, because the vector the id map points at is then another passage's and the true
|
|
481
|
+
* one outranks it.
|
|
482
|
+
*/
|
|
483
|
+
export async function probePassageEmbeddings({
|
|
484
|
+
rvfPath, stored, idmap, passages, embedder, sampleSize = RVF_PROBE_SAMPLE, maxRank = RVF_PROBE_MAX_RANK, seed = 0,
|
|
485
|
+
}) {
|
|
486
|
+
if (!embedder) return { state: 'NOT-RUN', reason: 'no embedder (pass embedder or set RUVNET_BRAIN_RVF_AUDIT_EMBED=1 to load the store\'s pinned model)', failures: [] };
|
|
487
|
+
const candidates = [...idmap.idToLabel.keys()].filter((id) => passages.get(id)?.text && stored.vectors.has(idmap.idToLabel.get(id))).sort();
|
|
488
|
+
const sample = sampleDistinct(candidates, sampleSize, seededRandom(`${storeName(rvfPath)}:probe:${seed}`));
|
|
489
|
+
if (!sample.length) return { state: 'FAIL', failures: [{ kind: 'probe-no-candidates', detail: 'no passage with text maps to a stored vector' }] };
|
|
490
|
+
const embedded = await embedder(sample.map((id) => passages.get(id).text));
|
|
491
|
+
const results = [];
|
|
492
|
+
const failures = [];
|
|
493
|
+
sample.forEach((id, index) => {
|
|
494
|
+
const ownLabel = idmap.idToLabel.get(id);
|
|
495
|
+
const probe = embedded[index];
|
|
496
|
+
const ownScore = 1 - DISTANCE.cosine(stored.vectors.get(ownLabel), probe);
|
|
497
|
+
let rank = 1;
|
|
498
|
+
let best = null;
|
|
499
|
+
for (const [label, vector] of stored.vectors) {
|
|
500
|
+
if (label === ownLabel) continue;
|
|
501
|
+
const score = 1 - DISTANCE.cosine(vector, probe);
|
|
502
|
+
if (score > ownScore) {
|
|
503
|
+
rank++;
|
|
504
|
+
if (best === null || score > best.score) best = { label, score };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
results.push({ id, cosine: Number(ownScore.toFixed(6)), rank });
|
|
508
|
+
if (rank > maxRank) {
|
|
509
|
+
failures.push({
|
|
510
|
+
kind: 'vector-passage-mismatch',
|
|
511
|
+
id,
|
|
512
|
+
detail: `the re-embedded passage ranks its own stored vector #${rank} (cosine ${ownScore.toFixed(4)}); ${best === null ? 'nothing' : `id ${JSON.stringify(idmap.labelToId.get(best.label) ?? best.label)} scores higher at ${best.score.toFixed(4)}`}`,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
return { state: failures.length ? 'FAIL' : 'PASS', sampleSize: sample.length, maxRank, results, failures };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// --- composite ------------------------------------------------------------------------------------
|
|
520
|
+
|
|
521
|
+
async function auditOneStore(rvfPath, { RvfDatabase, native, sampleSize, k, efSearch, seed, recallThreshold, embedderFor, probeSampleSize }) {
|
|
522
|
+
const idmap = readRvfIdMap(rvfPath);
|
|
523
|
+
if (idmap.failures.length) {
|
|
524
|
+
return { path: rvfPath, store: storeName(rvfPath), state: 'FAIL', totalVectors: null, recall: null, correspondence: null, passageProbe: { state: 'NOT-RUN', reason: 'id map invalid', failures: [] }, ...capFailures(idmap.failures) };
|
|
525
|
+
}
|
|
526
|
+
const opened = await openStore(rvfPath, RvfDatabase);
|
|
527
|
+
try {
|
|
528
|
+
const stored = readRvfStoredVectors(rvfPath, opened.segments);
|
|
529
|
+
const recall = await measureRecall({ rvfPath, RvfDatabase, sampleSize, k, efSearch, seed, recallThreshold, native, preloaded: { ...opened, stored, idmap } });
|
|
530
|
+
const { result: correspondence, passages } = checkStoreCorrespondence({ rvfPath, stored, idmap, totalVectors: opened.status.totalVectors });
|
|
531
|
+
const embedConfig = readJsonOrNull(`${rvfPath}.embed.json`);
|
|
532
|
+
const embedder = embedderFor ? await embedderFor(embedConfig) : null;
|
|
533
|
+
const passageProbe = await probePassageEmbeddings({ rvfPath, stored, idmap, passages, embedder, sampleSize: probeSampleSize, seed });
|
|
534
|
+
const failures = [...recall.failures, ...correspondence.failures, ...passageProbe.failures];
|
|
535
|
+
const { failures: recallFailures, omittedFailures: _omit, ...recallRest } = recall;
|
|
536
|
+
return {
|
|
537
|
+
path: rvfPath,
|
|
538
|
+
store: recall.store,
|
|
539
|
+
state: failures.length ? 'FAIL' : recall.indexRequired ? 'PASS' : 'PASS-SMALL-STORE',
|
|
540
|
+
totalVectors: opened.status.totalVectors,
|
|
541
|
+
recall: recallRest,
|
|
542
|
+
correspondence,
|
|
543
|
+
passageProbe,
|
|
544
|
+
...capFailures(failures),
|
|
545
|
+
};
|
|
546
|
+
} finally {
|
|
547
|
+
await closeReadonlyRvf(opened.db);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Deep-audit every `.big.rvf` under `dir` (or the supplied paths). Returns the full measured result —
|
|
553
|
+
* this object (not the corpus receipt) is where per-store recall, engine state and correspondence live.
|
|
554
|
+
* `privateStores` may be a Set/array of fenced names; otherwise `<dir>/PRIVATE-STORES.json` is required.
|
|
555
|
+
*/
|
|
556
|
+
export async function auditCorpusStores({
|
|
557
|
+
dir, rvfPaths = null, privateStores = null, RvfDatabase = null, sampleSize = RVF_RECALL_SAMPLE, k = RVF_RECALL_K,
|
|
558
|
+
efSearch = null, seed = 0, recallThreshold = RVF_RECALL_THRESHOLD, embedder = undefined, probeSampleSize = RVF_PROBE_SAMPLE,
|
|
559
|
+
} = {}) {
|
|
560
|
+
if (!dir) throw new TypeError('dir is required');
|
|
561
|
+
if (!RvfDatabase) ({ mod: { RvfDatabase } } = loadRvf());
|
|
562
|
+
const { native, versions } = loadRvfNative();
|
|
563
|
+
const paths = (rvfPaths || fs.readdirSync(dir).filter((name) => name.endsWith('.big.rvf')).map((name) => path.join(dir, name))).slice().sort();
|
|
564
|
+
const privateExclusion = checkPrivateExclusion({ dir, privateStores, rvfFiles: paths.map((file) => path.basename(file)) });
|
|
565
|
+
|
|
566
|
+
let embedderFor = null;
|
|
567
|
+
if (embedder) embedderFor = async () => embedder;
|
|
568
|
+
else if (embedder === undefined && process.env.RUVNET_BRAIN_RVF_AUDIT_EMBED === '1') {
|
|
569
|
+
const cache = new Map();
|
|
570
|
+
embedderFor = async (config) => {
|
|
571
|
+
if (!config?.model) return null;
|
|
572
|
+
const key = JSON.stringify([config.model, config.revision ?? null, config.pooling ?? null, config.normalize ?? null]);
|
|
573
|
+
if (!cache.has(key)) cache.set(key, createPassageEmbedder(config));
|
|
574
|
+
return cache.get(key);
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const stores = [];
|
|
579
|
+
for (const rvfPath of paths) {
|
|
580
|
+
try {
|
|
581
|
+
stores.push(await auditOneStore(rvfPath, { RvfDatabase, native, sampleSize, k, efSearch, seed, recallThreshold, embedderFor, probeSampleSize }));
|
|
582
|
+
} catch (error) {
|
|
583
|
+
// rvf-runtime verifies each segment's content hash when it loads it, so tampered VEC/manifest
|
|
584
|
+
// bytes surface here as an open/read failure ("Segment hash verification failed ...
|
|
585
|
+
// InvalidChecksum") rather than through this gate's own hash check. Classify it as the same
|
|
586
|
+
// finding so the failure vocabulary means one thing regardless of which layer caught it.
|
|
587
|
+
// (Index-segment damage is NOT caught by the engine — it rebuilds silently — which is why the
|
|
588
|
+
// gate hashes segments itself as well; see the b2 case in tests/unit/rvf-store-audit.test.mjs.)
|
|
589
|
+
const kind = /checksum|hash verification/i.test(error.message) ? 'segment-content-hash-mismatch' : 'audit-error';
|
|
590
|
+
stores.push({ path: rvfPath, store: storeName(rvfPath), state: 'FAIL', totalVectors: null, recall: null, correspondence: null, passageProbe: null, failures: [{ kind, detail: error.message }], omittedFailures: 0 });
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const failing = stores.filter((row) => !isPassingState(row.state));
|
|
594
|
+
return {
|
|
595
|
+
schemaVersion: 1,
|
|
596
|
+
kind: 'ruvnet-brain-rvf-audit',
|
|
597
|
+
createdAt: new Date().toISOString(),
|
|
598
|
+
dir,
|
|
599
|
+
sdk: versions,
|
|
600
|
+
contract: {
|
|
601
|
+
hnswThreshold: RVF_HNSW_THRESHOLD,
|
|
602
|
+
recallThreshold,
|
|
603
|
+
k,
|
|
604
|
+
sampleSize,
|
|
605
|
+
efSearch: efSearch === null ? 'sdk-default' : efSearch,
|
|
606
|
+
note: 'rvf-runtime floors ef_search at INDEX_MIN_EF_SEARCH=256 (index_path.rs:48; store.rs:793-797) and serves stores under 1024 vectors, or over 25% soft-deleted, by exact scan (store.rs:566-579); PASS-SMALL-STORE marks the exact-scan exception explicitly.',
|
|
607
|
+
},
|
|
608
|
+
state: failing.length || privateExclusion.leaks.length ? 'FAIL' : 'PASS',
|
|
609
|
+
storeCount: stores.length,
|
|
610
|
+
smallStoreExceptions: stores.filter((row) => row.state === 'PASS-SMALL-STORE').map((row) => row.store),
|
|
611
|
+
privateExclusion,
|
|
612
|
+
stores,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ---------------------------------------------------------------------------------------------
|
|
617
|
+
|
|
91
618
|
async function main() {
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const kbDirAt = process.argv.indexOf('--dir');
|
|
100
|
-
const kbDir = path.resolve(ROOT, kbDirAt >= 0 ? process.argv[kbDirAt + 1] : 'kb');
|
|
619
|
+
const has = (flag) => process.argv.includes(flag);
|
|
620
|
+
const value = (flag, fallback) => { const at = process.argv.indexOf(flag); return at >= 0 && process.argv[at + 1] !== undefined ? process.argv[at + 1] : fallback; };
|
|
621
|
+
const repair = has('--repair');
|
|
622
|
+
const deep = has('--deep');
|
|
623
|
+
const concurrency = Math.max(1, Number(value('--concurrency', 3)));
|
|
624
|
+
const names = has('--names') ? new Set(String(value('--names', '')).split(',').filter(Boolean)) : null;
|
|
625
|
+
const kbDir = path.resolve(ROOT, value('--dir', 'kb'));
|
|
101
626
|
const rvfPaths = fs.readdirSync(kbDir)
|
|
102
627
|
.filter((name) => name.endsWith('.rvf') && (!names || names.has(name.replace(/\.rvf$/, ''))))
|
|
103
628
|
.sort()
|
|
104
629
|
.map((name) => path.join(kbDir, name));
|
|
630
|
+
|
|
631
|
+
if (deep) {
|
|
632
|
+
if (has('--embed')) process.env.RUVNET_BRAIN_RVF_AUDIT_EMBED = '1';
|
|
633
|
+
const result = await auditCorpusStores({
|
|
634
|
+
dir: kbDir,
|
|
635
|
+
rvfPaths: rvfPaths.filter((file) => file.endsWith('.big.rvf')),
|
|
636
|
+
sampleSize: Number(value('--sample', RVF_RECALL_SAMPLE)),
|
|
637
|
+
k: Number(value('--k', RVF_RECALL_K)),
|
|
638
|
+
efSearch: has('--ef') ? Number(value('--ef', 0)) : null,
|
|
639
|
+
});
|
|
640
|
+
for (const row of result.stores) {
|
|
641
|
+
console.log(JSON.stringify({ store: row.store, state: row.state, vectors: row.totalVectors, recall: row.recall?.recallAtK, min: row.recall?.minRecall, p50Ms: row.recall?.p50Ms, hasIndex: row.recall?.hasIndex, metric: `${row.recall?.engineMetric}/${row.recall?.declaredMetric}`, probe: row.passageProbe?.state, failures: row.failures.map((f) => f.kind) }));
|
|
642
|
+
}
|
|
643
|
+
const out = value('--out', null);
|
|
644
|
+
if (out) fs.writeFileSync(out, `${JSON.stringify(result, null, 2)}\n`);
|
|
645
|
+
console.log(`SUMMARY deep state=${result.state} stores=${result.storeCount} pass=${result.stores.filter((r) => r.state === 'PASS').length} smallStore=${result.smallStoreExceptions.length} failed=${result.stores.filter((r) => r.state === 'FAIL').length} privateLeaks=${result.privateExclusion.leaks.length}${out ? ` out=${out}` : ''}`);
|
|
646
|
+
if (result.state !== 'PASS') process.exitCode = 1;
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
105
650
|
const { mod: { RvfDatabase } } = loadRvf();
|
|
106
651
|
const results = new Array(rvfPaths.length);
|
|
107
652
|
let next = 0;
|
|
@@ -121,7 +666,7 @@ async function main() {
|
|
|
121
666
|
}
|
|
122
667
|
}));
|
|
123
668
|
|
|
124
|
-
const failed = results.filter(({ state }) => state
|
|
669
|
+
const failed = results.filter(({ state }) => !isPassingState(state));
|
|
125
670
|
const repaired = results.filter(({ repaired }) => repaired).length;
|
|
126
671
|
const stamped = repair && !failed.length
|
|
127
672
|
? restampChangedRvfGenerations(kbDir, results)
|
|
@@ -130,7 +675,22 @@ async function main() {
|
|
|
130
675
|
if (failed.length) process.exitCode = 1;
|
|
131
676
|
}
|
|
132
677
|
|
|
133
|
-
|
|
678
|
+
// Entry-point guard. Compares REALPATHS on both sides: path.resolve() normalizes a path but does
|
|
679
|
+
// NOT follow symlinks, while import.meta.url IS symlink-resolved by Node. Through a symlink (npm bin
|
|
680
|
+
// shims, wrapper scripts, and every os.tmpdir() path on macOS) the two sides disagree, so main()
|
|
681
|
+
// never runs -- and because nothing throws, the process exits 0. A silent exit 0 is indistinguishable
|
|
682
|
+
// from "ran, found nothing", which is how prepareCorpusCandidate once reported SUCCESS with no
|
|
683
|
+
// archive on disk. Reproduced live 2026-07-27; pinned by tests/unit/entrypoint-symlink.test.mjs.
|
|
684
|
+
function isDirectInvocation() {
|
|
685
|
+
try {
|
|
686
|
+
if (!process.argv[1]) return false;
|
|
687
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
688
|
+
} catch {
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (isDirectInvocation()) {
|
|
134
694
|
main().catch((error) => {
|
|
135
695
|
console.error(error.stack || error.message);
|
|
136
696
|
process.exitCode = 1;
|