ruvnet-brain 4.3.35 → 4.3.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,7 +44,14 @@
44
44
  // Usage:
45
45
  // node scripts/rehearse-corpus-pipeline.mjs [--repos 2] [--gists 3] [--generations 2]
46
46
  // [--accuracy-sample <n>|full] [--receipt <file>] [--seed <local ruvnet-brain.zip>] [--keep] [--tamper <mode>]
47
- // [--no-seed-selection] [--no-code-release-between]
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.
48
55
  //
49
56
  // ADR-0091 D4 (V4): from generation 2 on, the seed is chosen by the REAL scripts/corpus-next-seed.mjs
50
57
  // resolver from a local registry holding generation N (exactly the files release.mjs tried to upload)
@@ -67,6 +74,13 @@ export const REAL_ROOT = path.resolve(HERE, '..');
67
74
  const BANNER = 'BOUNDED REHEARSAL — a small explicitly-bounded subset, NOT a full corpus build';
68
75
  const HEX64 = /^[a-f0-9]{64}$/;
69
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']);
70
84
  const PROBE_ARG = '__rehearsal_probe__';
71
85
 
72
86
  // Every `gh`/`npm` invocation that would MUTATE anything outside this process. Matched against the
@@ -636,6 +650,41 @@ export function recordReconciliation({ summarize, reconciliation, durationMs })
636
650
  };
637
651
  }
638
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
+
639
688
  async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds, recorder, tamper, log }) {
640
689
  const generation = { index, startedAt: new Date().toISOString(), seed: { tag: seed.tag, sha256: seed.sha256, bytes: seed.bytes, channel: seed.channel } };
641
690
  const assetsDir = path.join(workRoot, `assets-gen${index}`);
@@ -682,9 +731,43 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
682
731
  // seed RVF bytes so planReconciliation must rebuild them from upstream. Without this a seed whose
683
732
  // stores are already CURRENT would exercise no acquisition at all, and the rehearsal would prove
684
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
+ }
685
766
  if (bounds.forceRebuild) {
686
- for (const store of repoStores) fs.rmSync(path.join(assetsDir, `${store}.big.rvf`), { force: true });
687
- generation.forcedRebuild = repoStores;
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;
688
771
  }
689
772
 
690
773
  // Every other seed store stays in scope FROZEN at its seed sourceCommit (see boundObservation), so
@@ -706,11 +789,16 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
706
789
  .repositories.rows.map((row) => String(row.storeName || row.name));
707
790
  const scoped = scopeRecallFixture({
708
791
  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,
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,
711
797
  excludedStores: scoped.excluded,
712
798
  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)`);
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)`);
714
802
 
715
803
  // --- reconcile + assemble ONCE --------------------------------------------------------------
716
804
  const invocations = [];
@@ -723,6 +811,29 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
723
811
  row.exit = result.status ?? null;
724
812
  return result;
725
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);
836
+ };
726
837
  const reconcileStart = Date.now();
727
838
  const { reconciliation, candidate } = await api.reconcileAndPrepareCorpusCandidate({
728
839
  assetsDir, workspaceDir, root: checkoutRoot, owner: bounds.owner, builderSha: bounds.builderSha,
@@ -730,7 +841,8 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
730
841
  // reconcileAndPrepareCorpusCandidate reads `maxAttempts`; `maxRounds` was silently ignored after
731
842
  // cd0f032f, so --max-rounds never reached the acquisition loop.
732
843
  bootstrapIdentity, maxAttempts: bounds.maxRounds, accuracySample: bounds.accuracyQuestions,
733
- reconcile: (options) => api.acquireCorpusGeneration({ ...options, observe: boundedObserve }),
844
+ reconcile: (options) => api.acquireCorpusGeneration({ ...options, observe: boundedObserve,
845
+ ...(injection !== 'none' ? { execute: (executeOptions) => api.executeReconciliation({ ...executeOptions, run: injectingRun }) } : {}) }),
734
846
  prepare: (options) => api.prepareCorpusCandidate({ ...options, run: recordingRun }),
735
847
  });
