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
package/scripts/release.mjs
CHANGED
|
@@ -66,6 +66,9 @@ const recallNotes = (receipt) => {
|
|
|
66
66
|
};
|
|
67
67
|
import { verifyBundle } from './verify-bundle.mjs';
|
|
68
68
|
import { CORPUS_GENERATION_FIELD, evaluateCorpusPromotion } from './corpus-promotion.mjs';
|
|
69
|
+
import { bindCoverageToReceipt, writeCoverageAssets } from './corpus-coverage-sidecar.mjs';
|
|
70
|
+
import { degradedPublication } from './corpus-store-failure.mjs';
|
|
71
|
+
import { assertNoNewerCorpusGeneration } from './code-release-corpus.mjs';
|
|
69
72
|
|
|
70
73
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
71
74
|
const PUBLISH = process.argv.includes('--publish');
|
|
@@ -114,6 +117,28 @@ function corpusFailure(message) {
|
|
|
114
117
|
throw new Error(`[corpus-seed] ${message}`);
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
/**
|
|
121
|
+
* A newer code release was published after this corpus was built at its approved runtime. Promoting
|
|
122
|
+
* it now would put an OLDER runtime on releases/latest over a newer live code release (fresh installs
|
|
123
|
+
* then fail on a version mismatch). That is not a broken night -- the next night builds at the newer
|
|
124
|
+
* runtime -- so it is a distinct, typed outcome: exit CORPUS_SUPERSEDED_EXIT, recorded as `superseded`.
|
|
125
|
+
*/
|
|
126
|
+
export const CORPUS_SUPERSEDED_EXIT = 4;
|
|
127
|
+
export class CorpusSuperseded extends Error {
|
|
128
|
+
constructor(message) {
|
|
129
|
+
super(`[corpus-seed] superseded: ${message}`);
|
|
130
|
+
this.name = 'CorpusSuperseded';
|
|
131
|
+
this.code = 'CORPUS_SUPERSEDED';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const CODE_TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
|
|
136
|
+
const compareCodeTags = (left, right) => {
|
|
137
|
+
const a = CODE_TAG.exec(left).slice(1).map(Number);
|
|
138
|
+
const b = CODE_TAG.exec(right).slice(1).map(Number);
|
|
139
|
+
return Math.sign(a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
|
|
140
|
+
};
|
|
141
|
+
|
|
117
142
|
export async function runProtectedCorpusSeed({
|
|
118
143
|
argv = process.argv.slice(2),
|
|
119
144
|
env = process.env,
|
|
@@ -135,13 +160,15 @@ export async function runProtectedCorpusSeed({
|
|
|
135
160
|
const tag = cliArg(argv, '--corpus-tag');
|
|
136
161
|
const bundleFile = cliArg(argv, '--corpus-bundle');
|
|
137
162
|
const receiptFile = cliArg(argv, '--corpus-receipt');
|
|
163
|
+
// ADR-0091 D6.2: the generation's sealed coverage (the prepared artifact's source-coverage.json).
|
|
164
|
+
const coverageFile = cliArg(argv, '--corpus-coverage');
|
|
138
165
|
const target = cliArg(argv, '--target');
|
|
139
166
|
const repo = cliArg(argv, '--repo') || env.GITHUB_REPOSITORY;
|
|
140
167
|
const digestMatch = String(tag || '').match(/^corpus-sha256-([a-f0-9]{64})$/);
|
|
141
168
|
if (!digestMatch) corpusFailure('corpus tag must be corpus-sha256- followed by 64 lowercase hex characters');
|
|
142
169
|
if (repo !== env.GITHUB_REPOSITORY || repo !== 'stuinfla/ruvnet-brain') corpusFailure('repository does not match the protected workflow');
|
|
143
170
|
|
|
144
|
-
for (const [label, file] of [['bundle', bundleFile], ['receipt', receiptFile]]) {
|
|
171
|
+
for (const [label, file] of [['bundle', bundleFile], ['receipt', receiptFile], ['coverage', coverageFile]]) {
|
|
145
172
|
if (!file || !path.isAbsolute(file)) corpusFailure(`${label} must be an absolute regular file`);
|
|
146
173
|
try {
|
|
147
174
|
const stat = fs.lstatSync(file);
|
|
@@ -164,14 +191,28 @@ export async function runProtectedCorpusSeed({
|
|
|
164
191
|
} catch (error) {
|
|
165
192
|
corpusFailure(`corpus receipt is unreadable/corrupt (${error.message})`);
|
|
166
193
|
}
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
// DECOUPLED FROM main HEAD (2026-09-29 nightly redesign). The corpus is built at the APPROVED
|
|
195
|
+
// runtime -- the newest code release with a verified install aggregate -- whose source is on main's
|
|
196
|
+
// history but is usually NOT main HEAD. The old rule (target === GITHUB_SHA) stood the nightly down
|
|
197
|
+
// whenever main was ahead of the newest verified release. The guard that rule was protecting
|
|
198
|
+
// (independent review of ADR-0091 D3: never promote an OLDER runtime over the live code release)
|
|
199
|
+
// is now enforced directly: target must be the checkout, the receipt's builder, an ancestor of this
|
|
200
|
+
// protected run's GITHUB_SHA, and -- for a customer promotion -- the commit of --approved-tag, which
|
|
201
|
+
// must still be the NEWEST code release at publish time (below; otherwise CorpusSuperseded).
|
|
202
|
+
// Format checks run first; no value reaches a subprocess unvalidated.
|
|
203
|
+
if (!isHex(target, 40) || target !== head || target !== receipt.builderSourceSha || !isHex(env.GITHUB_SHA, 40)) {
|
|
204
|
+
corpusFailure('target must exactly equal HEAD and the corpus receipt builderSourceSha (and GITHUB_SHA must be a commit)');
|
|
205
|
+
}
|
|
206
|
+
const ancestry = run('git', ['merge-base', '--is-ancestor', target, env.GITHUB_SHA], {
|
|
207
|
+
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
208
|
+
});
|
|
209
|
+
if (ancestry.error || ancestry.status !== 0) corpusFailure(`target ${target} is not an ancestor of this run's GITHUB_SHA ${env.GITHUB_SHA}`);
|
|
210
|
+
const approvedTag = cliArg(argv, '--approved-tag');
|
|
211
|
+
if (promoteLatest) {
|
|
212
|
+
if (!CODE_TAG.test(String(approvedTag || ''))) corpusFailure('customer promotion requires --approved-tag vX.Y.Z (the approved runtime this corpus was built at)');
|
|
213
|
+
if (receipt.archiveManifestReleaseTag !== approvedTag) {
|
|
214
|
+
corpusFailure(`the archive ships runtime ${receipt.archiveManifestReleaseTag}, not the approved runtime ${approvedTag}`);
|
|
215
|
+
}
|
|
175
216
|
}
|
|
176
217
|
|
|
177
218
|
// Schema 3 (ADR-086 Step 15 / A6): the receipt binds the full provenance closure shipped INSIDE
|
|
@@ -261,11 +302,16 @@ export async function runProtectedCorpusSeed({
|
|
|
261
302
|
|| fs.statSync(recallReportFile).size !== receipt.recallReport.bytes) {
|
|
262
303
|
corpusFailure('detached repo-recall report bytes do not match the corpus receipt');
|
|
263
304
|
}
|
|
305
|
+
// ADR-0091 D7.3: a claimed retirement is recomputed from THIS generation's sealed coverage (the bytes
|
|
306
|
+
// published beside the archive as CORPUS-COVERAGE.json), never taken from the report's own claim.
|
|
307
|
+
const recallFixture = loadFixture();
|
|
264
308
|
try {
|
|
265
309
|
readRecallReport({
|
|
266
310
|
reportFile: recallReportFile,
|
|
267
311
|
archive: archiveIdentity,
|
|
268
|
-
expectedFixtureSha256:
|
|
312
|
+
expectedFixtureSha256: recallFixture.fixtureSha256,
|
|
313
|
+
coverageBytes: fs.readFileSync(coverageFile),
|
|
314
|
+
fixtureStores: recallFixture.questions.map((question) => question.store),
|
|
269
315
|
});
|
|
270
316
|
} catch (error) {
|
|
271
317
|
corpusFailure(`retrieval does not qualify this corpus for publication (${error.message})`);
|
|
@@ -280,12 +326,39 @@ export async function runProtectedCorpusSeed({
|
|
|
280
326
|
// here. It runs before any `gh` call so an untrue candidate never reaches the network.
|
|
281
327
|
try {
|
|
282
328
|
await verifyCorpusReceipt({
|
|
283
|
-
receiptFile, bundleFile, accuracyReportFile, recallReportFile, expectedBuilderSha: target, expectedArchiveSha256: archiveSha256,
|
|
329
|
+
receiptFile, bundleFile, accuracyReportFile, recallReportFile, coverageFile, expectedBuilderSha: target, expectedArchiveSha256: archiveSha256,
|
|
284
330
|
});
|
|
285
331
|
} catch (error) {
|
|
286
332
|
corpusFailure(`corpus receipt does not verify against the sealed archive (${error.message})`);
|
|
287
333
|
}
|
|
288
334
|
|
|
335
|
+
// ADR-0091 D6.2 + D10. The archive carries no coverage and the schema-3 receipt binds none, so this
|
|
336
|
+
// is the one place the publisher can SEE whether the generation is degraded. The coverage must be
|
|
337
|
+
// the coverage of THIS archive (bound store by store to the receipt), it is published beside the
|
|
338
|
+
// archive as CORPUS-COVERAGE.json + coverage-receipt.json (no receipt schema bump), and a
|
|
339
|
+
// generation with any carried or missing store is refused while D10 has recorded no soaked
|
|
340
|
+
// tolerant-validator transition -- installed clients would reject it. Local, before any network.
|
|
341
|
+
let coverageAssets;
|
|
342
|
+
try {
|
|
343
|
+
coverageAssets = writeCoverageAssets({
|
|
344
|
+
dir: fs.mkdtempSync(path.join(os.tmpdir(), 'corpus-coverage-assets-')),
|
|
345
|
+
coverageFile, generationTag: tag, archiveSha256, archiveBytes: archiveIdentity.bytes,
|
|
346
|
+
});
|
|
347
|
+
const degraded = bindCoverageToReceipt({
|
|
348
|
+
coverage: JSON.parse(fs.readFileSync(coverageAssets.coverageFile, 'utf8')), receipt,
|
|
349
|
+
});
|
|
350
|
+
if (degraded.carried.length + degraded.missing.length > 0) {
|
|
351
|
+
const decision = degradedPublication();
|
|
352
|
+
if (!decision.allowed) {
|
|
353
|
+
corpusFailure(`degraded generation (${degraded.carried.length} carried, ${degraded.missing.length} missing) `
|
|
354
|
+
+ `must not be published: ${decision.reason}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (String(error.message).startsWith('[corpus-seed]')) throw error;
|
|
359
|
+
corpusFailure(`the generation's sealed coverage does not bind this archive (${error.message})`);
|
|
360
|
+
}
|
|
361
|
+
|
|
289
362
|
// EVERY local proof happens before the first network call. `gh` must never be reached by a
|
|
290
363
|
// candidate that is already known to be unpublishable — that is the same discipline the deep
|
|
291
364
|
// verifyCorpusReceipt above follows, and a customer release with an unusable signature is exactly
|
|
@@ -307,16 +380,48 @@ export async function runProtectedCorpusSeed({
|
|
|
307
380
|
if (!Number.isFinite(Date.parse(generation))) corpusFailure('corpus receipt createdAt is not a readable generation timestamp');
|
|
308
381
|
}
|
|
309
382
|
|
|
310
|
-
const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
|
|
311
383
|
const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
|
|
312
384
|
const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
|
|
313
|
-
const
|
|
385
|
+
const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
386
|
+
const ghJson = (args, label) => {
|
|
387
|
+
const result = gh(args);
|
|
388
|
+
if (result.error || result.status !== 0) {
|
|
389
|
+
corpusFailure(`cannot read ${label} (${String(result.error?.message || result.stderr || result.stdout || '').trim() || `gh exited ${result.status}`})`);
|
|
390
|
+
}
|
|
391
|
+
try { return JSON.parse(String(result.stdout || 'null')); }
|
|
392
|
+
catch (error) { corpusFailure(`cannot parse ${label} (${error.message})`); }
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
if (promoteLatest) {
|
|
396
|
+
// PUBLISH-TIME RE-RESOLVE, before anything is written. Preparation takes hours; a code release
|
|
397
|
+
// may have been published meanwhile. The newest code release is selected exactly as
|
|
398
|
+
// scripts/approved-runtime.mjs selects it (non-draft, non-prerelease vX.Y.Z, highest version).
|
|
399
|
+
// Its signed install aggregate was re-verified for --approved-tag by the workflow step that built
|
|
400
|
+
// the runtime pin moments ago; what can change after that is only WHICH release is newest.
|
|
401
|
+
const listed = ghJson(['release', 'list', '--repo', repo, '--limit', '200', '--json', 'tagName,isDraft,isPrerelease'], 'the code release list');
|
|
402
|
+
const [newest] = (Array.isArray(listed) ? listed : [])
|
|
403
|
+
.filter((row) => !row?.isDraft && !row?.isPrerelease && CODE_TAG.test(String(row?.tagName || '')))
|
|
404
|
+
.map((row) => row.tagName).sort((a, b) => compareCodeTags(b, a));
|
|
405
|
+
if (!newest) corpusFailure(`no published code release is listed on ${repo}; the approved runtime ${approvedTag} cannot be confirmed`);
|
|
406
|
+
const order = compareCodeTags(newest, approvedTag);
|
|
407
|
+
if (order > 0) {
|
|
408
|
+
throw new CorpusSuperseded(`code release ${newest} was published after this corpus was built at ${approvedTag}; `
|
|
409
|
+
+ 'promoting it would put an older runtime over the live code release. The next night builds at the newer runtime.');
|
|
410
|
+
}
|
|
411
|
+
if (order < 0) corpusFailure(`approved runtime ${approvedTag} is newer than every published code release (newest ${newest})`);
|
|
412
|
+
const commit = ghJson(['api', `repos/${repo}/commits/${approvedTag}`], `the commit of ${approvedTag}`);
|
|
413
|
+
if (String(commit?.sha || '').toLowerCase() !== target) {
|
|
414
|
+
corpusFailure(`target ${target} is not the source of the approved runtime ${approvedTag} (${commit?.sha || 'unknown'})`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
|
|
419
|
+
const view = gh(viewArgs);
|
|
314
420
|
if (!view.error && view.status === 0) corpusFailure(`release ${tag} already exists; refusing to overwrite immutable corpus seed`);
|
|
315
421
|
const viewError = String(view.error?.message || view.stderr || view.stdout || '');
|
|
316
422
|
if (!/(release not found|no release found)/i.test(viewError)) corpusFailure(`cannot prove ${tag} is absent (${viewError.trim() || `gh exited ${view.status}`})`);
|
|
317
423
|
|
|
318
424
|
const receiptSha256 = sha256File(receiptFile);
|
|
319
|
-
const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
320
425
|
|
|
321
426
|
if (!promoteLatest) {
|
|
322
427
|
// BOOTSTRAP/RECOVERY seeds stay exactly as ADR-086's original contract left them: an immutable
|
|
@@ -344,6 +449,7 @@ export async function runProtectedCorpusSeed({
|
|
|
344
449
|
'--title', `Immutable corpus seed ${archiveSha256.slice(0, 16)}`,
|
|
345
450
|
'--notes', notes,
|
|
346
451
|
bundleFile, receiptFile, accuracyReportFile, recallReportFile,
|
|
452
|
+
coverageAssets.coverageFile, coverageAssets.receiptFile,
|
|
347
453
|
];
|
|
348
454
|
const create = gh(createArgs);
|
|
349
455
|
if (create.error || create.status !== 0) {
|
|
@@ -395,7 +501,8 @@ export async function runProtectedCorpusSeed({
|
|
|
395
501
|
// (or the next night's dispatcher) that downloads the archive must be able to reverify it against
|
|
396
502
|
// the identity it was actually measured under — the blocking recall gate AND the C3 diagnostic it
|
|
397
503
|
// scored 59.0% on, so nobody has to take either number on trust.
|
|
398
|
-
const assetFiles = [bundleFile, signatureFile, digestFile, receiptFile, accuracyReportFile, recallReportFile
|
|
504
|
+
const assetFiles = [bundleFile, signatureFile, digestFile, receiptFile, accuracyReportFile, recallReportFile,
|
|
505
|
+
coverageAssets.coverageFile, coverageAssets.receiptFile];
|
|
399
506
|
const create = gh([
|
|
400
507
|
'release', 'create', tag,
|
|
401
508
|
'--draft',
|
|
@@ -425,13 +532,22 @@ export async function runProtectedCorpusSeed({
|
|
|
425
532
|
corpusFailure(`corpus promotion to latest failed (${String(promote.error?.message || promote.stderr || promote.stdout || '').trim()})`);
|
|
426
533
|
}
|
|
427
534
|
|
|
428
|
-
|
|
535
|
+
// `isLatest` is NOT a `gh release view` field (gh 2.101.0: "Unknown JSON field"; it exists only on
|
|
536
|
+
// `gh release list`), so asking for it made this confirmation fail against the real CLI every time.
|
|
537
|
+
// Latest-ness is read from the one authoritative endpoint instead: releases/latest must BE this tag.
|
|
538
|
+
// tests/unit/gh-json-fields.test.mjs checks every --json field list against the captured real CLI.
|
|
539
|
+
const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isPrerelease,assets', '--repo', repo]);
|
|
429
540
|
if (finalView.error || finalView.status !== 0) corpusFailure('cannot confirm the promoted corpus release');
|
|
430
541
|
let promoted;
|
|
431
542
|
try { promoted = JSON.parse(String(finalView.stdout || 'null')); }
|
|
432
543
|
catch (error) { corpusFailure(`cannot read the promoted corpus release (${error.message})`); }
|
|
544
|
+
const latestNow = gh(['api', `repos/${repo}/releases/latest`]);
|
|
545
|
+
let latestTag = null;
|
|
546
|
+
if (!latestNow.error && latestNow.status === 0) {
|
|
547
|
+
try { latestTag = JSON.parse(String(latestNow.stdout || 'null'))?.tag_name ?? null; } catch { latestTag = null; }
|
|
548
|
+
}
|
|
433
549
|
const promotedAssets = (promoted?.assets || []).map((asset) => asset?.name).sort();
|
|
434
|
-
if (promoted?.tagName !== tag || promoted.isDraft !== false ||
|
|
550
|
+
if (promoted?.tagName !== tag || promoted.isDraft !== false || latestTag !== tag
|
|
435
551
|
|| promoted.isPrerelease !== false || JSON.stringify(promotedAssets) !== JSON.stringify(expectedAssets)) {
|
|
436
552
|
corpusFailure('corpus release did not reach a complete, non-draft, non-prerelease latest state');
|
|
437
553
|
}
|
|
@@ -448,7 +564,13 @@ if (CORPUS_SEED) {
|
|
|
448
564
|
console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
|
|
449
565
|
} catch (error) {
|
|
450
566
|
console.error(error.message);
|
|
451
|
-
|
|
567
|
+
if (error instanceof CorpusSuperseded) {
|
|
568
|
+
// Typed, not red: stdout carries the outcome the workflow records.
|
|
569
|
+
console.log(JSON.stringify({ ok: false, mode: 'corpus-seed', outcome: 'superseded', reason: error.message }));
|
|
570
|
+
process.exitCode = CORPUS_SUPERSEDED_EXIT;
|
|
571
|
+
} else {
|
|
572
|
+
process.exitCode = 1;
|
|
573
|
+
}
|
|
452
574
|
}
|
|
453
575
|
} else {
|
|
454
576
|
|
|
@@ -556,6 +678,22 @@ if (PUBLISH) {
|
|
|
556
678
|
process.exit(1);
|
|
557
679
|
}
|
|
558
680
|
}
|
|
681
|
+
// ADR-0091 D6.6 — THE BACKWARD-MOVE RACE. Release QE sealed the corpus generation this bundle was
|
|
682
|
+
// built from; publication happens later, after owner approval. Clients always accept a code release
|
|
683
|
+
// and drop their corpusGeneration marker when they install one (kb/forge-update.mjs), so publishing
|
|
684
|
+
// a bundle built from generation G after G+1 already shipped rolls every user back one night.
|
|
685
|
+
// Re-resolve with the SAME resolver, before any asset upload, and refuse on any difference -- or on
|
|
686
|
+
// any answer that could not prove there is no newer generation.
|
|
687
|
+
try {
|
|
688
|
+
const guard = await assertNoNewerCorpusGeneration({
|
|
689
|
+
sealedFile: assets.corpusSeedPath, repo: 'stuinfla/ruvnet-brain', runtimeRoot: ROOT,
|
|
690
|
+
});
|
|
691
|
+
console.log(` corpus seed still current at publish time: ${guard.origin} ${guard.tag}`);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
console.error(`\n${c.r('✗ GATE FAILED: corpus generation moved after release QE')} ${c.dim(error.message)}`);
|
|
694
|
+
console.error(`${c.r(' NOT shipped. Re-run release QE so this release is built from the newest generation.')}\n`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
559
697
|
const bundleSha256 = fs.readFileSync(assets.bundleDigestPath, 'utf8').trim().split(/\s+/)[0];
|
|
560
698
|
if (!/^[a-f0-9]{64}$/i.test(bundleSha256)) {
|
|
561
699
|
console.error(`\n${c.r('✗ GATE FAILED: release digest is not a SHA-256 value')}`);
|
|
@@ -5,7 +5,8 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import readline from 'node:readline';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { canonicalJson, digest, validateCoverageLedger } from './coverage-integrity.mjs';
|
|
8
|
+
import { canonicalJson, digest, eligibleRepositoryStanding, validateCoverageLedger } from './coverage-integrity.mjs';
|
|
9
|
+
import { fixtureDenominator } from './fixture-denominator.mjs';
|
|
9
10
|
|
|
10
11
|
// Both release phases resolve against an explicit installed context, never the checkout.
|
|
11
12
|
export async function resolveInstalledCanaryCitation({ kbDir, matched, expected, passageFileDigests = new Map() }) {
|
|
@@ -204,8 +205,10 @@ export function auditOracleCoverage({ coverage, queryEvidence, exemptions = null
|
|
|
204
205
|
const checked = validateCoverageLedger(coverage);
|
|
205
206
|
if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
|
|
206
207
|
validateRetrievalQueryEvidence(queryEvidence);
|
|
208
|
+
// ADR-0091 D5: a shipped store is CURRENT or STALE with a verified carry (its bytes ship). A
|
|
209
|
+
// MISSING-with-failure store ships nothing; the fixture-vs-available denominator is D6.4's change.
|
|
207
210
|
const eligibleRows = coverage.rows.filter((row) => row.kind === 'repository'
|
|
208
|
-
&& row.disposition === 'eligible' && row
|
|
211
|
+
&& row.disposition === 'eligible' && eligibleRepositoryStanding(row) === 'shipped');
|
|
209
212
|
const eligible = ordered(eligibleRows.map(storeOf));
|
|
210
213
|
if (!eligible.length || new Set(eligible).size !== eligible.length || eligible.some((store) => !store)) {
|
|
211
214
|
throw new Error('eligible coverage denominator is invalid');
|
|
@@ -310,9 +313,20 @@ export function validateRetrievalCanaryPlan(plan) {
|
|
|
310
313
|
'legacy population denominator');
|
|
311
314
|
checkedSet(plan.denominator.legacySelectedStores, plan.denominator.legacySelectedStoreSetSha256,
|
|
312
315
|
'legacy selected denominator');
|
|
316
|
+
// ADR-0091 D6.4: `eligibleStores` is the FROZEN FIXTURE denominator (it must equal the oracle's store
|
|
317
|
+
// set exactly, as before). Retired fixture stores are excluded from questioning; eligible stores the
|
|
318
|
+
// fixture does not cover are recorded, never questioned and never blocking.
|
|
319
|
+
checkedSet(plan.denominator.retiredFixtureStores, plan.denominator.retiredFixtureStoreSetSha256, 'retired fixture denominator');
|
|
320
|
+
checkedSet(plan.denominator.unfixturedEligibleStores, plan.denominator.unfixturedEligibleStoreSetSha256,
|
|
321
|
+
'unfixtured eligible record');
|
|
313
322
|
if (setDigest(plan.denominator.eligibleStores) !== plan.oracle.queryStoreSetSha256) {
|
|
314
323
|
throw new Error('oracle denominator differs from eligible coverage');
|
|
315
324
|
}
|
|
325
|
+
if (plan.denominator.retiredFixtureStores.some((store) => !plan.denominator.eligibleStores.includes(store))
|
|
326
|
+
|| plan.denominator.unfixturedEligibleStores.some((store) => plan.denominator.eligibleStores.includes(store))
|
|
327
|
+
|| plan.denominator.unfixturedEligibleCount !== plan.denominator.unfixturedEligibleStores.length) {
|
|
328
|
+
throw new Error('retrieval canary fixture denominator is inconsistent');
|
|
329
|
+
}
|
|
316
330
|
if (new Set(ids).size !== ids.length) throw new Error('retrieval canary plan has duplicate case ids');
|
|
317
331
|
const hasDelta = plan.cases.some(({ cohort }) => cohort === 'delta');
|
|
318
332
|
if ((!hasDelta && plan.noDelta !== true) || (hasDelta && plan.noDelta === true)
|
|
@@ -352,14 +366,18 @@ export function validatePlanAgainstCoverage(plan, coverage, { allowObservedBasel
|
|
|
352
366
|
if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
|
|
353
367
|
const generation = coverage.kind === 'ruvnet-brain-release-coverage'
|
|
354
368
|
? coverage.releaseCoverageGeneration : coverage.coverageGeneration;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (
|
|
369
|
+
// Recomputed from the coverage and the plan's own sealed fixture, never trusted from the plan.
|
|
370
|
+
const denominator = fixtureDenominator({ coverage, fixtureStores: Object.keys(plan.oracle.evidence.queries) });
|
|
371
|
+
if (denominator.blocking.length) throw new Error(`fixture store(s) neither available nor retired: ${denominator.blocking.map(({ store }) => store).join(', ')}`);
|
|
372
|
+
const eligible = denominator.questioned;
|
|
373
|
+
if (!eligible.length) throw new Error('eligible coverage denominator is invalid');
|
|
358
374
|
const baseline = new Set(plan.baseline.stores);
|
|
359
375
|
const delta = eligible.filter((store) => !baseline.has(store));
|
|
360
376
|
const legacy = eligible.filter((store) => baseline.has(store));
|
|
361
377
|
if (generation !== plan.coverage.releaseCoverageGeneration
|
|
362
|
-
|| canonicalJson(
|
|
378
|
+
|| canonicalJson(denominator.fixture) !== canonicalJson(plan.denominator.eligibleStores)
|
|
379
|
+
|| canonicalJson(denominator.retired) !== canonicalJson(plan.denominator.retiredFixtureStores)
|
|
380
|
+
|| canonicalJson(denominator.unfixturedEligible) !== canonicalJson(plan.denominator.unfixturedEligibleStores)
|
|
363
381
|
|| canonicalJson(delta) !== canonicalJson(plan.denominator.deltaStores)
|
|
364
382
|
|| canonicalJson(legacy) !== canonicalJson(plan.denominator.legacyPopulationStores)
|
|
365
383
|
|| plan.denominator.legacySelectedStores.some((store) => !legacy.includes(store))) {
|
|
@@ -417,20 +435,19 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
|
|
|
417
435
|
}
|
|
418
436
|
validateRetrievalQueryEvidence(queryEvidence);
|
|
419
437
|
if (queryEvidence.sourceCommit === candidate.sourceSha) throw new Error('independent query source is not pre-candidate');
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
438
|
+
// ADR-0091 D6.4: fixture ⊆ available. The frozen fixture no longer has to EQUAL a living eligible
|
|
439
|
+
// set; every fixture store must ship (or be verified retired), and eligible stores the fixture does
|
|
440
|
+
// not cover are recorded as unfixturedEligible, never blocking.
|
|
441
|
+
let denominator;
|
|
442
|
+
try { denominator = fixtureDenominator({ coverage, fixtureStores: Object.keys(queryEvidence.queries) }); }
|
|
443
|
+
catch (error) { throw new Error(`eligible repository coverage is incomplete (${error.message})`); }
|
|
444
|
+
if (denominator.blocking.length) {
|
|
445
|
+
throw new Error(`independent query oracle names ${denominator.blocking.length} fixture store(s) that are neither shipped nor `
|
|
446
|
+
+ `verified retired: ${denominator.blocking.map(({ store, status }) => `${store}:${status ?? 'no-row'}`).join(', ')}`);
|
|
447
|
+
}
|
|
448
|
+
const eligibleStores = denominator.fixture;
|
|
449
|
+
const eligible = denominator.questionedRows;
|
|
450
|
+
if (!eligible.length) throw new Error('eligible repository coverage is incomplete');
|
|
434
451
|
const baselineStores = new Set(baseline.stores.map((name) => String(name).toLowerCase()));
|
|
435
452
|
const delta = eligible.filter((row) => !baselineStores.has(storeOf(row)));
|
|
436
453
|
const legacyPool = eligible.filter((row) => baselineStores.has(storeOf(row)));
|
|
@@ -502,6 +519,11 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
|
|
|
502
519
|
denominator: {
|
|
503
520
|
eligibleStores,
|
|
504
521
|
eligibleStoreSetSha256: setDigest(eligibleStores),
|
|
522
|
+
retiredFixtureStores: denominator.retired,
|
|
523
|
+
retiredFixtureStoreSetSha256: setDigest(denominator.retired),
|
|
524
|
+
unfixturedEligibleStores: denominator.unfixturedEligible,
|
|
525
|
+
unfixturedEligibleStoreSetSha256: setDigest(denominator.unfixturedEligible),
|
|
526
|
+
unfixturedEligibleCount: denominator.unfixturedEligible.length,
|
|
505
527
|
deltaStores: ordered(delta.map(storeOf)),
|
|
506
528
|
deltaStoreSetSha256: setDigest(delta.map(storeOf)),
|
|
507
529
|
legacyPopulationStores: ordered(legacyPool.map(storeOf)),
|
|
@@ -341,7 +341,13 @@ function assertExclusionEvidence(repo, exclusion, upstreamSha) {
|
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
// ADR-0091 D5: `outcome` is what reconciliation recorded for this store when its refresh FAILED this
|
|
345
|
+
// generation -- { carry } (previous bytes kept, already re-hashed against the ledger), { failure } (no
|
|
346
|
+
// prior bytes), or { integrity } (carried bytes failed that re-hash). The status token is always the
|
|
347
|
+
// one the evidence proves; the record is attached only where it agrees with that status, so a carry
|
|
348
|
+
// can never dress up a row the bytes do not support. Rows are annotated BEFORE sealing, so the
|
|
349
|
+
// coverage generation digest covers them.
|
|
350
|
+
export function classifyRepository(repo, evidence, exclusion = null, outcome = null) {
|
|
345
351
|
const upstreamSha = repo.defaultBranchRef?.target?.oid || null;
|
|
346
352
|
const activeExclusion = Boolean(exclusion && String(exclusion.pushedAt || '') !== ''
|
|
347
353
|
&& String(exclusion.pushedAt) === String(repo.pushedAt || ''));
|
|
@@ -373,6 +379,14 @@ export function classifyRepository(repo, evidence, exclusion = null) {
|
|
|
373
379
|
else if (evidence.receipt.sourceCommit !== upstreamSha) { status = 'STALE'; reasons.push('receipt sourceCommit differs from upstream HEAD'); }
|
|
374
380
|
else if (!evidence.bytesVerified) { status = 'FAILED'; reasons.push('RVF bytes do not match receipt'); }
|
|
375
381
|
else if (!evidence.passagesPresent) { status = 'FAILED'; reasons.push('passage inventory is absent'); }
|
|
382
|
+
const disposed = isIngestibleDisposition(disposition);
|
|
383
|
+
if (disposed && outcome?.integrity && ['CURRENT', 'STALE'].includes(status)) {
|
|
384
|
+
status = 'FAILED';
|
|
385
|
+
reasons.push(outcome.integrity);
|
|
386
|
+
}
|
|
387
|
+
const record = {};
|
|
388
|
+
if (disposed && outcome?.carry && status === 'STALE') record.carry = { ...outcome.carry };
|
|
389
|
+
if (disposed && outcome?.failure && status === 'MISSING') record.failure = { ...outcome.failure };
|
|
376
390
|
return {
|
|
377
391
|
key: repo.fullName ? `repo:${repo.fullName.toLowerCase()}` : `repo:${repo.databaseId}`,
|
|
378
392
|
kind: 'repository',
|
|
@@ -391,6 +405,7 @@ export function classifyRepository(repo, evidence, exclusion = null) {
|
|
|
391
405
|
cardPresent: evidence.cardPresent },
|
|
392
406
|
status,
|
|
393
407
|
reasons,
|
|
408
|
+
...record,
|
|
394
409
|
};
|
|
395
410
|
}
|
|
396
411
|
|
|
@@ -567,7 +582,7 @@ export function explainCoverageDrift(recorded, current) {
|
|
|
567
582
|
// where they are source-controlled (`<repo>/kb`) unless the caller names both directories, as the
|
|
568
583
|
// release path does with `--assets`.
|
|
569
584
|
export function buildCoverage({ owner = 'ruvnet', env = process.env, home = os.homedir(), kbDir = null, policyDir = null,
|
|
570
|
-
observation = null, gh = runGh, now = () => new Date().toISOString() } = {}) {
|
|
585
|
+
observation = null, gh = runGh, now = () => new Date().toISOString(), storeOutcomes = null } = {}) {
|
|
571
586
|
policyDir ??= kbDir ?? path.join(ROOT, 'kb');
|
|
572
587
|
kbDir ??= storeRoot(env, home);
|
|
573
588
|
if (rootNeverMaterialized(kbDir)) {
|
|
@@ -597,7 +612,8 @@ export function buildCoverage({ owner = 'ruvnet', env = process.env, home = os.h
|
|
|
597
612
|
const exclusions = fs.existsSync(exclusionsPath) ? JSON.parse(fs.readFileSync(exclusionsPath, 'utf8')) : {};
|
|
598
613
|
const rows = repositories.rows.map((repo) => {
|
|
599
614
|
const store = storeName(repo.storeName || repo.name);
|
|
600
|
-
return classifyRepository(repo, artifactEvidence(kbDir, ledger, cardStores, store), exclusions[store] || null
|
|
615
|
+
return classifyRepository(repo, artifactEvidence(kbDir, ledger, cardStores, store), exclusions[store] || null,
|
|
616
|
+
storeOutcomes?.[store.toLowerCase()] || null);
|
|
601
617
|
});
|
|
602
618
|
const gistEvidence = { ...artifactEvidence(kbDir, ledger, cardStores, 'ruv-gists'), sources: gistSources };
|
|
603
619
|
try {
|