ruvnet-brain 4.3.34 → 4.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/scripts/coverage-integrity.mjs +58 -2
- package/scripts/approved-runtime.mjs +247 -26
- package/scripts/build-bundle.mjs +27 -1
- package/scripts/code-release-corpus.mjs +276 -0
- package/scripts/corpus-candidate.mjs +19 -3
- package/scripts/corpus-coverage-sidecar.mjs +202 -0
- package/scripts/corpus-currency.mjs +71 -0
- package/scripts/corpus-dispatch-decision.mjs +138 -0
- package/scripts/corpus-next-seed.mjs +250 -72
- package/scripts/corpus-reconcile.mjs +448 -75
- package/scripts/corpus-store-failure.mjs +82 -0
- package/scripts/corpus-watchdog.mjs +334 -0
- package/scripts/fixture-denominator.mjs +72 -0
- package/scripts/knowledge-input-digest.mjs +163 -0
- package/scripts/oracle/repo-recall.mjs +142 -21
- package/scripts/oracle/retrieval-accuracy.mjs +74 -6
- package/scripts/public-verification-inputs.mjs +18 -2
- package/scripts/rehearse-corpus-pipeline.mjs +651 -31
- package/scripts/rehearse-seed-selection.mjs +234 -0
- package/scripts/release.mjs +163 -18
- package/scripts/retrieval-canary.mjs +42 -20
- package/scripts/single-source-check.mjs +15 -2
- package/scripts/source-coverage.mjs +19 -3
|
@@ -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,21 @@
|
|
|
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] [--no-code-release-consumption]
|
|
48
|
+
// [--inject-store-failure transient|qa] [--inject-generation <n>] [--recall-fixture scoped|committed]
|
|
49
|
+
//
|
|
50
|
+
// ADR-0091 D7: `--recall-fixture committed` keeps the COMMITTED 182-question fixture unedited instead of
|
|
51
|
+
// scoping it. The fixture repositories the bounded observation drops then have no row in a complete
|
|
52
|
+
// sealed observation, so the production repo-recall gate RETIRES them (D7.2) -- the rehearsal asserts it
|
|
53
|
+
// retired exactly those and asked the rest, and every reader re-verifies the claim from coverage (D7.3).
|
|
54
|
+
// Every candidate's manifest.json `corpus` block (D7.1) is checked against its sealed coverage in both modes.
|
|
55
|
+
//
|
|
56
|
+
// ADR-0091 D4 (V4): from generation 2 on, the seed is chosen by the REAL scripts/corpus-next-seed.mjs
|
|
57
|
+
// resolver from a local registry holding generation N (exactly the files release.mjs tried to upload)
|
|
58
|
+
// and two newer INCOMPATIBLE decoys, after a simulated code release has moved the checkout's runtime
|
|
59
|
+
// version. Every candidate's runtime surface is then checked by the real verifyApprovedRuntime against
|
|
60
|
+
// a pin traced to the checkout's own bytes. See scripts/rehearse-seed-selection.mjs.
|
|
39
61
|
//
|
|
40
62
|
// Done is an exit code, not an opinion: FAIL exits non-zero. A phase that could not run is a SKIP
|
|
41
63
|
// with a stated reason, never a silent pass.
|
|
@@ -52,6 +74,13 @@ export const REAL_ROOT = path.resolve(HERE, '..');
|
|
|
52
74
|
const BANNER = 'BOUNDED REHEARSAL — a small explicitly-bounded subset, NOT a full corpus build';
|
|
53
75
|
const HEX64 = /^[a-f0-9]{64}$/;
|
|
54
76
|
const TAMPER_MODES = new Set(['none', 'extracted-byte', 'archive-byte', 'bypass-interception']);
|
|
77
|
+
// ADR-0091 D5 (V5): inject ONE store failure into a real reconciliation. `transient` fails the target's
|
|
78
|
+
// first `git clone` (it must be retried once, in a fresh worker directory, and recover); `qa` answers the
|
|
79
|
+
// target's forge-refresh with corpus-qa's refusal status without running it (it must NOT be retried, and
|
|
80
|
+
// the store must be carried at its re-hashed seed bytes as STALE + carry). Both are synthetic, declared
|
|
81
|
+
// in the receipt, and applied at the process-runner seam executeReconciliation already exposes.
|
|
82
|
+
const INJECTION_MODES = new Set(['none', 'transient', 'qa']);
|
|
83
|
+
const RECALL_FIXTURE_MODES = new Set(['scoped', 'committed']);
|
|
55
84
|
const PROBE_ARG = '__rehearsal_probe__';
|
|
56
85
|
|
|
57
86
|
// Every `gh`/`npm` invocation that would MUTATE anything outside this process. Matched against the
|
|
@@ -407,10 +436,33 @@ export function acquireSeed({ descriptor, repo, downloadDir, env, localCandidate
|
|
|
407
436
|
* downstream — planning, cloning, embedding, indexing, pruning, aggregation, assembly, sealing —
|
|
408
437
|
* is the production code path over that smaller universe.
|
|
409
438
|
*/
|
|
410
|
-
export function boundObservation({ observation, api, repoStores, gistCount }) {
|
|
411
|
-
const
|
|
412
|
-
|
|
439
|
+
export function boundObservation({ observation, api, repoStores, gistCount, frozenSourceCommits = null }) {
|
|
440
|
+
const storeOf = (row) => String(row.storeName || row.name).toLowerCase();
|
|
441
|
+
const sampled = repoStores.length
|
|
442
|
+
? observation.repositories.rows.filter((row) => repoStores.includes(storeOf(row)))
|
|
443
|
+
: [];
|
|
444
|
+
// FROZEN AT SEED (2026-09-28, ADR-0091 V1). Observing ONLY the sampled repositories makes every
|
|
445
|
+
// other seed store "not observed this round", and the production prune (correctly, for a real
|
|
446
|
+
// nightly that always observes everything) deletes it -- after which the repo-recall gate, which
|
|
447
|
+
// asks one frozen question of every fixture repository, fails 180/182 with `rvf not found`. So the
|
|
448
|
+
// rehearsal keeps every OTHER repository the seed already carries in the observation, with its
|
|
449
|
+
// head pinned to the seed's own ledger sourceCommit: planReconciliation sees it CURRENT (nothing to
|
|
450
|
+
// rebuild) and pruneIneligibleStores sees it eligible (nothing to delete). Repositories the seed does
|
|
451
|
+
// not carry are dropped, so a brand-new upstream repo can never turn a minutes-long rehearsal into a
|
|
452
|
+
// full clone-and-embed. This rewrite is a declared rehearsal bound, recorded in the receipt; the
|
|
453
|
+
// pipeline's own observation, planning and pruning code is untouched.
|
|
454
|
+
const frozen = frozenSourceCommits
|
|
455
|
+
? observation.repositories.rows
|
|
456
|
+
.filter((row) => !repoStores.includes(storeOf(row)) && frozenSourceCommits[storeOf(row)])
|
|
457
|
+
.map((row) => ({
|
|
458
|
+
...row,
|
|
459
|
+
defaultBranchRef: {
|
|
460
|
+
name: row.defaultBranchRef?.name || 'main',
|
|
461
|
+
target: { oid: frozenSourceCommits[storeOf(row)], committedDate: null },
|
|
462
|
+
},
|
|
463
|
+
}))
|
|
413
464
|
: [];
|
|
465
|
+
const chosen = [...sampled, ...frozen];
|
|
414
466
|
const gistRows = observation.gists.rows.slice(0, gistCount);
|
|
415
467
|
return api.canonicalSourceObservation({
|
|
416
468
|
schemaVersion: observation.schemaVersion,
|
|
@@ -422,6 +474,41 @@ export function boundObservation({ observation, api, repoStores, gistCount }) {
|
|
|
422
474
|
});
|
|
423
475
|
}
|
|
424
476
|
|
|
477
|
+
/** The seed ledger's own sourceCommit for every repository store OTHER than the sampled ones —
|
|
478
|
+
* the commits boundObservation pins those stores to. Aggregates (ruv-gists, concepts) are rebuilt
|
|
479
|
+
* from nothing every round and are never repository rows, so they are excluded. */
|
|
480
|
+
export function frozenSeedCommits(assetsDir, repoStores) {
|
|
481
|
+
const ledger = JSON.parse(fs.readFileSync(path.join(assetsDir, 'RVF-GENERATIONS.json'), 'utf8'));
|
|
482
|
+
const out = {};
|
|
483
|
+
for (const [store, row] of Object.entries(ledger.stores || {})) {
|
|
484
|
+
const folded = store.toLowerCase();
|
|
485
|
+
if (['ruv-gists', 'concepts'].includes(folded) || repoStores.includes(folded)) continue;
|
|
486
|
+
const sha = String(row?.sourceCommit || '').toLowerCase();
|
|
487
|
+
if (/^[a-f0-9]{40}$/.test(sha)) out[folded] = sha;
|
|
488
|
+
}
|
|
489
|
+
return out;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Scope the frozen repo-recall fixture to the repositories THIS bounded candidate carries (ADR-0091
|
|
494
|
+
* V1). A seed store whose ledger sourceCommit is null (57 of 182 in the v4.3.26 seed) cannot be frozen
|
|
495
|
+
* at a commit: the real nightly rebuilds it from upstream, which is hours, not minutes. Out of the
|
|
496
|
+
* rehearsal's scope, it is pruned, and asking the fixture question of it could only report
|
|
497
|
+
* `rvf not found`. Every in-scope question is kept byte for byte. Returns the scoped fixture document
|
|
498
|
+
* plus exactly what was excluded, so the receipt says it out loud.
|
|
499
|
+
*/
|
|
500
|
+
export function scopeRecallFixture({ fixture, inScopeStores }) {
|
|
501
|
+
const scope = new Set([...inScopeStores].map((store) => String(store).toLowerCase()));
|
|
502
|
+
const kept = {};
|
|
503
|
+
const excluded = [];
|
|
504
|
+
for (const [store, row] of Object.entries(fixture.queries || {})) {
|
|
505
|
+
if (scope.has(store.toLowerCase())) kept[store] = row;
|
|
506
|
+
else excluded.push(store);
|
|
507
|
+
}
|
|
508
|
+
if (!Object.keys(kept).length) fail('scoping the repo-recall fixture left no questions; the bounded scope carries no fixture repository');
|
|
509
|
+
return { fixture: { ...fixture, queries: kept }, kept: Object.keys(kept).length, excluded: excluded.sort() };
|
|
510
|
+
}
|
|
511
|
+
|
|
425
512
|
/** Deterministically pick the `count` smallest ELIGIBLE repositories, measured by the observation's
|
|
426
513
|
* own diskUsage, so a rehearsal clones and embeds real upstream source in minutes. Eligibility is
|
|
427
514
|
* decided by the production classifier (`buildCoverage`), never by a guess here. */
|
|
@@ -548,6 +635,56 @@ export async function verifyExtractedBytesWithoutOriginals({
|
|
|
548
635
|
// rehearse publication with the recorder, and emit candidate N as the seed for generation N+1.
|
|
549
636
|
// ---------------------------------------------------------------------------------------------
|
|
550
637
|
|
|
638
|
+
// The rehearsal's record of one generation's reconciliation. It never reads the acquisition history
|
|
639
|
+
// itself: it uses the disposable checkout's own summarizeReconciliation, the same reader main() uses
|
|
640
|
+
// (ADR-0091 D1). Its private copy of the old `rounds` read crashed exactly where main() did.
|
|
641
|
+
export function recordReconciliation({ summarize, reconciliation, durationMs }) {
|
|
642
|
+
const summary = summarize(reconciliation);
|
|
643
|
+
return {
|
|
644
|
+
attempts: summary.attempts,
|
|
645
|
+
observationSha256: summary.observationSha256,
|
|
646
|
+
refreshed: summary.refreshed,
|
|
647
|
+
pruned: summary.pruned.length,
|
|
648
|
+
rebuilt: summary.rebuilt,
|
|
649
|
+
durationMs,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* ADR-0091 D7, read from the REAL sealed artifacts of one generation (never recomputed from inputs):
|
|
655
|
+
* D7.1 the assembled manifest.json `corpus` block names the sealed observation's observedAt (not the
|
|
656
|
+
* assembly time) and counts exactly the sealed coverage's eligible repository rows;
|
|
657
|
+
* D7.2 in `committed` mode the recall report retired EXACTLY the fixture repositories the bounded
|
|
658
|
+
* observation dropped, asked every other one, and bound the claim to the sealed coverage bytes.
|
|
659
|
+
*/
|
|
660
|
+
export function checkGenerationCurrency({ checkoutRoot, candidateDir, bundleFile, committedFixture, expectedRetired, expectedAsked }) {
|
|
661
|
+
const coverageFile = path.join(checkoutRoot, 'data', 'source-coverage.json');
|
|
662
|
+
const coverage = JSON.parse(fs.readFileSync(coverageFile, 'utf8'));
|
|
663
|
+
const corpus = JSON.parse(fs.readFileSync(path.join(candidateDir, 'manifest.json'), 'utf8')).corpus;
|
|
664
|
+
const eligible = coverage.rows.filter((row) => row.kind === 'repository' && row.disposition === 'eligible');
|
|
665
|
+
const count = (status) => eligible.filter((row) => row.status === status).length;
|
|
666
|
+
const problems = [];
|
|
667
|
+
if (!corpus || corpus.basis !== 'sealed-observation') problems.push(`manifest.corpus basis is ${corpus?.basis}`);
|
|
668
|
+
if (corpus?.observedAt !== coverage.observedAt) problems.push(`manifest.corpus.observedAt ${corpus?.observedAt} is not the sealed observation's ${coverage.observedAt}`);
|
|
669
|
+
const counts = corpus?.counts || {};
|
|
670
|
+
for (const [key, status] of [['current', 'CURRENT'], ['stale', 'STALE'], ['missing', 'MISSING'], ['unverified', 'UNVERIFIED']]) {
|
|
671
|
+
if (counts[key] !== count(status)) problems.push(`manifest.corpus.counts.${key}=${counts[key]}, sealed coverage has ${count(status)}`);
|
|
672
|
+
}
|
|
673
|
+
if (counts.eligible !== eligible.length) problems.push(`manifest.corpus.counts.eligible=${counts.eligible}, sealed coverage has ${eligible.length}`);
|
|
674
|
+
const report = JSON.parse(fs.readFileSync(`${bundleFile}.recall.json`, 'utf8'));
|
|
675
|
+
const want = [...new Set(expectedRetired.map((store) => store.toLowerCase()))].sort();
|
|
676
|
+
const got = report.retirement?.stores ?? [];
|
|
677
|
+
if (committedFixture) {
|
|
678
|
+
if (JSON.stringify(got) !== JSON.stringify(want)) problems.push(`recall retired [${got.join(', ')}], expected exactly [${want.join(', ')}]`);
|
|
679
|
+
if (report.totals.questions !== expectedAsked) problems.push(`recall asked ${report.totals.questions}, expected ${expectedAsked}`);
|
|
680
|
+
if (want.length && report.retirement?.coverageSha256 !== sha256File(coverageFile)) problems.push('recall retirement is not bound to the sealed coverage bytes');
|
|
681
|
+
if (counts.retired !== want.length) problems.push(`manifest.corpus.counts.retired=${counts.retired}, expected ${want.length}`);
|
|
682
|
+
} else if (got.length) problems.push(`a scoped fixture retired [${got.join(', ')}]; nothing should retire`);
|
|
683
|
+
if (problems.length) fail(`ADR-0091 D7 generation checks failed: ${problems.join('; ')}`);
|
|
684
|
+
return { manifestCorpus: corpus, recall: { questions: report.totals.questions, retired: report.totals.retired ?? 0,
|
|
685
|
+
retiredStores: got, coverageSha256: report.retirement?.coverageSha256 ?? null } };
|
|
686
|
+
}
|
|
687
|
+
|
|
551
688
|
async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds, recorder, tamper, log }) {
|
|
552
689
|
const generation = { index, startedAt: new Date().toISOString(), seed: { tag: seed.tag, sha256: seed.sha256, bytes: seed.bytes, channel: seed.channel } };
|
|
553
690
|
const assetsDir = path.join(workRoot, `assets-gen${index}`);
|
|
@@ -571,8 +708,11 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
|
|
|
571
708
|
api.syncCorpusInputs({ root: checkoutRoot, assetsDir });
|
|
572
709
|
const bootstrapIdentity = { tag: bootstrap.tag, sha256: bootstrap.sha256,
|
|
573
710
|
privateFenceEvidence: api.seedPrivateFenceEvidence(assetsDir) };
|
|
574
|
-
|
|
575
|
-
|
|
711
|
+
const seedLedger = JSON.parse(fs.readFileSync(path.join(assetsDir, 'RVF-GENERATIONS.json'), 'utf8'));
|
|
712
|
+
generation.seedImport = { assetsDir, storesInSeed: Object.keys(seedLedger.stores || {}).length,
|
|
713
|
+
// normalizeExtractedCorpus refused the import unless this runtime can consume the ledger (ADR-0091 D4).
|
|
714
|
+
ledgerSchema: { schemaVersion: seedLedger.schemaVersion, kind: seedLedger.kind, brainVersion: seedLedger.brainVersion ?? null,
|
|
715
|
+
verdict: 'consumable by this runtime' } };
|
|
576
716
|
log(`[gen ${index}] seed imported: ${generation.seedImport.storesInSeed} stores in the ledger`);
|
|
577
717
|
|
|
578
718
|
// --- bounded observation over the REAL live source universe ---------------------------------
|
|
@@ -591,40 +731,128 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
|
|
|
591
731
|
// seed RVF bytes so planReconciliation must rebuild them from upstream. Without this a seed whose
|
|
592
732
|
// stores are already CURRENT would exercise no acquisition at all, and the rehearsal would prove
|
|
593
733
|
// nothing about the expensive half of the pipeline.
|
|
734
|
+
const injection = bounds.injectStoreFailure !== 'none' && index === bounds.injectGeneration ? bounds.injectStoreFailure : 'none';
|
|
735
|
+
let injectTarget = null;
|
|
736
|
+
if (injection !== 'none') {
|
|
737
|
+
const upstreamOf = (store) => full.repositories.rows.find((row) => String(row.storeName || row.name).toLowerCase() === store)
|
|
738
|
+
?.defaultBranchRef?.target?.oid?.toLowerCase() || null;
|
|
739
|
+
const seedCommitOf = (store) => String(Object.entries(seedLedger.stores || {})
|
|
740
|
+
.find(([name]) => name.toLowerCase() === store)?.[1]?.sourceCommit || '').toLowerCase();
|
|
741
|
+
// A carry needs seed bytes at a commit that DIFFERS from upstream; a transient retry needs neither.
|
|
742
|
+
const carryable = (store) => /^[0-9a-f]{40}$/.test(seedCommitOf(store)) && upstreamOf(store) && seedCommitOf(store) !== upstreamOf(store);
|
|
743
|
+
injectTarget = injection === 'transient' ? repoStores[0] : repoStores.find(carryable) || null;
|
|
744
|
+
if (!injectTarget && injection === 'qa') {
|
|
745
|
+
// The smallest bounded stores are often unchanged since the seed. A QA-injected store is never
|
|
746
|
+
// embedded (its forge-refresh is answered synthetically), so adding the smallest carryable seed
|
|
747
|
+
// store to the bounded set costs one clone. Declared in the receipt.
|
|
748
|
+
const extra = [...full.repositories.rows]
|
|
749
|
+
.sort((a, b) => (a.diskUsage ?? Number.MAX_SAFE_INTEGER) - (b.diskUsage ?? Number.MAX_SAFE_INTEGER))
|
|
750
|
+
.map((row) => String(row.storeName || row.name).toLowerCase())
|
|
751
|
+
.find((store) => !repoStores.includes(store) && carryable(store)) || null;
|
|
752
|
+
if (extra) {
|
|
753
|
+
repoStores.push(extra);
|
|
754
|
+
generation.bounded.repositories = repoStores;
|
|
755
|
+
generation.bounded.addedForInjection = extra;
|
|
756
|
+
injectTarget = extra;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
if (!injectTarget) {
|
|
760
|
+
fail(`--inject-store-failure ${injection}: no observed store has seed bytes at a commit that differs from upstream, `
|
|
761
|
+
+ 'so nothing could be carried');
|
|
762
|
+
}
|
|
763
|
+
generation.injection = { mode: injection, target: injectTarget, synthetic: true, events: [] };
|
|
764
|
+
log(`[gen ${index}] INJECTING a synthetic ${injection} failure into store ${injectTarget}`);
|
|
765
|
+
}
|
|
594
766
|
if (bounds.forceRebuild) {
|
|
595
|
-
|
|
596
|
-
|
|
767
|
+
// A QA-injected store keeps its seed bytes: they are what the carry must re-hash and keep.
|
|
768
|
+
const forced = repoStores.filter((store) => !(injection === 'qa' && store === injectTarget));
|
|
769
|
+
for (const store of forced) fs.rmSync(path.join(assetsDir, `${store}.big.rvf`), { force: true });
|
|
770
|
+
generation.forcedRebuild = forced;
|
|
597
771
|
}
|
|
598
772
|
|
|
599
|
-
|
|
773
|
+
// Every other seed store stays in scope FROZEN at its seed sourceCommit (see boundObservation), so
|
|
774
|
+
// the production prune leaves it alone and the repo-recall gate can ask its fixture question.
|
|
775
|
+
const frozenSourceCommits = frozenSeedCommits(assetsDir, repoStores);
|
|
776
|
+
generation.bounded.frozenAtSeed = Object.keys(frozenSourceCommits).length;
|
|
777
|
+
log(`[gen ${index}] ${generation.bounded.frozenAtSeed} other seed store(s) kept in scope frozen at their seed sourceCommit (not rebuilt, not pruned)`);
|
|
778
|
+
const boundedObserve = () => boundObservation({
|
|
779
|
+
observation: observeFull(), api, repoStores, gistCount: bounds.gists, frozenSourceCommits });
|
|
780
|
+
|
|
781
|
+
// The repo-recall gate asks one question per fixture repository. Scope it -- in the DISPOSABLE
|
|
782
|
+
// checkout only, where repo-recall, corpus-candidate --verify and release.mjs all read the same file
|
|
783
|
+
// -- to the repositories this bounded observation keeps. The committed original is preserved beside
|
|
784
|
+
// it so every generation scopes from the same source.
|
|
785
|
+
const fixtureFile = path.join(checkoutRoot, 'data', 'retrieval-query-evidence.json');
|
|
786
|
+
const originalFixtureFile = path.join(workRoot, 'retrieval-query-evidence.committed.json');
|
|
787
|
+
if (!fs.existsSync(originalFixtureFile)) fs.copyFileSync(fixtureFile, originalFixtureFile);
|
|
788
|
+
const inScope = boundObservation({ observation: full, api, repoStores, gistCount: 0, frozenSourceCommits })
|
|
789
|
+
.repositories.rows.map((row) => String(row.storeName || row.name));
|
|
790
|
+
const scoped = scopeRecallFixture({
|
|
791
|
+
fixture: JSON.parse(fs.readFileSync(originalFixtureFile, 'utf8')), inScopeStores: inScope });
|
|
792
|
+
const committedFixture = bounds.recallFixture === 'committed';
|
|
793
|
+
// ADR-0091 D7: in `committed` mode the fixture is NOT edited; the out-of-scope repositories are
|
|
794
|
+
// retired by the production gate instead, which the checks after sealing hold to exactly this list.
|
|
795
|
+
if (!committedFixture) fs.writeFileSync(fixtureFile, `${JSON.stringify(scoped.fixture, null, 2)}\n`);
|
|
796
|
+
generation.bounded.recallFixture = { mode: bounds.recallFixture, questions: scoped.kept, excluded: scoped.excluded.length,
|
|
797
|
+
excludedStores: scoped.excluded,
|
|
798
|
+
reason: 'no seed sourceCommit to freeze at (the real nightly rebuilds these from upstream) or not observed upstream' };
|
|
799
|
+
log(committedFixture
|
|
800
|
+
? `[gen ${index}] repo-recall fixture kept COMMITTED and unedited: ${scoped.excluded.length} out-of-scope repositories must be RETIRED by the gate (ADR-0091 D7), ${scoped.kept} asked`
|
|
801
|
+
: `[gen ${index}] repo-recall fixture scoped to ${scoped.kept} in-scope repositories (${scoped.excluded.length} out of scope, listed in the receipt)`);
|
|
600
802
|
|
|
601
803
|
// --- reconcile + assemble ONCE --------------------------------------------------------------
|
|
602
804
|
const invocations = [];
|
|
603
805
|
const recordingRun = (command, args, options = {}) => {
|
|
604
|
-
|
|
605
|
-
|
|
806
|
+
const row = { command, script: path.basename(String(args?.[0] || '')), args: args.map(String) };
|
|
807
|
+
invocations.push(row);
|
|
808
|
+
const began = Date.now();
|
|
809
|
+
const result = spawnSync(command, args, { encoding: 'utf8', ...options });
|
|
810
|
+
row.durationMs = Date.now() - began;
|
|
811
|
+
row.exit = result.status ?? null;
|
|
812
|
+
return result;
|
|
813
|
+
};
|
|
814
|
+
const injectingRun = (command, args, options) => {
|
|
815
|
+
const events = generation.injection?.events;
|
|
816
|
+
const target = generation.injection?.target;
|
|
817
|
+
const clone = command === 'git' && args[0] === 'clone';
|
|
818
|
+
const cloneDir = clone ? String(args.at(-1)) : '';
|
|
819
|
+
if (target && clone && path.basename(path.dirname(cloneDir)) === target && injection === 'transient') {
|
|
820
|
+
events.push({ event: 'git clone', dir: path.relative(workRoot, cloneDir), injected: 'transient failure (exit 128)' });
|
|
821
|
+
return Promise.resolve({ status: 128, stdout: '', stderr: '[rehearsal] injected transient failure: connection reset' });
|
|
822
|
+
}
|
|
823
|
+
if (target && clone && path.basename(path.dirname(cloneDir)).startsWith(`${target}-retry`)) {
|
|
824
|
+
events.push({ event: 'git clone', dir: path.relative(workRoot, cloneDir), injected: null });
|
|
825
|
+
}
|
|
826
|
+
const forge = command === process.execPath && String(args[0]).endsWith(`${path.sep}forge-refresh.mjs`);
|
|
827
|
+
if (target && forge && args[args.indexOf('--name') + 1] === target) {
|
|
828
|
+
if (injection === 'qa') {
|
|
829
|
+
events.push({ event: 'forge-refresh', out: path.relative(workRoot, args[args.indexOf('--out') + 1]),
|
|
830
|
+
injected: `corpus-qa refusal (exit ${api.CORPUS_QA_FAILED_EXIT}), forge not run` });
|
|
831
|
+
return Promise.resolve({ status: api.CORPUS_QA_FAILED_EXIT, stdout: '', stderr: '[rehearsal] injected corpus-qa refusal' });
|
|
832
|
+
}
|
|
833
|
+
events.push({ event: 'forge-refresh', out: path.relative(workRoot, args[args.indexOf('--out') + 1]), injected: null });
|
|
834
|
+
}
|
|
835
|
+
return api.defaultRunAsync(command, args, options);
|
|
606
836
|
};
|
|
607
837
|
const reconcileStart = Date.now();
|
|
608
838
|
const { reconciliation, candidate } = await api.reconcileAndPrepareCorpusCandidate({
|
|
609
839
|
assetsDir, workspaceDir, root: checkoutRoot, owner: bounds.owner, builderSha: bounds.builderSha,
|
|
610
840
|
candidateDir, receiptFile, coverageFile: path.join(checkoutRoot, 'data', 'source-coverage.json'),
|
|
611
|
-
|
|
612
|
-
|
|
841
|
+
// reconcileAndPrepareCorpusCandidate reads `maxAttempts`; `maxRounds` was silently ignored after
|
|
842
|
+
// cd0f032f, so --max-rounds never reached the acquisition loop.
|
|
843
|
+
bootstrapIdentity, maxAttempts: bounds.maxRounds, accuracySample: bounds.accuracyQuestions,
|
|
844
|
+
reconcile: (options) => api.acquireCorpusGeneration({ ...options, observe: boundedObserve,
|
|
845
|
+
...(injection !== 'none' ? { execute: (executeOptions) => api.executeReconciliation({ ...executeOptions, run: injectingRun }) } : {}) }),
|
|
613
846
|
prepare: (options) => api.prepareCorpusCandidate({ ...options, run: recordingRun }),
|
|
614
847
|
});
|
|
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
|
-
};
|
|
848
|
+
generation.reconciliation = recordReconciliation({
|
|
849
|
+
summarize: api.summarizeReconciliation, reconciliation, durationMs: Date.now() - reconcileStart });
|
|
623
850
|
const assemblyInvocations = invocations.filter((row) => row.script === 'build-bundle.mjs');
|
|
624
851
|
generation.assembly = {
|
|
625
852
|
assembleBundleInvocations: assemblyInvocations.length,
|
|
626
853
|
receiptInvocations: invocations.filter((row) => row.script === 'corpus-candidate.mjs').length,
|
|
627
854
|
bundleFile: candidate.bundleFile,
|
|
855
|
+
steps: invocations.map(({ script, durationMs, exit }) => ({ script, durationMs, exit })),
|
|
628
856
|
};
|
|
629
857
|
if (assemblyInvocations.length !== 1) {
|
|
630
858
|
fail(`assembly must happen exactly once per candidate; build-bundle.mjs ran ${assemblyInvocations.length} time(s)`);
|
|
@@ -649,6 +877,38 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
|
|
|
649
877
|
generation.candidate.storeCount = receipt.storeCount;
|
|
650
878
|
generation.candidate.stores = receipt.stores.map(({ name, kind }) => ({ name, kind }));
|
|
651
879
|
log(`[gen ${index}] sealed candidate ${archiveSha256.slice(0, 16)} — ${receipt.storeCount} stores, ${archiveBytes} bytes`);
|
|
880
|
+
generation.degraded = candidate.degraded || { carried: [], missing: [] };
|
|
881
|
+
if (injection !== 'none') {
|
|
882
|
+
const sealedCoverage = JSON.parse(fs.readFileSync(path.join(checkoutRoot, 'data', 'source-coverage.json'), 'utf8'));
|
|
883
|
+
const row = sealedCoverage.rows.find((entry) => entry.kind === 'repository' && String(entry.artifact?.store).toLowerCase() === injectTarget);
|
|
884
|
+
generation.injection.sealedRow = row ? { status: row.status, carry: row.carry ?? null, failure: row.failure ?? null,
|
|
885
|
+
artifactSourceCommit: row.artifact?.sourceCommit ?? null, upstreamSha: row.upstream?.sha ?? null } : null;
|
|
886
|
+
const events = generation.injection.events;
|
|
887
|
+
const problems = [];
|
|
888
|
+
if (injection === 'qa') {
|
|
889
|
+
const carried = generation.degraded.carried.find((entry) => entry.store === injectTarget);
|
|
890
|
+
if (!carried || carried.attempts !== 1 || !/^qa: /.test(carried.reason)) problems.push(`${injectTarget} was not carried once as a QA failure (${JSON.stringify(carried || null)})`);
|
|
891
|
+
if (events.filter((entry) => entry.event === 'forge-refresh').length !== 1) problems.push('a QA failure was retried');
|
|
892
|
+
if (row?.status !== 'STALE' || !row?.carry) problems.push(`the sealed coverage row is ${row?.status || 'absent'}${row?.carry ? '' : ' with no carry record'}, not STALE + carry`);
|
|
893
|
+
} else {
|
|
894
|
+
if (!generation.reconciliation.refreshed.includes(injectTarget)) problems.push(`${injectTarget} did not recover on its retry`);
|
|
895
|
+
const clones = events.filter((entry) => entry.event === 'git clone');
|
|
896
|
+
if (clones.length !== 2 || !clones[1].dir.includes(`${injectTarget}-retry1`)) problems.push(`expected one failed clone then one retry in ${injectTarget}-retry1 (${JSON.stringify(clones)})`);
|
|
897
|
+
if (generation.degraded.carried.length || generation.degraded.missing.length) problems.push('a recovered transient failure left the generation degraded');
|
|
898
|
+
if (row?.status !== 'CURRENT') problems.push(`the recovered store's sealed row is ${row?.status}, not CURRENT`);
|
|
899
|
+
}
|
|
900
|
+
generation.injection.verdict = problems.length ? 'FAIL' : 'PASS';
|
|
901
|
+
if (problems.length) fail(`injected ${injection} failure did not behave as ADR-0091 D5 requires: ${problems.join('; ')}`);
|
|
902
|
+
log(`[gen ${index}] injected ${injection} failure in ${injectTarget} behaved as required: `
|
|
903
|
+
+ `${injection === 'qa' ? 'not retried, carried at its seed bytes, sealed row STALE + carry' : 'retried once in a fresh directory and recovered'}`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// --- ADR-0091 D7: the manifest's currency block and the recall gate's retirement, from real bytes --
|
|
907
|
+
generation.d7 = checkGenerationCurrency({ checkoutRoot, candidateDir, bundleFile,
|
|
908
|
+
committedFixture, expectedRetired: scoped.excluded, expectedAsked: scoped.kept });
|
|
909
|
+
log(`[gen ${index}] ADR-0091 D7: manifest.corpus observedAt=${generation.d7.manifestCorpus.observedAt} `
|
|
910
|
+
+ `counts=${JSON.stringify(generation.d7.manifestCorpus.counts)}; recall asked ${generation.d7.recall.questions}, `
|
|
911
|
+
+ `retired ${generation.d7.recall.retired}`);
|
|
652
912
|
|
|
653
913
|
// --- remove access to the originals, then verify the extracted bytes ------------------------
|
|
654
914
|
generation.extractedByteVerification = await verifyExtractedBytesWithoutOriginals({
|
|
@@ -657,8 +917,20 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
|
|
|
657
917
|
});
|
|
658
918
|
log(`[gen ${index}] extracted-byte verification passed with staging revoked (${generation.extractedByteVerification.files} files)`);
|
|
659
919
|
|
|
660
|
-
// ---
|
|
920
|
+
// --- ADR-0091 D5 + D10: a degraded generation is sealed but not published until D10 allows it ---
|
|
661
921
|
const corpusTag = `corpus-sha256-${archiveSha256}`;
|
|
922
|
+
if (generation.degraded.carried.length || generation.degraded.missing.length) {
|
|
923
|
+
const decision = api.degradedPublication();
|
|
924
|
+
if (!decision.allowed) {
|
|
925
|
+
generation.publication = { status: 'WITHHELD-DEGRADED', corpusTag, reason: decision.reason,
|
|
926
|
+
carried: generation.degraded.carried.map((entry) => entry.store), missing: generation.degraded.missing.map((entry) => entry.store) };
|
|
927
|
+
generation.finishedAt = new Date().toISOString();
|
|
928
|
+
log(`[gen ${index}] degraded generation sealed and WITHHELD from publication: ${decision.reason}`);
|
|
929
|
+
return { generation, withheld: true, nextSeed: null, published: null };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// --- rehearse the real product-consumption path, publication RECORDED not executed -----------
|
|
662
934
|
const proof = recorder.proveIntercepting();
|
|
663
935
|
if (!proof.ok) {
|
|
664
936
|
generation.publication = { status: 'FAIL', reason: 'command interception could not be proven; refusing to run a publication path that might reach the network', proof };
|
|
@@ -677,8 +949,13 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
|
|
|
677
949
|
GH_TOKEN: 'rehearsal-invalid-token-do-not-use',
|
|
678
950
|
GITHUB_TOKEN: 'rehearsal-invalid-token-do-not-use',
|
|
679
951
|
};
|
|
952
|
+
// ADR-0091 D6.2: the sealed coverage travels to the publisher the way corpus-seed.yml stages it
|
|
953
|
+
// (a copy of <checkout>/data/source-coverage.json beside the archive), and is published with it.
|
|
954
|
+
const stagedCoverage = path.join(path.dirname(bundleFile), 'source-coverage.json');
|
|
955
|
+
fs.copyFileSync(path.join(checkoutRoot, 'data', 'source-coverage.json'), stagedCoverage);
|
|
680
956
|
const publish = run(process.execPath, [path.join(checkoutRoot, 'scripts', 'release.mjs'), '--corpus-seed',
|
|
681
957
|
'--corpus-tag', corpusTag, '--corpus-bundle', bundleFile, '--corpus-receipt', candidate.receiptFile,
|
|
958
|
+
'--corpus-coverage', stagedCoverage,
|
|
682
959
|
'--target', bounds.builderSha, '--repo', 'stuinfla/ruvnet-brain'], { cwd: checkoutRoot, env: publishEnv });
|
|
683
960
|
const recordedNow = recorder.recorded().filter((row) => row.classification === 'publication-mutation');
|
|
684
961
|
generation.publication = {
|
|
@@ -705,16 +982,244 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
|
|
|
705
982
|
const nextSeedFile = path.join(workRoot, `seed-gen${index + 1}`, path.basename(bundleFile));
|
|
706
983
|
fs.mkdirSync(path.dirname(nextSeedFile), { recursive: true });
|
|
707
984
|
fs.copyFileSync(bundleFile, nextSeedFile);
|
|
985
|
+
// The detached C3 and repo-recall reports travel WITH the archive, exactly as release.mjs publishes
|
|
986
|
+
// them (zip, sig, digest, receipt, .accuracy.json, .recall.json). verifySeedBaseline re-reads both
|
|
987
|
+
// beside the seed, so handing over the zip alone failed every generation-2 import ("detached
|
|
988
|
+
// repo-recall report missing") -- unseen until 2026-09-28 because no rehearsal had reached gen 2.
|
|
989
|
+
for (const suffix of ['.accuracy.json', '.recall.json']) {
|
|
990
|
+
const report = `${bundleFile}${suffix}`;
|
|
991
|
+
if (!fs.existsSync(report)) fail(`candidate ${index} has no detached ${suffix} report to hand to generation ${index + 1}`);
|
|
992
|
+
fs.copyFileSync(report, `${nextSeedFile}${suffix}`);
|
|
993
|
+
}
|
|
994
|
+
// ADR-0091 D7.3: the generation's sealed coverage travels with it, so the next import can verify a
|
|
995
|
+
// retirement its recall report claims (the published CORPUS-COVERAGE.json is these exact bytes).
|
|
996
|
+
const nextSeedCoverage = path.join(path.dirname(nextSeedFile), 'CORPUS-COVERAGE.json');
|
|
997
|
+
fs.copyFileSync(stagedCoverage, nextSeedCoverage);
|
|
708
998
|
generation.nextSeed = { tag: corpusTag, sha256: archiveSha256, bytes: archiveBytes, file: nextSeedFile,
|
|
709
999
|
contentAddressed: true };
|
|
710
1000
|
generation.finishedAt = new Date().toISOString();
|
|
1001
|
+
const createRow = recordedNow.find((row) => row.args[0] === 'release' && row.args[1] === 'create' && row.args[2] === corpusTag);
|
|
711
1002
|
return {
|
|
712
1003
|
generation,
|
|
713
1004
|
nextSeed: { file: nextSeedFile, tag: corpusTag, sha256: archiveSha256, bytes: archiveBytes,
|
|
714
|
-
channel: `candidate-generation-${index}`, allowPinnedTag: false, receiptFile: candidate.receiptFile },
|
|
1005
|
+
channel: `candidate-generation-${index}`, allowPinnedTag: false, receiptFile: candidate.receiptFile, coverageFile: nextSeedCoverage },
|
|
1006
|
+
// ADR-0091 D4: exactly what release.mjs tried to upload, so the local registry invents nothing.
|
|
1007
|
+
published: { tag: corpusTag, createArgs: createRow ? createRow.args : [], receiptFile: candidate.receiptFile,
|
|
1008
|
+
recallFile: `${bundleFile}.recall.json`, accuracyFile: `${bundleFile}.accuracy.json`, sha256: archiveSha256, bytes: archiveBytes },
|
|
715
1009
|
};
|
|
716
1010
|
}
|
|
717
1011
|
|
|
1012
|
+
// ---------------------------------------------------------------------------------------------
|
|
1013
|
+
// ADR-0091 D4: seed selection through the real resolver (see scripts/rehearse-seed-selection.mjs)
|
|
1014
|
+
// ---------------------------------------------------------------------------------------------
|
|
1015
|
+
|
|
1016
|
+
/** Candidate N, "published" into the local registry from release.mjs's own upload list, plus two
|
|
1017
|
+
* newer incompatible decoys the resolver must walk past. */
|
|
1018
|
+
function publishLocally({ selectionMod, registryDir, published, workRoot, index }) {
|
|
1019
|
+
const files = selectionMod.uploadedFilesOf(published.createArgs);
|
|
1020
|
+
const names = files.map((file) => path.basename(file)).sort();
|
|
1021
|
+
const expectedNames = ['CORPUS-COVERAGE.json', 'corpus-receipt.json', 'coverage-receipt.json', 'ruvnet-brain.zip',
|
|
1022
|
+
'ruvnet-brain.zip.accuracy.json', 'ruvnet-brain.zip.recall.json'];
|
|
1023
|
+
if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
|
|
1024
|
+
fail(`release.mjs's recorded upload for generation ${index} is [${names.join(', ')}], expected [${expectedNames.join(', ')}]`);
|
|
1025
|
+
}
|
|
1026
|
+
const createdAtMs = Date.now();
|
|
1027
|
+
selectionMod.registerGeneration({ registryDir, tag: published.tag, createdAt: new Date(createdAtMs).toISOString(), files,
|
|
1028
|
+
note: `generation ${index}, from the files release.mjs --corpus-seed tried to upload` });
|
|
1029
|
+
const decoys = selectionMod.registerIncompatibleDecoys({ registryDir, createdAtMs,
|
|
1030
|
+
scratchDir: path.join(workRoot, `decoys-gen${index}`),
|
|
1031
|
+
real: { sha256: published.sha256, bytes: published.bytes, receiptFile: published.receiptFile,
|
|
1032
|
+
recallFile: published.recallFile, accuracyFile: published.accuracyFile } });
|
|
1033
|
+
return { ...published, decoys };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* What corpus-identity + corpus-seed.yml do on the nightly, against the local registry: resolve with
|
|
1038
|
+
* the REAL corpus-next-seed resolver (judged by this checkout's readers), prove it skipped both decoys
|
|
1039
|
+
* before any archive download, then "download" the chosen archive and re-check it exactly as
|
|
1040
|
+
* corpus-seed.yml does (sha256 + bytes, repo-recall at ABSOLUTE_FLOOR, C3 binding).
|
|
1041
|
+
*/
|
|
1042
|
+
async function selectPublishedSeed({ index, api, selectionMod, checkoutRoot, workRoot, registryDir, repoSlug, expected, receipt, phase }) {
|
|
1043
|
+
const gh = selectionMod.registryGh(registryDir);
|
|
1044
|
+
const selection = await api.resolveNextCorpusSeed({ repo: repoSlug, root: checkoutRoot, run: gh.run, runtimeRoot: checkoutRoot });
|
|
1045
|
+
const consumingVersion = JSON.parse(fs.readFileSync(path.join(checkoutRoot, 'plugin', '.claude-plugin', 'plugin.json'), 'utf8')).version;
|
|
1046
|
+
const publishedReceipt = JSON.parse(fs.readFileSync(expected.receiptFile, 'utf8'));
|
|
1047
|
+
const record = {
|
|
1048
|
+
generation: index,
|
|
1049
|
+
selected: { origin: selection.seed.origin, tag: selection.seed.tag, builtUnder: selection.seed.brainVersion },
|
|
1050
|
+
consumingRuntime: consumingVersion,
|
|
1051
|
+
judged: selection.judged,
|
|
1052
|
+
expects: selection.expects,
|
|
1053
|
+
rejected: selection.rejected,
|
|
1054
|
+
ghCalls: gh.calls,
|
|
1055
|
+
archiveDownloadAttempts: gh.archiveDownloadAttempts().length,
|
|
1056
|
+
// The rule D4 removed, evaluated on the generation it would have judged. Recorded, not enforced.
|
|
1057
|
+
preD4RuntimeRule: publishedReceipt.archiveManifestVersion === consumingVersion ? 'would-accept'
|
|
1058
|
+
: `would-REJECT (generation shipped v${publishedReceipt.archiveManifestVersion}, consuming runtime is v${consumingVersion}) and reset to the bootstrap`,
|
|
1059
|
+
};
|
|
1060
|
+
receipt.seedSelection.push(record);
|
|
1061
|
+
const problems = [];
|
|
1062
|
+
if (selection.seed.origin !== 'published-generation' || selection.seed.tag !== expected.tag) {
|
|
1063
|
+
problems.push(`selected ${selection.seed.origin} ${selection.seed.tag}, expected the published generation ${expected.tag}`);
|
|
1064
|
+
}
|
|
1065
|
+
if (record.archiveDownloadAttempts !== 0) problems.push(`the resolver attempted ${record.archiveDownloadAttempts} archive download(s)`);
|
|
1066
|
+
for (const decoy of expected.decoys) {
|
|
1067
|
+
const row = selection.rejected.find((entry) => entry.tag === decoy.tag);
|
|
1068
|
+
const wanted = decoy.kind === 'model-mismatch' ? /^incompatible: .*different model\/dimensions/ : /^incompatible: recall report was measured against fixture/;
|
|
1069
|
+
if (!row || !wanted.test(row.reason)) problems.push(`decoy ${decoy.kind} was not skipped for its own reason (${row ? row.reason : 'not judged'})`);
|
|
1070
|
+
const recallFetched = gh.downloads().some((line) => line.includes(decoy.tag) && line.includes('--pattern ruvnet-brain.zip.recall.json '));
|
|
1071
|
+
if (decoy.kind === 'model-mismatch' && recallFetched) problems.push('the model-mismatch decoy was not rejected from its receipt alone');
|
|
1072
|
+
if (decoy.kind === 'fixture-mismatch' && !recallFetched) problems.push('the fixture-mismatch decoy was rejected without reading its recall report');
|
|
1073
|
+
}
|
|
1074
|
+
if (problems.length) {
|
|
1075
|
+
phase(`generation-${index}-seed-selection`, 'FAIL', { reason: problems.join('; ') });
|
|
1076
|
+
fail(`seed selection for generation ${index} failed: ${problems.join('; ')}`);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// The download corpus-seed.yml performs, then its defence-in-depth re-check -- which by construction
|
|
1080
|
+
// now only ever sees a generation the resolver already passed.
|
|
1081
|
+
const dir = path.join(workRoot, `seed-gen${index}-selected`);
|
|
1082
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1083
|
+
for (const name of ['ruvnet-brain.zip', 'corpus-receipt.json', 'ruvnet-brain.zip.recall.json', 'ruvnet-brain.zip.accuracy.json',
|
|
1084
|
+
'CORPUS-COVERAGE.json', 'coverage-receipt.json']) {
|
|
1085
|
+
fs.copyFileSync(path.join(registryDir, selection.seed.tag, 'assets', name), path.join(dir, name), fs.constants.COPYFILE_FICLONE);
|
|
1086
|
+
}
|
|
1087
|
+
const file = path.join(dir, 'ruvnet-brain.zip');
|
|
1088
|
+
const sha256 = sha256File(file);
|
|
1089
|
+
const bytes = fs.statSync(file).size;
|
|
1090
|
+
if (sha256 !== selection.seed.sha256 || bytes !== selection.seed.bytes) fail(`selected seed download does not match its descriptor (${sha256}/${bytes})`);
|
|
1091
|
+
const archive = { file: 'ruvnet-brain.zip', sha256, bytes };
|
|
1092
|
+
api.readDiagnosticAccuracyReport({ reportFile: `${file}.accuracy.json`, archive });
|
|
1093
|
+
// corpus-seed.yml's seed-recall-verify block: the seed's own sidecar coverage, verified, then handed to
|
|
1094
|
+
// the reader so a claimed retirement is recomputed rather than trusted (ADR-0091 D7.3).
|
|
1095
|
+
const coverageBytes = fs.readFileSync(path.join(dir, 'CORPUS-COVERAGE.json'));
|
|
1096
|
+
api.verifyCoverageSidecar({ sidecar: JSON.parse(fs.readFileSync(path.join(dir, 'coverage-receipt.json'), 'utf8')), coverageBytes,
|
|
1097
|
+
generationTag: selection.seed.tag, archiveSha256: sha256, archiveBytes: bytes });
|
|
1098
|
+
const fixture = api.loadFixture();
|
|
1099
|
+
const { report } = api.readRecallReport({ reportFile: `${file}.recall.json`, archive,
|
|
1100
|
+
expectedFixtureSha256: fixture.fixtureSha256, floorValue: 0, coverageBytes,
|
|
1101
|
+
fixtureStores: fixture.questions.map((question) => question.store) });
|
|
1102
|
+
record.downstreamRecheck = { repoCoverage: report.totals.repoCoverage, questions: report.totals.questions, hitTop5: report.totals.hitTop5,
|
|
1103
|
+
retired: report.totals.retired ?? 0 };
|
|
1104
|
+
phase(`generation-${index}-seed-selection`, 'PASS', {
|
|
1105
|
+
selected: selection.seed.tag, builtUnder: `v${selection.seed.brainVersion}`, consumingRuntime: `v${consumingVersion}`,
|
|
1106
|
+
skipped: selection.rejected.map((row) => `${row.tag.slice(0, 26)}...: ${row.reason}`),
|
|
1107
|
+
archiveDownloadAttempts: 0, preD4RuntimeRule: record.preD4RuntimeRule,
|
|
1108
|
+
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` });
|
|
1109
|
+
return { file, tag: selection.seed.tag, sha256, bytes, channel: 'published-generation-selected', allowPinnedTag: false,
|
|
1110
|
+
receiptFile: path.join(dir, 'corpus-receipt.json'), builtUnder: selection.seed.brainVersion,
|
|
1111
|
+
coverageFile: path.join(dir, 'CORPUS-COVERAGE.json') };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// ---------------------------------------------------------------------------------------------
|
|
1115
|
+
// ADR-0091 D6 (V6a/V6c): a CODE RELEASE consumes the published generation
|
|
1116
|
+
// ---------------------------------------------------------------------------------------------
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* What ci.yml's release-qe does after D6, against the local registry: resolve in code-release mode
|
|
1120
|
+
* (the real resolver, --require-coverage: decoys without a coverage sidecar are skipped), download the
|
|
1121
|
+
* generation plus its sidecar, and assemble through the REAL orchestrator on the single-pass path --
|
|
1122
|
+
* a real build-bundle run with the generation's sealed coverage and seed identity, which had never
|
|
1123
|
+
* been exercised against a generation before. V6a: the generation directory is byte-unchanged and the
|
|
1124
|
+
* archive's store bytes equal the generation's. D6.4: the frozen fixture is judged against the live
|
|
1125
|
+
* eligible set by the shared denominator.
|
|
1126
|
+
*/
|
|
1127
|
+
async function rehearseCodeRelease({ index, api, selectionMod, checkoutRoot, workRoot, registryDir, repoSlug, expected, receipt, phase, log }) {
|
|
1128
|
+
const name = `code-release-from-generation-${index}`;
|
|
1129
|
+
const gh = selectionMod.registryGh(registryDir);
|
|
1130
|
+
const selection = await api.resolveNextCorpusSeed({ repo: repoSlug, root: checkoutRoot, run: gh.run, runtimeRoot: checkoutRoot,
|
|
1131
|
+
requireCoverage: true });
|
|
1132
|
+
const record = { generation: index, selected: { origin: selection.seed.origin, tag: selection.seed.tag },
|
|
1133
|
+
rejected: selection.rejected, archiveDownloadAttempts: gh.archiveDownloadAttempts().length };
|
|
1134
|
+
receipt.codeReleaseConsumption = [...(receipt.codeReleaseConsumption || []), record];
|
|
1135
|
+
if (selection.seed.origin !== 'published-generation' || selection.seed.tag !== expected.tag || !selection.seed.coverage) {
|
|
1136
|
+
phase(name, 'FAIL', { reason: `code-release resolution chose ${selection.seed.origin} ${selection.seed.tag}, expected ${expected.tag} with its coverage` });
|
|
1137
|
+
fail(`${name}: the code-release resolver did not choose the published generation`);
|
|
1138
|
+
}
|
|
1139
|
+
const dir = path.join(workRoot, `code-release-gen${index}`);
|
|
1140
|
+
const evidence = path.join(dir, 'release-evidence');
|
|
1141
|
+
const extracted = path.join(dir, 'extracted');
|
|
1142
|
+
fs.mkdirSync(evidence, { recursive: true });
|
|
1143
|
+
for (const asset of ['ruvnet-brain.zip', 'CORPUS-COVERAGE.json', 'coverage-receipt.json']) {
|
|
1144
|
+
fs.copyFileSync(path.join(registryDir, selection.seed.tag, 'assets', asset), path.join(dir, asset), fs.constants.COPYFILE_FICLONE);
|
|
1145
|
+
}
|
|
1146
|
+
const zip = path.join(dir, 'ruvnet-brain.zip');
|
|
1147
|
+
if (sha256File(zip) !== selection.seed.sha256) fail(`${name}: downloaded generation does not match its descriptor`);
|
|
1148
|
+
await api.extractZip(zip, extracted);
|
|
1149
|
+
const descriptorFile = path.join(evidence, 'corpus-seed.json');
|
|
1150
|
+
fs.writeFileSync(descriptorFile, `${JSON.stringify(selection.seed, null, 2)}\n`);
|
|
1151
|
+
const outputs = [];
|
|
1152
|
+
const capture = (command, args, options = {}) => {
|
|
1153
|
+
const result = spawnSync(command, args, { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024, ...options });
|
|
1154
|
+
outputs.push({ script: path.basename(String(args?.[0] || command)), exit: result.status,
|
|
1155
|
+
tail: result.status === 0 ? undefined : String(result.stderr || result.stdout || '').trim().slice(-800) });
|
|
1156
|
+
return result;
|
|
1157
|
+
};
|
|
1158
|
+
const started = Date.now();
|
|
1159
|
+
let assembled;
|
|
1160
|
+
try {
|
|
1161
|
+
assembled = api.assembleCodeReleaseCorpus({ root: checkoutRoot, descriptor: selection.seed, seedBundle: zip, assetsDir: extracted,
|
|
1162
|
+
evidenceDir: evidence, coverageFile: path.join(dir, 'CORPUS-COVERAGE.json'),
|
|
1163
|
+
coverageReceiptFile: path.join(dir, 'coverage-receipt.json'), run: capture, env: { ...process.env } });
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
record.steps = outputs;
|
|
1166
|
+
phase(name, 'FAIL', { reason: error.message, steps: outputs });
|
|
1167
|
+
throw error;
|
|
1168
|
+
}
|
|
1169
|
+
record.assembly = { mode: assembled.mode, durationMs: Date.now() - started, steps: outputs, unmutated: assembled.unmutated,
|
|
1170
|
+
seedTree: assembled.seedTree };
|
|
1171
|
+
const releaseCoverage = JSON.parse(fs.readFileSync(path.join(checkoutRoot, 'dist', 'ruvnet-brain', 'COVERAGE.json'), 'utf8'));
|
|
1172
|
+
// ADR-0091 D7.1: a code release built FROM a generation names it, and dates itself by its observation.
|
|
1173
|
+
const releaseCorpus = JSON.parse(fs.readFileSync(path.join(checkoutRoot, 'dist', 'ruvnet-brain', 'manifest.json'), 'utf8')).corpus;
|
|
1174
|
+
const generationCoverage = JSON.parse(fs.readFileSync(path.join(dir, 'CORPUS-COVERAGE.json'), 'utf8'));
|
|
1175
|
+
record.manifestCorpus = releaseCorpus;
|
|
1176
|
+
if (releaseCorpus?.basis !== 'sealed-observation' || releaseCorpus.generationTag !== selection.seed.tag
|
|
1177
|
+
|| releaseCorpus.observedAt !== generationCoverage.observedAt) {
|
|
1178
|
+
phase(name, 'FAIL', { reason: `code-release manifest.corpus does not name its generation and observation: ${JSON.stringify(releaseCorpus)}` });
|
|
1179
|
+
fail(`${name}: manifest.corpus is ${JSON.stringify(releaseCorpus)}`);
|
|
1180
|
+
}
|
|
1181
|
+
const fixture = JSON.parse(fs.readFileSync(path.join(checkoutRoot, 'data', 'retrieval-query-evidence.json'), 'utf8'));
|
|
1182
|
+
const denominator = api.fixtureDenominator({ coverage: releaseCoverage, fixtureStores: Object.keys(fixture.queries) });
|
|
1183
|
+
record.canaryDenominator = { fixture: denominator.fixture.length, questioned: denominator.questioned.length,
|
|
1184
|
+
retired: denominator.retired, blocking: denominator.blocking, unfixturedEligible: denominator.unfixturedEligible.length,
|
|
1185
|
+
corpusSeed: releaseCoverage.corpusSeed };
|
|
1186
|
+
if (assembled.mode !== 'single-pass' || denominator.blocking.length || releaseCoverage.corpusSeed?.tag !== selection.seed.tag) {
|
|
1187
|
+
phase(name, 'FAIL', { reason: `mode=${assembled.mode}, blocking=${JSON.stringify(denominator.blocking)}, coverage seed=${releaseCoverage.corpusSeed?.tag}` });
|
|
1188
|
+
fail(`${name}: the code release did not assemble single-pass from the generation with an admissible canary denominator`);
|
|
1189
|
+
}
|
|
1190
|
+
const guard = api.checkNoNewerCorpusGeneration({ sealed: selection.seed,
|
|
1191
|
+
resolution: await api.resolveNextCorpusSeed({ repo: repoSlug, root: checkoutRoot, run: selectionMod.registryGh(registryDir).run,
|
|
1192
|
+
runtimeRoot: checkoutRoot, requireCoverage: true }) });
|
|
1193
|
+
record.publishGuardBeforeNextGeneration = guard;
|
|
1194
|
+
phase(name, 'PASS', { mode: assembled.mode, durationMs: record.assembly.durationMs, unmutated: assembled.unmutated,
|
|
1195
|
+
canary: `${denominator.questioned.length}/${denominator.fixture.length} fixture stores questioned, ${denominator.retired.length} retired, `
|
|
1196
|
+
+ `${denominator.unfixturedEligible.length} unfixtured eligible recorded`,
|
|
1197
|
+
reason: `release built single-pass from ${selection.seed.tag.slice(0, 26)}...: ${assembled.unmutated.files} store files byte-equal to the generation, `
|
|
1198
|
+
+ 'generation directory unchanged, no capability-only refresh or index repair; the publish guard passes while no newer generation exists' });
|
|
1199
|
+
log(`[${name}] single-pass code release assembled in ${record.assembly.durationMs} ms`);
|
|
1200
|
+
return { sealed: selection.seed, record };
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/** V6c: once generation N+1 is published, a release QE'd from generation N must be refused at publish. */
|
|
1204
|
+
async function rehearsePublishGuard({ sealed, index, api, selectionMod, checkoutRoot, registryDir, repoSlug, record, phase }) {
|
|
1205
|
+
const name = `publish-guard-after-generation-${index}`;
|
|
1206
|
+
const resolution = await api.resolveNextCorpusSeed({ repo: repoSlug, root: checkoutRoot, run: selectionMod.registryGh(registryDir).run,
|
|
1207
|
+
runtimeRoot: checkoutRoot, requireCoverage: true });
|
|
1208
|
+
try {
|
|
1209
|
+
api.checkNoNewerCorpusGeneration({ sealed, resolution });
|
|
1210
|
+
} catch (error) {
|
|
1211
|
+
record.publishGuardAfterNextGeneration = { refused: true, reason: error.message, newest: resolution.seed.tag };
|
|
1212
|
+
if (!/re-run release QE/.test(error.message)) {
|
|
1213
|
+
phase(name, 'FAIL', { reason: `refused for the wrong reason: ${error.message}` });
|
|
1214
|
+
fail(`${name}: the guard refused for the wrong reason`);
|
|
1215
|
+
}
|
|
1216
|
+
phase(name, 'PASS', { reason: `publication of the release QE'd from ${sealed.tag.slice(0, 26)}... was REFUSED: ${error.message}` });
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
phase(name, 'FAIL', { reason: `generation ${index} was published after QE, yet the publish guard allowed the older release` });
|
|
1220
|
+
fail(`${name}: the backward-move guard did not refuse`);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
718
1223
|
// ---------------------------------------------------------------------------------------------
|
|
719
1224
|
// The rehearsal
|
|
720
1225
|
// ---------------------------------------------------------------------------------------------
|
|
@@ -728,14 +1233,34 @@ export async function rehearseCorpusPipeline({
|
|
|
728
1233
|
owner = 'ruvnet',
|
|
729
1234
|
tamper = 'none',
|
|
730
1235
|
tamperGeneration = 1,
|
|
1236
|
+
injectStoreFailure = 'none',
|
|
1237
|
+
injectGeneration = 1,
|
|
1238
|
+
recallFixture = 'scoped',
|
|
731
1239
|
forceRebuild = true,
|
|
732
1240
|
seedOverride = null,
|
|
1241
|
+
// ADR-0091 D4. seedSelection: generation N+1's seed is chosen by the real corpus-next-seed resolver
|
|
1242
|
+
// from a local registry of published generations (plus incompatible decoys), not handed over
|
|
1243
|
+
// directly. codeReleaseBetween: cut a version bump in the disposable checkout between generations,
|
|
1244
|
+
// so the chosen seed was built under an OLDER runtime than the one consuming it.
|
|
1245
|
+
seedSelection = true,
|
|
1246
|
+
codeReleaseBetween = true,
|
|
1247
|
+
// ADR-0091 D6: after generation N is published locally, a code release consumes it (single-pass,
|
|
1248
|
+
// real build) and, once generation N+1 is published, the publish-time guard must refuse that release.
|
|
1249
|
+
codeReleaseConsumption = true,
|
|
1250
|
+
// null = the checkout's own C3_DIAGNOSTIC_SAMPLE_QUESTIONS (what CI runs); 'full' = no sample.
|
|
1251
|
+
accuracySample = null,
|
|
733
1252
|
keep = false,
|
|
734
1253
|
workRootParent = null,
|
|
735
1254
|
installedBrainDir = path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb'),
|
|
736
1255
|
log = (line) => process.stderr.write(`${line}\n`),
|
|
737
1256
|
} = {}) {
|
|
738
1257
|
if (!TAMPER_MODES.has(tamper)) fail(`unknown --tamper mode ${tamper} (expected: ${[...TAMPER_MODES].join(', ')})`);
|
|
1258
|
+
if (!RECALL_FIXTURE_MODES.has(recallFixture)) {
|
|
1259
|
+
fail(`unknown --recall-fixture mode ${recallFixture} (expected: ${[...RECALL_FIXTURE_MODES].join(', ')})`);
|
|
1260
|
+
}
|
|
1261
|
+
if (!INJECTION_MODES.has(injectStoreFailure)) {
|
|
1262
|
+
fail(`unknown --inject-store-failure mode ${injectStoreFailure} (expected: ${[...INJECTION_MODES].join(', ')})`);
|
|
1263
|
+
}
|
|
739
1264
|
const startedAt = new Date().toISOString();
|
|
740
1265
|
const started = Date.now();
|
|
741
1266
|
const receipt = {
|
|
@@ -744,9 +1269,12 @@ export async function rehearseCorpusPipeline({
|
|
|
744
1269
|
banner: BANNER,
|
|
745
1270
|
startedAt,
|
|
746
1271
|
host: { node: process.version, platform: `${process.platform}/${process.arch}` },
|
|
747
|
-
bounds: { repos, gists, generations, maxRounds, owner, forceRebuild, tamper, tamperGeneration
|
|
1272
|
+
bounds: { repos, gists, generations, maxRounds, owner, forceRebuild, tamper, tamperGeneration, accuracySample,
|
|
1273
|
+
seedSelection, codeReleaseBetween, codeReleaseConsumption, injectStoreFailure, injectGeneration, recallFixture },
|
|
748
1274
|
phases: [],
|
|
749
1275
|
generations: [],
|
|
1276
|
+
seedSelection: [],
|
|
1277
|
+
approvedRuntime: [],
|
|
750
1278
|
verdict: 'FAIL',
|
|
751
1279
|
};
|
|
752
1280
|
const phase = (name, status, detail = {}) => {
|
|
@@ -817,10 +1345,21 @@ export async function rehearseCorpusPipeline({
|
|
|
817
1345
|
|
|
818
1346
|
// ---- phase 4: load the disposable checkout's own modules ---------------------------------
|
|
819
1347
|
const load = (relative) => import(pathToFileURL(path.join(checkoutRoot, relative)).href);
|
|
820
|
-
const [reconcileMod, coverageMod, candidateMod, zipMod] = await Promise.all([
|
|
1348
|
+
const [reconcileMod, coverageMod, candidateMod, zipMod, accuracyMod, nextSeedMod, approvedMod, recallMod, selectionMod, failureMod] = await Promise.all([
|
|
821
1349
|
load('scripts/corpus-reconcile.mjs'), load('scripts/source-coverage.mjs'),
|
|
822
|
-
load('scripts/corpus-candidate.mjs'), load('kb/zip-extract.mjs'),
|
|
1350
|
+
load('scripts/corpus-candidate.mjs'), load('kb/zip-extract.mjs'), load('scripts/oracle/retrieval-accuracy.mjs'),
|
|
1351
|
+
load('scripts/corpus-next-seed.mjs'), load('scripts/approved-runtime.mjs'), load('scripts/oracle/repo-recall.mjs'),
|
|
1352
|
+
load('scripts/rehearse-seed-selection.mjs'), load('scripts/corpus-store-failure.mjs'),
|
|
823
1353
|
]);
|
|
1354
|
+
const [codeReleaseMod, denominatorMod, sidecarMod] = await Promise.all([
|
|
1355
|
+
load('scripts/code-release-corpus.mjs'), load('scripts/fixture-denominator.mjs'), load('scripts/corpus-coverage-sidecar.mjs')]);
|
|
1356
|
+
// The same C3 question sample CI runs (ADR-0091 D2), unless the caller asked for the full audit.
|
|
1357
|
+
const accuracyQuestions = accuracySample === 'full' ? null
|
|
1358
|
+
: accuracySample == null ? accuracyMod.C3_DIAGNOSTIC_SAMPLE_QUESTIONS : Number(accuracySample);
|
|
1359
|
+
if (accuracyQuestions != null && (!Number.isSafeInteger(accuracyQuestions) || accuracyQuestions <= 0)) {
|
|
1360
|
+
fail(`--accuracy-sample must be a positive integer or 'full' (got ${accuracySample})`);
|
|
1361
|
+
}
|
|
1362
|
+
receipt.bounds.accuracySample = accuracyQuestions ?? 'full';
|
|
824
1363
|
const api = {
|
|
825
1364
|
assertBootstrapIdentity: reconcileMod.assertBootstrapIdentity,
|
|
826
1365
|
normalizeExtractedCorpus: reconcileMod.normalizeExtractedCorpus,
|
|
@@ -828,16 +1367,33 @@ export async function rehearseCorpusPipeline({
|
|
|
828
1367
|
syncCorpusInputs: reconcileMod.syncCorpusInputs,
|
|
829
1368
|
acquireCorpusGeneration: reconcileMod.acquireCorpusGeneration,
|
|
830
1369
|
reconcileAndPrepareCorpusCandidate: reconcileMod.reconcileAndPrepareCorpusCandidate,
|
|
1370
|
+
summarizeReconciliation: reconcileMod.summarizeReconciliation,
|
|
831
1371
|
prepareCorpusCandidate: reconcileMod.prepareCorpusCandidate,
|
|
1372
|
+
executeReconciliation: reconcileMod.executeReconciliation,
|
|
1373
|
+
defaultRunAsync: reconcileMod.defaultRunAsync,
|
|
1374
|
+
degradedPublication: failureMod.degradedPublication,
|
|
832
1375
|
observeSourceUniverse: coverageMod.observeSourceUniverse,
|
|
833
1376
|
canonicalSourceObservation: coverageMod.canonicalSourceObservation,
|
|
834
1377
|
buildCoverage: coverageMod.buildCoverage,
|
|
835
1378
|
verifySeedBaseline: candidateMod.verifySeedBaseline,
|
|
836
1379
|
extractZip: zipMod.extractZip,
|
|
1380
|
+
resolveNextCorpusSeed: nextSeedMod.resolveNextCorpusSeed,
|
|
1381
|
+
verifyApprovedRuntime: approvedMod.verifyApprovedRuntime,
|
|
1382
|
+
emitApprovedRuntime: approvedMod.emitApprovedRuntime,
|
|
1383
|
+
readArchiveManifestFromZip: approvedMod.readArchiveManifestFromZip,
|
|
1384
|
+
isRuntimeFile: approvedMod.isRuntimeFile,
|
|
1385
|
+
readRecallReport: recallMod.readRecallReport,
|
|
1386
|
+
loadFixture: recallMod.loadFixture,
|
|
1387
|
+
readDiagnosticAccuracyReport: accuracyMod.readDiagnosticAccuracyReport,
|
|
1388
|
+
assembleCodeReleaseCorpus: codeReleaseMod.assembleCodeReleaseCorpus,
|
|
1389
|
+
checkNoNewerCorpusGeneration: codeReleaseMod.checkNoNewerCorpusGeneration,
|
|
1390
|
+
fixtureDenominator: denominatorMod.fixtureDenominator,
|
|
1391
|
+
verifyCoverageSidecar: sidecarMod.verifyCoverageSidecar,
|
|
837
1392
|
};
|
|
838
1393
|
for (const [name, value] of Object.entries(api)) {
|
|
839
1394
|
if (typeof value !== 'function') fail(`the disposable checkout does not export ${name}`);
|
|
840
1395
|
}
|
|
1396
|
+
api.CORPUS_QA_FAILED_EXIT = failureMod.CORPUS_QA_FAILED_EXIT;
|
|
841
1397
|
phase('load-pipeline-entrypoints', 'PASS', { entrypoints: Object.keys(api).length });
|
|
842
1398
|
|
|
843
1399
|
// ---- phase 5: real seed acquisition (or an explicit SKIP) --------------------------------
|
|
@@ -864,14 +1420,31 @@ export async function rehearseCorpusPipeline({
|
|
|
864
1420
|
|
|
865
1421
|
// ---- phase 6..N: the generations ---------------------------------------------------------
|
|
866
1422
|
let seed = { file: acquisition.file, tag: descriptor.tag, sha256: descriptor.sha256, bytes: descriptor.bytes,
|
|
867
|
-
channel: acquisition.channel, allowPinnedTag: true };
|
|
1423
|
+
channel: acquisition.channel, allowPinnedTag: true, builtUnder: String(descriptor.tag).replace(/^v/, '') };
|
|
1424
|
+
let builderSha = checkout.head;
|
|
1425
|
+
const registryDir = path.join(workRoot, 'release-registry');
|
|
1426
|
+
fs.mkdirSync(registryDir, { recursive: true });
|
|
1427
|
+
let lastPublished = null;
|
|
1428
|
+
let codeRelease = null;
|
|
868
1429
|
for (let index = 1; index <= generations; index += 1) {
|
|
1430
|
+
if (index > 1 && codeReleaseBetween) {
|
|
1431
|
+
const release = selectionMod.simulateCodeRelease({ checkoutRoot });
|
|
1432
|
+
builderSha = release.head;
|
|
1433
|
+
receipt.codeReleases = [...(receipt.codeReleases || []), { beforeGeneration: index, ...release }];
|
|
1434
|
+
phase(`code-release-before-generation-${index}`, 'PASS', { from: release.from, to: release.to, head: release.head,
|
|
1435
|
+
committed: release.committed,
|
|
1436
|
+
reason: `a code release (v${release.from} -> v${release.to}) landed between generations, the event that reset the pre-D4 seed chain` });
|
|
1437
|
+
}
|
|
1438
|
+
if (index > 1 && seedSelection) {
|
|
1439
|
+
seed = await selectPublishedSeed({ index, api, selectionMod, checkoutRoot, workRoot, registryDir, repoSlug,
|
|
1440
|
+
expected: lastPublished, receipt, phase });
|
|
1441
|
+
}
|
|
869
1442
|
if (index > 1) {
|
|
870
1443
|
// Import candidate N as seed N+1 through the SAME baseline verifier the future consumer
|
|
871
1444
|
// uses — the content-addressed tag, the archive bytes, and the schema-2 receipt together.
|
|
872
1445
|
const baseline = await api.verifySeedBaseline({
|
|
873
1446
|
seedDescriptor: { tag: seed.tag, sha256: seed.sha256, bytes: seed.bytes },
|
|
874
|
-
bundleFile: seed.file, receiptFile: seed.receiptFile,
|
|
1447
|
+
bundleFile: seed.file, receiptFile: seed.receiptFile, coverageFile: seed.coverageFile ?? null,
|
|
875
1448
|
});
|
|
876
1449
|
phase(`generation-${index}-seed-baseline`, 'PASS', {
|
|
877
1450
|
tag: baseline.tag, sha256: baseline.sha256, stores: baseline.receipt.storeCount,
|
|
@@ -879,13 +1452,53 @@ export async function rehearseCorpusPipeline({
|
|
|
879
1452
|
}
|
|
880
1453
|
const result = await runGeneration({
|
|
881
1454
|
index, api, checkoutRoot, workRoot, seed, recorder, tamper, log,
|
|
882
|
-
bounds: { repos, gists, owner, maxRounds, builderSha
|
|
1455
|
+
bounds: { repos, gists, owner, maxRounds, builderSha, forceRebuild, tamperGeneration, accuracyQuestions,
|
|
1456
|
+
injectStoreFailure, injectGeneration, recallFixture },
|
|
883
1457
|
});
|
|
884
1458
|
receipt.generations.push(result.generation);
|
|
885
1459
|
phase(`generation-${index}`, 'PASS', {
|
|
886
1460
|
candidateSha256: result.generation.candidate.sha256,
|
|
887
1461
|
stores: result.generation.candidate.storeCount,
|
|
888
1462
|
durationMs: result.generation.reconciliation.durationMs });
|
|
1463
|
+
|
|
1464
|
+
// ADR-0091 D4 / V4: the candidate's runtime surface, judged by the real verifyApprovedRuntime
|
|
1465
|
+
// against a pin traced to the CHECKOUT's own bytes (never emitted from the candidate).
|
|
1466
|
+
const manifest = api.readArchiveManifestFromZip(result.generation.candidate.bundleFile);
|
|
1467
|
+
const version = JSON.parse(fs.readFileSync(path.join(checkoutRoot, 'plugin', '.claude-plugin', 'plugin.json'), 'utf8')).version;
|
|
1468
|
+
const { pin, traced, untraced } = selectionMod.checkoutRuntimePin({
|
|
1469
|
+
checkoutRoot, manifest, approvedCodeSha: builderSha, version, api });
|
|
1470
|
+
const verdict = api.verifyApprovedRuntime({ manifest, pin });
|
|
1471
|
+
const runtimeCheck = { generation: index, seedTag: seed.tag, seedBuiltUnder: seed.builtUnder ?? null,
|
|
1472
|
+
consumingRuntime: `v${version}`, pinnedFromCheckout: traced, untraced, verdict: verdict.verdict,
|
|
1473
|
+
checked: verdict.checked, failures: verdict.failures };
|
|
1474
|
+
receipt.approvedRuntime.push(runtimeCheck);
|
|
1475
|
+
if (verdict.verdict !== 'PASS') {
|
|
1476
|
+
phase(`generation-${index}-approved-runtime`, 'FAIL', { ...runtimeCheck, reason: verdict.failures.slice(0, 5).join('; ') });
|
|
1477
|
+
fail(`generation ${index}'s candidate fails verifyApprovedRuntime against the checkout: ${verdict.failures.slice(0, 5).join('; ')}`);
|
|
1478
|
+
}
|
|
1479
|
+
phase(`generation-${index}-approved-runtime`, 'PASS', { ...runtimeCheck,
|
|
1480
|
+
reason: `verifyApprovedRuntime PASS: ${verdict.checked} runtime files equal the checkout's, none unpinned, seed ${seed.tag} did not leak an executable` });
|
|
1481
|
+
|
|
1482
|
+
if (result.withheld) {
|
|
1483
|
+
phase(`generation-${index}-publication`, 'PASS', { status: 'WITHHELD-DEGRADED', ...result.generation.publication,
|
|
1484
|
+
reason: `degraded generation sealed but not published (ADR-0091 D5/D10): ${result.generation.publication.reason}` });
|
|
1485
|
+
if (index < generations) {
|
|
1486
|
+
phase('remaining-generations', 'SKIP', { reason: `generation ${index} was withheld, so there is no published generation ${index} to seed from` });
|
|
1487
|
+
}
|
|
1488
|
+
break;
|
|
1489
|
+
}
|
|
1490
|
+
const consumeNow = seedSelection && codeReleaseConsumption && !codeRelease;
|
|
1491
|
+
if (seedSelection && (index < generations || codeRelease)) {
|
|
1492
|
+
lastPublished = publishLocally({ selectionMod, registryDir, published: result.published, workRoot, index });
|
|
1493
|
+
}
|
|
1494
|
+
if (codeRelease) {
|
|
1495
|
+
await rehearsePublishGuard({ sealed: codeRelease.sealed, index, api, selectionMod, checkoutRoot, registryDir, repoSlug,
|
|
1496
|
+
record: codeRelease.record, phase });
|
|
1497
|
+
codeRelease = null;
|
|
1498
|
+
} else if (consumeNow && index < generations) {
|
|
1499
|
+
codeRelease = await rehearseCodeRelease({ index, api, selectionMod, checkoutRoot, workRoot, registryDir, repoSlug,
|
|
1500
|
+
expected: lastPublished, receipt, phase, log });
|
|
1501
|
+
}
|
|
889
1502
|
seed = result.nextSeed;
|
|
890
1503
|
}
|
|
891
1504
|
|
|
@@ -972,8 +1585,15 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
972
1585
|
owner: arg(argv, '--owner', 'ruvnet'),
|
|
973
1586
|
tamper: arg(argv, '--tamper', 'none'),
|
|
974
1587
|
tamperGeneration: Number(arg(argv, '--tamper-generation', 1)),
|
|
1588
|
+
injectStoreFailure: arg(argv, '--inject-store-failure', 'none'),
|
|
1589
|
+
injectGeneration: Number(arg(argv, '--inject-generation', 1)),
|
|
1590
|
+
recallFixture: arg(argv, '--recall-fixture', 'scoped'),
|
|
975
1591
|
forceRebuild: !argv.includes('--no-force-rebuild'),
|
|
976
1592
|
seedOverride: arg(argv, '--seed', null),
|
|
1593
|
+
seedSelection: !argv.includes('--no-seed-selection'),
|
|
1594
|
+
codeReleaseBetween: !argv.includes('--no-code-release-between'),
|
|
1595
|
+
codeReleaseConsumption: !argv.includes('--no-code-release-consumption'),
|
|
1596
|
+
accuracySample: arg(argv, '--accuracy-sample', null),
|
|
977
1597
|
keep: argv.includes('--keep'),
|
|
978
1598
|
workRootParent: arg(argv, '--work-root', null),
|
|
979
1599
|
});
|