736
848
  generation.reconciliation = recordReconciliation({
@@ -765,6 +877,38 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
765
877
  generation.candidate.storeCount = receipt.storeCount;
766
878
  generation.candidate.stores = receipt.stores.map(({ name, kind }) => ({ name, kind }));
767
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}`);
768
912
 
769
913
  // --- remove access to the originals, then verify the extracted bytes ------------------------
770
914
  generation.extractedByteVerification = await verifyExtractedBytesWithoutOriginals({
@@ -773,8 +917,20 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
773
917
  });
774
918
  log(`[gen ${index}] extracted-byte verification passed with staging revoked (${generation.extractedByteVerification.files} files)`);
775
919
 
776
- // --- rehearse the real product-consumption path, publication RECORDED not executed -----------
920
+ // --- ADR-0091 D5 + D10: a degraded generation is sealed but not published until D10 allows it ---
777
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 -----------
778
934
  const proof = recorder.proveIntercepting();
779
935
  if (!proof.ok) {
780
936
  generation.publication = { status: 'FAIL', reason: 'command interception could not be proven; refusing to run a publication path that might reach the network', proof };
@@ -793,8 +949,13 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
793
949
  GH_TOKEN: 'rehearsal-invalid-token-do-not-use',
794
950
  GITHUB_TOKEN: 'rehearsal-invalid-token-do-not-use',
795
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);
796
956
  const publish = run(process.execPath, [path.join(checkoutRoot, 'scripts', 'release.mjs'), '--corpus-seed',
797
957
  '--corpus-tag', corpusTag, '--corpus-bundle', bundleFile, '--corpus-receipt', candidate.receiptFile,
958
+ '--corpus-coverage', stagedCoverage,
798
959
  '--target', bounds.builderSha, '--repo', 'stuinfla/ruvnet-brain'], { cwd: checkoutRoot, env: publishEnv });
799
960
  const recordedNow = recorder.recorded().filter((row) => row.classification === 'publication-mutation');
800
961
  generation.publication = {
@@ -830,6 +991,10 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
830
991
  if (!fs.existsSync(report)) fail(`candidate ${index} has no detached ${suffix} report to hand to generation ${index + 1}`);
831
992
  fs.copyFileSync(report, `${nextSeedFile}${suffix}`);
832
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);
833
998
  generation.nextSeed = { tag: corpusTag, sha256: archiveSha256, bytes: archiveBytes, file: nextSeedFile,
834
999
  contentAddressed: true };
835
1000
  generation.finishedAt = new Date().toISOString();
@@ -837,7 +1002,7 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
837
1002
  return {
838
1003
  generation,
839
1004
  nextSeed: { file: nextSeedFile, tag: corpusTag, sha256: archiveSha256, bytes: archiveBytes,
840
- channel: `candidate-generation-${index}`, allowPinnedTag: false, receiptFile: candidate.receiptFile },
1005
+ channel: `candidate-generation-${index}`, allowPinnedTag: false, receiptFile: candidate.receiptFile, coverageFile: nextSeedCoverage },
841
1006
  // ADR-0091 D4: exactly what release.mjs tried to upload, so the local registry invents nothing.
842
1007
  published: { tag: corpusTag, createArgs: createRow ? createRow.args : [], receiptFile: candidate.receiptFile,
843
1008
  recallFile: `${bundleFile}.recall.json`, accuracyFile: `${bundleFile}.accuracy.json`, sha256: archiveSha256, bytes: archiveBytes },
@@ -853,7 +1018,8 @@ async function runGeneration({ index, api, checkoutRoot, workRoot, seed, bounds,
853
1018
  function publishLocally({ selectionMod, registryDir, published, workRoot, index }) {
854
1019
  const files = selectionMod.uploadedFilesOf(published.createArgs);
855
1020
  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'];
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'];
857
1023
  if (JSON.stringify(names) !== JSON.stringify(expectedNames)) {
858
1024
  fail(`release.mjs's recorded upload for generation ${index} is [${names.join(', ')}], expected [${expectedNames.join(', ')}]`);
859
1025
  }
