ruvnet-brain 4.3.1 → 4.3.8
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 +12 -13
- package/bin/install.mjs +18 -8
- package/console/app.js +4 -4
- package/data/model-catalog.json +3 -3
- package/kb/verify-citation.mjs +32 -5
- package/package.json +13 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/mcp/managed-cli-interface.mjs +4 -2
- package/plugin/scripts/codex-hook-adapter.mjs +12 -7
- package/plugin/scripts/codex-hook-events.mjs +39 -0
- package/plugin/scripts/coverage-integrity.mjs +4 -4
- package/plugin/scripts/hook-shim.mjs +9 -7
- package/plugin/scripts/learn-capture.sh +20 -1
- package/plugin/scripts/learn-flush.mjs +7 -1
- package/plugin/scripts/lesson-gate.mjs +17 -2
- package/plugin/scripts/lesson-hooks.sh +17 -2
- package/plugin/scripts/session-start-core.mjs +47 -11
- package/plugin/skills/release-proof/SKILL.md +19 -10
- package/plugin/skills/release-proof/references/receipt-contract.md +18 -8
- package/scripts/brain-stamp-resolve.mjs +22 -0
- package/scripts/brain-stamp.mjs +5 -3
- package/scripts/build-bundle.mjs +41 -4
- package/scripts/candidate-host-evidence.mjs +9 -2
- package/scripts/ci/step-watchdog.mjs +86 -0
- package/scripts/convergence-manifest.mjs +66 -0
- package/scripts/distill-project.mjs +22 -4
- package/scripts/eval-brain.mjs +13 -3
- package/scripts/execution-policy.mjs +111 -0
- package/scripts/execution-preflight.mjs +18 -0
- package/scripts/git-hooks/pre-push +16 -0
- package/scripts/health-repair.mjs +5 -1
- package/scripts/host-install-matrix.mjs +247 -40
- package/scripts/integration-evidence.mjs +73 -0
- package/scripts/nightly-wrapper.sh +4 -3
- package/scripts/onboarding-console.mjs +3 -3
- package/scripts/prepublication-evidence.mjs +174 -0
- package/scripts/protected-release-invocation.mjs +32 -4
- package/scripts/public-verification-aggregate.mjs +27 -54
- package/scripts/public-verification-inputs.mjs +5 -3
- package/scripts/qa-runner.mjs +72 -0
- package/scripts/qe/agentic-qe-4.3.mjs +195 -0
- package/scripts/qe/aggregate-4.3.mjs +42 -0
- package/scripts/record-lesson.mjs +28 -6
- package/scripts/refresh-model-catalog.mjs +32 -3
- package/scripts/release-authority.mjs +19 -7
- package/scripts/release-convergence-watchdog.mjs +10 -22
- package/scripts/release-projection.mjs +76 -32
- package/scripts/release-transaction.mjs +1 -1
- package/scripts/release.mjs +9 -15
- package/scripts/retrieval-canary.mjs +14 -6
- package/scripts/set-version.mjs +31 -0
- package/scripts/source-coverage.mjs +11 -2
- package/scripts/staged-host-verifier.mjs +2 -2
- package/scripts/verify-channels.mjs +1 -1
- package/scripts/wired-check.mjs +1 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { aggregateEvidence } from './release-evidence-aggregate.mjs';
|
|
7
|
+
import { canonicalJson } from './release-transaction.mjs';
|
|
8
|
+
import { EXCLUSION_POLICY } from './integration-evidence.mjs';
|
|
9
|
+
|
|
10
|
+
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
11
|
+
|
|
12
|
+
function readJson(file) {
|
|
13
|
+
const bytes = fs.readFileSync(file);
|
|
14
|
+
return { value: JSON.parse(bytes), digest: sha256(bytes) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function requireExactSet(actual, expected, label) {
|
|
18
|
+
const got = [...actual].sort();
|
|
19
|
+
const want = [...expected].sort();
|
|
20
|
+
if (canonicalJson(got) !== canonicalJson(want)) {
|
|
21
|
+
throw new Error(`${label} differs: expected ${want.join(', ')}, got ${got.join(', ')}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function passLeaf({ name, sha, payloadId, source, receiptSha256, ...details }) {
|
|
26
|
+
const validated = /^[a-f0-9]{64}$/.test(receiptSha256 || '');
|
|
27
|
+
return {
|
|
28
|
+
name,
|
|
29
|
+
sha,
|
|
30
|
+
payloadId,
|
|
31
|
+
status: validated ? 'completed' : 'failed',
|
|
32
|
+
conclusion: validated ? 'success' : 'failure',
|
|
33
|
+
verdict: validated ? 'PASS' : 'FAIL',
|
|
34
|
+
source,
|
|
35
|
+
receiptSha256,
|
|
36
|
+
...details,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildPrepublicationEvidence({
|
|
41
|
+
sha,
|
|
42
|
+
version,
|
|
43
|
+
runId,
|
|
44
|
+
manifestFile,
|
|
45
|
+
payloadProofFile,
|
|
46
|
+
hostFile,
|
|
47
|
+
ciFile,
|
|
48
|
+
integrationFile,
|
|
49
|
+
uxFiles,
|
|
50
|
+
strangerFile,
|
|
51
|
+
}) {
|
|
52
|
+
if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^\d+\.\d+\.\d+$/.test(version || '') || !Number.isSafeInteger(runId) || runId <= 0) {
|
|
53
|
+
throw new Error('prepublication identity is malformed');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const manifestBytes = fs.readFileSync(manifestFile);
|
|
57
|
+
const manifest = JSON.parse(manifestBytes);
|
|
58
|
+
const payload = JSON.parse(fs.readFileSync(payloadProofFile));
|
|
59
|
+
if (manifest.candidateSha !== sha || manifest.version !== version || manifest.tag !== `v${version}`) {
|
|
60
|
+
throw new Error('candidate manifest identity mismatch');
|
|
61
|
+
}
|
|
62
|
+
if (!/^[a-f0-9]{64}$/.test(payload.payloadId || '')) throw new Error('payload identity is malformed');
|
|
63
|
+
|
|
64
|
+
const host = readJson(hostFile);
|
|
65
|
+
if (host.value.schemaVersion !== 1 || host.value.sha !== sha || host.value.payloadId !== payload.payloadId
|
|
66
|
+
|| !/^[a-f0-9]{64}$/.test(host.value.artifactSha256 || '') || host.value.leaves?.length !== 3) {
|
|
67
|
+
throw new Error('candidate host evidence identity or shape mismatch');
|
|
68
|
+
}
|
|
69
|
+
requireExactSet(host.value.leaves.map(({ name }) => name), ['claude-only', 'codex-only', 'dual-host'], 'candidate host leaves');
|
|
70
|
+
for (const leaf of host.value.leaves) {
|
|
71
|
+
const grounding = leaf.grounding;
|
|
72
|
+
const grounded = grounding && ['repo', 'path', 'file', 'storedPath']
|
|
73
|
+
.every((field) => typeof grounding[field] === 'string' && grounding[field].trim());
|
|
74
|
+
if (leaf.sha !== sha || leaf.payloadId !== payload.payloadId || leaf.status !== 'completed'
|
|
75
|
+
|| leaf.conclusion !== 'success' || leaf.verdict !== 'PASS'
|
|
76
|
+
|| leaf.functionalSearch !== true || leaf.searchExit !== 0 || !grounded
|
|
77
|
+
|| leaf.artifactSha256 !== host.value.artifactSha256) {
|
|
78
|
+
throw new Error(`candidate host leaf is not an exact PASS: ${leaf.name || '(missing)'}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ci = readJson(ciFile);
|
|
83
|
+
const requiredCiJobs = ['candidate-preflight', 'check', 'windows-unit', 'warm-brain', 'release-qe'];
|
|
84
|
+
if (ci.value.schemaVersion !== 1 || ci.value.kind !== 'ruvnet-brain-candidate-ci-evidence'
|
|
85
|
+
|| ci.value.sourceSha !== sha || ci.value.version !== version || ci.value.payloadId !== payload.payloadId
|
|
86
|
+
|| ci.value.payloadManifestSha256 !== sha256(manifestBytes) || ci.value.workflow !== 'ci'
|
|
87
|
+
|| ci.value.runId !== runId || ci.value.verdict !== 'PASS' || ci.value.skipped !== 0 || ci.value.unknown !== 0) {
|
|
88
|
+
throw new Error('candidate CI receipt identity or verdict mismatch');
|
|
89
|
+
}
|
|
90
|
+
requireExactSet(ci.value.jobs?.map(({ name }) => name) || [], requiredCiJobs, 'candidate CI jobs');
|
|
91
|
+
if (ci.value.jobs.some(({ conclusion }) => conclusion !== 'success')) throw new Error('candidate CI receipt contains a non-success job');
|
|
92
|
+
|
|
93
|
+
const integration = readJson(integrationFile);
|
|
94
|
+
if (integration.value.schemaVersion !== 1 || integration.value.kind !== 'ruvnet-brain-integration-evidence'
|
|
95
|
+
|| integration.value.sourceSha !== sha || integration.value.workflow !== 'integration-linux'
|
|
96
|
+
|| integration.value.runId !== runId || integration.value.verdict !== 'PASS'
|
|
97
|
+
|| integration.value.total <= 0
|
|
98
|
+
|| integration.value.passed + Number(integration.value.todo || 0) + Number(integration.value.skipped || 0) !== integration.value.total
|
|
99
|
+
|| integration.value.failed !== 0
|
|
100
|
+
|| !Array.isArray(integration.value.skippedTests)
|
|
101
|
+
|| integration.value.skippedTests.length !== integration.value.skipped
|
|
102
|
+
|| integration.value.skippedTests.some((name) => typeof name !== 'string' || !name.trim())) {
|
|
103
|
+
throw new Error('integration receipt is not an exact, fully accounted PASS');
|
|
104
|
+
}
|
|
105
|
+
if (integration.value.exclusionPolicy !== EXCLUSION_POLICY
|
|
106
|
+
|| !Array.isArray(integration.value.todoTests)
|
|
107
|
+
|| integration.value.todoTests.length !== Number(integration.value.todo || 0)
|
|
108
|
+
|| !/^[a-f0-9]{64}$/.test(integration.value.exclusionsSha256 || '')) {
|
|
109
|
+
throw new Error('integration receipt exclusions are not governed and fully enumerated');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!Array.isArray(uxFiles) || uxFiles.length !== 3) throw new Error('exactly three UX receipts are required');
|
|
113
|
+
const ux = uxFiles.map(readJson);
|
|
114
|
+
requireExactSet(ux.map(({ value }) => value.platform), ['darwin', 'linux', 'win32'], 'UX platforms');
|
|
115
|
+
for (const receipt of ux) {
|
|
116
|
+
const value = receipt.value;
|
|
117
|
+
if (value.schemaVersion !== 1 || value.suite !== 'ruvnet-brain-ux-qe' || value.gitSha !== sha
|
|
118
|
+
|| value.pass !== true || !Array.isArray(value.hardFailures) || value.hardFailures.length !== 0) {
|
|
119
|
+
throw new Error(`UX receipt is not an exact PASS: ${value.platform || '(missing)'}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const uxDigest = sha256(Buffer.from(canonicalJson(ux.map(({ value, digest }) => ({ platform: value.platform, digest }))
|
|
123
|
+
.sort((a, b) => a.platform.localeCompare(b.platform)))));
|
|
124
|
+
|
|
125
|
+
const stranger = readJson(strangerFile);
|
|
126
|
+
const strangerJobs = ['ubuntu', 'macos', 'windows-gitbash', 'windows-powershell', 'hostile'];
|
|
127
|
+
if (stranger.value.schemaVersion !== 1 || stranger.value.sha !== sha || stranger.value.payloadId !== payload.payloadId
|
|
128
|
+
|| Number(stranger.value.sourceCiRunId) !== runId || Number(stranger.value.strangerRunId) !== runId
|
|
129
|
+
|| stranger.value.verdict !== 'PASS') {
|
|
130
|
+
throw new Error('stranger receipt identity or verdict mismatch');
|
|
131
|
+
}
|
|
132
|
+
requireExactSet(stranger.value.jobs || [], strangerJobs, 'stranger jobs');
|
|
133
|
+
|
|
134
|
+
const common = { sha, payloadId: payload.payloadId, runId };
|
|
135
|
+
const leaves = [
|
|
136
|
+
passLeaf({ ...common, name: 'source-quality', source: 'candidate-ci-receipt:check', receiptSha256: ci.digest }),
|
|
137
|
+
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 }),
|
|
139
|
+
passLeaf({ ...common, name: 'integration-linux', source: 'integration-receipt', receiptSha256: integration.digest,
|
|
140
|
+
skipped: integration.value.skipped, todo: integration.value.todo }),
|
|
141
|
+
passLeaf({ ...common, name: 'stranger-linux', source: 'stranger-receipt:ubuntu', receiptSha256: stranger.digest }),
|
|
142
|
+
passLeaf({ ...common, name: 'stranger-macos', source: 'stranger-receipt:macos', receiptSha256: stranger.digest }),
|
|
143
|
+
passLeaf({ ...common, name: 'stranger-windows', source: 'stranger-receipt:windows', receiptSha256: stranger.digest }),
|
|
144
|
+
...host.value.leaves.map((leaf) => ({ ...leaf, runId, receiptSha256: host.digest })),
|
|
145
|
+
];
|
|
146
|
+
return { leaves, envelope: aggregateEvidence({ sha, payloadId: payload.payloadId, leaves }) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const isCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
150
|
+
if (isCli) {
|
|
151
|
+
const arg = (name) => {
|
|
152
|
+
const index = process.argv.indexOf(name);
|
|
153
|
+
return index >= 0 ? process.argv[index + 1] : null;
|
|
154
|
+
};
|
|
155
|
+
const uxDir = path.resolve(arg('--ux-dir'));
|
|
156
|
+
const uxFiles = fs.readdirSync(uxDir).filter((name) => name.endsWith('.json')).sort().map((name) => path.join(uxDir, name));
|
|
157
|
+
const result = buildPrepublicationEvidence({
|
|
158
|
+
sha: arg('--sha'),
|
|
159
|
+
version: arg('--version'),
|
|
160
|
+
runId: Number(arg('--run-id')),
|
|
161
|
+
manifestFile: path.resolve(arg('--manifest')),
|
|
162
|
+
payloadProofFile: path.resolve(arg('--payload-proof')),
|
|
163
|
+
hostFile: path.resolve(arg('--hosts')),
|
|
164
|
+
ciFile: path.resolve(arg('--ci')),
|
|
165
|
+
integrationFile: path.resolve(arg('--integration')),
|
|
166
|
+
uxFiles,
|
|
167
|
+
strangerFile: path.resolve(arg('--stranger')),
|
|
168
|
+
});
|
|
169
|
+
const leavesOut = path.resolve(arg('--leaves-out'));
|
|
170
|
+
const envelopeOut = path.resolve(arg('--envelope-out'));
|
|
171
|
+
fs.writeFileSync(leavesOut, `${JSON.stringify(result.leaves, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
172
|
+
fs.writeFileSync(envelopeOut, `${canonicalJson(result.envelope)}\n`, { flag: 'wx', mode: 0o600 });
|
|
173
|
+
process.stdout.write(`${JSON.stringify({ verdict: result.envelope.verdict, evidenceDigest: result.envelope.evidenceDigest })}\n`);
|
|
174
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
7
8
|
import { evaluateCandidateReceipt } from './release-proof.mjs';
|
|
8
9
|
import { getVersion } from './version.mjs';
|
|
9
10
|
|
|
@@ -13,12 +14,27 @@ export const PROTECTED_VERSION = getVersion();
|
|
|
13
14
|
const hex = (value, length) => new RegExp(`^[0-9a-f]{${length}}$`).test(String(value || ''));
|
|
14
15
|
const digestOf = (receipt) => String(receipt?.artifact?.sha256 || '').replace(/^sha256:/, '');
|
|
15
16
|
|
|
16
|
-
export function
|
|
17
|
+
export function validatePublishableVersion(version) {
|
|
18
|
+
return /^\d+\.\d+\.\d+$/.test(String(version || ''))
|
|
19
|
+
? []
|
|
20
|
+
: ['development versions cannot be published'];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateProtectedPublishEnvironment(env = process.env) {
|
|
17
24
|
const failures = [];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
if (env.GITHUB_ACTIONS !== 'true'
|
|
26
|
+
|| env.GITHUB_WORKFLOW !== PROTECTED_WORKFLOW
|
|
27
|
+
|| env.GITHUB_REPOSITORY !== 'stuinfla/ruvnet-brain'
|
|
28
|
+
|| env.GITHUB_EVENT_NAME !== 'workflow_dispatch'
|
|
29
|
+
|| env.GITHUB_REF_PROTECTED !== 'true') {
|
|
30
|
+
failures.push('publication is allowed only inside the protected-release GitHub workflow on a protected workflow_dispatch ref');
|
|
21
31
|
}
|
|
32
|
+
return failures;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateProtectedPublishInvocation({ root = process.cwd(), env = process.env } = {}) {
|
|
36
|
+
const failures = [...validateProtectedPublishEnvironment(env)];
|
|
37
|
+
let releaseMode = 'unknown';
|
|
22
38
|
|
|
23
39
|
const expectedSha = String(env.RUVNET_EXPECTED_SHA || '');
|
|
24
40
|
const expectedDigest = String(env.RUVNET_EXPECTED_ARTIFACT_SHA256 || '').replace(/^sha256:/, '');
|
|
@@ -26,6 +42,7 @@ export function validateProtectedPublishInvocation({ root = process.cwd(), env =
|
|
|
26
42
|
if (!hex(expectedSha, 40)) failures.push('expected candidate SHA is missing or malformed');
|
|
27
43
|
if (!hex(expectedDigest, 64)) failures.push('expected artifact digest is missing or malformed');
|
|
28
44
|
if (expectedVersion !== PROTECTED_VERSION) failures.push(`release generation must be ${PROTECTED_VERSION}`);
|
|
45
|
+
failures.push(...validatePublishableVersion(expectedVersion));
|
|
29
46
|
|
|
30
47
|
const receiptRelative = String(env.RUVNET_CANDIDATE_RECEIPT || '');
|
|
31
48
|
const evidenceRoot = path.resolve(root, 'release-evidence');
|
|
@@ -74,3 +91,14 @@ export function validateProtectedPublishInvocation({ root = process.cwd(), env =
|
|
|
74
91
|
failures,
|
|
75
92
|
};
|
|
76
93
|
}
|
|
94
|
+
|
|
95
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
|
96
|
+
const result = validateProtectedPublishInvocation();
|
|
97
|
+
if (result.verdict !== 'PASS') {
|
|
98
|
+
console.error('[protected-release] publication refused:');
|
|
99
|
+
for (const failure of result.failures) console.error(` - ${failure}`);
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
} else {
|
|
102
|
+
console.log('[protected-release] exact candidate publication authority verified');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -133,7 +133,7 @@ const identityOf = (leaf) => ({
|
|
|
133
133
|
releaseTransactionId: leaf.releaseTransactionId,
|
|
134
134
|
});
|
|
135
135
|
|
|
136
|
-
export function buildPublicVerificationAggregate(leaves
|
|
136
|
+
export function buildPublicVerificationAggregate(leaves) {
|
|
137
137
|
if (!Array.isArray(leaves) || leaves.length !== PUBLIC_VERIFICATION_OS.length * PUBLIC_VERIFICATION_MODES.length) {
|
|
138
138
|
throw new Error('public verification requires exactly nine leaves');
|
|
139
139
|
}
|
|
@@ -159,32 +159,20 @@ export function buildPublicVerificationAggregate(leaves, reviews) {
|
|
|
159
159
|
deltaCases: delta.length,
|
|
160
160
|
deltaHits: delta.filter(({ retrievalHit }) => retrievalHit === true).length,
|
|
161
161
|
recallAt10: retrievalCases.filter(({ retrievalHit }) => retrievalHit === true).length / retrievalCases.length,
|
|
162
|
-
|
|
162
|
+
// A release with no corpus delta has no delta citations to score; the sealed
|
|
163
|
+
// retrieval plan carries noDelta=true and still requires the legacy cohort.
|
|
164
|
+
deltaCitationRate: delta.length === 0 ? 1 : delta.filter(({ retrievalHit, citationResolved }) => retrievalHit === true && citationResolved === true).length / delta.length,
|
|
163
165
|
skipped: leaves.reduce((sum, leaf) => sum + leaf.skipped + leaf.retrieval.metrics.skipped, 0),
|
|
164
166
|
unknown: leaves.reduce((sum, leaf) => sum + leaf.unknown + leaf.retrieval.metrics.unknown, 0),
|
|
165
167
|
};
|
|
166
168
|
if (metrics.recallAt10 < 0.98 || metrics.deltaCitationRate !== 1 || metrics.skipped || metrics.unknown) {
|
|
167
169
|
throw new Error('public verification aggregate retrieval acceptance failed');
|
|
168
170
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (byModel.size !== REQUIRED_REVIEW_MODELS.length || REQUIRED_REVIEW_MODELS.some((model) => !byModel.has(model))) {
|
|
175
|
-
throw new Error('required independent review models are missing or duplicated');
|
|
176
|
-
}
|
|
177
|
-
const firstReview = byModel.get(REQUIRED_REVIEW_MODELS[0]);
|
|
178
|
-
const semanticIdentity = validateReviewOracleAgainstPlan(firstReview, first.retrievalPlan);
|
|
179
|
-
for (const model of REQUIRED_REVIEW_MODELS) {
|
|
180
|
-
const review = byModel.get(model);
|
|
181
|
-
const semantic = validateReviewOracleAgainstPlan(review, first.retrievalPlan);
|
|
182
|
-
if (review.sourceSha !== identity.sourceSha || review.artifactSha256 !== identity.artifactSha256
|
|
183
|
-
|| review.payloadId !== identity.payloadId || review.productContractSha256 !== firstReview.productContractSha256
|
|
184
|
-
|| review.rubricSha256 !== firstReview.rubricSha256
|
|
185
|
-
|| semantic.recordSetSha256 !== semanticIdentity.recordSetSha256) {
|
|
186
|
-
throw new Error(`${model} review identity, rubric, or oracle identity differs`);
|
|
187
|
-
}
|
|
171
|
+
const oracle = first.retrievalPlan.oracle;
|
|
172
|
+
if (!oracle || !HEX64.test(String(oracle.receiptSha256 || ''))
|
|
173
|
+
|| !HEX64.test(String(oracle.queryStoreSetSha256 || ''))
|
|
174
|
+
|| oracle.queryStoreSetSha256 !== digest(first.retrievalPlan.denominator.eligibleStores)) {
|
|
175
|
+
throw new Error('sealed retrieval oracle identity is incomplete');
|
|
188
176
|
}
|
|
189
177
|
return {
|
|
190
178
|
schemaVersion: 1,
|
|
@@ -192,17 +180,11 @@ export function buildPublicVerificationAggregate(leaves, reviews) {
|
|
|
192
180
|
identity,
|
|
193
181
|
coverage: first.coverage,
|
|
194
182
|
lanes: required.map((lane) => ({ lane, leafSha256: byLane.get(lane).leafSha256 })),
|
|
195
|
-
|
|
196
|
-
score: byModel.get(model).score })),
|
|
197
|
-
evidence: { leaves: required.map((lane) => structuredClone(byLane.get(lane))),
|
|
198
|
-
reviews: REQUIRED_REVIEW_MODELS.map((model) => structuredClone(byModel.get(model))) },
|
|
199
|
-
productContractSha256: firstReview.productContractSha256,
|
|
200
|
-
rubricSha256: firstReview.rubricSha256,
|
|
183
|
+
evidence: { leaves: required.map((lane) => structuredClone(byLane.get(lane))) },
|
|
201
184
|
retrievalOracle: {
|
|
202
|
-
oracleReceiptSha256:
|
|
203
|
-
queryStoreSetSha256:
|
|
204
|
-
recordCount:
|
|
205
|
-
recordSetSha256: semanticIdentity.recordSetSha256,
|
|
185
|
+
oracleReceiptSha256: oracle.receiptSha256,
|
|
186
|
+
queryStoreSetSha256: oracle.queryStoreSetSha256,
|
|
187
|
+
recordCount: first.retrievalPlan.denominator.eligibleStores.length,
|
|
206
188
|
},
|
|
207
189
|
metrics,
|
|
208
190
|
verdict: 'PASS',
|
|
@@ -210,8 +192,8 @@ export function buildPublicVerificationAggregate(leaves, reviews) {
|
|
|
210
192
|
};
|
|
211
193
|
}
|
|
212
194
|
|
|
213
|
-
export function signPublicVerificationAggregate({ leaves
|
|
214
|
-
const payload = buildPublicVerificationAggregate(leaves
|
|
195
|
+
export function signPublicVerificationAggregate({ leaves }, privateKey) {
|
|
196
|
+
const payload = buildPublicVerificationAggregate(leaves);
|
|
215
197
|
const aggregateSha256 = digest(payload);
|
|
216
198
|
const signed = { ...payload, aggregateSha256 };
|
|
217
199
|
return { ...signed, signature: crypto.sign(null, Buffer.from(canonicalJson(signed)), privateKey).toString('base64') };
|
|
@@ -225,10 +207,10 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
|
|
|
225
207
|
}
|
|
226
208
|
const payload = aggregatePayload(aggregate);
|
|
227
209
|
if (digest(payload) !== aggregate.aggregateSha256) throw new Error('public verification aggregate digest mismatch');
|
|
228
|
-
if (!Array.isArray(aggregate.evidence?.leaves)
|
|
229
|
-
throw new Error('public verification aggregate lacks raw leaf
|
|
210
|
+
if (!Array.isArray(aggregate.evidence?.leaves)) {
|
|
211
|
+
throw new Error('public verification aggregate lacks raw leaf evidence');
|
|
230
212
|
}
|
|
231
|
-
const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves
|
|
213
|
+
const rebuilt = buildPublicVerificationAggregate(aggregate.evidence.leaves);
|
|
232
214
|
if (canonicalJson(rebuilt) !== canonicalJson(payload)) {
|
|
233
215
|
throw new Error('public verification aggregate differs from rebuilt raw evidence');
|
|
234
216
|
}
|
|
@@ -247,17 +229,10 @@ export function verifyPublicVerificationAggregate(aggregate, publicKey, expected
|
|
|
247
229
|
|| aggregate.coverage.gistCurrent !== aggregate.coverage.gistTotal) {
|
|
248
230
|
throw new Error('public verification aggregate evidence is incomplete');
|
|
249
231
|
}
|
|
250
|
-
if (aggregate.reviews?.length !== REQUIRED_REVIEW_MODELS.length
|
|
251
|
-
|| REQUIRED_REVIEW_MODELS.some((model) => !aggregate.reviews.some((row) => row.model === model
|
|
252
|
-
&& Number.isInteger(row.score) && row.score >= 95 && HEX64.test(String(row.receiptSha256 || ''))))
|
|
253
|
-
|| !HEX64.test(String(aggregate.productContractSha256 || '')) || !HEX64.test(String(aggregate.rubricSha256 || ''))) {
|
|
254
|
-
throw new Error('public verification aggregate reviews are incomplete');
|
|
255
|
-
}
|
|
256
232
|
if (aggregate.retrievalOracle?.oracleReceiptSha256 !== aggregate.evidence.leaves[0].retrievalPlan.oracle.receiptSha256
|
|
257
233
|
|| aggregate.retrievalOracle?.queryStoreSetSha256 !== aggregate.evidence.leaves[0].retrievalPlan.oracle.queryStoreSetSha256
|
|
258
|
-
|| aggregate.retrievalOracle?.recordCount !== aggregate.evidence.leaves[0].retrievalPlan.denominator.eligibleStores.length
|
|
259
|
-
|
|
260
|
-
throw new Error('public verification aggregate retrieval oracle review is incomplete');
|
|
234
|
+
|| aggregate.retrievalOracle?.recordCount !== aggregate.evidence.leaves[0].retrievalPlan.denominator.eligibleStores.length) {
|
|
235
|
+
throw new Error('public verification aggregate retrieval oracle identity is incomplete');
|
|
261
236
|
}
|
|
262
237
|
if (expectedIdentity && canonicalJson(aggregate.identity) !== canonicalJson(expectedIdentity)) {
|
|
263
238
|
throw new Error('public verification aggregate identity differs from the release transaction');
|
|
@@ -270,13 +245,13 @@ function parseCliArgs(argv) {
|
|
|
270
245
|
for (let index = 0; index < argv.length; index += 2) {
|
|
271
246
|
const flag = argv[index];
|
|
272
247
|
const value = argv[index + 1];
|
|
273
|
-
if (!['--lanes', '--
|
|
274
|
-
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --
|
|
248
|
+
if (!['--lanes', '--out'].includes(flag) || !value) {
|
|
249
|
+
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
|
|
275
250
|
}
|
|
276
251
|
parsed[flag.slice(2)] = value;
|
|
277
252
|
}
|
|
278
|
-
if (!parsed.lanes || !parsed.
|
|
279
|
-
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --
|
|
253
|
+
if (!parsed.lanes || !parsed.out || Object.keys(parsed).length !== 2) {
|
|
254
|
+
throw new Error('usage: public-verification-aggregate.mjs --lanes <dir> --out <file>');
|
|
280
255
|
}
|
|
281
256
|
return parsed;
|
|
282
257
|
}
|
|
@@ -312,12 +287,11 @@ function readLaneLeaves(directory) {
|
|
|
312
287
|
return PUBLIC_VERIFICATION_OS.flatMap((osName) => byOs.get(osName));
|
|
313
288
|
}
|
|
314
289
|
|
|
315
|
-
export function generatePublicVerificationAggregate({ lanesDirectory,
|
|
290
|
+
export function generatePublicVerificationAggregate({ lanesDirectory, outputFile, privateKey }) {
|
|
316
291
|
if (!privateKey) throw new Error('RUVNET_SIGNING_KEY is required');
|
|
317
292
|
if (fs.existsSync(outputFile)) throw new Error(`refusing to overwrite existing aggregate: ${outputFile}`);
|
|
318
293
|
const leaves = readLaneLeaves(lanesDirectory);
|
|
319
|
-
const
|
|
320
|
-
const aggregate = signPublicVerificationAggregate({ leaves, reviews }, privateKey);
|
|
294
|
+
const aggregate = signPublicVerificationAggregate({ leaves }, privateKey);
|
|
321
295
|
fs.writeFileSync(outputFile, `${JSON.stringify(aggregate, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
322
296
|
return aggregate;
|
|
323
297
|
}
|
|
@@ -326,12 +300,11 @@ async function main() {
|
|
|
326
300
|
const options = parseCliArgs(process.argv.slice(2));
|
|
327
301
|
const aggregate = generatePublicVerificationAggregate({
|
|
328
302
|
lanesDirectory: options.lanes,
|
|
329
|
-
reviewsDirectory: options.reviews,
|
|
330
303
|
outputFile: options.out,
|
|
331
304
|
privateKey: process.env.RUVNET_SIGNING_KEY,
|
|
332
305
|
});
|
|
333
306
|
process.stdout.write(`${JSON.stringify({ ok: true, aggregateSha256: aggregate.aggregateSha256,
|
|
334
|
-
leaves: aggregate.metrics.leaves
|
|
307
|
+
leaves: aggregate.metrics.leaves })}\n`);
|
|
335
308
|
}
|
|
336
309
|
|
|
337
310
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
@@ -148,7 +148,7 @@ function validateCandidate({ root, bundleFile, packageFile }) {
|
|
|
148
148
|
const inventory = validatePublicInventory({ assetsDir: root, coverage, ledger });
|
|
149
149
|
if (inventory.publicStores.length !== storeCount
|
|
150
150
|
|| inventory.partitionSha256 !== coverage.publicInventoryPartitionSha256) {
|
|
151
|
-
fail(
|
|
151
|
+
fail(`candidate public inventory partition differs from release coverage (inventory=${inventory.partitionSha256}, coverage=${coverage.publicInventoryPartitionSha256}, stores=${inventory.publicStores.length}/${storeCount})`);
|
|
152
152
|
}
|
|
153
153
|
const coverageIdentity = { sha256: crypto.createHash('sha256').update(coverageBytes).digest('hex'), bytes: coverageBytes.length };
|
|
154
154
|
const candidate = {
|
|
@@ -412,10 +412,12 @@ export async function createPublicVerificationInputs({ baselineBundle, candidate
|
|
|
412
412
|
stores: baselineStores, storeCount: baselineStores.length };
|
|
413
413
|
const { value: queryEvidence } = readJson(oraclePath, 'independent strict-ancestor query oracle');
|
|
414
414
|
validateRetrievalQueryEvidence(queryEvidence);
|
|
415
|
-
|
|
415
|
+
verifyQueryOracleSource(queryEvidence, candidateResult.candidate.sourceSha, {
|
|
416
|
+
cwd: path.resolve(repo), allowSquashedSource: true,
|
|
417
|
+
});
|
|
416
418
|
const plan = buildRetrievalCanaryPlan({ coverage: candidateResult.coverage, baseline,
|
|
417
419
|
candidate: candidateResult.candidate, coverageIdentity: candidateResult.coverageIdentity,
|
|
418
|
-
queryEvidence, assetsDir: candidateTree.root });
|
|
420
|
+
queryEvidence, assetsDir: candidateTree.root, allowNoDelta: true });
|
|
419
421
|
writeExactOutputs(outDir, {
|
|
420
422
|
[baselineMode === 'observed' ? 'baseline-observation-receipt.json' : 'baseline-verification-receipt.json']: baselineProof.bytes,
|
|
421
423
|
'COVERAGE.json': candidateResult.coverageBytes,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Canonical bounded QA runner. Local and CI use the same lanes, command order, timeout policy,
|
|
3
|
+
// and machine-readable receipt; a human log is never the release verdict.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const release = process.argv.includes('--release');
|
|
11
|
+
const timeoutMs = Number(process.env.QA_TIMEOUT_MS || (release ? 15 * 60_000 : 8 * 60_000));
|
|
12
|
+
const lanes = [
|
|
13
|
+
['version', 'npm', ['run', 'version:check']],
|
|
14
|
+
['convergence', 'npm', ['run', 'convergence:check']],
|
|
15
|
+
['execution-policy', 'npm', ['run', 'execution-policy:check']],
|
|
16
|
+
['docs', 'npm', ['run', 'doc:currency']],
|
|
17
|
+
['wiring', 'npm', ['run', 'wired:check']],
|
|
18
|
+
['substitution', 'npm', ['run', 'substitution:check']],
|
|
19
|
+
['catalog', 'npm', ['run', 'catalog:verify']],
|
|
20
|
+
['contract', 'npx', ['vitest', 'run',
|
|
21
|
+
'tests/unit/brain-off.test.mjs',
|
|
22
|
+
'tests/unit/hook-hardening.test.mjs',
|
|
23
|
+
'tests/unit/hook-battery.test.mjs',
|
|
24
|
+
'tests/unit/session-start-core-parity.test.mjs',
|
|
25
|
+
'tests/unit/lesson-gate.test.mjs',
|
|
26
|
+
'tests/unit/wired-check.test.mjs',
|
|
27
|
+
'tests/unit/no-restated-truth.test.mjs',
|
|
28
|
+
'tests/unit/verify-citation.test.mjs',
|
|
29
|
+
'tests/unit/eval-brain-gate.test.mjs',
|
|
30
|
+
'tests/unit/distill-project.test.mjs',
|
|
31
|
+
'tests/unit/record-lesson.test.mjs',
|
|
32
|
+
'tests/unit/learn-capture-project-root.test.mjs',
|
|
33
|
+
'tests/unit/codex-claude-hook-parity.test.mjs',
|
|
34
|
+
'tests/unit/brain-stamp-resolve-built-from-sha.test.mjs',
|
|
35
|
+
'tests/unit/qa-runner-concurrency.test.mjs',
|
|
36
|
+
'tests/unit/execution-preflight.test.mjs',
|
|
37
|
+
'--reporter=dot']],
|
|
38
|
+
['mesh', 'npm', ['run', 'test:mesh']],
|
|
39
|
+
['plugin', 'npm', ['test']],
|
|
40
|
+
...(release ? [['mutation', 'npm', ['run', 'test:mutation']], ['claims', 'npm', ['run', 'claims:verify']], ['integration', 'npm', ['run', 'test:integration']]] : []),
|
|
41
|
+
];
|
|
42
|
+
const receiptDir = path.join(root, '.qa');
|
|
43
|
+
fs.mkdirSync(receiptDir, { recursive: true });
|
|
44
|
+
const started = new Date().toISOString();
|
|
45
|
+
const sha = (() => {
|
|
46
|
+
try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); }
|
|
47
|
+
catch { return null; }
|
|
48
|
+
})();
|
|
49
|
+
const run = ([name, command, args]) => new Promise((resolve) => {
|
|
50
|
+
const begin = Date.now();
|
|
51
|
+
const child = spawn(command, args, { cwd: root, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false });
|
|
52
|
+
let stdout = '', stderr = '', timedOut = false;
|
|
53
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; process.stdout.write(chunk); });
|
|
54
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; process.stderr.write(chunk); });
|
|
55
|
+
const timer = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); setTimeout(() => child.kill('SIGKILL'), 2000).unref(); }, timeoutMs);
|
|
56
|
+
child.on('close', (code, signal) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
resolve({ name, command: [command, ...args].join(' '), status: timedOut ? 'TIMEOUT' : code === 0 ? 'PASS' : 'FAIL', exitCode: code, signal, elapsedMs: Date.now() - begin, stdoutTail: stdout.slice(-4000), stderrTail: stderr.slice(-4000) });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
// Every lane is independent. Run the complete set concurrently so one failure does not hide
|
|
62
|
+
// later failures and force a serial fix-rerun cycle. The aggregate receipt remains the single
|
|
63
|
+
// verdict, while each lane keeps its own evidence for targeted repair.
|
|
64
|
+
const results = await Promise.all(lanes.map(async (lane) => {
|
|
65
|
+
const result = await run(lane);
|
|
66
|
+
fs.writeFileSync(path.join(receiptDir, `${result.name}.json`), `${JSON.stringify({ schema: 'ruvnet-brain.qa.lane', sha, ...result }, null, 2)}\n`);
|
|
67
|
+
return result;
|
|
68
|
+
}));
|
|
69
|
+
const receipt = { schema: 'ruvnet-brain.qa.aggregate', contract: release ? 'release' : 'pr', sha, started, ended: new Date().toISOString(), status: results.every((r) => r.status === 'PASS') && results.length === lanes.length ? 'PASS' : 'FAIL', requiredLanes: lanes.map(([name]) => name), results };
|
|
70
|
+
fs.writeFileSync(path.join(receiptDir, 'aggregate.json'), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
71
|
+
console.log(JSON.stringify({ status: receipt.status, sha, lanes: results.map(({ name, status, elapsedMs }) => ({ name, status, elapsedMs })) }));
|
|
72
|
+
process.exit(receipt.status === 'PASS' ? 0 : 1);
|