ruvnet-brain 4.3.35 → 4.3.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/.codex-plugin/plugin.json +1 -1
  5. package/plugin/hooks/codex-hooks.json +6 -1
  6. package/plugin/hooks/hook-contracts.json +6 -4
  7. package/plugin/scripts/continuity-hook-policy.mjs +10 -3
  8. package/plugin/scripts/coverage-integrity.mjs +58 -2
  9. package/plugin/scripts/hook-shim.mjs +3 -1
  10. package/plugin/scripts/session-snapshot-hook.mjs +11 -1
  11. package/plugin/scripts/turn-outcome-capture.mjs +292 -0
  12. package/scripts/build-bundle.mjs +27 -1
  13. package/scripts/code-release-corpus.mjs +276 -0
  14. package/scripts/corpus-candidate.mjs +19 -3
  15. package/scripts/corpus-coverage-sidecar.mjs +202 -0
  16. package/scripts/corpus-currency.mjs +71 -0
  17. package/scripts/corpus-dispatch-decision.mjs +138 -0
  18. package/scripts/corpus-next-seed.mjs +69 -12
  19. package/scripts/corpus-reconcile.mjs +340 -61
  20. package/scripts/corpus-store-failure.mjs +82 -0
  21. package/scripts/corpus-watchdog.mjs +334 -0
  22. package/scripts/derive-passage-content-map.mjs +71 -0
  23. package/scripts/fixture-denominator.mjs +72 -0
  24. package/scripts/knowledge-input-digest.mjs +163 -0
  25. package/scripts/oracle/repo-recall.mjs +142 -21
  26. package/scripts/public-verification-inputs.mjs +42 -4
  27. package/scripts/rehearse-corpus-pipeline.mjs +350 -20
  28. package/scripts/release-transaction-provider.mjs +29 -6
  29. package/scripts/release.mjs +156 -18
  30. package/scripts/retrieval-canary.mjs +83 -24
  31. package/scripts/retrieval-passage-identity.mjs +58 -0
  32. package/scripts/source-coverage.mjs +19 -3
  33. package/scripts/sync-census.mjs +0 -0
  34. package/scripts/wired-check.mjs +4 -0
