ruvnet-brain 4.3.36 → 4.3.38

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 (46) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +32 -9
  3. package/kb/forge-update.mjs +1867 -0
  4. package/package.json +3 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/hooks/codex-hooks.json +6 -1
  8. package/plugin/hooks/hook-contracts.json +11 -9
  9. package/plugin/scripts/capability-claim-evidence.mjs +11 -2
  10. package/plugin/scripts/capacity-aware-parallel-work.mjs +71 -15
  11. package/plugin/scripts/completion-claim-evidence.mjs +262 -0
  12. package/plugin/scripts/continuation-gate.mjs +105 -21
  13. package/plugin/scripts/continuation-objective.mjs +15 -0
  14. package/plugin/scripts/continuity-hook-policy.mjs +10 -3
  15. package/plugin/scripts/decision-gate.mjs +22 -2
  16. package/plugin/scripts/duplicate-gate.mjs +503 -0
  17. package/plugin/scripts/grounding-turn-evidence.mjs +339 -0
  18. package/plugin/scripts/grounding-turn-gate.mjs +77 -25
  19. package/plugin/scripts/grounding-turn-mark.mjs +61 -14
  20. package/plugin/scripts/hook-input.mjs +15 -0
  21. package/plugin/scripts/hook-shim.mjs +3 -1
  22. package/plugin/scripts/host-update.mjs +45 -0
  23. package/plugin/scripts/nightly-scheduler.mjs +34 -0
  24. package/plugin/scripts/session-snapshot-hook.mjs +11 -1
  25. package/plugin/scripts/session-start-budget.mjs +1 -0
  26. package/plugin/scripts/session-start-core.mjs +15 -2
  27. package/plugin/scripts/session-start-health.mjs +101 -1
  28. package/plugin/scripts/session-start-update-plane.mjs +71 -1
  29. package/plugin/scripts/turn-outcome-capture.mjs +292 -0
  30. package/scripts/completion-claim-replay.mjs +114 -0
  31. package/scripts/corpus-canary.mjs +399 -0
  32. package/scripts/corpus-dispatch-decision.mjs +2 -2
  33. package/scripts/corpus-promotion.mjs +49 -0
  34. package/scripts/corpus-reconcile.mjs +27 -3
  35. package/scripts/corpus-watchdog.mjs +45 -6
  36. package/scripts/derive-passage-content-map.mjs +71 -0
  37. package/scripts/duplicate-gate-replay.mjs +98 -0
  38. package/scripts/grounding-turn-replay.mjs +131 -0
  39. package/scripts/nightly-watchdog.mjs +3 -3
  40. package/scripts/public-verification-inputs.mjs +24 -2
  41. package/scripts/release-transaction-provider.mjs +29 -6
  42. package/scripts/release.mjs +177 -74
  43. package/scripts/retrieval-canary.mjs +41 -4
  44. package/scripts/retrieval-passage-identity.mjs +58 -0
  45. package/scripts/sync-census.mjs +0 -0
  46. package/scripts/wired-check.mjs +6 -0
@@ -65,7 +65,9 @@ const recallNotes = (receipt) => {
65
65
  ];
66
66
  };
67
67
  import { verifyBundle } from './verify-bundle.mjs';
68
- import { CORPUS_GENERATION_FIELD, evaluateCorpusPromotion } from './corpus-promotion.mjs';
68
+ import {
69
+ CORPUS_GENERATION_FIELD, CORPUS_TAG_PATTERN, evaluateCanaryVerdict, evaluateCorpusPromotion, parseCorpusGeneration,
70
+ } from './corpus-promotion.mjs';
69
71
  import { bindCoverageToReceipt, writeCoverageAssets } from './corpus-coverage-sidecar.mjs';
70
72
  import { degradedPublication } from './corpus-store-failure.mjs';
71
73
  import { assertNoNewerCorpusGeneration } from './code-release-corpus.mjs';
