ruvnet-brain 4.3.35 → 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/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 +69 -12
- package/scripts/corpus-reconcile.mjs +340 -61
- 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/public-verification-inputs.mjs +18 -2
- package/scripts/rehearse-corpus-pipeline.mjs +350 -20
- package/scripts/release.mjs +156 -18
- package/scripts/retrieval-canary.mjs +42 -20
- 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,
|
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,
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/code-release-corpus.mjs — ADR-0091 D6: a code release is built from the newest compatible
|
|
3
|
+
// corpus generation, through one of TWO assembly paths chosen by where the seed came from.
|
|
4
|
+
//
|
|
5
|
+
// seed origin path what runs
|
|
6
|
+
// ------------------------------- ----------------- ---------------------------------------------
|
|
7
|
+
// published-generation single-pass observe-baseline, ONE build-bundle with the
|
|
8
|
+
// (corpus-sha256-<digest>, with generation's sealed coverage. The generation's
|
|
9
|
+
// its D6.2 coverage sidecar) bytes are NEVER mutated: no capability-only
|
|
10
|
+
// refresh, no index repair. Proven afterwards.
|
|
11
|
+
// committed-bootstrap (v4.3.26, legacy-two-pass exactly the pre-D6 ci.yml sequence: capability-
|
|
12
|
+
// pre-ADR-0091 Step 4) only refresh + index repair, observe-baseline,
|
|
13
|
+
// build / release-projection / build.
|
|
14
|
+
//
|
|
15
|
+
// This is a genuine dual path, not one path with a flag to retire: the bootstrap remains the recovery
|
|
16
|
+
// seed (ADR-0091 D6.1), and it predates every sealed-input contract the single-pass assembly checks.
|
|
17
|
+
// Running refreshCapabilityOnlyStore on a sealed generation would rewrite one store and prune another
|
|
18
|
+
// AFTER the generation's coverage measured them, so the coverage would describe bytes the release
|
|
19
|
+
// does not ship. The path choice lives in ONE function (assemblyPathFor) so a test can prove neither
|
|
20
|
+
// collapses into the other.
|
|
21
|
+
//
|
|
22
|
+
// It also owns the D6.6 publish-time guard (assertNoNewerCorpusGeneration), because it is the same
|
|
23
|
+
// resolver question asked a second time.
|
|
24
|
+
//
|
|
25
|
+
// Usage (ci.yml release-qe, after downloading and extracting the resolved seed):
|
|
26
|
+
// node scripts/code-release-corpus.mjs assemble --descriptor <release-evidence/corpus-seed.json>
|
|
27
|
+
// --seed-bundle <seed zip> --assets <extracted seed dir> --evidence-dir <release-evidence>
|
|
28
|
+
// [--coverage <CORPUS-COVERAGE.json> --coverage-receipt <coverage-receipt.json>] (single-pass only)
|
|
29
|
+
|
|
30
|
+
import crypto from 'node:crypto';
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import path from 'node:path';
|
|
33
|
+
import { spawnSync } from 'node:child_process';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
import { verifyCoverageSidecar } from './corpus-coverage-sidecar.mjs';
|
|
36
|
+
import { resolveNextCorpusSeed } from './corpus-next-seed.mjs';
|
|
37
|
+
|
|
38
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
39
|
+
const CORPUS_TAG = /^corpus-sha256-([0-9a-f]{64})$/;
|
|
40
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
41
|
+
export const SINGLE_PASS = 'single-pass';
|
|
42
|
+
export const LEGACY_TWO_PASS = 'legacy-two-pass';
|
|
43
|
+
// The seed reader modules the legacy capability-only refresh embeds with -- the exact pins the pre-D6
|
|
44
|
+
// ci.yml step installed (npm package @ruvector/rvf 0.3.4: ruvector/npm/packages/rvf/package.json).
|
|
45
|
+
const LEGACY_READER_PACKAGES = ['@xenova/transformers@2.17.2', '@ruvector/rvf@0.3.4'];
|
|
46
|
+
// Every file of a store family build-bundle copies from the corpus (build-bundle.mjs step 3).
|
|
47
|
+
const STORE_SUFFIXES = ['.big.rvf', '.big.rvf.idmap.json', '.big.rvf.embed.json', '.passages.jsonl', '.meta.json',
|
|
48
|
+
'.symbols.json', '.sources.json'];
|
|
49
|
+
|
|
50
|
+
function fail(message) {
|
|
51
|
+
throw new Error(`[code-release-corpus] ${message}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function sha256File(file) {
|
|
55
|
+
const hash = crypto.createHash('sha256');
|
|
56
|
+
const fd = fs.openSync(file, 'r');
|
|
57
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
58
|
+
try {
|
|
59
|
+
let bytes;
|
|
60
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
|
|
61
|
+
} finally { fs.closeSync(fd); }
|
|
62
|
+
return hash.digest('hex');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The ONE place the path is chosen. Anything that is neither shape is refused, never defaulted. */
|
|
66
|
+
export function assemblyPathFor(descriptor) {
|
|
67
|
+
if (descriptor?.origin === 'published-generation') {
|
|
68
|
+
const match = CORPUS_TAG.exec(String(descriptor.tag || ''));
|
|
69
|
+
if (!match || match[1] !== descriptor.sha256) fail(`generation seed tag ${descriptor.tag} does not name its own digest`);
|
|
70
|
+
if (!descriptor.coverage || !HEX64.test(String(descriptor.coverage.sha256 || ''))) {
|
|
71
|
+
fail(`generation seed ${descriptor.tag} was resolved without its coverage sidecar; resolve with --require-coverage`);
|
|
72
|
+
}
|
|
73
|
+
return SINGLE_PASS;
|
|
74
|
+
}
|
|
75
|
+
if (descriptor?.origin === 'committed-bootstrap') {
|
|
76
|
+
if (CORPUS_TAG.test(String(descriptor.tag || ''))) fail('a committed bootstrap seed must be the pinned pre-ADR-0091 tag, not a generation tag');
|
|
77
|
+
return LEGACY_TWO_PASS;
|
|
78
|
+
}
|
|
79
|
+
fail(`resolved seed has no recognised origin (${JSON.stringify(descriptor?.origin ?? null)}); `
|
|
80
|
+
+ 'it must come from scripts/corpus-next-seed.mjs');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Digest of every regular file under `dir` -- the "was the sealed generation touched?" proof. */
|
|
84
|
+
export function treeDigest(dir) {
|
|
85
|
+
const rows = [];
|
|
86
|
+
const walk = (current, prefix) => {
|
|
87
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
88
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
89
|
+
const file = path.join(current, entry.name);
|
|
90
|
+
if (entry.isDirectory()) walk(file, relative);
|
|
91
|
+
else if (entry.isFile()) rows.push(`${relative}\0${sha256File(file)}`);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
walk(path.resolve(dir), '');
|
|
95
|
+
rows.sort();
|
|
96
|
+
return { files: rows.length, sha256: crypto.createHash('sha256').update(rows.join('\n')).digest('hex') };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ADR-0091 V6a: the assembled archive ships the generation's store bytes, unchanged. The store set
|
|
101
|
+
* must equal the generation's ledger, and every store-family file in the assembly must be
|
|
102
|
+
* byte-identical to the generation's copy.
|
|
103
|
+
*/
|
|
104
|
+
export function assertStoresUnmutated({ seedDir, bundleDir }) {
|
|
105
|
+
const readLedger = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'RVF-GENERATIONS.json'), 'utf8'));
|
|
106
|
+
const seedStores = Object.keys(readLedger(seedDir).stores || {}).map((name) => name.toLowerCase()).sort();
|
|
107
|
+
const bundleStores = Object.keys(readLedger(bundleDir).stores || {}).sort();
|
|
108
|
+
if (JSON.stringify(bundleStores.map((name) => name.toLowerCase())) !== JSON.stringify(seedStores)) {
|
|
109
|
+
fail(`assembled store set differs from the generation's (${bundleStores.length} vs ${seedStores.length})`);
|
|
110
|
+
}
|
|
111
|
+
let compared = 0;
|
|
112
|
+
const changed = [];
|
|
113
|
+
for (const store of bundleStores) {
|
|
114
|
+
for (const suffix of STORE_SUFFIXES) {
|
|
115
|
+
const shipped = path.join(bundleDir, `${store}${suffix}`);
|
|
116
|
+
if (!fs.existsSync(shipped)) continue;
|
|
117
|
+
const sealed = path.join(seedDir, `${store}${suffix}`);
|
|
118
|
+
if (!fs.existsSync(sealed) || sha256File(sealed) !== sha256File(shipped)) changed.push(`${store}${suffix}`);
|
|
119
|
+
compared += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (changed.length) fail(`assembly changed ${changed.length} store file(s) the generation sealed: ${changed.slice(0, 5).join(', ')}`);
|
|
123
|
+
return { stores: bundleStores.length, files: compared };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const defaultRun = (command, args, options = {}) => spawnSync(command, args, { stdio: 'inherit', ...options });
|
|
127
|
+
|
|
128
|
+
function checkedRun(run, label, command, args, options) {
|
|
129
|
+
const result = run(command, args, options) || {};
|
|
130
|
+
if (result.error || result.status !== 0) {
|
|
131
|
+
fail(`${label} failed (${result.error?.message || `exit ${result.status}`})`);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Assemble the release bundle into <root>/dist from the resolved seed. `run` is the process seam: the
|
|
138
|
+
* tests record it to prove which commands each path runs, and in which order.
|
|
139
|
+
*/
|
|
140
|
+
export function assembleCodeReleaseCorpus({
|
|
141
|
+
root = ROOT, descriptor, seedBundle, assetsDir, evidenceDir, coverageFile = null, coverageReceiptFile = null,
|
|
142
|
+
run = defaultRun, env = process.env,
|
|
143
|
+
} = {}) {
|
|
144
|
+
const mode = assemblyPathFor(descriptor);
|
|
145
|
+
const node = process.execPath;
|
|
146
|
+
const script = (name) => path.join(root, 'scripts', name);
|
|
147
|
+
const version = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version;
|
|
148
|
+
const assets = path.resolve(assetsDir || '');
|
|
149
|
+
const evidence = path.resolve(evidenceDir || '');
|
|
150
|
+
const baselineReceipt = path.join(evidence, 'baseline-observation-receipt.json');
|
|
151
|
+
const observeBaseline = () => {
|
|
152
|
+
checkedRun(run, 'observe-baseline', node, [script('public-verification-inputs.mjs'), 'observe-baseline',
|
|
153
|
+
'--baseline-bundle', seedBundle, '--expected-tag', descriptor.tag, '--expected-sha256', descriptor.sha256,
|
|
154
|
+
'--expected-bytes', String(descriptor.bytes), '--out', baselineReceipt], { cwd: root, env });
|
|
155
|
+
return sha256File(baselineReceipt);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
if (mode === LEGACY_TWO_PASS) {
|
|
159
|
+
// The pre-D6 ci.yml sequence, unchanged. The public ruOS input is capabilities-only and the
|
|
160
|
+
// bootstrap predates that policy, so its RVF is rebuilt from the curated summary; --repair also
|
|
161
|
+
// re-seals stale ledger hashes after index persistence (the v4.2.1 seed exposed three roots whose
|
|
162
|
+
// HNSW bytes advanced after their ledger rows were written).
|
|
163
|
+
checkedRun(run, 'install seed reader dependencies', 'npm', ['install', '--prefix', assets, '--no-save',
|
|
164
|
+
'--package-lock=false', '--ignore-scripts', ...LEGACY_READER_PACKAGES], { cwd: root, env });
|
|
165
|
+
const embedEnv = { ...env, XENOVA_PATH: path.join(assets, 'node_modules', '@xenova', 'transformers'),
|
|
166
|
+
RVF_MODULE_PATH: path.join(assets, 'node_modules') };
|
|
167
|
+
checkedRun(run, 'capability-only refresh', node, [script('refresh-capability-only-store.mjs'), '--assets', assets],
|
|
168
|
+
{ cwd: root, env: embedEnv });
|
|
169
|
+
checkedRun(run, 'index repair', node, [script('rvf-index-audit.mjs'), '--dir', assets, '--repair'], { cwd: root, env: embedEnv });
|
|
170
|
+
const baselineSha256 = observeBaseline();
|
|
171
|
+
const build = ['--version', `v${version}`, '--assets', assets, '--legacy-seed-projection'];
|
|
172
|
+
checkedRun(run, 'legacy build pass 1', node, [script('build-bundle.mjs'), ...build], { cwd: root, env });
|
|
173
|
+
const projection = path.join(path.dirname(evidence), 'release-projection');
|
|
174
|
+
checkedRun(run, 'release projection', node, [script('release-projection.mjs'), '--corpus', 'data/source-coverage.json',
|
|
175
|
+
'--assets', 'dist/ruvnet-brain', '--out', projection, '--version', version,
|
|
176
|
+
'--source-snapshot', env.GITHUB_SHA || '', '--baseline-receipt-sha256', baselineSha256,
|
|
177
|
+
'--seed-tag', descriptor.tag, '--seed-sha256', descriptor.sha256, '--seed-bytes', String(descriptor.bytes)], { cwd: root, env });
|
|
178
|
+
checkedRun(run, 'legacy build pass 2', node, [script('build-bundle.mjs'), ...build,
|
|
179
|
+
'--coverage', path.join(projection, 'COVERAGE.json'), '--projection', projection], { cwd: root, env });
|
|
180
|
+
return { mode };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// SINGLE-PASS. The coverage file must be the one the resolver bound to this generation.
|
|
184
|
+
const coverageBytes = fs.readFileSync(path.resolve(coverageFile || ''));
|
|
185
|
+
const verified = verifyCoverageSidecar({ sidecar: JSON.parse(fs.readFileSync(path.resolve(coverageReceiptFile || ''), 'utf8')),
|
|
186
|
+
coverageBytes, generationTag: descriptor.tag, archiveSha256: descriptor.sha256, archiveBytes: descriptor.bytes });
|
|
187
|
+
if (crypto.createHash('sha256').update(coverageBytes).digest('hex') !== descriptor.coverage.sha256) {
|
|
188
|
+
fail('the downloaded coverage is not the coverage the resolver sealed into the descriptor');
|
|
189
|
+
}
|
|
190
|
+
if (verified.degraded.carried.length || verified.degraded.missing.length) {
|
|
191
|
+
fail(`generation ${descriptor.tag} is degraded (${verified.degraded.carried.length} carried, `
|
|
192
|
+
+ `${verified.degraded.missing.length} missing); a code release does not ship one (ADR-0091 D10)`);
|
|
193
|
+
}
|
|
194
|
+
const before = treeDigest(assets);
|
|
195
|
+
// build-bundle reads the sealed coverage from the ONE canonical path (<root>/data/source-coverage.json),
|
|
196
|
+
// exactly as prepareCorpusCandidate does. The committed file is restored afterwards so no later
|
|
197
|
+
// release-QE step reads the generation's coverage believing it is the checkout's.
|
|
198
|
+
const canonical = path.join(root, 'data', 'source-coverage.json');
|
|
199
|
+
const committed = fs.existsSync(canonical) ? fs.readFileSync(canonical) : null;
|
|
200
|
+
try {
|
|
201
|
+
fs.writeFileSync(canonical, coverageBytes);
|
|
202
|
+
const baselineSha256 = observeBaseline();
|
|
203
|
+
checkedRun(run, 'single-pass build', node, [script('build-bundle.mjs'), '--version', `v${version}`, '--assets', assets,
|
|
204
|
+
'--coverage', canonical, '--seed-tag', descriptor.tag, '--seed-sha256', descriptor.sha256,
|
|
205
|
+
'--seed-bytes', String(descriptor.bytes), '--baseline-receipt-sha256', baselineSha256], { cwd: root, env });
|
|
206
|
+
} finally {
|
|
207
|
+
if (committed === null) fs.rmSync(canonical, { force: true });
|
|
208
|
+
else fs.writeFileSync(canonical, committed);
|
|
209
|
+
}
|
|
210
|
+
const after = treeDigest(assets);
|
|
211
|
+
if (after.sha256 !== before.sha256) fail(`the sealed generation directory was modified during assembly (${before.files} -> ${after.files} files)`);
|
|
212
|
+
const unmutated = assertStoresUnmutated({ seedDir: assets, bundleDir: path.join(root, 'dist', 'ruvnet-brain') });
|
|
213
|
+
return { mode, seedTree: before, unmutated };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* ADR-0091 D6.6. `sealed` is the descriptor release QE sealed into the payload; `resolution` is a fresh
|
|
218
|
+
* resolveNextCorpusSeed({ requireCoverage: true }) answer. Refuses on any difference, and on any
|
|
219
|
+
* resolution that could not prove there is nothing newer (a failed list, view or small download).
|
|
220
|
+
*/
|
|
221
|
+
export function checkNoNewerCorpusGeneration({ sealed, resolution }) {
|
|
222
|
+
if (!sealed?.origin || !sealed?.tag || !HEX64.test(String(sealed.sha256 || ''))) {
|
|
223
|
+
fail('the sealed corpus seed descriptor carries no resolver origin/tag/sha256; it did not come from corpus-next-seed.mjs');
|
|
224
|
+
}
|
|
225
|
+
const indeterminate = (resolution?.rejected || []).filter((row) => row.indeterminate);
|
|
226
|
+
if (indeterminate.length) {
|
|
227
|
+
fail(`cannot prove no newer corpus generation was published (${indeterminate.map((row) => `${row.tag || '(list)'}: ${row.reason}`).join('; ')})`);
|
|
228
|
+
}
|
|
229
|
+
const now = resolution?.seed;
|
|
230
|
+
if (now?.tag !== sealed.tag || now?.sha256 !== sealed.sha256) {
|
|
231
|
+
fail(`corpus generation ${now?.tag} published after this release was QE'd from ${sealed.tag}; re-run release QE`);
|
|
232
|
+
}
|
|
233
|
+
return { origin: sealed.origin, tag: sealed.tag };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function assertNoNewerCorpusGeneration({ sealedFile, repo, runtimeRoot = ROOT, run = undefined,
|
|
237
|
+
resolve = resolveNextCorpusSeed } = {}) {
|
|
238
|
+
const sealed = JSON.parse(fs.readFileSync(sealedFile, 'utf8'));
|
|
239
|
+
const resolution = await resolve({ repo, root: runtimeRoot, runtimeRoot, requireCoverage: true, ...(run ? { run } : {}) });
|
|
240
|
+
return checkNoNewerCorpusGeneration({ sealed, resolution });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const arg = (argv, name) => {
|
|
244
|
+
const index = argv.indexOf(name);
|
|
245
|
+
return index >= 0 ? argv[index + 1] : null;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export function main(argv = process.argv.slice(2)) {
|
|
249
|
+
if (argv[0] !== 'assemble') {
|
|
250
|
+
process.stderr.write('usage: code-release-corpus.mjs assemble --descriptor <f> --seed-bundle <f> --assets <d> --evidence-dir <d> [--coverage <f> --coverage-receipt <f>]\n');
|
|
251
|
+
return 2;
|
|
252
|
+
}
|
|
253
|
+
const descriptor = JSON.parse(fs.readFileSync(arg(argv, '--descriptor'), 'utf8'));
|
|
254
|
+
const result = assembleCodeReleaseCorpus({
|
|
255
|
+
descriptor,
|
|
256
|
+
seedBundle: arg(argv, '--seed-bundle'),
|
|
257
|
+
assetsDir: arg(argv, '--assets'),
|
|
258
|
+
evidenceDir: arg(argv, '--evidence-dir'),
|
|
259
|
+
coverageFile: arg(argv, '--coverage'),
|
|
260
|
+
coverageReceiptFile: arg(argv, '--coverage-receipt'),
|
|
261
|
+
});
|
|
262
|
+
process.stdout.write(`${JSON.stringify({ ok: true, seed: descriptor.tag, ...result })}\n`);
|
|
263
|
+
return 0;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isMain() {
|
|
267
|
+
try {
|
|
268
|
+
return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
269
|
+
} catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (isMain()) {
|
|
275
|
+
try { process.exitCode = main(); } catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
276
|
+
}
|
|
@@ -124,7 +124,7 @@ function normalizeBootstrapIdentity(bootstrapIdentity) {
|
|
|
124
124
|
// from bundleFile, or is one of the three inputs that are not archive-derived (builderSourceSha,
|
|
125
125
|
// bootstrapIdentity, createdAt) — verification supplies those from the receipt being checked, so a
|
|
126
126
|
// receipt can never claim archive contents that were not really shipped.
|
|
127
|
-
async function deriveCorpusCandidate({ bundleFile, builderSourceSha, bootstrapIdentity, createdAt, accuracyReportFile, recallReportFile }) {
|
|
127
|
+
async function deriveCorpusCandidate({ bundleFile, builderSourceSha, bootstrapIdentity, createdAt, accuracyReportFile, recallReportFile, coverageFile = null }) {
|
|
128
128
|
const bundle = path.resolve(bundleFile || '');
|
|
129
129
|
if (!fs.existsSync(bundle) || !fs.statSync(bundle).isFile()) fail(`bundle missing (${bundle || 'no path supplied'})`);
|
|
130
130
|
if (!HEX_SOURCE.test(builderSourceSha || '')) fail('builderSourceSha must be a 40-64 hex source identity');
|
|
@@ -145,10 +145,16 @@ async function deriveCorpusCandidate({ bundleFile, builderSourceSha, bootstrapId
|
|
|
145
145
|
// a DECLARED REDUCTION in release requirements, not a demonstration that C3 passed — so the C3
|
|
146
146
|
// report is still required to exist and still bound to these exact archive bytes, and is published
|
|
147
147
|
// alongside rather than quietly dropped.
|
|
148
|
+
// ADR-0091 D7.3: a report that claims retired questions is only accepted against this candidate's
|
|
149
|
+
// sealed coverage (the archive carries none, and the schema-3 receipt binds none), recomputed here
|
|
150
|
+
// from those bytes. With no coverage supplied, any claimed retirement is rejected.
|
|
151
|
+
const fixture = loadFixture();
|
|
148
152
|
const recall = readRecallReport({
|
|
149
153
|
reportFile: recallReportFile || `${bundle}.recall.json`,
|
|
150
154
|
archive,
|
|
151
|
-
expectedFixtureSha256:
|
|
155
|
+
expectedFixtureSha256: fixture.fixtureSha256,
|
|
156
|
+
coverageBytes: coverageFile ? fs.readFileSync(path.resolve(coverageFile)) : null,
|
|
157
|
+
fixtureStores: fixture.questions.map((question) => question.store),
|
|
152
158
|
});
|
|
153
159
|
const accuracyDiagnostic = readDiagnosticAccuracyReport({
|
|
154
160
|
reportFile: accuracyReportFile || `${bundle}.accuracy.json`,
|
|
@@ -418,6 +424,7 @@ function currentGitSha() {
|
|
|
418
424
|
|
|
419
425
|
export async function createCorpusReceipt({
|
|
420
426
|
bundleFile, builderSourceSha, bootstrapIdentity = null, receiptFile, createdAt, accuracyReportFile = null, recallReportFile = null,
|
|
427
|
+
coverageFile = null,
|
|
421
428
|
} = {}) {
|
|
422
429
|
const resolvedReceiptFile = path.resolve(receiptFile || 'dist/corpus-receipt.json');
|
|
423
430
|
const receipt = await deriveCorpusCandidate({
|
|
@@ -427,6 +434,7 @@ export async function createCorpusReceipt({
|
|
|
427
434
|
createdAt: createdAt || new Date().toISOString(),
|
|
428
435
|
accuracyReportFile,
|
|
429
436
|
recallReportFile,
|
|
437
|
+
coverageFile,
|
|
430
438
|
});
|
|
431
439
|
fs.mkdirSync(path.dirname(resolvedReceiptFile), { recursive: true });
|
|
432
440
|
fs.writeFileSync(resolvedReceiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
@@ -435,6 +443,7 @@ export async function createCorpusReceipt({
|
|
|
435
443
|
|
|
436
444
|
export async function verifyCorpusReceipt({
|
|
437
445
|
bundleFile, receiptFile, expectedBuilderSha, expectedArchiveSha256, expectedReceiptSha256, accuracyReportFile = null, recallReportFile = null,
|
|
446
|
+
coverageFile = null,
|
|
438
447
|
} = {}) {
|
|
439
448
|
const resolvedReceiptFile = path.resolve(receiptFile || '');
|
|
440
449
|
const resolvedBundleFile = path.resolve(bundleFile || '');
|
|
@@ -465,6 +474,7 @@ export async function verifyCorpusReceipt({
|
|
|
465
474
|
createdAt: receipt.createdAt,
|
|
466
475
|
accuracyReportFile,
|
|
467
476
|
recallReportFile,
|
|
477
|
+
coverageFile,
|
|
468
478
|
});
|
|
469
479
|
if (canonicalJson(derived) !== canonicalJson(receipt)) fail('receipt does not match the exact corpus archive contents');
|
|
470
480
|
return receipt;
|
|
@@ -474,7 +484,9 @@ export async function verifyCorpusReceipt({
|
|
|
474
484
|
// (corpus-sha256-<digest>) and its accompanying schema-2 candidate receipt. This never compares
|
|
475
485
|
// the external tag against the archive's own internal ARCHIVE-MANIFEST releaseTag/version — those
|
|
476
486
|
// are two independent identity domains and conflating them was the historical bug this fixes.
|
|
477
|
-
export async function verifySeedBaseline({
|
|
487
|
+
export async function verifySeedBaseline({
|
|
488
|
+
seedDescriptor, bundleFile, receiptFile, accuracyReportFile = null, recallReportFile = null, coverageFile = null,
|
|
489
|
+
} = {}) {
|
|
478
490
|
if (!seedDescriptor || typeof seedDescriptor !== 'object') fail('seed descriptor is required');
|
|
479
491
|
const { tag, sha256, bytes, sourceCommit = null, allowPinnedTag = false } = seedDescriptor;
|
|
480
492
|
const expectedSha256 = String(sha256 || '').toLowerCase();
|
|
@@ -498,6 +510,7 @@ export async function verifySeedBaseline({ seedDescriptor, bundleFile, receiptFi
|
|
|
498
510
|
receiptFile,
|
|
499
511
|
accuracyReportFile,
|
|
500
512
|
recallReportFile,
|
|
513
|
+
coverageFile,
|
|
501
514
|
expectedArchiveSha256: expectedSha256,
|
|
502
515
|
...(sourceCommit ? { expectedBuilderSha: sourceCommit } : {}),
|
|
503
516
|
});
|
|
@@ -515,6 +528,7 @@ async function main() {
|
|
|
515
528
|
const receiptFile = arg('--receipt', arg('--out', 'dist/corpus-receipt.json'));
|
|
516
529
|
const accuracyReportFile = arg('--accuracy-report');
|
|
517
530
|
const recallReportFile = arg('--recall-report');
|
|
531
|
+
const coverageFile = arg('--coverage');
|
|
518
532
|
if (mode === 'create') {
|
|
519
533
|
const bootstrapTag = arg('--bootstrap-tag');
|
|
520
534
|
const bootstrapSha256 = arg('--bootstrap-sha256');
|
|
@@ -526,6 +540,7 @@ async function main() {
|
|
|
526
540
|
receiptFile,
|
|
527
541
|
accuracyReportFile,
|
|
528
542
|
recallReportFile,
|
|
543
|
+
coverageFile,
|
|
529
544
|
});
|
|
530
545
|
console.log(JSON.stringify({
|
|
531
546
|
ok: true, mode, archive: receipt.archive, stores: receipt.storeCount, accuracyReport: receipt.accuracyReport, recall: receipt.recallSummary,
|
|
@@ -536,6 +551,7 @@ async function main() {
|
|
|
536
551
|
receiptFile,
|
|
537
552
|
accuracyReportFile,
|
|
538
553
|
recallReportFile,
|
|
554
|
+
coverageFile,
|
|
539
555
|
expectedBuilderSha: arg('--expected-builder-sha'),
|
|
540
556
|
expectedArchiveSha256: arg('--expected-archive-sha256'),
|
|
541
557
|
expectedReceiptSha256: arg('--expected-receipt-sha256'),
|