ruvnet-brain 4.3.34 → 4.3.35

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.
@@ -17,9 +17,17 @@
17
17
  // scripts/corpus-candidate.mjs --verify (CLI) and verifySeedBaseline
18
18
  // scripts/release.mjs --corpus-seed (CLI, the real future product-consumption path)
19
19
  //
20
- // WHAT IS BOUNDED, AND SAID OUT LOUD: `--repos N` (default 2) and `--gists M` (default 3). A
20
+ // WHAT IS BOUNDED, AND SAID OUT LOUD: `--repos N` (default 2) and `--gists M` (default 3). The C3
21
+ // diagnostic measures the same deterministic question sample corpus-seed.yml passes
22
+ // (`--accuracy-sample`, default C3_DIAGNOSTIC_SAMPLE_QUESTIONS; `--accuracy-sample full` for the
23
+ // whole oracle, which is ~1,164 queries against the full seed and runs well over an hour). A
21
24
  // rehearsal receipt ALWAYS carries `bounds` and the banner below so nobody can mistake a 2-store
22
- // rehearsal for a 194-store corpus build. Everything else is the production code path.
25
+ // rehearsal for a 194-store corpus build. Two further declared bounds keep the unsampled seed stores
26
+ // from being pruned (which would fail the repo-recall gate for every one of them, ADR-0091 V1): seed
27
+ // stores with a ledger sourceCommit stay in the observation FROZEN at that commit (not rebuilt, not
28
+ // pruned), and the repo-recall fixture in the disposable checkout is scoped to the repositories the
29
+ // candidate carries (seed stores with no sourceCommit cannot be frozen and are out of scope). Both are
30
+ // counted in the receipt. Everything else is the production code path.
23
31
  //
24
32
  // SAFETY — publication mutations are RECORDED, NEVER EXECUTED, and the interception is PROVEN:
25
33
  // 1. The explicit seam the code offers (`RUVNET_GH_COMMAND` / `RUVNET_GH_SCRIPT`, read by
@@ -35,7 +43,14 @@
35
43
  //
36
44
  // Usage:
37
45
  // node scripts/rehearse-corpus-pipeline.mjs [--repos 2] [--gists 3] [--generations 2]
38
- // [--receipt <file>] [--seed <local ruvnet-brain.zip>] [--keep] [--tamper <mode>]
46
+ // [--accuracy-sample <n>|full] [--receipt <file>] [--seed <local ruvnet-brain.zip>] [--keep] [--tamper <mode>]
47
+ // [--no-seed-selection] [--no-code-release-between]
48
+ //
49
+ // ADR-0091 D4 (V4): from generation 2 on, the seed is chosen by the REAL scripts/corpus-next-seed.mjs
50
+ // resolver from a local registry holding generation N (exactly the files release.mjs tried to upload)
51
+ // and two newer INCOMPATIBLE decoys, after a simulated code release has moved the checkout's runtime
52
+ // version. Every candidate's runtime surface is then checked by the real verifyApprovedRuntime against
53
+ // a pin traced to the checkout's own bytes. See scripts/rehearse-seed-selection.mjs.
39
54
  //
40
55
  // Done is an exit code, not an opinion: FAIL exits non-zero. A phase that could not run is a SKIP
41
56
  // with a stated reason, never a silent pass.
