ruvnet-brain 4.3.35 → 4.3.37

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.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/.codex-plugin/plugin.json +1 -1
  5. package/plugin/hooks/codex-hooks.json +6 -1
  6. package/plugin/hooks/hook-contracts.json +6 -4
  7. package/plugin/scripts/continuity-hook-policy.mjs +10 -3
  8. package/plugin/scripts/coverage-integrity.mjs +58 -2
  9. package/plugin/scripts/hook-shim.mjs +3 -1
  10. package/plugin/scripts/session-snapshot-hook.mjs +11 -1
  11. package/plugin/scripts/turn-outcome-capture.mjs +292 -0
  12. package/scripts/build-bundle.mjs +27 -1
  13. package/scripts/code-release-corpus.mjs +276 -0
  14. package/scripts/corpus-candidate.mjs +19 -3
  15. package/scripts/corpus-coverage-sidecar.mjs +202 -0
  16. package/scripts/corpus-currency.mjs +71 -0
  17. package/scripts/corpus-dispatch-decision.mjs +138 -0
  18. package/scripts/corpus-next-seed.mjs +69 -12
  19. package/scripts/corpus-reconcile.mjs +340 -61
  20. package/scripts/corpus-store-failure.mjs +82 -0
  21. package/scripts/corpus-watchdog.mjs +334 -0
  22. package/scripts/derive-passage-content-map.mjs +71 -0
  23. package/scripts/fixture-denominator.mjs +72 -0
  24. package/scripts/knowledge-input-digest.mjs +163 -0
  25. package/scripts/oracle/repo-recall.mjs +142 -21
  26. package/scripts/public-verification-inputs.mjs +42 -4
  27. package/scripts/rehearse-corpus-pipeline.mjs +350 -20
  28. package/scripts/release-transaction-provider.mjs +29 -6
  29. package/scripts/release.mjs +156 -18
  30. package/scripts/retrieval-canary.mjs +83 -24
  31. package/scripts/retrieval-passage-identity.mjs +58 -0
  32. package/scripts/source-coverage.mjs +19 -3
  33. package/scripts/sync-census.mjs +0 -0
  34. package/scripts/wired-check.mjs +4 -0
