ruvnet-brain 4.3.20 → 4.3.25

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 (143) hide show
  1. package/README.md +5 -5
  2. package/bin/install.mjs +383 -78
  3. package/console/app.js +141 -9
  4. package/console/index.html +51 -24
  5. package/console/scope.css +137 -0
  6. package/console/scope.html +144 -0
  7. package/console/scope.js +209 -0
  8. package/console/tips.html +1 -0
  9. package/kb/corpus-release-identity.mjs +239 -0
  10. package/kb/update-storage-transaction.mjs +20 -3
  11. package/package.json +9 -2
  12. package/plugin/.claude-plugin/plugin.json +2 -2
  13. package/plugin/.codex-plugin/plugin.json +1 -1
  14. package/plugin/commands/checkpoint.md +61 -0
  15. package/plugin/hooks/codex-hooks.json +64 -1
  16. package/plugin/hooks/hook-contracts.json +299 -6
  17. package/plugin/hooks/hooks.json +81 -1
  18. package/plugin/mcp/server.mjs +23 -0
  19. package/plugin/scripts/advocacy-catalog.mjs +245 -0
  20. package/plugin/scripts/advocacy-route.mjs +460 -0
  21. package/plugin/scripts/continuation-gate.mjs +25 -2
  22. package/plugin/scripts/continuation-objective.mjs +7 -1
  23. package/plugin/scripts/continuity-hook-policy.mjs +190 -15
  24. package/plugin/scripts/coverage-integrity.mjs +7 -0
  25. package/plugin/scripts/gates.mjs +113 -10
  26. package/plugin/scripts/grounding-turn-gate.mjs +167 -0
  27. package/plugin/scripts/grounding-turn-mark.mjs +91 -0
  28. package/plugin/scripts/hook-shim.mjs +14 -0
  29. package/plugin/scripts/host-shell-boundary.mjs +43 -0
  30. package/plugin/scripts/nightly-scheduler.mjs +37 -4
  31. package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
  32. package/plugin/scripts/project-progression-contract.mjs +16 -0
  33. package/plugin/scripts/project-progression-hook.mjs +3 -0
  34. package/plugin/scripts/project-progression-producer.mjs +252 -0
  35. package/plugin/scripts/project-progression-reader.mjs +271 -0
  36. package/plugin/scripts/project-progression-session-start.mjs +93 -16
  37. package/plugin/scripts/project-progression-sources.mjs +220 -0
  38. package/plugin/scripts/project-progression-store.mjs +106 -13
  39. package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
  40. package/plugin/scripts/session-snapshot-hook.mjs +115 -7
  41. package/plugin/scripts/session-start-budget.mjs +59 -0
  42. package/plugin/scripts/session-start-core.mjs +234 -457
  43. package/plugin/scripts/session-start-fsutil.mjs +61 -0
  44. package/plugin/scripts/session-start-health.mjs +64 -0
  45. package/plugin/scripts/session-start-hook-description.mjs +45 -0
  46. package/plugin/scripts/session-start-issue-alert.mjs +77 -0
  47. package/plugin/scripts/session-start-repo-identity.mjs +54 -0
  48. package/plugin/scripts/session-start-signals.mjs +73 -0
  49. package/plugin/scripts/session-start-trace.mjs +86 -0
  50. package/plugin/scripts/session-start-update-plane.mjs +104 -0
  51. package/plugin/scripts/unprompted-runtime.mjs +32 -2
  52. package/plugin/scripts/update-apply.mjs +2 -32
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
  54. package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
  55. package/scripts/adr-072-completion.mjs +1 -1
  56. package/scripts/agentdb-fleet-doctor.mjs +5 -1
  57. package/scripts/approved-runtime.mjs +197 -0
  58. package/scripts/brain-novice-50.mjs +16 -1
  59. package/scripts/brain-score.mjs +23 -5
  60. package/scripts/build-bundle.mjs +971 -530
  61. package/scripts/build-concepts.mjs +36 -116
  62. package/scripts/console-engine.test.mjs +8 -7
  63. package/scripts/console-runtime-identity.mjs +4 -0
  64. package/scripts/corpus-aggregates.mjs +94 -77
  65. package/scripts/corpus-candidate.mjs +475 -222
  66. package/scripts/corpus-next-seed.mjs +225 -0
  67. package/scripts/corpus-promotion.mjs +58 -0
  68. package/scripts/corpus-reconcile.mjs +411 -105
  69. package/scripts/doc-currency.mjs +16 -1
  70. package/scripts/dual-host-deliberation.mjs +25 -2
  71. package/scripts/dual-host-suggest.mjs +17 -1
  72. package/scripts/falsify.mjs +13 -3
  73. package/scripts/gist-receipts.mjs +482 -87
  74. package/scripts/github-health-watch.mjs +12 -2
  75. package/scripts/handoff-asset.mjs +34 -0
  76. package/scripts/hook-retirement-check.mjs +8 -1
  77. package/scripts/host-registry.mjs +1 -1
  78. package/scripts/ingest-gists.mjs +74 -101
  79. package/scripts/job-heartbeat.sh +77 -14
  80. package/scripts/learning-replay-execution.mjs +10 -4
  81. package/scripts/nightly-gists.sh +27 -13
  82. package/scripts/nightly-two-run-proof.mjs +1 -1
  83. package/scripts/nightly-watchdog.mjs +61 -4
  84. package/scripts/onboarding-console.mjs +319 -27
  85. package/scripts/oracle/produce-questions.mjs +293 -0
  86. package/scripts/oracle/producer-hosts.mjs +235 -0
  87. package/scripts/oracle/repo-recall.mjs +448 -0
  88. package/scripts/oracle/retrieval-accuracy.mjs +818 -0
  89. package/scripts/oracle/source-tree.mjs +165 -0
  90. package/scripts/oracle/source-units.mjs +391 -0
  91. package/scripts/oracle/spike-run.mjs +98 -0
  92. package/scripts/oracle/unit-inventory.mjs +141 -0
  93. package/scripts/oracle/unit-sampling.mjs +128 -0
  94. package/scripts/oracle/validate-labels.mjs +250 -0
  95. package/scripts/private-overlay.mjs +248 -0
  96. package/scripts/product-integrity-contract.mjs +1 -1
  97. package/scripts/proxy/claude-proxied.sh +6 -0
  98. package/scripts/proxy/proxy-revert.sh +5 -0
  99. package/scripts/proxy/proxy-up.sh +6 -0
  100. package/scripts/proxy/proxy-verify.mjs +4 -0
  101. package/scripts/public-inputs.mjs +409 -0
  102. package/scripts/public-verification-inputs.mjs +112 -26
  103. package/scripts/public-verification-lane.mjs +1 -1
  104. package/scripts/published-surface-probe.mjs +34 -4
  105. package/scripts/qe/card-lane-gate.mjs +16 -1
  106. package/scripts/qe/session-start-gate.mjs +16 -1
  107. package/scripts/rebuild-gists-from-receipts.mjs +58 -78
  108. package/scripts/record-lesson.mjs +4 -1
  109. package/scripts/rehearse-corpus-pipeline.mjs +994 -0
  110. package/scripts/release-abort-stale.mjs +5 -1
  111. package/scripts/release-authority.mjs +104 -12
  112. package/scripts/release-channel-kind.mjs +86 -0
  113. package/scripts/release-convergence-watchdog.mjs +7 -2
  114. package/scripts/release-projection.mjs +177 -72
  115. package/scripts/release-transaction-provider.mjs +23 -6
  116. package/scripts/release.mjs +252 -17
  117. package/scripts/retrieval-canary.mjs +87 -0
  118. package/scripts/rvf-index-audit.mjs +573 -13
  119. package/scripts/rvf-wire.mjs +269 -0
  120. package/scripts/seal-gist-receipt.mjs +65 -0
  121. package/scripts/selfcheck.mjs +42 -21
  122. package/scripts/source-coverage.mjs +253 -24
  123. package/scripts/status-honesty.mjs +25 -0
  124. package/scripts/sync-census.mjs +0 -0
  125. package/scripts/sync-version.mjs +2 -0
  126. package/scripts/trismart.mjs +42 -0
  127. package/scripts/updater-manifest.mjs +162 -0
  128. package/scripts/verify-channels.mjs +17 -5
  129. package/scripts/wired-check.mjs +48 -10
  130. package/tri-smart-skill/QUICKSTART.md +37 -0
  131. package/tri-smart-skill/README.md +92 -0
  132. package/tri-smart-skill/install.cmd +14 -0
  133. package/tri-smart-skill/install.command +13 -0
  134. package/tri-smart-skill/install.mjs +51 -0
  135. package/tri-smart-skill/install.sh +9 -0
  136. package/tri-smart-skill/tri-smart/SKILL.md +90 -0
  137. package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
  138. package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
  139. package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
  140. package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
  141. package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
  142. package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
  143. package/scripts/corpus-seed-publish.mjs +0 -110
