ruvnet-brain 4.3.21 → 4.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/install.mjs +275 -60
- package/console/app.js +189 -9
- package/console/index.html +70 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/style.css +26 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +364 -28
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +47 -10
- package/scripts/release-transaction.mjs +40 -11
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
package/scripts/build-bundle.mjs
CHANGED
|
@@ -1,426 +1,275 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// build-bundle.mjs — assemble the shippable RuvNet Brain bundle from
|
|
2
|
+
// build-bundle.mjs — assemble the shippable RuvNet Brain bundle from ONE finalized corpus directory.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Step 5 of the corpus-seed/release pipeline consolidation (2026-09-13). Before this, assembly was
|
|
5
|
+
// two independent, ad hoc passes glued together by an external CLI round-trip: build-bundle.mjs
|
|
6
|
+
// discovered stores from disk, copied SOURCE.json from the maintainer's own checkout (a SECOND
|
|
7
|
+
// production point for the exact bug Step 3 already fixed for public inputs), stamped `builtFromSha`
|
|
8
|
+
// from a stale externally-tracked `data/manifest.json`, special-cased "concepts"/"ruv-gists" with
|
|
9
|
+
// near-duplicate `if` blocks, and — for a real release — had to be invoked TWICE around a separate
|
|
10
|
+
// `release-projection.mjs` subprocess just so the second invocation could see a coverage-scoped
|
|
11
|
+
// selection the first invocation's own output made possible. `assembleBundle` replaces all of that
|
|
12
|
+
// with ONE pass: selection is DISCOVERED directly from the finalized corpus directory, which Step 4's
|
|
13
|
+
// real pruning guarantees already equals the exact eligible set — no external coverage-filtering
|
|
14
|
+
// round-trip is needed to compute it, only (optionally) a fail-loud cross-check against the sealed
|
|
15
|
+
// corpus coverage, never a repair. SOURCE.json, the shipped generation ledger, and the manifest's
|
|
16
|
+
// per-repo rows are now generated ONCE, together, by `projectStoreViews`, from explicitly selected
|
|
17
|
+
// records — never re-derived independently or copied from the checkout. The release coverage
|
|
18
|
+
// projection (COVERAGE.json / CORPUS-COVERAGE.json / PUBLIC-RVF-GENERATIONS.json) is produced
|
|
19
|
+
// in-process, from the exact same selected records, and written alongside everything else before the
|
|
20
|
+
// ONE zip is created.
|
|
9
21
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
22
|
+
// node scripts/build-bundle.mjs [--assets /path/to/finalized-corpus] [--out dist/ruvnet-brain]
|
|
23
|
+
// [--version v0.2.0-dev] [--source-snapshot <40-hex>]
|
|
24
|
+
// [--seed-tag <tag> --seed-sha256 <64-hex> --seed-bytes <n>
|
|
25
|
+
// --baseline-receipt-sha256 <64-hex>]
|
|
12
26
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
27
|
+
// `--seed-tag`/`--seed-sha256`/`--seed-bytes`/`--baseline-receipt-sha256` are optional AS A SET. When
|
|
28
|
+
// all four are supplied, this run additionally produces and validates the release coverage
|
|
29
|
+
// projection (the real-release shape a step-11 consumer switch will call). When NONE is supplied,
|
|
30
|
+
// assembleBundle produces a complete, correctly-selected candidate archive with no release-
|
|
31
|
+
// projection files — the shape scripts/corpus-reconcile.mjs's prepareCorpusCandidate and
|
|
32
|
+
// scripts/self-update.mjs both need. A PARTIAL set is rejected loudly: a build is either a release
|
|
33
|
+
// build or it is not, never a silently downgraded one.
|
|
34
|
+
//
|
|
35
|
+
// assembleBundle is OFFLINE. It never calls GitHub: the org repo total in manifest.json comes from
|
|
36
|
+
// the committed record (data/org-repo-count.json, `source: 'recorded'`) or is honestly `unknown`.
|
|
37
|
+
// Assembling sealed bytes must not depend on network availability, and must not vary with it.
|
|
38
|
+
//
|
|
39
|
+
// CONSUMER STATUS (P1-c, 2026-09-14): the previous note here said ci.yml's release-qe job ran a
|
|
40
|
+
// sequence that "cannot pass this file's sealed-input checks, by design" — it invoked a
|
|
41
|
+
// release-projection.mjs CLI that could only `exit 1`, then passed --coverage/--projection flags this
|
|
42
|
+
// CLI rejected outright. Documenting an expected failure is not a release gate. The consumer and the
|
|
43
|
+
// scripts now AGREE: ci.yml declares the legacy rail with --legacy-seed-projection, and this file
|
|
44
|
+
// honours it as ONE explicitly-named, temporary mode (assembleBundle's `legacySeedProjection`) that
|
|
45
|
+
// runs the pre-consolidation two-pass semantics against the pinned pre-Step-3/4 seed — with the
|
|
46
|
+
// public-prose selection materialized fresh and FENCED (never the pre-Step-3 wholesale checkout copy)
|
|
47
|
+
// and WITHOUT the packaging-time receipt fabrication Step 3 deleted. Measured against the live
|
|
48
|
+
// v4.2.1-dev seed on 2026-09-14: 186 stores, no PUBLIC-INPUT-SELECTION.json, no
|
|
49
|
+
// public-store-classes.json, no ruv-gists.sources.json. Its unprovable aggregate stores are scoped
|
|
50
|
+
// out rather than shipped unproven. THE WHOLE MODE RETIRES AT PLAN STEP 11, when a Step-4-produced
|
|
51
|
+
// seed is published and data/corpus-seed.json is re-pinned to it.
|
|
15
52
|
import crypto from 'node:crypto';
|
|
16
53
|
import fs from 'node:fs';
|
|
17
54
|
import os from 'node:os';
|
|
18
55
|
import path from 'node:path';
|
|
19
56
|
import { spawnSync } from 'node:child_process';
|
|
20
57
|
import { fileURLToPath } from 'node:url';
|
|
21
|
-
import {
|
|
58
|
+
import { getVersionTag, stripTag } from './version.mjs';
|
|
22
59
|
import { auditRvfIndexes } from './rvf-index-audit.mjs';
|
|
23
60
|
import { readRvfGenerations, validateSelectedRvfGenerations } from './rvf-generation.mjs';
|
|
24
61
|
import { validatePublicInventory } from './public-inventory.mjs';
|
|
25
|
-
import { bindAssembledReleaseProjection } from './release-projection.mjs';
|
|
62
|
+
import { bindAssembledReleaseProjection, createReleaseProjection } from './release-projection.mjs';
|
|
63
|
+
import { materializePublicInputs, SELECTION_FILE, validateSelectionReceipt } from './public-inputs.mjs';
|
|
64
|
+
import { isPrivate, loadPrivateSlugs, shouldFenceL2 } from './private-fence.mjs';
|
|
65
|
+
import { validateCoverageLedger } from './coverage-integrity.mjs';
|
|
26
66
|
// The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
|
|
27
67
|
// sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
|
|
28
68
|
import { orgRepoCount } from './org-repo-count.mjs';
|
|
29
69
|
|
|
30
70
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
|
|
72
|
+
function fail(message) {
|
|
73
|
+
throw new Error(message);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** A directory must not be, contain, or be contained by any of `forbidden` (algorithm step 2: a
|
|
77
|
+
* fresh output directory disjoint from all inputs). Deliberately self-contained (no import of
|
|
78
|
+
* corpus-reconcile.mjs's equivalent) so this file's own dependency graph — walked by
|
|
79
|
+
* tests/integration/build-bundle-fence.test.mjs's isolated subprocess fixture — never grows. */
|
|
80
|
+
function assertDisjoint(label, targetDir, forbidden) {
|
|
81
|
+
const resolved = path.resolve(targetDir || '');
|
|
82
|
+
const overlaps = (a, b) => {
|
|
83
|
+
const relative = path.relative(a, b);
|
|
84
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
85
|
+
};
|
|
86
|
+
for (const entry of forbidden) {
|
|
87
|
+
const forbiddenDir = path.resolve(entry.dir);
|
|
88
|
+
if (overlaps(forbiddenDir, resolved) || overlaps(resolved, forbiddenDir)) {
|
|
89
|
+
fail(`${label} must not be, or contain, or be contained by, ${entry.label} (${resolved})`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Best-effort only: NOT every caller runs inside a real git checkout (an isolated subprocess-test
|
|
95
|
+
// fixture, deliberately, does not — see tests/integration/build-bundle-fence.test.mjs's own header on
|
|
96
|
+
// why it copies only a scoped dependency tree into a plain tmpdir). A source snapshot is genuinely
|
|
97
|
+
// required only when a release coverage projection is being produced (assembleBundle's `seedIdentity`
|
|
98
|
+
// branch, and createReleaseProjection's own validation) — everywhere else it is optional enrichment,
|
|
99
|
+
// so failure here must never abort assembly.
|
|
100
|
+
function tryGitSha(root) {
|
|
101
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
102
|
+
const sha = String(result.stdout || '').trim();
|
|
103
|
+
return result.status === 0 && /^[a-f0-9]{40}$/.test(sha) ? sha : null;
|
|
104
|
+
}
|
|
56
105
|
|
|
57
106
|
// ---- PRIVATE stores (excluded from any publishable bundle) --------------------------------------
|
|
58
|
-
// kb/PRIVATE-STORES.json
|
|
59
|
-
//
|
|
107
|
+
// kb/PRIVATE-STORES.json (read from the exact code checkout, NEVER from the finalized corpus
|
|
108
|
+
// directory — corpus-reconcile.mjs's own SEED-PRIVATE-STORES.json comment states the reason: a
|
|
109
|
+
// seed's own fence is authenticated HISTORICAL evidence, never the current builder's live policy)
|
|
110
|
+
// lists store names built from PRIVATE source that MUST NOT ship. We read it here and drop those
|
|
111
|
+
// names during discovery so private code can never leak into dist/.
|
|
60
112
|
// FAIL-CLOSED (security-critical — SEC-0010 #4). A private-store fence that degrades to an EMPTY
|
|
61
113
|
// set on any error means a truncated/corrupt/missing PRIVATE-STORES.json silently ships EVERY store,
|
|
62
114
|
// including private cognitum source. So: a present-but-unparseable fence ALWAYS aborts the build; a
|
|
63
115
|
// missing fence aborts too, unless the operator explicitly opts out (ALLOW_NO_PRIVATE_FENCE=1) — the
|
|
64
116
|
// escape hatch a genuine no-private public fork needs, but never the silent default.
|
|
65
|
-
function loadPrivateStores() {
|
|
66
|
-
const p = path.join(
|
|
117
|
+
function loadPrivateStores(runtimeKbDir) {
|
|
118
|
+
const p = path.join(runtimeKbDir, 'PRIVATE-STORES.json');
|
|
67
119
|
if (!fs.existsSync(p)) {
|
|
68
120
|
if (process.env.ALLOW_NO_PRIVATE_FENCE === '1') {
|
|
69
121
|
console.warn('[build-bundle] no PRIVATE-STORES.json; ALLOW_NO_PRIVATE_FENCE=1 → proceeding with NO fence.');
|
|
70
122
|
return new Set();
|
|
71
123
|
}
|
|
72
|
-
|
|
124
|
+
fail(`private-store fence missing (${p}). Refusing to build — a bundle ` +
|
|
73
125
|
`without a verified fence could ship private source. Set ALLOW_NO_PRIVATE_FENCE=1 only for a genuine ` +
|
|
74
126
|
`no-private fork.`);
|
|
75
|
-
process.exit(1);
|
|
76
127
|
}
|
|
77
128
|
let j;
|
|
78
129
|
try {
|
|
79
130
|
j = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
80
131
|
} catch (e) {
|
|
81
|
-
|
|
132
|
+
fail(`private-store fence is present but unreadable/corrupt (${e.message}). ` +
|
|
82
133
|
`Refusing to build — cannot prove private stores are fenced out.`);
|
|
83
|
-
process.exit(1);
|
|
84
134
|
}
|
|
85
135
|
if (!Array.isArray(j.privateStores)) {
|
|
86
|
-
|
|
87
|
-
process.exit(1);
|
|
136
|
+
fail('private-store fence has no valid "privateStores" array. Refusing to build.');
|
|
88
137
|
}
|
|
89
138
|
return new Set(j.privateStores.map((s) => String(s).toLowerCase()));
|
|
90
139
|
}
|
|
91
|
-
const PRIVATE_STORES = loadPrivateStores();
|
|
92
140
|
|
|
93
|
-
|
|
94
|
-
|
|
141
|
+
/**
|
|
142
|
+
* assertSelectionMatchesCurrentFence — P1-b (Dual's step-5 review, 2026-09-14).
|
|
143
|
+
*
|
|
144
|
+
* A public-input selection receipt proves what its PRODUCER decided, against the fence that existed
|
|
145
|
+
* when it ran. This build ships a DIFFERENT artifact: `runtimeRoot/kb/PRIVATE-STORES.json`, the
|
|
146
|
+
* CURRENT fence. When the two disagree, the archive ships prose for a store its own shipped fence
|
|
147
|
+
* declares private. The four surfaces the producer filters (public-inputs.mjs:287-341) are checked
|
|
148
|
+
* here, ALL FOUR — primers, per-repo topics files, L2 slugs, and capability-card sections. The card
|
|
149
|
+
* sections were the surface a narrower check would have missed: a store with no selected RVF keeps
|
|
150
|
+
* its card (and its primer and topics) because the store-inventory checks never look at prose.
|
|
151
|
+
*
|
|
152
|
+
* Fails LOUD — never silently filters. Derived concepts already embed this text
|
|
153
|
+
* (scripts/corpus-aggregates.mjs:59-65 reads the same prose into concepts.passages.jsonl), so a
|
|
154
|
+
* file-drop here would ship the private text anyway, inside the aggregate. Rejection forces the
|
|
155
|
+
* corpus back to preparation, where the aggregates are rebuilt under the current fence.
|
|
156
|
+
*/
|
|
157
|
+
function assertSelectionMatchesCurrentFence({ selectionReceipt, privateStores, kbDir, corpusLabel }) {
|
|
158
|
+
const slugFence = loadPrivateSlugs(kbDir, privateStores);
|
|
159
|
+
if (!slugFence.ok) fail(`current private fence cannot be read (${slugFence.reason})`);
|
|
160
|
+
const included = selectionReceipt.included || {};
|
|
161
|
+
const ownership = selectionReceipt.ownership || {};
|
|
162
|
+
const violations = [];
|
|
163
|
+
for (const repo of included.primers || []) {
|
|
164
|
+
if (isPrivate(privateStores, repo)) violations.push(`primer ${repo}-primer.md (repo "${repo}" is private now)`);
|
|
165
|
+
}
|
|
166
|
+
for (const repo of included.topics || []) {
|
|
167
|
+
if (isPrivate(privateStores, repo)) violations.push(`topics l2-topics.${repo}.json (repo "${repo}" is private now)`);
|
|
168
|
+
}
|
|
169
|
+
for (const slug of included.l2 || []) {
|
|
170
|
+
const repo = ownership[slug] || 'ruvnet';
|
|
171
|
+
if (shouldFenceL2({ repo, slug }, privateStores, slugFence.slugs)) {
|
|
172
|
+
violations.push(`l2/${slug}.md (owned by "${repo}", fenced now)`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const repo of included.cards || []) {
|
|
176
|
+
if (isPrivate(privateStores, repo)) violations.push(`capability-cards.md section "## ${repo}" (repo "${repo}" is private now)`);
|
|
177
|
+
}
|
|
178
|
+
if (!violations.length) return;
|
|
179
|
+
fail(`the sealed public-input selection is incompatible with the CURRENT private fence ` +
|
|
180
|
+
`(${path.join(kbDir, 'PRIVATE-STORES.json')}) — it was sealed before ${violations.length} item(s) were fenced, ` +
|
|
181
|
+
`and this build ships that fence alongside them:\n${violations.map((v) => ` ${v}`).join('\n')}\n` +
|
|
182
|
+
`Selection: ${corpusLabel}. Rejected, not filtered: derived concepts already embed this prose, so the ` +
|
|
183
|
+
`corpus returns to preparation and its aggregates are rebuilt under the current fence.`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- discover BUILT stores (those with <name>.big.rvf in the finalized corpus) ------------------
|
|
187
|
+
function discoverBuilt(corpusDir, privateStores) {
|
|
95
188
|
const names = new Set();
|
|
96
189
|
const excludedPrivate = [];
|
|
97
|
-
if (!fs.existsSync(
|
|
98
|
-
for (const f of fs.readdirSync(
|
|
190
|
+
if (!fs.existsSync(corpusDir)) return { built: [], excludedPrivate: [] };
|
|
191
|
+
for (const f of fs.readdirSync(corpusDir)) {
|
|
99
192
|
if (f.startsWith('._')) continue; // AppleDouble metadata is never a vector store.
|
|
100
193
|
const m = f.match(/^(.+?)\.big\.rvf$/);
|
|
101
194
|
if (!m) continue;
|
|
102
195
|
if (/\.(idmap|embed)\b/.test(f)) continue;
|
|
103
|
-
if (
|
|
196
|
+
if (privateStores.has(m[1].toLowerCase())) { excludedPrivate.push(m[1]); continue; }
|
|
104
197
|
names.add(m[1]);
|
|
105
198
|
}
|
|
106
|
-
|
|
107
|
-
return [...names].sort();
|
|
199
|
+
return { built: [...names].sort(), excludedPrivate };
|
|
108
200
|
}
|
|
109
201
|
|
|
110
|
-
// ---- best-effort grade lookup (REAL-USE avg)
|
|
111
|
-
|
|
202
|
+
// ---- best-effort grade lookup (REAL-USE avg), bound to the SELECTED generation ------------------
|
|
203
|
+
// Algorithm step 8: omit historical grades lacking matching source/build evidence. A grade file only
|
|
204
|
+
// counts as evidence for the store's CURRENTLY selected generation if it names the exact sourceCommit
|
|
205
|
+
// or RVF digest it was measured against. Today's grading harness (scripts/build-l2.mjs,
|
|
206
|
+
// scripts/brain-grade-groundtruth.mjs — both human-run per CONTRIBUTING.md §5) does not yet stamp
|
|
207
|
+
// that binding into its output JSON, so no grade file can currently satisfy this and gradeRealUse is
|
|
208
|
+
// correctly omitted everywhere until the grading harness is updated to record it — a genuine, honest
|
|
209
|
+
// consequence of this rule, not a bug: a REAL-USE score with no way to prove which build it measured
|
|
210
|
+
// is exactly the kind of unproven historical claim this consolidation removes.
|
|
211
|
+
function gradeFor(dataDir, name, generation) {
|
|
112
212
|
for (const f of [`grade-${name}-big.json`, `grade-${name}.json`, `grade-${name}-small.json`]) {
|
|
113
|
-
const p = path.join(
|
|
213
|
+
const p = path.join(dataDir, f);
|
|
114
214
|
if (!fs.existsSync(p)) continue;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
215
|
+
let g;
|
|
216
|
+
try { g = JSON.parse(fs.readFileSync(p, 'utf8')); } catch { continue; }
|
|
217
|
+
const evidenceSha = g.summary?.sourceCommit || g.sourceCommit || null;
|
|
218
|
+
const evidenceRvfSha256 = g.summary?.rvfSha256 || g.rvfSha256 || null;
|
|
219
|
+
const boundBySha = evidenceSha && generation?.sourceCommit
|
|
220
|
+
&& String(evidenceSha).toLowerCase() === String(generation.sourceCommit).toLowerCase();
|
|
221
|
+
const boundByRvf = evidenceRvfSha256 && generation?.sha256
|
|
222
|
+
&& String(evidenceRvfSha256).toLowerCase() === String(generation.sha256).toLowerCase();
|
|
223
|
+
if (!boundBySha && !boundByRvf) continue;
|
|
224
|
+
if (typeof g.summary?.avgRealUse === 'number') return g.summary.avgRealUse;
|
|
225
|
+
const qs = Array.isArray(g.questions) ? g.questions : Array.isArray(g) ? g : null;
|
|
226
|
+
if (qs && qs.length) {
|
|
227
|
+
const vals = qs.map((q) => q.realUse).filter((v) => typeof v === 'number');
|
|
228
|
+
if (vals.length) return +(vals.reduce((a, b) => a + b, 0) / vals.length).toFixed(1);
|
|
229
|
+
}
|
|
125
230
|
}
|
|
126
231
|
return null;
|
|
127
232
|
}
|
|
128
233
|
|
|
129
234
|
// ---- file copy helper ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (const p of PRIVATE_STORES) {
|
|
143
|
-
const tf = path.join(KB, `l2-topics.${p}.json`);
|
|
144
|
-
if (!fs.existsSync(tf)) continue;
|
|
145
|
-
try { for (const t of JSON.parse(fs.readFileSync(tf, 'utf8'))) if (t.slug) PRIVATE_L2_SLUGS.add(`${t.slug}.md`); }
|
|
146
|
-
catch (e) { console.error(`[build-bundle] FATAL: private topics ${tf} corrupt (${e.message}). Refusing to build.`); process.exit(1); }
|
|
147
|
-
}
|
|
148
|
-
// skipNames: an optional Set of filenames to exclude (used to fence private L2 .md out of the l2/ copy).
|
|
149
|
-
function cpDir(srcDir, destDir, skipNames) {
|
|
150
|
-
if (!fs.existsSync(srcDir)) return false;
|
|
151
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
152
|
-
for (const e of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
153
|
-
if (skipNames && skipNames.has(e.name)) continue;
|
|
154
|
-
if (e.isDirectory()) cpDir(path.join(srcDir, e.name), path.join(destDir, e.name), skipNames);
|
|
155
|
-
else { fs.copyFileSync(path.join(srcDir, e.name), path.join(destDir, e.name)); copied++; }
|
|
156
|
-
}
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// ---- assemble ----------------------------------------------------------------------------------
|
|
161
|
-
const ZIP = path.join(path.dirname(OUT), `${path.basename(OUT)}.zip`);
|
|
162
|
-
fs.rmSync(OUT, { recursive: true, force: true });
|
|
163
|
-
// A failed rebuild must not leave an older archive looking like this invocation's output.
|
|
164
|
-
fs.rmSync(ZIP, { force: true });
|
|
165
|
-
fs.mkdirSync(OUT, { recursive: true });
|
|
166
|
-
// The shipped bundle must carry the exact policy boundary used during assembly. The public
|
|
167
|
-
// inventory validator consumes this file from the candidate output, so reading it only from the
|
|
168
|
-
// source checkout would leave the release projection unable to prove its private-store exclusion.
|
|
169
|
-
cp(path.join(KB, 'PRIVATE-STORES.json'), OUT, { required: true });
|
|
170
|
-
|
|
171
|
-
let built = discoverBuilt();
|
|
172
|
-
let excludedPublicStores = [];
|
|
173
|
-
if (COVERAGE) {
|
|
174
|
-
const coverageFile = path.resolve(COVERAGE);
|
|
175
|
-
if (!fs.existsSync(coverageFile)) {
|
|
176
|
-
console.error(`[build-bundle] FATAL: coverage policy missing (${coverageFile})`);
|
|
177
|
-
process.exit(1);
|
|
178
|
-
}
|
|
179
|
-
let coverage;
|
|
180
|
-
try { coverage = JSON.parse(fs.readFileSync(coverageFile, 'utf8')); }
|
|
181
|
-
catch (error) {
|
|
182
|
-
console.error(`[build-bundle] FATAL: coverage policy unreadable (${error.message})`);
|
|
183
|
-
process.exit(1);
|
|
184
|
-
}
|
|
185
|
-
let inventory;
|
|
186
|
-
try {
|
|
187
|
-
if (PROJECTION) {
|
|
188
|
-
const projectionDir = path.resolve(PROJECTION);
|
|
189
|
-
const receiptFile = path.join(projectionDir, 'ruv-gists.sources.json');
|
|
190
|
-
if (fs.existsSync(receiptFile)) fs.copyFileSync(receiptFile, path.join(ASSETS, 'ruv-gists.sources.json'));
|
|
191
|
-
const classesFile = path.join(projectionDir, 'public-store-classes.json');
|
|
192
|
-
if (fs.existsSync(classesFile)) fs.copyFileSync(classesFile, path.join(ASSETS, 'public-store-classes.json'));
|
|
193
|
-
const conceptsReceipt = path.join(projectionDir, 'concepts.sources.json');
|
|
194
|
-
if (fs.existsSync(conceptsReceipt)) fs.copyFileSync(conceptsReceipt, path.join(ASSETS, 'concepts.sources.json'));
|
|
195
|
-
// The projection is derived from the public seed and therefore does not
|
|
196
|
-
// contain the repository's release-policy fence. The inventory validator
|
|
197
|
-
// still needs that fence when validating the projected input.
|
|
198
|
-
const privateStores = path.join(KB, 'PRIVATE-STORES.json');
|
|
199
|
-
if (fs.existsSync(privateStores) && !fs.existsSync(path.join(ASSETS, 'PRIVATE-STORES.json'))) {
|
|
200
|
-
fs.copyFileSync(privateStores, path.join(ASSETS, 'PRIVATE-STORES.json'));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
inventory = validatePublicInventory({ assetsDir: ASSETS, coverage, ledger: readRvfGenerations(ASSETS) });
|
|
204
|
-
} catch (error) {
|
|
205
|
-
console.error(`[build-bundle] FATAL: public inventory is not exhaustive (${error.message})`);
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
const selected = new Set(inventory.publicStores);
|
|
209
|
-
built = built.filter((store) => selected.has(store.toLowerCase()));
|
|
210
|
-
excludedPublicStores = inventory.excludedRepositories;
|
|
211
|
-
const missing = inventory.publicStores.filter((store) => !built.some((candidate) => candidate.toLowerCase() === store));
|
|
212
|
-
if (missing.length) {
|
|
213
|
-
console.error(`[build-bundle] FATAL: classified public store(s) missing: ${missing.join(', ')}`);
|
|
214
|
-
process.exit(1);
|
|
215
|
-
}
|
|
216
|
-
if (excludedPublicStores.length) {
|
|
217
|
-
console.log(`[build-bundle] EXCLUDED ${excludedPublicStores.length} policy-ineligible public store(s): ${excludedPublicStores.join(', ')}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
if (built.length === 0) {
|
|
221
|
-
console.error('[build-bundle] FATAL: zero public RVF stores are eligible for release. Refusing to publish an empty brain bundle.');
|
|
222
|
-
process.exit(1);
|
|
223
|
-
}
|
|
224
|
-
const generationValidation = validateSelectedRvfGenerations(ASSETS, {
|
|
225
|
-
selectedStores: built,
|
|
226
|
-
privateStores: [...PRIVATE_STORES],
|
|
227
|
-
excludedStores: excludedPublicStores,
|
|
228
|
-
});
|
|
229
|
-
if (generationValidation.failures.length) {
|
|
230
|
-
console.error('[build-bundle] FATAL: RVF generation ledger does not exactly bind selected roots:');
|
|
231
|
-
for (const failure of generationValidation.failures) console.error(` ${failure}`);
|
|
232
|
-
process.exit(1);
|
|
233
|
-
}
|
|
234
|
-
const rvfIndexAudit = await auditRvfIndexes(
|
|
235
|
-
built.map((name) => path.join(ASSETS, `${name}.big.rvf`)),
|
|
236
|
-
);
|
|
237
|
-
const missingIndexes = rvfIndexAudit.filter(({ state }) => state !== 'PASS');
|
|
238
|
-
if (missingIndexes.length) {
|
|
239
|
-
console.error('[build-bundle] FATAL: eligible RVFs lack persisted HNSW indexes:');
|
|
240
|
-
for (const result of missingIndexes) {
|
|
241
|
-
console.error(` ${path.basename(result.path)} vectors=${result.totalVectors}`);
|
|
242
|
-
}
|
|
243
|
-
console.error('[build-bundle] Repair with: node scripts/rvf-index-audit.mjs --repair');
|
|
244
|
-
process.exit(1);
|
|
245
|
-
}
|
|
246
|
-
const builtRepos = [];
|
|
247
|
-
for (const name of built) {
|
|
248
|
-
// One canonical computer-focused vector store per repository. The unsuffixed passages/meta
|
|
249
|
-
// sidecars are shared with the .big RVF so source text and metadata are packaged once.
|
|
250
|
-
cp(`${name}.big.rvf`, OUT, { required: true, asset: true });
|
|
251
|
-
cp(`${name}.big.rvf.idmap.json`, OUT, { required: true, asset: true });
|
|
252
|
-
cp(`${name}.big.rvf.embed.json`, OUT, { required: true, asset: true });
|
|
253
|
-
cp(`${name}.passages.jsonl`, OUT, { required: true, asset: true });
|
|
254
|
-
cp(`${name}.meta.json`, OUT, { required: true, asset: true });
|
|
255
|
-
const hasSymbols = cp(`${name}.symbols.json`, OUT, { asset: true });
|
|
256
|
-
const hasPrimer = cp(`${name}-primer.md`, OUT);
|
|
257
|
-
// metadata
|
|
258
|
-
let chunks = null, model = null, dims = null;
|
|
259
|
-
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 { /* */ }
|
|
260
|
-
const reg = regByLower.get(name.toLowerCase()) || {};
|
|
261
|
-
const grade = gradeFor(name);
|
|
262
|
-
builtRepos.push({
|
|
263
|
-
name, tier: reg.tier || '?', stars: reg.stars ?? null,
|
|
264
|
-
chunks, baseModel: model, baseDims: dims,
|
|
265
|
-
variants: ['big'],
|
|
266
|
-
hasSymbols, hasPrimer, hasBig: true,
|
|
267
|
-
gradeRealUse: grade ? grade.realUse : null,
|
|
268
|
-
builtFromSha: priorSha[name.toLowerCase()] || 'unknown',
|
|
269
|
-
status: 'built',
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
// Publish only generation records for stores that passed the private-store fence above. Copying
|
|
273
|
-
// the machine-wide ledger wholesale would leak private store names/hashes even though their RVFs
|
|
274
|
-
// were correctly excluded.
|
|
275
|
-
{
|
|
276
|
-
const generationsFile = path.join(ASSETS, 'RVF-GENERATIONS.json');
|
|
277
|
-
if (!fs.existsSync(generationsFile)) {
|
|
278
|
-
missing.push('RVF-GENERATIONS.json');
|
|
279
|
-
} else {
|
|
280
|
-
const source = generationValidation.manifest;
|
|
281
|
-
const stores = {};
|
|
282
|
-
for (const { name } of builtRepos) {
|
|
283
|
-
if (!source.stores?.[name]) missing.push(`RVF-GENERATIONS.json:${name}`);
|
|
284
|
-
else stores[name] = source.stores[name];
|
|
285
|
-
}
|
|
286
|
-
fs.writeFileSync(path.join(OUT, 'RVF-GENERATIONS.json'), `${JSON.stringify({
|
|
287
|
-
schemaVersion: source.schemaVersion,
|
|
288
|
-
brainVersion: stripTag(BRAIN_VERSION),
|
|
289
|
-
releaseTag: BRAIN_VERSION,
|
|
290
|
-
stores,
|
|
291
|
-
}, null, 2)}\n`);
|
|
235
|
+
function makeCopier() {
|
|
236
|
+
let copied = 0;
|
|
237
|
+
const missing = [];
|
|
238
|
+
/** cp(relativeName, destDir, { required, from }) — `from` is the exact source directory; every
|
|
239
|
+
* call site names it explicitly (never an implicit ASSETS-vs-KB default) so "which root did this
|
|
240
|
+
* file come from" is always visible at the call site, not inferred from a flag. */
|
|
241
|
+
function cp(name, destDir, { required = false, from } = {}) {
|
|
242
|
+
const s = path.isAbsolute(name) ? name : path.join(from, name);
|
|
243
|
+
const stat = fs.existsSync(s) ? fs.lstatSync(s) : null;
|
|
244
|
+
if (!stat || !stat.isFile()) { if (required) missing.push(path.basename(name)); return false; }
|
|
245
|
+
if (stat.isSymbolicLink()) fail(`refusing to copy a symbolic link into the bundle: ${s}`);
|
|
246
|
+
fs.copyFileSync(s, path.join(destDir, path.basename(s)));
|
|
292
247
|
copied++;
|
|
248
|
+
return true;
|
|
293
249
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
cpDir(path.join(
|
|
300
|
-
|
|
301
|
-
// CONCEPTS store (L2 + primers embedded as prose; big-only) — the cross-repo tool unions it at query
|
|
302
|
-
// time so code-implemented capabilities are retrievable as high-confidence prose. discoverRepos in the
|
|
303
|
-
// bundle finds concepts.big.rvf automatically, so search_ruvnet searches it with no extra config.
|
|
304
|
-
const hasConcepts = fs.existsSync(path.join(ASSETS, 'concepts.big.rvf'));
|
|
305
|
-
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', 'concepts.sources.json']) cp(suf, OUT, { asset: true });
|
|
306
|
-
|
|
307
|
-
// RUV-GISTS store (rUv's public gists — release notes / integration dossiers; big-only, same shape as
|
|
308
|
-
// concepts, unioned by search_ruvnet at query time). BUG FIX: only `concepts` was special-cased above,
|
|
309
|
-
// so ruv-gists — a PUBLIC store, not private-fenced — was silently omitted from EVERY bundle ever
|
|
310
|
-
// shipped. Include it, WITH .big.passages.jsonl (the big variant opens it by name, same as concepts).
|
|
311
|
-
const hasGists = fs.existsSync(path.join(ASSETS, 'ruv-gists.big.rvf'));
|
|
312
|
-
if (hasGists) {
|
|
313
|
-
// RVF and passage bytes come from the sealed seed; the source receipt is governed source
|
|
314
|
-
// evidence from this checkout. The seed predates shipping that receipt, so sourcing every file
|
|
315
|
-
// from ASSETS makes the release projection fail closed even though the exact gist bytes exist.
|
|
316
|
-
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 });
|
|
317
|
-
cp('ruv-gists.sources.json', OUT, { required: !PROJECTION });
|
|
318
|
-
}
|
|
319
|
-
// The inventory projection consumes this registry from the assembled bundle. Older corpus seeds
|
|
320
|
-
// do not carry it, so derive the only valid legacy class (concepts) from the exact bytes already
|
|
321
|
-
// assembled instead of failing before projection can create its release-bound evidence set.
|
|
322
|
-
if (!cp('public-store-classes.json', OUT, { asset: true })) {
|
|
323
|
-
const derived = [];
|
|
324
|
-
const conceptsPassages = path.join(OUT, 'concepts.passages.jsonl');
|
|
325
|
-
if (fs.existsSync(conceptsPassages)) {
|
|
326
|
-
const bytes = fs.readFileSync(conceptsPassages);
|
|
327
|
-
const receipt = { schemaVersion: 1, kind: 'ruvnet-brain-derived-store-receipt', store: 'concepts',
|
|
328
|
-
inputs: [{ path: 'concepts.passages.jsonl', sha256: sha256(bytes) }], passagesSha256: sha256(bytes) };
|
|
329
|
-
fs.writeFileSync(path.join(OUT, 'concepts.sources.json'), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
330
|
-
derived.push({ store: 'concepts', receipt: 'concepts.sources.json' });
|
|
331
|
-
}
|
|
332
|
-
fs.writeFileSync(path.join(OUT, 'public-store-classes.json'), `${JSON.stringify({ schemaVersion: 1, derived }, null, 2)}\n`);
|
|
333
|
-
copied++;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// capability-cards.md — the FAST LANE's zero-ML answer source (kb/card-lane.mjs, the first
|
|
337
|
-
// responder search_ruvnet consults before the heavy cross-repo search). Ships as its own small
|
|
338
|
-
// text file, NOT only baked into concepts.big.rvf, so the fast lane can answer without opening
|
|
339
|
-
// any vector store or loading either ONNX model. Re-filtered by the SAME PRIVATE_STORES fence
|
|
340
|
-
// applied to every other shipped artifact above — reapplied here rather than trusted from the
|
|
341
|
-
// source file, because a raw-file copy is a NEW shipping path for this exact content
|
|
342
|
-
// (scripts/build-concepts.mjs already fences it into the concepts store; that fence must not be
|
|
343
|
-
// silently regained by a second path that copies the same source file without it).
|
|
344
|
-
{
|
|
345
|
-
const cardsSrc = path.join(KB, 'capability-cards.md');
|
|
346
|
-
if (fs.existsSync(cardsSrc)) {
|
|
347
|
-
const raw = fs.readFileSync(cardsSrc, 'utf8');
|
|
348
|
-
const parts = raw.split(/^##\s+/m);
|
|
349
|
-
const kept = [];
|
|
350
|
-
let filteredAny = false;
|
|
351
|
-
for (const sec of parts.slice(1)) {
|
|
352
|
-
const nl = sec.indexOf('\n');
|
|
353
|
-
const repo = nl < 0 ? '' : sec.slice(0, nl).trim();
|
|
354
|
-
if (repo && PRIVATE_STORES.has(repo.toLowerCase())) { filteredAny = true; continue; }
|
|
355
|
-
kept.push(`## ${sec}`);
|
|
356
|
-
}
|
|
357
|
-
fs.writeFileSync(path.join(OUT, 'capability-cards.md'), parts[0] + kept.join(''));
|
|
358
|
-
copied++;
|
|
359
|
-
if (filteredAny) console.log('[build-bundle] filtered private repo card(s) out of shipped capability-cards.md');
|
|
360
|
-
|
|
361
|
-
// repo-aliases.json TRAVELS WITH THE CARDS OR EVERY ALIAS IS DEAD ON ARRIVAL.
|
|
362
|
-
//
|
|
363
|
-
// card-lane.mjs reads it (REPO_ALIASES_FILE) to resolve a PRODUCT name to its installed store:
|
|
364
|
-
// `agent-harness-generator -> metaharness`, `ruvnet-brain -> brain`, `ruvector -> rvf`,
|
|
365
|
-
// `agentic-qe -> qe`. The bundle never copied it, so on every install those mappings were absent
|
|
366
|
-
// and the fast lane could not route them. Measured 2026-08-19 against the live store root:
|
|
367
|
-
// repo-aliases.json ABSENT, and `search_ruvnet("metaharness: what does Darwin mode mutate?")`
|
|
368
|
-
// answered "card router was ambiguous" — the product cannot be found by its own name.
|
|
369
|
-
//
|
|
370
|
-
// `forge-update.mjs:505` already lists this file among the ones it manages, so the updater knew
|
|
371
|
-
// about it and the builder did not — one fact, two shipping paths, only one of them told. Same
|
|
372
|
-
// shape as the reader/writer store-root split and the ingest `--out` bug fixed this week.
|
|
373
|
-
const aliasesSrc = path.join(KB, 'repo-aliases.json');
|
|
374
|
-
if (fs.existsSync(aliasesSrc)) {
|
|
375
|
-
fs.copyFileSync(aliasesSrc, path.join(OUT, 'repo-aliases.json'));
|
|
376
|
-
copied++;
|
|
377
|
-
} else {
|
|
378
|
-
console.log('[build-bundle] note: kb/repo-aliases.json absent — product-name aliases will not resolve on install');
|
|
250
|
+
function cpDir(srcDir, destDir) {
|
|
251
|
+
if (!fs.existsSync(srcDir)) return false;
|
|
252
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
253
|
+
for (const e of fs.readdirSync(srcDir, { withFileTypes: true })) {
|
|
254
|
+
if (e.isSymbolicLink()) fail(`refusing to copy a symbolic link into the bundle: ${path.join(srcDir, e.name)}`);
|
|
255
|
+
if (e.isDirectory()) cpDir(path.join(srcDir, e.name), path.join(destDir, e.name));
|
|
256
|
+
else { fs.copyFileSync(path.join(srcDir, e.name), path.join(destDir, e.name)); copied++; }
|
|
379
257
|
}
|
|
380
|
-
|
|
381
|
-
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');
|
|
258
|
+
return true;
|
|
382
259
|
}
|
|
260
|
+
return { cp, cpDir, get copied() { return copied; }, get missing() { return missing; } };
|
|
383
261
|
}
|
|
384
262
|
|
|
385
|
-
//
|
|
386
|
-
// bundle without it crashes the brain on startup (MODULE_NOT_FOUND). forge-update.mjs is the consumer
|
|
387
|
-
// self-updater (reads SOURCE.json, copied below).
|
|
388
|
-
// package-lock.json ships so the installer can `npm ci` (pinned, reproducible) instead of an
|
|
389
|
-
// unpinned `npm i` resolve (SEC-0010 #8).
|
|
390
|
-
// verify-citation.mjs ships WITH the KB because it verifies the KB: --doctor loads it from the
|
|
391
|
-
// installed bundle to prove an answer's citation resolves to a real passage on disk.
|
|
392
|
-
//
|
|
393
|
-
// DERIVED, NOT HAND-MAINTAINED (issue #32, Jan Lafko / @lafinak, 2026-07-20).
|
|
394
|
-
// This list used to be typed by hand, and twice a new required import fell out of it and shipped a
|
|
395
|
-
// bundle that crashed on startup with MODULE_NOT_FOUND — most recently `forge-hybrid.mjs`, which
|
|
396
|
-
// `forge-ask-all.mjs` imports unconditionally (commit 57354dd). The warning comment above was already
|
|
397
|
-
// there and did not prevent it, because a comment cannot know what a file imports. The build now does.
|
|
398
|
-
//
|
|
399
|
-
// We walk the real static import graph from the entry points that must be runnable out of the
|
|
400
|
-
// installed bundle, and ship every local module they transitively reach. A new import is therefore
|
|
401
|
-
// bundled the moment it is written, with no list to remember to update.
|
|
263
|
+
// ---- runtime module graph (unchanged discipline from before this step; scoped to a `kbDir` arg) --
|
|
402
264
|
const ENTRYPOINTS = [
|
|
403
265
|
'forge-ask.mjs', 'forge-ask-all.mjs', 'forge-mcp.mjs', 'forge-mcp-all.mjs',
|
|
404
266
|
'forge-rerank.mjs', 'forge-guard.mjs', 'forge-update.mjs', 'verify-citation.mjs',
|
|
405
267
|
];
|
|
406
|
-
// Non-module assets, and modules reached only by a path the graph walk cannot see (e.g. a spawned
|
|
407
|
-
// child process). Kept explicit BECAUSE they are genuinely not derivable — not as a duplicate list.
|
|
408
268
|
const EXTRA_FILES = ['package.json', 'package-lock.json', 'package-owners.json'];
|
|
409
269
|
|
|
410
270
|
/** Local (relative) specifiers a module imports — static, side-effect, and literal dynamic. */
|
|
411
271
|
function localImportsOf(absFile) {
|
|
412
272
|
const raw = fs.readFileSync(absFile, 'utf8');
|
|
413
|
-
// STRIP COMMENTS BEFORE SCANNING. The static-import pattern below uses [^;'"]* to bridge the gap
|
|
414
|
-
// between `import` and `from`, which cannot cross an apostrophe. This file's own house style is
|
|
415
|
-
// dense commentary full of contractions, so a perfectly ordinary multi-line import —
|
|
416
|
-
// import {
|
|
417
|
-
// foo, // don't forget bar
|
|
418
|
-
// bar,
|
|
419
|
-
// } from './mod.mjs';
|
|
420
|
-
// matched NOTHING: no throw, no escape warning, the walker never queued './mod.mjs', and the build
|
|
421
|
-
// happily shipped a bundle missing it. That is strictly worse than the #32 bug it replaced, which
|
|
422
|
-
// at least crashed loudly on startup. Comments carry no imports, so removing them first costs
|
|
423
|
-
// nothing and closes the whole class.
|
|
424
273
|
const src = raw
|
|
425
274
|
.replace(/\/\*[\s\S]*?\*\//g, ' ') // block comments
|
|
426
275
|
.replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // line comments (the [^:] guard spares "https://")
|
|
@@ -428,36 +277,23 @@ function localImportsOf(absFile) {
|
|
|
428
277
|
for (const m of src.matchAll(/\b(?:import|export)\b[^;'"]*?\bfrom\s*['"]([^'"]+)['"]/g)) specs.add(m[1]);
|
|
429
278
|
for (const m of src.matchAll(/\bimport\s*['"]([^'"]+)['"]/g)) specs.add(m[1]);
|
|
430
279
|
for (const m of src.matchAll(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) specs.add(m[1]);
|
|
431
|
-
// `import(new URL('./x.mjs', import.meta.url).href)` — the computed-URL form. Missed by the plain
|
|
432
|
-
// literal pattern above, and it is not hypothetical: forge-ask-all.mjs and forge-mcp-all.mjs both
|
|
433
|
-
// load brain-alarm.mjs and telemetry-ping.mjs exactly this way. Neither was reaching the bundle,
|
|
434
|
-
// and because both call sites are try/catch-guarded it failed SILENTLY — meaning the brain-down
|
|
435
|
-
// "GONG" alarm, the thing whose entire job is to make a broken brain impossible to miss, has been
|
|
436
|
-
// dead in every installed bundle. Found by adversarial review, not by any test.
|
|
437
280
|
for (const m of src.matchAll(/\bimport\s*\(\s*new\s+URL\s*\(\s*['"]([^'"]+)['"]/g)) specs.add(m[1]);
|
|
438
281
|
return [...specs].filter((s) => s.startsWith('./') || s.startsWith('../'));
|
|
439
282
|
}
|
|
440
283
|
|
|
441
|
-
/** Transitive closure of local modules reachable from the entry points, relative to
|
|
442
|
-
function resolveModuleGraph() {
|
|
284
|
+
/** Transitive closure of local modules reachable from the entry points, relative to `kbDir`. */
|
|
285
|
+
function resolveModuleGraph(kbDir) {
|
|
443
286
|
const seen = new Set();
|
|
444
|
-
// `from` distinguishes the two absent-file cases, which are NOT the same failure:
|
|
445
|
-
// from === null -> an entry point this kb/ simply does not contain (a minimal fixture, a
|
|
446
|
-
// partial tree). Nothing to bundle; skip it.
|
|
447
|
-
// from !== null -> a module that something ACTUALLY IMPORTS is missing. That is precisely the
|
|
448
|
-
// MODULE_NOT_FOUND of #32, and it must stop the build.
|
|
449
287
|
const queue = ENTRYPOINTS.map((rel) => ({ rel, from: null }));
|
|
450
288
|
const escapes = [];
|
|
451
289
|
while (queue.length) {
|
|
452
290
|
const { rel, from } = queue.shift();
|
|
453
291
|
if (seen.has(rel)) continue;
|
|
454
|
-
const abs = path.join(
|
|
292
|
+
const abs = path.join(kbDir, rel);
|
|
455
293
|
if (!fs.existsSync(abs)) {
|
|
456
294
|
if (from === null) { console.log(`[build-bundle] note: entry point ${rel} absent from kb/ — skipping`); continue; }
|
|
457
|
-
|
|
458
|
-
`
|
|
459
|
-
`a bundle built from this tree would crash on startup (issue #32)`,
|
|
460
|
-
);
|
|
295
|
+
fail(`${from} imports ${rel}, which does not exist in kb/ — ` +
|
|
296
|
+
`a bundle built from this tree would crash on startup (issue #32)`);
|
|
461
297
|
}
|
|
462
298
|
seen.add(rel);
|
|
463
299
|
for (const spec of localImportsOf(abs)) {
|
|
@@ -467,96 +303,621 @@ function resolveModuleGraph() {
|
|
|
467
303
|
}
|
|
468
304
|
}
|
|
469
305
|
if (escapes.length) {
|
|
470
|
-
|
|
471
|
-
throw new Error(`[build-bundle] import escapes kb/ and would break the installed bundle:\n ${escapes.join('\n ')}`);
|
|
306
|
+
fail(`import escapes kb/ and would break the installed bundle:\n ${escapes.join('\n ')}`);
|
|
472
307
|
}
|
|
473
308
|
return [...seen].sort();
|
|
474
309
|
}
|
|
475
310
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
311
|
+
/**
|
|
312
|
+
* projectStoreViews({ selectedResults, identity, updaterConfig }) -> { source, ledger, manifestEntries }
|
|
313
|
+
*
|
|
314
|
+
* Generates SOURCE, the product manifest rows, and the runtime/public generation ledger ONCE from
|
|
315
|
+
* one explicit list of selected records and the explicit release identity (algorithm step 6) — never
|
|
316
|
+
* three independently-derived copies. `updaterConfig` is the finalized corpus's OWN SOURCE.json
|
|
317
|
+
* (produced fresh by reconciliation), read as an explicit configuration ADAPTER: only its non-identity
|
|
318
|
+
* updater fields (canonicalManifestUrl/canonicalBundleUrl/selfUpdate/sourceRepo/sourceDescribe/kbName)
|
|
319
|
+
* are borrowed per store; `sourceCommit`/`builtUtc` are always bound from that store's own selected
|
|
320
|
+
* generation record, never independently trusted from `updaterConfig`'s own copy (algorithm step 5:
|
|
321
|
+
* an adapter, never a checkout-`SOURCE.stores` copy). Every `manifestEntries[i].builtFromSha` is
|
|
322
|
+
* likewise derived directly from the selected generation (algorithm step 7) — there is no external
|
|
323
|
+
* "prior manifest" lookup left to drift from it.
|
|
324
|
+
*/
|
|
325
|
+
export function projectStoreViews({ selectedResults, identity, updaterConfig }) {
|
|
326
|
+
if (!Array.isArray(selectedResults) || !selectedResults.length) {
|
|
327
|
+
fail('projectStoreViews requires at least one selected record');
|
|
328
|
+
}
|
|
329
|
+
const version = identity?.version;
|
|
330
|
+
const sourceSnapshot = identity?.sourceSnapshot ?? null;
|
|
331
|
+
if (typeof version !== 'string' || !version) fail('projectStoreViews requires an explicit release version');
|
|
332
|
+
// The source snapshot is optional here (see tryGitSha's own note) but, when supplied, must be a
|
|
333
|
+
// genuine 40-hex commit — never a garbage value silently carried into the shipped ledger.
|
|
334
|
+
if (sourceSnapshot !== null && !/^[a-f0-9]{40}$/.test(sourceSnapshot)) fail('projectStoreViews source snapshot, when supplied, must be exact 40-hex');
|
|
335
|
+
const config = updaterConfig && typeof updaterConfig === 'object' ? updaterConfig : {};
|
|
336
|
+
const ledgerStores = {};
|
|
337
|
+
const sourceStores = {};
|
|
338
|
+
const manifestEntries = [];
|
|
339
|
+
for (const result of [...selectedResults].sort((a, b) => a.name.localeCompare(b.name))) {
|
|
340
|
+
const { name, kind, generation } = result;
|
|
341
|
+
if (!generation || typeof generation.sha256 !== 'string' || !generation.sha256) {
|
|
342
|
+
fail(`${name}: selected record has no generation to project`);
|
|
343
|
+
}
|
|
344
|
+
ledgerStores[name] = {
|
|
345
|
+
file: generation.file || `${name}.big.rvf`,
|
|
346
|
+
sha256: generation.sha256,
|
|
347
|
+
bytes: generation.bytes,
|
|
348
|
+
model: generation.model,
|
|
349
|
+
dimensions: generation.dimensions,
|
|
350
|
+
sourceCommit: generation.sourceCommit ?? null,
|
|
351
|
+
builtUtc: generation.builtUtc,
|
|
352
|
+
};
|
|
353
|
+
if (kind !== 'repository') continue; // SOURCE.json and manifest rows are repository-only, exactly as forge-refresh.mjs's own SOURCE.json has always been.
|
|
354
|
+
const updater = (config.stores && config.stores[name]) || {};
|
|
355
|
+
// Spread the corpus's own updater entry FIRST so every field kb/forge-update.mjs may read
|
|
356
|
+
// (updateManaged, a per-store releaseTag, anything added later) survives; then bind the
|
|
357
|
+
// identity fields from the selected generation, which always win.
|
|
358
|
+
sourceStores[name] = {
|
|
359
|
+
...updater,
|
|
360
|
+
kbName: updater.kbName || name,
|
|
361
|
+
sourceRepo: updater.sourceRepo || null,
|
|
362
|
+
sourceCommit: generation.sourceCommit ?? null,
|
|
363
|
+
sourceDescribe: updater.sourceDescribe || null,
|
|
364
|
+
builtUtc: generation.builtUtc,
|
|
365
|
+
builder: updater.builder || config.builder || 'rvf-kb-forge',
|
|
366
|
+
canonicalManifestUrl: updater.canonicalManifestUrl || null,
|
|
367
|
+
canonicalBundleUrl: updater.canonicalBundleUrl || null,
|
|
368
|
+
selfUpdate: updater.selfUpdate || `node forge-update.mjs ${name}`,
|
|
369
|
+
};
|
|
370
|
+
manifestEntries.push({
|
|
371
|
+
name, tier: result.tier, stars: result.stars,
|
|
372
|
+
chunks: result.chunks, baseModel: result.baseModel, baseDims: result.baseDims,
|
|
373
|
+
variants: ['big'], hasSymbols: result.hasSymbols, hasPrimer: result.hasPrimer, hasBig: true,
|
|
374
|
+
gradeRealUse: result.gradeRealUse,
|
|
375
|
+
builtFromSha: generation.sourceCommit || 'unknown',
|
|
376
|
+
status: 'built',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// Top-level envelope: the corpus's own top-level updater fields survive (spread first); identity
|
|
380
|
+
// fields and the store map are then bound from this assembly and always win.
|
|
381
|
+
const { stores: _configStores, ...configTopLevel } = config;
|
|
382
|
+
const source = {
|
|
383
|
+
...configTopLevel,
|
|
384
|
+
builder: config.builder || 'rvf-kb-forge',
|
|
385
|
+
brainVersion: version,
|
|
386
|
+
releaseTag: `v${version}`,
|
|
387
|
+
builtUtc: new Date().toISOString(),
|
|
388
|
+
canonicalManifestUrl: config.canonicalManifestUrl || null,
|
|
389
|
+
selfUpdate: config.selfUpdate || 'node forge-update.mjs',
|
|
390
|
+
stores: sourceStores,
|
|
391
|
+
};
|
|
392
|
+
const ledger = { schemaVersion: 2, brainVersion: version, releaseTag: `v${version}`, sourceSnapshot, stores: ledgerStores };
|
|
393
|
+
return { source, ledger, manifestEntries };
|
|
495
394
|
}
|
|
496
395
|
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
396
|
+
// Extra sidecar families beyond the universal per-store family (big.rvf/idmap/embed/passages/meta,
|
|
397
|
+
// handled generically for every discovered store) — driven by KIND, from one explicit table, never
|
|
398
|
+
// by ad hoc `hasConcepts`/`hasGists` boolean special-casing.
|
|
399
|
+
const EXTRA_SIDECARS_BY_KIND = {
|
|
400
|
+
'gist-aggregate': (name) => [{ name: `${name}.sources.json`, required: true }],
|
|
401
|
+
derived: (name) => [{ name: `${name}.sources.json`, required: true }],
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* assembleBundle({ corpusDir, runtimeRoot, outDir, identity, seedIdentity }) -> Promise<ArchiveResult>
|
|
406
|
+
*
|
|
407
|
+
* See this file's header for the full algorithm. `identity` = { version?, sourceSnapshot? } (both
|
|
408
|
+
* optional; default to the checkout's own version/HEAD). `seedIdentity` = { tag, archiveSha256,
|
|
409
|
+
* archiveBytes, baselineReceiptSha256 } — when every field is present, a release coverage projection
|
|
410
|
+
* (COVERAGE.json / CORPUS-COVERAGE.json / PUBLIC-RVF-GENERATIONS.json) is additionally produced and
|
|
411
|
+
* validated; when absent, assembleBundle still produces a complete, correctly-selected candidate.
|
|
412
|
+
*/
|
|
413
|
+
export async function assembleBundle(options = {}) {
|
|
414
|
+
// Every temporary staging directory this pass creates is removed here, on success AND on failure.
|
|
415
|
+
// P1-a: the public-prose staging directory lives in os.tmpdir(); the corpus checkout is never one.
|
|
416
|
+
const scratchDirs = [];
|
|
417
|
+
try {
|
|
418
|
+
return await assembleBundleImpl(options, scratchDirs);
|
|
419
|
+
} finally {
|
|
420
|
+
for (const dir of scratchDirs) fs.rmSync(dir, { recursive: true, force: true });
|
|
513
421
|
}
|
|
514
422
|
}
|
|
515
423
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const
|
|
519
|
-
const
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
424
|
+
async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {}, seedIdentity = null,
|
|
425
|
+
legacySeedProjection = null }, scratchDirs) {
|
|
426
|
+
const runtime = path.resolve(runtimeRoot || ROOT);
|
|
427
|
+
const corpus = path.resolve(corpusDir || '');
|
|
428
|
+
const out = path.resolve(outDir || '');
|
|
429
|
+
const kbDir = path.join(runtime, 'kb');
|
|
430
|
+
const dataDir = path.join(runtime, 'data');
|
|
431
|
+
const version = stripTag(identity.version || getVersionTag());
|
|
432
|
+
const versionTag = `v${version}`;
|
|
433
|
+
const sourceSnapshot = identity.sourceSnapshot || tryGitSha(runtime);
|
|
434
|
+
|
|
435
|
+
// A build is a release build or it is not. A seedIdentity that is present but incomplete is
|
|
436
|
+
// rejected HERE, before any work, never silently downgraded to a non-release candidate.
|
|
437
|
+
if (seedIdentity !== null && seedIdentity !== undefined) {
|
|
438
|
+
const { tag, archiveSha256, archiveBytes, baselineReceiptSha256 } = seedIdentity;
|
|
439
|
+
if (typeof tag !== 'string' || !tag || !/^[a-f0-9]{64}$/.test(String(archiveSha256 || ''))
|
|
440
|
+
|| !Number.isSafeInteger(archiveBytes) || archiveBytes < 1
|
|
441
|
+
|| !/^[a-f0-9]{64}$/.test(String(baselineReceiptSha256 || ''))) {
|
|
442
|
+
fail('seed identity is incomplete or malformed (tag, archiveSha256, archiveBytes, and baselineReceiptSha256 '
|
|
443
|
+
+ 'are all required together) — refusing to silently downgrade to a non-release build');
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---- LEGACY SEED PROJECTION (temporary; retires at plan step 11) --------------------------------
|
|
448
|
+
// The ONE explicitly-declared compatibility mode for the pinned PRE-Step-3/4 corpus seed. See this
|
|
449
|
+
// file's header. It is never inferred: a caller must pass it, and it is mutually exclusive with the
|
|
450
|
+
// new in-process release path, so no run can accidentally be half of each.
|
|
451
|
+
if (legacySeedProjection !== null && legacySeedProjection !== undefined) {
|
|
452
|
+
if (seedIdentity !== null && seedIdentity !== undefined) {
|
|
453
|
+
fail('legacySeedProjection and seedIdentity are mutually exclusive: the legacy two-pass seed path and the '
|
|
454
|
+
+ 'in-process single-pass release projection are two different release rails, never half of each');
|
|
455
|
+
}
|
|
456
|
+
// Pass 1 (no projection yet) produces the candidate the projection producer measures; pass 2
|
|
457
|
+
// carries the produced projection back in. A HALF-supplied pass 2 is rejected, never silently
|
|
458
|
+
// treated as pass 1 — that would publish an archive with no COVERAGE.json at all.
|
|
459
|
+
const { coverageFile: legacyCoverage = null, projectionDir = null } = legacySeedProjection;
|
|
460
|
+
if ((projectionDir === null) !== (legacyCoverage === null)) {
|
|
461
|
+
fail('legacy seed projection needs BOTH --coverage and --projection together (pass 2), or NEITHER (pass 1)');
|
|
462
|
+
}
|
|
463
|
+
if (projectionDir !== null) {
|
|
464
|
+
if (!fs.existsSync(path.resolve(projectionDir))) {
|
|
465
|
+
fail(`legacy seed projection directory is missing (${projectionDir}) — run scripts/release-projection.mjs first`);
|
|
466
|
+
}
|
|
467
|
+
if (!fs.existsSync(path.resolve(legacyCoverage))) fail(`legacy seed projection coverage is missing (${legacyCoverage})`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ---- algorithm step 1: validate the finalized corpus and derive one explicit corpus file allowlist
|
|
472
|
+
const privateStores = loadPrivateStores(kbDir);
|
|
473
|
+
|
|
474
|
+
// Public-prose selection (primers, L2, capability cards, repo aliases) happens EXACTLY ONCE per
|
|
475
|
+
// corpus round, in materializePublicInputs (scripts/public-inputs.mjs) — a SEPARATE policy from the
|
|
476
|
+
// per-repo CODE store fence above. PACKAGING MUST NEVER RE-DERIVE IT — and must never TRUST a seal
|
|
477
|
+
// it has not verified, either. THREE cases, chosen by an EXPLICIT condition, never by whether a file
|
|
478
|
+
// happens to exist:
|
|
479
|
+
//
|
|
480
|
+
// STANDALONE — corpus IS runtimeRoot/kb (this CLI's own `--assets kb` default; self-update.mjs).
|
|
481
|
+
// Nothing reconciled can live here, so the selection is ALWAYS materialized fresh — into a
|
|
482
|
+
// PRIVATE TEMPORARY STAGING DIRECTORY, never into the tracked checkout (see below). A receipt
|
|
483
|
+
// already sitting in kb/ is never read, so a stale checkout receipt can never flip this toggle.
|
|
484
|
+
// EXTERNAL — any other corpus directory. It MUST carry the receipt reconciliation sealed there,
|
|
485
|
+
// and that receipt must VALIDATE (validateSelectionReceipt: kind, schemaVersion, recomputed
|
|
486
|
+
// receiptSha256, every sealed file present with exact bytes, every included name backed, and
|
|
487
|
+
// no unsealed managed prose on disk). No receipt, or a failing one, is a defect in the corpus:
|
|
488
|
+
// assembly stops and the seed returns to preparation. It is never self-materialized into (and
|
|
489
|
+
// thereby mutated) a directory this build does not own.
|
|
490
|
+
// LEGACY SEED — the operator explicitly declared `--legacy-seed-projection` (see this file's
|
|
491
|
+
// header). The corpus is the pinned PRE-Step-3/4 release seed, which carries stores but no
|
|
492
|
+
// sealed public-input selection at all. Prose is selected fresh from the checkout into the same
|
|
493
|
+
// private staging directory STANDALONE uses; the seed directory is read-only throughout.
|
|
494
|
+
// RETIRES AT PLAN STEP 11.
|
|
495
|
+
//
|
|
496
|
+
// P1-a (Dual, 2026-09-14): STANDALONE used to pass `corpus` (= the tracked kb/) as
|
|
497
|
+
// materializePublicInputs' `outDir`, so a plain `node scripts/build-bundle.mjs` DELETED tracked
|
|
498
|
+
// source files — public-inputs.mjs:363-366 removes every managed entry the round did not reproduce
|
|
499
|
+
// and promotes `l2/` wholesale. That silently destroyed kb/cognitum-api-primer.md (fenced) and both
|
|
500
|
+
// kb/l2/rejected/*.md during a test run on 2026-09-13. Materialization now always targets a private
|
|
501
|
+
// staging directory under os.tmpdir(); the corpus checkout is never an output.
|
|
502
|
+
const sameDirectory = (left, right) => {
|
|
503
|
+
// FILESYSTEM IDENTITY, not a lexical string compare (P2, Dual 2026-09-14): `corpus === kbDir`
|
|
504
|
+
// was bypassable in both directions — a runtimeRoot/kb symlinked to an external directory
|
|
505
|
+
// satisfied it and got materialized into, while a symlink ALIAS of the same kb directory
|
|
506
|
+
// compared unequal and took the external branch. dev+ino classifies both spellings correctly.
|
|
507
|
+
try {
|
|
508
|
+
const a = fs.statSync(left), b = fs.statSync(right);
|
|
509
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
510
|
+
} catch { return path.resolve(left) === path.resolve(right); }
|
|
545
511
|
};
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
512
|
+
const legacySeed = legacySeedProjection !== null && legacySeedProjection !== undefined;
|
|
513
|
+
const standalone = !legacySeed && sameDirectory(corpus, kbDir);
|
|
514
|
+
const sealedSelectionFile = path.join(corpus, SELECTION_FILE);
|
|
515
|
+
let selectionReceipt;
|
|
516
|
+
// Where the SEALED PROSE is read from. For a sealed external corpus that is the corpus itself; for
|
|
517
|
+
// a freshly-materialized selection it is the staging directory, never the corpus.
|
|
518
|
+
let proseDir = corpus;
|
|
519
|
+
if (standalone || legacySeed) {
|
|
520
|
+
proseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-public-inputs-'));
|
|
521
|
+
scratchDirs.push(proseDir);
|
|
522
|
+
try {
|
|
523
|
+
({ selectionReceipt } = materializePublicInputs({
|
|
524
|
+
builderRoot: runtime, outDir: proseDir,
|
|
525
|
+
policy: { allowNoFence: process.env.ALLOW_NO_PRIVATE_FENCE === '1' },
|
|
526
|
+
}));
|
|
527
|
+
} catch (error) {
|
|
528
|
+
fail(`public-prose selection failed (${error.message})`);
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
if (!fs.existsSync(sealedSelectionFile)) {
|
|
532
|
+
fail(`external corpus ${corpus} carries no sealed public-input selection (${SELECTION_FILE}) — ` +
|
|
533
|
+
'reconciliation never sealed this directory; it returns to preparation and is never self-materialized here');
|
|
534
|
+
}
|
|
535
|
+
try { selectionReceipt = JSON.parse(fs.readFileSync(sealedSelectionFile, 'utf8')); }
|
|
536
|
+
catch (error) { fail(`sealed public-input selection (${sealedSelectionFile}) is present but unreadable (${error.message})`); }
|
|
537
|
+
}
|
|
538
|
+
// Verified on read in ALL cases (a fresh materialization is checked against its own output too):
|
|
539
|
+
// the seal is a proof, not a toggle.
|
|
540
|
+
try {
|
|
541
|
+
validateSelectionReceipt({ receipt: selectionReceipt, dir: proseDir });
|
|
542
|
+
} catch (error) {
|
|
543
|
+
fail(`${error.message} (${proseDir === corpus ? sealedSelectionFile : 'freshly materialized selection'})`);
|
|
544
|
+
}
|
|
545
|
+
// P1-b (Dual, 2026-09-14): a VALID seal is not the same as a seal this build is allowed to ship.
|
|
546
|
+
// The producer filtered with its THEN-current fence; assembly loads the CURRENT checkout fence at
|
|
547
|
+
// the top of this function and SHIPS it (PRIVATE-STORES.json, below) — so a selection sealed while
|
|
548
|
+
// a store was public, then fenced afterwards, used to ship that store's prose alongside a fence
|
|
549
|
+
// declaring it private. Reject the selection outright rather than filter it: derived concepts
|
|
550
|
+
// (scripts/corpus-aggregates.mjs:59-65) have ALREADY embedded this text into their passages, so
|
|
551
|
+
// dropping files here would leave the private prose inside concepts.passages.jsonl. The corpus
|
|
552
|
+
// returns to preparation and its aggregates are rebuilt under the current fence.
|
|
553
|
+
assertSelectionMatchesCurrentFence({ selectionReceipt, privateStores, kbDir, corpusLabel: proseDir });
|
|
554
|
+
if (!standalone && !legacySeed) {
|
|
555
|
+
console.log(`[build-bundle] trusting already-reconciled public-input selection at ${sealedSelectionFile} (verified, never re-derived)`);
|
|
556
|
+
}
|
|
557
|
+
{
|
|
558
|
+
const excluded = selectionReceipt.excluded || { primers: [], topics: [], l2: [], cards: [] };
|
|
559
|
+
const excludedCount = excluded.primers.length + excluded.topics.length + excluded.l2.length + excluded.cards.length;
|
|
560
|
+
if (excludedCount) console.log(`[build-bundle] public-input selection excluded ${excludedCount} private prose item(s)`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
let { built: discovered, excludedPrivate } = discoverBuilt(corpus, privateStores);
|
|
564
|
+
// Stores the LEGACY SEED mode scopes out. The seed's own generation ledger is immutable evidence
|
|
565
|
+
// and is never rewritten here, so they are declared to validateSelectedRvfGenerations as explicitly
|
|
566
|
+
// EXCLUDED rather than left looking like unexplained extra generation records.
|
|
567
|
+
const legacyExcluded = [];
|
|
568
|
+
// LEGACY SEED pass 2 (retires at plan step 11): the externally-produced release projection is the
|
|
569
|
+
// selection authority for this mode, exactly as it was before the step-5 consolidation. Scope the
|
|
570
|
+
// discovered set to the stores that projection actually bound, so the assembled tree and
|
|
571
|
+
// PUBLIC-RVF-GENERATIONS.json describe the same set (validateCoverageDirectory requires it).
|
|
572
|
+
if (legacySeed) {
|
|
573
|
+
// A store whose REQUIRED provenance sidecar the seed never carried is not publishable, and is
|
|
574
|
+
// scoped out loudly rather than shipped unproven. Measured against v4.2.1-dev on 2026-09-14: the
|
|
575
|
+
// seed carries ruv-gists.big.rvf with no ruv-gists.sources.json, and concepts.big.rvf with no
|
|
576
|
+
// public-store-classes.json to classify it. The PRE-Step-3 build-bundle papered over exactly this
|
|
577
|
+
// by SYNTHESIZING both files at packaging time — a receipt whose only declared input was its own
|
|
578
|
+
// packaged output. Step 3 deleted that fabrication on purpose; this mode does not bring it back.
|
|
579
|
+
const unprovable = discovered.filter((name) => {
|
|
580
|
+
const folded = name.toLowerCase();
|
|
581
|
+
if (folded === 'ruv-gists') return !fs.existsSync(path.join(corpus, 'ruv-gists.sources.json'));
|
|
582
|
+
if (folded === 'concepts') return !fs.existsSync(path.join(corpus, 'public-store-classes.json'));
|
|
583
|
+
return false;
|
|
584
|
+
});
|
|
585
|
+
if (unprovable.length) {
|
|
586
|
+
discovered = discovered.filter((name) => !unprovable.includes(name));
|
|
587
|
+
legacyExcluded.push(...unprovable);
|
|
588
|
+
console.log(`[build-bundle] legacy seed carries no provenance receipt for ${unprovable.sort().join(', ')} — `
|
|
589
|
+
+ 'scoped out of this assembly rather than shipped unproven (fixed by the step-8..11 re-seed)');
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (legacySeed && legacySeedProjection.projectionDir) {
|
|
593
|
+
let legacyCoverage;
|
|
594
|
+
try { legacyCoverage = JSON.parse(fs.readFileSync(path.resolve(legacySeedProjection.coverageFile), 'utf8')); }
|
|
595
|
+
catch (error) { fail(`legacy release projection coverage is unreadable (${error.message})`); }
|
|
596
|
+
if (legacyCoverage?.kind !== 'ruvnet-brain-release-coverage' || !Array.isArray(legacyCoverage.rows)) {
|
|
597
|
+
fail('legacy release projection coverage is not a ruvnet-brain-release-coverage ledger');
|
|
598
|
+
}
|
|
599
|
+
const projectedClasses = path.join(path.resolve(legacySeedProjection.projectionDir), 'public-store-classes.json');
|
|
600
|
+
const projectedDerived = fs.existsSync(projectedClasses)
|
|
601
|
+
? (JSON.parse(fs.readFileSync(projectedClasses, 'utf8')).derived || []).map((e) => String(e?.store || '').toLowerCase())
|
|
602
|
+
: [];
|
|
603
|
+
const projected = new Set([
|
|
604
|
+
...legacyCoverage.rows.filter((row) => row.disposition === 'eligible')
|
|
605
|
+
.map((row) => String(row?.artifact?.store || '').toLowerCase()),
|
|
606
|
+
...projectedDerived,
|
|
607
|
+
].filter(Boolean));
|
|
608
|
+
const absent = [...projected].filter((store) => !discovered.some((name) => name.toLowerCase() === store));
|
|
609
|
+
if (absent.length) fail(`legacy release projection names store(s) the seed does not carry: ${absent.sort().join(', ')}`);
|
|
610
|
+
const dropped = discovered.filter((name) => !projected.has(name.toLowerCase()));
|
|
611
|
+
discovered = discovered.filter((name) => projected.has(name.toLowerCase()));
|
|
612
|
+
legacyExcluded.push(...dropped);
|
|
613
|
+
if (dropped.length) console.log(`[build-bundle] legacy projection scoped out ${dropped.length} store(s): ${dropped.sort().join(', ')}`);
|
|
614
|
+
}
|
|
615
|
+
if (excludedPrivate.length) {
|
|
616
|
+
console.log(`[build-bundle] EXCLUDED ${excludedPrivate.length} PRIVATE store(s): ${[...new Set(excludedPrivate)].sort().join(', ')}`);
|
|
617
|
+
}
|
|
618
|
+
if (discovered.length === 0) {
|
|
619
|
+
fail('zero public RVF stores are eligible for release. Refusing to publish an empty brain bundle.');
|
|
620
|
+
}
|
|
621
|
+
const ledgerIn = readRvfGenerations(corpus);
|
|
622
|
+
const generationValidation = validateSelectedRvfGenerations(corpus, {
|
|
623
|
+
selectedStores: discovered, privateStores: [...privateStores], excludedStores: legacyExcluded,
|
|
624
|
+
});
|
|
625
|
+
if (generationValidation.failures.length) {
|
|
626
|
+
fail(`RVF generation ledger does not exactly bind selected roots:\n${generationValidation.failures.map((f) => ` ${f}`).join('\n')}`);
|
|
627
|
+
}
|
|
628
|
+
const rvfIndexAudit = await auditRvfIndexes(discovered.map((name) => path.join(corpus, `${name}.big.rvf`)));
|
|
629
|
+
const missingIndexes = rvfIndexAudit.filter(({ state }) => state !== 'PASS');
|
|
630
|
+
if (missingIndexes.length) {
|
|
631
|
+
fail(`eligible RVFs lack persisted HNSW indexes:\n${missingIndexes.map((r) => ` ${path.basename(r.path)} vectors=${r.totalVectors}`).join('\n')}\nRepair with: node scripts/rvf-index-audit.mjs --repair`);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// A deficient seed returns to preparation; packaging does NOT repair its source evidence. When a
|
|
635
|
+
// sealed corpus coverage measurement exists (prepareCorpusCandidate and ci.yml's release-qe job
|
|
636
|
+
// both always write one to runtimeRoot/data/source-coverage.json before calling this), the
|
|
637
|
+
// finalized corpus's own discovered store set must equal it EXACTLY — a mismatch means the corpus
|
|
638
|
+
// directory drifted from what was sealed, and assembly fails loudly rather than silently adapting.
|
|
639
|
+
const coverageFile = path.join(dataDir, 'source-coverage.json');
|
|
640
|
+
let corpusCoverage = null;
|
|
641
|
+
let inventory = null;
|
|
642
|
+
if (legacySeed) {
|
|
643
|
+
// LEGACY SEED (retires at plan step 11): the pinned seed is an OLDER, immutable corpus and
|
|
644
|
+
// data/source-coverage.json is the CURRENT observation of the source universe — two different
|
|
645
|
+
// evidence planes, by design. Measured against the live v4.2.1-dev seed on 2026-09-14: the seed
|
|
646
|
+
// carries 186 stores and no gist receipt, while the current coverage carries 195 eligible stores
|
|
647
|
+
// and 492 eligible gists, so the equality cross-check above cannot hold and is not asserted here.
|
|
648
|
+
// The projection producer (scripts/release-projection.mjs) does the scoping instead: it keeps
|
|
649
|
+
// only rows the seed actually carries and preserves the complete observation in
|
|
650
|
+
// CORPUS-COVERAGE.json. Step 11 re-pins a Step-4-produced seed and this branch is deleted.
|
|
651
|
+
console.log('[build-bundle] LEGACY SEED MODE (retires at plan step 11): the sealed-coverage equality '
|
|
652
|
+
+ 'cross-check is not asserted against this pre-Step-3/4 seed; scoping comes from the release projection');
|
|
653
|
+
} else if (fs.existsSync(coverageFile)) {
|
|
654
|
+
try { corpusCoverage = JSON.parse(fs.readFileSync(coverageFile, 'utf8')); }
|
|
655
|
+
catch (error) { fail(`sealed corpus coverage is present but unreadable (${error.message})`); }
|
|
656
|
+
// The coverage is itself a sealed ledger: its own digests (coverageGeneration, enumeration
|
|
657
|
+
// receipt, policy digests) must recompute before anything here trusts a row in it.
|
|
658
|
+
const ledgerCheck = validateCoverageLedger(corpusCoverage);
|
|
659
|
+
if (corpusCoverage?.kind !== 'ruvnet-brain-corpus-coverage' || !ledgerCheck.valid) {
|
|
660
|
+
fail(`sealed corpus coverage is invalid: ${ledgerCheck.failures.join('; ') || 'not a ruvnet-brain-corpus-coverage ledger'}`);
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
inventory = validatePublicInventory({ assetsDir: corpus, coverage: corpusCoverage, ledger: ledgerIn });
|
|
664
|
+
} catch (error) {
|
|
665
|
+
fail(`finalized corpus does not match its sealed coverage (${error.message}) — a deficient seed ` +
|
|
666
|
+
'returns to preparation; packaging never repairs source evidence');
|
|
667
|
+
}
|
|
668
|
+
// Bind the coverage to THIS corpus, not merely to "a" corpus: every eligible row's recorded
|
|
669
|
+
// artifact identity (the RVF digest it was measured against; for repositories also the source
|
|
670
|
+
// generation) must be exactly what this corpus's own ledger carries for that store —
|
|
671
|
+
// validatePublicInventory has already bound that ledger to the bytes on disk.
|
|
672
|
+
for (const row of corpusCoverage.rows) {
|
|
673
|
+
if (row.disposition !== 'eligible') continue;
|
|
674
|
+
const store = String(row.artifact?.store || '');
|
|
675
|
+
const generation = ledgerIn.stores?.[store]
|
|
676
|
+
|| Object.entries(ledgerIn.stores || {}).find(([key]) => key.toLowerCase() === store.toLowerCase())?.[1];
|
|
677
|
+
if (!generation) fail(`coverage row ${row.key} names store ${store}, which this corpus's ledger does not carry`);
|
|
678
|
+
if (String(row.artifact?.rvfSha256 || '').toLowerCase() !== String(generation.sha256 || '').toLowerCase()) {
|
|
679
|
+
fail(`coverage row ${row.key} was measured against different ${store} RVF bytes than this corpus carries`);
|
|
680
|
+
}
|
|
681
|
+
if (row.kind === 'repository'
|
|
682
|
+
&& String(row.artifact?.sourceCommit || '').toLowerCase() !== String(generation.sourceCommit || '').toLowerCase()) {
|
|
683
|
+
fail(`coverage row ${row.key} records a different ${store} source generation than this corpus's ledger`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
const discoveredLower = new Set(discovered.map((s) => s.toLowerCase()));
|
|
687
|
+
const missingFromDisk = inventory.publicStores.filter((s) => !discoveredLower.has(s));
|
|
688
|
+
if (missingFromDisk.length) fail(`classified public store(s) missing from the finalized corpus: ${missingFromDisk.join(', ')}`);
|
|
689
|
+
const selected = new Set(inventory.publicStores);
|
|
690
|
+
const extraOnDisk = discovered.filter((s) => !selected.has(s.toLowerCase()));
|
|
691
|
+
if (extraOnDisk.length) fail(`finalized corpus carries unclassified public store(s) not present in sealed coverage: ${extraOnDisk.join(', ')}`);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ---- classify each selected store + gather the facts every downstream view needs ---------------
|
|
695
|
+
// public-store-classes.json is the derived-store registry (concepts). FAIL-LOUD: an unparseable or
|
|
696
|
+
// malformed copy is never tolerated. A MISSING copy is tolerated only in the standalone case (a
|
|
697
|
+
// local kb/ that has never built a derived store) — and even there never when a `concepts` store
|
|
698
|
+
// is present, since misclassifying it as a repository would yield a wrong SOURCE.json row, a wrong
|
|
699
|
+
// manifest row, a wrong grade lookup, and a wrong per-repo MCP entry. A sealed corpus always has
|
|
700
|
+
// one (buildConceptAggregate writes it every reconciliation round), so its absence there is a defect.
|
|
701
|
+
const classesFile = path.join(corpus, 'public-store-classes.json');
|
|
702
|
+
const derivedNames = new Set();
|
|
703
|
+
if (fs.existsSync(classesFile)) {
|
|
704
|
+
let classes;
|
|
705
|
+
try { classes = JSON.parse(fs.readFileSync(classesFile, 'utf8')); }
|
|
706
|
+
catch (error) { fail(`public-store-classes.json is present but unreadable (${error.message})`); }
|
|
707
|
+
if (classes?.schemaVersion !== 1 || !Array.isArray(classes.derived)) {
|
|
708
|
+
fail('public-store-classes.json is malformed (expected schemaVersion 1 with a derived array)');
|
|
709
|
+
}
|
|
710
|
+
for (const entry of classes.derived) derivedNames.add(String(entry?.store || '').toLowerCase());
|
|
711
|
+
} else if (legacySeed) {
|
|
712
|
+
// The pinned pre-Step-3/4 seed predates the derived-store registry (measured 2026-09-14: absent
|
|
713
|
+
// from v4.2.1-dev). The legacy projection producer already treated its absence as "no derived
|
|
714
|
+
// stores", exactly as here. Retires at plan step 11 with the rest of this mode.
|
|
715
|
+
console.log('[build-bundle] legacy seed carries no public-store-classes.json — no derived stores are classified');
|
|
716
|
+
} else if (!standalone) {
|
|
717
|
+
fail(`sealed corpus ${corpus} carries no public-store-classes.json — reconciliation always writes one; this corpus returns to preparation`);
|
|
718
|
+
} else if (discovered.some((name) => name.toLowerCase() === 'concepts')) {
|
|
719
|
+
fail('a concepts store is present but public-store-classes.json is missing — it cannot be classified, and will not be guessed');
|
|
720
|
+
}
|
|
721
|
+
// Unchanged from before this step: registry.tiers.json is a required checkout input (tier/star
|
|
722
|
+
// labels for the manifest and README), never optional enrichment — a missing or corrupt copy
|
|
723
|
+
// fails loudly here exactly as it always has, rather than silently shipping every store as tier '?'.
|
|
724
|
+
const registry = JSON.parse(fs.readFileSync(path.join(dataDir, 'registry.tiers.json'), 'utf8'));
|
|
725
|
+
const regFlat = [];
|
|
726
|
+
for (const [tier, t] of Object.entries(registry.tiers || {})) for (const r of (t.repos || [])) regFlat.push({ ...r, tier });
|
|
727
|
+
const regByLower = new Map(regFlat.map((r) => [r.name.toLowerCase(), r]));
|
|
728
|
+
|
|
729
|
+
// SOURCE.json is the installed self-updater's configuration: kb/forge-update.mjs reads its
|
|
730
|
+
// top-level canonicalManifestUrl/releaseTag and every per-store entry (kbName, canonicalBundleUrl,
|
|
731
|
+
// updateManaged). It is a REQUIRED corpus input, read through projectStoreViews as an explicit
|
|
732
|
+
// adapter — never optional enrichment. A missing or corrupt copy used to be silently replaced by
|
|
733
|
+
// `{ stores: {} }`, which shipped every installer with self-update UNCONFIGURED. Fail loud instead.
|
|
734
|
+
const updaterFile = path.join(corpus, 'SOURCE.json');
|
|
735
|
+
if (!fs.existsSync(updaterFile)) fail(`corpus SOURCE.json is missing (${updaterFile}) — self-update configuration is a required input`);
|
|
736
|
+
let updaterConfig;
|
|
737
|
+
try { updaterConfig = JSON.parse(fs.readFileSync(updaterFile, 'utf8')); }
|
|
738
|
+
catch (error) { fail(`corpus SOURCE.json is unreadable (${error.message})`); }
|
|
739
|
+
if (!updaterConfig || typeof updaterConfig !== 'object' || Array.isArray(updaterConfig)
|
|
740
|
+
|| (updaterConfig.stores !== undefined && (typeof updaterConfig.stores !== 'object' || Array.isArray(updaterConfig.stores)))) {
|
|
741
|
+
fail('corpus SOURCE.json is malformed (expected an object with an optional stores object)');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const selectedResults = discovered.map((name) => {
|
|
745
|
+
const folded = name.toLowerCase();
|
|
746
|
+
const generation = ledgerIn.stores?.[name]
|
|
747
|
+
|| Object.entries(ledgerIn.stores || {}).find(([key]) => key.toLowerCase() === folded)?.[1];
|
|
748
|
+
if (!generation) fail(`${name}: no generation record in the finalized corpus ledger`);
|
|
749
|
+
let chunks = null, model = null, dims = null;
|
|
750
|
+
try {
|
|
751
|
+
const m = JSON.parse(fs.readFileSync(path.join(corpus, `${name}.meta.json`), 'utf8'));
|
|
752
|
+
chunks = m.entries ? Object.keys(m.entries).length : null; model = m.model; dims = m.dimensions;
|
|
753
|
+
} catch { /* meta.json missing is reported later, as a missing required bundle file */ }
|
|
754
|
+
const hasSymbols = fs.existsSync(path.join(corpus, `${name}.symbols.json`));
|
|
755
|
+
// The primer that actually SHIPS is the one the verified selection sealed, so this reports the
|
|
756
|
+
// staging directory when the selection was materialized fresh — never the unfenced checkout.
|
|
757
|
+
const hasPrimer = fs.existsSync(path.join(proseDir, `${name}-primer.md`));
|
|
758
|
+
const kind = folded === 'ruv-gists' ? 'gist-aggregate' : derivedNames.has(folded) ? 'derived' : 'repository';
|
|
759
|
+
// A sealed corpus's SOURCE.json carries an updater entry for every repository store it holds
|
|
760
|
+
// (corpus-reconcile.mjs's validateWorkerOutput requires it before a worker result is merged).
|
|
761
|
+
// Its absence would ship that store with no canonicalBundleUrl/kbName for the installed updater
|
|
762
|
+
// — refuse, rather than ship a half-configured self-update. A standalone kb/ may legitimately
|
|
763
|
+
// predate that convention for some store, so it is noted there rather than fatal.
|
|
764
|
+
if (kind === 'repository' && !(updaterConfig.stores && typeof updaterConfig.stores[name] === 'object' && updaterConfig.stores[name] !== null)) {
|
|
765
|
+
if (!standalone && !legacySeed) fail(`${name}: sealed corpus SOURCE.json carries no updater entry for this repository store`);
|
|
766
|
+
console.log(`[build-bundle] note: ${legacySeed ? 'legacy seed' : 'standalone kb'} SOURCE.json has no updater entry for `
|
|
767
|
+
+ `${name} — its per-store self-update fields ship null`);
|
|
768
|
+
}
|
|
769
|
+
const reg = regByLower.get(folded) || {};
|
|
770
|
+
return {
|
|
771
|
+
name, kind, tier: reg.tier || '?', stars: reg.stars ?? null,
|
|
772
|
+
chunks, baseModel: model, baseDims: dims, hasSymbols, hasPrimer,
|
|
773
|
+
gradeRealUse: kind === 'repository' ? gradeFor(dataDir, name, generation) : null,
|
|
774
|
+
generation,
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
// ---- algorithm step 2: fresh output directory, disjoint from all inputs ------------------------
|
|
779
|
+
const ZIP = path.join(path.dirname(out), `${path.basename(out)}.zip`);
|
|
780
|
+
assertDisjoint('the candidate output directory', out, [
|
|
781
|
+
{ label: 'the finalized corpus', dir: corpus },
|
|
782
|
+
{ label: 'the runtime kb build workspace', dir: kbDir },
|
|
783
|
+
]);
|
|
784
|
+
fs.rmSync(out, { recursive: true, force: true });
|
|
785
|
+
fs.rmSync(ZIP, { force: true }); // a failed rebuild must not leave an older archive looking like this invocation's output
|
|
786
|
+
fs.mkdirSync(out, { recursive: true });
|
|
787
|
+
|
|
788
|
+
const { cp, cpDir, missing } = makeCopier();
|
|
789
|
+
|
|
790
|
+
// The shipped bundle must carry the exact policy boundary used during assembly.
|
|
791
|
+
cp('PRIVATE-STORES.json', out, { required: true, from: kbDir });
|
|
792
|
+
|
|
793
|
+
// ---- algorithm step 3: copy corpus content EXCLUSIVELY from corpusDir ---------------------------
|
|
794
|
+
for (const result of selectedResults) {
|
|
795
|
+
const { name } = result;
|
|
796
|
+
cp(`${name}.big.rvf`, out, { required: true, from: corpus });
|
|
797
|
+
cp(`${name}.big.rvf.idmap.json`, out, { required: true, from: corpus });
|
|
798
|
+
cp(`${name}.big.rvf.embed.json`, out, { required: true, from: corpus });
|
|
799
|
+
cp(`${name}.passages.jsonl`, out, { required: true, from: corpus });
|
|
800
|
+
cp(`${name}.meta.json`, out, { required: true, from: corpus });
|
|
801
|
+
cp(`${name}.symbols.json`, out, { from: corpus });
|
|
802
|
+
for (const extra of (EXTRA_SIDECARS_BY_KIND[result.kind]?.(name) || [])) {
|
|
803
|
+
cp(extra.name, out, { required: extra.required, from: corpus });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// The derived-store registry ships whenever the corpus has one (every sealed corpus does), not only
|
|
807
|
+
// when a derived store happens to be selected: validateCoverageDirectory — the release-projection
|
|
808
|
+
// proof bindAssembledReleaseProjection runs on the assembled tree — requires it unconditionally.
|
|
809
|
+
if (fs.existsSync(classesFile)) cp('public-store-classes.json', out, { required: true, from: corpus });
|
|
810
|
+
// The public prose: EXACTLY the files the (verified) selection receipt seals — the receipt IS the
|
|
811
|
+
// allowlist — plus the receipt itself, so every assembled archive carries its own proof and can be
|
|
812
|
+
// re-assembled from (round-tripped) without re-deriving anything. Per-repo primers, l2/ articles,
|
|
813
|
+
// l2-topics files, capability-cards.md and repo-aliases.json all arrive through this one loop; no
|
|
814
|
+
// per-store primer copy and no ad hoc directory copy exist beside it to disagree with it.
|
|
815
|
+
// validateSelectionReceipt has already proven each row is a regular, non-symlink file inside
|
|
816
|
+
// `corpus` with exactly these bytes; the only check left is that nothing else already claimed the
|
|
817
|
+
// destination path.
|
|
818
|
+
for (const row of selectionReceipt.files) {
|
|
819
|
+
const dest = path.join(out, row.path);
|
|
820
|
+
if (fs.existsSync(dest)) fail(`destination collision while copying sealed public prose: ${row.path}`);
|
|
821
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
822
|
+
fs.copyFileSync(path.join(proseDir, row.path), dest);
|
|
823
|
+
}
|
|
824
|
+
cp(SELECTION_FILE, out, { required: true, from: proseDir });
|
|
825
|
+
if (!selectionReceipt.files.some((row) => row.path === 'capability-cards.md')) {
|
|
826
|
+
console.log('[build-bundle] note: no capability-cards.md in the sealed selection -- the fast lane ships with no card source and will honestly fall through on every query');
|
|
827
|
+
}
|
|
828
|
+
if (!selectionReceipt.included.aliases) {
|
|
829
|
+
console.log('[build-bundle] note: no repo-aliases.json in the sealed selection -- product-name aliases will not resolve on install');
|
|
830
|
+
}
|
|
831
|
+
// The master ruvnet-primer overview is a single static runtime doc, not per-repo corpus content —
|
|
832
|
+
// nothing private to fence, so (unlike everything above) it ships from the exact code checkout.
|
|
833
|
+
cpDir(path.join(runtime, 'primer'), path.join(out, 'primer'));
|
|
834
|
+
|
|
835
|
+
// ---- algorithm step 4: copy runtime modules from the exact code checkout, retained module graph
|
|
836
|
+
const derivedTools = resolveModuleGraph(kbDir);
|
|
837
|
+
const tools = [...derivedTools, ...EXTRA_FILES];
|
|
838
|
+
console.log(`[build-bundle] module graph: ${derivedTools.length} modules from ${ENTRYPOINTS.length} entry points`);
|
|
839
|
+
for (const t of tools) cp(t, out, { required: true, from: kbDir });
|
|
840
|
+
cp(path.join(runtime, 'scripts', 'verify-bundle.mjs'), out, { required: true, from: runtime });
|
|
841
|
+
fs.mkdirSync(path.join(out, 'keys'), { recursive: true });
|
|
842
|
+
cp(path.join(runtime, 'keys', 'ruvnet-brain-signing.pub.pem'), path.join(out, 'keys'), { required: true, from: runtime });
|
|
551
843
|
|
|
552
|
-
// ----
|
|
553
|
-
|
|
554
|
-
|
|
844
|
+
// ---- LEGACY SEED pass 2: ship the externally-produced projection (retires at plan step 11) ------
|
|
845
|
+
// scripts/release-projection.mjs's CLI produced these from the exact candidate this pass is
|
|
846
|
+
// re-assembling. All three ledgers travel together — a partial projection is never publishable —
|
|
847
|
+
// and any derived-store evidence the projection carried travels with them. NOTHING is fabricated
|
|
848
|
+
// here when a file is absent: the pre-Step-3 build-bundle synthesized a concepts.sources.json whose
|
|
849
|
+
// only declared input was its own packaged output, and Step 3 deleted that on purpose.
|
|
850
|
+
if (legacySeed && legacySeedProjection.projectionDir) {
|
|
851
|
+
const projectionDir = path.resolve(legacySeedProjection.projectionDir);
|
|
852
|
+
for (const file of ['COVERAGE.json', 'CORPUS-COVERAGE.json', 'PUBLIC-RVF-GENERATIONS.json']) {
|
|
853
|
+
cp(file, out, { required: true, from: projectionDir });
|
|
854
|
+
}
|
|
855
|
+
for (const file of ['ruv-gists.sources.json', 'public-store-classes.json', 'concepts.sources.json']) {
|
|
856
|
+
cp(file, out, { from: projectionDir });
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// LEGACY SEED, either pass: the derived-store registry is a REQUIRED activation-boundary input
|
|
860
|
+
// (plugin/scripts/coverage-integrity.mjs reads it unconditionally), and the pre-Step-3/4 seed has
|
|
861
|
+
// none. What ships is the only statement that is true of this assembly — an EMPTY registry, because
|
|
862
|
+
// every derived/aggregate store was scoped out above for want of a receipt. No digest, receipt, or
|
|
863
|
+
// input list is invented. Retires at plan step 11 with the rest of this mode.
|
|
864
|
+
if (legacySeed && !fs.existsSync(path.join(out, 'public-store-classes.json'))) {
|
|
865
|
+
fs.writeFileSync(path.join(out, 'public-store-classes.json'),
|
|
866
|
+
`${JSON.stringify({ schemaVersion: 1, derived: [] }, null, 2)}\n`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// ---- algorithm steps 5-7: SOURCE, manifest, and generation ledger — generated ONCE ---------------
|
|
870
|
+
const { source, ledger, manifestEntries } = projectStoreViews({
|
|
871
|
+
selectedResults, identity: { version, sourceSnapshot }, updaterConfig,
|
|
872
|
+
});
|
|
873
|
+
fs.writeFileSync(path.join(out, 'SOURCE.json'), `${JSON.stringify(source, null, 2)}\n`);
|
|
874
|
+
fs.writeFileSync(path.join(out, 'RVF-GENERATIONS.json'), `${JSON.stringify({
|
|
875
|
+
...ledger, kind: 'ruvnet-brain-runtime-generation-ledger',
|
|
876
|
+
}, null, 2)}\n`);
|
|
877
|
+
|
|
878
|
+
// ---- manifest.json -------------------------------------------------------------------------------
|
|
879
|
+
const builtLower = new Set(discovered.map((b) => b.toLowerCase()));
|
|
880
|
+
const pendingRepos = regFlat.filter((r) => !builtLower.has(r.name.toLowerCase())).map((r) => ({ name: r.name, tier: r.tier }));
|
|
881
|
+
const now = new Date();
|
|
882
|
+
// OFFLINE by construction: `fetch: () => null` disables orgRepoCount's live `gh api` probe, so the
|
|
883
|
+
// total comes from the committed record (source 'recorded') or is honestly `unknown`. Assembly of
|
|
884
|
+
// sealed bytes never depends on, or varies with, network availability.
|
|
885
|
+
const ORG = orgRepoCount({ fetch: () => null });
|
|
886
|
+
const hasConcepts = selectedResults.some((r) => r.kind === 'derived' && r.name.toLowerCase() === 'concepts');
|
|
887
|
+
const manifest = {
|
|
888
|
+
brainVersion: version, // FIELD = bare literal; versionTag stays the v-prefixed Release tag
|
|
889
|
+
generated: now.toISOString(),
|
|
890
|
+
generatedHuman: now.toUTCString(),
|
|
891
|
+
coverage: { built: manifestEntries.length, catalogued: regFlat.length, orgTotalApprox: ORG.count, orgTotalSource: ORG.source, orgTotalAt: ORG.at, pending: pendingRepos.length },
|
|
892
|
+
crossRepoTool: { mcp: 'forge-mcp-all.mjs', cli: 'forge-ask-all.mjs', tool: 'search_ruvnet' },
|
|
893
|
+
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,
|
|
894
|
+
builtRepos: manifestEntries,
|
|
895
|
+
pendingRepos,
|
|
896
|
+
};
|
|
897
|
+
fs.writeFileSync(path.join(out, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
898
|
+
|
|
899
|
+
// ---- mcp.snippet.json: cross-repo entry + one per-repo entry -----------------------------------
|
|
900
|
+
const mcpServers = {
|
|
901
|
+
'ruvnet-brain': { command: 'node', args: [path.join(out, 'forge-mcp-all.mjs')],
|
|
902
|
+
env: { KB_DIR: out, KB_MODEL_CACHE: '/ABSOLUTE/PATH/TO/models-cache (optional; speeds first run)' } },
|
|
903
|
+
};
|
|
904
|
+
for (const b of manifestEntries) {
|
|
905
|
+
mcpServers[`ruvnet-${b.name}`] = { command: 'node', args: [path.join(out, 'forge-mcp.mjs')],
|
|
906
|
+
env: { KB_DIR: out, KB_NAME: b.name } };
|
|
907
|
+
}
|
|
908
|
+
fs.writeFileSync(path.join(out, 'mcp.snippet.json'), JSON.stringify({
|
|
909
|
+
'//': '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.',
|
|
910
|
+
mcpServers,
|
|
911
|
+
}, null, 2));
|
|
912
|
+
|
|
913
|
+
// ---- README --------------------------------------------------------------------------------------
|
|
914
|
+
const repoLines = manifestEntries.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');
|
|
915
|
+
fs.writeFileSync(path.join(out, 'README.md'), `# RuvNet Brain — ${versionTag}
|
|
555
916
|
|
|
556
917
|
The source-grounded knowledge base for the RuvNet ecosystem. One question → the best answer from
|
|
557
918
|
rUv's REAL source code, across every built repo, with the file path it came from.
|
|
558
919
|
|
|
559
|
-
## Built repos (${
|
|
920
|
+
## Built repos (${manifestEntries.length}/${regFlat.length})
|
|
560
921
|
${repoLines}
|
|
561
922
|
|
|
562
923
|
## Install
|
|
@@ -585,102 +946,182 @@ node forge-ask.mjs --dir . --name ruvector --variant big --q "what is the RVF co
|
|
|
585
946
|
- See \`manifest.json\` for per-repo chunk counts, variants, grades, and pinned source SHAs.
|
|
586
947
|
`);
|
|
587
948
|
|
|
588
|
-
if (missing.length) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
949
|
+
if (missing.length) {
|
|
950
|
+
const requiredMissing = [...new Set(missing)];
|
|
951
|
+
fail(`missing required bundle files:\n ${requiredMissing.join('\n ')}`);
|
|
952
|
+
}
|
|
593
953
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
954
|
+
// ---- algorithm step 9: release coverage projection, only when a seed identity is supplied -------
|
|
955
|
+
let projection = null;
|
|
956
|
+
// seedIdentity was fully validated (or rejected) at the top of this function; a non-null value
|
|
957
|
+
// here is a complete release identity.
|
|
958
|
+
if (seedIdentity !== null && seedIdentity !== undefined) {
|
|
959
|
+
if (!corpusCoverage || !inventory) {
|
|
960
|
+
fail('a release coverage projection was requested (seedIdentity supplied) but no sealed corpus ' +
|
|
961
|
+
`coverage was found at ${coverageFile}`);
|
|
962
|
+
}
|
|
963
|
+
const publicLedger = { ...ledger, kind: 'ruvnet-brain-public-generation-ledger' };
|
|
964
|
+
const result = createReleaseProjection({
|
|
965
|
+
corpusCoverage, selectedLedger: publicLedger, identity: { version, sourceSnapshot },
|
|
966
|
+
seedIdentity, inventory,
|
|
967
|
+
});
|
|
968
|
+
fs.writeFileSync(path.join(out, 'COVERAGE.json'), `${JSON.stringify(result.releaseCoverage, null, 2)}\n`);
|
|
969
|
+
fs.writeFileSync(path.join(out, 'CORPUS-COVERAGE.json'), result.corpusCoverageBytes);
|
|
970
|
+
fs.writeFileSync(path.join(out, 'PUBLIC-RVF-GENERATIONS.json'), result.publicGenerationLedgerBytes);
|
|
971
|
+
// The binding is CARRIED IN THE RESULT, not merely performed: a caller (and
|
|
972
|
+
// tests/unit/assemble-bundle.test.mjs) can then prove the assembled tree was re-validated by the
|
|
973
|
+
// independent activation-boundary reader, rather than assuming it because the line is in the
|
|
974
|
+
// source. Deleting the call makes `projection.binding` undefined, and the proof fails.
|
|
975
|
+
projection = { ...result, binding: bindAssembledReleaseProjection({ assetsDir: out, version, sourceSnapshot }) };
|
|
976
|
+
}
|
|
977
|
+
// LEGACY SEED pass 2 (retires at plan step 11): the projection was produced OUT of process by
|
|
978
|
+
// scripts/release-projection.mjs against this same candidate; the runtime ledger must describe the
|
|
979
|
+
// set that projection bound, and the assembled tree is then proven by the same validator the
|
|
980
|
+
// in-process path uses. Same proof, different producer — never a weaker one.
|
|
981
|
+
if (legacySeed && legacySeedProjection.projectionDir) {
|
|
982
|
+
const publicLedgerFile = path.join(out, 'PUBLIC-RVF-GENERATIONS.json');
|
|
983
|
+
const publicLedger = JSON.parse(fs.readFileSync(publicLedgerFile, 'utf8'));
|
|
984
|
+
if (publicLedger.schemaVersion !== 2 || publicLedger.kind !== 'ruvnet-brain-public-generation-ledger'
|
|
985
|
+
|| publicLedger.brainVersion !== version || publicLedger.releaseTag !== versionTag
|
|
986
|
+
|| publicLedger.sourceSnapshot !== sourceSnapshot) {
|
|
987
|
+
fail('the legacy release projection\'s public ledger does not bind this release identity');
|
|
988
|
+
}
|
|
989
|
+
fs.writeFileSync(path.join(out, 'RVF-GENERATIONS.json'), `${JSON.stringify({
|
|
990
|
+
...publicLedger, kind: 'ruvnet-brain-runtime-generation-ledger',
|
|
991
|
+
}, null, 2)}\n`);
|
|
992
|
+
projection = { releaseCoverage: JSON.parse(fs.readFileSync(path.join(out, 'COVERAGE.json'), 'utf8')),
|
|
993
|
+
binding: bindAssembledReleaseProjection({ assetsDir: out, version, sourceSnapshot }) };
|
|
994
|
+
}
|
|
600
995
|
|
|
601
|
-
// ----
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const relative = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
610
|
-
if (entry.isDirectory()) collectArchiveFiles(path.join(dir, entry.name), relative);
|
|
611
|
-
else if (entry.isFile()) archiveFiles.push(relative);
|
|
996
|
+
// ---- algorithm step 10: validate the assembled tree, write ARCHIVE-MANIFEST.json, ONE zip --------
|
|
997
|
+
const archiveFiles = [];
|
|
998
|
+
function collectArchiveFiles(dir, prefix = '') {
|
|
999
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
1000
|
+
const relative = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
1001
|
+
if (entry.isDirectory()) collectArchiveFiles(path.join(dir, entry.name), relative);
|
|
1002
|
+
else if (entry.isFile()) archiveFiles.push(relative);
|
|
1003
|
+
}
|
|
612
1004
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1005
|
+
collectArchiveFiles(out);
|
|
1006
|
+
archiveFiles.sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
1007
|
+
function archiveIdentity(relative) {
|
|
1008
|
+
const file = path.join(out, relative);
|
|
1009
|
+
const hash = crypto.createHash('sha256');
|
|
1010
|
+
const fd = fs.openSync(file, 'r');
|
|
1011
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
1012
|
+
try {
|
|
1013
|
+
let bytes;
|
|
1014
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
|
|
1015
|
+
} finally { fs.closeSync(fd); }
|
|
1016
|
+
return { path: relative.split(path.sep).join('/'), sha256: hash.digest('hex'), bytes: fs.statSync(file).size };
|
|
1017
|
+
}
|
|
1018
|
+
const archiveManifest = {
|
|
1019
|
+
schemaVersion: 1,
|
|
1020
|
+
kind: 'ruvnet-brain-archive-manifest',
|
|
1021
|
+
version, releaseTag: versionTag,
|
|
1022
|
+
fileCount: archiveFiles.length,
|
|
1023
|
+
totalBytes: archiveFiles.reduce((total, file) => total + fs.statSync(path.join(out, file)).size, 0),
|
|
1024
|
+
files: archiveFiles.map(archiveIdentity),
|
|
1025
|
+
};
|
|
1026
|
+
fs.writeFileSync(path.join(out, 'ARCHIVE-MANIFEST.json'), `${JSON.stringify(archiveManifest, null, 2)}\n`);
|
|
1027
|
+
archiveFiles.push('ARCHIVE-MANIFEST.json');
|
|
1028
|
+
archiveFiles.sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
1029
|
+
// Required proof 5: exactly ONE zip-creation call per assembly — no repeated build/project/build.
|
|
1030
|
+
const zipped = process.platform === 'win32'
|
|
1031
|
+
? spawnSync('powershell.exe', [
|
|
1032
|
+
'-NoProfile', '-NonInteractive', '-Command',
|
|
1033
|
+
`Compress-Archive -Path '${out.replaceAll("'", "''")}\\*' -DestinationPath '${ZIP.replaceAll("'", "''")}' -Force`,
|
|
1034
|
+
], { encoding: 'utf8' })
|
|
1035
|
+
: spawnSync('zip', ['-q', '-X', ZIP, ...archiveFiles], {
|
|
1036
|
+
cwd: out, encoding: 'utf8', env: { ...process.env, COPYFILE_DISABLE: '1' },
|
|
1037
|
+
});
|
|
1038
|
+
if (zipped.status !== 0 || !fs.existsSync(ZIP)) {
|
|
1039
|
+
fail(`could not create exact release ZIP (${zipped.stderr || zipped.error?.message || `exit ${zipped.status}`})`);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const extracted = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-brain-release-audit-'));
|
|
623
1043
|
try {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
cwd: OUT,
|
|
650
|
-
encoding: 'utf8',
|
|
651
|
-
env: { ...process.env, COPYFILE_DISABLE: '1' },
|
|
652
|
-
});
|
|
653
|
-
if (zipped.status !== 0 || !fs.existsSync(ZIP)) {
|
|
654
|
-
console.error(`[build-bundle] FATAL: could not create exact release ZIP (${zipped.stderr || zipped.error?.message || `exit ${zipped.status}`})`);
|
|
655
|
-
process.exit(1);
|
|
1044
|
+
const { extractZip } = await import('../kb/zip-extract.mjs');
|
|
1045
|
+
await extractZip(ZIP, extracted);
|
|
1046
|
+
const packagedRvfs = fs.readdirSync(extracted)
|
|
1047
|
+
.filter((name) => name.endsWith('.rvf')).sort().map((name) => path.join(extracted, name));
|
|
1048
|
+
const packagedAudit = await auditRvfIndexes(packagedRvfs);
|
|
1049
|
+
const packagedFailures = packagedAudit.filter(({ state }) => state !== 'PASS');
|
|
1050
|
+
if (packagedFailures.length) {
|
|
1051
|
+
fail(`extracted release artifact contains RVFs without persisted HNSW indexes:\n${packagedFailures
|
|
1052
|
+
.map((r) => ` ${path.basename(r.path)} vectors=${r.totalVectors ?? '?'}`).join('\n')}`);
|
|
1053
|
+
}
|
|
1054
|
+
console.log(`[build-bundle] exact archive proof: ${packagedRvfs.length} RVFs audited after extraction; 0 index failures`);
|
|
1055
|
+
} finally {
|
|
1056
|
+
fs.rmSync(extracted, { recursive: true, force: true });
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
console.log(`\n=== build-bundle → ${path.relative(runtime, out)} (${versionTag}) ===`);
|
|
1060
|
+
console.log(`built repos: ${manifestEntries.length}/${regFlat.length} | selected stores: ${discovered.length}`);
|
|
1061
|
+
for (const b of manifestEntries) 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)}`);
|
|
1062
|
+
console.log(`\nmanifest: ${path.join(path.relative(runtime, out), 'manifest.json')} | mcp snippet + README written.`);
|
|
1063
|
+
console.log('STATUS: assembled OK.');
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
outDir: out, zipFile: ZIP, archiveManifest,
|
|
1067
|
+
selectedStores: discovered, manifestEntries, projection,
|
|
1068
|
+
};
|
|
656
1069
|
}
|
|
657
1070
|
|
|
658
|
-
|
|
659
|
-
try {
|
|
660
|
-
const {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
1071
|
+
// ---- CLI -----------------------------------------------------------------------------------------
|
|
1072
|
+
if (((() => { try { return process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })())) {
|
|
1073
|
+
const arg = (f, d) => { const i = process.argv.indexOf(f); return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : d; };
|
|
1074
|
+
// Corpus content is a release asset, not a Git blob. A clean worktree has zero stores unless the
|
|
1075
|
+
// operator supplies the canonical external corpus directory (or has already built one into kb/,
|
|
1076
|
+
// the genuine local/self-update case — see public-inputs.mjs's own header for why that is safe).
|
|
1077
|
+
const corpusDir = path.resolve(ROOT, arg('--assets', 'kb'));
|
|
1078
|
+
const outDir = path.resolve(ROOT, arg('--out', 'dist/ruvnet-brain'));
|
|
1079
|
+
// Coverage is a SEALED INPUT, not a per-run argument: assembleBundle reads it from the one
|
|
1080
|
+
// canonical path prepareCorpusCandidate writes (runtimeRoot/data/source-coverage.json). A
|
|
1081
|
+
// `--coverage` that names any OTHER file is rejected loudly — accepting-and-ignoring it (as this
|
|
1082
|
+
// CLI briefly did) would let a caller believe a coverage it supplied was consumed when it was not.
|
|
1083
|
+
// `--projection` named the retired two-pass round-trip; it is rejected for the same reason.
|
|
1084
|
+
const canonicalCoverage = path.join(ROOT, 'data', 'source-coverage.json');
|
|
1085
|
+
const coverageFlag = process.argv.indexOf('--coverage');
|
|
1086
|
+
const coverageValue = coverageFlag >= 0 ? process.argv[coverageFlag + 1] : undefined;
|
|
1087
|
+
const coverageIsCanonical = coverageFlag < 0
|
|
1088
|
+
|| (typeof coverageValue === 'string' && path.resolve(ROOT, coverageValue) === canonicalCoverage);
|
|
1089
|
+
// --legacy-seed-projection: the ONE explicit declaration that this run consumes the pinned
|
|
1090
|
+
// PRE-Step-3/4 corpus seed through the two-pass release-projection rail (P1-c, 2026-09-14). Without
|
|
1091
|
+
// it, --coverage/--projection keep the step-5 meanings below (canonical-coverage-only / retired).
|
|
1092
|
+
// RETIRES AT PLAN STEP 11, together with assembleBundle's legacySeedProjection branch.
|
|
1093
|
+
const legacyFlag = process.argv.includes('--legacy-seed-projection');
|
|
1094
|
+
const projectionValue = arg('--projection');
|
|
1095
|
+
const legacySeedProjection = legacyFlag
|
|
1096
|
+
? { coverageFile: coverageValue === undefined ? null : path.resolve(ROOT, coverageValue),
|
|
1097
|
+
projectionDir: projectionValue === undefined ? null : path.resolve(ROOT, projectionValue) }
|
|
1098
|
+
: null;
|
|
1099
|
+
// Seed flags are an all-or-nothing SET: any present flag builds the object as given (possibly
|
|
1100
|
+
// partial), and assembleBundle rejects a partial one before doing any work.
|
|
1101
|
+
const seedTag = arg('--seed-tag');
|
|
1102
|
+
const seedSha256 = arg('--seed-sha256');
|
|
1103
|
+
const seedBytesArg = arg('--seed-bytes');
|
|
1104
|
+
const baselineReceiptSha256 = arg('--baseline-receipt-sha256');
|
|
1105
|
+
const seedIdentity = [seedTag, seedSha256, seedBytesArg, baselineReceiptSha256].some((value) => value !== undefined)
|
|
1106
|
+
? { tag: seedTag, archiveSha256: seedSha256, archiveBytes: seedBytesArg === undefined ? undefined : Number(seedBytesArg), baselineReceiptSha256 }
|
|
1107
|
+
: null;
|
|
1108
|
+
try {
|
|
1109
|
+
if (!legacyFlag && !coverageIsCanonical) {
|
|
1110
|
+
fail(`--coverage must name the canonical sealed coverage (${canonicalCoverage}); got ` +
|
|
1111
|
+
`${coverageValue === undefined ? '(no value)' : path.resolve(ROOT, coverageValue)}. Coverage is a sealed input, not a per-run argument`);
|
|
1112
|
+
}
|
|
1113
|
+
if (!legacyFlag && process.argv.includes('--projection')) {
|
|
1114
|
+
fail('--projection was retired by the step-5 consolidation: the release coverage projection is produced ' +
|
|
1115
|
+
'in-process by assembleBundle (pass --seed-tag/--seed-sha256/--seed-bytes/--baseline-receipt-sha256 instead). ' +
|
|
1116
|
+
'The pinned pre-Step-3/4 seed still needs the two-pass rail: declare it with --legacy-seed-projection');
|
|
672
1117
|
}
|
|
673
|
-
|
|
1118
|
+
await assembleBundle({
|
|
1119
|
+
corpusDir, runtimeRoot: ROOT, outDir,
|
|
1120
|
+
identity: { version: arg('--version', getVersionTag()), sourceSnapshot: arg('--source-snapshot') },
|
|
1121
|
+
seedIdentity, legacySeedProjection,
|
|
1122
|
+
});
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
console.error(`[build-bundle] FATAL: ${error.message}`);
|
|
1125
|
+
process.exitCode = 1;
|
|
674
1126
|
}
|
|
675
|
-
console.log(`[build-bundle] exact archive proof: ${packagedRvfs.length} RVFs audited after extraction; 0 index failures`);
|
|
676
|
-
} finally {
|
|
677
|
-
fs.rmSync(extracted, { recursive: true, force: true });
|
|
678
1127
|
}
|
|
679
|
-
|
|
680
|
-
// ---- report ------------------------------------------------------------------------------------
|
|
681
|
-
console.log(`\n=== build-bundle → ${path.relative(ROOT, OUT)} (${BRAIN_VERSION}) ===`);
|
|
682
|
-
console.log(`built repos: ${builtRepos.length}/${regFlat.length} | files copied: ${copied}`);
|
|
683
|
-
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)}`);
|
|
684
|
-
if (missing.length) console.log(`\n⚠ MISSING required files: ${[...new Set(missing)].join(', ')}`);
|
|
685
|
-
console.log(`\nmanifest: ${path.join(path.relative(ROOT, OUT), 'manifest.json')} | mcp snippet + README written.`);
|
|
686
|
-
console.log(missing.length ? 'STATUS: assembled WITH MISSING FILES (see above).' : 'STATUS: assembled OK.');
|