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.
Files changed (143) hide show
  1. package/README.md +5 -5
  2. package/bin/install.mjs +383 -78
  3. package/console/app.js +141 -9
  4. package/console/index.html +51 -24
  5. package/console/scope.css +137 -0
  6. package/console/scope.html +144 -0
  7. package/console/scope.js +209 -0
  8. package/console/tips.html +1 -0
  9. package/kb/corpus-release-identity.mjs +239 -0
  10. package/kb/update-storage-transaction.mjs +20 -3
  11. package/package.json +9 -2
  12. package/plugin/.claude-plugin/plugin.json +2 -2
  13. package/plugin/.codex-plugin/plugin.json +1 -1
  14. package/plugin/commands/checkpoint.md +61 -0
  15. package/plugin/hooks/codex-hooks.json +64 -1
  16. package/plugin/hooks/hook-contracts.json +299 -6
  17. package/plugin/hooks/hooks.json +81 -1
  18. package/plugin/mcp/server.mjs +23 -0
  19. package/plugin/scripts/advocacy-catalog.mjs +245 -0
  20. package/plugin/scripts/advocacy-route.mjs +460 -0
  21. package/plugin/scripts/continuation-gate.mjs +25 -2
  22. package/plugin/scripts/continuation-objective.mjs +7 -1
  23. package/plugin/scripts/continuity-hook-policy.mjs +190 -15
  24. package/plugin/scripts/coverage-integrity.mjs +7 -0
  25. package/plugin/scripts/gates.mjs +113 -10
  26. package/plugin/scripts/grounding-turn-gate.mjs +167 -0
  27. package/plugin/scripts/grounding-turn-mark.mjs +91 -0
  28. package/plugin/scripts/hook-shim.mjs +14 -0
  29. package/plugin/scripts/host-shell-boundary.mjs +43 -0
  30. package/plugin/scripts/nightly-scheduler.mjs +37 -4
  31. package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
  32. package/plugin/scripts/project-progression-contract.mjs +16 -0
  33. package/plugin/scripts/project-progression-hook.mjs +3 -0
  34. package/plugin/scripts/project-progression-producer.mjs +252 -0
  35. package/plugin/scripts/project-progression-reader.mjs +271 -0
  36. package/plugin/scripts/project-progression-session-start.mjs +93 -16
  37. package/plugin/scripts/project-progression-sources.mjs +220 -0
  38. package/plugin/scripts/project-progression-store.mjs +106 -13
  39. package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
  40. package/plugin/scripts/session-snapshot-hook.mjs +115 -7
  41. package/plugin/scripts/session-start-budget.mjs +59 -0
  42. package/plugin/scripts/session-start-core.mjs +234 -457
  43. package/plugin/scripts/session-start-fsutil.mjs +61 -0
  44. package/plugin/scripts/session-start-health.mjs +64 -0
  45. package/plugin/scripts/session-start-hook-description.mjs +45 -0
  46. package/plugin/scripts/session-start-issue-alert.mjs +77 -0
  47. package/plugin/scripts/session-start-repo-identity.mjs +54 -0
  48. package/plugin/scripts/session-start-signals.mjs +73 -0
  49. package/plugin/scripts/session-start-trace.mjs +86 -0
  50. package/plugin/scripts/session-start-update-plane.mjs +104 -0
  51. package/plugin/scripts/unprompted-runtime.mjs +32 -2
  52. package/plugin/scripts/update-apply.mjs +2 -32
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
  54. package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
  55. package/scripts/adr-072-completion.mjs +1 -1
  56. package/scripts/agentdb-fleet-doctor.mjs +5 -1
  57. package/scripts/approved-runtime.mjs +197 -0
  58. package/scripts/brain-novice-50.mjs +16 -1
  59. package/scripts/brain-score.mjs +23 -5
  60. package/scripts/build-bundle.mjs +971 -530
  61. package/scripts/build-concepts.mjs +36 -116
  62. package/scripts/console-engine.test.mjs +8 -7
  63. package/scripts/console-runtime-identity.mjs +4 -0
  64. package/scripts/corpus-aggregates.mjs +94 -77
  65. package/scripts/corpus-candidate.mjs +475 -222
  66. package/scripts/corpus-next-seed.mjs +225 -0
  67. package/scripts/corpus-promotion.mjs +58 -0
  68. package/scripts/corpus-reconcile.mjs +411 -105
  69. package/scripts/doc-currency.mjs +16 -1
  70. package/scripts/dual-host-deliberation.mjs +25 -2
  71. package/scripts/dual-host-suggest.mjs +17 -1
  72. package/scripts/falsify.mjs +13 -3
  73. package/scripts/gist-receipts.mjs +482 -87
  74. package/scripts/github-health-watch.mjs +12 -2
  75. package/scripts/handoff-asset.mjs +34 -0
  76. package/scripts/hook-retirement-check.mjs +8 -1
  77. package/scripts/host-registry.mjs +1 -1
  78. package/scripts/ingest-gists.mjs +74 -101
  79. package/scripts/job-heartbeat.sh +77 -14
  80. package/scripts/learning-replay-execution.mjs +10 -4
  81. package/scripts/nightly-gists.sh +27 -13
  82. package/scripts/nightly-two-run-proof.mjs +1 -1
  83. package/scripts/nightly-watchdog.mjs +61 -4
  84. package/scripts/onboarding-console.mjs +319 -27
  85. package/scripts/oracle/produce-questions.mjs +293 -0
  86. package/scripts/oracle/producer-hosts.mjs +235 -0
  87. package/scripts/oracle/repo-recall.mjs +448 -0
  88. package/scripts/oracle/retrieval-accuracy.mjs +818 -0
  89. package/scripts/oracle/source-tree.mjs +165 -0
  90. package/scripts/oracle/source-units.mjs +391 -0
  91. package/scripts/oracle/spike-run.mjs +98 -0
  92. package/scripts/oracle/unit-inventory.mjs +141 -0
  93. package/scripts/oracle/unit-sampling.mjs +128 -0
  94. package/scripts/oracle/validate-labels.mjs +250 -0
  95. package/scripts/private-overlay.mjs +248 -0
  96. package/scripts/product-integrity-contract.mjs +1 -1
  97. package/scripts/proxy/claude-proxied.sh +6 -0
  98. package/scripts/proxy/proxy-revert.sh +5 -0
  99. package/scripts/proxy/proxy-up.sh +6 -0
  100. package/scripts/proxy/proxy-verify.mjs +4 -0
  101. package/scripts/public-inputs.mjs +409 -0
  102. package/scripts/public-verification-inputs.mjs +112 -26
  103. package/scripts/public-verification-lane.mjs +1 -1
  104. package/scripts/published-surface-probe.mjs +34 -4
  105. package/scripts/qe/card-lane-gate.mjs +16 -1
  106. package/scripts/qe/session-start-gate.mjs +16 -1
  107. package/scripts/rebuild-gists-from-receipts.mjs +58 -78
  108. package/scripts/record-lesson.mjs +4 -1
  109. package/scripts/rehearse-corpus-pipeline.mjs +994 -0
  110. package/scripts/release-abort-stale.mjs +5 -1
  111. package/scripts/release-authority.mjs +104 -12
  112. package/scripts/release-channel-kind.mjs +86 -0
  113. package/scripts/release-convergence-watchdog.mjs +7 -2
  114. package/scripts/release-projection.mjs +177 -72
  115. package/scripts/release-transaction-provider.mjs +23 -6
  116. package/scripts/release.mjs +252 -17
  117. package/scripts/retrieval-canary.mjs +87 -0
  118. package/scripts/rvf-index-audit.mjs +573 -13
  119. package/scripts/rvf-wire.mjs +269 -0
  120. package/scripts/seal-gist-receipt.mjs +65 -0
  121. package/scripts/selfcheck.mjs +42 -21
  122. package/scripts/source-coverage.mjs +253 -24
  123. package/scripts/status-honesty.mjs +25 -0
  124. package/scripts/sync-census.mjs +0 -0
  125. package/scripts/sync-version.mjs +2 -0
  126. package/scripts/trismart.mjs +42 -0
  127. package/scripts/updater-manifest.mjs +162 -0
  128. package/scripts/verify-channels.mjs +17 -5
  129. package/scripts/wired-check.mjs +48 -10
  130. package/tri-smart-skill/QUICKSTART.md +37 -0
  131. package/tri-smart-skill/README.md +92 -0
  132. package/tri-smart-skill/install.cmd +14 -0
  133. package/tri-smart-skill/install.command +13 -0
  134. package/tri-smart-skill/install.mjs +51 -0
  135. package/tri-smart-skill/install.sh +9 -0
  136. package/tri-smart-skill/tri-smart/SKILL.md +90 -0
  137. package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
  138. package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
  139. package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
  140. package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
  141. package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
  142. package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
  143. 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 !== 'PASS' || !result.path.endsWith('.big.rvf')) continue;
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 repair = process.argv.includes('--repair');
93
- const concurrencyArg = process.argv.indexOf('--concurrency');
94
- const concurrency = Math.max(1, Number(concurrencyArg >= 0 ? process.argv[concurrencyArg + 1] : 3));
95
- const namesAt = process.argv.indexOf('--names');
96
- const names = namesAt >= 0
97
- ? new Set(String(process.argv[namesAt + 1] || '').split(',').filter(Boolean))
98
- : null;
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 !== 'PASS');
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
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
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;