@@ -4,6 +4,7 @@ import fs from 'node:fs';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { canonicalJson } from './coverage-integrity.mjs';
7
+ import { isCorpusReleaseTag, latestCodeReleaseTag } from './release-channel-kind.mjs';
7
8
  import {
8
9
  pollObservation, RECEIPT_PREFIX, receiptDisposition, transactionIdFor,
9
10
  } from './release-transaction.mjs';
@@ -16,6 +17,10 @@ const command = (name, args, options = {}) => execFileSync(name, args, {
16
17
  encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000, ...options,
17
18
  }).trim();
18
19
  const json = (name, args, options) => JSON.parse(command(name, args, options));
20
+ // ADR-086 S1: `releases/latest` is the customer download pointer and is a corpus generation on any
21
+ // night a corpus round shipped. Every question this provider asks is about the CODE generation, so
22
+ // it must resolve the latest CODE release rather than read a pointer that now answers differently.
23
+ const latestCodeTag = () => latestCodeReleaseTag(json('gh', ['api', `repos/${REPO}/releases?per_page=30`]));
19
24
  const maybe = (callback, fallback = null) => {
20
25
  try { return callback(); } catch { return fallback; }
21
26
  };
@@ -216,7 +221,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
216
221
  }
217
222
  }
218
223
  const npmLatest = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
219
- const githubLatest = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
224
+ const githubLatest = latestCodeTag();
220
225
  const legacySettled = [];
221
226
  const pending = [];
