ruvnet-brain 4.3.21 → 4.3.26
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 +5 -5
- package/bin/install.mjs +275 -60
- package/console/app.js +189 -9
- package/console/index.html +70 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/style.css +26 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +364 -28
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +47 -10
- package/scripts/release-transaction.mjs +40 -11
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- 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,15 @@ 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`]));
|
|
24
|
+
// Two DIFFERENT questions, deliberately: latestCodeTag() is "which CODE generation is newest" (a
|
|
25
|
+
// corpus generation may hold the pointer — designed steady state); githubPointerTag() is "what does
|
|
26
|
+
// GitHub's releases/latest actually point at right now". The publish-github-nonlatest gate needs the
|
|
27
|
+
// second and was reading the first, which made it unsatisfiable for every freshly published release.
|
|
28
|
+
const githubPointerTag = () => maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
|
|
19
29
|
const maybe = (callback, fallback = null) => {
|
|
20
30
|
try { return callback(); } catch { return fallback; }
|
|
21
31
|
};
|
|
@@ -74,14 +84,26 @@ const sha256File = (file) => {
|
|
|
74
84
|
return hash.digest('hex');
|
|
75
85
|
};
|
|
76
86
|
const assetDigest = (asset) => withTempAsset(asset, sha256File);
|
|
87
|
+
// 4.3.25 run 35056636514: `npm publish` succeeded at ~04:47:31Z and the registry first showed the
|
|
88
|
+
// version at 04:53:40Z (time.modified) — roughly six minutes. The poll gave up at 156s, the job
|
|
89
|
+
// reported FAILURE, and an already-published release was left half-promoted with npm and GitHub
|
|
90
|
+
// naming different generations (the exact #77 split). The action was never the problem; the
|
|
91
|
+
// deadline was shorter than npm's propagation for this package. Ten minutes covers the observed
|
|
92
|
+
// ~6 with margin; the env overrides remain for a runner that knows better.
|
|
77
93
|
const OBSERVATION_POLICY = {
|
|
78
|
-
maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS ||
|
|
79
|
-
maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS ||
|
|
94
|
+
maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS || 600_000),
|
|
95
|
+
maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS || 40),
|
|
80
96
|
initialDelayMs: 1_000,
|
|
81
97
|
maxDelayMs: 15_000,
|
|
82
98
|
multiplier: 1.8,
|
|
83
99
|
jitter: (delay) => Math.floor(Math.random() * Math.min(1_000, delay * 0.2)),
|
|
84
100
|
};
|
|
101
|
+
// The one command in this file that mutates a public channel gets its stderr in the job log and a
|
|
102
|
+
// timeout sized for a real registry upload. command()'s defaults — stderr piped and dropped, 30s —
|
|
103
|
+
// meant 4.3.25's first run left NO diagnostic between "stage-npm" and the deadline error, and a slow
|
|
104
|
+
// registry would have killed a legitimate publish mid-upload. Named, so the publish call stays one
|
|
105
|
+
// pinnable line for tests/unit/release-lineage + publication-receipt-wiring (one publisher, one tag).
|
|
106
|
+
const PUBLISH_COMMAND_OPTIONS = Object.freeze({ stdio: ['ignore', 'pipe', 'inherit'], timeout: 600_000 });
|
|
85
107
|
|
|
86
108
|
export function selectCurrentReleaseBytes({ remoteBytes, localBytes, generatedBytes, identity }) {
|
|
87
109
|
const bytes = remoteBytes || localBytes || generatedBytes;
|
|
@@ -216,7 +238,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
216
238
|
}
|
|
217
239
|
}
|
|
218
240
|
const npmLatest = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
219
|
-
const githubLatest =
|
|
241
|
+
const githubLatest = latestCodeTag();
|
|
220
242
|
const legacySettled = [];
|
|
221
243
|
const pending = [];
|
|
222
244
|
for (const { receipt, release } of latestByTransaction.values()) {
|
|
@@ -257,7 +279,8 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
257
279
|
async observeSnapshot(identity, draft = activeDraft, { forceAssets = false } = {}) {
|
|
258
280
|
try {
|
|
259
281
|
const release = draft?.id ? hydratedRelease(releaseById(draft.id)) : null;
|
|
260
|
-
const latestTag =
|
|
282
|
+
const latestTag = latestCodeTag();
|
|
283
|
+
const pointerTag = githubPointerTag();
|
|
261
284
|
const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
|
|
262
285
|
const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
|
|
263
286
|
const latestVersion = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
@@ -290,8 +313,10 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
290
313
|
published: release.draft === false,
|
|
291
314
|
latest: latestTag === identity.tag,
|
|
292
315
|
latestTag,
|
|
316
|
+
// What releases/latest actually points at — distinct from `latest` (code recency) above.
|
|
317
|
+
pointerTag,
|
|
293
318
|
assetsExact: assetsExactFor(release, identity, forceAssets),
|
|
294
|
-
} : { tag: null, sha: null, draft: false, published: false, latest: false, assetsExact: false },
|
|
319
|
+
} : { tag: null, sha: null, draft: false, published: false, latest: false, pointerTag, assetsExact: false },
|
|
295
320
|
publicReceiptExact,
|
|
296
321
|
publicHostsExact,
|
|
297
322
|
};
|
|
@@ -395,7 +420,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
395
420
|
command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, `candidate-v${identity.version}`]);
|
|
396
421
|
return;
|
|
397
422
|
}
|
|
398
|
-
command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`]);
|
|
423
|
+
command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`], PUBLISH_COMMAND_OPTIONS);
|
|
399
424
|
},
|
|
400
425
|
|
|
401
426
|
async observeNpmCandidate(identity) {
|
|
@@ -409,7 +434,10 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
409
434
|
|
|
410
435
|
async observeGithub(identity) {
|
|
411
436
|
const release = json('gh', ['api', `repos/${REPO}/releases/tags/${identity.tag}`]);
|
|
412
|
-
|
|
437
|
+
// "Is this release the current CODE generation", not "does it hold the latest pointer". Once a
|
|
438
|
+
// corpus generation takes the pointer — which is the designed steady state — a genuinely
|
|
439
|
+
// published code release would otherwise start observing itself as no longer latest.
|
|
440
|
+
const latest = maybe(() => latestCodeTag(), null);
|
|
413
441
|
return { tag: release.tag_name, sha: tagSha(identity.tag, root), latest: latest === identity.tag };
|
|
414
442
|
},
|
|
415
443
|
|
|
@@ -424,14 +452,23 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
424
452
|
return { version: command('npm', ['view', `${PACKAGE}@latest`, 'version']) };
|
|
425
453
|
},
|
|
426
454
|
async makeGithubLatest(draft, identity, expectedPrior) {
|
|
427
|
-
|
|
455
|
+
// The prior-generation check is about the CODE lineage: it exists to catch "someone promoted a
|
|
456
|
+
// different product generation while this transaction was in flight". A corpus generation
|
|
457
|
+
// legitimately holding `releases/latest` is NOT that — it is the designed steady state between
|
|
458
|
+
// code releases — so the comparison is made against the latest CODE release, and the pointer's
|
|
459
|
+
// own kind is recorded rather than treated as a conflict.
|
|
460
|
+
const pointer = githubPointerTag();
|
|
461
|
+
const current = latestCodeTag();
|
|
428
462
|
if (current !== expectedPrior && current !== identity.tag) {
|
|
429
|
-
throw new Error(`refusing GitHub promotion: latest is ${current}, expected ${expectedPrior}`);
|
|
463
|
+
throw new Error(`refusing GitHub promotion: latest code release is ${current}, expected ${expectedPrior}`);
|
|
464
|
+
}
|
|
465
|
+
if (isCorpusReleaseTag(pointer)) {
|
|
466
|
+
process.stdout.write(`[release] promoting over corpus generation ${pointer} — the code bundle carries its own corpus\n`);
|
|
430
467
|
}
|
|
431
468
|
command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-f', 'make_latest=true']);
|
|
432
469
|
},
|
|
433
470
|
async observeGithubLatest() {
|
|
434
|
-
return { tag:
|
|
471
|
+
return { tag: latestCodeTag() };
|
|
435
472
|
},
|
|
436
473
|
async restoreNpmLatest(prior, expected) {
|
|
437
474
|
const current = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
@@ -194,6 +194,25 @@ const npmCandidateExact = (snapshot, identity) => snapshot?.npm?.candidateVersio
|
|
|
194
194
|
const npmLatestIsB = (snapshot, identity) => snapshot?.npm?.latestVersion === identity.version;
|
|
195
195
|
const githubIsB = (snapshot, identity) => snapshot?.github?.sha === identity.candidateSha
|
|
196
196
|
&& snapshot?.github?.tag === identity.tag;
|
|
197
|
+
// TWO DIFFERENT QUESTIONS ABOUT "LATEST", ANSWERED BY TWO DIFFERENT FIELDS (4.3.25, 2026-09-16).
|
|
198
|
+
//
|
|
199
|
+
// `snapshot.github.latest` is "is this the newest CODE release" — observeGithub derives it from
|
|
200
|
+
// latestCodeReleaseTag(), deliberately, because per ADR-086 S1 a corpus generation may legitimately
|
|
201
|
+
// hold GitHub's releases/latest pointer and a code release must not observe itself as demoted.
|
|
202
|
+
// `snapshot.github.pointerTag` is what releases/latest actually points at.
|
|
203
|
+
//
|
|
204
|
+
// Every promotion, recovery, verification and abort check in this file wants the SECOND question:
|
|
205
|
+
// "has the pointer been moved to B yet". Ten sites read the first. A freshly published code release
|
|
206
|
+
// is ALWAYS the newest code release, so `!latest` was unsatisfiable (publish gate, resume clause —
|
|
207
|
+
// the transaction wedged at github-promote-intent), `latest` was trivially true (promotion looked
|
|
208
|
+
// already done, the pointer never moved, converge and finalize passed with it unmoved), and abort
|
|
209
|
+
// refused because "latest" had moved. Ask the pointer. Snapshots without one (recorded receipts,
|
|
210
|
+
// older providers) fall back to code recency so history stays interpretable.
|
|
211
|
+
const githubPointerIsB = (snapshot, identity) => (snapshot?.github?.pointerTag !== undefined
|
|
212
|
+
? snapshot.github.pointerTag === identity.tag
|
|
213
|
+
: snapshot?.github?.latest === true);
|
|
214
|
+
const githubPointerTag = (snapshot) => (snapshot?.github?.pointerTag !== undefined
|
|
215
|
+
? snapshot.github.pointerTag : snapshot?.github?.latestTag);
|
|
197
216
|
|
|
198
217
|
/**
|
|
199
218
|
* Pure, total next-action selector. Receipt history is an audit chain, never permission to skip a
|
|
@@ -206,7 +225,7 @@ export function reduceReleaseState({ lastReceipt, snapshot, identity, prior }) {
|
|
|
206
225
|
const npmB = npmLatestIsB(snapshot, identity);
|
|
207
226
|
const githubB = githubIsB(snapshot, identity);
|
|
208
227
|
const githubPublished = githubB && snapshot.github?.published === true;
|
|
209
|
-
const githubLatest = githubPublished && snapshot
|
|
228
|
+
const githubLatest = githubPublished && githubPointerIsB(snapshot, identity);
|
|
210
229
|
|
|
211
230
|
if (snapshot.npm?.candidateVersion === identity.version && snapshot.npm?.candidateIntegrity
|
|
212
231
|
&& snapshot.npm.candidateIntegrity !== identity.packageIntegrity) {
|
|
@@ -338,8 +357,11 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
338
357
|
await transition('prepared', { recovered: true });
|
|
339
358
|
continue;
|
|
340
359
|
}
|
|
360
|
+
// 4.3.25 run 35109036116: run 2 died after writing github-promote-intent, run 3 found the draft
|
|
361
|
+
// published, the reducer asked for promote-npm — not a legal edge from here. This clause was the
|
|
362
|
+
// only legal exit and its `!latest` was unsatisfiable. Wedged.
|
|
341
363
|
if (current.state === 'github-promote-intent' && githubIsB(snapshot, identity)
|
|
342
|
-
&& snapshot.github?.published && !snapshot
|
|
364
|
+
&& snapshot.github?.published && !githubPointerIsB(snapshot, identity)) {
|
|
343
365
|
await transition('github-promoted-nonlatest', { github: snapshot.github, recovered: true });
|
|
344
366
|
continue;
|
|
345
367
|
}
|
|
@@ -348,7 +370,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
348
370
|
continue;
|
|
349
371
|
}
|
|
350
372
|
if (current.state === 'github-latest-intent' && npmLatestIsB(snapshot, identity)
|
|
351
|
-
&& githubIsB(snapshot, identity) && snapshot
|
|
373
|
+
&& githubIsB(snapshot, identity) && githubPointerIsB(snapshot, identity)) {
|
|
352
374
|
await transition('defaults-promoted', { npm: snapshot.npm, github: snapshot.github, recovered: true });
|
|
353
375
|
continue;
|
|
354
376
|
}
|
|
@@ -357,7 +379,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
357
379
|
throw new Error('npm compensation recovered; resume the same transaction');
|
|
358
380
|
}
|
|
359
381
|
if (current.state === 'finalize-intent' && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
|
|
360
|
-
&& githubIsB(snapshot, identity) && snapshot.github?.published && snapshot
|
|
382
|
+
&& githubIsB(snapshot, identity) && snapshot.github?.published && githubPointerIsB(snapshot, identity)
|
|
361
383
|
&& snapshot.github?.assetsExact) {
|
|
362
384
|
return append('channels-converged', { verdict: 'PUBLISHED_NOT_VERIFIED', recovered: true,
|
|
363
385
|
npm: snapshot.npm, github: snapshot.github });
|
|
@@ -425,7 +447,10 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
425
447
|
await transition('github-promote-intent');
|
|
426
448
|
await adapter.publishDraftNonLatest(draft, identity);
|
|
427
449
|
const observed = await adapter.observeSnapshot(identity, draft);
|
|
428
|
-
|
|
450
|
+
// Published as B, and the POINTER has not moved to B yet — it is promoted later, in
|
|
451
|
+
// make-github-latest. 4.3.25 run 35057738103 published this correctly and then threw here on
|
|
452
|
+
// `!latest`, which a just-published code release can never satisfy.
|
|
453
|
+
if (!(githubIsB(observed, identity) && observed.github.published && !githubPointerIsB(observed, identity))) {
|
|
429
454
|
throw new Error('GitHub non-latest publication not observed');
|
|
430
455
|
}
|
|
431
456
|
await transition('github-promoted-nonlatest', { github: observed.github });
|
|
@@ -446,7 +471,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
446
471
|
await transition('github-latest-intent');
|
|
447
472
|
await adapter.makeGithubLatest(draft, identity, prior.githubLatest);
|
|
448
473
|
const observed = await adapter.observeSnapshot(identity, draft);
|
|
449
|
-
if (!(npmLatestIsB(observed, identity) && observed
|
|
474
|
+
if (!(npmLatestIsB(observed, identity) && githubPointerIsB(observed, identity) && githubIsB(observed, identity))) {
|
|
450
475
|
throw new Error('provider defaults not jointly observed at candidate B');
|
|
451
476
|
}
|
|
452
477
|
await transition('defaults-promoted', { npm: observed.npm, github: observed.github });
|
|
@@ -469,7 +494,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
469
494
|
}
|
|
470
495
|
await transition('finalize-intent');
|
|
471
496
|
const reobserved = await adapter.observeSnapshot(identity, draft, { forceAssets: true });
|
|
472
|
-
if (!(npmLatestIsB(reobserved, identity) && reobserved
|
|
497
|
+
if (!(npmLatestIsB(reobserved, identity) && githubPointerIsB(reobserved, identity) && githubIsB(reobserved, identity))) {
|
|
473
498
|
throw new Error('provider defaults drifted before channel convergence');
|
|
474
499
|
}
|
|
475
500
|
if (!reobserved.github?.assetsExact || !npmCandidateExact(reobserved, identity)) {
|
|
@@ -491,8 +516,12 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
|
|
|
491
516
|
}
|
|
492
517
|
const snapshot = await adapter.observeSnapshot(identity);
|
|
493
518
|
const prior = chain[0]?.observation?.prior;
|
|
494
|
-
|
|
495
|
-
|
|
519
|
+
// "Both defaults still at the captured prior generation" is a question about the npm `latest`
|
|
520
|
+
// dist-tag and GitHub's releases/latest POINTER. A published-but-not-promoted candidate is the
|
|
521
|
+
// newest CODE release, so the old `latest === true` / `latestTag !== prior` reads refused to abort
|
|
522
|
+
// exactly the transaction that most needs aborting: one wedged after publish, before promotion.
|
|
523
|
+
if (!prior || snapshot.npm?.latestVersion === identity.version || githubPointerIsB(snapshot, identity)
|
|
524
|
+
|| snapshot.npm?.latestVersion !== prior.npmLatest || githubPointerTag(snapshot) !== prior.githubLatest) {
|
|
496
525
|
throw new Error('release abort cannot prove both defaults at captured prior generation');
|
|
497
526
|
}
|
|
498
527
|
const receipt = stateReceipt({
|
|
@@ -550,7 +579,7 @@ export async function finalizeReleaseTransaction({
|
|
|
550
579
|
}
|
|
551
580
|
const snapshot = await adapter.observeSnapshot(identity, discovered.matchingDrafts?.[0], { forceAssets: true });
|
|
552
581
|
if (snapshot.readError || !npmLatestIsB(snapshot, identity) || !npmCandidateExact(snapshot, identity)
|
|
553
|
-
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || snapshot
|
|
582
|
+
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || !githubPointerIsB(snapshot, identity)
|
|
554
583
|
|| snapshot.github?.assetsExact !== true) throw new Error('public channels drifted before install verification');
|
|
555
584
|
const materialized = await adapter.materializePublicVerificationAggregate({ identity, aggregate });
|
|
556
585
|
const aggregateAssetSha256 = crypto.createHash('sha256').update(canonicalJson(aggregate)).digest('hex');
|
|
@@ -642,7 +671,7 @@ export async function abandonPublicVerificationTransaction({ identity, expected,
|
|
|
642
671
|
}
|
|
643
672
|
const snapshot = await adapter.observeSnapshot(identity, discovered.matchingDrafts?.[0], { forceAssets: true });
|
|
644
673
|
if (snapshot.readError || !npmLatestIsB(snapshot, identity) || !npmCandidateExact(snapshot, identity)
|
|
645
|
-
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || snapshot
|
|
674
|
+
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || !githubPointerIsB(snapshot, identity)
|
|
646
675
|
|| snapshot.github?.assetsExact !== true
|
|
647
676
|
|| canonicalJson(snapshot.npm) !== canonicalJson(current.observation.npm)
|
|
648
677
|
|| canonicalJson(snapshot.github) !== canonicalJson(current.observation.github)) {
|
package/scripts/release.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
&&
|
|
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 !==
|
|
150
|
-
|| !receipt.createdAt || !
|
|
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
|
-
'
|
|
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: ${
|
|
366
|
+
`Receipt SHA-256: ${receiptSha256}`,
|
|
175
367
|
`Stores: ${receipt.storeCount}`,
|
|
176
|
-
`
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
'--
|
|
386
|
+
'--draft',
|
|
182
387
|
'--target', target,
|
|
183
388
|
'--repo', repo,
|
|
184
|
-
'--title', `
|
|
389
|
+
'--title', `RuvNet Brain corpus ${archiveSha256.slice(0, 16)}`,
|
|
185
390
|
'--notes', notes,
|
|
186
|
-
|
|
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
|
-
|
|
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);
|