@@ -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: loadFixture().fixtureSha256,
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({ seedDescriptor, bundleFile, receiptFile, accuracyReportFile = null, recallReportFile = null } = {}) {
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'),
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+ // scripts/corpus-coverage-sidecar.mjs — ADR-0091 D6.2: a corpus generation publishes its own sealed
3
+ // coverage, as two small release assets beside the archive, WITHOUT bumping the schema-3 receipt.
4
+ //
5
+ // Why a sidecar and not a receipt field: three readers require receipt schema 3 exactly
6
+ // (corpus-next-seed.mjs, release.mjs, corpus-candidate.mjs), and ADR-0091 D3 runs OLDER readers on the
7
+ // nightly, so a schema bump breaks the nightly in one direction or the other. New readers use the
8
+ // sidecar; old readers never look at it.
9
+ //
10
+ // Why it is needed at all (found while implementing D5): a corpus archive carries NO coverage file,
11
+ // and the receipt binds none, so nothing downstream of preparation could tell a degraded generation
12
+ // (carried / missing stores) from a fully current one. The coverage lived only in a 14-day workflow
13
+ // artifact. D6's code-release path needs the generation's sealed coverage to assemble single-pass,
14
+ // and D10's publisher-side "no degraded generation before the soak" check needs it to exist at all.
15
+ //
16
+ // CORPUS-COVERAGE.json -- the sealed ruvnet-brain-corpus-coverage ledger, exact bytes.
17
+ // coverage-receipt.json -- { generationTag, archiveSha256, archiveBytes, coverageSha256, ... }.
18
+ //
19
+ // A reader NEVER trusts the sidecar's own `degraded` summary: verifyCoverageSidecar recomputes it
20
+ // from the coverage bytes the sidecar binds.
21
+
22
+ import crypto from 'node:crypto';
23
+ import fs from 'node:fs';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+ import { eligibleRepositoryStanding, validateCoverageLedger } from '../plugin/scripts/coverage-integrity.mjs';
27
+
28
+ export const COVERAGE_ASSET = 'CORPUS-COVERAGE.json';
29
+ export const COVERAGE_RECEIPT_ASSET = 'coverage-receipt.json';
30
+ export const COVERAGE_RECEIPT_KIND = 'ruvnet-brain-corpus-coverage-receipt';
31
+ const CORPUS_TAG = /^corpus-sha256-([0-9a-f]{64})$/;
32
+ const HEX64 = /^[0-9a-f]{64}$/;
33
+
34
+ function fail(message) {
35
+ throw new Error(`[corpus-coverage-sidecar] ${message}`);
36
+ }
37
+
38
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
39
+ const lower = (value) => String(value || '').toLowerCase();
40
+
41
+ function parseCoverage(bytes) {
42
+ let coverage;
43
+ try { coverage = JSON.parse(Buffer.from(bytes).toString('utf8')); }
44
+ catch (error) { fail(`coverage is unreadable (${error.message})`); }
45
+ const checked = validateCoverageLedger(coverage);
46
+ if (coverage?.kind !== 'ruvnet-brain-corpus-coverage' || !checked.valid) {
47
+ fail(`coverage is not a valid sealed ruvnet-brain-corpus-coverage ledger (${checked.failures.join('; ') || coverage?.kind})`);
48
+ }
49
+ return coverage;
50
+ }
51
+
52
+ /**
53
+ * Every eligible row, classified by the SAME predicate the installed validator uses. Anything that is
54
+ * neither shipped nor a recorded absence is a blocker: such a coverage cannot describe a publishable
55
+ * generation at all.
56
+ */
57
+ export function coverageStanding(coverage) {
58
+ const carried = [];
59
+ const missing = [];
60
+ const blockers = [];
61
+ const shipped = [];
62
+ for (const row of coverage.rows.filter((entry) => entry.disposition === 'eligible')) {
63
+ const store = lower(row.artifact?.store);
64
+ const standing = row.kind === 'repository' ? eligibleRepositoryStanding(row)
65
+ : row.status === 'CURRENT' && row.carry === undefined && row.failure === undefined ? 'shipped' : null;
66
+ if (standing === null) blockers.push(`${row.key}:${row.status}`);
67
+ else if (standing === 'absent') missing.push(store);
68
+ else {
69
+ if (row.carry) carried.push(store);
70
+ shipped.push({ row, store });
71
+ }
72
+ }
73
+ const sorted = (values) => [...new Set(values)].sort();
74
+ return { carried: sorted(carried), missing: sorted(missing), blockers, shipped };
75
+ }
76
+
77
+ /**
78
+ * The coverage describes THIS archive, not merely "a" corpus: every shipped eligible row's measured
79
+ * RVF digest (and, for a repository, its source generation) must be exactly what the schema-3
80
+ * receipt binds for that store, and the receipt's repository stores must be exactly the shipped
81
+ * repository rows -- no store the coverage does not account for, none it claims that is absent.
82
+ */
83
+ export function bindCoverageToReceipt({ coverage, receipt }) {
84
+ const standing = coverageStanding(coverage);
85
+ if (standing.blockers.length) {
86
+ fail(`coverage has ${standing.blockers.length} eligible row(s) that are neither shipped nor a recorded absence `
87
+ + `(${standing.blockers.slice(0, 5).join(', ')})`);
88
+ }
89
+ const stores = new Map((Array.isArray(receipt?.stores) ? receipt.stores : []).map((store) => [lower(store?.name), store]));
90
+ for (const { row, store } of standing.shipped) {
91
+ const bound = stores.get(store);
92
+ if (!bound) fail(`coverage row ${row.key} names store ${store}, which the receipt does not bind`);
93
+ const rvf = (bound.files || []).find((file) => lower(file?.file) === `${store}.big.rvf`);
94
+ if (!rvf || lower(rvf.sha256) !== lower(row.artifact?.rvfSha256)) {
95
+ fail(`coverage row ${row.key} was measured against different ${store} RVF bytes than the receipt binds`);
96
+ }
97
+ if (row.kind === 'repository' && lower(bound.sourceCommit) !== lower(row.artifact?.sourceCommit)) {
98
+ fail(`coverage row ${row.key} records a different ${store} source generation than the receipt`);
99
+ }
100
+ }
101
+ const shippedRepositories = new Set(standing.shipped.filter(({ row }) => row.kind === 'repository').map(({ store }) => store));
102
+ const receiptRepositories = [...stores.values()].filter((store) => store?.kind === 'repository').map((store) => lower(store.name));
103
+ const unaccounted = receiptRepositories.filter((store) => !shippedRepositories.has(store)).sort();
104
+ if (unaccounted.length) fail(`receipt binds repository store(s) the coverage does not ship: ${unaccounted.join(', ')}`);
105
+ const shippedAbsent = standing.missing.filter((store) => stores.has(store));
106
+ if (shippedAbsent.length) fail(`coverage records store(s) as MISSING that the receipt ships: ${shippedAbsent.join(', ')}`);
107
+ return { carried: standing.carried, missing: standing.missing };
108
+ }
109
+
110
+ export function createCoverageReceipt({ generationTag, archiveSha256, archiveBytes, coverageBytes }) {
111
+ const digestMatch = CORPUS_TAG.exec(String(generationTag || ''));
112
+ if (!digestMatch || digestMatch[1] !== archiveSha256) fail('generation tag must be corpus-sha256-<the archive sha256>');
113
+ if (!Number.isSafeInteger(archiveBytes) || archiveBytes < 1) fail('archive byte length is malformed');
114
+ const coverage = parseCoverage(coverageBytes);
115
+ const { carried, missing } = coverageStanding(coverage);
116
+ return {
117
+ schemaVersion: 1,
118
+ kind: COVERAGE_RECEIPT_KIND,
119
+ generationTag,
120
+ archiveSha256,
121
+ archiveBytes,
122
+ coverageFile: COVERAGE_ASSET,
123
+ coverageSha256: sha256(coverageBytes),
124
+ coverageBytes: Buffer.byteLength(coverageBytes),
125
+ coverageGeneration: coverage.coverageGeneration,
126
+ degraded: { carried, missing },
127
+ };
128
+ }
129
+
130
+ /**
131
+ * The reader. Verifies the sidecar names THIS generation and THESE coverage bytes, re-validates the
132
+ * coverage ledger, and recomputes the degraded summary rather than trusting the sidecar's copy.
133
+ */
134
+ export function verifyCoverageSidecar({ sidecar, coverageBytes, generationTag, archiveSha256, archiveBytes = null }) {
135
+ const keys = ['archiveBytes', 'archiveSha256', 'coverageBytes', 'coverageFile', 'coverageGeneration', 'coverageSha256',
136
+ 'degraded', 'generationTag', 'kind', 'schemaVersion'];
137
+ if (!sidecar || typeof sidecar !== 'object' || JSON.stringify(Object.keys(sidecar).sort()) !== JSON.stringify(keys)
138
+ || sidecar.schemaVersion !== 1 || sidecar.kind !== COVERAGE_RECEIPT_KIND) fail('coverage receipt shape is not recognized');
139
+ if (sidecar.generationTag !== generationTag || !HEX64.test(String(archiveSha256 || '')) || sidecar.archiveSha256 !== archiveSha256
140
+ || (archiveBytes !== null && sidecar.archiveBytes !== archiveBytes)) {
141
+ fail(`coverage receipt names ${sidecar.generationTag}/${sidecar.archiveSha256}, not ${generationTag}/${archiveSha256}`);
142
+ }
143
+ if (sidecar.coverageFile !== COVERAGE_ASSET || sidecar.coverageSha256 !== sha256(coverageBytes)
144
+ || sidecar.coverageBytes !== Buffer.byteLength(coverageBytes)) {
145
+ fail('coverage bytes are not the ones the coverage receipt binds');
146
+ }
147
+ const coverage = parseCoverage(coverageBytes);
148
+ if (sidecar.coverageGeneration !== coverage.coverageGeneration) fail('coverage receipt names another coverage generation');
149
+ const { carried, missing, blockers } = coverageStanding(coverage);
150
+ if (blockers.length) fail(`coverage has eligible row(s) that are neither shipped nor a recorded absence (${blockers.slice(0, 5).join(', ')})`);
151
+ if (JSON.stringify(sidecar.degraded) !== JSON.stringify({ carried, missing })) {
152
+ fail('coverage receipt\'s degraded summary disagrees with the coverage it binds');
153
+ }
154
+ return { coverage, degraded: { carried, missing } };
155
+ }
156
+
157
+ /** Writes the two assets into `dir` (a fresh directory the publisher owns), returning their paths. */
158
+ export function writeCoverageAssets({ dir, coverageFile, generationTag, archiveSha256, archiveBytes }) {
159
+ const coverageBytes = fs.readFileSync(coverageFile);
160
+ const receipt = createCoverageReceipt({ generationTag, archiveSha256, archiveBytes, coverageBytes });
161
+ fs.mkdirSync(dir, { recursive: true });
162
+ const coverageOut = path.join(dir, COVERAGE_ASSET);
163
+ const receiptOut = path.join(dir, COVERAGE_RECEIPT_ASSET);
164
+ fs.writeFileSync(coverageOut, coverageBytes, { flag: 'wx' });
165
+ fs.writeFileSync(receiptOut, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx' });
166
+ return { coverageFile: coverageOut, receiptFile: receiptOut, receipt };
167
+ }
168
+
169
+ const arg = (argv, name) => {
170
+ const index = argv.indexOf(name);
171
+ return index >= 0 ? argv[index + 1] : undefined;
172
+ };
173
+
174
+ // CLI: `--verify --sidecar <f> --coverage <f> --tag <t> --archive-sha256 <h> [--archive-bytes <n>]`
175
+ export function main(argv = process.argv.slice(2), { stdout = process.stdout, stderr = process.stderr } = {}) {
176
+ if (!argv.includes('--verify')) { stderr.write('usage: corpus-coverage-sidecar.mjs --verify ...\n'); return 2; }
177
+ try {
178
+ const bytes = arg(argv, '--archive-bytes');
179
+ const result = verifyCoverageSidecar({
180
+ sidecar: JSON.parse(fs.readFileSync(arg(argv, '--sidecar'), 'utf8')),
181
+ coverageBytes: fs.readFileSync(arg(argv, '--coverage')),
182
+ generationTag: arg(argv, '--tag'),
183
+ archiveSha256: arg(argv, '--archive-sha256'),
184
+ archiveBytes: bytes === undefined ? null : Number(bytes),
185
+ });
186
+ stdout.write(`${JSON.stringify({ ok: true, coverageGeneration: result.coverage.coverageGeneration, degraded: result.degraded })}\n`);
187
+ return 0;
188
+ } catch (error) {
189
+ stderr.write(`${error.message}\n`);
190
+ return 1;
191
+ }
192
+ }
193
+
194
+ function isMain() {
195
+ try {
196
+ return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ if (isMain()) process.exitCode = main();