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
@@ -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
- // EXACT equality, never "GITHUB_SHA or an ancestor of it" (independent review of ADR-0091 D3,
168
- // 2026-09-28). Accepting an ancestor let the unattended corpus job promote an OLDER runtime over the
169
- // current live code release as `releases/latest` — fresh installs then fail on a version mismatch
170
- // and already-updated clients refuse it as incompatible. The corpus is built at the newest
171
- // install-verified release's sourceSha, and that must BE the protected main commit this run executes.
172
- // The format check runs first; the comparisons below never hand the value to a subprocess.
173
- if (!isHex(target, 40) || target !== head || target !== env.GITHUB_SHA || target !== receipt.builderSourceSha) {
174
- corpusFailure('target must exactly equal HEAD, GITHUB_SHA, and the corpus receipt builderSourceSha');
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: loadFixture().fixtureSha256,
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 view = run(ghCommand, [...ghPrefix, ...viewArgs], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
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
- const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isLatest,isPrerelease,assets', '--repo', repo]);
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 || promoted.isLatest !== true
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
- process.exitCode = 1;
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,10 +5,12 @@ 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';
10
+ import { passageMatches } from './retrieval-passage-identity.mjs';
9
11
 
10
12
  // Both release phases resolve against an explicit installed context, never the checkout.
11
- export async function resolveInstalledCanaryCitation({ kbDir, matched, expected, passageFileDigests = new Map() }) {
13
+ export async function resolveInstalledCanaryCitation({ kbDir, matched, expected, passageFileDigests = new Map(), contentMap }) {
12
14
  if (!path.isAbsolute(kbDir || '')) throw new Error('installed canary KB path must be absolute');
13
15
  if (String(matched?.repo || '').toLowerCase() !== expected.repo || matched?.path !== expected.path) return { resolved: false };
14
16
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(expected.repo)) throw new Error('installed citation repository violates containment');
@@ -34,7 +36,7 @@ export async function resolveInstalledCanaryCitation({ kbDir, matched, expected,
34
36
  let record;
35
37
  try { record = JSON.parse(line); } catch { continue; }
36
38
  if (record?.path !== expected.path) continue;
37
- if (digest(record) !== expected.passageSha256) continue;
39
+ if (!passageMatches(record, expected.passageSha256, contentMap)) continue;
38
40
  const text = record.fullText || record.text;
39
41
  if (typeof text !== 'string' || !text || typeof matched.text !== 'string' || !matched.text.includes(text)) continue;
40
42
  passageSha256 = expected.passageSha256;
@@ -204,8 +206,10 @@ export function auditOracleCoverage({ coverage, queryEvidence, exemptions = null
204
206
  const checked = validateCoverageLedger(coverage);
205
207
  if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
206
208
  validateRetrievalQueryEvidence(queryEvidence);
209
+ // ADR-0091 D5: a shipped store is CURRENT or STALE with a verified carry (its bytes ship). A
210
+ // MISSING-with-failure store ships nothing; the fixture-vs-available denominator is D6.4's change.
207
211
  const eligibleRows = coverage.rows.filter((row) => row.kind === 'repository'
208
- && row.disposition === 'eligible' && row.status === 'CURRENT');
212
+ && row.disposition === 'eligible' && eligibleRepositoryStanding(row) === 'shipped');
209
213
  const eligible = ordered(eligibleRows.map(storeOf));
210
214
  if (!eligible.length || new Set(eligible).size !== eligible.length || eligible.some((store) => !store)) {
211
215
  throw new Error('eligible coverage denominator is invalid');
@@ -310,9 +314,20 @@ export function validateRetrievalCanaryPlan(plan) {
310
314
  'legacy population denominator');
311
315
  checkedSet(plan.denominator.legacySelectedStores, plan.denominator.legacySelectedStoreSetSha256,
312
316
  'legacy selected denominator');
317
+ // ADR-0091 D6.4: `eligibleStores` is the FROZEN FIXTURE denominator (it must equal the oracle's store
318
+ // set exactly, as before). Retired fixture stores are excluded from questioning; eligible stores the
319
+ // fixture does not cover are recorded, never questioned and never blocking.
320
+ checkedSet(plan.denominator.retiredFixtureStores, plan.denominator.retiredFixtureStoreSetSha256, 'retired fixture denominator');
321
+ checkedSet(plan.denominator.unfixturedEligibleStores, plan.denominator.unfixturedEligibleStoreSetSha256,
322
+ 'unfixtured eligible record');
313
323
  if (setDigest(plan.denominator.eligibleStores) !== plan.oracle.queryStoreSetSha256) {
314
324
  throw new Error('oracle denominator differs from eligible coverage');
315
325
  }
326
+ if (plan.denominator.retiredFixtureStores.some((store) => !plan.denominator.eligibleStores.includes(store))
327
+ || plan.denominator.unfixturedEligibleStores.some((store) => plan.denominator.eligibleStores.includes(store))
328
+ || plan.denominator.unfixturedEligibleCount !== plan.denominator.unfixturedEligibleStores.length) {
329
+ throw new Error('retrieval canary fixture denominator is inconsistent');
330
+ }
316
331
  if (new Set(ids).size !== ids.length) throw new Error('retrieval canary plan has duplicate case ids');
317
332
  const hasDelta = plan.cases.some(({ cohort }) => cohort === 'delta');
318
333
  if ((!hasDelta && plan.noDelta !== true) || (hasDelta && plan.noDelta === true)
@@ -352,14 +367,18 @@ export function validatePlanAgainstCoverage(plan, coverage, { allowObservedBasel
352
367
  if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
353
368
  const generation = coverage.kind === 'ruvnet-brain-release-coverage'
354
369
  ? coverage.releaseCoverageGeneration : coverage.coverageGeneration;
355
- const eligible = ordered(coverage.rows.filter((row) => row.kind === 'repository'
356
- && row.disposition === 'eligible' && row.status === 'CURRENT').map(storeOf));
357
- if (!eligible.length || new Set(eligible).size !== eligible.length) throw new Error('eligible coverage denominator is invalid');
370
+ // Recomputed from the coverage and the plan's own sealed fixture, never trusted from the plan.
371
+ const denominator = fixtureDenominator({ coverage, fixtureStores: Object.keys(plan.oracle.evidence.queries) });
372
+ if (denominator.blocking.length) throw new Error(`fixture store(s) neither available nor retired: ${denominator.blocking.map(({ store }) => store).join(', ')}`);
373
+ const eligible = denominator.questioned;
374
+ if (!eligible.length) throw new Error('eligible coverage denominator is invalid');
358
375
  const baseline = new Set(plan.baseline.stores);
359
376
  const delta = eligible.filter((store) => !baseline.has(store));
360
377
  const legacy = eligible.filter((store) => baseline.has(store));
361
378
  if (generation !== plan.coverage.releaseCoverageGeneration
362
- || canonicalJson(eligible) !== canonicalJson(plan.denominator.eligibleStores)
379
+ || canonicalJson(denominator.fixture) !== canonicalJson(plan.denominator.eligibleStores)
380
+ || canonicalJson(denominator.retired) !== canonicalJson(plan.denominator.retiredFixtureStores)
381
+ || canonicalJson(denominator.unfixturedEligible) !== canonicalJson(plan.denominator.unfixturedEligibleStores)
363
382
  || canonicalJson(delta) !== canonicalJson(plan.denominator.deltaStores)
364
383
  || canonicalJson(legacy) !== canonicalJson(plan.denominator.legacyPopulationStores)
365
384
  || plan.denominator.legacySelectedStores.some((store) => !legacy.includes(store))) {
@@ -368,7 +387,8 @@ export function validatePlanAgainstCoverage(plan, coverage, { allowObservedBasel
368
387
  return plan;
369
388
  }
370
389
  export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, coverageIdentity = null, queryEvidence, assetsDir = '.',
371
- readPassages = defaultReadPassages, legacySampleSize, allowNoDelta = false } = {}) {
390
+ readPassages = defaultReadPassages, legacySampleSize, allowNoDelta = false, contentMap, knownHitStores = null,
391
+ notice = (message) => process.stderr.write(`${message}\n`) } = {}) {
372
392
  const checked = validateCoverageLedger(coverage);
373
393
  if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
374
394
  const coverageGeneration = coverage.kind === 'ruvnet-brain-release-coverage'
@@ -417,20 +437,19 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
417
437
  }
418
438
  validateRetrievalQueryEvidence(queryEvidence);
419
439
  if (queryEvidence.sourceCommit === candidate.sourceSha) throw new Error('independent query source is not pre-candidate');
420
- const eligible = coverage.rows.filter((row) => row.kind === 'repository' && row.disposition === 'eligible');
421
- if (!eligible.length || eligible.some((row) => row.status !== 'CURRENT' || !storeOf(row))) {
422
- throw new Error('eligible repository coverage is incomplete');
423
- }
424
- const duplicateStores = eligible.map(storeOf).filter((store, index, stores) => stores.indexOf(store) !== index);
425
- if (duplicateStores.length) throw new Error(`eligible repository stores are duplicated: ${ordered(new Set(duplicateStores)).join(', ')}`);
426
- const eligibleStores = ordered(eligible.map(storeOf));
427
- if (queryEvidence.queryStoreSetSha256 !== setDigest(eligibleStores)
428
- || canonicalJson(ordered(Object.keys(queryEvidence.queries))) !== canonicalJson(eligibleStores)) {
429
- const oracleStores = new Set(Object.keys(queryEvidence.queries));
430
- const missing = eligibleStores.filter((store) => !oracleStores.has(store));
431
- const extra = [...oracleStores].filter((store) => !eligibleStores.includes(store)).sort();
432
- throw new Error(`independent query oracle does not cover the exact eligible store set (eligible=${eligibleStores.length}, oracle=${oracleStores.size}, missing=${missing.join(',') || 'none'}, extra=${extra.join(',') || 'none'})`);
433
- }
440
+ // ADR-0091 D6.4: fixture ⊆ available. The frozen fixture no longer has to EQUAL a living eligible
441
+ // set; every fixture store must ship (or be verified retired), and eligible stores the fixture does
442
+ // not cover are recorded as unfixturedEligible, never blocking.
443
+ let denominator;
444
+ try { denominator = fixtureDenominator({ coverage, fixtureStores: Object.keys(queryEvidence.queries) }); }
445
+ catch (error) { throw new Error(`eligible repository coverage is incomplete (${error.message})`); }
446
+ if (denominator.blocking.length) {
447
+ throw new Error(`independent query oracle names ${denominator.blocking.length} fixture store(s) that are neither shipped nor `
448
+ + `verified retired: ${denominator.blocking.map(({ store, status }) => `${store}:${status ?? 'no-row'}`).join(', ')}`);
449
+ }
450
+ const eligibleStores = denominator.fixture;
451
+ const eligible = denominator.questionedRows;
452
+ if (!eligible.length) throw new Error('eligible repository coverage is incomplete');
434
453
  const baselineStores = new Set(baseline.stores.map((name) => String(name).toLowerCase()));
435
454
  const delta = eligible.filter((row) => !baselineStores.has(storeOf(row)));
436
455
  const legacyPool = eligible.filter((row) => baselineStores.has(storeOf(row)));
@@ -438,12 +457,47 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
438
457
  const passages = new Map(eligible.map((row) => [storeOf(row), readPassages(assetsDir, storeOf(row))]));
439
458
  const rankedLegacy = legacyPool.map((row) => ({ row, count: passages.get(storeOf(row)).length }))
440
459
  .sort((a, b) => a.count - b.count || storeOf(a.row).localeCompare(storeOf(b.row)));
460
+ // The legacy sample is drawn only from stores whose sealed passage still exists, unchanged, exactly
461
+ // once in the shipped store. When upstream edits the very file a fixture question was written against,
462
+ // that question can no longer identify its passage — the fixture is stale for that store, which says
463
+ // nothing about retrieval. Such stores stay in the sealed POPULATION (recomputed from coverage by
464
+ // validatePlanAgainstCoverage) but cannot be sampled; they are named below so it is never silent, and
465
+ // the nightly per-repository recall gate still exercises every one of them by file path.
466
+ const sealedPassageResolves = (store) => {
467
+ const evidence = queryEvidence.queries[store];
468
+ return Boolean(evidence) && expectedSources(evidence.expected).every((source) =>
469
+ passages.get(store).filter((row) => row.path === source.path && passageMatches(row, source.passageSha256, contentMap)).length === 1);
470
+ };
471
+ // INTEGRITY, NOT QUALITY. When the generation being shipped carries its own repo-recall measurement,
472
+ // `knownHitStores` names the stores that measurement retrieved. The release sample is then drawn from
473
+ // them, so the canary proves the SHIPPED, INSTALLED bundle reproduces what the generation measured on
474
+ // the same stores (a packaging, index, model or runtime break shows up as a miss on a store that hit).
475
+ // It deliberately does NOT re-judge stores the generation already missed: whole-corpus retrieval quality
476
+ // is the recall report's job and stays visible there (and in the corpus watchdog), because sampling ~19
477
+ // stores at an absolute 98% bar is a coin flip for any corpus below ~98% true recall (measured
478
+ // 2026-09-30: previous corpus 18/19, fresh corpus 17/19 on the same questions). Without a measurement
479
+ // (the committed bootstrap seed) nothing is filtered and the historical behaviour is unchanged.
480
+ const measuredHit = (store) => !knownHitStores || knownHitStores.has(store);
441
481
  const strata = new Map();
482
+ const staleFixtureStores = [];
483
+ const generationMissStores = [];
442
484
  rankedLegacy.forEach((entry, index) => {
443
485
  const stratum = Math.min(3, Math.floor(index * 4 / rankedLegacy.length));
486
+ const store = storeOf(entry.row);
487
+ if (!sealedPassageResolves(store)) { staleFixtureStores.push(store); return; }
488
+ if (!measuredHit(store)) { generationMissStores.push(store); return; }
444
489
  if (!strata.has(stratum)) strata.set(stratum, []);
445
490
  strata.get(stratum).push(entry);
446
491
  });
492
+ if (generationMissStores.length) {
493
+ notice(`[retrieval-canary] ${generationMissStores.length} of ${rankedLegacy.length} fixture store(s) not sampled: the generation's own `
494
+ + `recall measurement did not retrieve their sealed file (retrieval-quality debt, tracked by the recall report, not re-judged here): `
495
+ + `${ordered(generationMissStores).join(', ')}`);
496
+ }
497
+ if (staleFixtureStores.length) {
498
+ notice(`[retrieval-canary] ${staleFixtureStores.length} of ${rankedLegacy.length} fixture store(s) excluded from the legacy sample: `
499
+ + `their sealed passage no longer exists unchanged in the shipped store: ${ordered(staleFixtureStores).join(', ')}`);
500
+ }
447
501
  // Source-only releases retain the same corpus sample; the plan still seals exact release bytes.
448
502
  const samplingGeneration = coverage.kind === 'ruvnet-brain-release-coverage'
449
503
  ? coverage.corpusCoverage.coverageGeneration : coverageGeneration;
@@ -470,7 +524,7 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
470
524
  const observedPassageCount = passageCount ?? passages.get(store).length;
471
525
  const evidence = queryEvidence.queries[store];
472
526
  if (!evidence || expectedSources(evidence.expected).some((source) =>
473
- passages.get(store).filter((row) => row.path === source.path && digest(row) === source.passageSha256).length !== 1)) {
527
+ passages.get(store).filter((row) => row.path === source.path && passageMatches(row, source.passageSha256, contentMap)).length !== 1)) {
474
528
  throw new Error(`${store} has no sealed independent query evidence`);
475
529
  }
476
530
  return {
@@ -502,6 +556,11 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
502
556
  denominator: {
503
557
  eligibleStores,
504
558
  eligibleStoreSetSha256: setDigest(eligibleStores),
559
+ retiredFixtureStores: denominator.retired,
560
+ retiredFixtureStoreSetSha256: setDigest(denominator.retired),
561
+ unfixturedEligibleStores: denominator.unfixturedEligible,
562
+ unfixturedEligibleStoreSetSha256: setDigest(denominator.unfixturedEligible),
563
+ unfixturedEligibleCount: denominator.unfixturedEligible.length,
505
564
  deltaStores: ordered(delta.map(storeOf)),
506
565
  deltaStoreSetSha256: setDigest(delta.map(storeOf)),
507
566
  legacyPopulationStores: ordered(legacyPool.map(storeOf)),
@@ -0,0 +1,58 @@
1
+ // How the retrieval fixture recognises "the expected passage" across store rebuilds.
2
+ //
3
+ // data/retrieval-query-evidence.json pins each expected passage by digest(row), where a row is
4
+ // { id, path, text, title }. Until 2026-09-29 the `id` was an ordinal ("2824"); the CI corpus builder
5
+ // now writes content-addressed ids ("chunk:<hash>"). The same passage — identical path, title and text —
6
+ // therefore changed digest with no content change, and every release that consumed a freshly built
7
+ // generation failed with "<store> has no sealed independent query evidence" (4.3.37 preflight, 2026-09-29).
8
+ //
9
+ // The fixture bytes are FROZEN on purpose: its sha256 is what corpus-next-seed judges a generation's
10
+ // recall report against, so editing it would make the newest generation an incompatible seed and force
11
+ // a full multi-hour rebuild. Instead this module adds a second, id-independent identity, looked up
12
+ // through a committed map (pinned digest -> content digest) derived once, mechanically, from the last
13
+ // corpus built with ordinal ids (scripts/derive-passage-content-map.mjs). The map can only ADD
14
+ // acceptance for a row whose path, title and text equal the row the fixture originally pinned.
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { digest } from './coverage-integrity.mjs';
19
+
20
+ export const CONTENT_MAP_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
21
+ '..', 'data', 'retrieval-passage-content-digests.json');
22
+ export const CONTENT_MAP_KIND = 'ruvnet-brain-retrieval-passage-content-digests';
23
+ const HEX64 = /^[a-f0-9]{64}$/;
24
+
25
+ /** Digest of everything about a passage row except its (build-dependent) id. */
26
+ export function passageContentDigest(row) {
27
+ const { id: _id, ...rest } = row ?? {};
28
+ return digest(rest);
29
+ }
30
+
31
+ /** pinned digest -> content digest. A missing file is an empty map: legacy exact-digest matching only. */
32
+ export function loadContentMap(file = CONTENT_MAP_FILE) {
33
+ let parsed;
34
+ try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) {
35
+ if (error.code === 'ENOENT') return new Map();
36
+ throw new Error(`passage content map is unreadable: ${error.message}`);
37
+ }
38
+ if (parsed?.kind !== CONTENT_MAP_KIND || parsed.schemaVersion !== 1
39
+ || !parsed.entries || typeof parsed.entries !== 'object' || Array.isArray(parsed.entries)) {
40
+ throw new Error('passage content map is malformed');
41
+ }
42
+ const map = new Map();
43
+ for (const [pinned, content] of Object.entries(parsed.entries)) {
44
+ if (!HEX64.test(pinned) || !HEX64.test(String(content))) throw new Error('passage content map holds a non-sha256 entry');
45
+ map.set(pinned, content);
46
+ }
47
+ return map;
48
+ }
49
+
50
+ let cachedMap = null;
51
+ const defaultMap = () => (cachedMap ??= loadContentMap());
52
+
53
+ /** Does this row satisfy a fixture pin: exact digest, or (when the map knows the pin) equal content. */
54
+ export function passageMatches(row, pinnedSha256, map = defaultMap()) {
55
+ if (digest(row) === pinnedSha256) return true;
56
+ const content = map.get(pinnedSha256);
57
+ return Boolean(content) && passageContentDigest(row) === content;
58
+ }
@@ -341,7 +341,13 @@ function assertExclusionEvidence(repo, exclusion, upstreamSha) {
341
341
  }
342
342
  }
343
343
 
344
- export function classifyRepository(repo, evidence, exclusion = null) {
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 {
Binary file
@@ -120,6 +120,10 @@ const STANDALONE = [
120
120
  ['self-update', 'author-run candidate rebuild; --apply is guarded by worktree-integrity.mjs and is not scheduled'],
121
121
  ['ingest-new-repos', 'author-run corpus expansion; --apply is guarded by worktree-integrity.mjs and is not scheduled'],
122
122
  ['count-chunks', 'human-run CLI — recount + restamp chunk surfaces (--check for drift); no scheduler'],
123
+ ['derive-passage-content-map', 'human-run maintainer tool — regenerates data/retrieval-passage-content-digests.json '
124
+ + 'from a corpus built with ordinal passage ids, only when the frozen fixture changes; '
125
+ + 'tests/unit/retrieval-passage-identity.test.mjs fails if the committed map stops matching the fixture, '
126
+ + 'so a stale map cannot go unnoticed and there is nothing to schedule'],
123
127
  ['brain-stamp', 'invoked by the author-run self-update.mjs candidate builder'],
124
128
  ['lesson-promote', 'human-run CLI — promotion is manual (--apply); no scheduler yet (automation is ADR-029 #4, open)'],
125
129
  ['behavioral-l1-l4', 'behavioural harness invoked by its own test file — not a product path'],