ruvnet-brain 4.0.90-dev → 4.3.1-dev
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 +31 -7
- package/bin/install.mjs +153 -22
- package/console/architecture.html +12 -14
- package/console/install-mockup.html +3 -3
- package/kb/zip-extract.mjs +8 -2
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/host-adapters/claude.json +11 -0
- package/plugin/host-adapters/codex.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +77 -10
- package/plugin/mcp/server.mjs +2 -1
- package/plugin/scripts/anticipate.sh +30 -1
- package/plugin/scripts/capability-claim-evidence.mjs +431 -0
- package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
- package/plugin/scripts/capability-registry.mjs +6 -0
- package/plugin/scripts/capability-routing.mjs +70 -0
- package/plugin/scripts/continuation-gate.mjs +68 -2
- package/plugin/scripts/coverage-integrity.mjs +575 -0
- package/plugin/scripts/decision-outcomes.mjs +0 -0
- package/plugin/scripts/ground-ruvnet.sh +2 -2
- package/plugin/scripts/nightly-controller.mjs +19 -2
- package/plugin/scripts/project-progression-contract.mjs +409 -0
- package/plugin/scripts/project-progression-hook.mjs +196 -0
- package/plugin/scripts/project-progression-outbox.mjs +95 -0
- package/plugin/scripts/project-progression-session-start.mjs +183 -0
- package/plugin/scripts/project-progression-store.mjs +248 -0
- package/plugin/scripts/project-store-resolver.mjs +106 -0
- package/plugin/scripts/session-snapshot-hook.mjs +29 -1
- package/plugin/scripts/session-start-core.mjs +22 -18
- package/plugin/scripts/spend-guard.mjs +6 -1
- package/scripts/adr-072-completion.mjs +98 -0
- package/scripts/behavioral-l1-l4.mjs +11 -8
- package/scripts/brain-score.mjs +23 -9
- package/scripts/build-bundle.mjs +94 -8
- package/scripts/card-from-source.mjs +140 -0
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +314 -0
- package/scripts/corpus-reconcile.mjs +540 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/coverage-integrity.mjs +3 -0
- package/scripts/gist-receipts.mjs +183 -0
- package/scripts/git-hooks/pre-push +21 -0
- package/scripts/host-registry.mjs +118 -0
- package/scripts/independent-review-receipt.mjs +499 -0
- package/scripts/ingest-gists.mjs +34 -5
- package/scripts/ingest-new-repos.mjs +127 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +105 -64
- package/scripts/product-integrity-contract.mjs +156 -0
- package/scripts/public-inventory.mjs +4 -0
- package/scripts/public-verification-aggregate.mjs +342 -0
- package/scripts/public-verification-finalizer.mjs +59 -0
- package/scripts/public-verification-inputs.mjs +473 -0
- package/scripts/public-verification-lane.mjs +244 -0
- package/scripts/publication-receipt.mjs +50 -6
- package/scripts/rebuild-gists-from-receipts.mjs +291 -0
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +165 -49
- package/scripts/release.mjs +158 -3
- package/scripts/restore-local-ingests.mjs +2 -2
- package/scripts/retrieval-canary.mjs +533 -0
- package/scripts/routing-flywheel.mjs +12 -1
- package/scripts/rvf-generation.mjs +50 -0
- package/scripts/self-update.mjs +19 -44
- package/scripts/source-coverage.mjs +415 -0
- package/scripts/source-scope-receipt.mjs +84 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +114 -24
- package/scripts/worktree-integrity.mjs +123 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build the release-bound coverage projection from an observed corpus ledger and exact bundle bytes.
|
|
3
|
+
// This is intentionally a producer, not a validator: public-verification-inputs.mjs remains the
|
|
4
|
+
// independent consumer that proves the emitted files agree with the archive.
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { digest, generationLedgerBytes, projectPublicGenerationLedger,
|
|
10
|
+
releaseCoverageGenerationFor } from '../plugin/scripts/coverage-integrity.mjs';
|
|
11
|
+
import { validatePublicInventory } from './public-inventory.mjs';
|
|
12
|
+
import { readRvfGenerations } from './rvf-generation.mjs';
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
+
const arg = (name, fallback = null) => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : fallback; };
|
|
16
|
+
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
18
|
+
const storeName = (value) => String(value || '').toLowerCase().replace(/\.big\.rvf$|\.rvf$/i, '');
|
|
19
|
+
|
|
20
|
+
export function createReleaseProjection({ corpusCoverage, assetsDir, version, sourceSnapshot,
|
|
21
|
+
corpusSeed, baselineReceiptSha256, outDir }) {
|
|
22
|
+
if (!corpusCoverage || corpusCoverage.kind !== 'ruvnet-brain-corpus-coverage') {
|
|
23
|
+
throw new Error('corpus coverage must be an observed ruvnet-brain-corpus-coverage ledger');
|
|
24
|
+
}
|
|
25
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version || '')) throw new Error('release version is invalid');
|
|
26
|
+
if (!/^[a-f0-9]{40}$/.test(sourceSnapshot || '')) throw new Error('release source snapshot is invalid');
|
|
27
|
+
if (!corpusSeed?.tag || !/^[a-f0-9]{64}$/.test(corpusSeed.archiveSha256 || '')
|
|
28
|
+
|| !Number.isSafeInteger(corpusSeed.archiveBytes) || !/^[a-f0-9]{64}$/.test(baselineReceiptSha256 || '')) {
|
|
29
|
+
throw new Error('corpus seed identity is incomplete');
|
|
30
|
+
}
|
|
31
|
+
const assets = path.resolve(assetsDir);
|
|
32
|
+
const sourceLedger = readRvfGenerations(assets);
|
|
33
|
+
// The coverage ledger describes the full upstream universe. A release candidate contains
|
|
34
|
+
// only the immutable seed's asset set, so absent-but-eligible upstream rows are not release
|
|
35
|
+
// blockers; shipped stores must still all be CURRENT.
|
|
36
|
+
const assetStores = fs.readdirSync(assets)
|
|
37
|
+
.filter((name) => /\.rvf$/i.test(name)).map(storeName);
|
|
38
|
+
const availableStores = new Set([
|
|
39
|
+
...Object.keys(sourceLedger.stores || {}).map(storeName),
|
|
40
|
+
...assetStores,
|
|
41
|
+
]);
|
|
42
|
+
const eligible = corpusCoverage.rows.filter((row) => row.disposition === 'eligible'
|
|
43
|
+
&& availableStores.has(storeName(row.artifact?.store)));
|
|
44
|
+
if (!eligible.length || eligible.some((row) => row.status !== 'CURRENT')) throw new Error('eligible corpus rows are not all CURRENT');
|
|
45
|
+
const publicStores = [...new Set(eligible.map((row) => String(row.artifact.store).toLowerCase()))];
|
|
46
|
+
// Derived public stores are not corpus rows, but they are still part of the
|
|
47
|
+
// exact shipped generation ledger when the candidate asset set contains them.
|
|
48
|
+
if (sourceLedger.stores?.concepts && !publicStores.includes('concepts')) publicStores.push('concepts');
|
|
49
|
+
const ledger = projectPublicGenerationLedger({ ledger: sourceLedger, publicStores, version, sourceSnapshot });
|
|
50
|
+
const releaseBase = {
|
|
51
|
+
schemaVersion: 1, kind: 'ruvnet-brain-release-coverage', owner: corpusCoverage.owner,
|
|
52
|
+
observedAt: corpusCoverage.observedAt, generatorSourceSha: corpusCoverage.generatorSourceSha,
|
|
53
|
+
sourceObservationSha256: corpusCoverage.sourceObservationSha256, snapshotRoot: corpusCoverage.snapshotRoot,
|
|
54
|
+
releaseIdentity: { version, tag: `v${version}`, sourceSnapshot },
|
|
55
|
+
corpusSeed: { tag: corpusSeed.tag, archiveSha256: corpusSeed.archiveSha256,
|
|
56
|
+
archiveBytes: corpusSeed.archiveBytes, receiptSha256: baselineReceiptSha256 },
|
|
57
|
+
corpusCoverage: { sha256: sha256(Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`)), coverageGeneration: corpusCoverage.coverageGeneration },
|
|
58
|
+
generationLedger: { file: 'PUBLIC-RVF-GENERATIONS.json', sha256: sha256(generationLedgerBytes(ledger)),
|
|
59
|
+
bytes: generationLedgerBytes(ledger).length, storeCount: Object.keys(ledger.stores).length },
|
|
60
|
+
installedProjectionSchema: 2, policy: corpusCoverage.policy, enumerationReceipt: corpusCoverage.enumerationReceipt,
|
|
61
|
+
rows: corpusCoverage.rows, totals: corpusCoverage.totals,
|
|
62
|
+
};
|
|
63
|
+
const inventory = validatePublicInventory({ assetsDir: assets, coverage: releaseBase, ledger });
|
|
64
|
+
releaseBase.publicInventoryPartitionSha256 = inventory.partitionSha256;
|
|
65
|
+
releaseBase.releaseCoverageGeneration = releaseCoverageGenerationFor(releaseBase);
|
|
66
|
+
const out = path.resolve(outDir);
|
|
67
|
+
fs.mkdirSync(out, { recursive: true });
|
|
68
|
+
const corpusBytes = Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`);
|
|
69
|
+
const coverageBytes = Buffer.from(`${JSON.stringify(releaseBase, null, 2)}\n`);
|
|
70
|
+
fs.writeFileSync(path.join(out, 'CORPUS-COVERAGE.json'), corpusBytes);
|
|
71
|
+
fs.writeFileSync(path.join(out, 'COVERAGE.json'), coverageBytes);
|
|
72
|
+
fs.writeFileSync(path.join(out, 'PUBLIC-RVF-GENERATIONS.json'), generationLedgerBytes(ledger));
|
|
73
|
+
return releaseBase;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
77
|
+
try {
|
|
78
|
+
const corpusFile = path.resolve(arg('--corpus', path.join(ROOT, 'data', 'source-coverage.json')));
|
|
79
|
+
const corpus = readJson(corpusFile);
|
|
80
|
+
const projection = createReleaseProjection({ corpusCoverage: corpus,
|
|
81
|
+
assetsDir: arg('--assets'), outDir: arg('--out', 'release-evidence'), version: arg('--version'),
|
|
82
|
+
sourceSnapshot: arg('--source-snapshot'), baselineReceiptSha256: arg('--baseline-receipt-sha256'),
|
|
83
|
+
corpusSeed: { tag: arg('--seed-tag'), archiveSha256: arg('--seed-sha256'), archiveBytes: Number(arg('--seed-bytes')) } });
|
|
84
|
+
console.log(JSON.stringify({ ok: true, releaseCoverageGeneration: projection.releaseCoverageGeneration }));
|
|
85
|
+
} catch (error) { console.error(`[release-projection] ${error.message}`); process.exitCode = 1; }
|
|
86
|
+
}
|
|
@@ -3,8 +3,9 @@ import crypto from 'node:crypto';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { canonicalJson } from './coverage-integrity.mjs';
|
|
6
7
|
import {
|
|
7
|
-
pollObservation, RECEIPT_PREFIX,
|
|
8
|
+
pollObservation, RECEIPT_PREFIX, receiptDisposition, transactionIdFor,
|
|
8
9
|
} from './release-transaction.mjs';
|
|
9
10
|
|
|
10
11
|
const REPO = 'stuinfla/ruvnet-brain';
|
|
@@ -94,9 +95,7 @@ export function selectCurrentReleaseBytes({ remoteBytes, localBytes, generatedBy
|
|
|
94
95
|
return Buffer.from(bytes);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
export function liveReleaseProvider({
|
|
98
|
-
root = process.cwd(), candidateReceipt = null, publicationReceipt = null,
|
|
99
|
-
} = {}) {
|
|
98
|
+
export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
100
99
|
let releases = [];
|
|
101
100
|
let activeDraft = null;
|
|
102
101
|
const assetDigestCache = new Map();
|
|
@@ -215,7 +214,8 @@ export function liveReleaseProvider({
|
|
|
215
214
|
const legacySettled = [];
|
|
216
215
|
const pending = [];
|
|
217
216
|
for (const { receipt, release } of latestByTransaction.values()) {
|
|
218
|
-
|
|
217
|
+
const disposition = receiptDisposition(receipt);
|
|
218
|
+
if (['verified', 'closed-unsuccessful', 'legacy-closed'].includes(disposition)) continue;
|
|
219
219
|
// CONVERGENCE IS A FACT ABOUT THE CHANNELS, NOT ABOUT THE RECEIPT FORMAT (fixed 2026-08-07).
|
|
220
220
|
//
|
|
221
221
|
// This required `receipt.schemaVersion === 1`. Receipts are written as schemaVersion 2
|
|
@@ -230,7 +230,7 @@ export function liveReleaseProvider({
|
|
|
230
230
|
// all three hold, the transaction reached its goal whatever schema its receipts use. The
|
|
231
231
|
// check is not weakened — the schema clause was never load-bearing for that question, it
|
|
232
232
|
// just silently expired when the schema moved on.
|
|
233
|
-
const settled = release.draft === false
|
|
233
|
+
const settled = receipt.schemaVersion <= 2 && release.draft === false
|
|
234
234
|
&& receipt.identity?.version === npmLatest && receipt.identity?.tag === githubLatest;
|
|
235
235
|
if (settled) legacySettled.push(receipt.transactionId);
|
|
236
236
|
else pending.push(receipt);
|
|
@@ -318,6 +318,43 @@ export function liveReleaseProvider({
|
|
|
318
318
|
}
|
|
319
319
|
},
|
|
320
320
|
|
|
321
|
+
async materializePublicVerificationAggregate({ aggregate }) {
|
|
322
|
+
const anchor = activeDraft;
|
|
323
|
+
if (!anchor) throw new Error('no release anchor for public verification aggregate');
|
|
324
|
+
const release = hydratedRelease(releaseById(anchor.id));
|
|
325
|
+
const aggregateAsset = Buffer.from(canonicalJson(aggregate));
|
|
326
|
+
const signatureAsset = Buffer.from(aggregate.signature, 'base64');
|
|
327
|
+
const expected = {
|
|
328
|
+
'public-verification-aggregate.json': aggregateAsset,
|
|
329
|
+
'public-verification-aggregate.sig': signatureAsset,
|
|
330
|
+
};
|
|
331
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-public-aggregate-'));
|
|
332
|
+
try {
|
|
333
|
+
for (const [name, bytes] of Object.entries(expected)) {
|
|
334
|
+
const existing = release.assets?.find((asset) => asset.name === name);
|
|
335
|
+
if (existing) {
|
|
336
|
+
if (!assetBytes(existing).equals(bytes)) throw new Error(`public verification asset conflict: ${name}`);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const file = path.join(temp, name);
|
|
340
|
+
fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 });
|
|
341
|
+
command('gh', ['release', 'upload', anchor.tag, file, '--repo', REPO]);
|
|
342
|
+
}
|
|
343
|
+
const observed = hydratedRelease(releaseById(anchor.id));
|
|
344
|
+
for (const [name, bytes] of Object.entries(expected)) {
|
|
345
|
+
const asset = observed.assets?.find((item) => item.name === name);
|
|
346
|
+
if (!asset || !assetBytes(asset).equals(bytes)) throw new Error(`public verification asset readback differs: ${name}`);
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
aggregateSha256: aggregate.aggregateSha256,
|
|
350
|
+
aggregateAssetSha256: sha256(aggregateAsset),
|
|
351
|
+
signatureAssetSha256: sha256(signatureAsset),
|
|
352
|
+
};
|
|
353
|
+
} finally {
|
|
354
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
|
|
321
358
|
async readReceipt(draft, sequence) {
|
|
322
359
|
const name = `${RECEIPT_PREFIX}${String(sequence).padStart(4, '0')}.json`;
|
|
323
360
|
const release = hydratedRelease(releaseById((draft || activeDraft).id));
|
|
@@ -395,101 +432,5 @@ export function liveReleaseProvider({
|
|
|
395
432
|
if (current !== expected) throw new Error(`refusing compensation: npm latest is ${current}, expected ${expected}`);
|
|
396
433
|
command('npm', ['dist-tag', 'add', `${PACKAGE}@${prior}`, 'latest']);
|
|
397
434
|
},
|
|
398
|
-
async finalize(identity, receipt, hostVerifier) {
|
|
399
|
-
if (!candidateReceipt || !publicationReceipt) {
|
|
400
|
-
throw new Error('final convergence requires candidate and publication receipt paths');
|
|
401
|
-
}
|
|
402
|
-
// THE HOST VERDICT IS MEASURED, NOT DECLARED — and it is measured FIRST.
|
|
403
|
-
//
|
|
404
|
-
// finalize() used to accept the verifier as `_hostVerifier` and ignore it, then build
|
|
405
|
-
// `hosts = { verdict: 'PASS', … }` as a literal. Every release therefore asserted host
|
|
406
|
-
// convergence with no host verified, while tests/helpers/release-transaction-fixture.mjs:153
|
|
407
|
-
// called the seam faithfully — a fault-injection suite certifying wiring the real publisher
|
|
408
|
-
// never ran. Fable 5 and GPT-5.6-Sol independently made this their #1 finding.
|
|
409
|
-
//
|
|
410
|
-
// It runs BEFORE publication and sealing because the order encodes the meaning: you seal a
|
|
411
|
-
// release you have verified, not the reverse. It is also the cheapest way to fail — an
|
|
412
|
-
// unusable artifact stops here instead of after a 20-minute publish.
|
|
413
|
-
if (!hostVerifier || typeof hostVerifier.verify !== 'function') {
|
|
414
|
-
return { verdict: 'FAIL', hostVerifierError: 'no host verifier was supplied to finalize' };
|
|
415
|
-
}
|
|
416
|
-
// No `assets` override: stagedHostVerifier is constructed in release.mjs:238 with the exact
|
|
417
|
-
// staged bytes for this candidate and defaults to them (staged-host-verifier.mjs:57), so
|
|
418
|
-
// passing nothing is what keeps production verifying the artifact actually being shipped.
|
|
419
|
-
const verified = await hostVerifier.verify({ source: 'final', identity });
|
|
420
|
-
if (verified.verdict !== 'PASS') {
|
|
421
|
-
return {
|
|
422
|
-
verdict: 'FAIL',
|
|
423
|
-
hosts: { verdict: verified.verdict, verifier: { error: verified.error, fixtures: verified.fixtures } },
|
|
424
|
-
previousReceiptDigest: receipt.receiptDigest,
|
|
425
|
-
};
|
|
426
|
-
}
|
|
427
|
-
if (!fs.existsSync(path.resolve(root, publicationReceipt))) {
|
|
428
|
-
const publication = spawnSync(process.execPath, [
|
|
429
|
-
'scripts/publication-receipt.mjs', '--candidate', candidateReceipt, '--out', publicationReceipt,
|
|
430
|
-
], { cwd: root, encoding: 'utf8', timeout: 1_200_000 });
|
|
431
|
-
if (publication.error || publication.status !== 0) {
|
|
432
|
-
return { verdict: 'FAIL', publicationError: String(publication.stderr || publication.error?.message) };
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
const seal = spawnSync(process.execPath, [
|
|
436
|
-
'scripts/release-proof.mjs', '--candidate', candidateReceipt, '--publication', publicationReceipt,
|
|
437
|
-
], { cwd: root, encoding: 'utf8', timeout: 300_000 });
|
|
438
|
-
if (seal.error || seal.status !== 0) {
|
|
439
|
-
return { verdict: 'FAIL', sealError: String(seal.stderr || seal.error?.message) };
|
|
440
|
-
}
|
|
441
|
-
const publication = JSON.parse(fs.readFileSync(path.resolve(root, publicationReceipt), 'utf8'));
|
|
442
|
-
|
|
443
|
-
// `verified` was measured at the top of finalize, before publication and sealing.
|
|
444
|
-
const hosts = {
|
|
445
|
-
verdict: verified.verdict,
|
|
446
|
-
claudeOnly: publication.installed?.claudeOnly,
|
|
447
|
-
codexOnly: publication.installed?.codexOnly,
|
|
448
|
-
dual: publication.installed?.dual,
|
|
449
|
-
verifier: { artifactSha256: verified.artifactSha256, fixtures: verified.fixtures, error: verified.error },
|
|
450
|
-
};
|
|
451
|
-
const observed = publication.postPublicationChecks?.find(({ name }) => name === 'published-surface-probe');
|
|
452
|
-
const result = {
|
|
453
|
-
verdict: observed?.status === 'completed' && observed?.conclusion === 'success' ? 'PASS' : 'FAIL',
|
|
454
|
-
hosts, surface: observed, publicationReceipt, previousReceiptDigest: receipt.receiptDigest,
|
|
455
|
-
};
|
|
456
|
-
if (result.verdict === 'PASS') {
|
|
457
|
-
const currentRelease = path.join(path.dirname(path.resolve(root, publicationReceipt)), 'current-release.json');
|
|
458
|
-
const generatedCurrentRelease = Buffer.from(`${JSON.stringify({
|
|
459
|
-
schemaVersion: 1,
|
|
460
|
-
transactionId: receipt.transactionId,
|
|
461
|
-
version: identity.version,
|
|
462
|
-
tag: identity.tag,
|
|
463
|
-
candidateSha: identity.candidateSha,
|
|
464
|
-
verdict: 'PASS',
|
|
465
|
-
publicationReceiptSha256: sha256(fs.readFileSync(path.resolve(root, publicationReceipt))),
|
|
466
|
-
hosts,
|
|
467
|
-
surface: observed,
|
|
468
|
-
}, null, 2)}\n`);
|
|
469
|
-
let remote = hydratedRelease(releaseById(activeDraft.id)).assets
|
|
470
|
-
?.find((asset) => asset.name === 'current-release.json');
|
|
471
|
-
let currentReleaseBytes;
|
|
472
|
-
try {
|
|
473
|
-
currentReleaseBytes = selectCurrentReleaseBytes({
|
|
474
|
-
remoteBytes: remote ? assetBytes(remote) : null,
|
|
475
|
-
localBytes: !remote && fs.existsSync(currentRelease) ? fs.readFileSync(currentRelease) : null,
|
|
476
|
-
generatedBytes: generatedCurrentRelease,
|
|
477
|
-
identity: { transactionId: receipt.transactionId, version: identity.version, candidateSha: identity.candidateSha },
|
|
478
|
-
});
|
|
479
|
-
} catch (error) {
|
|
480
|
-
return { ...result, verdict: 'FAIL', currentReleaseError: error.message };
|
|
481
|
-
}
|
|
482
|
-
if (!fs.existsSync(currentRelease)) fs.writeFileSync(currentRelease, currentReleaseBytes, { flag: 'wx', mode: 0o600 });
|
|
483
|
-
if (!remote) {
|
|
484
|
-
command('gh', ['release', 'upload', activeDraft.tag, currentRelease, '--repo', REPO]);
|
|
485
|
-
remote = hydratedRelease(releaseById(activeDraft.id)).assets
|
|
486
|
-
?.find((asset) => asset.name === 'current-release.json');
|
|
487
|
-
}
|
|
488
|
-
if (!remote || sha256(assetBytes(remote)) !== sha256(currentReleaseBytes)) {
|
|
489
|
-
return { ...result, verdict: 'FAIL', currentReleaseError: 'public receipt verification failed' };
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
return result;
|
|
493
|
-
},
|
|
494
435
|
};
|
|
495
436
|
}
|
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
+
import { canonicalJson } from './coverage-integrity.mjs';
|
|
4
|
+
import { verifyPublicVerificationAggregate } from './public-verification-aggregate.mjs';
|
|
5
|
+
|
|
6
|
+
export { canonicalJson };
|
|
3
7
|
|
|
4
8
|
export const RECEIPT_PREFIX = 'release-transaction-';
|
|
5
|
-
export const
|
|
9
|
+
export const CURRENT_RECEIPT_SCHEMA_VERSION = 3;
|
|
10
|
+
export const SUCCESS_CLOSED_STATES = new Set(['install-verified']);
|
|
11
|
+
export const UNSUCCESSFUL_CLOSED_STATES = new Set(['aborted', 'abandoned', 'superseded']);
|
|
12
|
+
|
|
13
|
+
export function receiptDisposition(receipt) {
|
|
14
|
+
if (!receipt || !Number.isInteger(receipt.schemaVersion)) return 'invalid';
|
|
15
|
+
if (receipt.schemaVersion <= 2 && receipt.state === 'channels-converged') return 'legacy-closed';
|
|
16
|
+
if (receipt.schemaVersion >= 3 && SUCCESS_CLOSED_STATES.has(receipt.state)) return 'verified';
|
|
17
|
+
if (UNSUCCESSFUL_CLOSED_STATES.has(receipt.state)) return 'closed-unsuccessful';
|
|
18
|
+
if (receipt.schemaVersion >= 3 && receipt.state === 'channels-converged') return 'pending-public-verification';
|
|
19
|
+
return 'pending';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const isClosedReceipt = (receipt) => ['verified', 'closed-unsuccessful', 'legacy-closed']
|
|
23
|
+
.includes(receiptDisposition(receipt));
|
|
6
24
|
|
|
7
25
|
// `aborted` WAS UNREACHABLE, AND THAT BRICKED THE RELEASE RAIL (found 2026-08-07, issue #77).
|
|
8
26
|
//
|
|
9
|
-
// `aborted`
|
|
27
|
+
// `aborted` was historically declared terminal, but no state below listed it as a target — so it
|
|
10
28
|
// was a terminal state nothing could ever enter. Combined with `manual-intervention-required`
|
|
11
29
|
// (which has NO outgoing transitions and is NOT terminal), an interrupted release had exactly two
|
|
12
30
|
// destinations and both were permanent non-terminal dead ends.
|
|
@@ -33,7 +51,7 @@ export const ALLOWED_TRANSITIONS = Object.freeze({
|
|
|
33
51
|
'npm-candidate-staged': ['remote-materialization-intent', 'manual-intervention-required', ...ABORTABLE],
|
|
34
52
|
'remote-materialization-intent': ['prepared', 'manual-intervention-required', ...ABORTABLE],
|
|
35
53
|
prepared: ['github-promote-intent', 'manual-intervention-required', ...ABORTABLE],
|
|
36
|
-
'github-promote-intent': ['github-promoted-nonlatest', 'manual-intervention-required', ...ABORTABLE],
|
|
54
|
+
'github-promote-intent': ['github-promoted-nonlatest', 'compensation-intent', 'manual-intervention-required', ...ABORTABLE],
|
|
37
55
|
'github-promoted-nonlatest': ['npm-promote-intent', 'manual-intervention-required', ...ABORTABLE],
|
|
38
56
|
'npm-promote-intent': ['npm-promoted', 'compensation-intent', 'manual-intervention-required', ...ABORTABLE],
|
|
39
57
|
// `defaults-promoted` added 2026-08-07: when GitHub is ALREADY latest at the moment npm is
|
|
@@ -51,18 +69,37 @@ export const ALLOWED_TRANSITIONS = Object.freeze({
|
|
|
51
69
|
'defaults-promoted': ['finalize-intent', 'manual-intervention-required', ...ABORTABLE],
|
|
52
70
|
'finalize-intent': ['channels-converged', 'manual-intervention-required', ...ABORTABLE],
|
|
53
71
|
'manual-intervention-required': [],
|
|
54
|
-
'channels-converged': [],
|
|
72
|
+
'channels-converged': ['install-verified', 'abandoned', 'superseded'],
|
|
73
|
+
'install-verified': [],
|
|
74
|
+
abandoned: [],
|
|
75
|
+
superseded: [],
|
|
55
76
|
aborted: [],
|
|
56
77
|
});
|
|
57
78
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
/**
|
|
80
|
+
* THE DIGEST SERIALISER MUST AGREE WITH THE ONE THAT WRITES THE FILE.
|
|
81
|
+
*
|
|
82
|
+
* It did not, and it silently corrupted the terminal receipt of EVERY successful release. Measured
|
|
83
|
+
* 2026-08-20 on v4.0.36 and v4.0.90-dev — both `channels-converged` receipts, both digest=BAD, both
|
|
84
|
+
* with the same missing key:
|
|
85
|
+
*
|
|
86
|
+
* canonicalJson({a: undefined}) -> {"a":undefined} key KEPT (and not even valid JSON)
|
|
87
|
+
* JSON.stringify({a: undefined}) -> {} key DROPPED
|
|
88
|
+
*
|
|
89
|
+
* The converge observation carries optional fields — `hosts.verifier.error`, `claudeOnly`,
|
|
90
|
+
* `codexOnly`, `dual` — and on a clean run `verified.error` is `undefined`. So the digest was
|
|
91
|
+
* computed over `…"error":undefined…` while the bytes written omitted `error` entirely: a digest
|
|
92
|
+
* over a shape that can never be read back. `runReleaseTransaction` appends a receipt, reads it
|
|
93
|
+
* back and verifies it, so the publish then died with `release receipt digest mismatch` AFTER npm
|
|
94
|
+
* and GitHub had both been promoted — the release shipped and the rail reported failure.
|
|
95
|
+
*
|
|
96
|
+
* Proven by reconstruction: restoring `observation.hosts.verifier.error = undefined` on the stored
|
|
97
|
+
* receipt makes `digestReceipt` reproduce the stored digest exactly.
|
|
98
|
+
*
|
|
99
|
+
* Arrays have the same asymmetry (`JSON.stringify([undefined])` is `[null]`), so both are matched
|
|
100
|
+
* here. This does not change the digest of any receipt that never held an undefined value, which is
|
|
101
|
+
* every receipt that currently verifies.
|
|
102
|
+
*/
|
|
66
103
|
export const receiptPayload = (receipt) => {
|
|
67
104
|
const { signature: _signature, receiptDigest: _digest, ...payload } = receipt;
|
|
68
105
|
return canonicalJson(payload);
|
|
@@ -86,6 +123,8 @@ export const transactionIdFor = (identity) => crypto.createHash('sha256').update
|
|
|
86
123
|
bundleSha256: identity.bundleSha256,
|
|
87
124
|
bundleSignatureSha256: identity.bundleSignatureSha256 || null,
|
|
88
125
|
bundleDigestSha256: identity.bundleDigestSha256 || null,
|
|
126
|
+
...(identity.corpusSeedSha256 ? { corpusSeedSha256: identity.corpusSeedSha256 } : {}),
|
|
127
|
+
...(identity.generationLedgerSha256 ? { generationLedgerSha256: identity.generationLedgerSha256 } : {}),
|
|
89
128
|
})).digest('hex');
|
|
90
129
|
|
|
91
130
|
export function signReceipt(receipt, privateKey) {
|
|
@@ -97,7 +136,8 @@ export function signReceipt(receipt, privateKey) {
|
|
|
97
136
|
}
|
|
98
137
|
|
|
99
138
|
export function verifyReceipt(receipt, publicKey) {
|
|
100
|
-
if (!receipt || ![1, 2].includes(receipt.schemaVersion)
|
|
139
|
+
if (!receipt || ![1, 2, CURRENT_RECEIPT_SCHEMA_VERSION].includes(receipt.schemaVersion)
|
|
140
|
+
|| !/^[a-f0-9]{64}$/.test(receipt.transactionId || '')) {
|
|
101
141
|
throw new Error('invalid release transaction receipt');
|
|
102
142
|
}
|
|
103
143
|
const { signature, receiptDigest, ...unsigned } = receipt;
|
|
@@ -121,13 +161,20 @@ export function validateReceiptChain(receipts, identity, publicKey) {
|
|
|
121
161
|
if ((receipt.previousReceiptDigest || null) !== (previous?.receiptDigest || null)) {
|
|
122
162
|
throw new Error('release receipt chain conflict');
|
|
123
163
|
}
|
|
164
|
+
if (previous && receipt.schemaVersion >= 3 && receiptDisposition(previous) === 'legacy-closed') {
|
|
165
|
+
throw new Error('legacy-closed release receipt cannot be promoted into schema 3');
|
|
166
|
+
}
|
|
167
|
+
if (previous && receipt.schemaVersion >= 3
|
|
168
|
+
&& !ALLOWED_TRANSITIONS[previous.state]?.includes(receipt.state)) {
|
|
169
|
+
throw new Error(`illegal release transition ${previous.state} -> ${receipt.state}`);
|
|
170
|
+
}
|
|
124
171
|
previous = receipt;
|
|
125
172
|
}
|
|
126
173
|
return sorted;
|
|
127
174
|
}
|
|
128
175
|
|
|
129
176
|
const stateReceipt = ({ identity, prior, state, observation = {}, privateKey }) => signReceipt({
|
|
130
|
-
schemaVersion:
|
|
177
|
+
schemaVersion: CURRENT_RECEIPT_SCHEMA_VERSION,
|
|
131
178
|
transactionId: transactionIdFor(identity),
|
|
132
179
|
sequence: prior ? prior.sequence + 1 : 0,
|
|
133
180
|
previousReceiptDigest: prior?.receiptDigest || null,
|
|
@@ -175,11 +222,15 @@ export function reduceReleaseState({ lastReceipt, snapshot, identity, prior }) {
|
|
|
175
222
|
}
|
|
176
223
|
if (npmB && snapshot.github?.draft === true) return { action: 'compensate-npm' };
|
|
177
224
|
if (npmB && githubLatest) {
|
|
178
|
-
if (lastReceipt.state === '
|
|
225
|
+
if (lastReceipt.state === 'install-verified') {
|
|
179
226
|
return githubPublished && assetsExact && candidateExact
|
|
180
|
-
&& snapshot.publicReceiptExact && snapshot.publicHostsExact
|
|
181
227
|
? { action: 'complete' }
|
|
182
|
-
: { action: 'manual', reason: '
|
|
228
|
+
: { action: 'manual', reason: 'verified public channels drifted' };
|
|
229
|
+
}
|
|
230
|
+
if (lastReceipt.state === 'channels-converged') {
|
|
231
|
+
return githubPublished && assetsExact && candidateExact
|
|
232
|
+
? { action: 'await-install-verification' }
|
|
233
|
+
: { action: 'manual', reason: 'channel-converged state drifted' };
|
|
183
234
|
}
|
|
184
235
|
return { action: 'finalize' };
|
|
185
236
|
}
|
|
@@ -229,7 +280,7 @@ export async function pollObservation(read, predicate, {
|
|
|
229
280
|
throw new Error(`visibility deadline exceeded after ${now() - started}ms: ${detail}`);
|
|
230
281
|
}
|
|
231
282
|
|
|
232
|
-
export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey
|
|
283
|
+
export async function runReleaseTransaction({ identity, assets, adapter, privateKey, publicKey }) {
|
|
233
284
|
const expectedId = transactionIdFor(identity);
|
|
234
285
|
const discovered = await adapter.discover(identity);
|
|
235
286
|
const competing = discovered.pending?.filter((item) => item.transactionId !== expectedId) || [];
|
|
@@ -241,7 +292,8 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
241
292
|
const draft = discovered.matchingDrafts?.[0] || await adapter.createDraft(identity);
|
|
242
293
|
const append = async (state, observation = {}) => {
|
|
243
294
|
const recoveryCompensation = state === 'compensation-intent' && current
|
|
244
|
-
&& !
|
|
295
|
+
&& !isClosedReceipt(current) && current.state !== 'manual-intervention-required'
|
|
296
|
+
&& current.state !== 'channels-converged';
|
|
245
297
|
if (current && !ALLOWED_TRANSITIONS[current.state]?.includes(state) && current.state !== state
|
|
246
298
|
&& !recoveryCompensation) {
|
|
247
299
|
throw new Error(`illegal release transition ${current.state} -> ${state}`);
|
|
@@ -270,10 +322,10 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
270
322
|
const snapshot = await adapter.observeSnapshot(identity, draft, {
|
|
271
323
|
forceAssets: current.state === 'finalize-intent' || current.state === 'channels-converged',
|
|
272
324
|
});
|
|
273
|
-
if (current.state === 'channels-converged') {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
throw new Error(`
|
|
325
|
+
if (current.state === 'channels-converged' || current.state === 'install-verified') {
|
|
326
|
+
const disposition = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
|
|
327
|
+
if (['complete', 'await-install-verification'].includes(disposition.action)) return current;
|
|
328
|
+
throw new Error(`release state drift: ${disposition.reason || disposition.action}`);
|
|
277
329
|
}
|
|
278
330
|
// A process may die after the provider side effect and before its observation receipt. Rebuild
|
|
279
331
|
// that missing receipt from fresh state before asking the reducer for the next command.
|
|
@@ -304,11 +356,11 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
304
356
|
await transition('compensated', { npm: snapshot.npm, github: snapshot.github, recovered: true });
|
|
305
357
|
throw new Error('npm compensation recovered; resume the same transaction');
|
|
306
358
|
}
|
|
307
|
-
if (current.state === 'finalize-intent' && snapshot
|
|
308
|
-
&& npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
|
|
359
|
+
if (current.state === 'finalize-intent' && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
|
|
309
360
|
&& githubIsB(snapshot, identity) && snapshot.github?.published && snapshot.github?.latest
|
|
310
361
|
&& snapshot.github?.assetsExact) {
|
|
311
|
-
return append('channels-converged', { verdict: '
|
|
362
|
+
return append('channels-converged', { verdict: 'PUBLISHED_NOT_VERIFIED', recovered: true,
|
|
363
|
+
npm: snapshot.npm, github: snapshot.github });
|
|
312
364
|
}
|
|
313
365
|
const decision = reduceReleaseState({ lastReceipt: current, snapshot, identity, prior });
|
|
314
366
|
const stageRecord = {
|
|
@@ -323,6 +375,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
323
375
|
? `::notice title=Release stage ${decision.action}::${JSON.stringify(stageRecord)}`
|
|
324
376
|
: JSON.stringify(stageRecord));
|
|
325
377
|
if (decision.action === 'complete') return current;
|
|
378
|
+
if (decision.action === 'await-install-verification') return current;
|
|
326
379
|
if (decision.action === 'manual') {
|
|
327
380
|
await transition('manual-intervention-required', { reason: decision.reason, snapshot });
|
|
328
381
|
throw new Error(`release requires manual intervention: ${decision.reason}`);
|
|
@@ -415,32 +468,15 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
415
468
|
await transition('defaults-promoted', { npm: snapshot.npm, github: snapshot.github });
|
|
416
469
|
}
|
|
417
470
|
await transition('finalize-intent');
|
|
418
|
-
const final = await adapter.finalize(identity, current, hostVerifier);
|
|
419
|
-
// SAY WHICH CONVERGENCE FAILED, AND WHY (2026-08-07). This threw the bare sentence
|
|
420
|
-
// `final release convergence failed` while `finalize` had already returned exactly the reason
|
|
421
|
-
// — hostVerifierError, hosts.verifier.error, publicationError or sealError — and the throw
|
|
422
|
-
// discarded every one of them. That is the same defect that let `spawnSync ENOBUFS` masquerade
|
|
423
|
-
// as `staged GitHub payload mismatch` for three days: an error that reports its category and
|
|
424
|
-
// withholds its evidence. Publication had ALREADY succeeded on both channels here, so the
|
|
425
|
-
// operator is reading this line while npm and GitHub are correct, with nothing to act on.
|
|
426
|
-
if (final.verdict !== 'PASS') {
|
|
427
|
-
const why = final.hostVerifierError
|
|
428
|
-
|| final.hosts?.verifier?.error
|
|
429
|
-
|| final.publicationError
|
|
430
|
-
|| final.sealError
|
|
431
|
-
|| `verdict=${final.verdict ?? '(none)'}`;
|
|
432
|
-
const fixtures = final.hosts?.verifier?.fixtures;
|
|
433
|
-
const detail = fixtures ? ` | fixtures: ${JSON.stringify(fixtures).slice(0, 400)}` : '';
|
|
434
|
-
throw new Error(`final release convergence failed: ${why}${detail}`);
|
|
435
|
-
}
|
|
436
471
|
const reobserved = await adapter.observeSnapshot(identity, draft, { forceAssets: true });
|
|
437
472
|
if (!(npmLatestIsB(reobserved, identity) && reobserved.github?.latest && githubIsB(reobserved, identity))) {
|
|
438
|
-
throw new Error('provider defaults drifted
|
|
473
|
+
throw new Error('provider defaults drifted before channel convergence');
|
|
439
474
|
}
|
|
440
|
-
if (!reobserved.github?.assetsExact || !reobserved
|
|
441
|
-
throw new Error('
|
|
475
|
+
if (!reobserved.github?.assetsExact || !npmCandidateExact(reobserved, identity)) {
|
|
476
|
+
throw new Error('provider artifacts drifted before channel convergence');
|
|
442
477
|
}
|
|
443
|
-
return append('channels-converged',
|
|
478
|
+
return append('channels-converged', { verdict: 'PUBLISHED_NOT_VERIFIED',
|
|
479
|
+
npm: reobserved.npm, github: reobserved.github });
|
|
444
480
|
}
|
|
445
481
|
}
|
|
446
482
|
throw new Error('release reducer exceeded its bounded transition count');
|
|
@@ -450,7 +486,9 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
|
|
|
450
486
|
if (!authorized) throw new Error('release abort requires explicit human authorization');
|
|
451
487
|
const chain = validateReceiptChain(receipts, identity, publicKey);
|
|
452
488
|
const current = chain.at(-1);
|
|
453
|
-
if (!current ||
|
|
489
|
+
if (!current || isClosedReceipt(current) || !ALLOWED_TRANSITIONS[current.state]?.includes('aborted')) {
|
|
490
|
+
throw new Error('release transaction is not abortable');
|
|
491
|
+
}
|
|
454
492
|
const snapshot = await adapter.observeSnapshot(identity);
|
|
455
493
|
const prior = chain[0]?.observation?.prior;
|
|
456
494
|
if (!prior || snapshot.npm?.latestVersion === identity.version || snapshot.github?.latest === true
|
|
@@ -464,3 +502,81 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
|
|
|
464
502
|
await adapter.appendReceipt(null, receipt, `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`);
|
|
465
503
|
return receipt;
|
|
466
504
|
}
|
|
505
|
+
|
|
506
|
+
export async function finalizeReleaseTransaction({
|
|
507
|
+
identity,
|
|
508
|
+
aggregate,
|
|
509
|
+
adapter,
|
|
510
|
+
privateKey,
|
|
511
|
+
publicKey,
|
|
512
|
+
aggregatePublicKey,
|
|
513
|
+
} = {}) {
|
|
514
|
+
if (!adapter || typeof adapter.discover !== 'function' || typeof adapter.observeSnapshot !== 'function'
|
|
515
|
+
|| typeof adapter.materializePublicVerificationAggregate !== 'function'
|
|
516
|
+
|| typeof adapter.appendReceipt !== 'function' || typeof adapter.readReceipt !== 'function') {
|
|
517
|
+
throw new Error('public verification finalizer adapter is incomplete');
|
|
518
|
+
}
|
|
519
|
+
const expectedAggregateIdentity = {
|
|
520
|
+
...aggregate?.identity,
|
|
521
|
+
sourceSha: identity?.candidateSha,
|
|
522
|
+
version: identity?.version,
|
|
523
|
+
tag: identity?.tag,
|
|
524
|
+
artifactSha256: identity?.packageSha256,
|
|
525
|
+
bundleSha256: identity?.bundleSha256,
|
|
526
|
+
payloadId: identity?.payloadId,
|
|
527
|
+
releaseTransactionId: transactionIdFor(identity),
|
|
528
|
+
};
|
|
529
|
+
verifyPublicVerificationAggregate(aggregate, aggregatePublicKey, expectedAggregateIdentity);
|
|
530
|
+
const discovered = await adapter.discover(identity);
|
|
531
|
+
const chain = validateReceiptChain(discovered.receipts || [], identity, publicKey);
|
|
532
|
+
const current = chain.at(-1);
|
|
533
|
+
if (!current) throw new Error('release transaction has no receipt chain');
|
|
534
|
+
if (current.state === 'install-verified') {
|
|
535
|
+
if (current.observation?.publicVerification?.aggregateSha256 !== aggregate.aggregateSha256) {
|
|
536
|
+
throw new Error('release is already install-verified by a different aggregate');
|
|
537
|
+
}
|
|
538
|
+
return current;
|
|
539
|
+
}
|
|
540
|
+
if (current.schemaVersion < 3 || current.state !== 'channels-converged') {
|
|
541
|
+
throw new Error(`public verification finalizer requires schema-3 channels-converged, found ${current.state}`);
|
|
542
|
+
}
|
|
543
|
+
const snapshot = await adapter.observeSnapshot(identity, discovered.matchingDrafts?.[0], { forceAssets: true });
|
|
544
|
+
if (snapshot.readError || !npmLatestIsB(snapshot, identity) || !npmCandidateExact(snapshot, identity)
|
|
545
|
+
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || snapshot.github?.latest !== true
|
|
546
|
+
|| snapshot.github?.assetsExact !== true) throw new Error('public channels drifted before install verification');
|
|
547
|
+
const materialized = await adapter.materializePublicVerificationAggregate({ identity, aggregate });
|
|
548
|
+
const aggregateAssetSha256 = crypto.createHash('sha256').update(canonicalJson(aggregate)).digest('hex');
|
|
549
|
+
const signatureAssetSha256 = crypto.createHash('sha256').update(Buffer.from(aggregate.signature, 'base64')).digest('hex');
|
|
550
|
+
if (materialized?.aggregateSha256 !== aggregate.aggregateSha256
|
|
551
|
+
|| materialized?.aggregateAssetSha256 !== aggregateAssetSha256
|
|
552
|
+
|| materialized?.signatureAssetSha256 !== signatureAssetSha256) {
|
|
553
|
+
throw new Error('materialized public verification aggregate differs from signed evidence');
|
|
554
|
+
}
|
|
555
|
+
const receipt = stateReceipt({
|
|
556
|
+
identity,
|
|
557
|
+
prior: current,
|
|
558
|
+
state: 'install-verified',
|
|
559
|
+
observation: {
|
|
560
|
+
verdict: 'INSTALL_VERIFIED',
|
|
561
|
+
publicVerification: {
|
|
562
|
+
aggregateSha256: aggregate.aggregateSha256,
|
|
563
|
+
aggregateAssetSha256,
|
|
564
|
+
signatureAssetSha256,
|
|
565
|
+
lanes: aggregate.metrics.leaves,
|
|
566
|
+
recallAt10: aggregate.metrics.recallAt10,
|
|
567
|
+
deltaCitationRate: aggregate.metrics.deltaCitationRate,
|
|
568
|
+
reviewReceipts: aggregate.reviews,
|
|
569
|
+
},
|
|
570
|
+
npm: snapshot.npm,
|
|
571
|
+
github: snapshot.github,
|
|
572
|
+
},
|
|
573
|
+
privateKey,
|
|
574
|
+
});
|
|
575
|
+
const name = `${RECEIPT_PREFIX}${String(receipt.sequence).padStart(4, '0')}.json`;
|
|
576
|
+
await adapter.appendReceipt(discovered.matchingDrafts?.[0] || null, receipt, name);
|
|
577
|
+
const persisted = await adapter.readReceipt(discovered.matchingDrafts?.[0] || null, receipt.sequence);
|
|
578
|
+
verifyReceipt(persisted, publicKey);
|
|
579
|
+
if (canonicalJson(persisted) !== canonicalJson(receipt)) throw new Error('install-verified receipt readback differs');
|
|
580
|
+
validateReceiptChain([...chain, persisted], identity, publicKey);
|
|
581
|
+
return persisted;
|
|
582
|
+
}
|