ruvnet-brain 4.3.1-dev → 4.3.3
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 +10 -1
- package/console/app.js +4 -4
- package/kb/verify-citation.mjs +32 -5
- package/package.json +12 -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/scripts/brain-stamp-resolve.mjs +22 -0
- package/scripts/brain-stamp.mjs +5 -3
- package/scripts/build-bundle.mjs +41 -4
- 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 +66 -0
- package/scripts/git-hooks/pre-push +16 -0
- package/scripts/health-repair.mjs +5 -1
- package/scripts/public-verification-aggregate.mjs +3 -1
- package/scripts/public-verification-inputs.mjs +5 -3
- package/scripts/qa-runner.mjs +71 -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/release-projection.mjs +76 -32
- package/scripts/release.mjs +6 -11
- package/scripts/retrieval-canary.mjs +14 -6
- package/scripts/set-version.mjs +31 -0
- package/scripts/verify-channels.mjs +1 -1
|
@@ -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,71 @@
|
|
|
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
|
+
'--reporter=dot']],
|
|
37
|
+
['mesh', 'npm', ['run', 'test:mesh']],
|
|
38
|
+
['plugin', 'npm', ['test']],
|
|
39
|
+
...(release ? [['mutation', 'npm', ['run', 'test:mutation']], ['claims', 'npm', ['run', 'claims:verify']], ['integration', 'npm', ['run', 'test:integration']]] : []),
|
|
40
|
+
];
|
|
41
|
+
const receiptDir = path.join(root, '.qa');
|
|
42
|
+
fs.mkdirSync(receiptDir, { recursive: true });
|
|
43
|
+
const started = new Date().toISOString();
|
|
44
|
+
const sha = (() => {
|
|
45
|
+
try { return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); }
|
|
46
|
+
catch { return null; }
|
|
47
|
+
})();
|
|
48
|
+
const run = ([name, command, args]) => new Promise((resolve) => {
|
|
49
|
+
const begin = Date.now();
|
|
50
|
+
const child = spawn(command, args, { cwd: root, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false });
|
|
51
|
+
let stdout = '', stderr = '', timedOut = false;
|
|
52
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; process.stdout.write(chunk); });
|
|
53
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; process.stderr.write(chunk); });
|
|
54
|
+
const timer = setTimeout(() => { timedOut = true; child.kill('SIGTERM'); setTimeout(() => child.kill('SIGKILL'), 2000).unref(); }, timeoutMs);
|
|
55
|
+
child.on('close', (code, signal) => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
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) });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
// Every lane is independent. Run the complete set concurrently so one failure does not hide
|
|
61
|
+
// later failures and force a serial fix-rerun cycle. The aggregate receipt remains the single
|
|
62
|
+
// verdict, while each lane keeps its own evidence for targeted repair.
|
|
63
|
+
const results = await Promise.all(lanes.map(async (lane) => {
|
|
64
|
+
const result = await run(lane);
|
|
65
|
+
fs.writeFileSync(path.join(receiptDir, `${result.name}.json`), `${JSON.stringify({ schema: 'ruvnet-brain.qa.lane', sha, ...result }, null, 2)}\n`);
|
|
66
|
+
return result;
|
|
67
|
+
}));
|
|
68
|
+
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 };
|
|
69
|
+
fs.writeFileSync(path.join(receiptDir, 'aggregate.json'), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
70
|
+
console.log(JSON.stringify({ status: receipt.status, sha, lanes: results.map(({ name, status, elapsedMs }) => ({ name, status, elapsedMs })) }));
|
|
71
|
+
process.exit(receipt.status === 'PASS' ? 0 : 1);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
const ROOT = path.resolve(import.meta.dirname, '../..');
|
|
9
|
+
const RECEIPT_VERSION = 1;
|
|
10
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
11
|
+
const vitestBin = path.join(ROOT, 'node_modules', 'vitest', 'vitest.mjs');
|
|
12
|
+
const FORBIDDEN_SPEND_KEYS = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'OPENROUTER_API_KEY'];
|
|
13
|
+
const vitest = (files, timeoutMs = 180_000) => ({
|
|
14
|
+
kind: 'vitest', command: process.execPath, args: [vitestBin, 'run', ...files], timeoutMs,
|
|
15
|
+
});
|
|
16
|
+
const windowsInstallImportProbe = {
|
|
17
|
+
kind: 'command', command: process.execPath,
|
|
18
|
+
args: ['--input-type=module', '-e', "process.env.RUVNET_BRAIN_IMPORT_ONLY='1'; await import('./bin/install.mjs')"],
|
|
19
|
+
timeoutMs: 30_000,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Deterministic slices only. Agentic-QE's grounded quality-gate contract names correctness,
|
|
23
|
+
// safety, reliability, test adequacy, parity, performance, distribution, and release evidence.
|
|
24
|
+
// The fleet is intentionally not invoked here: it is an on-demand generator, never release proof.
|
|
25
|
+
const LANES = Object.freeze({
|
|
26
|
+
// Fail design/provenance issues before any long behavioral or platform lane starts.
|
|
27
|
+
preflight: [
|
|
28
|
+
{ kind: 'command', command: process.execPath, args: ['--check', 'scripts/qe/agentic-qe-4.3.mjs'], timeoutMs: 10_000 },
|
|
29
|
+
{ kind: 'command', command: process.execPath, args: ['--check', 'scripts/qe/aggregate-4.3.mjs'], timeoutMs: 10_000 },
|
|
30
|
+
{ kind: 'command', command: npm, args: ['run', 'version:check'], timeoutMs: 60_000 },
|
|
31
|
+
vitest(['tests/qe/gpt56/critical-risk-map.test.mjs'], 60_000),
|
|
32
|
+
],
|
|
33
|
+
check: [
|
|
34
|
+
vitest([
|
|
35
|
+
'tests/qe/gpt56/critical-risk-map.test.mjs',
|
|
36
|
+
'tests/unit/npm-tarball-codex.test.mjs',
|
|
37
|
+
'tests/unit/codex-lifecycle-hooks.test.mjs',
|
|
38
|
+
'tests/unit/codex-console-invocation.test.mjs',
|
|
39
|
+
'tests/unit/console-advocacy-dial.test.mjs',
|
|
40
|
+
'tests/unit/console-advocacy-precision.test.mjs',
|
|
41
|
+
'tests/unit/mcp-timeout-outage.test.mjs',
|
|
42
|
+
]),
|
|
43
|
+
],
|
|
44
|
+
integration: [
|
|
45
|
+
vitest(['tests/integration/hook-conformance-both-hosts.test.mjs'], 240_000),
|
|
46
|
+
vitest(['tests/integration/managed-cli-mcp.test.mjs'], 180_000),
|
|
47
|
+
],
|
|
48
|
+
release: [
|
|
49
|
+
vitest([
|
|
50
|
+
'tests/qe/release/packed-clean-install.test.mjs',
|
|
51
|
+
'tests/qe/release/release-publish-contract.test.mjs',
|
|
52
|
+
'tests/qe/release/stable-spine-recovery.test.mjs',
|
|
53
|
+
'tests/qe/security/release-abuse-cases.test.mjs',
|
|
54
|
+
], 240_000),
|
|
55
|
+
],
|
|
56
|
+
resources: [
|
|
57
|
+
vitest([
|
|
58
|
+
'tests/qe/gpt56/worker-concurrency-retirement.test.mjs',
|
|
59
|
+
'tests/unit/mcp-timeout-outage.test.mjs',
|
|
60
|
+
], 180_000),
|
|
61
|
+
],
|
|
62
|
+
'windows-unit': [
|
|
63
|
+
windowsInstallImportProbe,
|
|
64
|
+
vitest([
|
|
65
|
+
'tests/qe/gpt56/critical-risk-map.test.mjs',
|
|
66
|
+
'tests/unit/codex-console-invocation.test.mjs',
|
|
67
|
+
'tests/unit/console-advocacy-dial.test.mjs',
|
|
68
|
+
'tests/unit/console-advocacy-precision.test.mjs',
|
|
69
|
+
'tests/unit/mcp-timeout-outage.test.mjs',
|
|
70
|
+
], 120_000),
|
|
71
|
+
vitest(['tests/unit/npm-tarball-codex.test.mjs'], 180_000),
|
|
72
|
+
vitest(['tests/unit/codex-lifecycle-hooks.test.mjs'], 180_000),
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
|
77
|
+
const now = () => new Date().toISOString();
|
|
78
|
+
const arg = (name) => {
|
|
79
|
+
const i = process.argv.indexOf(name);
|
|
80
|
+
return i < 0 ? null : process.argv[i + 1];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function enforceZeroSpend() {
|
|
84
|
+
const exposed = FORBIDDEN_SPEND_KEYS.filter((key) => process.env[key]);
|
|
85
|
+
if (exposed.length || process.env.RUVNET_ALLOW_METERED_SPEND === '1') {
|
|
86
|
+
throw new Error(`refusing QE: metered API credentials exposed (${exposed.join(', ') || 'override'})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function gitSha() {
|
|
91
|
+
if (process.env.GITHUB_SHA) return process.env.GITHUB_SHA;
|
|
92
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' });
|
|
93
|
+
return result.status === 0 ? result.stdout.trim() : 'unknown';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function outputDir() {
|
|
97
|
+
const dir = path.resolve(ROOT, process.env.QE_RECEIPT_DIR || '.qe/receipts');
|
|
98
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
99
|
+
return dir;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function runStep(step, index, dir) {
|
|
103
|
+
const startedAt = now();
|
|
104
|
+
const report = path.join(dir, `vitest-${index}.json`);
|
|
105
|
+
const args = step.kind === 'vitest'
|
|
106
|
+
? [...step.args, '--reporter=json', '--reporter=verbose', `--outputFile=${report}`]
|
|
107
|
+
: step.args;
|
|
108
|
+
const result = spawnSync(step.command, args, {
|
|
109
|
+
cwd: ROOT,
|
|
110
|
+
env: { ...process.env, CI: '1' },
|
|
111
|
+
encoding: 'utf8',
|
|
112
|
+
timeout: step.timeoutMs,
|
|
113
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
114
|
+
});
|
|
115
|
+
const stdout = String(result.stdout || '');
|
|
116
|
+
const stderr = String(result.stderr || '');
|
|
117
|
+
const stepReceipt = {
|
|
118
|
+
index,
|
|
119
|
+
command: [step.command, ...args].join(' '),
|
|
120
|
+
startedAt,
|
|
121
|
+
endedAt: now(),
|
|
122
|
+
exitCode: result.status,
|
|
123
|
+
spawnError: result.error?.message || null,
|
|
124
|
+
timedOut: Boolean(result.error?.code === 'ETIMEDOUT'),
|
|
125
|
+
stdoutSha256: sha256(stdout),
|
|
126
|
+
stderrSha256: sha256(stderr),
|
|
127
|
+
stdoutTail: stdout.slice(-4000),
|
|
128
|
+
stderrTail: stderr.slice(-4000),
|
|
129
|
+
};
|
|
130
|
+
if (step.kind === 'vitest') {
|
|
131
|
+
try {
|
|
132
|
+
const json = JSON.parse(fs.readFileSync(report, 'utf8'));
|
|
133
|
+
stepReceipt.tests = {
|
|
134
|
+
total: json.numTotalTests ?? 0,
|
|
135
|
+
passed: json.numPassedTests ?? 0,
|
|
136
|
+
failed: json.numFailedTests ?? 0,
|
|
137
|
+
skipped: json.numPendingTests ?? 0,
|
|
138
|
+
suitesFailed: json.numFailedTestSuites ?? 0,
|
|
139
|
+
};
|
|
140
|
+
stepReceipt.failedTests = (json.testResults || []).flatMap((suite) =>
|
|
141
|
+
(suite.assertionResults || [])
|
|
142
|
+
.filter((test) => test.status === 'failed')
|
|
143
|
+
.map((test) => ({ file: suite.name, name: test.fullName })));
|
|
144
|
+
stepReceipt.skippedTests = (json.testResults || []).flatMap((suite) =>
|
|
145
|
+
(suite.assertionResults || [])
|
|
146
|
+
.filter((test) => test.status === 'pending' || test.status === 'skipped')
|
|
147
|
+
.map((test) => ({ file: suite.name, name: test.fullName })));
|
|
148
|
+
stepReceipt.suiteErrors = (json.testResults || [])
|
|
149
|
+
.filter((suite) => suite.status === 'failed' || suite.message)
|
|
150
|
+
.map((suite) => ({ file: suite.name, message: suite.message || 'suite failed' }));
|
|
151
|
+
} catch (error) { stepReceipt.reportError = error.message; }
|
|
152
|
+
}
|
|
153
|
+
stepReceipt.status = result.error || result.status !== 0 ? 'FAIL' : 'PASS';
|
|
154
|
+
if (stepReceipt.tests && (stepReceipt.tests.total === 0 || stepReceipt.tests.skipped > 0)) {
|
|
155
|
+
stepReceipt.status = 'UNKNOWN';
|
|
156
|
+
}
|
|
157
|
+
return stepReceipt;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function main() {
|
|
161
|
+
enforceZeroSpend();
|
|
162
|
+
const requestedLane = arg('--lane');
|
|
163
|
+
const lane = requestedLane;
|
|
164
|
+
if (!requestedLane || !LANES[lane]) throw new Error(`unknown lane: ${requestedLane || '<missing>'}`);
|
|
165
|
+
const dir = outputDir();
|
|
166
|
+
fs.rmSync(path.join(dir, `${requestedLane}.json`), { force: true });
|
|
167
|
+
const runDir = path.join(dir, `.run-${requestedLane}-${process.pid}-${Date.now()}`);
|
|
168
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
169
|
+
const steps = [];
|
|
170
|
+
for (const [index, step] of LANES[lane].entries()) {
|
|
171
|
+
const result = runStep(step, index, runDir);
|
|
172
|
+
steps.push(result);
|
|
173
|
+
if (result.status !== 'PASS') break;
|
|
174
|
+
}
|
|
175
|
+
const status = steps.every((step) => step.status === 'PASS') ? 'PASS' : 'FAIL';
|
|
176
|
+
const receipt = {
|
|
177
|
+
schema: 'ruvnet-brain.agentic-qe.receipt', receiptVersion: RECEIPT_VERSION,
|
|
178
|
+
contract: 'agentic-qe-4.3', lane: requestedLane, sha: gitSha(),
|
|
179
|
+
runId: process.env.GITHUB_RUN_ID || `local-${gitSha()}`, host: `${os.platform()}-${os.arch()}`,
|
|
180
|
+
startedAt: steps[0]?.startedAt || now(), endedAt: steps.at(-1)?.endedAt || now(), status, steps,
|
|
181
|
+
};
|
|
182
|
+
const file = path.join(dir, `${requestedLane}.json`);
|
|
183
|
+
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
184
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
185
|
+
console.log(JSON.stringify({
|
|
186
|
+
lane: requestedLane, status, receipt: file, sha: receipt.sha,
|
|
187
|
+
failedTests: steps.flatMap((step) => step.failedTests || []),
|
|
188
|
+
}));
|
|
189
|
+
process.exitCode = status === 'PASS' ? 0 : 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try { main(); } catch (error) {
|
|
193
|
+
console.error(`agentic-qe-4.3: ${error.message}`);
|
|
194
|
+
process.exitCode = 2;
|
|
195
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const ROOT = path.resolve(import.meta.dirname, '../..');
|
|
6
|
+
const dir = path.resolve(ROOT, process.env.QE_RECEIPT_DIR || '.qe/receipts');
|
|
7
|
+
const required = ['preflight', 'check', 'integration', 'release', 'resources', 'windows-unit'];
|
|
8
|
+
const failures = [];
|
|
9
|
+
const receipts = required.map((lane) => {
|
|
10
|
+
const file = path.join(dir, `${lane}.json`);
|
|
11
|
+
if (!fs.existsSync(file)) { failures.push(`${lane}: receipt missing`); return null; }
|
|
12
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) {
|
|
13
|
+
failures.push(`${lane}: invalid receipt (${error.message})`); return null;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
const shas = new Set(receipts.filter(Boolean).map((receipt) => receipt.sha));
|
|
17
|
+
if (shas.size !== 1) failures.push(`receipts are not bound to one SHA: ${[...shas].join(', ') || '<none>'}`);
|
|
18
|
+
const runIds = new Set(receipts.filter(Boolean).map((receipt) => receipt.runId));
|
|
19
|
+
if (runIds.size !== 1) failures.push(`receipts are not bound to one run: ${[...runIds].join(', ') || '<none>'}`);
|
|
20
|
+
for (const [index, receipt] of receipts.entries()) {
|
|
21
|
+
const lane = required[index];
|
|
22
|
+
if (!receipt) continue;
|
|
23
|
+
if (receipt.schema !== 'ruvnet-brain.agentic-qe.receipt' || receipt.contract !== 'agentic-qe-4.3') {
|
|
24
|
+
failures.push(`${lane}: wrong receipt contract`);
|
|
25
|
+
}
|
|
26
|
+
if (receipt.lane !== lane || receipt.status !== 'PASS') failures.push(`${lane}: status ${receipt.status || '<missing>'}`);
|
|
27
|
+
for (const step of receipt.steps || []) {
|
|
28
|
+
if (step.status !== 'PASS') failures.push(`${lane} step ${step.index}: ${step.status}`);
|
|
29
|
+
if (step.tests && (step.tests.total === 0 || step.tests.skipped > 0)) {
|
|
30
|
+
failures.push(`${lane} step ${step.index}: skipped or empty test result`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const result = {
|
|
35
|
+
schema: 'ruvnet-brain.agentic-qe.aggregate', contract: 'agentic-qe-4.3',
|
|
36
|
+
sha: shas.size === 1 ? [...shas][0] : null, required,
|
|
37
|
+
status: failures.length ? 'FAIL' : 'PASS', failures, receipts,
|
|
38
|
+
};
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
40
|
+
fs.writeFileSync(path.join(dir, 'aggregate.json'), `${JSON.stringify(result, null, 2)}\n`);
|
|
41
|
+
console.log(JSON.stringify({ status: result.status, sha: result.sha, failures }));
|
|
42
|
+
process.exitCode = failures.length ? 1 : 0;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { execFileSync } from 'node:child_process';
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import { resolveRuflo, RUFLO_MISSING } from '../plugin/scripts/ruflo-bin.mjs';
|
|
22
23
|
|
|
23
24
|
const arg = (name, def = '') => {
|
|
24
25
|
const i = process.argv.indexOf(`--${name}`);
|
|
@@ -46,6 +47,12 @@ if (!fs.existsSync(db)) {
|
|
|
46
47
|
process.exit(2);
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
const RUFLO = resolveRuflo();
|
|
51
|
+
if (!RUFLO) {
|
|
52
|
+
console.error(`ERROR: ${RUFLO_MISSING}`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
const key = `lesson-${slug}`;
|
|
50
57
|
const value = [
|
|
51
58
|
`TASK: ${task}`,
|
|
@@ -56,21 +63,36 @@ const value = [
|
|
|
56
63
|
].filter(Boolean).join(' ');
|
|
57
64
|
|
|
58
65
|
const ruflo = (args) =>
|
|
59
|
-
execFileSync(
|
|
66
|
+
execFileSync(RUFLO, args, { cwd: dir, encoding: 'utf8', timeout: 60000,
|
|
67
|
+
shell: process.platform === 'win32' });
|
|
60
68
|
|
|
61
69
|
console.log(`\nRecording lesson into ${path.basename(dir)}/.swarm/memory.db (namespace: ${ns})`);
|
|
62
70
|
console.log(` key: ${key}`);
|
|
63
71
|
|
|
64
|
-
//
|
|
65
|
-
let stored = false;
|
|
72
|
+
// 1a. STORE (native, signal namespace) — L1 content + L2 embedding
|
|
66
73
|
try {
|
|
67
|
-
|
|
68
|
-
stored = /OK|stored/i.test(out);
|
|
74
|
+
ruflo(['memory', 'store', '-k', key, '-n', ns, '--value', value]);
|
|
69
75
|
} catch (e) {
|
|
70
76
|
console.error(' store FAILED:', String(e.stdout || e.message).split('\n')[0]);
|
|
71
77
|
process.exit(1);
|
|
72
78
|
}
|
|
73
|
-
|
|
79
|
+
|
|
80
|
+
// 1b. PROVE THE WRITE (exact-key round trip, ADR-063). `ruflo memory store` printing "[OK] Data
|
|
81
|
+
// stored successfully" is not evidence of a write — that exact line was on stdout throughout the
|
|
82
|
+
// 2026-08-13 incident that left three days of memory unrecoverable (rowcount 0, no store-side
|
|
83
|
+
// error). The only accepted proof in this repo is retrieving the SAME key back through the managed
|
|
84
|
+
// interface and reading the VALUE, the pattern `degradation-watch.mjs`'s `proveMemoryDurable()` and
|
|
85
|
+
// `learning-replay-fixture.mjs`'s `retrieveExact()` already establish — never the store command's
|
|
86
|
+
// own claimed-success wording, and never its exit status (the CLI can exit 0 while printing
|
|
87
|
+
// `[ERROR]`).
|
|
88
|
+
let stored = false;
|
|
89
|
+
try {
|
|
90
|
+
const back = ruflo(['memory', 'retrieve', '-k', key, '-n', ns, '--value-only', '--path', db]);
|
|
91
|
+
stored = String(back).includes(value);
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.error(' round-trip FAILED:', String(e.stdout || e.message).split('\n')[0]);
|
|
94
|
+
}
|
|
95
|
+
console.log(` 1. store -> ${stored ? 'OK (round-trip verified)' : 'store reported no error, but retrieve did not return the value'}`);
|
|
74
96
|
|
|
75
97
|
// 2. REFINE (native) — L3 patterns + L4 episodes
|
|
76
98
|
let batchEpisodes = '?';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Build the release-bound coverage projection from an observed corpus ledger and exact bundle bytes.
|
|
3
|
-
// This is
|
|
4
|
-
// independent consumer that proves the emitted files agree with the archive.
|
|
3
|
+
// This is a producer; public-verification-inputs.mjs independently proves the emitted files.
|
|
5
4
|
import crypto from 'node:crypto';
|
|
6
5
|
import fs from 'node:fs';
|
|
7
6
|
import path from 'node:path';
|
|
@@ -15,7 +14,20 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
15
14
|
const arg = (name, fallback = null) => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : fallback; };
|
|
16
15
|
const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
17
16
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
function seedCompatibleGistReceipt() {
|
|
19
|
+
const source = readJson(path.join(ROOT, 'kb', 'ruv-gists.sources.json'));
|
|
20
|
+
const gists = {};
|
|
21
|
+
for (const [id, row] of Object.entries(source.gists || {})) {
|
|
22
|
+
const files = row.files;
|
|
23
|
+
gists[id] = { versionSha: row.versionSha, files,
|
|
24
|
+
contentDigest: crypto.createHash('sha256').update(JSON.stringify(files)).digest('hex') };
|
|
25
|
+
}
|
|
26
|
+
// The v4.2.1 seed predates the schema-3 source-byte binding. Keep the release proof honest:
|
|
27
|
+
// schema 2 proves the exact gist identity/file inventory, while CORPUS-COVERAGE retains the
|
|
28
|
+
// newer source observation separately. Do not relabel the old seed as current-source evidence.
|
|
29
|
+
return { schemaVersion: 2, owner: source.owner, generated: source.generated, gists };
|
|
30
|
+
}
|
|
19
31
|
|
|
20
32
|
export function createReleaseProjection({ corpusCoverage, assetsDir, version, sourceSnapshot,
|
|
21
33
|
corpusSeed, baselineReceiptSha256, outDir }) {
|
|
@@ -30,23 +42,48 @@ export function createReleaseProjection({ corpusCoverage, assetsDir, version, so
|
|
|
30
42
|
}
|
|
31
43
|
const assets = path.resolve(assetsDir);
|
|
32
44
|
const sourceLedger = readRvfGenerations(assets);
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
&& availableStores.has(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
// Source observation and release assets are different evidence planes. The observation is
|
|
46
|
+
// generated against the maintainer checkout, where RVF binaries are intentionally absent;
|
|
47
|
+
// the seed is the immutable release input. Build the release-scoped rows from the actual seed
|
|
48
|
+
// files and its byte-bound ledger, while preserving the complete observation in CORPUS-COVERAGE.
|
|
49
|
+
const availableStores = new Set(fs.readdirSync(assets)
|
|
50
|
+
.filter((name) => /^.+\.big\.rvf$/.test(name)).map((name) => name.slice(0, -'.big.rvf'.length).toLowerCase()));
|
|
51
|
+
const seededRows = corpusCoverage.rows.filter((row) => row.disposition === 'eligible'
|
|
52
|
+
&& availableStores.has(String(row.artifact?.store || '').toLowerCase()));
|
|
53
|
+
if (!seededRows.length) throw new Error('immutable seed contains no eligible corpus stores');
|
|
54
|
+
const seededExcludedRows = corpusCoverage.rows.filter((row) => row.disposition !== 'eligible'
|
|
55
|
+
&& availableStores.has(String(row.artifact?.store || '').toLowerCase()));
|
|
56
|
+
const rows = [...seededRows, ...seededExcludedRows].map((row) => {
|
|
57
|
+
if (row.disposition !== 'eligible') return { ...row };
|
|
58
|
+
const store = String(row.artifact.store);
|
|
59
|
+
const generation = sourceLedger.stores[store];
|
|
60
|
+
if (!generation) throw new Error(`immutable seed ledger is missing public store ${store}`);
|
|
61
|
+
return { ...row, status: 'CURRENT', artifact: { ...row.artifact,
|
|
62
|
+
sourceCommit: generation.sourceCommit, rvfSha256: generation.sha256 } };
|
|
63
|
+
});
|
|
64
|
+
// The release ledger is the shipped public set, not a catalog of policy-excluded rows.
|
|
65
|
+
// Excluded rows remain in COVERAGE.json as auditable policy evidence, but must not be
|
|
66
|
+
// inserted into PUBLIC-RVF-GENERATIONS.json or counted as installed public stores.
|
|
67
|
+
const publicStores = [...new Set(rows.filter((row) => row.disposition === 'eligible')
|
|
68
|
+
.map((row) => String(row.artifact.store).toLowerCase()))];
|
|
69
|
+
const classesFile = path.join(assets, 'public-store-classes.json');
|
|
70
|
+
const derivedStores = fs.existsSync(classesFile)
|
|
71
|
+
? readJson(classesFile).derived.map((entry) => String(entry.store).toLowerCase()) : [];
|
|
72
|
+
const ledgerStores = [...new Set([...publicStores, ...derivedStores])];
|
|
73
|
+
const projectedCoverage = { ...corpusCoverage, rows,
|
|
74
|
+
totals: { ...corpusCoverage.totals, rows: rows.length,
|
|
75
|
+
repositories: rows.filter((row) => row.kind === 'repository').length,
|
|
76
|
+
gists: rows.filter((row) => row.kind === 'gist').length,
|
|
77
|
+
byStatus: Object.fromEntries([...new Set(rows.map((row) => row.status))].sort()
|
|
78
|
+
.map((status) => [status, rows.filter((row) => row.status === status).length])) } };
|
|
79
|
+
const projectedEnumerationReceipt = { ...corpusCoverage.enumerationReceipt,
|
|
80
|
+
repositories: { ...corpusCoverage.enumerationReceipt.repositories,
|
|
81
|
+
expected: projectedCoverage.totals.repositories },
|
|
82
|
+
gists: { ...corpusCoverage.enumerationReceipt.gists,
|
|
83
|
+
expected: projectedCoverage.totals.gists } };
|
|
84
|
+
const ledger = projectPublicGenerationLedger({ ledger: sourceLedger, publicStores: ledgerStores, version, sourceSnapshot });
|
|
85
|
+
const ledgerBytes = generationLedgerBytes(ledger);
|
|
86
|
+
const corpusBytes = Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`);
|
|
50
87
|
const releaseBase = {
|
|
51
88
|
schemaVersion: 1, kind: 'ruvnet-brain-release-coverage', owner: corpusCoverage.owner,
|
|
52
89
|
observedAt: corpusCoverage.observedAt, generatorSourceSha: corpusCoverage.generatorSourceSha,
|
|
@@ -54,22 +91,29 @@ export function createReleaseProjection({ corpusCoverage, assetsDir, version, so
|
|
|
54
91
|
releaseIdentity: { version, tag: `v${version}`, sourceSnapshot },
|
|
55
92
|
corpusSeed: { tag: corpusSeed.tag, archiveSha256: corpusSeed.archiveSha256,
|
|
56
93
|
archiveBytes: corpusSeed.archiveBytes, receiptSha256: baselineReceiptSha256 },
|
|
57
|
-
corpusCoverage: { sha256: sha256(
|
|
58
|
-
generationLedger: { file: 'PUBLIC-RVF-GENERATIONS.json', sha256: sha256(
|
|
59
|
-
bytes:
|
|
60
|
-
installedProjectionSchema: 2, policy: corpusCoverage.policy, enumerationReceipt:
|
|
61
|
-
rows:
|
|
94
|
+
corpusCoverage: { sha256: sha256(corpusBytes), coverageGeneration: corpusCoverage.coverageGeneration },
|
|
95
|
+
generationLedger: { file: 'PUBLIC-RVF-GENERATIONS.json', sha256: sha256(ledgerBytes),
|
|
96
|
+
bytes: ledgerBytes.length, storeCount: Object.keys(ledger.stores).length },
|
|
97
|
+
installedProjectionSchema: 2, policy: corpusCoverage.policy, enumerationReceipt: projectedEnumerationReceipt,
|
|
98
|
+
rows: projectedCoverage.rows, totals: projectedCoverage.totals,
|
|
62
99
|
};
|
|
100
|
+
const gistReceipt = seedCompatibleGistReceipt();
|
|
101
|
+
// Keep the projected receipt in the same asset root that the final archive
|
|
102
|
+
// validator reads, so both partition hashes include identical evidence.
|
|
103
|
+
fs.writeFileSync(path.join(assets, 'ruv-gists.sources.json'), `${JSON.stringify(gistReceipt, null, 2)}\n`);
|
|
63
104
|
const inventory = validatePublicInventory({ assetsDir: assets, coverage: releaseBase, ledger });
|
|
64
105
|
releaseBase.publicInventoryPartitionSha256 = inventory.partitionSha256;
|
|
65
106
|
releaseBase.releaseCoverageGeneration = releaseCoverageGenerationFor(releaseBase);
|
|
66
107
|
const out = path.resolve(outDir);
|
|
67
108
|
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
109
|
fs.writeFileSync(path.join(out, 'CORPUS-COVERAGE.json'), corpusBytes);
|
|
71
|
-
fs.writeFileSync(path.join(out, 'COVERAGE.json'),
|
|
72
|
-
fs.writeFileSync(path.join(out, 'PUBLIC-RVF-GENERATIONS.json'),
|
|
110
|
+
fs.writeFileSync(path.join(out, 'COVERAGE.json'), `${JSON.stringify(releaseBase, null, 2)}\n`);
|
|
111
|
+
fs.writeFileSync(path.join(out, 'PUBLIC-RVF-GENERATIONS.json'), ledgerBytes);
|
|
112
|
+
fs.writeFileSync(path.join(out, 'ruv-gists.sources.json'), `${JSON.stringify(gistReceipt, null, 2)}\n`);
|
|
113
|
+
for (const file of ['public-store-classes.json', 'concepts.sources.json']) {
|
|
114
|
+
const source = path.join(assets, file);
|
|
115
|
+
if (fs.existsSync(source)) fs.copyFileSync(source, path.join(out, file));
|
|
116
|
+
}
|
|
73
117
|
return releaseBase;
|
|
74
118
|
}
|
|
75
119
|
|
|
@@ -77,9 +121,9 @@ if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
|
77
121
|
try {
|
|
78
122
|
const corpusFile = path.resolve(arg('--corpus', path.join(ROOT, 'data', 'source-coverage.json')));
|
|
79
123
|
const corpus = readJson(corpusFile);
|
|
80
|
-
const projection = createReleaseProjection({ corpusCoverage: corpus,
|
|
81
|
-
|
|
82
|
-
|
|
124
|
+
const projection = createReleaseProjection({ corpusCoverage: corpus, assetsDir: arg('--assets'),
|
|
125
|
+
outDir: arg('--out', 'release-evidence'), version: arg('--version'), sourceSnapshot: arg('--source-snapshot'),
|
|
126
|
+
baselineReceiptSha256: arg('--baseline-receipt-sha256'),
|
|
83
127
|
corpusSeed: { tag: arg('--seed-tag'), archiveSha256: arg('--seed-sha256'), archiveBytes: Number(arg('--seed-bytes')) } });
|
|
84
128
|
console.log(JSON.stringify({ ok: true, releaseCoverageGeneration: projection.releaseCoverageGeneration }));
|
|
85
129
|
} catch (error) { console.error(`[release-projection] ${error.message}`); process.exitCode = 1; }
|
package/scripts/release.mjs
CHANGED
|
@@ -32,10 +32,10 @@ import crypto from 'node:crypto';
|
|
|
32
32
|
import { validateProtectedPublishInvocation } from './protected-release-invocation.mjs';
|
|
33
33
|
import { runReleaseTransaction } from './release-transaction.mjs';
|
|
34
34
|
import { liveReleaseProvider } from './release-transaction-provider.mjs';
|
|
35
|
+
import { stagedHostVerifier } from './staged-host-verifier.mjs';
|
|
35
36
|
import { verifyPayload } from './release-payload.mjs';
|
|
36
37
|
|
|
37
38
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
38
|
-
const IS_MAIN = path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url);
|
|
39
39
|
const PUBLISH = process.argv.includes('--publish');
|
|
40
40
|
const CORPUS_SEED = process.argv.includes('--corpus-seed');
|
|
41
41
|
let protectedCandidate = null;
|
|
@@ -190,7 +190,7 @@ export function runProtectedCorpusSeed({
|
|
|
190
190
|
return { tag, target, repository: repo, archiveSha256, receiptSha256: sha256File(receiptFile) };
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
if (
|
|
193
|
+
if (CORPUS_SEED) {
|
|
194
194
|
try {
|
|
195
195
|
const result = runProtectedCorpusSeed();
|
|
196
196
|
console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
|
|
@@ -198,7 +198,7 @@ if (IS_MAIN && CORPUS_SEED) {
|
|
|
198
198
|
console.error(error.message);
|
|
199
199
|
process.exitCode = 1;
|
|
200
200
|
}
|
|
201
|
-
} else
|
|
201
|
+
} else {
|
|
202
202
|
|
|
203
203
|
console.log(`\n${c.b('RuvNet Brain — release / definition-of-done')} ${c.dim('· ' + (PUBLISH ? 'PUBLISH' : 'check-only') + ' · shipping ' + V())}\n`);
|
|
204
204
|
|
|
@@ -373,9 +373,6 @@ if (PUBLISH) {
|
|
|
373
373
|
corpusSeedSha256: crypto.createHash('sha256').update(fs.readFileSync(assets.corpusSeedPath)).digest('hex'),
|
|
374
374
|
generationLedgerSha256: crypto.createHash('sha256').update(fs.readFileSync(assets.generationLedgerPath)).digest('hex'),
|
|
375
375
|
};
|
|
376
|
-
const identityPath = path.resolve(ROOT, process.env.RUVNET_RELEASE_IDENTITY || 'release-evidence/release-identity.json');
|
|
377
|
-
if (fs.existsSync(identityPath)) throw new Error(`refusing to overwrite release identity: ${identityPath}`);
|
|
378
|
-
fs.writeFileSync(identityPath, `${JSON.stringify(identity, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
379
376
|
const privatePem = process.env.RUVNET_SIGNING_KEY;
|
|
380
377
|
if (!privatePem) throw new Error('RUVNET_SIGNING_KEY is required for signed transaction receipts');
|
|
381
378
|
const finalReceipt = await runReleaseTransaction({
|
|
@@ -386,12 +383,9 @@ if (PUBLISH) {
|
|
|
386
383
|
}),
|
|
387
384
|
privateKey: crypto.createPrivateKey(privatePem),
|
|
388
385
|
publicKey: crypto.createPublicKey(fs.readFileSync(path.join(ROOT, 'keys/ruvnet-brain-signing.pub.pem'), 'utf8')),
|
|
386
|
+
hostVerifier: stagedHostVerifier({ assets, identity }),
|
|
389
387
|
});
|
|
390
388
|
if (finalReceipt.state !== 'channels-converged') throw new Error(`release transaction stopped at ${finalReceipt.state}`);
|
|
391
|
-
const channelReceiptPath = path.resolve(ROOT,
|
|
392
|
-
process.env.RUVNET_CHANNEL_RECEIPT || 'release-evidence/channels-converged-receipt.json');
|
|
393
|
-
if (fs.existsSync(channelReceiptPath)) throw new Error(`refusing to overwrite channel receipt: ${channelReceiptPath}`);
|
|
394
|
-
fs.writeFileSync(channelReceiptPath, `${JSON.stringify(finalReceipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
395
389
|
} else {
|
|
396
390
|
step('D', 'remote staged release transaction — SKIPPED (check-only; pass --publish to publish)');
|
|
397
391
|
}
|
|
@@ -404,7 +398,8 @@ if (!PUBLISH) {
|
|
|
404
398
|
}
|
|
405
399
|
|
|
406
400
|
if (PUBLISH) {
|
|
407
|
-
console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))}
|
|
401
|
+
console.log(`\n${c.y(c.b('PUBLISHED, NOT VERIFIED'))}`);
|
|
402
|
+
console.log(`\n${c.g(c.b('✓✓✓ SHIPPED'))} — every gate passed and every live channel is current. ${c.dim('A user on any path (npm, npx, explainer, --update) gets the working, current build.')}\n`);
|
|
408
403
|
} else {
|
|
409
404
|
console.log(`\n${c.g(c.b('✓✓✓ PREFLIGHT PASS — NOT PUBLISHED'))} — the committed candidate passed every check-only gate.\n`);
|
|
410
405
|
}
|