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.
- package/README.md +2 -2
- package/bin/install.mjs +32 -9
- package/kb/forge-update.mjs +1867 -0
- package/package.json +3 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +6 -1
- package/plugin/hooks/hook-contracts.json +11 -9
- package/plugin/scripts/capability-claim-evidence.mjs +11 -2
- package/plugin/scripts/capacity-aware-parallel-work.mjs +71 -15
- package/plugin/scripts/completion-claim-evidence.mjs +262 -0
- package/plugin/scripts/continuation-gate.mjs +105 -21
- package/plugin/scripts/continuation-objective.mjs +15 -0
- package/plugin/scripts/continuity-hook-policy.mjs +10 -3
- package/plugin/scripts/decision-gate.mjs +22 -2
- package/plugin/scripts/duplicate-gate.mjs +503 -0
- package/plugin/scripts/grounding-turn-evidence.mjs +339 -0
- package/plugin/scripts/grounding-turn-gate.mjs +77 -25
- package/plugin/scripts/grounding-turn-mark.mjs +61 -14
- package/plugin/scripts/hook-input.mjs +15 -0
- package/plugin/scripts/hook-shim.mjs +3 -1
- package/plugin/scripts/host-update.mjs +45 -0
- package/plugin/scripts/nightly-scheduler.mjs +34 -0
- package/plugin/scripts/session-snapshot-hook.mjs +11 -1
- package/plugin/scripts/session-start-budget.mjs +1 -0
- package/plugin/scripts/session-start-core.mjs +15 -2
- package/plugin/scripts/session-start-health.mjs +101 -1
- package/plugin/scripts/session-start-update-plane.mjs +71 -1
- package/plugin/scripts/turn-outcome-capture.mjs +292 -0
- package/scripts/completion-claim-replay.mjs +114 -0
- package/scripts/corpus-canary.mjs +399 -0
- package/scripts/corpus-dispatch-decision.mjs +2 -2
- package/scripts/corpus-promotion.mjs +49 -0
- package/scripts/corpus-reconcile.mjs +27 -3
- package/scripts/corpus-watchdog.mjs +45 -6
- package/scripts/derive-passage-content-map.mjs +71 -0
- package/scripts/duplicate-gate-replay.mjs +98 -0
- package/scripts/grounding-turn-replay.mjs +131 -0
- package/scripts/nightly-watchdog.mjs +3 -3
- package/scripts/public-verification-inputs.mjs +24 -2
- package/scripts/release-transaction-provider.mjs +29 -6
- package/scripts/release.mjs +177 -74
- package/scripts/retrieval-canary.mjs +41 -4
- package/scripts/retrieval-passage-identity.mjs +58 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +6 -0
package/scripts/release.mjs
CHANGED
|
@@ -65,7 +65,9 @@ const recallNotes = (receipt) => {
|
|
|
65
65
|
];
|
|
66
66
|
};
|
|
67
67
|
import { verifyBundle } from './verify-bundle.mjs';
|
|
68
|
-
import {
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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 (!
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
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
|
|
499
|
-
//
|
|
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
|
|
564
|
+
corpusFailure(`refusing to stage an incomplete corpus release; expected ${expectedAssets.join(', ')} fully uploaded on a draft`);
|
|
528
565
|
}
|
|
529
566
|
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
532
|
-
corpusFailure(`corpus
|
|
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
|
|
537
|
-
//
|
|
538
|
-
|
|
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
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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 (
|
|
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 &&
|
|
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
|
+
}
|
package/scripts/sync-census.mjs
CHANGED
|
Binary file
|
package/scripts/wired-check.mjs
CHANGED
|
@@ -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'],
|