@@ -139,6 +141,68 @@ const compareCodeTags = (left, right) => {
139
141
  return Math.sign(a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
140
142
  };
141
143
 
144
+ /** The one gh seam both corpus entry points use (RUVNET_GH_COMMAND / RUVNET_GH_SCRIPT for tests). */
145
+ function corpusGh(env, run) {
146
+ const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
147
+ const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
148
+ const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
149
+ const ghJson = (args, label) => {
150
+ const result = gh(args);
151
+ if (result.error || result.status !== 0) {
152
+ corpusFailure(`cannot read ${label} (${String(result.error?.message || result.stderr || result.stdout || '').trim() || `gh exited ${result.status}`})`);
153
+ }
154
+ try { return JSON.parse(String(result.stdout || 'null')); }
155
+ catch (error) { corpusFailure(`cannot parse ${label} (${error.message})`); }
156
+ };
157
+ return { gh, ghJson };
158
+ }
159
+
160
+ /**
161
+ * PUBLISH-TIME RE-RESOLVE, before anything is written. Preparation takes hours and the customer canary
162
+ * adds more; a code release may have been published meanwhile. The newest code release is selected
163
+ * exactly as scripts/approved-runtime.mjs selects it (non-draft, non-prerelease vX.Y.Z, highest).
164
+ */
165
+ function assertApprovedRuntimeIsNewest({ ghJson, repo, approvedTag, target }) {
166
+ const listed = ghJson(['release', 'list', '--repo', repo, '--limit', '200', '--json', 'tagName,isDraft,isPrerelease'], 'the code release list');
167
+ const [newest] = (Array.isArray(listed) ? listed : [])
168
+ .filter((row) => !row?.isDraft && !row?.isPrerelease && CODE_TAG.test(String(row?.tagName || '')))
169
+ .map((row) => row.tagName).sort((a, b) => compareCodeTags(b, a));
170
+ if (!newest) corpusFailure(`no published code release is listed on ${repo}; the approved runtime ${approvedTag} cannot be confirmed`);
171
+ const order = compareCodeTags(newest, approvedTag);
172
+ if (order > 0) {
173
+ throw new CorpusSuperseded(`code release ${newest} was published after this corpus was built at ${approvedTag}; `
174
+ + 'promoting it would put an older runtime over the live code release. The next night builds at the newer runtime.');
175
+ }
176
+ if (order < 0) corpusFailure(`approved runtime ${approvedTag} is newer than every published code release (newest ${newest})`);
177
+ const commit = ghJson(['api', `repos/${repo}/commits/${approvedTag}`], `the commit of ${approvedTag}`);
178
+ if (String(commit?.sha || '').toLowerCase() !== target) {
179
+ corpusFailure(`target ${target} is not the source of the approved runtime ${approvedTag} (${commit?.sha || 'unknown'})`);
180
+ }
181
+ }
182
+
183
+ /** What releases/latest is right now, or null when the repository has none. */
184
+ function readCurrentLatest(gh, repo) {
185
+ const latestView = gh(['release', 'view', '--json', 'tagName,body', '--repo', repo]);
186
+ if (!latestView.error && latestView.status === 0) {
187
+ let currentLatest;
188
+ try { currentLatest = JSON.parse(String(latestView.stdout || 'null')); }
189
+ catch (error) { corpusFailure(`cannot read the current latest release (${error.message})`); }
190
+ if (!currentLatest || typeof currentLatest.tagName !== 'string') corpusFailure('current latest release carries no tag name');
191
+ return currentLatest;
192
+ }
193
+ const latestError = String(latestView.error?.message || latestView.stderr || latestView.stdout || '');
194
+ if (!/(release not found|no release found)/i.test(latestError)) {
195
+ corpusFailure(`cannot determine the current latest release (${latestError.trim() || `gh exited ${latestView.status}`})`);
196
+ }
197
+ return null;
198
+ }
199
+
200
+ function latestTagNow(gh, repo) {
201
+ const latestNow = gh(['api', `repos/${repo}/releases/latest`]);
202
+ if (latestNow.error || latestNow.status !== 0) return null;
203
+ try { return JSON.parse(String(latestNow.stdout || 'null'))?.tag_name ?? null; } catch { return null; }
204
+ }
205
+
142
206
  export async function runProtectedCorpusSeed({
143
207
  argv = process.argv.slice(2),
144
208
  env = process.env,
@@ -155,7 +219,15 @@ export async function runProtectedCorpusSeed({
155
219
  // construction; this refuses the combined invocation outright so the two modes can never share one
156
220
  // process even if a future workflow edit put them in the same job.
157
221
  if (argv.includes('--publish')) corpusFailure('--corpus-seed cannot be combined with --publish; corpus routing must never enter product publication');
158
- const promoteLatest = argv.includes('--promote-latest');
222
+ // THE PRODUCER CANNOT DECLARE SUCCESS UNLESS THE CONSUMER ACCEPTED (customer canary). There is no
223
+ // longer any single invocation that both publishes a corpus and moves releases/latest: a customer
224
+ // candidate is STAGED (--stage-candidate: a signed, public, non-latest prerelease), a clean customer
225
+ // install applies it (scripts/corpus-canary.mjs), and only --promote-staged with that verdict moves
226
+ // latest. The old one-shot flag is refused outright so no stale workflow text can bypass the canary.
227
+ if (argv.includes('--promote-latest')) {
228
+ corpusFailure('--promote-latest was removed: stage with --stage-candidate, then promote with --promote-staged and the customer canary verdict');
229
+ }
230
+ const customerCandidate = argv.includes('--stage-candidate');
159
231
 
160
232
  const tag = cliArg(argv, '--corpus-tag');
161
233
  const bundleFile = cliArg(argv, '--corpus-bundle');
@@ -208,7 +280,7 @@ export async function runProtectedCorpusSeed({
208
280
  });
209
281
  if (ancestry.error || ancestry.status !== 0) corpusFailure(`target ${target} is not an ancestor of this run's GITHUB_SHA ${env.GITHUB_SHA}`);
210
282
  const approvedTag = cliArg(argv, '--approved-tag');
211
- if (promoteLatest) {
283
+ if (customerCandidate) {
212
284
  if (!CODE_TAG.test(String(approvedTag || ''))) corpusFailure('customer promotion requires --approved-tag vX.Y.Z (the approved runtime this corpus was built at)');
213
285
  if (receipt.archiveManifestReleaseTag !== approvedTag) {
214
286
  corpusFailure(`the archive ships runtime ${receipt.archiveManifestReleaseTag}, not the approved runtime ${approvedTag}`);
@@ -366,7 +438,7 @@ export async function runProtectedCorpusSeed({
366
438
  const signatureFile = `${bundleFile}.sig`;
367
439
  const digestFile = `${bundleFile}.sha256`;
368
440
  const generation = String(receipt.createdAt || '');
369
- if (promoteLatest) {
441
+ if (customerCandidate) {
370
442
  for (const [label, file] of [['detached signature', signatureFile], ['sha256 sidecar', digestFile]]) {
371
443
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
372
444
  corpusFailure(`customer corpus promotion requires a ${label} beside the archive (${path.basename(file)} missing) — the updater fails closed without it`);
@@ -380,40 +452,11 @@ export async function runProtectedCorpusSeed({
380
452
  if (!Number.isFinite(Date.parse(generation))) corpusFailure('corpus receipt createdAt is not a readable generation timestamp');
381
453
  }
382
454
 
383
- const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
384
- const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
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
- };
455
+ const { gh, ghJson } = corpusGh(env, run);
394
456
 
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
- }
457
+ // Its signed install aggregate was re-verified for --approved-tag by the workflow step that built the
458
+ // runtime pin moments ago; what can change after that is only WHICH release is newest.
459
+ if (customerCandidate) assertApprovedRuntimeIsNewest({ ghJson, repo, approvedTag, target });
417
460
 
418
461
  const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
419
462
  const view = gh(viewArgs);
@@ -423,7 +466,7 @@ export async function runProtectedCorpusSeed({
423
466
 
424
467
  const receiptSha256 = sha256File(receiptFile);
425
468
 
426
- if (!promoteLatest) {
469
+ if (!customerCandidate) {
427
470
  // BOOTSTRAP/RECOVERY seeds stay exactly as ADR-086's original contract left them: an immutable
428
471
  // prerelease that never touches releases/latest. Dual's C4 resolution (S1) narrows the change to
429
472
  // CUSTOMER releases — "Bootstrap-only releases may remain prereleases."
@@ -465,23 +508,16 @@ export async function runProtectedCorpusSeed({
465
508
  // (kb/forge-update.mjs:1275 fetches `${url}.sig`, :1284-1285 exits 4 when verification fails).
466
509
  // scripts/verify-channels.mjs checks exactly these two things (checks 3 and 4) against the live
467
510
  // endpoints, and is the owner's post-publish acceptance gate.
468
- const latestView = gh(['release', 'view', '--json', 'tagName,body', '--repo', repo]);
469
- let currentLatest = null;
470
- if (!latestView.error && latestView.status === 0) {
471
- try { currentLatest = JSON.parse(String(latestView.stdout || 'null')); }
472
- catch (error) { corpusFailure(`cannot read the current latest release (${error.message})`); }
473
- if (!currentLatest || typeof currentLatest.tagName !== 'string') corpusFailure('current latest release carries no tag name');
474
- } else {
475
- const latestError = String(latestView.error?.message || latestView.stderr || latestView.stdout || '');
476
- if (!/(release not found|no release found)/i.test(latestError)) {
477
- corpusFailure(`cannot determine the current latest release (${latestError.trim() || `gh exited ${latestView.status}`})`);
478
- }
479
- }
511
+ //
512
+ // STAGED, NOT PROMOTED (customer canary). This path ends with a signed, public PRERELEASE that
513
+ // releases/latest cannot resolve to. The ordering check still runs here so a candidate that could
514
+ // never be promoted is not staged at all; it runs again at promotion time.
515
+ const currentLatest = readCurrentLatest(gh, repo);
480
516
  const promotion = evaluateCorpusPromotion({ tag, generation, currentLatest });
481
517
  if (!promotion.allowed) corpusFailure(promotion.reason);
482
518
 
483
519
  const notes = [
484
- 'RuvNet Brain corpus generation — signed, content-addressed, and promoted to latest.',
520
+ 'RuvNet Brain corpus generation — signed and content-addressed. Staged as a prerelease; promoted to latest only after a clean customer install applied it.',
485
521
  `${CORPUS_GENERATION_FIELD} ${generation}`,
486
522
  `Archive SHA-256: ${archiveSha256}`,
487
523
  `Receipt SHA-256: ${receiptSha256}`,
@@ -495,8 +531,9 @@ export async function runProtectedCorpusSeed({
495
531
  // ASSETS COMPLETE BEFORE PROMOTION. `gh release create` uploads assets AFTER the release exists, so
496
532
  // creating a non-draft release directly opens a window in which releases/latest resolves to a
497
533
  // release with no archive — every polling client in that window fails or, worse, half-downloads.
498
- // Create as a draft (invisible to releases/latest), prove all four assets landed, and only then
499
- // flip draft off and claim latest in one edit.
534
+ // Create as a draft (invisible to releases/latest), prove every asset landed, and only then flip
535
+ // draft off AS A PRERELEASE — public, so an anonymous customer install can download it exactly as it
536
+ // downloads latest, and never latest, so no customer receives it until the canary has applied it.
500
537
  // Both reports ride with every corpus release for the same reason they ride with a seed: a customer
501
538
  // (or the next night's dispatcher) that downloads the archive must be able to reverify it against
502
539
  // the identity it was actually measured under — the blocking recall gate AND the C3 diagnostic it
@@ -524,43 +561,109 @@ export async function runProtectedCorpusSeed({
524
561
  catch (error) { corpusFailure(`cannot read the draft corpus release (${error.message})`); }
525
562
  const uploaded = (draft?.assets || []).filter((asset) => asset?.state === 'uploaded' && Number.isSafeInteger(asset.size) && asset.size > 0);
526
563
  if (draft?.isDraft !== true || JSON.stringify(uploaded.map((asset) => asset.name).sort()) !== JSON.stringify(expectedAssets)) {
527
- corpusFailure(`refusing to promote an incomplete corpus release; expected ${expectedAssets.join(', ')} fully uploaded on a draft`);
564
+ corpusFailure(`refusing to stage an incomplete corpus release; expected ${expectedAssets.join(', ')} fully uploaded on a draft`);
528
565
  }
529
566
 
530
- const promote = gh(['release', 'edit', tag, '--repo', repo, '--draft=false', '--latest', '--prerelease=false']);
531
- if (promote.error || promote.status !== 0) {
532
- corpusFailure(`corpus promotion to latest failed (${String(promote.error?.message || promote.stderr || promote.stdout || '').trim()})`);
567
+ const stage = gh(['release', 'edit', tag, '--repo', repo, '--draft=false', '--prerelease', '--latest=false']);
568
+ if (stage.error || stage.status !== 0) {
569
+ corpusFailure(`corpus staging failed (${String(stage.error?.message || stage.stderr || stage.stdout || '').trim()})`);
533
570
  }
534
571
 
535
572
  // `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.
573
+ // `gh release list`), so latest-ness is read from the one authoritative endpoint: releases/latest
574
+ // must NOT be this tag. tests/unit/gh-json-fields.test.mjs checks every --json field list.
575
+ const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isPrerelease,assets', '--repo', repo]);
576
+ if (finalView.error || finalView.status !== 0) corpusFailure('cannot confirm the staged corpus release');
577
+ let staged;
578
+ try { staged = JSON.parse(String(finalView.stdout || 'null')); }
579
+ catch (error) { corpusFailure(`cannot read the staged corpus release (${error.message})`); }
580
+ const stagedAssets = (staged?.assets || []).map((asset) => asset?.name).sort();
581
+ if (staged?.tagName !== tag || staged.isDraft !== false || staged.isPrerelease !== true
582
+ || latestTagNow(gh, repo) === tag || JSON.stringify(stagedAssets) !== JSON.stringify(expectedAssets)) {
583
+ corpusFailure('corpus release did not reach a complete, public, non-latest prerelease state');
584
+ }
585
+
586
+ return {
587
+ tag, target, repository: repo, archiveSha256, receiptSha256, staged: true, promoted: false,
588
+ generation, currentLatest: currentLatest?.tagName || null,
589
+ };
590
+ }
591
+
592
+ /**
593
+ * PROMOTION — the only code path that moves releases/latest to a corpus generation, and it requires
594
+ * the consumer's consent: a PASS verdict from scripts/corpus-canary.mjs for THIS run, over exactly the
595
+ * asset digests still on the staged prerelease. Everything that could have changed since staging is
596
+ * re-proved: the protected environment, the checkout, the approved runtime still being the newest code
597
+ * release (else CorpusSuperseded), the release still being the staged prerelease, and the ordering
598
+ * against whatever is latest now.
599
+ */
600
+ export async function runProtectedCorpusPromotion({
601
+ argv = process.argv.slice(2),
602
+ env = process.env,
603
+ root = ROOT,
604
+ run = (command, args, options) => spawnSync(command, args, { encoding: 'utf8', ...options }),
605
+ } = {}) {
606
+ const environmentFailures = validateProtectedPublishEnvironment(env);
607
+ if (environmentFailures.length) corpusFailure(environmentFailures.join('; '));
608
+ if (argv.includes('--publish')) corpusFailure('--corpus-seed cannot be combined with --publish; corpus routing must never enter product publication');
609
+ const tag = cliArg(argv, '--corpus-tag');
610
+ const verdictFile = cliArg(argv, '--canary-verdict');
611
+ const target = cliArg(argv, '--target');
612
+ const approvedTag = cliArg(argv, '--approved-tag');
613
+ const repo = cliArg(argv, '--repo') || env.GITHUB_REPOSITORY;
614
+ if (!CORPUS_TAG_PATTERN.test(String(tag || ''))) corpusFailure('corpus tag must be corpus-sha256- followed by 64 lowercase hex characters');
615
+ if (repo !== env.GITHUB_REPOSITORY || repo !== 'stuinfla/ruvnet-brain') corpusFailure('repository does not match the protected workflow');
616
+ if (!CODE_TAG.test(String(approvedTag || ''))) corpusFailure('promotion requires --approved-tag vX.Y.Z (the approved runtime this corpus was built at)');
617
+ const head = String(run('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).stdout || '').trim();
618
+ if (!isHex(target, 40) || target !== head || !isHex(env.GITHUB_SHA, 40)) corpusFailure('target must exactly equal HEAD (and GITHUB_SHA must be a commit)');
619
+ const ancestry = run('git', ['merge-base', '--is-ancestor', target, env.GITHUB_SHA], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
620
+ if (ancestry.error || ancestry.status !== 0) corpusFailure(`target ${target} is not an ancestor of this run's GITHUB_SHA ${env.GITHUB_SHA}`);
621
+ let verdict;
622
+ try {
623
+ if (!verdictFile || !path.isAbsolute(verdictFile)) throw new Error('--canary-verdict must be an absolute file');
624
+ verdict = JSON.parse(fs.readFileSync(verdictFile, 'utf8'));
625
+ } catch (error) {
626
+ corpusFailure(`no customer consent: the canary verdict is unreadable (${error.message})`);
627
+ }
628
+ // Local refusal first: a FAIL verdict never reaches the network at all.
629
+ if (verdict?.verdict !== 'PASS') corpusFailure(`no customer consent: the customer canary reported ${verdict?.verdict || '(no verdict)'}`);
630
+
631
+ const { gh, ghJson } = corpusGh(env, run);
632
+ assertApprovedRuntimeIsNewest({ ghJson, repo, approvedTag, target });
633
+ const release = ghJson(['release', 'view', tag, '--json', 'tagName,isDraft,isPrerelease,assets,body', '--repo', repo], `the staged release ${tag}`);
634
+ if (release?.tagName !== tag || release.isDraft !== false || release.isPrerelease !== true) {
635
+ corpusFailure(`${tag} is not a staged (public, non-draft) prerelease; refusing to promote it`);
636
+ }
637
+ const consent = evaluateCanaryVerdict({ verdict, tag, runId: env.GITHUB_RUN_ID, runAttempt: env.GITHUB_RUN_ATTEMPT,
638
+ approvedTag, releaseAssets: (release.assets || []).map((asset) => ({ name: asset?.name, digest: asset?.digest ?? null })) });
639
+ if (!consent.allowed) corpusFailure(consent.reason);
640
+ const generation = parseCorpusGeneration(release.body)?.value;
641
+ if (!generation) corpusFailure(`${tag} carries no readable "${CORPUS_GENERATION_FIELD}" ordering key`);
642
+ const currentLatest = readCurrentLatest(gh, repo);
643
+ const promotion = evaluateCorpusPromotion({ tag, generation, currentLatest });
644
+ if (!promotion.allowed) corpusFailure(promotion.reason);
645
+
646
+ const promote = gh(['release', 'edit', tag, '--repo', repo, '--prerelease=false', '--latest']);
647
+ if (promote.error || promote.status !== 0) {
648
+ corpusFailure(`corpus promotion to latest failed (${String(promote.error?.message || promote.stderr || promote.stdout || '').trim()})`);
649
+ }
539
650
  const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isPrerelease,assets', '--repo', repo]);
540
651
  if (finalView.error || finalView.status !== 0) corpusFailure('cannot confirm the promoted corpus release');
541
652
  let promoted;
542
653
  try { promoted = JSON.parse(String(finalView.stdout || 'null')); }
543
654
  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
- }
549
- const promotedAssets = (promoted?.assets || []).map((asset) => asset?.name).sort();
550
- if (promoted?.tagName !== tag || promoted.isDraft !== false || latestTag !== tag
551
- || promoted.isPrerelease !== false || JSON.stringify(promotedAssets) !== JSON.stringify(expectedAssets)) {
655
+ const names = (assets) => (assets || []).map((asset) => asset?.name).sort();
656
+ if (promoted?.tagName !== tag || promoted.isDraft !== false || promoted.isPrerelease !== false
657
+ || latestTagNow(gh, repo) !== tag || JSON.stringify(names(promoted.assets)) !== JSON.stringify(names(release.assets))) {
552
658
  corpusFailure('corpus release did not reach a complete, non-draft, non-prerelease latest state');
553
659
  }
554
-
555
- return {
556
- tag, target, repository: repo, archiveSha256, receiptSha256, promoted: true,
557
- generation, supersededLatest: currentLatest?.tagName || null,
558
- };
660
+ return { tag, target, repository: repo, promoted: true, generation, consent: consent.reason,
661
+ supersededLatest: currentLatest?.tagName || null };
559
662
  }
560
663
 
561
664
  if (CORPUS_SEED) {
562
665
  try {
563
- const result = await runProtectedCorpusSeed();
666
+ const result = process.argv.includes('--promote-staged') ? await runProtectedCorpusPromotion() : await runProtectedCorpusSeed();
564
667
  console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
565
668
  } catch (error) {
566
669
  console.error(error.message);
@@ -7,9 +7,10 @@ import { spawnSync } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { canonicalJson, digest, eligibleRepositoryStanding, validateCoverageLedger } from './coverage-integrity.mjs';
9
9
  import { fixtureDenominator } from './fixture-denominator.mjs';
10
+ import { passageMatches } from './retrieval-passage-identity.mjs';
10
11
 
11
12
  // Both release phases resolve against an explicit installed context, never the checkout.
12
- export async function resolveInstalledCanaryCitation({ kbDir, matched, expected, passageFileDigests = new Map() }) {
13
+ export async function resolveInstalledCanaryCitation({ kbDir, matched, expected, passageFileDigests = new Map(), contentMap }) {
13
14
  if (!path.isAbsolute(kbDir || '')) throw new Error('installed canary KB path must be absolute');
14
15
  if (String(matched?.repo || '').toLowerCase() !== expected.repo || matched?.path !== expected.path) return { resolved: false };
15
16
  if (!/^[a-z0-9][a-z0-9._-]*$/i.test(expected.repo)) throw new Error('installed citation repository violates containment');
@@ -35,7 +36,7 @@ export async function resolveInstalledCanaryCitation({ kbDir, matched, expected,
35
36
  let record;
36
37
  try { record = JSON.parse(line); } catch { continue; }
37
38
  if (record?.path !== expected.path) continue;
38
- if (digest(record) !== expected.passageSha256) continue;
39
+ if (!passageMatches(record, expected.passageSha256, contentMap)) continue;
39
40
  const text = record.fullText || record.text;
40
41
  if (typeof text !== 'string' || !text || typeof matched.text !== 'string' || !matched.text.includes(text)) continue;
41
42
  passageSha256 = expected.passageSha256;
@@ -386,7 +387,8 @@ export function validatePlanAgainstCoverage(plan, coverage, { allowObservedBasel
386
387
  return plan;
387
388
  }
388
389
  export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, coverageIdentity = null, queryEvidence, assetsDir = '.',
389
- readPassages = defaultReadPassages, legacySampleSize, allowNoDelta = false } = {}) {
390
+ readPassages = defaultReadPassages, legacySampleSize, allowNoDelta = false, contentMap, knownHitStores = null,
391
+ notice = (message) => process.stderr.write(`${message}\n`) } = {}) {
390
392
  const checked = validateCoverageLedger(coverage);
391
393
  if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
392
394
  const coverageGeneration = coverage.kind === 'ruvnet-brain-release-coverage'
@@ -455,12 +457,47 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
455
457
  const passages = new Map(eligible.map((row) => [storeOf(row), readPassages(assetsDir, storeOf(row))]));
456
458
  const rankedLegacy = legacyPool.map((row) => ({ row, count: passages.get(storeOf(row)).length }))
457
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);
458
481
  const strata = new Map();
482
+ const staleFixtureStores = [];
483
+ const generationMissStores = [];
459
484
  rankedLegacy.forEach((entry, index) => {
460
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; }
461
489
  if (!strata.has(stratum)) strata.set(stratum, []);
462
490
  strata.get(stratum).push(entry);
463
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
+ }
464
501
  // Source-only releases retain the same corpus sample; the plan still seals exact release bytes.
465
502
  const samplingGeneration = coverage.kind === 'ruvnet-brain-release-coverage'
466
503
  ? coverage.corpusCoverage.coverageGeneration : coverageGeneration;
@@ -487,7 +524,7 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
487
524
  const observedPassageCount = passageCount ?? passages.get(store).length;
488
525
  const evidence = queryEvidence.queries[store];
489
526
  if (!evidence || expectedSources(evidence.expected).some((source) =>
490
- 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)) {
491
528
  throw new Error(`${store} has no sealed independent query evidence`);
492
529
  }
493
530
  return {
@@ -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
+ }
Binary file
@@ -77,6 +77,8 @@ const STANDALONE = [
77
77
  ['gate', 'retired automatic-hook helper and manual benchmark retained for explicit human use; no workflow or scheduler invokes this expensive command'],
78
78
  ['dream-issue-gate', 'pure Dream Cycle disposition policy; invoked by the external issue adapter, never a GitHub writer'],
79
79
  ['sync-census', 'explicit maintainer census writer; a destructive source-to-surface refresh is never scheduled'],
80
+ ['grounding-turn-replay', 'human-run measurement harness for grounding-turn-gate.mjs (ADR-0030 #1 and shadow #2/#3): replays real transcripts read-only through the same pure functions the hooks call, sharing completion-claim-replay.mjs\'s turn walkers; nothing to schedule'],
81
+ ['duplicate-gate-replay','human-run tuning harness for plugin/scripts/duplicate-gate.mjs: replays the gate over git history, read-only, when its thresholds are re-tuned; it imports the gate\'s own scoring functions, so there is no second copy to drift and nothing to schedule'],
80
82
  ['sync-commands', 'explicit maintainer alias synchronizer; run deliberately before release, never from a lifecycle hook'],
81
83
  ['project-progression-checkpoint', 'the body of the shipped `/ruvnet-brain:checkpoint` command '
82
84
  + '(plugin/commands/checkpoint.md:39; added a7167b6b 2026-09-11). The command host executes that '
@@ -120,6 +122,10 @@ const STANDALONE = [
120
122
  ['self-update', 'author-run candidate rebuild; --apply is guarded by worktree-integrity.mjs and is not scheduled'],
121
123
  ['ingest-new-repos', 'author-run corpus expansion; --apply is guarded by worktree-integrity.mjs and is not scheduled'],
122
124
  ['count-chunks', 'human-run CLI — recount + restamp chunk surfaces (--check for drift); no scheduler'],
125
+ ['derive-passage-content-map', 'human-run maintainer tool — regenerates data/retrieval-passage-content-digests.json '
126
+ + 'from a corpus built with ordinal passage ids, only when the frozen fixture changes; '
127
+ + 'tests/unit/retrieval-passage-identity.test.mjs fails if the committed map stops matching the fixture, '
128
+ + 'so a stale map cannot go unnoticed and there is nothing to schedule'],
123
129
  ['brain-stamp', 'invoked by the author-run self-update.mjs candidate builder'],
124
130
  ['lesson-promote', 'human-run CLI — promotion is manual (--apply); no scheduler yet (automation is ADR-029 #4, open)'],
125
131
  ['behavioral-l1-l4', 'behavioural harness invoked by its own test file — not a product path'],