ruvnet-brain 4.3.9 → 4.3.10
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 +26 -9
- package/bin/install.mjs +646 -366
- package/bin/nightly-refresh.mjs +115 -0
- package/kb/brain-profile.mjs +118 -32
- package/kb/lifecycle-evidence-retention.mjs +237 -0
- package/kb/refresh-run.mjs +367 -0
- package/kb/retrieval-result.mjs +39 -0
- package/kb/update-storage-transaction.mjs +439 -0
- package/package.json +11 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +2 -165
- package/plugin/hooks/hook-contracts.json +4 -65
- package/plugin/hooks/hooks.json +2 -208
- package/plugin/scripts/adr-currency-gate.mjs +29 -19
- package/plugin/scripts/capability-registry.mjs +10 -83
- package/plugin/scripts/codex-hook-adapter.mjs +62 -9
- package/plugin/scripts/codex-hook-wrapper.mjs +19 -1
- package/plugin/scripts/continuation-gate.mjs +30 -28
- package/plugin/scripts/continuation-objective.mjs +34 -0
- package/plugin/scripts/development-maintenance.mjs +49 -0
- package/plugin/scripts/hook-shim.mjs +3 -0
- package/plugin/scripts/learn-flush.mjs +6 -1
- package/plugin/scripts/lesson-gate.mjs +5 -2
- package/plugin/scripts/lesson-presentation.mjs +5 -0
- package/plugin/scripts/md-stamp.mjs +109 -6
- package/plugin/scripts/memory-doctor.mjs +3 -7
- package/plugin/scripts/nightly-controller.mjs +9 -27
- package/plugin/scripts/nightly-scheduler.mjs +472 -0
- package/plugin/scripts/project-progression-contract.mjs +4 -4
- package/plugin/scripts/project-progression-store.mjs +35 -6
- package/plugin/scripts/ruflo-bin.mjs +20 -0
- package/plugin/scripts/session-snapshot-contract.mjs +6 -1
- package/plugin/scripts/version-bump-gate.sh +14 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +15 -23
- package/scripts/build-bundle.mjs +8 -0
- package/scripts/build-primer.mjs +2 -3
- package/scripts/candidate-host-evidence.mjs +59 -41
- package/scripts/ci/build-fixture-kb.mjs +59 -2
- package/scripts/ci/mutate-hook-timeout.mjs +24 -47
- package/scripts/ci/stranger-scenario.mjs +13 -28
- package/scripts/claims-verify.mjs +219 -21
- package/scripts/console-engine.mjs +4 -18
- package/scripts/console-runtime-identity.mjs +6 -0
- package/scripts/development-maintenance.mjs +47 -0
- package/scripts/development-push-check.mjs +35 -0
- package/scripts/distill-project.mjs +24 -47
- package/scripts/doc-currency.mjs +91 -19
- package/scripts/git-hooks/pre-push +4 -162
- package/scripts/health-repair.mjs +22 -4
- package/scripts/hook-retirement-check.mjs +22 -0
- package/scripts/host-install-matrix.mjs +125 -61
- package/scripts/integration-evidence.mjs +11 -6
- package/scripts/learning-replay-fixture.mjs +23 -2
- package/scripts/nightly-two-run-proof.mjs +415 -0
- package/scripts/nightly-watchdog.mjs +17 -2
- package/scripts/npm-invocation.mjs +20 -0
- package/scripts/prepublication-evidence.mjs +42 -6
- package/scripts/primer-grounding.mjs +20 -0
- package/scripts/product-integrity-contract.mjs +22 -2
- package/scripts/public-verification-abandon.mjs +101 -0
- package/scripts/public-verification-aggregate.mjs +53 -19
- package/scripts/public-verification-finalizer.mjs +8 -2
- package/scripts/public-verification-lane.mjs +63 -8
- package/scripts/publication-receipt.mjs +109 -45
- package/scripts/published-surface-probe.mjs +10 -2
- package/scripts/qa-contract.mjs +57 -0
- package/scripts/qa-lanes.mjs +40 -0
- package/scripts/qa-runner.mjs +54 -64
- package/scripts/qe/ux-suite.mjs +10 -34
- package/scripts/qualified-candidate-check.mjs +124 -0
- package/scripts/release-abort-stale.mjs +2 -2
- package/scripts/release-projection.mjs +20 -1
- package/scripts/release-qualification-contract.mjs +87 -0
- package/scripts/release-qualification.mjs +135 -0
- package/scripts/release-transaction.mjs +81 -1
- package/scripts/remedy-registry.mjs +6 -13
- package/scripts/retrieval-canary.mjs +91 -18
- package/scripts/selfcheck.mjs +12 -4
- package/scripts/snapshot-freshness.mjs +58 -0
- package/scripts/stack-sync.mjs +41 -43
- package/scripts/staged-host-verifier.mjs +63 -14
- package/scripts/wired-check.mjs +31 -2
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { assessLifecycleEvidence } from '../kb/lifecycle-evidence-retention.mjs';
|
|
10
|
+
import { managedStorageInventory } from '../kb/update-storage-transaction.mjs';
|
|
11
|
+
import { REQUIRED_REFRESH_PHASES, validateCurrentRunPhaseExecution } from '../kb/refresh-run.mjs';
|
|
12
|
+
import { npmInvocation } from './npm-invocation.mjs';
|
|
13
|
+
import { validateRefreshReceiptEnvelope } from '../plugin/scripts/nightly-scheduler.mjs';
|
|
14
|
+
import { validateCoverageDirectory, validateCoverageLedger } from '../plugin/scripts/coverage-integrity.mjs';
|
|
15
|
+
|
|
16
|
+
const PUBLIC_KEY = fs.readFileSync(new URL('../keys/ruvnet-brain-signing.pub.pem', import.meta.url), 'utf8');
|
|
17
|
+
|
|
18
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
19
|
+
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const result = {};
|
|
23
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
24
|
+
const token = argv[i];
|
|
25
|
+
if (!token.startsWith('--') || !argv[i + 1] || argv[i + 1].startsWith('--')) {
|
|
26
|
+
throw new Error(`invalid argument: ${token}`);
|
|
27
|
+
}
|
|
28
|
+
result[token.slice(2)] = argv[++i];
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function run(command, args, options = {}) {
|
|
34
|
+
const result = spawnSync(command, args, { encoding: 'utf8', ...options });
|
|
35
|
+
if (result.error || result.status !== 0) {
|
|
36
|
+
throw new Error(`${command} ${args.join(' ')} failed: ${result.error?.message
|
|
37
|
+
|| String(result.stderr || result.stdout || `exit ${result.status}`).trim()}`);
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function canonical(value) {
|
|
43
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
44
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort()
|
|
45
|
+
.map((key) => [key, canonical(value[key])]));
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function receiptDigest(receipt) {
|
|
50
|
+
return sha256(Buffer.from(JSON.stringify(canonical(receipt))));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function stageExactBundle({ bundlePath, packageRoot } = {}) {
|
|
54
|
+
const sourcePath = path.resolve(bundlePath || '');
|
|
55
|
+
let sourceStat;
|
|
56
|
+
try { sourceStat = fs.lstatSync(sourcePath); } catch { /* handled below */ }
|
|
57
|
+
if (!sourcePath.endsWith('.zip') || !sourceStat?.isFile() || sourceStat.isSymbolicLink()) {
|
|
58
|
+
throw new Error('bundle must be a regular candidate bundle .zip file');
|
|
59
|
+
}
|
|
60
|
+
const packageDir = path.resolve(packageRoot || '');
|
|
61
|
+
if (!fs.lstatSync(packageDir).isDirectory()) throw new Error('installed package root is not a directory');
|
|
62
|
+
const bytes = fs.readFileSync(sourcePath);
|
|
63
|
+
const dist = path.join(packageDir, 'dist');
|
|
64
|
+
const stagedPath = path.join(dist, 'ruvnet-brain.zip');
|
|
65
|
+
fs.mkdirSync(dist, { recursive: false });
|
|
66
|
+
fs.copyFileSync(sourcePath, stagedPath, fs.constants.COPYFILE_EXCL);
|
|
67
|
+
return { sourcePath, stagedPath, bytes: bytes.length, sha256: sha256(bytes) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function nightlyReceipts(brainHome, identity) {
|
|
71
|
+
const dir = path.join(brainHome, 'refresh-runs');
|
|
72
|
+
if (!fs.existsSync(dir)) return [];
|
|
73
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith('.json')).sort().map((name) => {
|
|
74
|
+
const file = path.join(dir, name);
|
|
75
|
+
const stat = fs.lstatSync(file);
|
|
76
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`untrusted refresh evidence: ${file}`);
|
|
77
|
+
const receipt = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
78
|
+
return { file, receipt };
|
|
79
|
+
}).filter(({ receipt }) => receipt.kind === 'ruvnet-brain-refresh-run'
|
|
80
|
+
&& receipt.action === 'nightly' && receipt.schedulerIdentity === identity);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Dependency seams here exercise orchestration without touching a developer's native scheduler.
|
|
84
|
+
// Only runNightlyTwoRunProof writes proof, and it supplies the real installed adapter and commands.
|
|
85
|
+
export async function triggerNativeRun({ scheduler, registration, platform, env, brainHome, kbDir,
|
|
86
|
+
timeoutMs, command = run, receipts = nightlyReceipts, pause = sleep, now = Date.now }) {
|
|
87
|
+
const identity = registration.identity;
|
|
88
|
+
if (!/^com\.ruvnet\.brain-update\.proof-[A-Za-z0-9._-]+$/.test(identity || '')) {
|
|
89
|
+
throw new Error('native proof requires a unique proof scheduler identity');
|
|
90
|
+
}
|
|
91
|
+
if (!['darwin', 'linux', 'win32'].includes(platform)) throw new Error(`unsupported native scheduler platform: ${platform}`);
|
|
92
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error('proof timeout must be a positive integer');
|
|
93
|
+
const options = { platform, env, brainHome, kbDir, identity, proofTick: platform === 'linux',
|
|
94
|
+
...(platform === 'linux' ? { proofAt: now() + 60_000 } : {}) };
|
|
95
|
+
const before = scheduler.schedulerStatus(options);
|
|
96
|
+
if (before.state !== 'off') throw new Error(`proof scheduler identity is not absent: ${before.evidence}`);
|
|
97
|
+
const beforeIds = new Set(receipts(brainHome, identity).map(({ receipt }) => receipt.runId));
|
|
98
|
+
let attempted = false;
|
|
99
|
+
let removed = false;
|
|
100
|
+
const remove = () => {
|
|
101
|
+
const result = scheduler.removeScheduler(options);
|
|
102
|
+
if (!result.ok) throw new Error(`proof scheduler cleanup failed: ${result.why}`);
|
|
103
|
+
const status = scheduler.schedulerStatus(options);
|
|
104
|
+
if (status.state !== 'off') throw new Error(`proof scheduler absence unverified: ${status.evidence}`);
|
|
105
|
+
removed = true;
|
|
106
|
+
};
|
|
107
|
+
try {
|
|
108
|
+
attempted = true;
|
|
109
|
+
const installed = scheduler.installScheduler(registration, { ...options, pathValue: env.PATH });
|
|
110
|
+
if (!installed.ok) throw new Error(`native scheduler unavailable: ${installed.why}`);
|
|
111
|
+
const status = scheduler.schedulerStatus(options);
|
|
112
|
+
if (status.state !== 'on') throw new Error(`proof scheduler is not on: ${status.evidence}`);
|
|
113
|
+
if (platform === 'darwin') command('launchctl', ['kickstart', '-k', `gui/${process.getuid()}/${identity}`]);
|
|
114
|
+
if (platform === 'win32') command('schtasks', ['/Run', '/TN', identity]);
|
|
115
|
+
// Cron itself must deliver the Linux trigger. Never execute its command directly as proof.
|
|
116
|
+
const startedAt = now();
|
|
117
|
+
let observedStart = false;
|
|
118
|
+
while (now() - startedAt < timeoutMs) {
|
|
119
|
+
const created = receipts(brainHome, identity)
|
|
120
|
+
.filter(({ receipt }) => !beforeIds.has(receipt.runId));
|
|
121
|
+
if (created.length > 1) throw new Error(`scheduler produced ${created.length} refresh runs; expected exactly one`);
|
|
122
|
+
if (created.length === 1) {
|
|
123
|
+
observedStart = true;
|
|
124
|
+
// Stop the minute cadence at the first receipt, including RUNNING, so long updates do
|
|
125
|
+
// not launch again. Removing a cron row does not kill its already running child.
|
|
126
|
+
if (platform === 'linux' && !removed) remove();
|
|
127
|
+
if (['SUCCEEDED', 'FAILED', 'ABANDONED'].includes(created[0].receipt.status)) return { ...created[0],
|
|
128
|
+
trigger: { kind: { darwin: 'launchctl-kickstart', linux: 'cron-tick', win32: 'schtasks-run' }[platform], identity } };
|
|
129
|
+
}
|
|
130
|
+
if (platform === 'linux' && !observedStart && now() - startedAt >= 120_000) {
|
|
131
|
+
throw new Error('cron did not start the owned proof job within two minutes; check that the cron service is running');
|
|
132
|
+
}
|
|
133
|
+
await pause(1000);
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`timed out waiting for ${identity} terminal refresh receipt`);
|
|
136
|
+
} finally {
|
|
137
|
+
if (attempted && !removed) remove();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function validateTwoRunEvidence({ first, second, inventoryBefore, inventoryAfterFirst,
|
|
142
|
+
inventoryAfterSecond, retention, validateEnvelope, identity }) {
|
|
143
|
+
const failures = [];
|
|
144
|
+
for (const [label, receipt] of [['first', first], ['second', second]]) {
|
|
145
|
+
const envelope = validateEnvelope(receipt);
|
|
146
|
+
if (!envelope.ok) failures.push(`${label} run: ${envelope.why}`);
|
|
147
|
+
const execution = validateCurrentRunPhaseExecution(receipt);
|
|
148
|
+
failures.push(...execution.failures.map((failure) => `${label} run: ${failure}`));
|
|
149
|
+
if (receipt.schedulerIdentity !== identity) failures.push(`${label} run scheduler identity differs`);
|
|
150
|
+
}
|
|
151
|
+
failures.push(...validateStorageEvidence({ first, second, inventoryBefore, inventoryAfterFirst, inventoryAfterSecond, retention }));
|
|
152
|
+
return { ok: failures.length === 0, failures };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validateStorageEvidence({ first, second, inventoryBefore, inventoryAfterFirst, inventoryAfterSecond, retention }) {
|
|
156
|
+
const failures = [];
|
|
157
|
+
if (first.runId === second.runId) failures.push('two scheduler invocations reused one run ID');
|
|
158
|
+
if (!Number.isFinite(Date.parse(first.finishedAt)) || !Number.isFinite(Date.parse(second.startedAt))
|
|
159
|
+
|| Date.parse(second.startedAt) < Date.parse(first.finishedAt)) failures.push('second run did not start after first completion');
|
|
160
|
+
if (!['applied', 'noop'].includes(first.terminalVerdict)) failures.push('first run did not converge');
|
|
161
|
+
if (second.terminalVerdict !== 'noop') failures.push('second run was not a no-op');
|
|
162
|
+
const secondDelta = second.detail?.storageDelta;
|
|
163
|
+
if (!secondDelta || secondDelta.redundantCopyCount !== 0
|
|
164
|
+
|| !Number.isSafeInteger(secondDelta.additionalFullCorpusCopyDelta)
|
|
165
|
+
|| secondDelta.additionalFullCorpusCopyDelta > 0) {
|
|
166
|
+
failures.push('second run did not prove a zero-redundancy storage delta');
|
|
167
|
+
}
|
|
168
|
+
for (const [label, inventory] of [['before', inventoryBefore], ['after first', inventoryAfterFirst],
|
|
169
|
+
['after second', inventoryAfterSecond]]) {
|
|
170
|
+
if (inventory.additionalFullCorpusCopyCount !== 0) failures.push(`${label} inventory has redundant corpus copies`);
|
|
171
|
+
if (!Number.isSafeInteger(inventory.totalManagedBytes) || inventory.totalManagedBytes < 0) failures.push(`${label} inventory has no measured byte count`);
|
|
172
|
+
}
|
|
173
|
+
if (inventoryAfterSecond.totalManagedBytes > inventoryAfterFirst.totalManagedBytes
|
|
174
|
+
+ Math.max(0, retention.after?.bytes - retention.before?.bytes)) {
|
|
175
|
+
failures.push('second no-op increased managed storage outside retained lifecycle evidence');
|
|
176
|
+
}
|
|
177
|
+
if (retention.withinBudget !== true || retention.unsafe?.length) failures.push('lifecycle evidence is not within policy');
|
|
178
|
+
if (![retention.before?.bytes, retention.after?.bytes].every((bytes) => Number.isSafeInteger(bytes) && bytes >= 0)) failures.push('lifecycle evidence byte measurements are missing');
|
|
179
|
+
return failures;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validateInstalledRun(run, proof) {
|
|
183
|
+
const receipt = run.receipt;
|
|
184
|
+
const failures = [];
|
|
185
|
+
const envelope = validateRefreshReceiptEnvelope(receipt);
|
|
186
|
+
if (!envelope.ok) failures.push(envelope.why);
|
|
187
|
+
const projection = run.installedCoverage;
|
|
188
|
+
const coverage = JSON.parse(projection?.raw || 'null');
|
|
189
|
+
if (!coverage || projection.sha256 !== sha256(Buffer.from(projection.raw))) throw new Error('installed coverage bytes differ');
|
|
190
|
+
const ledger = validateCoverageLedger(coverage);
|
|
191
|
+
failures.push(...ledger.failures);
|
|
192
|
+
if (coverage.kind !== 'ruvnet-brain-release-coverage' || coverage.releaseIdentity?.sourceSnapshot !== proof.sourceSha
|
|
193
|
+
|| coverage.releaseIdentity?.version !== proof.candidate.version) failures.push('installed ReleaseCoverage is not the exact candidate');
|
|
194
|
+
const start = Date.parse(receipt.startedAt), end = Date.parse(receipt.finishedAt);
|
|
195
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) failures.push('installed update time window is invalid');
|
|
196
|
+
if (!Number.isFinite(Date.parse(projection.observedAt)) || Date.parse(projection.observedAt) < end
|
|
197
|
+
|| Date.parse(projection.observedAt) > Date.parse(proof.observedAt)) failures.push('installed coverage observation is outside run completion');
|
|
198
|
+
if (JSON.stringify(receipt.phases?.map(({ phase }) => phase)) !== JSON.stringify(REQUIRED_REFRESH_PHASES)) failures.push('installed update phases are incomplete');
|
|
199
|
+
const performed = new Set(['update', 'host-convergence', 'cleanup']);
|
|
200
|
+
for (const phase of receipt.phases || []) {
|
|
201
|
+
const execution = phase.evidence?.execution;
|
|
202
|
+
const at = Date.parse(phase.at);
|
|
203
|
+
if (phase.status !== 'PASS' || !Number.isFinite(at) || at < start || at > end) failures.push(`${phase.phase} has invalid run evidence`);
|
|
204
|
+
if (performed.has(phase.phase) || (phase.phase === 'local-overlay-restoration' && execution?.kind === 'executed')) {
|
|
205
|
+
if (execution?.kind !== 'executed' || execution.runId !== receipt.runId) failures.push(`${phase.phase} was not executed by this run`);
|
|
206
|
+
} else if (phase.phase === 'local-overlay-restoration') {
|
|
207
|
+
if (execution?.kind !== 'not-executed' || phase.evidence?.restoredStores !== 0) failures.push('unexecuted private overlay restoration is not empty');
|
|
208
|
+
} else if (execution?.kind !== 'imported-release' || execution.sourceSnapshot !== proof.sourceSha
|
|
209
|
+
|| execution.upstreamFreshness !== 'UNKNOWN') failures.push(`${phase.phase} imported corpus provenance is invalid`);
|
|
210
|
+
}
|
|
211
|
+
const evidence = Object.fromEntries((receipt.phases || []).map(({ phase, evidence }) => [phase, evidence]));
|
|
212
|
+
if (evidence['coverage-generation']?.coverageSha256 !== projection.sha256
|
|
213
|
+
|| evidence['coverage-generation']?.releaseCoverageGeneration !== coverage.releaseCoverageGeneration
|
|
214
|
+
|| evidence['source-enumeration']?.sourceObservationSha256 !== coverage.sourceObservationSha256
|
|
215
|
+
|| evidence['source-enumeration']?.rows !== coverage.totals.rows
|
|
216
|
+
|| evidence['source-enumeration']?.terminal !== true
|
|
217
|
+
|| evidence.ingestion?.eligibleCurrent !== coverage.rows.filter((row) => row.disposition === 'eligible' && row.status === 'CURRENT').length
|
|
218
|
+
|| evidence.ingestion?.storeCount !== coverage.generationLedger.storeCount
|
|
219
|
+
|| evidence['bundle-assembly']?.version !== coverage.releaseIdentity.version
|
|
220
|
+
|| evidence['bundle-assembly']?.sourceSnapshot !== proof.sourceSha
|
|
221
|
+
|| evidence['generation-ledger-reconciliation']?.sha256 !== coverage.generationLedger.sha256) failures.push('imported corpus evidence differs from installed projection');
|
|
222
|
+
const r = proof.registration;
|
|
223
|
+
const expectedIdentity = { schedulerIdentity: proof.identity, registrationPath: r.recordPath,
|
|
224
|
+
nodePath: r.nodePath, runnerPath: r.runnerPath, runnerSha256: r.runnerSha256, argv: [] };
|
|
225
|
+
const absolute = (value) => typeof value === 'string' && (proof.platform === 'win32' ? path.win32.isAbsolute(value) && !value.startsWith('/') : path.posix.isAbsolute(value));
|
|
226
|
+
if (!/^[a-f0-9]{64}$/.test(r.runnerSha256 || '') || !absolute(r.recordPath)
|
|
227
|
+
|| !absolute(r.nodePath) || !absolute(r.runnerPath)
|
|
228
|
+
|| !receipt.executableIdentity || receiptDigest(receipt.executableIdentity) !== receiptDigest(expectedIdentity)
|
|
229
|
+
|| receipt.schedulerIdentity !== proof.identity) failures.push('native executable identity differs');
|
|
230
|
+
if (run.trigger?.identity !== proof.identity || run.trigger?.kind !== {
|
|
231
|
+
darwin: 'launchctl-kickstart', linux: 'cron-tick', win32: 'schtasks-run',
|
|
232
|
+
}[proof.platform]) failures.push('native scheduler trigger differs');
|
|
233
|
+
return failures;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function validateNightlyProofReceipt(receipt, { platform, version, packageSha256, bundleSha256,
|
|
237
|
+
sourceSha, workflowRunId, publicKey = PUBLIC_KEY } = {}) {
|
|
238
|
+
const failures = [];
|
|
239
|
+
if (!receipt || receipt.schemaVersion !== 1 || receipt.kind !== 'ruvnet-brain-native-two-run-nightly-proof') {
|
|
240
|
+
return { ok: false, failures: ['invalid native nightly proof envelope'] };
|
|
241
|
+
}
|
|
242
|
+
const { receiptSha256: declaredDigest, ...body } = receipt;
|
|
243
|
+
if (receipt.scope !== 'installed-update' || receipt.upstreamFreshness !== 'UNKNOWN') failures.push('native proof scope is not installed-update with unknown upstream freshness');
|
|
244
|
+
try {
|
|
245
|
+
if (!crypto.verify(null, Buffer.from(bundleSha256 || '', 'hex'), publicKey,
|
|
246
|
+
Buffer.from(receipt.candidate?.bundle?.signatureBase64 || '', 'base64'))) failures.push('public bundle signature is invalid');
|
|
247
|
+
} catch { failures.push('public bundle signature is invalid'); }
|
|
248
|
+
if (declaredDigest !== receiptDigest(body)) failures.push('native proof receipt digest differs');
|
|
249
|
+
if (!['darwin', 'linux', 'win32'].includes(platform) || receipt.platform !== platform) failures.push('native proof platform differs');
|
|
250
|
+
if (!version || receipt.candidate?.version !== version) failures.push('native proof version differs');
|
|
251
|
+
if (!/^[a-f0-9]{64}$/.test(packageSha256 || '') || receipt.candidate?.sha256 !== packageSha256) failures.push('native proof package digest differs');
|
|
252
|
+
if (!/^[a-f0-9]{64}$/.test(bundleSha256 || '') || receipt.candidate?.bundle?.sha256 !== bundleSha256) failures.push('native proof bundle digest differs');
|
|
253
|
+
if (!/^[a-f0-9]{40}$/.test(sourceSha || '') || receipt.sourceSha !== sourceSha) failures.push('native proof source SHA differs');
|
|
254
|
+
if (!/^\d+$/.test(String(workflowRunId || '')) || String(receipt.workflowRunId || '') !== String(workflowRunId)) failures.push('native proof workflow run differs');
|
|
255
|
+
if (!/^com\.ruvnet\.brain-update\.proof-[A-Za-z0-9._-]+$/.test(receipt.identity || '')) failures.push('native proof identity is not isolated');
|
|
256
|
+
const observedAt = Date.parse(receipt.observedAt);
|
|
257
|
+
if (!Number.isFinite(observedAt) || observedAt > Date.now() + 60_000) failures.push('native proof observation time is invalid');
|
|
258
|
+
if (!Array.isArray(receipt.runs) || receipt.runs.length !== 2) failures.push('native proof requires exactly two runs');
|
|
259
|
+
else {
|
|
260
|
+
for (const run of receipt.runs) {
|
|
261
|
+
if (!run.receipt || run.receiptSha256 !== receiptDigest(run.receipt)
|
|
262
|
+
|| run.runId !== run.receipt.runId || run.terminalVerdict !== run.receipt.terminalVerdict) {
|
|
263
|
+
failures.push('embedded run identity or digest differs');
|
|
264
|
+
}
|
|
265
|
+
const finishedAt = Date.parse(run.receipt?.finishedAt);
|
|
266
|
+
if (!Number.isFinite(finishedAt) || finishedAt > observedAt) failures.push('native run completion is not bounded by observation');
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
failures.push(...receipt.runs.flatMap((run) => validateInstalledRun(run, receipt)));
|
|
270
|
+
if (Date.parse(receipt.runs[0].installedCoverage.observedAt) > Date.parse(receipt.runs[1].receipt.startedAt)) {
|
|
271
|
+
failures.push('first installed coverage was not observed before the second run');
|
|
272
|
+
}
|
|
273
|
+
failures.push(...validateStorageEvidence({ first: receipt.runs[0].receipt, second: receipt.runs[1].receipt,
|
|
274
|
+
inventoryBefore: receipt.inventory.before, inventoryAfterFirst: receipt.inventory.afterFirst,
|
|
275
|
+
inventoryAfterSecond: receipt.inventory.afterSecond, retention: receipt.retention }));
|
|
276
|
+
} catch (error) { failures.push(`incomplete native proof evidence: ${error.message}`); }
|
|
277
|
+
}
|
|
278
|
+
return { ok: failures.length === 0, failures };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function runNightlyTwoRunProof({ packagePath, bundlePath, out, timeoutMs = 30 * 60 * 1000,
|
|
282
|
+
keepRoot = false, sourceSha = process.env.GITHUB_SHA || null,
|
|
283
|
+
workflowRunId = process.env.GITHUB_RUN_ID || null, signaturePath = `${bundlePath}.sig` } = {}) {
|
|
284
|
+
if (!['darwin', 'linux', 'win32'].includes(process.platform)) throw new Error(`unsupported native scheduler platform: ${process.platform}`);
|
|
285
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error('proof timeout must be a positive integer');
|
|
286
|
+
const candidate = path.resolve(packagePath || '');
|
|
287
|
+
const output = path.resolve(out || '');
|
|
288
|
+
if (!candidate.endsWith('.tgz') || !fs.lstatSync(candidate).isFile()) throw new Error('candidate must be a regular .tgz file');
|
|
289
|
+
const bundle = path.resolve(bundlePath || '');
|
|
290
|
+
let bundleStat;
|
|
291
|
+
try { bundleStat = fs.lstatSync(bundle); } catch { /* handled below */ }
|
|
292
|
+
if (!bundle.endsWith('.zip') || !bundleStat?.isFile() || bundleStat.isSymbolicLink()) {
|
|
293
|
+
throw new Error('bundle must be a regular candidate bundle .zip file');
|
|
294
|
+
}
|
|
295
|
+
const bundleSha256 = sha256(fs.readFileSync(bundle));
|
|
296
|
+
const signatureBase64 = fs.readFileSync(signaturePath).toString('base64');
|
|
297
|
+
if (!crypto.verify(null, Buffer.from(bundleSha256, 'hex'), PUBLIC_KEY, Buffer.from(signatureBase64, 'base64'))) {
|
|
298
|
+
throw new Error('public candidate bundle signature is invalid');
|
|
299
|
+
}
|
|
300
|
+
const packageSha256 = sha256(fs.readFileSync(candidate));
|
|
301
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ruvnet-nightly-proof-'));
|
|
302
|
+
const home = path.join(root, 'home');
|
|
303
|
+
const brainHome = path.join(root, 'brain');
|
|
304
|
+
const kbDir = path.join(brainHome, 'kb');
|
|
305
|
+
const prefix = path.join(root, 'prefix');
|
|
306
|
+
const npmCache = path.join(root, 'npm-cache');
|
|
307
|
+
const logPath = path.join(kbDir, 'update.log');
|
|
308
|
+
const identity = `com.ruvnet.brain-update.proof-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
309
|
+
const tempDir = path.join(root, 'tmp');
|
|
310
|
+
const env = {
|
|
311
|
+
...Object.fromEntries(['SystemRoot', 'SYSTEMROOT', 'ComSpec', 'COMSPEC', 'PATHEXT']
|
|
312
|
+
.filter((key) => process.env[key] !== undefined).map((key) => [key, process.env[key]])),
|
|
313
|
+
PATH: [path.dirname(process.execPath), process.env.PATH || ''].filter(Boolean).join(path.delimiter),
|
|
314
|
+
HOME: home,
|
|
315
|
+
USERPROFILE: home,
|
|
316
|
+
RUVNET_BRAIN_HOME: brainHome,
|
|
317
|
+
RUVNET_BRAIN_KB: kbDir,
|
|
318
|
+
npm_config_cache: npmCache,
|
|
319
|
+
TEMP: tempDir,
|
|
320
|
+
TMP: tempDir,
|
|
321
|
+
NO_COLOR: '1',
|
|
322
|
+
};
|
|
323
|
+
fs.mkdirSync(home, { recursive: true });
|
|
324
|
+
fs.mkdirSync(tempDir);
|
|
325
|
+
let scheduler;
|
|
326
|
+
let registration;
|
|
327
|
+
let completed = false;
|
|
328
|
+
try {
|
|
329
|
+
const npm = npmInvocation(['install', '--prefix', prefix, '--ignore-scripts', '--no-audit', '--no-fund', candidate]);
|
|
330
|
+
run(npm.executable, npm.args, { env });
|
|
331
|
+
const packageRoot = path.join(prefix, 'node_modules', 'ruvnet-brain');
|
|
332
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
333
|
+
const stagedBundle = stageExactBundle({ bundlePath: bundle, packageRoot });
|
|
334
|
+
run(process.execPath, [path.join(packageRoot, 'bin', 'install.mjs'), '--yes', '--no-nightly-prompt'], {
|
|
335
|
+
env, stdio: 'inherit', encoding: undefined,
|
|
336
|
+
});
|
|
337
|
+
scheduler = await import(`${pathToFileURL(path.join(packageRoot, 'plugin', 'scripts', 'nightly-scheduler.mjs')).href}?proof=${Date.now()}`);
|
|
338
|
+
registration = scheduler.installNightlyRunner({ brainHome,
|
|
339
|
+
source: path.join(packageRoot, 'bin', 'nightly-refresh.mjs'), nodePath: process.execPath, identity, env,
|
|
340
|
+
packageTarget: { spec: candidate, sha256: packageSha256 },
|
|
341
|
+
bundleTarget: { spec: stagedBundle.sourcePath, sha256: stagedBundle.sha256 } });
|
|
342
|
+
const inventoryBefore = managedStorageInventory(kbDir);
|
|
343
|
+
const kick = () => triggerNativeRun({ scheduler, registration, platform: process.platform,
|
|
344
|
+
env, brainHome, kbDir, timeoutMs });
|
|
345
|
+
const observeCoverage = () => {
|
|
346
|
+
const checked = validateCoverageDirectory(kbDir, { expectedVersion: packageJson.version, expectedSourceSnapshot: sourceSha });
|
|
347
|
+
if (!checked.valid) throw new Error(`installed candidate ReleaseCoverage failed: ${checked.failures.join('; ')}`);
|
|
348
|
+
const raw = fs.readFileSync(path.join(kbDir, 'COVERAGE.json'), 'utf8');
|
|
349
|
+
return { raw, sha256: sha256(Buffer.from(raw)), observedAt: new Date().toISOString() };
|
|
350
|
+
};
|
|
351
|
+
const firstEntry = await kick();
|
|
352
|
+
firstEntry.installedCoverage = observeCoverage();
|
|
353
|
+
const inventoryAfterFirst = managedStorageInventory(kbDir);
|
|
354
|
+
const secondEntry = await kick();
|
|
355
|
+
secondEntry.installedCoverage = observeCoverage();
|
|
356
|
+
const inventoryAfterSecond = managedStorageInventory(kbDir);
|
|
357
|
+
const retention = assessLifecycleEvidence({ brainHome, kbDir });
|
|
358
|
+
const receipt = {
|
|
359
|
+
schemaVersion: 1,
|
|
360
|
+
kind: 'ruvnet-brain-native-two-run-nightly-proof',
|
|
361
|
+
scope: 'installed-update',
|
|
362
|
+
upstreamFreshness: 'UNKNOWN',
|
|
363
|
+
observedAt: new Date().toISOString(),
|
|
364
|
+
platform: process.platform,
|
|
365
|
+
sourceSha,
|
|
366
|
+
workflowRunId: workflowRunId == null ? null : String(workflowRunId),
|
|
367
|
+
identity,
|
|
368
|
+
candidate: { path: candidate, sha256: packageSha256, version: packageJson.version,
|
|
369
|
+
bundle: { path: stagedBundle.sourcePath, sha256: stagedBundle.sha256, bytes: stagedBundle.bytes, signatureBase64 } },
|
|
370
|
+
registration: { recordPath: registration.recordPath, runnerSha256: registration.runnerSha256,
|
|
371
|
+
nodePath: registration.nodePath, runnerPath: registration.runnerPath,
|
|
372
|
+
packageTarget: registration.packageTarget, bundleTarget: registration.bundleTarget },
|
|
373
|
+
runs: [firstEntry, secondEntry].map(({ receipt: row, installedCoverage, trigger }) => ({ runId: row.runId,
|
|
374
|
+
terminalVerdict: row.terminalVerdict, receiptSha256: receiptDigest(row), finishedAt: row.finishedAt,
|
|
375
|
+
receipt: row, installedCoverage, trigger })),
|
|
376
|
+
inventory: { before: inventoryBefore, afterFirst: inventoryAfterFirst, afterSecond: inventoryAfterSecond },
|
|
377
|
+
retention,
|
|
378
|
+
log: { path: logPath, exists: fs.existsSync(logPath),
|
|
379
|
+
sha256: fs.existsSync(logPath) ? sha256(fs.readFileSync(logPath)) : null },
|
|
380
|
+
};
|
|
381
|
+
receipt.receiptSha256 = receiptDigest(receipt);
|
|
382
|
+
const validation = validateNightlyProofReceipt(receipt, { platform: process.platform, version: packageJson.version,
|
|
383
|
+
packageSha256, bundleSha256, sourceSha, workflowRunId });
|
|
384
|
+
delete receipt.receiptSha256;
|
|
385
|
+
receipt.validation = validation;
|
|
386
|
+
receipt.receiptSha256 = receiptDigest(receipt);
|
|
387
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
388
|
+
fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx' });
|
|
389
|
+
if (!validation.ok) throw new Error(`native two-run proof failed: ${validation.failures.join('; ')}`);
|
|
390
|
+
completed = true;
|
|
391
|
+
return receipt;
|
|
392
|
+
} finally {
|
|
393
|
+
// Each trigger owns cleanup and verifies absence before returning. Failed runs retain their
|
|
394
|
+
// root so an active child or failed cleanup never loses the evidence/runner it still needs.
|
|
395
|
+
if (completed && !keepRoot) fs.rmSync(root, { recursive: true, force: true });
|
|
396
|
+
if (!completed) console.error(`Native proof diagnostics retained at ${root}`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
401
|
+
try {
|
|
402
|
+
const args = parseArgs(process.argv.slice(2));
|
|
403
|
+
const receipt = await runNightlyTwoRunProof({ packagePath: args.package, bundlePath: args.bundle, out: args.out,
|
|
404
|
+
timeoutMs: args['timeout-ms'] ? Number(args['timeout-ms']) : undefined,
|
|
405
|
+
sourceSha: args['source-sha'] || process.env.GITHUB_SHA,
|
|
406
|
+
signaturePath: args.signature,
|
|
407
|
+
workflowRunId: args['workflow-run-id'] || process.env.GITHUB_RUN_ID,
|
|
408
|
+
keepRoot: args['keep-root'] === 'true' });
|
|
409
|
+
console.log(JSON.stringify({ ok: true, receipt: path.resolve(args.out),
|
|
410
|
+
receiptSha256: receipt.receiptSha256, runs: receipt.runs }, null, 2));
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error(error.message);
|
|
413
|
+
process.exitCode = 1;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
@@ -38,6 +38,7 @@ import path from 'node:path';
|
|
|
38
38
|
import os from 'node:os';
|
|
39
39
|
import { spawnSync, execFileSync } from 'node:child_process';
|
|
40
40
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
41
|
+
import { NIGHTLY_LABEL, schedulerStatus } from '../plugin/scripts/nightly-scheduler.mjs';
|
|
41
42
|
|
|
42
43
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
43
44
|
const REGISTRY = process.env.WATCHDOG_REGISTRY || path.join(ROOT, 'config', 'scheduled-jobs.json');
|
|
@@ -158,6 +159,17 @@ export function checkAll(now, { registry = REGISTRY, loaded = loadedLabels(), hb
|
|
|
158
159
|
});
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
export function productSchedulerVerdict(status) {
|
|
163
|
+
const base = { label: NIGHTLY_LABEL, what: 'Refreshes the installed Brain corpus and converges every detected host',
|
|
164
|
+
schedule: 'daily 03:47', maxAgeHours: 30, required: true };
|
|
165
|
+
if (status?.state !== 'on') return { ...base, state: MISSING, detail: status?.evidence || 'scheduler is not registered' };
|
|
166
|
+
const run = status.runHealth || { state: 'never-ran', evidence: 'No nightly refresh receipt exists yet.' };
|
|
167
|
+
const mapped = run.state === 'ok' || run.state === 'running' ? OK
|
|
168
|
+
: run.state === 'stale' ? STALE
|
|
169
|
+
: run.state === 'never-ran' ? NEVER_RAN : FAILING;
|
|
170
|
+
return { ...base, state: mapped, detail: run.evidence };
|
|
171
|
+
}
|
|
172
|
+
|
|
161
173
|
const loadState = () => { try { return JSON.parse(fs.readFileSync(STATE, 'utf8')); } catch { return {}; } };
|
|
162
174
|
const saveState = (s) => { fs.mkdirSync(path.dirname(STATE), { recursive: true }); fs.writeFileSync(STATE, JSON.stringify(s, null, 2)); };
|
|
163
175
|
|
|
@@ -183,7 +195,10 @@ async function push(title, body, priority) {
|
|
|
183
195
|
async function main() {
|
|
184
196
|
const json = process.argv.includes('--json');
|
|
185
197
|
const quiet = process.argv.includes('--quiet');
|
|
186
|
-
const
|
|
198
|
+
const brainHome = process.env.RUVNET_BRAIN_HOME || path.join(os.homedir(), '.cache', 'ruvnet-brain');
|
|
199
|
+
const results = [...checkAll(new Date()), productSchedulerVerdict(schedulerStatus({
|
|
200
|
+
brainHome, kbDir: process.env.RUVNET_BRAIN_KB || path.join(brainHome, 'kb'),
|
|
201
|
+
}))];
|
|
187
202
|
const bad = results.filter((r) => r.state !== OK);
|
|
188
203
|
|
|
189
204
|
const prev = loadState();
|
|
@@ -219,7 +234,7 @@ async function main() {
|
|
|
219
234
|
? `${bad.length} of ${results.length} job(s) are NOT confirmed working. NEVER-RAN means the schedule has never fired — it does not mean "probably fine".`
|
|
220
235
|
: `All ${results.length} jobs produced a fresh, successful receipt.`);
|
|
221
236
|
}
|
|
222
|
-
process.exit(bad.length ? 1 : 0);
|
|
237
|
+
process.exit(bad.length || undelivered.size ? 1 : 0);
|
|
223
238
|
}
|
|
224
239
|
|
|
225
240
|
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) await main();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Run the already installed npm CLI with Node, including on Windows where npm.cmd
|
|
5
|
+
// cannot be exec'd directly. Never install another npm or concatenate a shell command.
|
|
6
|
+
export function npmInvocation(args, { env = process.env, nodePath = process.execPath } = {}) {
|
|
7
|
+
const candidates = [env.npm_execpath, path.join(path.dirname(nodePath), 'node_modules/npm/bin/npm-cli.js'),
|
|
8
|
+
path.join(path.dirname(nodePath), 'npm')].filter(Boolean);
|
|
9
|
+
for (const candidate of candidates) {
|
|
10
|
+
try {
|
|
11
|
+
const entry = fs.realpathSync(candidate);
|
|
12
|
+
const root = path.dirname(path.dirname(entry));
|
|
13
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
14
|
+
if (manifest.name !== 'npm' || typeof manifest.bin?.npm !== 'string'
|
|
15
|
+
|| fs.realpathSync(path.resolve(root, manifest.bin.npm)) !== entry || !fs.statSync(entry).isFile()) continue;
|
|
16
|
+
return { executable: nodePath, args: [entry, ...args] };
|
|
17
|
+
} catch { /* only a positively identified installed npm entry is accepted */ }
|
|
18
|
+
}
|
|
19
|
+
throw new Error('acceptance requires installed npm beside Node or an explicit npm_execpath pointing to its CLI');
|
|
20
|
+
}
|
|
@@ -5,7 +5,10 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { aggregateEvidence } from './release-evidence-aggregate.mjs';
|
|
7
7
|
import { canonicalJson } from './release-transaction.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { RELEASE_QUALIFICATION_POLICY } from './integration-evidence.mjs';
|
|
9
|
+
import { readCandidateRetrieval } from './staged-host-verifier.mjs';
|
|
10
|
+
import { validateRetrievalCanaryReceipt } from './retrieval-canary.mjs';
|
|
11
|
+
import { payloadIdFor } from './release-payload.mjs';
|
|
9
12
|
|
|
10
13
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
11
14
|
|
|
@@ -44,10 +47,13 @@ export function buildPrepublicationEvidence({
|
|
|
44
47
|
manifestFile,
|
|
45
48
|
payloadProofFile,
|
|
46
49
|
hostFile,
|
|
50
|
+
runtimeCensusFile,
|
|
47
51
|
ciFile,
|
|
48
52
|
integrationFile,
|
|
49
53
|
uxFiles,
|
|
50
54
|
strangerFile,
|
|
55
|
+
planFile,
|
|
56
|
+
coverageFile,
|
|
51
57
|
}) {
|
|
52
58
|
if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^\d+\.\d+\.\d+$/.test(version || '') || !Number.isSafeInteger(runId) || runId <= 0) {
|
|
53
59
|
throw new Error('prepublication identity is malformed');
|
|
@@ -60,10 +66,30 @@ export function buildPrepublicationEvidence({
|
|
|
60
66
|
throw new Error('candidate manifest identity mismatch');
|
|
61
67
|
}
|
|
62
68
|
if (!/^[a-f0-9]{64}$/.test(payload.payloadId || '')) throw new Error('payload identity is malformed');
|
|
69
|
+
if (payload.payloadId !== payloadIdFor(manifest)) throw new Error('candidate manifest payload identity mismatch');
|
|
70
|
+
const retrieval = readCandidateRetrieval({ manifest, planFile, coverageFile });
|
|
71
|
+
|
|
72
|
+
const runtimeCensus = readJson(runtimeCensusFile);
|
|
73
|
+
const census = runtimeCensus.value;
|
|
74
|
+
const result = census.result;
|
|
75
|
+
const artifacts = result?.provenance?.artifacts;
|
|
76
|
+
const candidate = result?.provenance?.candidate;
|
|
77
|
+
const memberHash = (role) => manifest.members.find((member) => member.role === role)?.sha256;
|
|
78
|
+
if (census.schemaVersion !== 1 || census.kind !== 'ruvnet-brain-runtime-census-evidence'
|
|
79
|
+
|| census.sourceSha !== sha || census.version !== version || census.runId !== runId
|
|
80
|
+
|| census.payloadId !== payload.payloadId || census.payloadManifestSha256 !== sha256(manifestBytes)
|
|
81
|
+
|| result?.status !== 'PASS' || result.diagnostic !== false || result.qualification !== 'staged-candidate'
|
|
82
|
+
|| result.provenance?.projectionVerified !== true || candidate?.sourceSha !== sha || candidate?.version !== version
|
|
83
|
+
|| candidate?.coverageSha256 !== sha256(fs.readFileSync(coverageFile))
|
|
84
|
+
|| artifacts?.payloadId !== payload.payloadId || artifacts.claimSourceSha !== sha
|
|
85
|
+
|| artifacts.packageSha256 !== memberHash('npm') || artifacts.bundleSha256 !== memberHash('bundle')
|
|
86
|
+
|| !(artifacts.sidecarsVerified > 0) || !(artifacts.surfacesVerified > 0)) {
|
|
87
|
+
throw new Error('runtime census receipt is not an exact candidate-bound PASS');
|
|
88
|
+
}
|
|
63
89
|
|
|
64
90
|
const host = readJson(hostFile);
|
|
65
91
|
if (host.value.schemaVersion !== 1 || host.value.sha !== sha || host.value.payloadId !== payload.payloadId
|
|
66
|
-
||
|
|
92
|
+
|| host.value.artifactSha256 !== retrieval.artifactSha256 || host.value.leaves?.length !== 3) {
|
|
67
93
|
throw new Error('candidate host evidence identity or shape mismatch');
|
|
68
94
|
}
|
|
69
95
|
requireExactSet(host.value.leaves.map(({ name }) => name), ['claude-only', 'codex-only', 'dual-host'], 'candidate host leaves');
|
|
@@ -77,10 +103,11 @@ export function buildPrepublicationEvidence({
|
|
|
77
103
|
|| leaf.artifactSha256 !== host.value.artifactSha256) {
|
|
78
104
|
throw new Error(`candidate host leaf is not an exact PASS: ${leaf.name || '(missing)'}`);
|
|
79
105
|
}
|
|
106
|
+
validateRetrievalCanaryReceipt(leaf.retrieval, { plan: retrieval.plan });
|
|
80
107
|
}
|
|
81
108
|
|
|
82
109
|
const ci = readJson(ciFile);
|
|
83
|
-
const requiredCiJobs = ['candidate-preflight', '
|
|
110
|
+
const requiredCiJobs = ['candidate-preflight', 'release-acceptance-linux', 'release-acceptance-windows', 'release-acceptance-macos', 'release-qe'];
|
|
84
111
|
if (ci.value.schemaVersion !== 1 || ci.value.kind !== 'ruvnet-brain-candidate-ci-evidence'
|
|
85
112
|
|| ci.value.sourceSha !== sha || ci.value.version !== version || ci.value.payloadId !== payload.payloadId
|
|
86
113
|
|| ci.value.payloadManifestSha256 !== sha256(manifestBytes) || ci.value.workflow !== 'ci'
|
|
@@ -89,6 +116,10 @@ export function buildPrepublicationEvidence({
|
|
|
89
116
|
}
|
|
90
117
|
requireExactSet(ci.value.jobs?.map(({ name }) => name) || [], requiredCiJobs, 'candidate CI jobs');
|
|
91
118
|
if (ci.value.jobs.some(({ conclusion }) => conclusion !== 'success')) throw new Error('candidate CI receipt contains a non-success job');
|
|
119
|
+
requireExactSet(ci.value.acceptanceReceipts?.map(({ platform }) => platform) || [], ['linux', 'macos', 'windows'], 'release acceptance platforms');
|
|
120
|
+
if (ci.value.acceptanceReceipts.some(row => row.sourceSha !== sha || !(row.passed > 0)
|
|
121
|
+
|| !/^[a-f0-9]{64}$/.test(row.receiptSha256 || ''))) throw new Error('release acceptance receipt identity mismatch');
|
|
122
|
+
|
|
92
123
|
|
|
93
124
|
const integration = readJson(integrationFile);
|
|
94
125
|
if (integration.value.schemaVersion !== 1 || integration.value.kind !== 'ruvnet-brain-integration-evidence'
|
|
@@ -102,7 +133,9 @@ export function buildPrepublicationEvidence({
|
|
|
102
133
|
|| integration.value.skippedTests.some((name) => typeof name !== 'string' || !name.trim())) {
|
|
103
134
|
throw new Error('integration receipt is not an exact, fully accounted PASS');
|
|
104
135
|
}
|
|
105
|
-
if (integration.value.exclusionPolicy !==
|
|
136
|
+
if (integration.value.exclusionPolicy !== RELEASE_QUALIFICATION_POLICY
|
|
137
|
+
|| integration.value.skipped !== 0 || integration.value.todo !== 0
|
|
138
|
+
|| !/^[a-f0-9]{64}$/.test(integration.value.qualificationReceiptSha256 || '')
|
|
106
139
|
|| !Array.isArray(integration.value.todoTests)
|
|
107
140
|
|| integration.value.todoTests.length !== Number(integration.value.todo || 0)
|
|
108
141
|
|| !/^[a-f0-9]{64}$/.test(integration.value.exclusionsSha256 || '')) {
|
|
@@ -133,9 +166,9 @@ export function buildPrepublicationEvidence({
|
|
|
133
166
|
|
|
134
167
|
const common = { sha, payloadId: payload.payloadId, runId };
|
|
135
168
|
const leaves = [
|
|
136
|
-
passLeaf({ ...common, name: 'source-quality', source: 'candidate-ci-receipt:
|
|
169
|
+
passLeaf({ ...common, name: 'source-quality', source: 'candidate-ci-receipt:release-acceptance-linux', receiptSha256: ci.digest }),
|
|
137
170
|
passLeaf({ ...common, name: 'ux-qe', source: 'ux-qe-receipts:darwin,linux,win32', receiptSha256: uxDigest }),
|
|
138
|
-
passLeaf({ ...common, name: 'release-qe', source: 'candidate-ci-receipt:release-qe', receiptSha256: ci.digest }),
|
|
171
|
+
passLeaf({ ...common, name: 'release-qe', source: 'candidate-ci-receipt:release-qe', receiptSha256: ci.digest, runtimeCensusSha256: runtimeCensus.digest }),
|
|
139
172
|
passLeaf({ ...common, name: 'integration-linux', source: 'integration-receipt', receiptSha256: integration.digest,
|
|
140
173
|
skipped: integration.value.skipped, todo: integration.value.todo }),
|
|
141
174
|
passLeaf({ ...common, name: 'stranger-linux', source: 'stranger-receipt:ubuntu', receiptSha256: stranger.digest }),
|
|
@@ -161,6 +194,9 @@ if (isCli) {
|
|
|
161
194
|
manifestFile: path.resolve(arg('--manifest')),
|
|
162
195
|
payloadProofFile: path.resolve(arg('--payload-proof')),
|
|
163
196
|
hostFile: path.resolve(arg('--hosts')),
|
|
197
|
+
runtimeCensusFile: path.resolve(arg('--runtime-census')),
|
|
198
|
+
planFile: arg('--plan'),
|
|
199
|
+
coverageFile: arg('--coverage'),
|
|
164
200
|
ciFile: path.resolve(arg('--ci')),
|
|
165
201
|
integrationFile: path.resolve(arg('--integration')),
|
|
166
202
|
uxFiles,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Citation presence is an admission check, not semantic verification of each generated claim.
|
|
5
|
+
export function countPrimerReferences(primer, sourcePaths) {
|
|
6
|
+
const paths = [...new Set(sourcePaths)].filter((value) => typeof value === 'string' && value);
|
|
7
|
+
const citations = new Set([...primer.matchAll(/`([^`\n]+)`/g)].map((match) => match[1]));
|
|
8
|
+
const basenames = new Map();
|
|
9
|
+
for (const file of paths) basenames.set(path.basename(file), (basenames.get(path.basename(file)) || 0) + 1);
|
|
10
|
+
return paths.filter((file) => citations.has(file)
|
|
11
|
+
|| (basenames.get(path.basename(file)) === 1 && citations.has(path.basename(file))));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function writeGroundedPrimer({ primer, sourcePaths, output, minimum = 6 }) {
|
|
15
|
+
if (!Number.isInteger(minimum) || minimum < 1) throw new Error('invalid primer citation minimum');
|
|
16
|
+
const refs = countPrimerReferences(primer, sourcePaths);
|
|
17
|
+
if (refs.length < minimum) throw new Error(`THIN primer: ${refs.length} distinct source citations; requires ${minimum}. Existing primer preserved.`);
|
|
18
|
+
fs.writeFileSync(output, primer);
|
|
19
|
+
return refs;
|
|
20
|
+
}
|