ruvnet-brain 4.0.1 → 4.0.4

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 (195) hide show
  1. package/.claude-plugin/marketplace.json +1 -0
  2. package/README.md +4 -4
  3. package/bin/install.mjs +303 -24
  4. package/console/CONTRACT.md +172 -0
  5. package/console/activity.js +753 -0
  6. package/console/app.js +4189 -0
  7. package/console/architecture.html +1221 -0
  8. package/console/assets/depth-1.webp +0 -0
  9. package/console/assets/depth-2.webp +0 -0
  10. package/console/assets/depth-3.webp +0 -0
  11. package/console/assets/harness-vs-plain.svg +259 -0
  12. package/console/assets/hero.webp +0 -0
  13. package/console/assets/memory.webp +0 -0
  14. package/console/assets/metaharness.svg +247 -0
  15. package/console/index.html +777 -0
  16. package/console/install-architecture.html +162 -0
  17. package/console/install-mockup.html +543 -0
  18. package/console/style.css +2144 -0
  19. package/console/tips.css +926 -0
  20. package/console/tips.html +858 -0
  21. package/console/tips.js +128 -0
  22. package/docs/RELEASE-NOTES-4.0.md +88 -0
  23. package/kb/model-requirements.mjs +37 -6
  24. package/keys/ruvnet-brain-signing.pub.pem +3 -0
  25. package/package.json +8 -22
  26. package/plugin/.claude-plugin/marketplace.json +1 -0
  27. package/plugin/.claude-plugin/plugin.json +2 -3
  28. package/plugin/.codex-plugin/plugin.json +1 -1
  29. package/plugin/commands/brain-console.md +2 -2
  30. package/plugin/commands/configure.md +3 -2
  31. package/plugin/commands/rvbc.md +4 -3
  32. package/plugin/commands/rvcb.md +2 -2
  33. package/plugin/commands/whats-new.md +6 -6
  34. package/plugin/docs/RELEASE-NOTES-4.0.md +88 -0
  35. package/plugin/hooks/hooks.json +1 -2
  36. package/plugin/mcp/managed-cli-interface.mjs +47 -4
  37. package/plugin/mcp/server.mjs +90 -32
  38. package/plugin/scripts/detach.mjs +14 -0
  39. package/plugin/scripts/first-session-worker.mjs +38 -0
  40. package/plugin/scripts/ground-ruvnet.sh +16 -6
  41. package/plugin/scripts/hook-shim.mjs +34 -29
  42. package/plugin/scripts/learn-capture.sh +22 -3
  43. package/plugin/scripts/learn-flush.mjs +21 -4
  44. package/plugin/scripts/runtime-preferences.mjs +269 -0
  45. package/plugin/scripts/session-start-core.mjs +503 -0
  46. package/plugin/scripts/session-start.sh +3 -858
  47. package/plugin/scripts/whats-new.mjs +42 -0
  48. package/plugin/skills/brain-console/SKILL.md +4 -2
  49. package/plugin/skills/release-proof/SKILL.md +98 -0
  50. package/plugin/skills/release-proof/agents/openai.yaml +4 -0
  51. package/plugin/skills/release-proof/references/receipt-contract.md +44 -0
  52. package/plugin/skills/release-proof/scripts/release-proof.mjs +286 -0
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +5 -1
  54. package/plugin/skills/ruvnet-brain/SKILL.md +22 -7
  55. package/plugin/skills/rvbc/SKILL.md +9 -6
  56. package/plugin/skills/whats-new/SKILL.md +4 -4
  57. package/scripts/adr-backfill.mjs +107 -0
  58. package/scripts/advocacy-outcomes.mjs +808 -0
  59. package/scripts/agentdb-context.mjs +216 -0
  60. package/scripts/agentdb-fleet-doctor.mjs +101 -0
  61. package/scripts/ascii-drift.mjs +236 -0
  62. package/scripts/behavioral-l1-l4.mjs +210 -0
  63. package/scripts/brain-capability-check.mjs +72 -0
  64. package/scripts/brain-grade-groundtruth.mjs +100 -0
  65. package/scripts/brain-latency-50.mjs +227 -0
  66. package/scripts/brain-novice-50.mjs +189 -0
  67. package/scripts/brain-stamp.mjs +94 -0
  68. package/scripts/brain-state.mjs +212 -0
  69. package/scripts/build-bundle.mjs +531 -0
  70. package/scripts/build-concepts.mjs +132 -0
  71. package/scripts/build-l2.mjs +71 -0
  72. package/scripts/build-primer.mjs +73 -0
  73. package/scripts/build-symbols.mjs +68 -0
  74. package/scripts/calibrate-router.mjs +97 -0
  75. package/scripts/capability-audit.mjs +321 -0
  76. package/scripts/capability-registry.mjs +876 -0
  77. package/scripts/check-indexation.mjs +108 -0
  78. package/scripts/check-legibility.mjs +189 -0
  79. package/scripts/ci/build-fixture-kb.mjs +67 -0
  80. package/scripts/ci/learning-replay-codex-adapter.mjs +62 -0
  81. package/scripts/ci/learning-replay-recorder.mjs +59 -0
  82. package/scripts/ci/mutate-hook-timeout.mjs +70 -0
  83. package/scripts/ci/stranger-fixture-stage.mjs +17 -0
  84. package/scripts/ci/stranger-scenario.mjs +228 -0
  85. package/scripts/ci/stranger-timeout.mjs +25 -0
  86. package/scripts/ci-verdict.mjs +29 -0
  87. package/scripts/claims-verify.mjs +710 -0
  88. package/scripts/clear-claude-tmp.sh +31 -0
  89. package/scripts/console-engine.mjs +434 -0
  90. package/scripts/console-engine.test.mjs +125 -0
  91. package/scripts/corpus-qa.mjs +250 -0
  92. package/scripts/correction-detect-embed.mjs +346 -0
  93. package/scripts/correction-detect-measure.mjs +270 -0
  94. package/scripts/correction-detect.mjs +686 -0
  95. package/scripts/count-chunks.mjs +54 -0
  96. package/scripts/described-questions.json +30 -0
  97. package/scripts/design-grade.mjs +58 -0
  98. package/scripts/dev-plugin-link.sh +105 -0
  99. package/scripts/distill-project.mjs +200 -0
  100. package/scripts/doc-currency.mjs +801 -0
  101. package/scripts/eval-brain.mjs +244 -0
  102. package/scripts/fix-metaharness-memretrieve.mjs +121 -0
  103. package/scripts/fix-workstream.mjs +291 -0
  104. package/scripts/full-hints.mjs +87 -0
  105. package/scripts/gate.sh +39 -0
  106. package/scripts/gates.mjs +146 -0
  107. package/scripts/gen-console-images.mjs +54 -0
  108. package/scripts/gen-images.mjs +47 -0
  109. package/scripts/git-clone-refresh.mjs +52 -0
  110. package/scripts/git-hooks/pre-push +126 -0
  111. package/scripts/goal-match.mjs +398 -0
  112. package/scripts/goldie-research.mjs +223 -0
  113. package/scripts/goldie-weekly.sh +67 -0
  114. package/scripts/health-repair.mjs +237 -0
  115. package/scripts/helix-scenario-questions.json +10 -0
  116. package/scripts/ingest-gists.mjs +230 -0
  117. package/scripts/ingest-meeting.mjs +115 -0
  118. package/scripts/ingest-repo.mjs +79 -0
  119. package/scripts/install-npx-witness.sh +49 -0
  120. package/scripts/issue-fix.mjs +558 -0
  121. package/scripts/issue-watch.mjs +276 -0
  122. package/scripts/issue4-close-note.md +31 -0
  123. package/scripts/key-canary.mjs +91 -0
  124. package/scripts/latency-to-surface.mjs +233 -0
  125. package/scripts/learning-enable.mjs +380 -0
  126. package/scripts/learning-replay.mjs +1570 -0
  127. package/scripts/learnings.mjs +62 -0
  128. package/scripts/lesson-gate.mjs +680 -0
  129. package/scripts/lesson-lifecycle.mjs +449 -0
  130. package/scripts/lesson-promote.mjs +262 -0
  131. package/scripts/lesson-ratify.mjs +98 -0
  132. package/scripts/lesson-seed.mjs +252 -0
  133. package/scripts/lesson-store.mjs +447 -0
  134. package/scripts/loop-checkpoint.mjs +86 -0
  135. package/scripts/memdb-health.sh +14 -0
  136. package/scripts/memory-doctor.mjs +326 -0
  137. package/scripts/model-catalog.mjs +79 -0
  138. package/scripts/nightly-controller.mjs +66 -0
  139. package/scripts/nightly-gists.sh +72 -0
  140. package/scripts/nightly-wrapper.sh +172 -0
  141. package/scripts/notify.sh +12 -0
  142. package/scripts/npx-witness.sh +56 -0
  143. package/scripts/onboarding-console.mjs +2922 -0
  144. package/scripts/private-fence.mjs +69 -0
  145. package/scripts/proactivity-metrics.mjs +118 -0
  146. package/scripts/proof-questions.json +56 -0
  147. package/scripts/protected-release-invocation.mjs +76 -0
  148. package/scripts/prove.mjs +95 -0
  149. package/scripts/proxy/claude-proxied.sh +57 -0
  150. package/scripts/proxy/proxy-revert.sh +59 -0
  151. package/scripts/proxy/proxy-up.sh +60 -0
  152. package/scripts/proxy/proxy-verify.mjs +142 -0
  153. package/scripts/publication-receipt.mjs +307 -0
  154. package/scripts/published-surface-probe.mjs +241 -0
  155. package/scripts/qe/card-lane-gate.mjs +162 -0
  156. package/scripts/qe/session-start-gate.mjs +229 -0
  157. package/scripts/qe/ux-suite.mjs +323 -0
  158. package/scripts/reconcile-project.mjs +0 -0
  159. package/scripts/record-lesson.mjs +113 -0
  160. package/scripts/refresh-model-catalog.mjs +99 -0
  161. package/scripts/release-authority.mjs +93 -0
  162. package/scripts/release-proof.mjs +9 -0
  163. package/scripts/release-vector.mjs +281 -0
  164. package/scripts/release.mjs +439 -0
  165. package/scripts/remedy-registry.mjs +247 -0
  166. package/scripts/rerank-cap-eval.mjs +265 -0
  167. package/scripts/rerank-cap-warm-ab.mjs +129 -0
  168. package/scripts/route-cheap.mjs +20 -15
  169. package/scripts/router-utilization.mjs +182 -0
  170. package/scripts/routing-flywheel.mjs +596 -0
  171. package/scripts/rvf-generation.mjs +104 -0
  172. package/scripts/rvf-index-audit.mjs +138 -0
  173. package/scripts/self-update.mjs +296 -0
  174. package/scripts/selfcheck.mjs +7 -1
  175. package/scripts/sign-bundle.mjs +69 -0
  176. package/scripts/signal-watch.mjs +171 -0
  177. package/scripts/stabilization-receipt.mjs +108 -0
  178. package/scripts/stack-sync.mjs +469 -0
  179. package/scripts/stamp-existing-rvf-generations.mjs +53 -0
  180. package/scripts/stamp-sweep.mjs +144 -0
  181. package/scripts/status-honesty.mjs +102 -0
  182. package/scripts/sync-version.mjs +217 -0
  183. package/scripts/token-report.mjs +102 -0
  184. package/scripts/top100-benchmark.mjs +479 -0
  185. package/scripts/top100-corpus.mjs +112 -0
  186. package/scripts/top100-semantic-assertions.mjs +449 -0
  187. package/scripts/update-apply.mjs +9 -0
  188. package/scripts/upgrade-notice.mjs +14 -0
  189. package/scripts/verify-bundle.mjs +51 -0
  190. package/scripts/verify-channels.mjs +184 -0
  191. package/scripts/verify-model-catalog.mjs +104 -0
  192. package/scripts/verify-nightly-close-issue4.sh +31 -0
  193. package/scripts/version.mjs +40 -0
  194. package/scripts/wired-check.mjs +867 -0
  195. package/plugin/scripts/finalize-token-meter.mjs +0 -25
