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.
- 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/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 +69 -12
- package/scripts/corpus-reconcile.mjs +340 -61
- 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/public-verification-inputs.mjs +18 -2
- package/scripts/rehearse-corpus-pipeline.mjs +350 -20
- package/scripts/release.mjs +156 -18
- package/scripts/retrieval-canary.mjs +42 -20
- package/scripts/source-coverage.mjs +19 -3
|
@@ -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
|
-
|
|
687
|
-
|
|
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
|
-
|
|
710
|
-
|
|
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(
|
|
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
|
-
// ---
|
|
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 = ['
|
|
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:
|
|
928
|
-
|
|
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 (
|
|
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),
|