222
227
  for (const { receipt, release } of latestByTransaction.values()) {
@@ -257,7 +262,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
257
262
  async observeSnapshot(identity, draft = activeDraft, { forceAssets = false } = {}) {
258
263
  try {
259
264
  const release = draft?.id ? hydratedRelease(releaseById(draft.id)) : null;
260
- const latestTag = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
265
+ const latestTag = latestCodeTag();
261
266
  const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
262
267
  const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
263
268
  const latestVersion = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
@@ -409,7 +414,10 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
409
414
 
410
415
  async observeGithub(identity) {
411
416
  const release = json('gh', ['api', `repos/${REPO}/releases/tags/${identity.tag}`]);
412
- const latest = maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
417
+ // "Is this release the current CODE generation", not "does it hold the latest pointer". Once a
418
+ // corpus generation takes the pointer — which is the designed steady state — a genuinely
419
+ // published code release would otherwise start observing itself as no longer latest.
420
+ const latest = maybe(() => latestCodeTag(), null);
413
421
  return { tag: release.tag_name, sha: tagSha(identity.tag, root), latest: latest === identity.tag };
414
422
  },
415
423
 
@@ -424,14 +432,23 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
424
432
  return { version: command('npm', ['view', `${PACKAGE}@latest`, 'version']) };
425
433
  },
426
434
  async makeGithubLatest(draft, identity, expectedPrior) {
427
- const current = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
435
+ // The prior-generation check is about the CODE lineage: it exists to catch "someone promoted a
436
+ // different product generation while this transaction was in flight". A corpus generation
437
+ // legitimately holding `releases/latest` is NOT that — it is the designed steady state between
438
+ // code releases — so the comparison is made against the latest CODE release, and the pointer's
439
+ // own kind is recorded rather than treated as a conflict.
440
+ const pointer = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
441
+ const current = latestCodeTag();
428
442
  if (current !== expectedPrior && current !== identity.tag) {
429
- throw new Error(`refusing GitHub promotion: latest is ${current}, expected ${expectedPrior}`);
443
+ throw new Error(`refusing GitHub promotion: latest code release is ${current}, expected ${expectedPrior}`);
444
+ }
445
+ if (isCorpusReleaseTag(pointer)) {
446
+ process.stdout.write(`[release] promoting over corpus generation ${pointer} — the code bundle carries its own corpus\n`);
430
447
  }
431
448
  command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-f', 'make_latest=true']);
432
449
  },
433
450
  async observeGithubLatest() {
434
- return { tag: json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name };
451
+ return { tag: latestCodeTag() };
435
452
  },
436
453
  async restoreNpmLatest(prior, expected) {
437
454
  const current = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
@@ -35,6 +35,29 @@ import { materializePublicationHandoff, resolvePublicationHandoffPaths } from '.
35
35
  import { liveReleaseProvider } from './release-transaction-provider.mjs';
36
36
  import { stagedHostVerifier } from './staged-host-verifier.mjs';
37
37
  import { verifyPayload } from './release-payload.mjs';
38
+ import { verifyCorpusReceipt } from './corpus-candidate.mjs';
39
+ import { readDiagnosticAccuracyReport } from './oracle/retrieval-accuracy.mjs';
40
+ import { loadFixture, readRecallReport } from './oracle/repo-recall.mjs';
41
+
42
+ /**
43
+ * The measured retrieval numbers, stated in the release notes themselves rather than left behind a
44
+ * digest. Both halves go in together on purpose: the number that qualified the release, and the one
45
+ * it did NOT meet. A reader who sees only the first would reasonably assume the second was fine.
46
+ */
47
+ const recallNotes = (receipt) => {
48
+ const r = receipt.recallSummary;
49
+ if (!r) return [];
50
+ return [
51
+ `Retrieval (blocking): ${r.repositoriesAnswering}/${r.questions} repositories answer a real question`
52
+ + ` about themselves from their own content; ${r.exactFileTop5}/${r.questions} return the exact`
53
+ + ` labeled file in the top 5 (floor ${r.floor}).`,
54
+ 'NOT measured: generated-answer correctness, citation support, or unscoped whole-corpus discovery.',
55
+ `ADR-086 C3 was NOT met and is NOT claimed — its measurement ships as ${receipt.accuracyReport.file}`
56
+ + ' for inspection.',
57
+ ];
58
+ };
59
+ import { verifyBundle } from './verify-bundle.mjs';
60
+ import { CORPUS_GENERATION_FIELD, evaluateCorpusPromotion } from './corpus-promotion.mjs';
38
61
 
39
62
  const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
40
63
  const PUBLISH = process.argv.includes('--publish');
@@ -83,7 +106,7 @@ function corpusFailure(message) {
83
106
  throw new Error(`[corpus-seed] ${message}`);
84
107
  }
85
108
 
86
- export function runProtectedCorpusSeed({
109
+ export async function runProtectedCorpusSeed({
87
110
  argv = process.argv.slice(2),
88
111
  env = process.env,
89
112
  root = ROOT,
@@ -94,6 +117,13 @@ export function runProtectedCorpusSeed({
94
117
  corpusFailure(environmentFailures.join('; '));
95
118
  }
96
119
 
120
+ // The corpus route may never enter product publication. This is belt to the workflow's braces: the
121
+ // corpus job binds an environment that holds no NPM_TOKEN at all, so npm is unreachable from it by
122
+ // construction; this refuses the combined invocation outright so the two modes can never share one
123
+ // process even if a future workflow edit put them in the same job.
124
+ if (argv.includes('--publish')) corpusFailure('--corpus-seed cannot be combined with --publish; corpus routing must never enter product publication');
125
+ const promoteLatest = argv.includes('--promote-latest');
126
+
97
127
  const tag = cliArg(argv, '--corpus-tag');
98
128
  const bundleFile = cliArg(argv, '--corpus-bundle');
99
129
  const receiptFile = cliArg(argv, '--corpus-receipt');
@@ -130,11 +160,20 @@ export function runProtectedCorpusSeed({
130
160
  corpusFailure('target must exactly equal HEAD, GITHUB_SHA, and the corpus receipt builderSourceSha');
131
161
  }
132
162
 
133
- const boundaryIdentities = [receipt.privateFence, receipt.eligibilityPolicy, receipt.generationLedger];
163
+ // Schema 3 (ADR-086 Step 15 / A6): the receipt binds the full provenance closure shipped INSIDE
164
+ // the sealed archive (ARCHIVE-MANIFEST.json, PRIVATE-STORES.json, RVF-GENERATIONS.json,
165
+ // SOURCE.json) AND the detached, digest-bound retrieval-accuracy report that measured this exact
166
+ // archive. Schema 2 is refused outright: a schema-2 seed carries no accuracy binding, so it is
167
+ // UNPUBLISHABLE from here forward and data/corpus-seed.json must be re-pointed at a schema-3 seed
168
+ // in the same change that publishes one.
169
+ const boundaryIdentities = [receipt.privateFence, receipt.generationLedger, receipt.sourceManifest, receipt.archiveManifest];
134
170
  const storeBindingsValid = Number.isSafeInteger(receipt.storeCount) && receipt.storeCount > 0
135
171
  && Array.isArray(receipt.stores) && receipt.stores.length === receipt.storeCount
136
172
  && receipt.stores.every((store) => typeof store?.name === 'string' && store.name.length > 0
137
- && /^[a-f0-9]{7,64}$/.test(String(store.sourceCommit || ''))
173
+ && ['repository', 'gist-aggregate', 'derived'].includes(store.kind)
174
+ && (store.kind === 'repository'
175
+ ? /^[a-f0-9]{7,64}$/.test(String(store.sourceCommit || ''))
176
+ : store.sourceCommit === null || /^[a-f0-9]{7,64}$/i.test(String(store.sourceCommit || '')))
138
177
  && typeof store.builtUtc === 'string' && Number.isFinite(Date.parse(store.builtUtc))
139
178
  && typeof store.model === 'string' && store.model.length > 0
140
179
  && Number.isSafeInteger(store.dimensions) && store.dimensions > 0
@@ -146,9 +185,10 @@ export function runProtectedCorpusSeed({
146
185
  const generatorFile = path.join(root, 'scripts/corpus-candidate.mjs');
147
186
  const generatorValid = fs.existsSync(generatorFile)
148
187
  && receipt.generator?.corpusCandidateSha256 === sha256File(generatorFile);
149
- if (receipt.schemaVersion !== 1 || receipt.kind !== 'ruvnet-brain-corpus-candidate'
150
- || !receipt.createdAt || !receipt.coverageGeneration || !storeBindingsValid || !emptyFailureArrays
188
+ if (receipt.schemaVersion !== 3 || receipt.kind !== 'ruvnet-brain-corpus-candidate'
189
+ || !receipt.createdAt || !storeBindingsValid || !emptyFailureArrays
151
190
  || !privateExclusionsValid || !boundaryIdentities.every(exactFileIdentity) || !exactFileIdentity(receipt.archive)
191
+ || !exactFileIdentity(receipt.accuracyReport)
152
192
  || !generatorValid
153
193
  || receipt.archive.file !== path.basename(bundleFile)) {
154
194
  corpusFailure('corpus receipt bindings are incomplete or invalid');
@@ -160,6 +200,98 @@ export function runProtectedCorpusSeed({
160
200
  }
161
201
  if (digestMatch[1] !== archiveSha256) corpusFailure('corpus tag digest does not match the receipt and archive');
162
202
 
203
+ // ADR-086 Step 15's second binding. The detached accuracy report travels beside the archive; this
204
+ // proves (a) the file the receipt names is the file present here, byte for byte, (b) the report
205
+ // was measured against THESE archive bytes, (c) every partition in both query modes passed
206
+ // 20x>=19x with no timeouts and no bounded sampling, and (d) it was produced by the committed
207
+ // benchmark against the committed oracle — so a swapped oracle or a patched benchmark is caught
208
+ // here even though the receipt itself carries only {file, sha256, bytes}.
209
+ const accuracyReportFile = `${bundleFile}.accuracy.json`;
210
+ if (receipt.accuracyReport.file !== path.basename(accuracyReportFile)) {
211
+ corpusFailure('corpus receipt names an accuracy report that is not the one beside this archive');
212
+ }
213
+ if (!fs.existsSync(accuracyReportFile) || !fs.statSync(accuracyReportFile).isFile()) {
214
+ corpusFailure(`detached retrieval-accuracy report missing beside the archive (${path.basename(accuracyReportFile)})`);
215
+ }
216
+ if (sha256File(accuracyReportFile) !== receipt.accuracyReport.sha256
217
+ || fs.statSync(accuracyReportFile).size !== receipt.accuracyReport.bytes) {
218
+ corpusFailure('detached retrieval-accuracy report bytes do not match the corpus receipt');
219
+ }
220
+ const committedOracleFile = path.join(root, 'data/retrieval-accuracy-oracle.json');
221
+ const accuracyGeneratorFile = path.join(root, 'scripts/oracle/retrieval-accuracy.mjs');
222
+ if (!fs.existsSync(committedOracleFile)) corpusFailure('committed retrieval-accuracy oracle is missing from the release checkout');
223
+ if (!fs.existsSync(accuracyGeneratorFile)) corpusFailure('committed retrieval-accuracy benchmark is missing from the release checkout');
224
+ // The BLOCKING retrieval predicate at publication is the frozen-fixture repo-recall gate, read
225
+ // through the same module candidate acceptance used so the two can never drift apart. ADR-086's
226
+ // C3 report still has to exist and still has to be bound to these exact archive bytes — an
227
+ // unbound diagnostic looks like evidence and is worse than none — but its score no longer refuses
228
+ // publication. That reduction is declared in docs/adr/0086 and in the published report itself.
229
+ const archiveIdentity = { file: receipt.archive.file, sha256: archiveSha256, bytes: fs.statSync(bundleFile).size };
230
+ try {
231
+ readDiagnosticAccuracyReport({
232
+ reportFile: accuracyReportFile,
233
+ archive: archiveIdentity,
234
+ expectedOracleSha256: sha256File(committedOracleFile),
235
+ expectedGeneratorSha256: sha256File(accuracyGeneratorFile),
236
+ });
237
+ } catch (error) {
238
+ corpusFailure(`the published C3 diagnostic is not bound to this archive (${error.message})`);
239
+ }
240
+ const recallReportFile = `${bundleFile}.recall.json`;
241
+ if (!fs.existsSync(recallReportFile)) {
242
+ corpusFailure(`detached repo-recall report missing beside the archive (${path.basename(recallReportFile)})`);
243
+ }
244
+ if (!receipt.recallReport
245
+ || sha256File(recallReportFile) !== receipt.recallReport.sha256
246
+ || fs.statSync(recallReportFile).size !== receipt.recallReport.bytes) {
247
+ corpusFailure('detached repo-recall report bytes do not match the corpus receipt');
248
+ }
249
+ try {
250
+ readRecallReport({
251
+ reportFile: recallReportFile,
252
+ archive: archiveIdentity,
253
+ expectedFixtureSha256: loadFixture().fixtureSha256,
254
+ });
255
+ } catch (error) {
256
+ corpusFailure(`retrieval does not qualify this corpus for publication (${error.message})`);
257
+ }
258
+
259
+ // Deep re-verification — moved here 2026-09-13 from the deleted scripts/corpus-seed-publish.mjs
260
+ // (ADR-085). Everything above proves the receipt is well-FORMED and that the archive's outer
261
+ // digest matches it; none of it proves the receipt is TRUE. verifyCorpusReceipt re-extracts the
262
+ // sealed archive and re-derives the entire candidate from its own bytes — per-store file digests,
263
+ // private-store fence, generation ledger, RVF index audit — and requires canonical equality with
264
+ // the receipt. A receipt with a single forged store digest passes every check above and fails
265
+ // here. It runs before any `gh` call so an untrue candidate never reaches the network.
266
+ try {
267
+ await verifyCorpusReceipt({
268
+ receiptFile, bundleFile, accuracyReportFile, recallReportFile, expectedBuilderSha: target, expectedArchiveSha256: archiveSha256,
269
+ });
270
+ } catch (error) {
271
+ corpusFailure(`corpus receipt does not verify against the sealed archive (${error.message})`);
272
+ }
273
+
274
+ // EVERY local proof happens before the first network call. `gh` must never be reached by a
275
+ // candidate that is already known to be unpublishable — that is the same discipline the deep
276
+ // verifyCorpusReceipt above follows, and a customer release with an unusable signature is exactly
277
+ // as unpublishable as an untrue receipt.
278
+ const signatureFile = `${bundleFile}.sig`;
279
+ const digestFile = `${bundleFile}.sha256`;
280
+ const generation = String(receipt.createdAt || '');
281
+ if (promoteLatest) {
282
+ for (const [label, file] of [['detached signature', signatureFile], ['sha256 sidecar', digestFile]]) {
283
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
284
+ corpusFailure(`customer corpus promotion requires a ${label} beside the archive (${path.basename(file)} missing) — the updater fails closed without it`);
285
+ }
286
+ }
287
+ // The real verifier, against the trust root that ships inside the npm package. Signing happens in
288
+ // the workflow with the environment-scoped key; this proves the bytes about to be published
289
+ // verify with the key kb/forge-update.mjs actually carries.
290
+ const signature = verifyBundle(bundleFile, signatureFile);
291
+ if (!signature.ok) corpusFailure(`detached signature does not verify against the shipped trust root (${signature.reason})`);
292
+ if (!Number.isFinite(Date.parse(generation))) corpusFailure('corpus receipt createdAt is not a readable generation timestamp');
293
+ }
294
+
163
295
  const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
164
296
  const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
165
297
  const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
@@ -168,33 +300,136 @@ export function runProtectedCorpusSeed({
168
300
  const viewError = String(view.error?.message || view.stderr || view.stdout || '');
169
301
  if (!/(release not found|no release found)/i.test(viewError)) corpusFailure(`cannot prove ${tag} is absent (${viewError.trim() || `gh exited ${view.status}`})`);
170
302
 
303
+ const receiptSha256 = sha256File(receiptFile);
304
+ const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
305
+
306
+ if (!promoteLatest) {
307
+ // BOOTSTRAP/RECOVERY seeds stay exactly as ADR-086's original contract left them: an immutable
308
+ // prerelease that never touches releases/latest. Dual's C4 resolution (S1) narrows the change to
309
+ // CUSTOMER releases — "Bootstrap-only releases may remain prereleases."
310
+ const notes = [
311
+ 'Content-addressed RuvNet Brain corpus seed.',
312
+ `Archive SHA-256: ${archiveSha256}`,
313
+ `Receipt SHA-256: ${receiptSha256}`,
314
+ `Accuracy report SHA-256: ${receipt.accuracyReport.sha256}`,
315
+ `Recall report SHA-256: ${receipt.recallReport.sha256}`,
316
+ ...recallNotes(receipt),
317
+ `Stores: ${receipt.storeCount}`,
318
+ `Builder source SHA: ${receipt.builderSourceSha}`,
319
+ 'This published prerelease is immutable and must never be replaced.',
320
+ ].join('\n');
321
+ // The detached accuracy report ships AS AN ASSET. Without it a downloader holds an archive it
322
+ // cannot re-verify — "reverify the downloaded final artifact against the measured identity"
323
+ // requires the measurement to travel with the artifact it measured.
324
+ const createArgs = [
325
+ 'release', 'create', tag,
326
+ '--prerelease', '--latest=false',
327
+ '--target', target,
328
+ '--repo', repo,
329
+ '--title', `Immutable corpus seed ${archiveSha256.slice(0, 16)}`,
330
+ '--notes', notes,
331
+ bundleFile, receiptFile, accuracyReportFile, recallReportFile,
332
+ ];
333
+ const create = gh(createArgs);
334
+ if (create.error || create.status !== 0) {
335
+ corpusFailure(`protected corpus publication failed (${String(create.error?.message || create.stderr || create.stdout || '').trim()})`);
336
+ }
337
+ return { tag, target, repository: repo, archiveSha256, receiptSha256, promoted: false };
338
+ }
339
+
340
+ // ── CUSTOMER CORPUS RELEASE (ADR-086 step 17 / C4 resolution S1) ───────────────────────────────
341
+ // The old path was invisible AND unusable to a customer, for two independent reasons, and fixing
342
+ // only one leaves the channel dead. `--prerelease --latest=false` means kb/forge-update.mjs's
343
+ // releases/latest poll never sees it; and with no detached .sig the updater fails closed anyway
344
+ // (kb/forge-update.mjs:1275 fetches `${url}.sig`, :1284-1285 exits 4 when verification fails).
345
+ // scripts/verify-channels.mjs checks exactly these two things (checks 3 and 4) against the live
346
+ // endpoints, and is the owner's post-publish acceptance gate.
347
+ const latestView = gh(['release', 'view', '--json', 'tagName,body', '--repo', repo]);
348
+ let currentLatest = null;
349
+ if (!latestView.error && latestView.status === 0) {
350
+ try { currentLatest = JSON.parse(String(latestView.stdout || 'null')); }
351
+ catch (error) { corpusFailure(`cannot read the current latest release (${error.message})`); }
352
+ if (!currentLatest || typeof currentLatest.tagName !== 'string') corpusFailure('current latest release carries no tag name');
353
+ } else {
354
+ const latestError = String(latestView.error?.message || latestView.stderr || latestView.stdout || '');
355
+ if (!/(release not found|no release found)/i.test(latestError)) {
356
+ corpusFailure(`cannot determine the current latest release (${latestError.trim() || `gh exited ${latestView.status}`})`);
357
+ }
358
+ }
359
+ const promotion = evaluateCorpusPromotion({ tag, generation, currentLatest });
360
+ if (!promotion.allowed) corpusFailure(promotion.reason);
361
+
171
362
  const notes = [
172
- 'Content-addressed RuvNet Brain corpus seed.',
363
+ 'RuvNet Brain corpus generation — signed, content-addressed, and promoted to latest.',
364
+ `${CORPUS_GENERATION_FIELD} ${generation}`,
173
365
  `Archive SHA-256: ${archiveSha256}`,
174
- `Receipt SHA-256: ${sha256File(receiptFile)}`,
366
+ `Receipt SHA-256: ${receiptSha256}`,
175
367
  `Stores: ${receipt.storeCount}`,
176
- `Coverage generation: ${receipt.coverageGeneration}`,
177
- 'This published prerelease is immutable and must never be replaced.',
368
+ `Builder source SHA: ${receipt.builderSourceSha}`,
369
+ `Shipped runtime: ${receipt.archiveManifestReleaseTag}`,
370
+ ...recallNotes(receipt),
371
+ 'Immutable: this tag is the archive digest and must never be replaced.',
178
372
  ].join('\n');
179
- const createArgs = [
373
+
374
+ // ASSETS COMPLETE BEFORE PROMOTION. `gh release create` uploads assets AFTER the release exists, so
375
+ // creating a non-draft release directly opens a window in which releases/latest resolves to a
376
+ // release with no archive — every polling client in that window fails or, worse, half-downloads.
377
+ // Create as a draft (invisible to releases/latest), prove all four assets landed, and only then
378
+ // flip draft off and claim latest in one edit.
379
+ // Both reports ride with every corpus release for the same reason they ride with a seed: a customer
380
+ // (or the next night's dispatcher) that downloads the archive must be able to reverify it against
381
+ // the identity it was actually measured under — the blocking recall gate AND the C3 diagnostic it
382
+ // scored 59.0% on, so nobody has to take either number on trust.
383
+ const assetFiles = [bundleFile, signatureFile, digestFile, receiptFile, accuracyReportFile, recallReportFile];
384
+ const create = gh([
180
385
  'release', 'create', tag,
181
- '--prerelease', '--latest=false',
386
+ '--draft',
182
387
  '--target', target,
183
388
  '--repo', repo,
184
- '--title', `Immutable corpus seed ${archiveSha256.slice(0, 16)}`,
389
+ '--title', `RuvNet Brain corpus ${archiveSha256.slice(0, 16)}`,
185
390
  '--notes', notes,
186
- bundleFile, receiptFile,
187
- ];
188
- const create = run(ghCommand, [...ghPrefix, ...createArgs], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
391
+ ...assetFiles,
392
+ ]);
189
393
  if (create.error || create.status !== 0) {
190
394
  corpusFailure(`protected corpus publication failed (${String(create.error?.message || create.stderr || create.stdout || '').trim()})`);
191
395
  }
192
- return { tag, target, repository: repo, archiveSha256, receiptSha256: sha256File(receiptFile) };
396
+
397
+ const expectedAssets = assetFiles.map((file) => path.basename(file)).sort();
398
+ const draftView = gh(['release', 'view', tag, '--json', 'isDraft,assets', '--repo', repo]);
399
+ if (draftView.error || draftView.status !== 0) corpusFailure('cannot confirm the draft corpus release before promotion');
400
+ let draft;
401
+ try { draft = JSON.parse(String(draftView.stdout || 'null')); }
402
+ catch (error) { corpusFailure(`cannot read the draft corpus release (${error.message})`); }
403
+ const uploaded = (draft?.assets || []).filter((asset) => asset?.state === 'uploaded' && Number.isSafeInteger(asset.size) && asset.size > 0);
404
+ if (draft?.isDraft !== true || JSON.stringify(uploaded.map((asset) => asset.name).sort()) !== JSON.stringify(expectedAssets)) {
405
+ corpusFailure(`refusing to promote an incomplete corpus release; expected ${expectedAssets.join(', ')} fully uploaded on a draft`);
406
+ }
407
+
408
+ const promote = gh(['release', 'edit', tag, '--repo', repo, '--draft=false', '--latest', '--prerelease=false']);
409
+ if (promote.error || promote.status !== 0) {
410
+ corpusFailure(`corpus promotion to latest failed (${String(promote.error?.message || promote.stderr || promote.stdout || '').trim()})`);
411
+ }
412
+
413
+ const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isLatest,isPrerelease,assets', '--repo', repo]);
414
+ if (finalView.error || finalView.status !== 0) corpusFailure('cannot confirm the promoted corpus release');
415
+ let promoted;
416
+ try { promoted = JSON.parse(String(finalView.stdout || 'null')); }
417
+ catch (error) { corpusFailure(`cannot read the promoted corpus release (${error.message})`); }
418
+ const promotedAssets = (promoted?.assets || []).map((asset) => asset?.name).sort();
419
+ if (promoted?.tagName !== tag || promoted.isDraft !== false || promoted.isLatest !== true
420
+ || promoted.isPrerelease !== false || JSON.stringify(promotedAssets) !== JSON.stringify(expectedAssets)) {
421
+ corpusFailure('corpus release did not reach a complete, non-draft, non-prerelease latest state');
422
+ }
423
+
424
+ return {
425
+ tag, target, repository: repo, archiveSha256, receiptSha256, promoted: true,
426
+ generation, supersededLatest: currentLatest?.tagName || null,
427
+ };
193
428
  }
194
429
 
195
430
  if (CORPUS_SEED) {
196
431
  try {
197
- const result = runProtectedCorpusSeed();
432
+ const result = await runProtectedCorpusSeed();
198
433
  console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
199
434
  } catch (error) {
200
435
  console.error(error.message);
@@ -169,6 +169,72 @@ export function validateRetrievalQueryEvidence(evidence) {
169
169
  return evidence;
170
170
  }
171
171
 
172
+ // An exemption is a RECORDED, EVIDENCED admission that one eligible store's upstream bytes cannot
173
+ // support a source-grounded question — never a bypass. It satisfies the inventory obligation only;
174
+ // buildRetrievalCanaryPlan still demands a row for every eligible store, so an exempted store must
175
+ // also leave the eligible corpus before a canary plan can be sealed. Recording one is a coverage
176
+ // policy decision that someone has to make in the open, which is the entire point.
177
+ export function validateOracleExemptions(exemptions) {
178
+ if (exemptions === null || exemptions === undefined) return [];
179
+ if (canonicalJson(Object.keys(exemptions || {}).sort()) !== canonicalJson(['exemptions', 'kind', 'schemaVersion'])
180
+ || exemptions.schemaVersion !== 1 || exemptions.kind !== 'ruvnet-brain-retrieval-oracle-exemptions'
181
+ || !Array.isArray(exemptions.exemptions)) {
182
+ throw new Error('retrieval oracle exemptions are malformed');
183
+ }
184
+ for (const row of exemptions.exemptions) {
185
+ if (canonicalJson(Object.keys(row || {}).sort()) !== canonicalJson(['evidencePaths', 'reason', 'store', 'upstreamSha'])
186
+ || typeof row.store !== 'string' || !/^[a-z0-9][a-z0-9._-]*$/.test(row.store)
187
+ || typeof row.reason !== 'string' || row.reason.trim().length < 24
188
+ || !HEX40.test(String(row.upstreamSha || ''))
189
+ || !Array.isArray(row.evidencePaths) || row.evidencePaths.length < 1
190
+ || row.evidencePaths.some((file) => typeof file !== 'string' || !file || path.isAbsolute(file)
191
+ || file.split(/[\\/]/).includes('..'))) {
192
+ throw new Error(`retrieval oracle exemption for ${row?.store || '(missing)'} is malformed`);
193
+ }
194
+ }
195
+ const stores = exemptions.exemptions.map((row) => row.store);
196
+ if (new Set(stores).size !== stores.length) throw new Error('retrieval oracle exemptions name a store twice');
197
+ return exemptions.exemptions;
198
+ }
199
+
200
+ // The cheap, standalone form of the denominator gate buildRetrievalCanaryPlan enforces at release
201
+ // time against a 567MB seed. Missing evidence is a FAILURE here, named store by store, seconds after
202
+ // a new repository lands in coverage — not six minutes into release-qe on a runner.
203
+ export function auditOracleCoverage({ coverage, queryEvidence, exemptions = null } = {}) {
204
+ const checked = validateCoverageLedger(coverage);
205
+ if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
206
+ validateRetrievalQueryEvidence(queryEvidence);
207
+ const eligibleRows = coverage.rows.filter((row) => row.kind === 'repository'
208
+ && row.disposition === 'eligible' && row.status === 'CURRENT');
209
+ const eligible = ordered(eligibleRows.map(storeOf));
210
+ if (!eligible.length || new Set(eligible).size !== eligible.length || eligible.some((store) => !store)) {
211
+ throw new Error('eligible coverage denominator is invalid');
212
+ }
213
+ const oracle = new Set(Object.keys(queryEvidence.queries));
214
+ const exempted = validateOracleExemptions(exemptions);
215
+ const exemptStores = new Set(exempted.map((row) => row.store));
216
+ const uncovered = eligible.filter((store) => !oracle.has(store));
217
+ const missing = uncovered.filter((store) => !exemptStores.has(store));
218
+ const exempt = uncovered.filter((store) => exemptStores.has(store));
219
+ const extra = ordered([...oracle].filter((store) => !eligible.includes(store)));
220
+ const stale = ordered(exempted.map((row) => row.store).filter((store) => !uncovered.includes(store)));
221
+ const result = { eligible: eligible.length, covered: eligible.length - uncovered.length,
222
+ missing, exempt, extra, staleExemptions: stale };
223
+ if (missing.length) {
224
+ throw new Error(`independent retrieval oracle has no source-grounded row for ${missing.length} of `
225
+ + `${eligible.length} eligible stores: ${missing.join(', ')}`);
226
+ }
227
+ if (extra.length) {
228
+ throw new Error(`independent retrieval oracle covers ${extra.length} store(s) outside the eligible `
229
+ + `denominator: ${extra.join(', ')}`);
230
+ }
231
+ if (stale.length) {
232
+ throw new Error(`retrieval oracle exemption is stale for ${stale.join(', ')}; the store is covered `
233
+ + 'or no longer eligible, so the recorded admission must be removed');
234
+ }
235
+ return result;
236
+ }
237
+
172
238
  export function verifyQueryOracleSource(queryEvidence, candidateSourceSha, {
173
239
  cwd = process.cwd(), run = spawnSync, allowSquashedSource = false,
174
240
  } = {}) {
@@ -600,13 +666,34 @@ export function buildPlanFromFiles({ coverageFile, baselineFile, candidateFile,
600
666
  return plan;
601
667
  }
602
668
 
669
+ export function auditOracleCoverageFromFiles({ coverageFile, oracleFile, exemptionsFile = null,
670
+ candidateSourceSha = null, cwd = process.cwd() }) {
671
+ const coverage = JSON.parse(fs.readFileSync(regular(coverageFile, 'coverage'), 'utf8'));
672
+ const queryEvidence = JSON.parse(fs.readFileSync(regular(oracleFile, 'query oracle'), 'utf8'));
673
+ const exemptions = exemptionsFile
674
+ ? JSON.parse(fs.readFileSync(regular(exemptionsFile, 'oracle exemptions'), 'utf8')) : null;
675
+ const inventory = auditOracleCoverage({ coverage, queryEvidence, exemptions });
676
+ // The second half of step 7's proof: the oracle's own commit must be a strict ancestor of the
677
+ // candidate that consumes it, and its tracked bytes must be identical at both.
678
+ const strictAncestor = candidateSourceSha
679
+ ? Boolean(verifyQueryOracleSource(queryEvidence, candidateSourceSha, { cwd })) : null;
680
+ return { ...inventory, oracleSourceCommit: queryEvidence.sourceCommit, strictAncestor };
681
+ }
682
+
603
683
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
604
684
  try {
605
685
  const argv = process.argv.slice(2);
686
+ if (argv.includes('--audit')) {
687
+ const report = auditOracleCoverageFromFiles({ coverageFile: arg(argv, '--coverage'),
688
+ oracleFile: arg(argv, '--oracle'), exemptionsFile: arg(argv, '--exemptions'),
689
+ candidateSourceSha: arg(argv, '--candidate-sha'), cwd: arg(argv, '--repo') || process.cwd() });
690
+ console.log(JSON.stringify({ ok: true, ...report }));
691
+ } else {
606
692
  const plan = buildPlanFromFiles({ coverageFile: arg(argv, '--coverage'), baselineFile: arg(argv, '--baseline'),
607
693
  candidateFile: arg(argv, '--candidate'), oracleFile: arg(argv, '--oracle'), assetsDir: arg(argv, '--assets'),
608
694
  outFile: arg(argv, '--out'), cwd: arg(argv, '--repo') || process.cwd() });
609
695
  console.log(JSON.stringify({ ok: true, planSha256: plan.planSha256, cases: plan.cases.length }));
696
+ }
610
697
  } catch (error) {
611
698
  console.error(`[retrieval-canary] ${error.message}`);
612
699
  process.exitCode = 1;