@@ -0,0 +1,71 @@
1
+ // scripts/corpus-currency.mjs — ADR-0091 D7.1: the honest content-currency block of manifest.json.
2
+ //
3
+ // There is no single honest "content as of" date for a corpus that mixes current and carried stores:
4
+ // `builtUtc` is restamped to "now" every round (corpus-aggregates.mjs, refresh-capability-only-store.mjs)
5
+ // and `manifest.generated` is the ASSEMBLY time. So the manifest reports what is actually known:
6
+ //
7
+ // observedAt when upstream heads were read (the sealed observation). Every CURRENT store equals
8
+ // upstream as of that instant. null when nothing was observed (legacy/bootstrap path).
9
+ // generationTag the corpus generation this archive was assembled FROM, when the build knows it (a
10
+ // code release built from a published generation). null for a nightly candidate, whose
11
+ // own tag is corpus-sha256-<this archive's sha256> and so cannot be written inside it.
12
+ // counts eligible repository rows by status, plus fixture repositories RETIRED (no row at all
13
+ // in a complete observation, scripts/fixture-denominator.mjs -- the one shared rule).
14
+ // null when nothing was observed.
15
+ // oldestCarried the carried (STALE + carry) store whose content is oldest. Its committedAt comes only
16
+ // from carry.carriedCommittedAt; when any carried store's date is unknown the oldest
17
+ // cannot be known, so an undated one is reported with committedAt null. Never estimated.
18
+ //
19
+ // Stated plainly, a surface reads it as "N of M repositories match upstream as of <observedAt>".
20
+
21
+ import { retiredFixtureStores } from './fixture-denominator.mjs';
22
+
23
+ export const CURRENCY_BASIS = Object.freeze({
24
+ SEALED: 'sealed-observation',
25
+ LEGACY: 'legacy-seed-projection-unobserved',
26
+ NONE: 'no-observation',
27
+ });
28
+
29
+ const isoOrNull = (value) => (typeof value === 'string' && Number.isFinite(Date.parse(value)) ? value : null);
30
+
31
+ function oldestCarriedOf(rows) {
32
+ const carried = rows.filter((row) => row.status === 'STALE' && row.carry && typeof row.carry === 'object');
33
+ if (!carried.length) return null;
34
+ const byStore = (a, b) => String(a.artifact?.store).localeCompare(String(b.artifact?.store));
35
+ const undated = carried.filter((row) => isoOrNull(row.carry.carriedCommittedAt) === null).sort(byStore);
36
+ const pick = undated[0]
37
+ || [...carried].sort((a, b) => Date.parse(a.carry.carriedCommittedAt) - Date.parse(b.carry.carriedCommittedAt) || byStore(a, b))[0];
38
+ return {
39
+ store: pick.artifact?.store ?? null,
40
+ sourceCommit: pick.carry.carriedSourceCommit ?? null,
41
+ committedAt: isoOrNull(pick.carry.carriedCommittedAt),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * `coverage`: the sealed ruvnet-brain-corpus-coverage this archive was bound to (already validated by
47
+ * the caller), or null. `basis` says why it is null. `fixtureStores`: the frozen fixture's stores, or
48
+ * null when the runtime has no fixture (then `retired` is null, not 0 -- unknown is not none).
49
+ */
50
+ export function corpusCurrencyBlock({ coverage = null, basis, generationTag = null, fixtureStores = null }) {
51
+ if (!Object.values(CURRENCY_BASIS).includes(basis)) throw new Error(`unknown corpus currency basis ${basis}`);
52
+ if (basis !== CURRENCY_BASIS.SEALED || !coverage) {
53
+ return { basis, observedAt: null, generationTag: generationTag ?? null, counts: null, oldestCarried: null };
54
+ }
55
+ const eligible = (coverage.rows || []).filter((row) => row?.kind === 'repository' && row.disposition === 'eligible');
56
+ const count = (status) => eligible.filter((row) => row.status === status).length;
57
+ return {
58
+ basis,
59
+ observedAt: isoOrNull(coverage.observedAt),
60
+ generationTag: generationTag ?? null,
61
+ counts: {
62
+ eligible: eligible.length,
63
+ current: count('CURRENT'),
64
+ stale: count('STALE'),
65
+ missing: count('MISSING'),
66
+ unverified: count('UNVERIFIED'),
67
+ retired: fixtureStores ? retiredFixtureStores({ coverage, fixtureStores }).length : null,
68
+ },
69
+ oldestCarried: oldestCarriedOf(eligible),
70
+ };
71
+ }
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ // scripts/corpus-dispatch-decision.mjs — the ONE decision the nightly corpus dispatcher makes.
3
+ //
4
+ // WHY A MODULE (2026-09-29 nightly redesign). The decision used to live as bash in
5
+ // corpus-nightly-dispatch.yml and compared the approved runtime against main HEAD: whenever main
6
+ // carried a commit newer than the newest install-verified release -- which under "every main commit
7
+ // is a release" is exactly the window between a code release landing and its verification finishing,
8
+ // and forever if that verification failed -- the nightly stood down, silently. The corpus is built at
9
+ // the APPROVED runtime's own source, so main HEAD is irrelevant to it and is no longer consulted.
10
+ //
11
+ // decide() is pure and returns one of three verdicts:
12
+ // arm dispatch protected-release mode=corpus at the approved release's sourceSha
13
+ // stand-down a correct, quiet non-event (kill switch off, newest release not yet verified, or an
14
+ // approved runtime older than the first release whose pipeline understands this workflow)
15
+ // fail evidence is present but does not hold -- loud, never a silent skip
16
+ //
17
+ // minPipelineVersion: the dispatched workflow text is always main's, but the scripts it runs come from
18
+ // the approved release's checkout. A runtime older than MIN_PIPELINE_VERSION does not know the flags
19
+ // the new workflow passes (--approved-tag, --no-change-out), so the nightly stands down rather than
20
+ // let new workflow text drive old scripts.
21
+
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+ import { pathToFileURL } from 'node:url';
25
+
26
+ export const MIN_PIPELINE_VERSION = '4.3.36'; // sync-version-ignore: a fixed pipeline floor, not the current version
27
+ const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
28
+ const HEX40 = /^[0-9a-f]{40}$/;
29
+ const CODE_TAG = /^v\d+\.\d+\.\d+$/;
30
+ // scripts/approved-runtime.mjs names the release it refused: "code release vX.Y.Z (the newest) carries ...".
31
+ const REFUSED_NEWEST = /code release (v\d+\.\d+\.\d+) \(the newest\)/;
32
+
33
+ /** -1 / 0 / 1; throws on anything that is not plain x.y.z. */
34
+ export function compareVersions(left, right) {
35
+ const a = SEMVER.exec(String(left || ''));
36
+ const b = SEMVER.exec(String(right || ''));
37
+ if (!a || !b) throw new Error(`cannot compare versions ${JSON.stringify(left)} and ${JSON.stringify(right)}`);
38
+ for (let i = 1; i <= 3; i += 1) {
39
+ const delta = Number(a[i]) - Number(b[i]);
40
+ if (delta) return Math.sign(delta);
41
+ }
42
+ return 0;
43
+ }
44
+
45
+ /** The owner's kill switch: CORPUS_NIGHTLY reading `off` (any case, surrounding space ignored). */
46
+ export function killSwitchOff(nightlyVar) {
47
+ return String(nightlyVar ?? '').trim().toLowerCase() === 'off';
48
+ }
49
+
50
+ /**
51
+ * @param nightlyVar repository variable CORPUS_NIGHTLY (armed unless it reads `off`, any case; the owner's
52
+ * approval of the code release is the consent)
53
+ * @param resolution { status: 'resolved', release: {tag, version, sourceSha} }
54
+ * | { status: 'not-yet-verified' } | { status: 'invalid', reason }
55
+ */
56
+ export function decide({ nightlyVar, resolution, minPipelineVersion = MIN_PIPELINE_VERSION } = {}) {
57
+ if (killSwitchOff(nightlyVar)) {
58
+ return { verdict: 'stand-down', reason: 'repository variable CORPUS_NIGHTLY is off (the owner kill switch)' };
59
+ }
60
+ if (resolution?.status === 'not-yet-verified') {
61
+ return { verdict: 'stand-down', reason: 'the newest code release has not reached install-verified yet (no aggregate)' };
62
+ }
63
+ if (resolution?.status === 'invalid' && CODE_TAG.test(String(resolution.tag || ''))
64
+ && compareVersions(resolution.tag.slice(1), minPipelineVersion) < 0) {
65
+ // VERSION SKEW, not tampering (found 2026-09-29): this checkout's verifier is a newer generation
66
+ // than the newest release's aggregate (e.g. D7's retired-fixture denominator), so it cannot judge
67
+ // it. A runtime below the floor would stand down even if it verified, so standing down loses
68
+ // nothing -- and an aggregate that claims a runtime AT or above the floor still fails loud below.
69
+ return { verdict: 'stand-down', reason: `the newest code release ${resolution.tag} predates ${minPipelineVersion}; `
70
+ + `its install evidence is not judged by this pipeline (${resolution.reason || 'unverifiable here'})` };
71
+ }
72
+ if (resolution?.status !== 'resolved') {
73
+ return { verdict: 'fail', reason: `the newest code release carries install-verification evidence that does not hold: ${resolution?.reason || 'unknown resolution'}` };
74
+ }
75
+ const { tag, version, sourceSha } = resolution.release || {};
76
+ if (!SEMVER.test(String(version || '')) || tag !== `v${version}` || !HEX40.test(String(sourceSha || ''))) {
77
+ return { verdict: 'fail', reason: `resolved release identity is malformed (${JSON.stringify(resolution.release || null)})` };
78
+ }
79
+ if (compareVersions(version, minPipelineVersion) < 0) {
80
+ return { verdict: 'stand-down', reason: `approved runtime ${tag} predates ${minPipelineVersion}, the first release whose pipeline this workflow can drive` };
81
+ }
82
+ return { verdict: 'arm', reason: `building the corpus at approved runtime ${tag} @ ${sourceSha}`, tag, version, sourceSha };
83
+ }
84
+
85
+ /** approved-runtime.mjs --resolve's exit status + stdout (+ stderr, naming a refused release) -> a resolution. */
86
+ export function resolutionFrom({ status, stdout, stderr = '' }) {
87
+ if (String(status) === '3') return { status: 'not-yet-verified' };
88
+ if (String(status) !== '0') {
89
+ const tag = REFUSED_NEWEST.exec(String(stderr))?.[1] ?? null;
90
+ const detail = String(stderr).split(': ').slice(-1)[0].trim().slice(0, 300);
91
+ return { status: 'invalid', tag, reason: `approved-runtime.mjs --resolve exited ${status}${detail ? ` (${detail})` : ''}` };
92
+ }
93
+ try {
94
+ const release = JSON.parse(String(stdout || ''));
95
+ return { status: 'resolved', release };
96
+ } catch (error) {
97
+ return { status: 'invalid', reason: `approved-runtime.mjs --resolve printed no release (${error.message})` };
98
+ }
99
+ }
100
+
101
+ const arg = (argv, name) => {
102
+ const index = argv.indexOf(name);
103
+ return index >= 0 ? argv[index + 1] : undefined;
104
+ };
105
+
106
+ /**
107
+ * CLI: --nightly <value> --resolve-status <n|skipped> --release <file> [--resolve-stderr <file>]
108
+ * --github-output <file>. Exit 1 only on `fail`.
109
+ * --is-off <value> prints true|false (lets the workflow skip the ~555 MB resolve when the switch is off)
110
+ */
111
+ export function main(argv = process.argv.slice(2), { stdout = process.stdout } = {}) {
112
+ if (argv.includes('--is-off')) {
113
+ stdout.write(`${killSwitchOff(arg(argv, '--is-off'))}\n`);
114
+ return 0;
115
+ }
116
+ const nightlyVar = arg(argv, '--nightly') ?? '';
117
+ const status = arg(argv, '--resolve-status');
118
+ const releaseFile = arg(argv, '--release');
119
+ const stderrFile = arg(argv, '--resolve-stderr');
120
+ const githubOutput = arg(argv, '--github-output');
121
+ const resolution = status === 'skipped' ? null
122
+ : resolutionFrom({ status,
123
+ stdout: releaseFile && fs.existsSync(releaseFile) ? fs.readFileSync(releaseFile, 'utf8') : '',
124
+ stderr: stderrFile && fs.existsSync(stderrFile) ? fs.readFileSync(stderrFile, 'utf8') : '' });
125
+ const decision = decide({ nightlyVar, resolution });
126
+ const lines = decision.verdict === 'arm'
127
+ ? ['armed=true', `approved_sha=${decision.sourceSha}`, `approved_version=${decision.version}`, `approved_tag=${decision.tag}`]
128
+ : ['armed=false'];
129
+ lines.push(`verdict=${decision.verdict}`);
130
+ if (githubOutput) fs.appendFileSync(path.resolve(githubOutput), `${lines.join('\n')}\n`);
131
+ const annotation = decision.verdict === 'fail' ? '::error::' : '::notice::';
132
+ stdout.write(`${annotation}corpus-nightly-dispatch ${decision.verdict}: ${decision.reason}\n`);
133
+ return decision.verdict === 'fail' ? 1 : 0;
134
+ }
135
+
136
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
137
+ process.exitCode = main();
138
+ }
@@ -43,9 +43,16 @@
43
43
  // re-checks sha256 and byte length before reconciliation, corpus-reconcile.mjs checks the ledger
44
44
  // schema after extraction, and corpus-candidate.mjs re-derives the whole candidate from the bytes).
45
45
  //
46
+ // ADR-0091 D6: --require-coverage is the CODE-RELEASE mode (ci.yml's release-qe and warm-brain jobs,
47
+ // and release.mjs's publish-time re-check). A code release assembles single-pass from the
48
+ // generation's sealed coverage, so a generation that did not publish its coverage sidecar
49
+ // (CORPUS-COVERAGE.json + coverage-receipt.json, D6.2) -- every generation published before D6 --
50
+ // is skipped as incompatible FOR THAT PURPOSE, with its reason. It remains a valid nightly seed:
51
+ // without the flag nothing about the nightly's selection changes.
52
+ //
46
53
  // Usage:
47
54
  // node scripts/corpus-next-seed.mjs --repo owner/name [--runtime-root <source tree>]
48
- // [--bootstrap data/corpus-seed.json] [--out <file>]
55
+ // [--bootstrap data/corpus-seed.json] [--require-coverage] [--out <file>]
49
56
 
50
57
  import crypto from 'node:crypto';
51
58
  import fs from 'node:fs';
@@ -53,6 +60,7 @@ import os from 'node:os';
53
60
  import path from 'node:path';
54
61
  import { spawnSync } from 'node:child_process';
55
62
  import { fileURLToPath, pathToFileURL } from 'node:url';
63
+ import { COVERAGE_ASSET, COVERAGE_RECEIPT_ASSET, bindCoverageToReceipt, verifyCoverageSidecar } from './corpus-coverage-sidecar.mjs';
56
64
 
57
65
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
58
66
  const HEX64 = /^[0-9a-f]{64}$/;
@@ -110,11 +118,13 @@ export async function loadCompatibilityProfile({ runtimeRoot = ROOT, fixtureFile
110
118
  throw new Error(`${root} has no repo-recall reader to judge a seed with`);
111
119
  }
112
120
  const { model, dimensions } = parseBuildFingerprint(forge.FORGE_BUILD_FINGERPRINT);
121
+ const fixture = recall.loadFixture(fixtureFile || path.join(root, recall.DEFAULT_FIXTURE_FILE));
113
122
  return {
114
123
  runtimeRoot: root,
115
124
  model,
116
125
  dimensions,
117
- fixtureSha256: recall.loadFixture(fixtureFile || path.join(root, recall.DEFAULT_FIXTURE_FILE)).fixtureSha256,
126
+ fixtureSha256: fixture.fixtureSha256,
127
+ fixtureStores: fixture.questions.map((question) => question.store),
118
128
  floorValue: recall.ABSOLUTE_FLOOR,
119
129
  readRecallReport: recall.readRecallReport,
120
130
  };
@@ -135,13 +145,18 @@ const sha256Of = (file) => crypto.createHash('sha256').update(fs.readFileSync(fi
135
145
  * recorded on `rejected` so a no-op night is explainable rather than silent. Never downloads the
136
146
  * archive: every check below reads the release's asset list, its receipt, or its recall report.
137
147
  */
138
- function judgeRelease({ run, repo, tag, digest, profile, rejected }) {
139
- const reject = (reason) => { rejected.push({ tag, reason }); return null; };
148
+ function judgeRelease({ run, repo, tag, digest, profile, rejected, requireCoverage = false }) {
149
+ // `indeterminate` marks a rejection that says nothing about the release itself -- the network or
150
+ // `gh` failed. A publish-time re-check (ADR-0091 D6.6) must not read one as "no newer generation".
151
+ const reject = (reason, { indeterminate = false } = {}) => {
152
+ rejected.push({ tag, reason, ...(indeterminate ? { indeterminate: true } : {}) });
153
+ return null;
154
+ };
140
155
  let view;
141
156
  try {
142
157
  view = ghJson(run, ['release', 'view', tag, '--repo', repo, '--json', 'tagName,isDraft,assets']);
143
158
  } catch (error) {
144
- return reject(`release view failed (${error.message})`);
159
+ return reject(`release view failed (${error.message})`, { indeterminate: true });
145
160
  }
146
161
  if (view?.tagName !== tag || view.isDraft) return reject('release is a draft or names another tag');
147
162
  const assets = Array.isArray(view.assets) ? view.assets : [];
@@ -149,13 +164,17 @@ function judgeRelease({ run, repo, tag, digest, profile, rejected }) {
149
164
  for (const name of REQUIRED_ASSETS) {
150
165
  if (named(name).length !== 1) return reject(`unverified: expected exactly one ${name} asset`);
151
166
  }
167
+ if (requireCoverage && [COVERAGE_ASSET, COVERAGE_RECEIPT_ASSET].some((name) => named(name).length !== 1)) {
168
+ return reject(`incompatible for a code release: no coverage sidecar (${COVERAGE_ASSET} + ${COVERAGE_RECEIPT_ASSET}; `
169
+ + 'published before ADR-0091 D6.2)');
170
+ }
152
171
  const archive = named(ARCHIVE_ASSET)[0];
153
172
  if (!Number.isSafeInteger(archive.size) || archive.size < 1) return reject('unverified: archive asset has no usable byte length');
154
173
 
155
174
  const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'corpus-next-seed-'));
156
175
  try {
157
176
  const receiptFile = download(run, { repo, tag, pattern: RECEIPT_ASSET, dir: scratch });
158
- if (!receiptFile) return reject('unverified: corpus receipt could not be downloaded');
177
+ if (!receiptFile) return reject('unverified: corpus receipt could not be downloaded', { indeterminate: true });
159
178
  let receipt;
160
179
  try { receipt = JSON.parse(fs.readFileSync(receiptFile, 'utf8')); }
161
180
  catch (error) { return reject(`unverified: corpus receipt unreadable (${error.message})`); }
@@ -192,16 +211,47 @@ function judgeRelease({ run, repo, tag, digest, profile, rejected }) {
192
211
  // D4 check 2 -- the recall report, the one small file that decides whether the downstream seed
193
212
  // re-check can pass. Downloaded only now, after the receipt already qualified.
194
213
  const recallFile = download(run, { repo, tag, pattern: RECALL_ASSET, dir: scratch });
195
- if (!recallFile) return reject('unverified: repo-recall report could not be downloaded');
214
+ if (!recallFile) return reject('unverified: repo-recall report could not be downloaded', { indeterminate: true });
196
215
  if (sha256Of(recallFile) !== receipt.recallReport.sha256 || fs.statSync(recallFile).size !== receipt.recallReport.bytes) {
197
216
  return reject('unverified: repo-recall report is not the one the receipt binds');
198
217
  }
199
- let claimedFixture = null;
200
- try { claimedFixture = JSON.parse(fs.readFileSync(recallFile, 'utf8'))?.fixture?.sha256 ?? null; } catch { /* the reader names it below */ }
218
+ let claimed = null;
219
+ try { claimed = JSON.parse(fs.readFileSync(recallFile, 'utf8')); } catch { /* the reader names it below */ }
220
+ const claimedFixture = claimed?.fixture?.sha256 ?? null;
201
221
  if (claimedFixture !== profile.fixtureSha256) {
202
222
  return reject(`incompatible: recall report was measured against fixture ${String(claimedFixture).slice(0, 12)}, `
203
223
  + `this runtime's frozen fixture is ${profile.fixtureSha256.slice(0, 12)}`);
204
224
  }
225
+ // ADR-0091 D7.3: a report that claims retired questions is verified against the generation's own
226
+ // sealed coverage (its D6.2 sidecar), recomputed by the reader -- in the nightly mode too, or one
227
+ // retirement would push every night onto the bootstrap. No sidecar means the claim is unverifiable,
228
+ // and the generation is skipped.
229
+ const claimsRetirement = claimed?.retirement !== undefined;
230
+ if (claimsRetirement && !requireCoverage && [COVERAGE_ASSET, COVERAGE_RECEIPT_ASSET].some((name) => named(name).length !== 1)) {
231
+ return reject('incompatible: recall report claims retired question(s) but the generation published no coverage sidecar '
232
+ + `(${COVERAGE_ASSET} + ${COVERAGE_RECEIPT_ASSET}) to verify them against`);
233
+ }
234
+
235
+ // D6.2 -- the code-release mode's extra check, from two more small files: the sidecar must name
236
+ // THIS generation, bind THESE coverage bytes, and the coverage must describe this receipt's stores.
237
+ let coverage = null;
238
+ let coverageBytes = null;
239
+ if (requireCoverage || claimsRetirement) {
240
+ const sidecarFile = download(run, { repo, tag, pattern: COVERAGE_RECEIPT_ASSET, dir: scratch });
241
+ const coverageFile = sidecarFile && download(run, { repo, tag, pattern: COVERAGE_ASSET, dir: scratch });
242
+ if (!sidecarFile || !coverageFile) return reject('unverified: coverage sidecar could not be downloaded', { indeterminate: true });
243
+ try {
244
+ coverageBytes = fs.readFileSync(coverageFile);
245
+ const verified = verifyCoverageSidecar({ sidecar: JSON.parse(fs.readFileSync(sidecarFile, 'utf8')), coverageBytes,
246
+ generationTag: tag, archiveSha256: digest, archiveBytes: archive.size });
247
+ bindCoverageToReceipt({ coverage: verified.coverage, receipt });
248
+ coverage = { asset: COVERAGE_ASSET, sha256: sha256Of(coverageFile), bytes: coverageBytes.length,
249
+ receiptAsset: COVERAGE_RECEIPT_ASSET, receiptSha256: sha256Of(sidecarFile), degraded: verified.degraded };
250
+ } catch (error) {
251
+ return reject(`unverified: coverage sidecar does not bind this generation (${error.message})`);
252
+ }
253
+ }
254
+
205
255
  try {
206
256
  profile.readRecallReport({
207
257
  reportFile: recallFile,
@@ -210,6 +260,9 @@ function judgeRelease({ run, repo, tag, digest, profile, rejected }) {
210
260
  // The same bar corpus-seed.yml's seed re-check uses: a published seed is graded against the
211
261
  // fixed ABSOLUTE_FLOOR, never re-judged by a later, higher committed floor.
212
262
  floorValue: profile.floorValue,
263
+ // Ignored by a pre-D7 reader, which then fails closed on a retirement-bearing report's totals.
264
+ coverageBytes,
265
+ ...(profile.fixtureStores ? { fixtureStores: profile.fixtureStores } : {}),
213
266
  });
214
267
  } catch (error) {
215
268
  return reject(`incompatible: recall report fails this runtime's reader (${error.message})`);
@@ -221,6 +274,8 @@ function judgeRelease({ run, repo, tag, digest, profile, rejected }) {
221
274
  asset: ARCHIVE_ASSET,
222
275
  sha256: digest,
223
276
  bytes: archive.size,
277
+ // The nightly descriptor keeps its pre-D7 shape even when a retirement claim fetched the coverage.
278
+ ...(coverage && requireCoverage ? { coverage } : {}),
224
279
  sourceCommit: typeof receipt.builderSourceSha === 'string' ? receipt.builderSourceSha : null,
225
280
  // Informational only since D4: the runtime that BUILT the seed, which may be older than the
226
281
  // runtime about to consume it. Nothing gates on it.
@@ -233,7 +288,7 @@ function judgeRelease({ run, repo, tag, digest, profile, rejected }) {
233
288
 
234
289
  export async function resolveNextCorpusSeed({
235
290
  repo, run = defaultRun, root = ROOT, runtimeRoot = null, fixtureFile = null, bootstrapFile,
236
- limit = 100, searchBound = SEARCH_BOUND, profile = null,
291
+ limit = 100, searchBound = SEARCH_BOUND, profile = null, requireCoverage = false,
237
292
  } = {}) {
238
293
  if (!/^[^/\s]+\/[^/\s]+$/.test(String(repo || ''))) throw new Error('--repo must be owner/name');
239
294
  if (!Number.isSafeInteger(searchBound) || searchBound < 1) throw new Error('search bound must be a positive integer');
@@ -250,7 +305,7 @@ export async function resolveNextCorpusSeed({
250
305
  try {
251
306
  listed = ghJson(run, ['release', 'list', '--repo', repo, '--limit', String(limit), '--json', 'tagName,isDraft,createdAt']) || [];
252
307
  } catch (error) {
253
- rejected.push({ tag: null, reason: `release list failed (${error.message})` });
308
+ rejected.push({ tag: null, reason: `release list failed (${error.message})`, indeterminate: true });
254
309
  }
255
310
 
256
311
  // Every corpus-shaped tag that is NOT usable gets an explicit reason. A silently skipped row is
@@ -273,7 +328,8 @@ export async function resolveNextCorpusSeed({
273
328
  }
274
329
  const expects = { model: compatibility.model, dimensions: compatibility.dimensions, fixtureSha256: compatibility.fixtureSha256 };
275
330
  for (const candidate of judged) {
276
- const resolved = judgeRelease({ run, repo, tag: candidate.tag, digest: candidate.digest, profile: compatibility, rejected });
331
+ const resolved = judgeRelease({ run, repo, tag: candidate.tag, digest: candidate.digest, profile: compatibility, rejected,
332
+ requireCoverage });
277
333
  if (resolved) return { seed: resolved, rejected, judged: judged.length, expects };
278
334
  }
279
335
 
@@ -311,6 +367,7 @@ export async function main(argv = process.argv.slice(2), { run = defaultRun, std
311
367
  runtimeRoot: arg(argv, '--runtime-root', null),
312
368
  fixtureFile: arg(argv, '--fixture', null),
313
369
  bootstrapFile: arg(argv, '--bootstrap', null),
370
+ requireCoverage: argv.includes('--require-coverage'),
314
371
  run,
315
372
  });
316
373
  } catch (error) {