@@ -0,0 +1,531 @@
1
+ #!/usr/bin/env node
2
+ // build-bundle.mjs — assemble the shippable RuvNet Brain bundle from every BUILT per-repo KB.
3
+ //
4
+ // Collects, into dist/ruvnet-brain/: each built repo's vector store(s) + passages + meta + symbols
5
+ // (+ the sharp `big` variant, per-repo primer, and L2 articles when present), the shared runtime
6
+ // tools (forge-ask / forge-ask-all / forge-mcp / forge-mcp-all / forge-rerank / forge-guard /
7
+ // resolve-deps / package.json), a manifest.json (coverage + per-repo chunks/variants/grade/SHA),
8
+ // an mcp.snippet.json (a cross-repo "ruvnet-brain" entry + one per-repo entry), and a README.
9
+ //
10
+ // Idempotent: re-run any time as new variants/primers/grades land — it copies whatever exists now
11
+ // and records honestly what is still missing. Does NOT embed or grade; pure assembly.
12
+ //
13
+ // node scripts/build-bundle.mjs [--assets /path/to/release-assets]
14
+ // [--out dist/ruvnet-brain] [--version v0.2.0-dev]
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { spawnSync } from 'node:child_process';
19
+ import { fileURLToPath } from 'node:url';
20
+ import { getVersion, getVersionTag, stripTag } from './version.mjs';
21
+ import { auditRvfIndexes } from './rvf-index-audit.mjs';
22
+
23
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const KB = path.join(ROOT, 'kb');
25
+ const DATA = path.join(ROOT, 'data');
26
+ const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
27
+ // RVF binaries are intentionally release assets, not Git blobs. A clean worktree therefore has
28
+ // zero stores unless the release operator supplies the canonical external asset directory. Keep
29
+ // source/runtime files rooted in this exact checkout; only generated RVF families come from ASSETS.
30
+ const ASSETS = path.resolve(ROOT, arg('--assets', 'kb'));
31
+ const OUT = path.resolve(ROOT, arg('--out', 'dist/ruvnet-brain'));
32
+ const BRAIN_VERSION = arg('--version', getVersionTag()); // inherits the single source of truth
33
+
34
+ // ---- registry: tier + the full 169-repo pending list -------------------------------------------
35
+ const registry = JSON.parse(fs.readFileSync(path.join(DATA, 'registry.tiers.json'), 'utf8'));
36
+ const regFlat = [];
37
+ for (const [tier, t] of Object.entries(registry.tiers || {})) for (const r of (t.repos || [])) regFlat.push({ ...r, tier });
38
+ const regByLower = new Map(regFlat.map((r) => [r.name.toLowerCase(), r]));
39
+
40
+ // ---- prior manifest (for builtFromSha continuity if brain-stamp already ran) --------------------
41
+ let priorSha = {};
42
+ try {
43
+ const prev = JSON.parse(fs.readFileSync(path.join(DATA, 'manifest.json'), 'utf8'));
44
+ for (const b of (prev.builtRepos || [])) if (b.builtFromSha) priorSha[b.name.toLowerCase()] = b.builtFromSha;
45
+ } catch { /* none yet */ }
46
+
47
+ // ---- PRIVATE stores (excluded from any publishable bundle) --------------------------------------
48
+ // kb/PRIVATE-STORES.json lists store names built from PRIVATE source that MUST NOT ship. We read it
49
+ // here and drop those names during discovery so private code can never leak into dist/.
50
+ // FAIL-CLOSED (security-critical — SEC-0010 #4). A private-store fence that degrades to an EMPTY
51
+ // set on any error means a truncated/corrupt/missing PRIVATE-STORES.json silently ships EVERY store,
52
+ // including private cognitum source. So: a present-but-unparseable fence ALWAYS aborts the build; a
53
+ // missing fence aborts too, unless the operator explicitly opts out (ALLOW_NO_PRIVATE_FENCE=1) — the
54
+ // escape hatch a genuine no-private public fork needs, but never the silent default.
55
+ function loadPrivateStores() {
56
+ const p = path.join(KB, 'PRIVATE-STORES.json');
57
+ if (!fs.existsSync(p)) {
58
+ if (process.env.ALLOW_NO_PRIVATE_FENCE === '1') {
59
+ console.warn('[build-bundle] no PRIVATE-STORES.json; ALLOW_NO_PRIVATE_FENCE=1 → proceeding with NO fence.');
60
+ return new Set();
61
+ }
62
+ console.error(`[build-bundle] FATAL: private-store fence missing (${p}). Refusing to build — a bundle ` +
63
+ `without a verified fence could ship private source. Set ALLOW_NO_PRIVATE_FENCE=1 only for a genuine ` +
64
+ `no-private fork.`);
65
+ process.exit(1);
66
+ }
67
+ let j;
68
+ try {
69
+ j = JSON.parse(fs.readFileSync(p, 'utf8'));
70
+ } catch (e) {
71
+ console.error(`[build-bundle] FATAL: PRIVATE-STORES.json is present but unreadable/corrupt (${e.message}). ` +
72
+ `Refusing to build — cannot prove private stores are fenced out.`);
73
+ process.exit(1);
74
+ }
75
+ if (!Array.isArray(j.privateStores)) {
76
+ console.error('[build-bundle] FATAL: PRIVATE-STORES.json has no valid "privateStores" array. Refusing to build.');
77
+ process.exit(1);
78
+ }
79
+ return new Set(j.privateStores.map((s) => String(s).toLowerCase()));
80
+ }
81
+ const PRIVATE_STORES = loadPrivateStores();
82
+
83
+ // ---- discover BUILT repos (those with <name>.big.rvf in kb/) ------------------------------------
84
+ function discoverBuilt() {
85
+ const names = new Set();
86
+ const excludedPrivate = [];
87
+ if (!fs.existsSync(ASSETS)) return [];
88
+ for (const f of fs.readdirSync(ASSETS)) {
89
+ if (f.startsWith('._')) continue; // AppleDouble metadata is never a vector store.
90
+ const m = f.match(/^(.+?)\.big\.rvf$/);
91
+ if (!m) continue;
92
+ if (/\.(idmap|embed)\b/.test(f)) continue;
93
+ if (PRIVATE_STORES.has(m[1].toLowerCase())) { excludedPrivate.push(m[1]); continue; }
94
+ names.add(m[1]);
95
+ }
96
+ if (excludedPrivate.length) console.log(`[build-bundle] EXCLUDED ${excludedPrivate.length} PRIVATE store(s): ${[...new Set(excludedPrivate)].sort().join(', ')}`);
97
+ return [...names].sort();
98
+ }
99
+
100
+ // ---- best-effort grade lookup (REAL-USE avg) from data/grade-<name>-big.json -------------------
101
+ function gradeFor(name) {
102
+ for (const f of [`grade-${name}-big.json`, `grade-${name}.json`, `grade-${name}-small.json`]) {
103
+ const p = path.join(DATA, f);
104
+ if (!fs.existsSync(p)) continue;
105
+ try {
106
+ const g = JSON.parse(fs.readFileSync(p, 'utf8'));
107
+ if (typeof g.avgRealUse === 'number') return { realUse: g.avgRealUse, src: f };
108
+ if (g.summary && typeof g.summary.avgRealUse === 'number') return { realUse: g.summary.avgRealUse, src: f };
109
+ const qs = Array.isArray(g.questions) ? g.questions : Array.isArray(g) ? g : null;
110
+ if (qs && qs.length) {
111
+ const vals = qs.map((q) => q.realUse).filter((v) => typeof v === 'number');
112
+ if (vals.length) return { realUse: +(vals.reduce((a, b) => a + b, 0) / vals.length).toFixed(1), src: f };
113
+ }
114
+ } catch { /* ignore */ }
115
+ }
116
+ return null;
117
+ }
118
+
119
+ // ---- file copy helper ---------------------------------------------------------------------------
120
+ let copied = 0, missing = [];
121
+ function cp(src, destDir, { required = false, asset = false } = {}) {
122
+ const s = path.isAbsolute(src) ? src : path.join(asset ? ASSETS : KB, src);
123
+ if (!fs.existsSync(s)) { if (required) missing.push(path.basename(src)); return false; }
124
+ fs.copyFileSync(s, path.join(destDir, path.basename(s)));
125
+ copied++;
126
+ return true;
127
+ }
128
+ // Private SLUG set (QE-0011 security#1) — kb/l2/ is copied wholesale below; a private repo's raw L2
129
+ // .md would ship as a file even though the vector store fences it. Read each private repo's own
130
+ // l2-topics.<repo>.json to learn its slugs and skip those .md files. Fail-closed on a corrupt file.
131
+ const PRIVATE_L2_SLUGS = new Set();
132
+ for (const p of PRIVATE_STORES) {
133
+ const tf = path.join(KB, `l2-topics.${p}.json`);
134
+ if (!fs.existsSync(tf)) continue;
135
+ try { for (const t of JSON.parse(fs.readFileSync(tf, 'utf8'))) if (t.slug) PRIVATE_L2_SLUGS.add(`${t.slug}.md`); }
136
+ catch (e) { console.error(`[build-bundle] FATAL: private topics ${tf} corrupt (${e.message}). Refusing to build.`); process.exit(1); }
137
+ }
138
+ // skipNames: an optional Set of filenames to exclude (used to fence private L2 .md out of the l2/ copy).
139
+ function cpDir(srcDir, destDir, skipNames) {
140
+ if (!fs.existsSync(srcDir)) return false;
141
+ fs.mkdirSync(destDir, { recursive: true });
142
+ for (const e of fs.readdirSync(srcDir, { withFileTypes: true })) {
143
+ if (skipNames && skipNames.has(e.name)) continue;
144
+ if (e.isDirectory()) cpDir(path.join(srcDir, e.name), path.join(destDir, e.name), skipNames);
145
+ else { fs.copyFileSync(path.join(srcDir, e.name), path.join(destDir, e.name)); copied++; }
146
+ }
147
+ return true;
148
+ }
149
+
150
+ // ---- assemble ----------------------------------------------------------------------------------
151
+ const ZIP = path.join(path.dirname(OUT), `${path.basename(OUT)}.zip`);
152
+ fs.rmSync(OUT, { recursive: true, force: true });
153
+ // A failed rebuild must not leave an older archive looking like this invocation's output.
154
+ fs.rmSync(ZIP, { force: true });
155
+ fs.mkdirSync(OUT, { recursive: true });
156
+
157
+ const built = discoverBuilt();
158
+ if (built.length === 0) {
159
+ console.error('[build-bundle] FATAL: zero public RVF stores are eligible for release. Refusing to publish an empty brain bundle.');
160
+ process.exit(1);
161
+ }
162
+ const rvfIndexAudit = await auditRvfIndexes(
163
+ built.map((name) => path.join(ASSETS, `${name}.big.rvf`)),
164
+ );
165
+ const missingIndexes = rvfIndexAudit.filter(({ state }) => state !== 'PASS');
166
+ if (missingIndexes.length) {
167
+ console.error('[build-bundle] FATAL: eligible RVFs lack persisted HNSW indexes:');
168
+ for (const result of missingIndexes) {
169
+ console.error(` ${path.basename(result.path)} vectors=${result.totalVectors}`);
170
+ }
171
+ console.error('[build-bundle] Repair with: node scripts/rvf-index-audit.mjs --repair');
172
+ process.exit(1);
173
+ }
174
+ const builtRepos = [];
175
+ for (const name of built) {
176
+ // One canonical computer-focused vector store per repository. The unsuffixed passages/meta
177
+ // sidecars are shared with the .big RVF so source text and metadata are packaged once.
178
+ cp(`${name}.big.rvf`, OUT, { required: true, asset: true });
179
+ cp(`${name}.big.rvf.idmap.json`, OUT, { required: true, asset: true });
180
+ cp(`${name}.big.rvf.embed.json`, OUT, { required: true, asset: true });
181
+ cp(`${name}.passages.jsonl`, OUT, { required: true, asset: true });
182
+ cp(`${name}.meta.json`, OUT, { required: true, asset: true });
183
+ const hasSymbols = cp(`${name}.symbols.json`, OUT, { asset: true });
184
+ const hasPrimer = cp(`${name}-primer.md`, OUT);
185
+ // metadata
186
+ let chunks = null, model = null, dims = null;
187
+ try { const m = JSON.parse(fs.readFileSync(path.join(ASSETS, `${name}.meta.json`), 'utf8')); chunks = m.entries ? Object.keys(m.entries).length : null; model = m.model; dims = m.dimensions; } catch { /* */ }
188
+ const reg = regByLower.get(name.toLowerCase()) || {};
189
+ const grade = gradeFor(name);
190
+ builtRepos.push({
191
+ name, tier: reg.tier || '?', stars: reg.stars ?? null,
192
+ chunks, baseModel: model, baseDims: dims,
193
+ variants: ['big'],
194
+ hasSymbols, hasPrimer, hasBig: true,
195
+ gradeRealUse: grade ? grade.realUse : null,
196
+ builtFromSha: priorSha[name.toLowerCase()] || 'unknown',
197
+ status: 'built',
198
+ });
199
+ }
200
+ // Publish only generation records for stores that passed the private-store fence above. Copying
201
+ // the machine-wide ledger wholesale would leak private store names/hashes even though their RVFs
202
+ // were correctly excluded.
203
+ {
204
+ const generationsFile = path.join(ASSETS, 'RVF-GENERATIONS.json');
205
+ if (!fs.existsSync(generationsFile)) {
206
+ missing.push('RVF-GENERATIONS.json');
207
+ } else {
208
+ const source = JSON.parse(fs.readFileSync(generationsFile, 'utf8'));
209
+ const stores = {};
210
+ for (const { name } of builtRepos) {
211
+ if (!source.stores?.[name]) missing.push(`RVF-GENERATIONS.json:${name}`);
212
+ else stores[name] = source.stores[name];
213
+ }
214
+ fs.writeFileSync(path.join(OUT, 'RVF-GENERATIONS.json'), `${JSON.stringify({
215
+ schemaVersion: source.schemaVersion,
216
+ brainVersion: source.brainVersion,
217
+ releaseTag: source.releaseTag,
218
+ stores,
219
+ }, null, 2)}\n`);
220
+ copied++;
221
+ }
222
+ }
223
+
224
+ // L2 articles (whole dir, fencing out private repos' raw .md) + master primer dir (the master
225
+ // ruvnet-primer overview — not per-repo, so nothing private to fence there).
226
+ cpDir(path.join(KB, 'l2'), path.join(OUT, 'l2'), PRIVATE_L2_SLUGS);
227
+ cpDir(path.join(ROOT, 'primer'), path.join(OUT, 'primer'));
228
+
229
+ // CONCEPTS store (L2 + primers embedded as prose; big-only) — the cross-repo tool unions it at query
230
+ // time so code-implemented capabilities are retrievable as high-confidence prose. discoverRepos in the
231
+ // bundle finds concepts.big.rvf automatically, so search_ruvnet searches it with no extra config.
232
+ const hasConcepts = fs.existsSync(path.join(ASSETS, 'concepts.big.rvf'));
233
+ if (hasConcepts) for (const suf of ['concepts.big.rvf', 'concepts.big.rvf.idmap.json', 'concepts.big.rvf.embed.json', 'concepts.big.passages.jsonl', 'concepts.big.meta.json', 'concepts.passages.jsonl', 'concepts.meta.json']) cp(suf, OUT, { asset: true });
234
+
235
+ // RUV-GISTS store (rUv's public gists — release notes / integration dossiers; big-only, same shape as
236
+ // concepts, unioned by search_ruvnet at query time). BUG FIX: only `concepts` was special-cased above,
237
+ // so ruv-gists — a PUBLIC store, not private-fenced — was silently omitted from EVERY bundle ever
238
+ // shipped. Include it, WITH .big.passages.jsonl (the big variant opens it by name, same as concepts).
239
+ const hasGists = fs.existsSync(path.join(ASSETS, 'ruv-gists.big.rvf'));
240
+ if (hasGists) for (const suf of ['ruv-gists.big.rvf', 'ruv-gists.big.rvf.idmap.json', 'ruv-gists.big.rvf.embed.json', 'ruv-gists.big.passages.jsonl', 'ruv-gists.big.meta.json', 'ruv-gists.passages.jsonl', 'ruv-gists.meta.json']) cp(suf, OUT, { asset: true });
241
+
242
+ // capability-cards.md — the FAST LANE's zero-ML answer source (kb/card-lane.mjs, the first
243
+ // responder search_ruvnet consults before the heavy cross-repo search). Ships as its own small
244
+ // text file, NOT only baked into concepts.big.rvf, so the fast lane can answer without opening
245
+ // any vector store or loading either ONNX model. Re-filtered by the SAME PRIVATE_STORES fence
246
+ // applied to every other shipped artifact above — reapplied here rather than trusted from the
247
+ // source file, because a raw-file copy is a NEW shipping path for this exact content
248
+ // (scripts/build-concepts.mjs already fences it into the concepts store; that fence must not be
249
+ // silently regained by a second path that copies the same source file without it).
250
+ {
251
+ const cardsSrc = path.join(KB, 'capability-cards.md');
252
+ if (fs.existsSync(cardsSrc)) {
253
+ const raw = fs.readFileSync(cardsSrc, 'utf8');
254
+ const parts = raw.split(/^##\s+/m);
255
+ const kept = [];
256
+ let filteredAny = false;
257
+ for (const sec of parts.slice(1)) {
258
+ const nl = sec.indexOf('\n');
259
+ const repo = nl < 0 ? '' : sec.slice(0, nl).trim();
260
+ if (repo && PRIVATE_STORES.has(repo.toLowerCase())) { filteredAny = true; continue; }
261
+ kept.push(`## ${sec}`);
262
+ }
263
+ fs.writeFileSync(path.join(OUT, 'capability-cards.md'), parts[0] + kept.join(''));
264
+ copied++;
265
+ if (filteredAny) console.log('[build-bundle] filtered private repo card(s) out of shipped capability-cards.md');
266
+ } else {
267
+ console.log('[build-bundle] note: kb/capability-cards.md absent — the fast lane ships with no card source and will honestly fall through on every query');
268
+ }
269
+ }
270
+
271
+ // shared runtime tools. forge-guard-injection.mjs is REQUIRED — forge-mcp-all.mjs imports it, so a
272
+ // bundle without it crashes the brain on startup (MODULE_NOT_FOUND). forge-update.mjs is the consumer
273
+ // self-updater (reads SOURCE.json, copied below).
274
+ // package-lock.json ships so the installer can `npm ci` (pinned, reproducible) instead of an
275
+ // unpinned `npm i` resolve (SEC-0010 #8).
276
+ // verify-citation.mjs ships WITH the KB because it verifies the KB: --doctor loads it from the
277
+ // installed bundle to prove an answer's citation resolves to a real passage on disk.
278
+ //
279
+ // DERIVED, NOT HAND-MAINTAINED (issue #32, Jan Lafko / @lafinak, 2026-07-20).
280
+ // This list used to be typed by hand, and twice a new required import fell out of it and shipped a
281
+ // bundle that crashed on startup with MODULE_NOT_FOUND — most recently `forge-hybrid.mjs`, which
282
+ // `forge-ask-all.mjs` imports unconditionally (commit 57354dd). The warning comment above was already
283
+ // there and did not prevent it, because a comment cannot know what a file imports. The build now does.
284
+ //
285
+ // We walk the real static import graph from the entry points that must be runnable out of the
286
+ // installed bundle, and ship every local module they transitively reach. A new import is therefore
287
+ // bundled the moment it is written, with no list to remember to update.
288
+ const ENTRYPOINTS = [
289
+ 'forge-ask.mjs', 'forge-ask-all.mjs', 'forge-mcp.mjs', 'forge-mcp-all.mjs',
290
+ 'forge-rerank.mjs', 'forge-guard.mjs', 'forge-update.mjs', 'verify-citation.mjs',
291
+ ];
292
+ // Non-module assets, and modules reached only by a path the graph walk cannot see (e.g. a spawned
293
+ // child process). Kept explicit BECAUSE they are genuinely not derivable — not as a duplicate list.
294
+ const EXTRA_FILES = ['package.json', 'package-lock.json', 'package-owners.json'];
295
+
296
+ /** Local (relative) specifiers a module imports — static, side-effect, and literal dynamic. */
297
+ function localImportsOf(absFile) {
298
+ const raw = fs.readFileSync(absFile, 'utf8');
299
+ // STRIP COMMENTS BEFORE SCANNING. The static-import pattern below uses [^;'"]* to bridge the gap
300
+ // between `import` and `from`, which cannot cross an apostrophe. This file's own house style is
301
+ // dense commentary full of contractions, so a perfectly ordinary multi-line import —
302
+ // import {
303
+ // foo, // don't forget bar
304
+ // bar,
305
+ // } from './mod.mjs';
306
+ // matched NOTHING: no throw, no escape warning, the walker never queued './mod.mjs', and the build
307
+ // happily shipped a bundle missing it. That is strictly worse than the #32 bug it replaced, which
308
+ // at least crashed loudly on startup. Comments carry no imports, so removing them first costs
309
+ // nothing and closes the whole class.
310
+ const src = raw
311
+ .replace(/\/\*[\s\S]*?\*\//g, ' ') // block comments
312
+ .replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // line comments (the [^:] guard spares "https://")
313
+ const specs = new Set();
314
+ for (const m of src.matchAll(/\b(?:import|export)\b[^;'"]*?\bfrom\s*['"]([^'"]+)['"]/g)) specs.add(m[1]);
315
+ for (const m of src.matchAll(/\bimport\s*['"]([^'"]+)['"]/g)) specs.add(m[1]);
316
+ for (const m of src.matchAll(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) specs.add(m[1]);
317
+ // `import(new URL('./x.mjs', import.meta.url).href)` — the computed-URL form. Missed by the plain
318
+ // literal pattern above, and it is not hypothetical: forge-ask-all.mjs and forge-mcp-all.mjs both
319
+ // load brain-alarm.mjs and telemetry-ping.mjs exactly this way. Neither was reaching the bundle,
320
+ // and because both call sites are try/catch-guarded it failed SILENTLY — meaning the brain-down
321
+ // "GONG" alarm, the thing whose entire job is to make a broken brain impossible to miss, has been
322
+ // dead in every installed bundle. Found by adversarial review, not by any test.
323
+ for (const m of src.matchAll(/\bimport\s*\(\s*new\s+URL\s*\(\s*['"]([^'"]+)['"]/g)) specs.add(m[1]);
324
+ return [...specs].filter((s) => s.startsWith('./') || s.startsWith('../'));
325
+ }
326
+
327
+ /** Transitive closure of local modules reachable from the entry points, relative to KB. */
328
+ function resolveModuleGraph() {
329
+ const seen = new Set();
330
+ // `from` distinguishes the two absent-file cases, which are NOT the same failure:
331
+ // from === null -> an entry point this kb/ simply does not contain (a minimal fixture, a
332
+ // partial tree). Nothing to bundle; skip it.
333
+ // from !== null -> a module that something ACTUALLY IMPORTS is missing. That is precisely the
334
+ // MODULE_NOT_FOUND of #32, and it must stop the build.
335
+ const queue = ENTRYPOINTS.map((rel) => ({ rel, from: null }));
336
+ const escapes = [];
337
+ while (queue.length) {
338
+ const { rel, from } = queue.shift();
339
+ if (seen.has(rel)) continue;
340
+ const abs = path.join(KB, rel);
341
+ if (!fs.existsSync(abs)) {
342
+ if (from === null) { console.log(`[build-bundle] note: entry point ${rel} absent from kb/ — skipping`); continue; }
343
+ throw new Error(
344
+ `[build-bundle] ${from} imports ${rel}, which does not exist in kb/ — ` +
345
+ `a bundle built from this tree would crash on startup (issue #32)`,
346
+ );
347
+ }
348
+ seen.add(rel);
349
+ for (const spec of localImportsOf(abs)) {
350
+ const target = path.normalize(path.join(path.dirname(rel), spec));
351
+ if (target.startsWith('..')) { escapes.push(`${rel} -> ${spec}`); continue; }
352
+ queue.push({ rel: target, from: rel });
353
+ }
354
+ }
355
+ if (escapes.length) {
356
+ // A bundled module importing outside kb/ cannot work once installed — the file simply is not there.
357
+ throw new Error(`[build-bundle] import escapes kb/ and would break the installed bundle:\n ${escapes.join('\n ')}`);
358
+ }
359
+ return [...seen].sort();
360
+ }
361
+
362
+ const derivedTools = resolveModuleGraph();
363
+ const tools = [...derivedTools, ...EXTRA_FILES];
364
+ console.log(`[build-bundle] module graph: ${derivedTools.length} modules from ${ENTRYPOINTS.length} entry points`);
365
+ for (const t of tools) cp(t, OUT, { required: true });
366
+ // self-update provenance (where this bundle came from + the canonical manifest URL). Optional: a build
367
+ // without it simply ships a brain whose `forge-update.mjs --check` reports "self-update not configured".
368
+ cp('SOURCE.json', OUT, { required: true });
369
+
370
+ // Rebind copied SOURCE.json to this build. The external RVFs may come from the previous release,
371
+ // but the installed product identity belongs to the source generation that assembled them.
372
+ try {
373
+ const sj = path.join(OUT, 'SOURCE.json');
374
+ const doc = JSON.parse(fs.readFileSync(sj, 'utf8'));
375
+ doc.brainVersion = getVersion();
376
+ doc.releaseTag = getVersionTag();
377
+ fs.writeFileSync(sj, JSON.stringify(doc, null, 2));
378
+ } catch (e) {
379
+ console.error(`[build-bundle] FATAL: could not stamp SOURCE.json identity (${e.message}).`);
380
+ process.exit(1);
381
+ }
382
+
383
+ // SIGNED AUTO-APPLY trust root (SEC-0010 #6, 2026-07-17). The self-updater must VERIFY a downloaded
384
+ // bundle's Ed25519 signature before extracting executable code over the user's install. For that it
385
+ // needs the verifier AND the public key ON DISK — placed here by a trusted install, so the key the
386
+ // updater trusts is the one from the LAST good bundle, never one riding inside the (possibly tampered)
387
+ // download. verify-bundle.mjs lives in scripts/ and the key in keys/ — both outside kb/, so copy by
388
+ // absolute path. Required: shipping a bundle that can't verify its own successor re-opens the exact
389
+ // hole this closes.
390
+ cp(path.join(ROOT, 'scripts', 'verify-bundle.mjs'), OUT, { required: true });
391
+ fs.mkdirSync(path.join(OUT, 'keys'), { recursive: true });
392
+ cp(path.join(ROOT, 'keys', 'ruvnet-brain-signing.pub.pem'), path.join(OUT, 'keys'), { required: true });
393
+
394
+ // ---- manifest ----------------------------------------------------------------------------------
395
+ const builtLower = new Set(built.map((b) => b.toLowerCase()));
396
+ const pendingRepos = regFlat.filter((r) => !builtLower.has(r.name.toLowerCase())).map((r) => ({ name: r.name, tier: r.tier }));
397
+ const now = new Date();
398
+ const manifest = {
399
+ brainVersion: stripTag(BRAIN_VERSION), // FIELD = bare literal; BRAIN_VERSION stays the v-prefixed Release tag
400
+ generated: now.toISOString(),
401
+ generatedHuman: now.toUTCString(),
402
+ coverage: { built: builtRepos.length, catalogued: regFlat.length, orgTotalApprox: 248, pending: pendingRepos.length },
403
+ crossRepoTool: { mcp: 'forge-mcp-all.mjs', cli: 'forge-ask-all.mjs', tool: 'search_ruvnet' },
404
+ conceptsStore: hasConcepts ? { store: 'concepts.big.rvf', note: 'L2 synthesis + per-repo primers embedded as prose; unioned by search_ruvnet so code-implemented capabilities are retrievable as high-confidence prose.' } : null,
405
+ builtRepos,
406
+ pendingRepos,
407
+ };
408
+ fs.writeFileSync(path.join(OUT, 'manifest.json'), JSON.stringify(manifest, null, 2));
409
+
410
+ // ---- mcp.snippet.json: cross-repo entry + one per-repo entry -----------------------------------
411
+ const mcpServers = {
412
+ 'ruvnet-brain': {
413
+ command: 'node',
414
+ args: [path.join(OUT, 'forge-mcp-all.mjs')],
415
+ env: { KB_DIR: OUT, KB_MODEL_CACHE: '/ABSOLUTE/PATH/TO/models-cache (optional; speeds first run)' },
416
+ },
417
+ };
418
+ for (const b of builtRepos) {
419
+ mcpServers[`ruvnet-${b.name}`] = {
420
+ command: 'node', args: [path.join(OUT, 'forge-mcp.mjs')],
421
+ env: { KB_DIR: OUT, KB_NAME: b.name },
422
+ };
423
+ }
424
+ fs.writeFileSync(path.join(OUT, 'mcp.snippet.json'), JSON.stringify({
425
+ '//': 'Add the "ruvnet-brain" entry (one tool, searches ALL repos) to your .mcp.json under "mcpServers". The per-repo "ruvnet-<name>" entries are optional (scope to one repo). Replace KB_MODEL_CACHE or remove it. DO NOT use @ruvector/rvf-mcp-server.',
426
+ mcpServers,
427
+ }, null, 2));
428
+
429
+ // ---- README ------------------------------------------------------------------------------------
430
+ const repoLines = builtRepos.map((b) => `- **${b.name}** (${b.tier}) — ${b.chunks ?? '?'} chunks · variants: ${b.variants.join('+')}${b.gradeRealUse != null ? ` · REAL-USE ${b.gradeRealUse}` : ''}${b.hasPrimer ? ' · primer' : ''}`).join('\n');
431
+ fs.writeFileSync(path.join(OUT, 'README.md'), `# RuvNet Brain — ${BRAIN_VERSION}
432
+
433
+ The source-grounded knowledge base for the RuvNet ecosystem. One question → the best answer from
434
+ rUv's REAL source code, across every built repo, with the file path it came from.
435
+
436
+ ## Built repos (${builtRepos.length}/${regFlat.length})
437
+ ${repoLines}
438
+
439
+ ## Install
440
+ \`\`\`
441
+ unzip ruvnet-brain.zip -d kb/
442
+ cd kb && npm i # installs @ruvector/rvf + @xenova/transformers
443
+ \`\`\`
444
+
445
+ ## Ask across ALL of RuvNet (recommended)
446
+ \`\`\`
447
+ node forge-ask-all.mjs --dir . --q "how does RVF store vectors and run HNSW search?"
448
+ \`\`\`
449
+ Or wire the cross-repo MCP tool \`search_ruvnet\` into your assistant — see \`mcp.snippet.json\`
450
+ (add the \`ruvnet-brain\` entry). It searches every repo and returns whole documents labeled by repo.
451
+
452
+ ## Ask one repo
453
+ \`\`\`
454
+ node forge-ask.mjs --dir . --name ruvector --variant big --q "what is the RVF cognitive container?"
455
+ \`\`\`
456
+
457
+ ## Notes
458
+ - Each repo ships one canonical \`big\` store (bge-base-en-v1.5, 768 dimensions) plus one shared
459
+ passages/meta sidecar set. \`RVF-GENERATIONS.json\` binds the exact RVF bytes to this release.
460
+ - ADR results surface shipped-vs-proposed **status** — a "Proposed" ADR is design intent, not shipped.
461
+ - Everything runs locally; no network calls at query time. DO NOT use @ruvector/rvf-mcp-server (stub).
462
+ - See \`manifest.json\` for per-repo chunk counts, variants, grades, and pinned source SHAs.
463
+ `);
464
+
465
+ if (missing.length) {
466
+ const requiredMissing = [...new Set(missing)];
467
+ console.error(`[build-bundle] FATAL: missing required bundle files:\n ${requiredMissing.join('\n ')}`);
468
+ process.exit(1);
469
+ }
470
+
471
+ // ---- exact release artifact -------------------------------------------------------------------
472
+ // The release signer signs dist/ruvnet-brain.zip, not this assembly directory. Rebuilding only the
473
+ // directory can therefore sign stale bytes left by an older run—the source tree is green while
474
+ // users receive an old brain. Build the exact ZIP here, then extract it through the same safe
475
+ // extractor users run and audit the RVF indexes in those extracted bytes before signing is allowed.
476
+ const archiveFiles = [];
477
+ function collectArchiveFiles(dir, prefix = '') {
478
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
479
+ const relative = prefix ? path.join(prefix, entry.name) : entry.name;
480
+ if (entry.isDirectory()) collectArchiveFiles(path.join(dir, entry.name), relative);
481
+ else if (entry.isFile()) archiveFiles.push(relative);
482
+ }
483
+ }
484
+ collectArchiveFiles(OUT);
485
+ archiveFiles.sort();
486
+ const zipped = process.platform === 'win32'
487
+ ? spawnSync('powershell.exe', [
488
+ '-NoProfile',
489
+ '-NonInteractive',
490
+ '-Command',
491
+ `Compress-Archive -Path '${OUT.replaceAll("'", "''")}\\*' -DestinationPath '${ZIP.replaceAll("'", "''")}' -Force`,
492
+ ], { encoding: 'utf8' })
493
+ : spawnSync('zip', ['-q', '-X', ZIP, ...archiveFiles], {
494
+ cwd: OUT,
495
+ encoding: 'utf8',
496
+ env: { ...process.env, COPYFILE_DISABLE: '1' },
497
+ });
498
+ if (zipped.status !== 0 || !fs.existsSync(ZIP)) {
499
+ console.error(`[build-bundle] FATAL: could not create exact release ZIP (${zipped.stderr || zipped.error?.message || `exit ${zipped.status}`})`);
500
+ process.exit(1);
501
+ }
502
+
503
+ const extracted = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-release-audit-'));
504
+ try {
505
+ const { extractZip } = await import('../kb/zip-extract.mjs');
506
+ await extractZip(ZIP, extracted);
507
+ const packagedRvfs = fs.readdirSync(extracted)
508
+ .filter((name) => name.endsWith('.rvf'))
509
+ .sort()
510
+ .map((name) => path.join(extracted, name));
511
+ const packagedAudit = await auditRvfIndexes(packagedRvfs);
512
+ const packagedFailures = packagedAudit.filter(({ state }) => state !== 'PASS');
513
+ if (packagedFailures.length) {
514
+ console.error('[build-bundle] FATAL: extracted release artifact contains RVFs without persisted HNSW indexes:');
515
+ for (const result of packagedFailures) {
516
+ console.error(` ${path.basename(result.path)} vectors=${result.totalVectors ?? '?'}`);
517
+ }
518
+ throw new Error('extracted release artifact failed the persisted HNSW index audit');
519
+ }
520
+ console.log(`[build-bundle] exact archive proof: ${packagedRvfs.length} RVFs audited after extraction; 0 index failures`);
521
+ } finally {
522
+ fs.rmSync(extracted, { recursive: true, force: true });
523
+ }
524
+
525
+ // ---- report ------------------------------------------------------------------------------------
526
+ console.log(`\n=== build-bundle → ${path.relative(ROOT, OUT)} (${BRAIN_VERSION}) ===`);
527
+ console.log(`built repos: ${builtRepos.length}/${regFlat.length} | files copied: ${copied}`);
528
+ for (const b of builtRepos) console.log(` ${b.tier} ${b.name.padEnd(10)} chunks=${String(b.chunks).padStart(6)} variants=${b.variants.join('+').padEnd(10)} symbols=${b.hasSymbols ? 'y' : '-'} primer=${b.hasPrimer ? 'y' : '-'} grade=${b.gradeRealUse ?? '-'} sha=${(b.builtFromSha || '').slice(0, 10)}`);
529
+ if (missing.length) console.log(`\n⚠ MISSING required files: ${[...new Set(missing)].join(', ')}`);
530
+ console.log(`\nmanifest: ${path.join(path.relative(ROOT, OUT), 'manifest.json')} | mcp snippet + README written.`);
531
+ console.log(missing.length ? 'STATUS: assembled WITH MISSING FILES (see above).' : 'STATUS: assembled OK.');
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ // build-concepts.mjs — assemble the CONCEPTS store: all accepted L2 articles + every per-repo primer,
3
+ // chunked into kb/concepts.passages.jsonl (+ concepts.meta.json) so `forge-big both --name concepts`
4
+ // can embed it into concepts.big.rvf. The cross-repo tool (forge-ask-all / search_ruvnet) then unions
5
+ // this prose layer with the deep-source repos at query time — making code-implemented capabilities
6
+ // retrievable as high-confidence PROSE without re-embedding the 40K-chunk source stores.
7
+ //
8
+ // Each passage path is "<repo>/<kind>/<slug>#<chunk>" so a consumer (and the capability gate) can
9
+ // attribute a concept hit back to its source repo.
10
+ //
11
+ // node scripts/build-concepts.mjs → writes kb/concepts.passages.jsonl + kb/concepts.meta.json
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { loadPrivateFence, isPrivate as isPrivateIn, loadPrivateSlugs, shouldFenceL2 } from './private-fence.mjs';
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+ const KB = path.join(ROOT, 'kb');
19
+
20
+ // PRIVATE fence (SEC-0010 #5) — the concepts store folds in primers + L2 PROSE, so it must honor the
21
+ // same fence as the raw .rvf stores, or a private repo's write-up could ship even though its vectors
22
+ // are excluded. FAIL-CLOSED, same contract as build-bundle.mjs: a present-but-corrupt fence aborts.
23
+ // The decisions live in scripts/private-fence.mjs (pure, unit-tested); this script owns the exit.
24
+ const fence = loadPrivateFence(KB, { allowNoFence: process.env.ALLOW_NO_PRIVATE_FENCE === '1' });
25
+ if (!fence.ok) {
26
+ console.error(`[build-concepts] FATAL: ${fence.reason}. Refusing to build the shipped concepts store ` +
27
+ `without a verified fence. Set ALLOW_NO_PRIVATE_FENCE=1 only for a no-private fork.`);
28
+ process.exit(1);
29
+ }
30
+ const PRIVATE = fence.privateSet;
31
+ const isPrivate = (repo) => isPrivateIn(PRIVATE, repo);
32
+
33
+ // PRIVATE SLUG set (QE-0011 security#1) — a private repo's L2 article is keyed by SLUG, not repo, and
34
+ // an unknown slug used to default to 'ruvnet' (fail-OPEN → it shipped). Read each private repo's own
35
+ // l2-topics.<repo>.json to learn its slugs, so an L2 article whose slug belongs to a private repo is
36
+ // fenced out even when its repo attribution is unknown. Fail-closed: if a private topics file exists
37
+ // but can't be parsed, abort rather than ship.
38
+ const slugFence = loadPrivateSlugs(KB, PRIVATE);
39
+ if (!slugFence.ok) {
40
+ console.error(`[build-concepts] FATAL: ${slugFence.reason}. Refusing to build.`);
41
+ process.exit(1);
42
+ }
43
+ const PRIVATE_SLUGS = slugFence.slugs;
44
+
45
+ // Auto-discover every repo that has a primer (skip PRIVATE ones — their prose must not ship).
46
+ const REPOS = fs.readdirSync(KB)
47
+ .filter((f) => f.endsWith('-primer.md'))
48
+ .map((f) => f.replace(/-primer\.md$/, ''))
49
+ .filter((r) => !isPrivate(r))
50
+ .sort();
51
+
52
+ // slug -> repo, from the per-repo l2-topics files (+ ruflo defaults that predate them)
53
+ const slugRepo = new Map([['guidance-mechanism', 'ruflo'], ['memory-end-to-end', 'ruflo'], ['adr-coverage', 'ruflo']]);
54
+ for (const r of REPOS) {
55
+ const tf = path.join(KB, `l2-topics.${r}.json`);
56
+ if (fs.existsSync(tf)) for (const t of JSON.parse(fs.readFileSync(tf, 'utf8'))) slugRepo.set(t.slug, r);
57
+ }
58
+
59
+ // ~3200-char paragraph-aligned chunks
60
+ function chunk(text, size = 3200) {
61
+ const out = []; const paras = text.split(/\n\n+/); let buf = '';
62
+ for (const p of paras) {
63
+ if (buf && (buf.length + p.length + 2) > size) { out.push(buf); buf = ''; }
64
+ buf += (buf ? '\n\n' : '') + p;
65
+ }
66
+ if (buf.trim()) out.push(buf);
67
+ return out.length ? out : [text];
68
+ }
69
+
70
+ const passages = []; // {id, text, path, title}
71
+ const entries = {}; // id -> {path, kind, title, chunk, preview}
72
+ let id = 0;
73
+ function add(repo, kind, slug, title, text) {
74
+ const chunks = chunk(text);
75
+ chunks.forEach((c, i) => {
76
+ const sid = String(id++);
77
+ const p = `${repo}/${kind}/${slug}${chunks.length > 1 ? `#${i}` : ''}`;
78
+ passages.push({ id: sid, text: c, path: p, title });
79
+ entries[sid] = { path: p, kind: kind === 'PRIMER' ? 'doc' : 'doc', title, chunk: i, preview: c.slice(0, 200) };
80
+ });
81
+ }
82
+
83
+ // 1) accepted L2 articles (skip kb/l2/rejected/)
84
+ let l2n = 0;
85
+ for (const f of fs.readdirSync(path.join(KB, 'l2'))) {
86
+ if (!f.endsWith('.md')) continue;
87
+ const slug = f.replace(/\.md$/, '');
88
+ const repo = slugRepo.get(slug) || 'ruvnet';
89
+ // Fence a private repo's L2 prose whether it's attributed by repo OR only recognizable by slug
90
+ // (QE-0011 security#1 — closes the fail-open 'ruvnet' default for unknown attribution).
91
+ if (shouldFenceL2({ repo, slug }, PRIVATE, PRIVATE_SLUGS)) continue;
92
+ const text = fs.readFileSync(path.join(KB, 'l2', f), 'utf8');
93
+ const title = (text.match(/^#\s+(.+)/m) || [, slug])[1];
94
+ add(repo, 'L2', slug, title, text); l2n++;
95
+ }
96
+
97
+ // 2) per-repo primers
98
+ let pn = 0;
99
+ for (const r of REPOS) {
100
+ const pf = path.join(KB, `${r}-primer.md`);
101
+ if (!fs.existsSync(pf)) { console.log(` (no primer for ${r})`); continue; }
102
+ add(r, 'PRIMER', `${r}-primer`, `${r} — Primer`, fs.readFileSync(pf, 'utf8')); pn++;
103
+ }
104
+
105
+ // 3) capability cards — short, capability-phrased, one per building block. These are the high-signal
106
+ // passages that let a BY-DESCRIPTION query ("what caches vector queries?") rerank to the right repo
107
+ // even when the user never names it. Each card is attributed to its repo via the path prefix.
108
+ let cn = 0;
109
+ const cardsFile = path.join(KB, 'capability-cards.md');
110
+ if (fs.existsSync(cardsFile)) {
111
+ const raw = fs.readFileSync(cardsFile, 'utf8');
112
+ for (const sec of raw.split(/^##\s+/m).slice(1)) {
113
+ const nl = sec.indexOf('\n');
114
+ if (nl < 0) continue;
115
+ const repo = sec.slice(0, nl).trim();
116
+ const body = sec.slice(nl + 1).trim();
117
+ if (!repo || !body) continue;
118
+ if (isPrivate(repo)) continue; // QE-0011 security#1: fence a private repo's capability card too
119
+ add(repo, 'CARD', `${repo}-card`, `${repo} — Capability`, `${repo} — ${body}`);
120
+ cn++;
121
+ }
122
+ }
123
+
124
+ fs.writeFileSync(path.join(KB, 'concepts.passages.jsonl'), passages.map((p) => JSON.stringify(p)).join('\n') + '\n');
125
+ fs.writeFileSync(path.join(KB, 'concepts.meta.json'), JSON.stringify({
126
+ model: 'concepts', dimensions: 0, metric: 'cosine', name: 'concepts',
127
+ generated: new Date().toISOString(), repo: 'ruvnet-concepts',
128
+ note: 'L2 synthesis + per-repo primers; embedded into concepts.big.rvf via forge-big.',
129
+ entries,
130
+ }, null, 2));
131
+ console.log(`concepts: ${l2n} L2 articles + ${pn} primers + ${cn} capability cards → ${passages.length} passages → kb/concepts.passages.jsonl`);
132
+ console.log('next: node kb/forge-big.mjs both --dir kb --name concepts (builds concepts.big.rvf)');