ruvnet-brain 4.3.34 → 4.3.36
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 +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/coverage-integrity.mjs +58 -2
- package/scripts/approved-runtime.mjs +247 -26
- package/scripts/build-bundle.mjs +27 -1
- package/scripts/code-release-corpus.mjs +276 -0
- package/scripts/corpus-candidate.mjs +19 -3
- package/scripts/corpus-coverage-sidecar.mjs +202 -0
- package/scripts/corpus-currency.mjs +71 -0
- package/scripts/corpus-dispatch-decision.mjs +138 -0
- package/scripts/corpus-next-seed.mjs +250 -72
- package/scripts/corpus-reconcile.mjs +448 -75
- package/scripts/corpus-store-failure.mjs +82 -0
- package/scripts/corpus-watchdog.mjs +334 -0
- package/scripts/fixture-denominator.mjs +72 -0
- package/scripts/knowledge-input-digest.mjs +163 -0
- package/scripts/oracle/repo-recall.mjs +142 -21
- package/scripts/oracle/retrieval-accuracy.mjs +74 -6
- package/scripts/public-verification-inputs.mjs +18 -2
- package/scripts/rehearse-corpus-pipeline.mjs +651 -31
- package/scripts/rehearse-seed-selection.mjs +234 -0
- package/scripts/release.mjs +163 -18
- package/scripts/retrieval-canary.mjs +42 -20
- package/scripts/single-source-check.mjs +15 -2
- package/scripts/source-coverage.mjs +19 -3
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ Created: 2026-06-29 22:36:38 EDT
|
|
|
7
7
|
|
|
8
8
|
# 🧠 RuvNet Brain
|
|
9
9
|
|
|
10
|
-
### 🧠 RuvNet Brain — [](https://github.com/stuinfla/ruvnet-brain/blob/main/plugin/.claude-plugin/plugin.json)
|
|
11
11
|
|
|
12
12
|
**A portable, source-grounded brain over Reuven Cohen's (rUv's) RuvNet stack — delivered as a Claude Code plugin that makes Claude _use_ the stack instead of fighting it.**
|
|
13
13
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.36",
|
|
4
4
|
"description": "One-command installer for RuvNet Brain \u2014 a portable, source-grounded brain over rUv's RuvNet building blocks, delivered as a Claude Code plugin so Claude uses the stack instead of fighting it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ruvnet-brain",
|
|
3
3
|
"description": "RuvNet brain transplant for Claude Code — grounds every RuvNet decision in real source across 77 rUv repositories, prefers Ruflo / RuVector-RVF / AgentDB over training-prior defaults (pgvector, Pinecone, hand-rolled cosine), and can pull in any RuvNet repo on demand. Ships a UserPromptSubmit retrieve-and-inject grounding hook and a PreToolUse write gate that refuses ungrounded rUv-product code until search_ruvnet has been consulted (ADR-0012 / ADR-067).",
|
|
4
|
-
"version": "4.3.
|
|
4
|
+
"version": "4.3.36",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Stuart Kerr"
|
|
7
7
|
},
|
|
@@ -225,6 +225,41 @@ export function generationLedgerBytes(ledger) {
|
|
|
225
225
|
return Buffer.from(`${JSON.stringify(ledger, null, 2)}\n`);
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
const isIsoOrNull = (value) => value === null || (typeof value === 'string' && Number.isFinite(Date.parse(value)));
|
|
229
|
+
const isAttemptCount = (value) => Number.isSafeInteger(value) && value >= 1;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* ADR-0091 D5: how an ELIGIBLE repository row stands in a bundle. It returns
|
|
233
|
+
* 'shipped' — the store's bytes ship: CURRENT, or STALE with a verified `carry` record (the refresh
|
|
234
|
+
* failed and the previous generation's bytes, re-hashed against the ledger, were kept);
|
|
235
|
+
* 'absent' — MISSING with a `failure` record: a store that failed with no prior bytes, excluded
|
|
236
|
+
* from the repository store set;
|
|
237
|
+
* null — anything else. A STALE/MISSING row WITHOUT its record, FAILED and UNVERIFIED stay
|
|
238
|
+
* rejected, exactly as before.
|
|
239
|
+
* The status token is never new: the distinction lives in the object (0.1.0's STALE_CARRIED and
|
|
240
|
+
* PENDING_FAILED were withdrawn).
|
|
241
|
+
*/
|
|
242
|
+
export function eligibleRepositoryStanding(row) {
|
|
243
|
+
const status = row?.status;
|
|
244
|
+
const { carry, failure } = row || {};
|
|
245
|
+
if (status === 'CURRENT') return carry === undefined && failure === undefined ? 'shipped' : null;
|
|
246
|
+
if (status === 'STALE' && carry && typeof carry === 'object' && failure === undefined) {
|
|
247
|
+
const carried = String(carry.carriedSourceCommit || '').toLowerCase();
|
|
248
|
+
const shipped = typeof carry.reason === 'string' && carry.reason.length > 0
|
|
249
|
+
&& HEX40.test(carried) && carried === String(row.artifact?.sourceCommit || '').toLowerCase()
|
|
250
|
+
&& HEX40.test(String(carry.missedUpstream || '').toLowerCase())
|
|
251
|
+
&& String(carry.missedUpstream).toLowerCase() === String(row.upstream?.sha || '').toLowerCase()
|
|
252
|
+
&& carried !== String(carry.missedUpstream).toLowerCase()
|
|
253
|
+
&& isAttemptCount(carry.attempts) && isIsoOrNull(carry.carriedCommittedAt)
|
|
254
|
+
&& row.artifact?.bytesVerified === true && row.artifact?.passagesPresent === true;
|
|
255
|
+
return shipped ? 'shipped' : null;
|
|
256
|
+
}
|
|
257
|
+
if (status === 'MISSING' && failure && typeof failure === 'object' && carry === undefined) {
|
|
258
|
+
return typeof failure.reason === 'string' && failure.reason.length > 0 && isAttemptCount(failure.attempts) ? 'absent' : null;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
228
263
|
export function validatePublicInventory({ assetsDir, coverage, ledger, installedPublicStores = null, gistReceipt = null }) {
|
|
229
264
|
const root = path.resolve(assetsDir);
|
|
230
265
|
const selected = installedPublicStores === null ? null : [...installedPublicStores].map((store) => String(store).toLowerCase());
|
|
@@ -248,11 +283,23 @@ export function validatePublicInventory({ assetsDir, coverage, ledger, installed
|
|
|
248
283
|
if (stores.some((store) => !store) || new Set(stores).size !== stores.length) throw new Error(`${family} stores are missing or duplicated`);
|
|
249
284
|
return stores.sort();
|
|
250
285
|
};
|
|
251
|
-
const
|
|
252
|
-
|
|
286
|
+
const eligibleRepos = coverage.rows.filter((row) => row.kind === 'repository' && row.disposition === 'eligible');
|
|
287
|
+
const rejected = eligibleRepos.find((row) => eligibleRepositoryStanding(row) === null);
|
|
288
|
+
if (rejected) {
|
|
289
|
+
throw new Error(`an eligible repository is not CURRENT (${rejected.artifact?.store || rejected.name}: ${rejected.status} `
|
|
290
|
+
+ 'without a verified carry/failure record)');
|
|
291
|
+
}
|
|
292
|
+
const repos = eligibleRepos.filter((row) => eligibleRepositoryStanding(row) === 'shipped');
|
|
253
293
|
const repositories = uniqueStores(repos, 'repository');
|
|
294
|
+
// A MISSING-with-failure store ships no bytes: it is in neither the repository set nor the excluded
|
|
295
|
+
// set, so a stray file under its name still fails below as an unclassified public store.
|
|
296
|
+
const absentRepositories = uniqueStores(eligibleRepos.filter((row) => eligibleRepositoryStanding(row) === 'absent'),
|
|
297
|
+
'absent repository');
|
|
254
298
|
const excludedRepositories = uniqueStores(coverage.rows.filter((row) => row.kind === 'repository'
|
|
255
299
|
&& row.disposition !== 'eligible'), 'excluded repository');
|
|
300
|
+
if (absentRepositories.some((store) => repositories.includes(store) || excludedRepositories.includes(store))) {
|
|
301
|
+
throw new Error('an absent repository store is also shipped or excluded');
|
|
302
|
+
}
|
|
256
303
|
const excludedSet = new Set(excludedRepositories);
|
|
257
304
|
if (repositories.some((store) => excludedSet.has(store))) throw new Error('eligible and excluded repository stores overlap');
|
|
258
305
|
const gists = coverage.rows.filter((row) => row.kind === 'gist' && row.disposition === 'eligible');
|
|
@@ -339,6 +386,15 @@ export function validatePublicInventory({ assetsDir, coverage, ledger, installed
|
|
|
339
386
|
throw new Error(`${store} generation record provenance is incomplete`);
|
|
340
387
|
}
|
|
341
388
|
}
|
|
389
|
+
// A carried store ships the PREVIOUS generation's bytes: the ledger must name exactly the commit
|
|
390
|
+
// the carry record says was kept, or the row describes bytes this bundle does not carry.
|
|
391
|
+
for (const row of repos) {
|
|
392
|
+
if (!row.carry) continue;
|
|
393
|
+
const store = String(row.artifact.store).toLowerCase();
|
|
394
|
+
if (String(ledger.stores[store]?.sourceCommit || '').toLowerCase() !== String(row.carry.carriedSourceCommit).toLowerCase()) {
|
|
395
|
+
throw new Error(`${store} carry record names a source commit the generation ledger does not carry`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
342
398
|
const publicStores = expected;
|
|
343
399
|
evidenceFiles.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
|
|
344
400
|
return { repositories, excludedRepositories, gistAggregate, derived, publicStores,
|
|
@@ -14,25 +14,41 @@
|
|
|
14
14
|
// "Pinning survives only through enforced equality to the approved shipped runtime and its
|
|
15
15
|
// executable hashes. Copying current-main package.json or preserving a version string alone is
|
|
16
16
|
// insufficient." So this compares every executable/runtime file's sha256 AND byte length against a
|
|
17
|
-
//
|
|
17
|
+
// inventory produced from the owner-approved shipped code artifact — and, in the other
|
|
18
18
|
// direction, refuses any executable-shaped file in the archive that the inventory does not cover, so
|
|
19
19
|
// a NEW unpinned executable cannot ride along.
|
|
20
20
|
//
|
|
21
|
-
// builderSourceSha stays
|
|
22
|
-
//
|
|
21
|
+
// builderSourceSha stays a separately-bound identity in the corpus receipt. Under ADR-0091 D3 the
|
|
22
|
+
// corpus is built at the approved release's own sourceSha, so the two are equal by construction.
|
|
23
|
+
//
|
|
24
|
+
// WHERE THE PIN COMES FROM (ADR-0091 D3, 2026-09-28). It used to be a committed file,
|
|
25
|
+
// data/approved-runtime.json, emitted by hand after a code release reached `install-verified`. That
|
|
26
|
+
// could never stay valid: under "every main commit is a release", the commit carrying a pin for
|
|
27
|
+
// release X is itself release X+1, whose archive then fails the pin for X. The committed file is gone
|
|
28
|
+
// for good. The pin is now RESOLVED AT RUN TIME from evidence GitHub already holds: the newest code
|
|
29
|
+
// release whose signed public-verification-aggregate.json verifies against the committed
|
|
30
|
+
// keys/ruvnet-brain-signing.pub.pem (verdict PASS over the 3-OS x 3-host install matrix), whose
|
|
31
|
+
// identity.bundleSha256 is the exact ruvnet-brain.zip on that release, and whose identity.sourceSha
|
|
32
|
+
// is reachable from origin/main. The pin is emitted from THAT zip's own ARCHIVE-MANIFEST.json, and the
|
|
33
|
+
// corpus is built at identity.sourceSha, so runtime equality holds by construction. It is still
|
|
34
|
+
// enforced byte for byte, both directions, by the unchanged verifyApprovedRuntime.
|
|
23
35
|
//
|
|
24
36
|
// Usage:
|
|
25
|
-
// node scripts/approved-runtime.mjs --
|
|
26
|
-
// --
|
|
37
|
+
// node scripts/approved-runtime.mjs --resolve --repo owner/name [--tag vX.Y.Z] [--main-ref origin/main] \
|
|
38
|
+
// --out <pin.json> # resolve (newest ONLY, or exactly --tag) and write the pin
|
|
39
|
+
// # exit 3 = newest release not yet install-verified
|
|
40
|
+
// # exit 1 = evidence present but invalid (loud)
|
|
27
41
|
// node scripts/approved-runtime.mjs --verify --archive-manifest <ARCHIVE-MANIFEST.json> \
|
|
28
|
-
//
|
|
42
|
+
// --pin <pin.json> # every corpus promotion
|
|
29
43
|
|
|
44
|
+
import crypto from 'node:crypto';
|
|
30
45
|
import fs from 'node:fs';
|
|
46
|
+
import os from 'node:os';
|
|
31
47
|
import path from 'node:path';
|
|
48
|
+
import { spawnSync } from 'node:child_process';
|
|
32
49
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
50
|
|
|
34
51
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
35
|
-
export const APPROVED_RUNTIME_FILE = 'data/approved-runtime.json';
|
|
36
52
|
export const APPROVED_RUNTIME_KIND = 'ruvnet-brain-approved-runtime';
|
|
37
53
|
|
|
38
54
|
// Executable/runtime surface. Extensions cover every interpretable artifact; the three exact
|
|
@@ -127,11 +143,13 @@ export function verifyApprovedRuntime({ manifest, pin } = {}) {
|
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
export function readApprovedRuntime(pinFile) {
|
|
130
|
-
|
|
146
|
+
// No committed default (ADR-0091 D3): a pin is only ever this run's own --resolve output.
|
|
147
|
+
const remedy = 'node scripts/approved-runtime.mjs --resolve --repo <owner/name> --out <pin.json>';
|
|
148
|
+
if (!pinFile) throw new Error(`no approved runtime pin supplied; resolve one from the newest install-verified code release: ${remedy}`);
|
|
149
|
+
const resolved = path.resolve(pinFile);
|
|
131
150
|
if (!fs.existsSync(resolved)) {
|
|
132
|
-
throw new Error(`no approved runtime pin at ${resolved}. Unattended corpus promotion is refused until
|
|
133
|
-
+ `
|
|
134
|
-
+ `<ARCHIVE-MANIFEST.json> --code-sha <sha> --out ${APPROVED_RUNTIME_FILE}`);
|
|
151
|
+
throw new Error(`no approved runtime pin at ${resolved}. Unattended corpus promotion is refused until one is `
|
|
152
|
+
+ `resolved from an install-verified code release: ${remedy}`);
|
|
135
153
|
}
|
|
136
154
|
return JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
137
155
|
}
|
|
@@ -156,29 +174,232 @@ export function emitApprovedRuntime({ manifest, approvedCodeSha } = {}) {
|
|
|
156
174
|
};
|
|
157
175
|
}
|
|
158
176
|
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------------------------
|
|
179
|
+
// Run-time resolution (ADR-0091 D3)
|
|
180
|
+
// ---------------------------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
export const AGGREGATE_ASSET = 'public-verification-aggregate.json';
|
|
183
|
+
export const ARCHIVE_ASSET = 'ruvnet-brain.zip';
|
|
184
|
+
export const SIGNING_PUBLIC_KEY_FILE = 'keys/ruvnet-brain-signing.pub.pem';
|
|
185
|
+
const CODE_TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
|
|
186
|
+
|
|
187
|
+
const semverDescending = (left, right) => {
|
|
188
|
+
const a = CODE_TAG.exec(left).slice(1).map(Number);
|
|
189
|
+
const b = CODE_TAG.exec(right).slice(1).map(Number);
|
|
190
|
+
return b[0] - a[0] || b[1] - a[1] || b[2] - a[2];
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
function defaultGh(args) {
|
|
194
|
+
const result = spawnSync(process.env.RUVNET_GH_COMMAND || 'gh', args,
|
|
195
|
+
{ encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, timeout: 15 * 60_000 });
|
|
196
|
+
if (result.error || result.status !== 0) {
|
|
197
|
+
throw new Error(`gh ${args.slice(0, 3).join(' ')} failed: ${String(result.error?.message || result.stderr || `exit ${result.status}`).trim().slice(0, 400)}`);
|
|
198
|
+
}
|
|
199
|
+
return result.stdout;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function defaultGit(args, { cwd = ROOT } = {}) {
|
|
203
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
204
|
+
return { status: result.status, stdout: String(result.stdout || ''), stderr: String(result.stderr || '') };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function sha256File(file) {
|
|
208
|
+
const hash = crypto.createHash('sha256');
|
|
209
|
+
const fd = fs.openSync(file, 'r');
|
|
210
|
+
try {
|
|
211
|
+
const buffer = Buffer.alloc(8 * 1024 * 1024);
|
|
212
|
+
let read;
|
|
213
|
+
while ((read = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, read));
|
|
214
|
+
} finally { fs.closeSync(fd); }
|
|
215
|
+
return hash.digest('hex');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Read the ONE ARCHIVE-MANIFEST.json out of a release zip without extracting 500 MB of corpus. */
|
|
219
|
+
export function readArchiveManifestFromZip(zipFile) {
|
|
220
|
+
const listing = spawnSync('unzip', ['-Z1', zipFile], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
221
|
+
if (listing.status !== 0) throw new Error(`cannot list ${path.basename(zipFile)}: ${String(listing.stderr).trim().slice(0, 200)}`);
|
|
222
|
+
const entries = listing.stdout.split('\n').filter((entry) => /(^|\/)ARCHIVE-MANIFEST\.json$/.test(entry));
|
|
223
|
+
if (entries.length !== 1) throw new Error(`${path.basename(zipFile)} carries ${entries.length} ARCHIVE-MANIFEST.json entries, expected exactly 1`);
|
|
224
|
+
const body = spawnSync('unzip', ['-p', zipFile, entries[0]], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
|
|
225
|
+
if (body.status !== 0) throw new Error(`cannot read ${entries[0]}: ${String(body.stderr).trim().slice(0, 200)}`);
|
|
226
|
+
return JSON.parse(body.stdout);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The newest code release exists but carries NO public-verification aggregate yet: install
|
|
231
|
+
* verification has not finished (or never ran). That is "not yet verified", not a fault — the nightly
|
|
232
|
+
* stands down cleanly on it. It is the ONLY resolution failure that is not loud.
|
|
233
|
+
*/
|
|
234
|
+
export class ApprovedRuntimeNotYetVerified extends Error {
|
|
235
|
+
constructor(tag, message) {
|
|
236
|
+
super(message);
|
|
237
|
+
this.name = 'ApprovedRuntimeNotYetVerified';
|
|
238
|
+
this.code = 'APPROVED_RUNTIME_NOT_YET_VERIFIED';
|
|
239
|
+
this.tag = tag;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** CLI exit code for ApprovedRuntimeNotYetVerified; every other resolution failure exits 1. */
|
|
243
|
+
export const EXIT_NOT_YET_VERIFIED = 3;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Judge ONE code release. Returns the resolution, or throws a reason that names exactly which piece of
|
|
247
|
+
* evidence is missing or wrong. Order is cheapest-first: the 1 MB signed aggregate and git ancestry are
|
|
248
|
+
* checked before the ~555 MB archive is downloaded at all.
|
|
249
|
+
*/
|
|
250
|
+
function judgeCodeRelease({ repo, tag, gh, git, mainRef, publicKey, verifyAggregate, downloadAsset, readArchiveManifest, scratch }) {
|
|
251
|
+
const release = JSON.parse(gh(['api', `repos/${repo}/releases/tags/${tag}`]));
|
|
252
|
+
if (release.draft || release.prerelease) throw new Error('release is a draft or prerelease');
|
|
253
|
+
const assets = new Map((release.assets || []).map((asset) => [asset.name, asset]));
|
|
254
|
+
if (!assets.has(AGGREGATE_ASSET)) {
|
|
255
|
+
throw new ApprovedRuntimeNotYetVerified(tag, `code release ${tag} has no ${AGGREGATE_ASSET} yet — `
|
|
256
|
+
+ 'it has not reached install-verified, so there is no approved runtime to build at');
|
|
257
|
+
}
|
|
258
|
+
if (!assets.has(ARCHIVE_ASSET)) throw new Error(`no ${ARCHIVE_ASSET} asset`);
|
|
259
|
+
|
|
260
|
+
const aggregateFile = downloadAsset({ repo, tag, name: AGGREGATE_ASSET, dir: scratch });
|
|
261
|
+
let aggregate;
|
|
262
|
+
try { aggregate = JSON.parse(fs.readFileSync(aggregateFile, 'utf8')); }
|
|
263
|
+
catch (error) { throw new Error(`${AGGREGATE_ASSET} is not JSON (${error.message})`); }
|
|
264
|
+
// Signature over canonical JSON with the COMMITTED trust root, digest, and a full rebuild of the
|
|
265
|
+
// aggregate from its nine raw leaves — the same verifier the release finalizer uses.
|
|
266
|
+
verifyAggregate(aggregate, publicKey);
|
|
267
|
+
const identity = aggregate.identity || {};
|
|
268
|
+
if (aggregate.verdict !== 'PASS') throw new Error(`aggregate verdict is ${aggregate.verdict}, not PASS`);
|
|
269
|
+
if (identity.tag !== tag || identity.version !== tag.slice(1)) {
|
|
270
|
+
throw new Error(`aggregate identity ${identity.tag}/${identity.version} does not describe release ${tag}`);
|
|
271
|
+
}
|
|
272
|
+
const sourceSha = String(identity.sourceSha || '').toLowerCase();
|
|
273
|
+
const bundleSha256 = String(identity.bundleSha256 || '').toLowerCase();
|
|
274
|
+
if (!HEX40.test(sourceSha) || !HEX64.test(bundleSha256)) throw new Error('aggregate identity sourceSha/bundleSha256 malformed');
|
|
275
|
+
|
|
276
|
+
const apiDigest = String(assets.get(ARCHIVE_ASSET).digest || '');
|
|
277
|
+
if (apiDigest && apiDigest !== `sha256:${bundleSha256}`) {
|
|
278
|
+
throw new Error(`${ARCHIVE_ASSET} on ${tag} (${apiDigest}) is not the archive the aggregate verified (sha256:${bundleSha256})`);
|
|
279
|
+
}
|
|
280
|
+
const ancestry = git(['merge-base', '--is-ancestor', sourceSha, mainRef]);
|
|
281
|
+
if (ancestry.status === 1) throw new Error(`aggregate sourceSha ${sourceSha} is not reachable from ${mainRef}`);
|
|
282
|
+
if (ancestry.status !== 0) {
|
|
283
|
+
throw new Error(`aggregate sourceSha ${sourceSha} cannot be checked against ${mainRef} (${ancestry.stderr.trim().slice(0, 200) || `exit ${ancestry.status}`}); is the clone complete?`);
|
|
284
|
+
}
|
|
285
|
+
// The corpus is built AT sourceSha, and build-bundle.mjs stamps the archive with package.json's
|
|
286
|
+
// version there. Prove that equals the approved release, or the pin could never match the build.
|
|
287
|
+
const pkg = git(['show', `${sourceSha}:package.json`]);
|
|
288
|
+
let pkgVersion = null;
|
|
289
|
+
try { pkgVersion = JSON.parse(pkg.stdout).version; } catch { /* reported below */ }
|
|
290
|
+
if (pkg.status !== 0 || pkgVersion !== identity.version) {
|
|
291
|
+
throw new Error(`package.json at ${sourceSha.slice(0, 12)} is ${pkgVersion ?? 'unreadable'}, not ${identity.version}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const zipFile = downloadAsset({ repo, tag, name: ARCHIVE_ASSET, dir: scratch });
|
|
295
|
+
const actual = sha256File(zipFile);
|
|
296
|
+
if (actual !== bundleSha256) throw new Error(`downloaded ${ARCHIVE_ASSET} is ${actual}, not the verified ${bundleSha256}`);
|
|
297
|
+
const manifest = readArchiveManifest(zipFile);
|
|
298
|
+
if (manifest.version !== identity.version || manifest.releaseTag !== tag) {
|
|
299
|
+
throw new Error(`archive manifest is ${manifest.releaseTag}, not ${tag}`);
|
|
300
|
+
}
|
|
301
|
+
const pin = emitApprovedRuntime({ manifest, approvedCodeSha: sourceSha });
|
|
302
|
+
return {
|
|
303
|
+
pin,
|
|
304
|
+
release: { tag, version: identity.version, sourceSha, bundleSha256, aggregateSha256: aggregate.aggregateSha256 },
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Resolve the approved runtime pin from the NEWEST published code release (or exactly `tag`).
|
|
310
|
+
*
|
|
311
|
+
* NO FALLBACK (independent review of ADR-0091 D3, 2026-09-28). The newest vX.Y.Z must itself carry a
|
|
312
|
+
* PASS aggregate. It never walks back to an older release: an older runtime promoted as the corpus
|
|
313
|
+
* `releases/latest` over a newer live code release breaks fresh installs (version mismatch) and is
|
|
314
|
+
* refused by already-updated clients as incompatible — a self-inflicted outage, and a deliberate
|
|
315
|
+
* downgrade path for anyone able to withhold or corrupt the newest aggregate. So:
|
|
316
|
+
* - newest release has NO aggregate asset -> throws ApprovedRuntimeNotYetVerified (clean stand-down)
|
|
317
|
+
* - newest release has an aggregate that does not verify, is not PASS, or any other evidence fails
|
|
318
|
+
* -> throws a plain Error (loud failure; never skipped)
|
|
319
|
+
*/
|
|
320
|
+
export async function resolveApprovedRuntime({
|
|
321
|
+
repo, tag = null, mainRef = 'origin/main', root = ROOT,
|
|
322
|
+
gh = defaultGh, git = (args) => defaultGit(args, { cwd: root }),
|
|
323
|
+
publicKey = null,
|
|
324
|
+
// Loaded lazily: bin/install.mjs reaches this module (via installed-brain-health.mjs ->
|
|
325
|
+
// isRuntimeFile) on every customer install, and must not drag the release-verification graph in.
|
|
326
|
+
verifyAggregate = null,
|
|
327
|
+
downloadAsset = ({ repo: slug, tag: releaseTag, name, dir }) => {
|
|
328
|
+
gh(['release', 'download', releaseTag, '--repo', slug, '--pattern', name, '--dir', dir, '--clobber']);
|
|
329
|
+
return path.join(dir, name);
|
|
330
|
+
},
|
|
331
|
+
readArchiveManifest = readArchiveManifestFromZip,
|
|
332
|
+
scratchDir = null,
|
|
333
|
+
} = {}) {
|
|
334
|
+
if (!/^[^/\s]+\/[^/\s]+$/.test(String(repo || ''))) throw new Error('--repo must be owner/name');
|
|
335
|
+
if (tag !== null && !CODE_TAG.test(String(tag))) throw new Error('--tag must be vX.Y.Z');
|
|
336
|
+
const key = publicKey || crypto.createPublicKey(fs.readFileSync(path.join(root, SIGNING_PUBLIC_KEY_FILE), 'utf8'));
|
|
337
|
+
const verify = verifyAggregate
|
|
338
|
+
|| (await import('./public-verification-aggregate.mjs')).verifyPublicVerificationAggregate;
|
|
339
|
+
|
|
340
|
+
let candidate = tag;
|
|
341
|
+
if (!candidate) {
|
|
342
|
+
const listed = JSON.parse(gh(['release', 'list', '--repo', repo, '--limit', '200',
|
|
343
|
+
'--json', 'tagName,isDraft,isPrerelease']) || '[]');
|
|
344
|
+
[candidate] = listed.filter((row) => !row.isDraft && !row.isPrerelease && CODE_TAG.test(String(row.tagName || '')))
|
|
345
|
+
.map((row) => row.tagName).sort(semverDescending);
|
|
346
|
+
}
|
|
347
|
+
if (!candidate) throw new Error(`no code releases (vX.Y.Z) listed on ${repo}`);
|
|
348
|
+
|
|
349
|
+
const scratch = scratchDir || fs.mkdtempSync(path.join(os.tmpdir(), 'approved-runtime-resolve-'));
|
|
350
|
+
const dir = path.join(scratch, candidate);
|
|
351
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
352
|
+
try {
|
|
353
|
+
return judgeCodeRelease({ repo, tag: candidate, gh, git, mainRef, publicKey: key,
|
|
354
|
+
verifyAggregate: verify, downloadAsset, readArchiveManifest, scratch: dir });
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (error instanceof ApprovedRuntimeNotYetVerified) throw error;
|
|
357
|
+
throw new Error(`code release ${candidate}${tag ? '' : ' (the newest)'} carries install-verification evidence that does not hold `
|
|
358
|
+
+ `(refusing it, and refusing to fall back to an older release): ${error.message}`);
|
|
359
|
+
} finally {
|
|
360
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
361
|
+
if (!scratchDir) fs.rmSync(scratch, { recursive: true, force: true });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
159
365
|
const arg = (name, fallback) => {
|
|
160
366
|
const index = process.argv.indexOf(name);
|
|
161
367
|
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
|
|
162
368
|
};
|
|
163
369
|
|
|
164
|
-
function main() {
|
|
370
|
+
async function main() {
|
|
371
|
+
if (process.argv.includes('--resolve')) {
|
|
372
|
+
let result;
|
|
373
|
+
try {
|
|
374
|
+
result = await resolveApprovedRuntime({
|
|
375
|
+
repo: arg('--repo', process.env.GITHUB_REPOSITORY),
|
|
376
|
+
tag: arg('--tag', null),
|
|
377
|
+
mainRef: arg('--main-ref', 'origin/main'),
|
|
378
|
+
});
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error(`[approved-runtime] ${error.message}`);
|
|
381
|
+
return error instanceof ApprovedRuntimeNotYetVerified ? EXIT_NOT_YET_VERIFIED : 1;
|
|
382
|
+
}
|
|
383
|
+
const out = arg('--out');
|
|
384
|
+
if (out) {
|
|
385
|
+
fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true });
|
|
386
|
+
fs.writeFileSync(path.resolve(out), `${JSON.stringify(result.pin, null, 2)}\n`);
|
|
387
|
+
}
|
|
388
|
+
process.stdout.write(`${JSON.stringify({ ...result.release, fileCount: result.pin.fileCount })}\n`);
|
|
389
|
+
console.error(`[approved-runtime] resolved ${result.release.tag} @ ${result.release.sourceSha} `
|
|
390
|
+
+ `(${result.pin.fileCount} executable/runtime file(s) pinned from its own archive)`);
|
|
391
|
+
return 0;
|
|
392
|
+
}
|
|
393
|
+
|
|
165
394
|
const manifestFile = arg('--archive-manifest');
|
|
166
|
-
if (!
|
|
395
|
+
if (!process.argv.includes('--verify') || !manifestFile) {
|
|
396
|
+
console.error('usage: approved-runtime.mjs --resolve --repo <owner/name> [--tag vX.Y.Z] [--main-ref <ref>] --out <pin.json>\n'
|
|
397
|
+
+ ' approved-runtime.mjs --verify --archive-manifest <file> --pin <pin.json>');
|
|
398
|
+
return 2;
|
|
399
|
+
}
|
|
167
400
|
let manifest;
|
|
168
401
|
try { manifest = JSON.parse(fs.readFileSync(path.resolve(manifestFile), 'utf8')); }
|
|
169
402
|
catch (error) { console.error(`[approved-runtime] cannot read archive manifest: ${error.message}`); return 1; }
|
|
170
|
-
|
|
171
|
-
if (process.argv.includes('--emit')) {
|
|
172
|
-
let pin;
|
|
173
|
-
try { pin = emitApprovedRuntime({ manifest, approvedCodeSha: arg('--code-sha') }); }
|
|
174
|
-
catch (error) { console.error(`[approved-runtime] ${error.message}`); return 1; }
|
|
175
|
-
const out = path.resolve(arg('--out', path.join(ROOT, APPROVED_RUNTIME_FILE)));
|
|
176
|
-
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
177
|
-
fs.writeFileSync(out, `${JSON.stringify(pin, null, 2)}\n`);
|
|
178
|
-
console.log(`[approved-runtime] pinned ${pin.fileCount} executable/runtime file(s) at ${pin.releaseTag} → ${path.relative(ROOT, out)}`);
|
|
179
|
-
return 0;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
403
|
let pin;
|
|
183
404
|
try { pin = readApprovedRuntime(arg('--pin')); }
|
|
184
405
|
catch (error) { console.error(`[approved-runtime] ${error.message}`); return 1; }
|
|
@@ -193,5 +414,5 @@ function main() {
|
|
|
193
414
|
}
|
|
194
415
|
|
|
195
416
|
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
|
196
|
-
process.exitCode =
|
|
417
|
+
main().then((code) => { process.exitCode = code; });
|
|
197
418
|
}
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -62,11 +62,13 @@ import { validatePublicInventory } from './public-inventory.mjs';
|
|
|
62
62
|
import { bindAssembledReleaseProjection, createReleaseProjection } from './release-projection.mjs';
|
|
63
63
|
import { materializePublicInputs, SELECTION_FILE, validateSelectionReceipt } from './public-inputs.mjs';
|
|
64
64
|
import { isPrivate, loadPrivateSlugs, shouldFenceL2 } from './private-fence.mjs';
|
|
65
|
-
import { validateCoverageLedger } from './coverage-integrity.mjs';
|
|
65
|
+
import { eligibleRepositoryStanding, validateCoverageLedger } from './coverage-integrity.mjs';
|
|
66
66
|
// The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
|
|
67
67
|
// sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
|
|
68
68
|
import { orgRepoCount } from './org-repo-count.mjs';
|
|
69
69
|
import { assertCapabilityOnlyStore } from '../kb/capability-only.mjs';
|
|
70
|
+
import { CURRENCY_BASIS, corpusCurrencyBlock } from './corpus-currency.mjs';
|
|
71
|
+
import { loadFixture } from './oracle/repo-recall.mjs';
|
|
70
72
|
|
|
71
73
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
72
74
|
|
|
@@ -566,6 +568,8 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
|
|
|
566
568
|
// and is never rewritten here, so they are declared to validateSelectedRvfGenerations as explicitly
|
|
567
569
|
// EXCLUDED rather than left looking like unexplained extra generation records.
|
|
568
570
|
const legacyExcluded = [];
|
|
571
|
+
// The bootstrap tag the legacy pass-2 projection was produced from (manifest.corpus.generationTag).
|
|
572
|
+
let legacySeedTag = null;
|
|
569
573
|
// LEGACY SEED pass 2 (retires at plan step 11): the externally-produced release projection is the
|
|
570
574
|
// selection authority for this mode, exactly as it was before the step-5 consolidation. Scope the
|
|
571
575
|
// discovered set to the stores that projection actually bound, so the assembled tree and
|
|
@@ -597,6 +601,7 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
|
|
|
597
601
|
if (legacyCoverage?.kind !== 'ruvnet-brain-release-coverage' || !Array.isArray(legacyCoverage.rows)) {
|
|
598
602
|
fail('legacy release projection coverage is not a ruvnet-brain-release-coverage ledger');
|
|
599
603
|
}
|
|
604
|
+
legacySeedTag = typeof legacyCoverage.corpusSeed?.tag === 'string' ? legacyCoverage.corpusSeed.tag : null;
|
|
600
605
|
const projectedClasses = path.join(path.resolve(legacySeedProjection.projectionDir), 'public-store-classes.json');
|
|
601
606
|
const projectedDerived = fs.existsSync(projectedClasses)
|
|
602
607
|
? (JSON.parse(fs.readFileSync(projectedClasses, 'utf8')).derived || []).map((e) => String(e?.store || '').toLowerCase())
|
|
@@ -672,6 +677,11 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
|
|
|
672
677
|
// validatePublicInventory has already bound that ledger to the bytes on disk.
|
|
673
678
|
for (const row of corpusCoverage.rows) {
|
|
674
679
|
if (row.disposition !== 'eligible') continue;
|
|
680
|
+
// ADR-0091 D5: a MISSING row with a `failure` record ships no store at all (validatePublicInventory
|
|
681
|
+
// already proved no bytes exist under its name), so there is no ledger generation to bind. A
|
|
682
|
+
// STALE row with a `carry` record binds below exactly like a CURRENT one: its artifact digest and
|
|
683
|
+
// source generation ARE the carried bytes this ledger carries.
|
|
684
|
+
if (row.kind === 'repository' && eligibleRepositoryStanding(row) === 'absent') continue;
|
|
675
685
|
const store = String(row.artifact?.store || '');
|
|
676
686
|
const generation = ledgerIn.stores?.[store]
|
|
677
687
|
|| Object.entries(ledgerIn.stores || {}).find(([key]) => key.toLowerCase() === store.toLowerCase())?.[1];
|
|
@@ -886,10 +896,26 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
|
|
|
886
896
|
// sealed bytes never depends on, or varies with, network availability.
|
|
887
897
|
const ORG = orgRepoCount({ fetch: () => null });
|
|
888
898
|
const hasConcepts = selectedResults.some((r) => r.kind === 'derived' && r.name.toLowerCase() === 'concepts');
|
|
899
|
+
// ADR-0091 D7.1: `generated` is the ASSEMBLY time and must never be read as freshness. `corpus` says
|
|
900
|
+
// what is actually known about content currency, from the sealed coverage this archive was bound to
|
|
901
|
+
// above (null fields where nothing was observed -- never estimated). See scripts/corpus-currency.mjs.
|
|
902
|
+
const fixtureFile = path.join(dataDir, 'retrieval-query-evidence.json');
|
|
903
|
+
let fixtureStores = null;
|
|
904
|
+
if (corpusCoverage && fs.existsSync(fixtureFile)) {
|
|
905
|
+
try { fixtureStores = loadFixture(fixtureFile).questions.map((question) => question.store); }
|
|
906
|
+
catch (error) { fail(`frozen retrieval fixture is unreadable (${error.message})`); }
|
|
907
|
+
}
|
|
908
|
+
const corpusCurrency = corpusCurrencyBlock({
|
|
909
|
+
coverage: corpusCoverage,
|
|
910
|
+
basis: corpusCoverage ? CURRENCY_BASIS.SEALED : legacySeed ? CURRENCY_BASIS.LEGACY : CURRENCY_BASIS.NONE,
|
|
911
|
+
generationTag: seedIdentity?.tag ?? legacySeedTag,
|
|
912
|
+
fixtureStores,
|
|
913
|
+
});
|
|
889
914
|
const manifest = {
|
|
890
915
|
brainVersion: version, // FIELD = bare literal; versionTag stays the v-prefixed Release tag
|
|
891
916
|
generated: now.toISOString(),
|
|
892
917
|
generatedHuman: now.toUTCString(),
|
|
918
|
+
corpus: corpusCurrency,
|
|
893
919
|
coverage: { built: manifestEntries.length, catalogued: regFlat.length, orgTotalApprox: ORG.count, orgTotalSource: ORG.source, orgTotalAt: ORG.at, pending: pendingRepos.length },
|
|
894
920
|
crossRepoTool: { mcp: 'forge-mcp-all.mjs', cli: 'forge-ask-all.mjs', tool: 'search_ruvnet' },
|
|
895
921
|
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,
|