@@ -914,7 +1080,8 @@ async function selectPublishedSeed({ index, api, selectionMod, checkoutRoot, wor
914
1080
  // now only ever sees a generation the resolver already passed.
915
1081
  const dir = path.join(workRoot, `seed-gen${index}-selected`);
916
1082
  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']) {
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']) {
918
1085
  fs.copyFileSync(path.join(registryDir, selection.seed.tag, 'assets', name), path.join(dir, name), fs.constants.COPYFILE_FICLONE);
919
1086
  }
920
1087
  const file = path.join(dir, 'ruvnet-brain.zip');
@@ -923,16 +1090,134 @@ async function selectPublishedSeed({ index, api, selectionMod, checkoutRoot, wor
923
1090
  if (sha256 !== selection.seed.sha256 || bytes !== selection.seed.bytes) fail(`selected seed download does not match its descriptor (${sha256}/${bytes})`);
924
1091
  const archive = { file: 'ruvnet-brain.zip', sha256, bytes };
925
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();
926
1099
  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 };
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 };
929
1104
  phase(`generation-${index}-seed-selection`, 'PASS', {
930
1105
  selected: selection.seed.tag, builtUnder: `v${selection.seed.brainVersion}`, consumingRuntime: `v${consumingVersion}`,
931
1106
  skipped: selection.rejected.map((row) => `${row.tag.slice(0, 26)}...: ${row.reason}`),
932
1107
  archiveDownloadAttempts: 0, preD4RuntimeRule: record.preD4RuntimeRule,
933
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` });
934
1109
  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 };
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`);
936
1221
  }
937
1222
 
938
1223
  // ---------------------------------------------------------------------------------------------
@@ -948,6 +1233,9 @@ export async function rehearseCorpusPipeline({
948
1233
  owner = 'ruvnet',
949
1234
  tamper = 'none',
950
1235
  tamperGeneration = 1,
1236
+ injectStoreFailure = 'none',
1237
+ injectGeneration = 1,
1238
+ recallFixture = 'scoped',
951
1239
  forceRebuild = true,
952
1240
  seedOverride = null,
953
1241
  // ADR-0091 D4. seedSelection: generation N+1's seed is chosen by the real corpus-next-seed resolver
@@ -956,6 +1244,9 @@ export async function rehearseCorpusPipeline({
956
1244
  // so the chosen seed was built under an OLDER runtime than the one consuming it.
957
1245
  seedSelection = true,
958
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,
959
1250
  // null = the checkout's own C3_DIAGNOSTIC_SAMPLE_QUESTIONS (what CI runs); 'full' = no sample.
960
1251
  accuracySample = null,
961
1252
  keep = false,
@@ -964,6 +1255,12 @@ export async function rehearseCorpusPipeline({
964
1255
  log = (line) => process.stderr.write(`${line}\n`),
965
1256
  } = {}) {
966
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
+ }
967
1264
  const startedAt = new Date().toISOString();
968
1265
  const started = Date.now();
969
1266
  const receipt = {
@@ -973,7 +1270,7 @@ export async function rehearseCorpusPipeline({
973
1270
  startedAt,
974
1271
  host: { node: process.version, platform: `${process.platform}/${process.arch}` },
975
1272
  bounds: { repos, gists, generations, maxRounds, owner, forceRebuild, tamper, tamperGeneration, accuracySample,
976
- seedSelection, codeReleaseBetween },
1273
+ seedSelection, codeReleaseBetween, codeReleaseConsumption, injectStoreFailure, injectGeneration, recallFixture },
977
1274
  phases: [],
978
1275
  generations: [],
979
1276
  seedSelection: [],
@@ -1048,12 +1345,14 @@ export async function rehearseCorpusPipeline({
1048
1345
 
1049
1346
  // ---- phase 4: load the disposable checkout's own modules ---------------------------------
1050
1347
  const load = (relative) => import(pathToFileURL(path.join(checkoutRoot, relative)).href);
1051
- const [reconcileMod, coverageMod, candidateMod, zipMod, accuracyMod, nextSeedMod, approvedMod, recallMod, selectionMod] = await Promise.all([
1348
+ const [reconcileMod, coverageMod, candidateMod, zipMod, accuracyMod, nextSeedMod, approvedMod, recallMod, selectionMod, failureMod] = await Promise.all([
1052
1349
  load('scripts/corpus-reconcile.mjs'), load('scripts/source-coverage.mjs'),
1053
1350
  load('scripts/corpus-candidate.mjs'), load('kb/zip-extract.mjs'), load('scripts/oracle/retrieval-accuracy.mjs'),
1054
1351
  load('scripts/corpus-next-seed.mjs'), load('scripts/approved-runtime.mjs'), load('scripts/oracle/repo-recall.mjs'),
1055
- load('scripts/rehearse-seed-selection.mjs'),
1352
+ load('scripts/rehearse-seed-selection.mjs'), load('scripts/corpus-store-failure.mjs'),
1056
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')]);
1057
1356
  // The same C3 question sample CI runs (ADR-0091 D2), unless the caller asked for the full audit.
1058
1357
  const accuracyQuestions = accuracySample === 'full' ? null
1059
1358
  : accuracySample == null ? accuracyMod.C3_DIAGNOSTIC_SAMPLE_QUESTIONS : Number(accuracySample);
@@ -1070,6 +1369,9 @@ export async function rehearseCorpusPipeline({
1070
1369
  reconcileAndPrepareCorpusCandidate: reconcileMod.reconcileAndPrepareCorpusCandidate,
1071
1370
  summarizeReconciliation: reconcileMod.summarizeReconciliation,
1072
1371
  prepareCorpusCandidate: reconcileMod.prepareCorpusCandidate,
1372
+ executeReconciliation: reconcileMod.executeReconciliation,
1373
+ defaultRunAsync: reconcileMod.defaultRunAsync,
1374
+ degradedPublication: failureMod.degradedPublication,
1073
1375
  observeSourceUniverse: coverageMod.observeSourceUniverse,
1074
1376
  canonicalSourceObservation: coverageMod.canonicalSourceObservation,
1075
1377
  buildCoverage: coverageMod.buildCoverage,
@@ -1083,10 +1385,15 @@ export async function rehearseCorpusPipeline({
1083
1385
  readRecallReport: recallMod.readRecallReport,
1084
1386
  loadFixture: recallMod.loadFixture,
1085
1387
  readDiagnosticAccuracyReport: accuracyMod.readDiagnosticAccuracyReport,
1388
+ assembleCodeReleaseCorpus: codeReleaseMod.assembleCodeReleaseCorpus,
1389
+ checkNoNewerCorpusGeneration: codeReleaseMod.checkNoNewerCorpusGeneration,
1390
+ fixtureDenominator: denominatorMod.fixtureDenominator,
1391
+ verifyCoverageSidecar: sidecarMod.verifyCoverageSidecar,
1086
1392
  };
1087
1393
  for (const [name, value] of Object.entries(api)) {
1088
1394
  if (typeof value !== 'function') fail(`the disposable checkout does not export ${name}`);
1089
1395
  }
1396
+ api.CORPUS_QA_FAILED_EXIT = failureMod.CORPUS_QA_FAILED_EXIT;
1090
1397
  phase('load-pipeline-entrypoints', 'PASS', { entrypoints: Object.keys(api).length });
1091
1398
 
1092
1399
  // ---- phase 5: real seed acquisition (or an explicit SKIP) --------------------------------
@@ -1118,6 +1425,7 @@ export async function rehearseCorpusPipeline({
1118
1425
  const registryDir = path.join(workRoot, 'release-registry');
1119
1426
  fs.mkdirSync(registryDir, { recursive: true });
1120
1427
  let lastPublished = null;
1428
+ let codeRelease = null;
1121
1429
  for (let index = 1; index <= generations; index += 1) {
1122
1430
  if (index > 1 && codeReleaseBetween) {
1123
1431
  const release = selectionMod.simulateCodeRelease({ checkoutRoot });
@@ -1136,7 +1444,7 @@ export async function rehearseCorpusPipeline({
1136
1444
  // uses — the content-addressed tag, the archive bytes, and the schema-2 receipt together.
1137
1445
  const baseline = await api.verifySeedBaseline({
1138
1446
  seedDescriptor: { tag: seed.tag, sha256: seed.sha256, bytes: seed.bytes },
1139
- bundleFile: seed.file, receiptFile: seed.receiptFile,
1447
+ bundleFile: seed.file, receiptFile: seed.receiptFile, coverageFile: seed.coverageFile ?? null,
1140
1448
  });
1141
1449
  phase(`generation-${index}-seed-baseline`, 'PASS', {
1142
1450
  tag: baseline.tag, sha256: baseline.sha256, stores: baseline.receipt.storeCount,
@@ -1144,7 +1452,8 @@ export async function rehearseCorpusPipeline({
1144
1452
  }
1145
1453
  const result = await runGeneration({
1146
1454
  index, api, checkoutRoot, workRoot, seed, recorder, tamper, log,
1147
- bounds: { repos, gists, owner, maxRounds, builderSha, forceRebuild, tamperGeneration, accuracyQuestions },
1455
+ bounds: { repos, gists, owner, maxRounds, builderSha, forceRebuild, tamperGeneration, accuracyQuestions,
1456
+ injectStoreFailure, injectGeneration, recallFixture },
1148
1457
  });
1149
1458
  receipt.generations.push(result.generation);
1150
1459
  phase(`generation-${index}`, 'PASS', {
@@ -1170,9 +1479,26 @@ export async function rehearseCorpusPipeline({
1170
1479
  phase(`generation-${index}-approved-runtime`, 'PASS', { ...runtimeCheck,
1171
1480
  reason: `verifyApprovedRuntime PASS: ${verdict.checked} runtime files equal the checkout's, none unpinned, seed ${seed.tag} did not leak an executable` });
1172
1481
 
1173
- if (seedSelection && index < generations) {
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)) {
1174
1492
  lastPublished = publishLocally({ selectionMod, registryDir, published: result.published, workRoot, index });
1175
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
+ }
1176
1502
  seed = result.nextSeed;
1177
1503
  }
1178
1504
 
@@ -1259,10 +1585,14 @@ export async function main(argv = process.argv.slice(2)) {
1259
1585
  owner: arg(argv, '--owner', 'ruvnet'),
1260
1586
  tamper: arg(argv, '--tamper', 'none'),
1261
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'),
1262
1591
  forceRebuild: !argv.includes('--no-force-rebuild'),
1263
1592
  seedOverride: arg(argv, '--seed', null),
1264
1593
  seedSelection: !argv.includes('--no-seed-selection'),
1265
1594
  codeReleaseBetween: !argv.includes('--no-code-release-between'),
1595
+ codeReleaseConsumption: !argv.includes('--no-code-release-consumption'),
1266
1596
  accuracySample: arg(argv, '--accuracy-sample', null),
1267
1597
  keep: argv.includes('--keep'),
1268
1598
  workRootParent: arg(argv, '--work-root', null),