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