ruvnet-brain 4.3.33 → 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.
- 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/scripts/agentdb-fleet-doctor.mjs +7 -3
- package/scripts/approved-runtime.mjs +247 -26
- package/scripts/corpus-next-seed.mjs +191 -70
- package/scripts/corpus-reconcile.mjs +110 -16
- package/scripts/oracle/retrieval-accuracy.mjs +74 -6
- package/scripts/rehearse-corpus-pipeline.mjs +315 -25
- package/scripts/rehearse-seed-selection.mjs +234 -0
- package/scripts/release.mjs +13 -6
- package/scripts/single-source-check.mjs +15 -2
|
@@ -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).
|
|
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.
|
|
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
|
|
412
|
-
|
|
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
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
});
|