@@ -407,10 +422,33 @@ export function acquireSeed({ descriptor, repo, downloadDir, env, localCandidate
407
422
  * downstream — planning, cloning, embedding, indexing, pruning, aggregation, assembly, sealing —
408
423
  * is the production code path over that smaller universe.
409
424
  */
410
- export function boundObservation({ observation, api, repoStores, gistCount }) {
411
- const chosen = repoStores.length
412
- ? observation.repositories.rows.filter((row) => repoStores.includes(String(row.storeName || row.name).toLowerCase()))
425
+ export function boundObservation({ observation, api, repoStores, gistCount, frozenSourceCommits = null }) {
426
+ const storeOf = (row) => String(row.storeName || row.name).toLowerCase();
427
+ const sampled = repoStores.length
428
+ ? observation.repositories.rows.filter((row) => repoStores.includes(storeOf(row)))
429
+ : [];
430
+ // FROZEN AT SEED (2026-09-28, ADR-0091 V1). Observing ONLY the sampled repositories makes every
431
+ // other seed store "not observed this round", and the production prune (correctly, for a real
432
+ // nightly that always observes everything) deletes it -- after which the repo-recall gate, which
433
+ // asks one frozen question of every fixture repository, fails 180/182 with `rvf not found`. So the
434
+ // rehearsal keeps every OTHER repository the seed already carries in the observation, with its
435
+ // head pinned to the seed's own ledger sourceCommit: planReconciliation sees it CURRENT (nothing to
436
+ // rebuild) and pruneIneligibleStores sees it eligible (nothing to delete). Repositories the seed does
437
+ // not carry are dropped, so a brand-new upstream repo can never turn a minutes-long rehearsal into a
438
+ // full clone-and-embed. This rewrite is a declared rehearsal bound, recorded in the receipt; the
439
+ // pipeline's own observation, planning and pruning code is untouched.
440
+ const frozen = frozenSourceCommits
441
+ ? observation.repositories.rows
442
+ .filter((row) => !repoStores.includes(storeOf(row)) && frozenSourceCommits[storeOf(row)])
443
+ .map((row) => ({
444
+ ...row,
445
+ defaultBranchRef: {
446
+ name: row.defaultBranchRef?.name || 'main',
447
+ target: { oid: frozenSourceCommits[storeOf(row)], committedDate: null },
448
+ },
449
+ }))
413
450
  : [];
451
+ const chosen = [...sampled, ...frozen];
414
452
  const gistRows = observation.gists.rows.slice(0, gistCount);
415
453
  return api.canonicalSourceObservation({
416
454
  schemaVersion: observation.schemaVersion,
@@ -422,6 +460,41 @@ export function boundObservation({ observation, api, repoStores, gistCount }) {
422
460
  });
423
461
  }
424
462
 
463
+ /** The seed ledger's own sourceCommit for every repository store OTHER than the sampled ones —
464
+ * the commits boundObservation pins those stores to. Aggregates (ruv-gists, concepts) are rebuilt
465
+ * from nothing every round and are never repository rows, so they are excluded. */
466
+ export function frozenSeedCommits(assetsDir, repoStores) {
467
+ const ledger = JSON.parse(fs.readFileSync(path.join(assetsDir, 'RVF-GENERATIONS.json'), 'utf8'));
468
+ const out = {};
469
+ for (const [store, row] of Object.entries(ledger.stores || {})) {
470
+ const folded = store.toLowerCase();
471
+ if (['ruv-gists', 'concepts'].includes(folded) || repoStores.includes(folded)) continue;
472
+ const sha = String(row?.sourceCommit || '').toLowerCase();
473
+ if (/^[a-f0-9]{40}$/.test(sha)) out[folded] = sha;
474
+ }
475
+ return out;
476
+ }
477
+
478
+ /**
479
+ * Scope the frozen repo-recall fixture to the repositories THIS bounded candidate carries (ADR-0091
480
+ * V1). A seed store whose ledger sourceCommit is null (57 of 182 in the v4.3.26 seed) cannot be frozen
481
+ * at a commit: the real nightly rebuilds it from upstream, which is hours, not minutes. Out of the
482
+ * rehearsal's scope, it is pruned, and asking the fixture question of it could only report
483
+ * `rvf not found`. Every in-scope question is kept byte for byte. Returns the scoped fixture document
484
+ * plus exactly what was excluded, so the receipt says it out loud.
485
+ */
486
+ export function scopeRecallFixture({ fixture, inScopeStores }) {
487
+ const scope = new Set([...inScopeStores].map((store) => String(store).toLowerCase()));
488
+ const kept = {};
489
+ const excluded = [];
490
+ for (const [store, row] of Object.entries(fixture.queries || {})) {
491
+ if (scope.has(store.toLowerCase())) kept[store] = row;
492
+ else excluded.push(store);
493
+ }
494
+ if (!Object.keys(kept).length) fail('scoping the repo-recall fixture left no questions; the bounded scope carries no fixture repository');
495
+ return { fixture: { ...fixture, queries: kept }, kept: Object.keys(kept).length, excluded: excluded.sort() };
496
+ }
497
+
425
498
  /** Deterministically pick the `count` smallest ELIGIBLE repositories, measured by the observation's
426
499
  * own diskUsage, so a rehearsal clones and embeds real upstream source in minutes. Eligibility is
427
500
  * decided by the production classifier (`buildCoverage`), never by a guess here. */
@@ -548,6 +621,21 @@ export async function verifyExtractedBytesWithoutOriginals({
548
621
  // rehearse publication with the recorder, and emit candidate N as the seed for generation N+1.
549
622
  // ---------------------------------------------------------------------------------------------
550
623
 
624
+ // The rehearsal's record of one generation's reconciliation. It never reads the acquisition history
625
+ // itself: it uses the disposable checkout's own summarizeReconciliation, the same reader main() uses
626
+ // (ADR-0091 D1). Its private copy of the old `rounds` read crashed exactly where main() did.
627
+ export function recordReconciliation({ summarize, reconciliation, durationMs }) {
628
+ const summary = summarize(reconciliation);
629
+ return {
630
+ attempts: summary.attempts,
631
+ observationSha256: summary.observationSha256,
632
+ refreshed: summary.refreshed,
633
+ pruned: summary.pruned.length,
634
+ rebuilt: summary.rebuilt,
635
+ durationMs,
636
+ };
637
+ }
638
+
551
639
  async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds, recorder, tamper, log }) {
552
640
  const generation = { index, startedAt: new Date().toISOString(), seed: { tag: seed.tag, sha256: seed.sha256, bytes: seed.bytes, channel: seed.channel } };
553
641
  const assetsDir = path.join(workRoot, `assets-gen${index}`);
@@ -571,8 +659,11 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
571
659
  api.syncCorpusInputs({ root: checkoutRoot, assetsDir });
572
660
  const bootstrapIdentity = { tag: bootstrap.tag, sha256: bootstrap.sha256,
573
661
  privateFenceEvidence: api.seedPrivateFenceEvidence(assetsDir) };
574
- generation.seedImport = { assetsDir, storesInSeed: Object.keys(
575
- JSON.parse(fs.readFileSync(path.join(assetsDir, 'RVF-GENERATIONS.json'), 'utf8')).stores || {}).length };
662
+ const seedLedger = JSON.parse(fs.readFileSync(path.join(assetsDir, 'RVF-GENERATIONS.json'), 'utf8'));
663
+ generation.seedImport = { assetsDir, storesInSeed: Object.keys(seedLedger.stores || {}).length,
664
+ // normalizeExtractedCorpus refused the import unless this runtime can consume the ledger (ADR-0091 D4).
665
+ ledgerSchema: { schemaVersion: seedLedger.schemaVersion, kind: seedLedger.kind, brainVersion: seedLedger.brainVersion ?? null,
666
+ verdict: 'consumable by this runtime' } };
576
667
  log(`[gen ${index}] seed imported: ${generation.seedImport.storesInSeed} stores in the ledger`);
577
668
 
578
669
  // --- bounded observation over the REAL live source universe ---------------------------------
@@ -596,35 +687,60 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
596
687
  generation.forcedRebuild = repoStores;
597
688
  }
598
689
 
599
- const boundedObserve = () => boundObservation({ observation: observeFull(), api, repoStores, gistCount: bounds.gists });
690
+ // Every other seed store stays in scope FROZEN at its seed sourceCommit (see boundObservation), so
691
+ // the production prune leaves it alone and the repo-recall gate can ask its fixture question.
692
+ const frozenSourceCommits = frozenSeedCommits(assetsDir, repoStores);
693
+ generation.bounded.frozenAtSeed = Object.keys(frozenSourceCommits).length;
694
+ log(`[gen ${index}] ${generation.bounded.frozenAtSeed} other seed store(s) kept in scope frozen at their seed sourceCommit (not rebuilt, not pruned)`);
695
+ const boundedObserve = () => boundObservation({
696
+ observation: observeFull(), api, repoStores, gistCount: bounds.gists, frozenSourceCommits });
697
+
698
+ // The repo-recall gate asks one question per fixture repository. Scope it -- in the DISPOSABLE
699
+ // checkout only, where repo-recall, corpus-candidate --verify and release.mjs all read the same file
700
+ // -- to the repositories this bounded observation keeps. The committed original is preserved beside
701
+ // it so every generation scopes from the same source.
702
+ const fixtureFile = path.join(checkoutRoot, 'data', 'retrieval-query-evidence.json');
703
+ const originalFixtureFile = path.join(workRoot, 'retrieval-query-evidence.committed.json');
704
+ if (!fs.existsSync(originalFixtureFile)) fs.copyFileSync(fixtureFile, originalFixtureFile);
705
+ const inScope = boundObservation({ observation: full, api, repoStores, gistCount: 0, frozenSourceCommits })
706
+ .repositories.rows.map((row) => String(row.storeName || row.name));
707
+ const scoped = scopeRecallFixture({
708
+ fixture: JSON.parse(fs.readFileSync(originalFixtureFile, 'utf8')), inScopeStores: inScope });
709
+ fs.writeFileSync(fixtureFile, `${JSON.stringify(scoped.fixture, null, 2)}\n`);
710
+ generation.bounded.recallFixture = { questions: scoped.kept, excluded: scoped.excluded.length,
711
+ excludedStores: scoped.excluded,
712
+ reason: 'no seed sourceCommit to freeze at (the real nightly rebuilds these from upstream) or not observed upstream' };
713
+ log(`[gen ${index}] repo-recall fixture scoped to ${scoped.kept} in-scope repositories (${scoped.excluded.length} out of scope, listed in the receipt)`);
600
714
 
601
715
  // --- reconcile + assemble ONCE --------------------------------------------------------------
602
716
  const invocations = [];
603
717
  const recordingRun = (command, args, options = {}) => {
604
- invocations.push({ command, script: path.basename(String(args?.[0] || '')), args: args.map(String) });
605
- return spawnSync(command, args, { encoding: 'utf8', ...options });
718
+ const row = { command, script: path.basename(String(args?.[0] || '')), args: args.map(String) };
719
+ invocations.push(row);
720
+ const began = Date.now();
721
+ const result = spawnSync(command, args, { encoding: 'utf8', ...options });
722
+ row.durationMs = Date.now() - began;
723
+ row.exit = result.status ?? null;
724
+ return result;
606
725
  };
607
726
  const reconcileStart = Date.now();
608
727
  const { reconciliation, candidate } = await api.reconcileAndPrepareCorpusCandidate({
609
728
  assetsDir, workspaceDir, root: checkoutRoot, owner: bounds.owner, builderSha: bounds.builderSha,
610
729
  candidateDir, receiptFile, coverageFile: path.join(checkoutRoot, 'data', 'source-coverage.json'),
611
- bootstrapIdentity, maxRounds: bounds.maxRounds,
730
+ // reconcileAndPrepareCorpusCandidate reads `maxAttempts`; `maxRounds` was silently ignored after
731
+ // cd0f032f, so --max-rounds never reached the acquisition loop.
732
+ bootstrapIdentity, maxAttempts: bounds.maxRounds, accuracySample: bounds.accuracyQuestions,
612
733
  reconcile: (options) => api.acquireCorpusGeneration({ ...options, observe: boundedObserve }),
613
734
  prepare: (options) => api.prepareCorpusCandidate({ ...options, run: recordingRun }),
614
735
  });
615
- generation.reconciliation = {
616
- rounds: reconciliation.rounds.length,
617
- observationSha256: reconciliation.observation.observationSha256,
618
- refreshed: reconciliation.rounds.flatMap((round) => round.refreshed || []),
619
- pruned: reconciliation.rounds.flatMap((round) => round.pruned || []).length,
620
- rebuilt: reconciliation.rounds.flatMap((round) => round.rebuilt || []),
621
- durationMs: Date.now() - reconcileStart,
622
- };
736
+ generation.reconciliation = recordReconciliation({
737
+ summarize: api.summarizeReconciliation, reconciliation, durationMs: Date.now() - reconcileStart });
623
738
  const assemblyInvocations = invocations.filter((row) => row.script === 'build-bundle.mjs');
624
739
  generation.assembly = {
625
740
  assembleBundleInvocations: assemblyInvocations.length,
626
741
  receiptInvocations: invocations.filter((row) => row.script === 'corpus-candidate.mjs').length,
627
742
  bundleFile: candidate.bundleFile,
743
+ steps: invocations.map(({ script, durationMs, exit }) => ({ script, durationMs, exit })),
628
744
  };
629
745
  if (assemblyInvocations.length !== 1) {
630
746
  fail(`assembly must happen exactly once per candidate; build-bundle.mjs ran ${assemblyInvocations.length} time(s)`);
@@ -705,16 +821,120 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
705
821
  const nextSeedFile = path.join(workRoot, `seed-gen${index + 1}`, path.basename(bundleFile));
706
822
  fs.mkdirSync(path.dirname(nextSeedFile), { recursive: true });
707
823
  fs.copyFileSync(bundleFile, nextSeedFile);
824
+ // The detached C3 and repo-recall reports travel WITH the archive, exactly as release.mjs publishes
825
+ // them (zip, sig, digest, receipt, .accuracy.json, .recall.json). verifySeedBaseline re-reads both
826
+ // beside the seed, so handing over the zip alone failed every generation-2 import ("detached
827
+ // repo-recall report missing") -- unseen until 2026-09-28 because no rehearsal had reached gen 2.
828
+ for (const suffix of ['.accuracy.json', '.recall.json']) {
829
+ const report = `${bundleFile}${suffix}`;
830
+ if (!fs.existsSync(report)) fail(`candidate ${index} has no detached ${suffix} report to hand to generation ${index + 1}`);
831
+ fs.copyFileSync(report, `${nextSeedFile}${suffix}`);
832
+ }
708
833
  generation.nextSeed = { tag: corpusTag, sha256: archiveSha256, bytes: archiveBytes, file: nextSeedFile,
709
834
  contentAddressed: true };
710
835
  generation.finishedAt = new Date().toISOString();
836
+ const createRow = recordedNow.find((row) => row.args[0] === 'release' && row.args[1] === 'create' && row.args[2] === corpusTag);
711
837
  return {
712
838
  generation,
713
839
  nextSeed: { file: nextSeedFile, tag: corpusTag, sha256: archiveSha256, bytes: archiveBytes,
714
840
  channel: `candidate-generation-${index}`, allowPinnedTag: false, receiptFile: candidate.receiptFile },
841
+ // ADR-0091 D4: exactly what release.mjs tried to upload, so the local registry invents nothing.
842
+ published: { tag: corpusTag, createArgs: createRow ? createRow.args : [], receiptFile: candidate.receiptFile,
843
+ recallFile: `${bundleFile}.recall.json`, accuracyFile: `${bundleFile}.accuracy.json`, sha256: archiveSha256, bytes: archiveBytes },
715
844
  };
716
845
  }
717
846
 
847
+ // ---------------------------------------------------------------------------------------------
848
+ // ADR-0091 D4: seed selection through the real resolver (see scripts/rehearse-seed-selection.mjs)
849
+ // ---------------------------------------------------------------------------------------------
850
+
851
+ /** Candidate N, "published" into the local registry from release.mjs's own upload list, plus two
852
+ * newer incompatible decoys the resolver must walk past. */
853
+ function publishLocally({ selectionMod, registryDir, published, workRoot, index }) {
854
+ const files = selectionMod.uploadedFilesOf(published.createArgs);
855
+ const names = files.map((file) => path.basename(file)).sort();
856
+ const expectedNames = ['corpus-receipt.json', 'ruvnet-brain.zip', 'ruvnet-brain.zip.accuracy.json', 'ruvnet-brain.zip.recall.json'];
857
+ if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
858
+ fail(`release.mjs's recorded upload for generation ${index} is [${names.join(', ')}], expected [${expectedNames.join(', ')}]`);
859
+ }
860
+ const createdAtMs = Date.now();
861
+ selectionMod.registerGeneration({ registryDir, tag: published.tag, createdAt: new Date(createdAtMs).toISOString(), files,
862
+ note: `generation ${index}, from the files release.mjs --corpus-seed tried to upload` });
863
+ const decoys = selectionMod.registerIncompatibleDecoys({ registryDir, createdAtMs,
864
+ scratchDir: path.join(workRoot, `decoys-gen${index}`),
865
+ real: { sha256: published.sha256, bytes: published.bytes, receiptFile: published.receiptFile,
866
+ recallFile: published.recallFile, accuracyFile: published.accuracyFile } });
867
+ return { ...published, decoys };
868
+ }
869
+
870
+ /**
871
+ * What corpus-identity + corpus-seed.yml do on the nightly, against the local registry: resolve with
872
+ * the REAL corpus-next-seed resolver (judged by this checkout's readers), prove it skipped both decoys
873
+ * before any archive download, then "download" the chosen archive and re-check it exactly as
874
+ * corpus-seed.yml does (sha256 + bytes, repo-recall at ABSOLUTE_FLOOR, C3 binding).
875
+ */
876
+ async function selectPublishedSeed({ index, api, selectionMod, checkoutRoot, workRoot, registryDir, repoSlug, expected, receipt, phase }) {
877
+ const gh = selectionMod.registryGh(registryDir);
878
+ const selection = await api.resolveNextCorpusSeed({ repo: repoSlug, root: checkoutRoot, run: gh.run, runtimeRoot: checkoutRoot });
879
+ const consumingVersion = JSON.parse(fs.readFileSync(path.join(checkoutRoot, 'plugin', '.claude-plugin', 'plugin.json'), 'utf8')).version;
880
+ const publishedReceipt = JSON.parse(fs.readFileSync(expected.receiptFile, 'utf8'));
881
+ const record = {
882
+ generation: index,
883
+ selected: { origin: selection.seed.origin, tag: selection.seed.tag, builtUnder: selection.seed.brainVersion },
884
+ consumingRuntime: consumingVersion,
885
+ judged: selection.judged,
886
+ expects: selection.expects,
887
+ rejected: selection.rejected,
888
+ ghCalls: gh.calls,
889
+ archiveDownloadAttempts: gh.archiveDownloadAttempts().length,
890
+ // The rule D4 removed, evaluated on the generation it would have judged. Recorded, not enforced.
891
+ preD4RuntimeRule: publishedReceipt.archiveManifestVersion === consumingVersion ? 'would-accept'
892
+ : `would-REJECT (generation shipped v${publishedReceipt.archiveManifestVersion}, consuming runtime is v${consumingVersion}) and reset to the bootstrap`,
893
+ };
894
+ receipt.seedSelection.push(record);
895
+ const problems = [];
896
+ if (selection.seed.origin !== 'published-generation' || selection.seed.tag !== expected.tag) {
897
+ problems.push(`selected ${selection.seed.origin} ${selection.seed.tag}, expected the published generation ${expected.tag}`);
898
+ }
899
+ if (record.archiveDownloadAttempts !== 0) problems.push(`the resolver attempted ${record.archiveDownloadAttempts} archive download(s)`);
900
+ for (const decoy of expected.decoys) {
901
+ const row = selection.rejected.find((entry) => entry.tag === decoy.tag);
902
+ const wanted = decoy.kind === 'model-mismatch' ? /^incompatible: .*different model\/dimensions/ : /^incompatible: recall report was measured against fixture/;
903
+ if (!row || !wanted.test(row.reason)) problems.push(`decoy ${decoy.kind} was not skipped for its own reason (${row ? row.reason : 'not judged'})`);
904
+ const recallFetched = gh.downloads().some((line) => line.includes(decoy.tag) && line.includes('--pattern ruvnet-brain.zip.recall.json '));
905
+ if (decoy.kind === 'model-mismatch' && recallFetched) problems.push('the model-mismatch decoy was not rejected from its receipt alone');
906
+ if (decoy.kind === 'fixture-mismatch' && !recallFetched) problems.push('the fixture-mismatch decoy was rejected without reading its recall report');
907
+ }
908
+ if (problems.length) {
909
+ phase(`generation-${index}-seed-selection`, 'FAIL', { reason: problems.join('; ') });
910
+ fail(`seed selection for generation ${index} failed: ${problems.join('; ')}`);
911
+ }
912
+
913
+ // The download corpus-seed.yml performs, then its defence-in-depth re-check -- which by construction
914
+ // now only ever sees a generation the resolver already passed.
915
+ const dir = path.join(workRoot, `seed-gen${index}-selected`);
916
+ fs.mkdirSync(dir, { recursive: true });
917
+ for (const name of ['ruvnet-brain.zip', 'corpus-receipt.json', 'ruvnet-brain.zip.recall.json', 'ruvnet-brain.zip.accuracy.json']) {
918
+ fs.copyFileSync(path.join(registryDir, selection.seed.tag, 'assets', name), path.join(dir, name), fs.constants.COPYFILE_FICLONE);
919
+ }
920
+ const file = path.join(dir, 'ruvnet-brain.zip');
921
+ const sha256 = sha256File(file);
922
+ const bytes = fs.statSync(file).size;
923
+ if (sha256 !== selection.seed.sha256 || bytes !== selection.seed.bytes) fail(`selected seed download does not match its descriptor (${sha256}/${bytes})`);
924
+ const archive = { file: 'ruvnet-brain.zip', sha256, bytes };
925
+ api.readDiagnosticAccuracyReport({ reportFile: `${file}.accuracy.json`, archive });
926
+ const { report } = api.readRecallReport({ reportFile: `${file}.recall.json`, archive,
927
+ expectedFixtureSha256: api.loadFixture().fixtureSha256, floorValue: 0 });
928
+ record.downstreamRecheck = { repoCoverage: report.totals.repoCoverage, questions: report.totals.questions, hitTop5: report.totals.hitTop5 };
929
+ phase(`generation-${index}-seed-selection`, 'PASS', {
930
+ selected: selection.seed.tag, builtUnder: `v${selection.seed.brainVersion}`, consumingRuntime: `v${consumingVersion}`,
931
+ skipped: selection.rejected.map((row) => `${row.tag.slice(0, 26)}...: ${row.reason}`),
932
+ archiveDownloadAttempts: 0, preD4RuntimeRule: record.preD4RuntimeRule,
933
+ reason: `the real corpus-next-seed resolver walked past ${expected.decoys.length} newer incompatible generation(s) without downloading an archive and chose generation ${index - 1}, built under an older runtime` });
934
+ return { file, tag: selection.seed.tag, sha256, bytes, channel: 'published-generation-selected', allowPinnedTag: false,
935
+ receiptFile: path.join(dir, 'corpus-receipt.json'), builtUnder: selection.seed.brainVersion };
936
+ }
937
+
718
938
  // ---------------------------------------------------------------------------------------------
719
939
  // The rehearsal
720
940
  // ---------------------------------------------------------------------------------------------
@@ -730,6 +950,14 @@ export async function rehearseCorpusPipeline({
730
950
  tamperGeneration = 1,
731
951
  forceRebuild = true,
732
952
  seedOverride = null,
953
+ // ADR-0091 D4. seedSelection: generation N+1's seed is chosen by the real corpus-next-seed resolver
954
+ // from a local registry of published generations (plus incompatible decoys), not handed over
955
+ // directly. codeReleaseBetween: cut a version bump in the disposable checkout between generations,
956
+ // so the chosen seed was built under an OLDER runtime than the one consuming it.
957
+ seedSelection = true,
958
+ codeReleaseBetween = true,
959
+ // null = the checkout's own C3_DIAGNOSTIC_SAMPLE_QUESTIONS (what CI runs); 'full' = no sample.
960
+ accuracySample = null,
733
961
  keep = false,
734
962
  workRootParent = null,
735
963
  installedBrainDir = path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb'),
@@ -744,9 +972,12 @@ export async function rehearseCorpusPipeline({
744
972
  banner: BANNER,
745
973
  startedAt,
746
974
  host: { node: process.version, platform: `${process.platform}/${process.arch}` },
747
- bounds: { repos, gists, generations, maxRounds, owner, forceRebuild, tamper, tamperGeneration },
975
+ bounds: { repos, gists, generations, maxRounds, owner, forceRebuild, tamper, tamperGeneration, accuracySample,
976
+ seedSelection, codeReleaseBetween },
748
977
  phases: [],
749
978
  generations: [],
979
+ seedSelection: [],
980
+ approvedRuntime: [],
750
981
  verdict: 'FAIL',
751
982
  };
752
983
  const phase = (name, status, detail = {}) => {
@@ -817,10 +1048,19 @@ export async function rehearseCorpusPipeline({
817
1048
 
818
1049
  // ---- phase 4: load the disposable checkout's own modules ---------------------------------
819
1050
  const load = (relative) => import(pathToFileURL(path.join(checkoutRoot, relative)).href);
820
- const [reconcileMod, coverageMod, candidateMod, zipMod] = await Promise.all([
1051
+ const [reconcileMod, coverageMod, candidateMod, zipMod, accuracyMod, nextSeedMod, approvedMod, recallMod, selectionMod] = await Promise.all([
821
1052
  load('scripts/corpus-reconcile.mjs'), load('scripts/source-coverage.mjs'),
822
- load('scripts/corpus-candidate.mjs'), load('kb/zip-extract.mjs'),
1053
+ load('scripts/corpus-candidate.mjs'), load('kb/zip-extract.mjs'), load('scripts/oracle/retrieval-accuracy.mjs'),
1054
+ load('scripts/corpus-next-seed.mjs'), load('scripts/approved-runtime.mjs'), load('scripts/oracle/repo-recall.mjs'),
1055
+ load('scripts/rehearse-seed-selection.mjs'),
823
1056
  ]);
1057
+ // The same C3 question sample CI runs (ADR-0091 D2), unless the caller asked for the full audit.
1058
+ const accuracyQuestions = accuracySample === 'full' ? null
1059
+ : accuracySample == null ? accuracyMod.C3_DIAGNOSTIC_SAMPLE_QUESTIONS : Number(accuracySample);
1060
+ if (accuracyQuestions != null && (!Number.isSafeInteger(accuracyQuestions) || accuracyQuestions <= 0)) {
1061
+ fail(`--accuracy-sample must be a positive integer or 'full' (got ${accuracySample})`);
1062
+ }
1063
+ receipt.bounds.accuracySample = accuracyQuestions ?? 'full';
824
1064
  const api = {
825
1065
  assertBootstrapIdentity: reconcileMod.assertBootstrapIdentity,
826
1066
  normalizeExtractedCorpus: reconcileMod.normalizeExtractedCorpus,
@@ -828,12 +1068,21 @@ export async function rehearseCorpusPipeline({
828
1068
  syncCorpusInputs: reconcileMod.syncCorpusInputs,
829
1069
  acquireCorpusGeneration: reconcileMod.acquireCorpusGeneration,
830
1070
  reconcileAndPrepareCorpusCandidate: reconcileMod.reconcileAndPrepareCorpusCandidate,
1071
+ summarizeReconciliation: reconcileMod.summarizeReconciliation,
831
1072
  prepareCorpusCandidate: reconcileMod.prepareCorpusCandidate,
832
1073
  observeSourceUniverse: coverageMod.observeSourceUniverse,
833
1074
  canonicalSourceObservation: coverageMod.canonicalSourceObservation,
834
1075
  buildCoverage: coverageMod.buildCoverage,
835
1076
  verifySeedBaseline: candidateMod.verifySeedBaseline,
836
1077
  extractZip: zipMod.extractZip,
1078
+ resolveNextCorpusSeed: nextSeedMod.resolveNextCorpusSeed,
1079
+ verifyApprovedRuntime: approvedMod.verifyApprovedRuntime,
1080
+ emitApprovedRuntime: approvedMod.emitApprovedRuntime,
1081
+ readArchiveManifestFromZip: approvedMod.readArchiveManifestFromZip,
1082
+ isRuntimeFile: approvedMod.isRuntimeFile,
1083
+ readRecallReport: recallMod.readRecallReport,
1084
+ loadFixture: recallMod.loadFixture,
1085
+ readDiagnosticAccuracyReport: accuracyMod.readDiagnosticAccuracyReport,
837
1086
  };
838
1087
  for (const [name, value] of Object.entries(api)) {
839
1088
  if (typeof value !== 'function') fail(`the disposable checkout does not export ${name}`);
@@ -864,8 +1113,24 @@ export async function rehearseCorpusPipeline({
864
1113
 
865
1114
  // ---- phase 6..N: the generations ---------------------------------------------------------
866
1115
  let seed = { file: acquisition.file, tag: descriptor.tag, sha256: descriptor.sha256, bytes: descriptor.bytes,
867
- channel: acquisition.channel, allowPinnedTag: true };
1116
+ channel: acquisition.channel, allowPinnedTag: true, builtUnder: String(descriptor.tag).replace(/^v/, '') };
1117
+ let builderSha = checkout.head;
1118
+ const registryDir = path.join(workRoot, 'release-registry');
1119
+ fs.mkdirSync(registryDir, { recursive: true });
1120
+ let lastPublished = null;
868
1121
  for (let index = 1; index <= generations; index += 1) {
1122
+ if (index > 1 && codeReleaseBetween) {
1123
+ const release = selectionMod.simulateCodeRelease({ checkoutRoot });
1124
+ builderSha = release.head;
1125
+ receipt.codeReleases = [...(receipt.codeReleases || []), { beforeGeneration: index, ...release }];
1126
+ phase(`code-release-before-generation-${index}`, 'PASS', { from: release.from, to: release.to, head: release.head,
1127
+ committed: release.committed,
1128
+ reason: `a code release (v${release.from} -> v${release.to}) landed between generations, the event that reset the pre-D4 seed chain` });
1129
+ }
1130
+ if (index > 1 && seedSelection) {
1131
+ seed = await selectPublishedSeed({ index, api, selectionMod, checkoutRoot, workRoot, registryDir, repoSlug,
1132
+ expected: lastPublished, receipt, phase });
1133
+ }
869
1134
  if (index > 1) {
870
1135
  // Import candidate N as seed N+1 through the SAME baseline verifier the future consumer
871
1136
  // uses — the content-addressed tag, the archive bytes, and the schema-2 receipt together.
@@ -879,13 +1144,35 @@ export async function rehearseCorpusPipeline({
879
1144
  }
880
1145
  const result = await runGeneration({
881
1146
  index, api, checkoutRoot, workRoot, seed, recorder, tamper, log,
882
- bounds: { repos, gists, owner, maxRounds, builderSha: checkout.head, forceRebuild, tamperGeneration },
1147
+ bounds: { repos, gists, owner, maxRounds, builderSha, forceRebuild, tamperGeneration, accuracyQuestions },
883
1148
  });
884
1149
  receipt.generations.push(result.generation);
885
1150
  phase(`generation-${index}`, 'PASS', {
886
1151
  candidateSha256: result.generation.candidate.sha256,
887
1152
  stores: result.generation.candidate.storeCount,
888
1153
  durationMs: result.generation.reconciliation.durationMs });
1154
+
1155
+ // ADR-0091 D4 / V4: the candidate's runtime surface, judged by the real verifyApprovedRuntime
1156
+ // against a pin traced to the CHECKOUT's own bytes (never emitted from the candidate).
1157
+ const manifest = api.readArchiveManifestFromZip(result.generation.candidate.bundleFile);
1158
+ const version = JSON.parse(fs.readFileSync(path.join(checkoutRoot, 'plugin', '.claude-plugin', 'plugin.json'), 'utf8')).version;
1159
+ const { pin, traced, untraced } = selectionMod.checkoutRuntimePin({
1160
+ checkoutRoot, manifest, approvedCodeSha: builderSha, version, api });
1161
+ const verdict = api.verifyApprovedRuntime({ manifest, pin });
1162
+ const runtimeCheck = { generation: index, seedTag: seed.tag, seedBuiltUnder: seed.builtUnder ?? null,
1163
+ consumingRuntime: `v${version}`, pinnedFromCheckout: traced, untraced, verdict: verdict.verdict,
1164
+ checked: verdict.checked, failures: verdict.failures };
1165
+ receipt.approvedRuntime.push(runtimeCheck);
1166
+ if (verdict.verdict !== 'PASS') {
1167
+ phase(`generation-${index}-approved-runtime`, 'FAIL', { ...runtimeCheck, reason: verdict.failures.slice(0, 5).join('; ') });
1168
+ fail(`generation ${index}'s candidate fails verifyApprovedRuntime against the checkout: ${verdict.failures.slice(0, 5).join('; ')}`);
1169
+ }
1170
+ phase(`generation-${index}-approved-runtime`, 'PASS', { ...runtimeCheck,
1171
+ reason: `verifyApprovedRuntime PASS: ${verdict.checked} runtime files equal the checkout's, none unpinned, seed ${seed.tag} did not leak an executable` });
1172
+
1173
+ if (seedSelection && index < generations) {
1174
+ lastPublished = publishLocally({ selectionMod, registryDir, published: result.published, workRoot, index });
1175
+ }
889
1176
  seed = result.nextSeed;
890
1177
  }
891
1178
 
@@ -974,6 +1261,9 @@ export async function main(argv = process.argv.slice(2)) {
974
1261
  tamperGeneration: Number(arg(argv, '--tamper-generation', 1)),
975
1262
  forceRebuild: !argv.includes('--no-force-rebuild'),
976
1263
  seedOverride: arg(argv, '--seed', null),
1264
+ seedSelection: !argv.includes('--no-seed-selection'),
1265
+ codeReleaseBetween: !argv.includes('--no-code-release-between'),
1266
+ accuracySample: arg(argv, '--accuracy-sample', null),
977
1267
  keep: argv.includes('--keep'),
978
1268
  workRootParent: arg(argv, '--work-root', null),
979
1269
  });