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,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,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// scripts/refresh-model-catalog.mjs — pulls the LIVE OpenRouter model catalog and writes the committed
|
|
3
|
-
// snapshot data/openrouter-catalog-snapshot.json (with a pulledAt stamp)
|
|
3
|
+
// snapshot data/openrouter-catalog-snapshot.json (with a pulledAt stamp) and synchronizes the catalog's
|
|
4
|
+
// factual prices. verify-model-catalog.mjs then
|
|
4
5
|
// enforces, offline in CI, that every model in data/model-catalog.json exists and is priced correctly
|
|
5
|
-
// against this snapshot — and that the snapshot is fresh. Run
|
|
6
|
+
// against this snapshot — and that the snapshot is fresh. Run weekly (the anti-rot mechanism) + on demand.
|
|
6
7
|
// ADR-0016.
|
|
7
8
|
//
|
|
8
9
|
// It ALSO flags drift so a new flagship (e.g. a GPT-5.6-class release) surfaces instead of rotting:
|
|
@@ -45,6 +46,30 @@ export function detectDrift(catalog, models) {
|
|
|
45
46
|
return missing;
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/** Synchronize factual catalog prices from the same live map that backs the snapshot. */
|
|
50
|
+
export function syncCatalogPrices(catalog, models, pulledAt) {
|
|
51
|
+
const changes = [];
|
|
52
|
+
for (const [provider, entry] of Object.entries(catalog.providers || {})) {
|
|
53
|
+
if (entry.aliasOf) continue;
|
|
54
|
+
for (const tier of ['frontier', 'mid', 'cheap']) {
|
|
55
|
+
const candidate = entry[tier];
|
|
56
|
+
if (!candidate?.model) continue;
|
|
57
|
+
const live = models[candidate.model]
|
|
58
|
+
|| (!candidate.model.includes('/') ? models[`anthropic/${candidate.model}`] : null);
|
|
59
|
+
if (!live) continue;
|
|
60
|
+
if (candidate.in !== live.in || candidate.out !== live.out) {
|
|
61
|
+
changes.push({ provider, tier, model: candidate.model, from: { in: candidate.in, out: candidate.out }, to: live });
|
|
62
|
+
candidate.in = live.in;
|
|
63
|
+
candidate.out = live.out;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (catalog._meta?.sources) {
|
|
68
|
+
catalog._meta.sources.prices = `OpenRouter /api/v1/models live catalog, pulled ${pulledAt.slice(0, 10)} (in/out USD per Mtok).`;
|
|
69
|
+
}
|
|
70
|
+
return changes;
|
|
71
|
+
}
|
|
72
|
+
|
|
48
73
|
export function shapeSnapshot(models, pulledAt) {
|
|
49
74
|
return { _meta: { source: 'OpenRouter /api/v1/models (live metadata)', pulledAt, modelCount: Object.keys(models).length }, models };
|
|
50
75
|
}
|
|
@@ -73,7 +98,8 @@ async function main() {
|
|
|
73
98
|
|
|
74
99
|
const catalog = JSON.parse(fs.readFileSync(CATALOG, 'utf8'));
|
|
75
100
|
const missing = detectDrift(catalog, models);
|
|
76
|
-
const
|
|
101
|
+
const pulledAt = new Date().toISOString();
|
|
102
|
+
const snapshot = shapeSnapshot(models, pulledAt);
|
|
77
103
|
const next = JSON.stringify(snapshot, null, 2) + '\n';
|
|
78
104
|
|
|
79
105
|
if (check) {
|
|
@@ -86,8 +112,11 @@ async function main() {
|
|
|
86
112
|
return;
|
|
87
113
|
}
|
|
88
114
|
|
|
115
|
+
const priceChanges = syncCatalogPrices(catalog, models, pulledAt);
|
|
89
116
|
fs.writeFileSync(SNAPSHOT, next);
|
|
117
|
+
fs.writeFileSync(CATALOG, JSON.stringify(catalog, null, 2) + '\n');
|
|
90
118
|
console.log(`✓ wrote ${SNAPSHOT.replace(ROOT + '/', '')} — ${Object.keys(models).length} live models, pulledAt ${snapshot._meta.pulledAt}`);
|
|
119
|
+
console.log(`✓ synchronized ${priceChanges.length} catalog price field(s) from the same live response`);
|
|
91
120
|
if (missing.length) {
|
|
92
121
|
console.log(`\n⚠ DRIFT: ${missing.length} catalog model(s) no longer in the live catalog — update data/model-catalog.json:`);
|
|
93
122
|
for (const m of missing) console.log(' - ' + m);
|
|
@@ -11,7 +11,7 @@ const CANONICAL_PUBLISHERS = new Set([
|
|
|
11
11
|
'scripts/release.mjs',
|
|
12
12
|
'scripts/release-transaction-provider.mjs',
|
|
13
13
|
]);
|
|
14
|
-
const SOURCE_EXTENSIONS = new Set(['.mjs', '.js', '.cjs', '.sh']);
|
|
14
|
+
const SOURCE_EXTENSIONS = new Set(['.mjs', '.js', '.cjs', '.sh', '.yml', '.yaml']);
|
|
15
15
|
|
|
16
16
|
function executableSource(source) {
|
|
17
17
|
const withoutBlocks = source.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
@@ -28,7 +28,7 @@ const ACTIONS = [
|
|
|
28
28
|
jsPatterns: [
|
|
29
29
|
/\[\s*['"]release['"]\s*,\s*['"]create['"]/,
|
|
30
30
|
],
|
|
31
|
-
shellPatterns: [/^\s*gh\s+release\s+create\b/m],
|
|
31
|
+
shellPatterns: [/^\s*(?:-\s*run:\s*)?gh\s+release\s+create\b/m],
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
action: 'github-release-update',
|
|
@@ -36,7 +36,7 @@ const ACTIONS = [
|
|
|
36
36
|
/['"]PATCH['"][\s\S]{0,120}releases\//,
|
|
37
37
|
/\[\s*['"]release['"]\s*,\s*['"]edit['"]/,
|
|
38
38
|
],
|
|
39
|
-
shellPatterns: [/^\s*gh\s+(?:release\s+edit|api\s+.*releases\/.*PATCH)\b/m],
|
|
39
|
+
shellPatterns: [/^\s*(?:-\s*run:\s*)?gh\s+(?:release\s+edit|api\s+.*releases\/.*PATCH)\b/m],
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
action: 'npm-publish',
|
|
@@ -44,7 +44,7 @@ const ACTIONS = [
|
|
|
44
44
|
/(?:execFileSync|spawnSync)\(\s*['"]npm['"]\s*,\s*\[\s*['"]publish['"]/,
|
|
45
45
|
/runOrDie\(\s*['"]npm publish['"]/,
|
|
46
46
|
],
|
|
47
|
-
shellPatterns: [/^\s*npm\s+publish\b/m],
|
|
47
|
+
shellPatterns: [/^\s*(?:-\s*run:\s*)?npm\s+publish\b/m],
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
action: 'npm-dist-tag',
|
|
@@ -52,7 +52,7 @@ const ACTIONS = [
|
|
|
52
52
|
/(?:execFileSync|spawnSync)\(\s*['"]npm['"]\s*,\s*\[\s*['"]dist-tag['"]\s*,\s*['"]add['"]/,
|
|
53
53
|
/runOrDie\(\s*['"]npm dist-tag[^'"]*['"]/,
|
|
54
54
|
],
|
|
55
|
-
shellPatterns: [/^\s*npm\s+dist-tag\s+add\b/m],
|
|
55
|
+
shellPatterns: [/^\s*(?:-\s*run:\s*)?npm\s+dist-tag\s+add\b/m],
|
|
56
56
|
},
|
|
57
57
|
];
|
|
58
58
|
|
|
@@ -60,7 +60,10 @@ export function detectPublisherActions(file, source) {
|
|
|
60
60
|
const relative = file.split(path.sep).join('/');
|
|
61
61
|
if (CANONICAL_PUBLISHERS.has(relative)) return [];
|
|
62
62
|
const executable = executableSource(source);
|
|
63
|
-
const
|
|
63
|
+
const extension = path.extname(relative);
|
|
64
|
+
const kind = ['.sh', '.yml', '.yaml'].includes(extension) || relative.startsWith('package.json#scripts.')
|
|
65
|
+
? 'shellPatterns'
|
|
66
|
+
: 'jsPatterns';
|
|
64
67
|
return ACTIONS
|
|
65
68
|
.filter((action) => action[kind].some((pattern) => pattern.test(executable)))
|
|
66
69
|
.map(({ action }) => ({ file: relative, action }));
|
|
@@ -79,14 +82,23 @@ function sourceFiles(root) {
|
|
|
79
82
|
visit(path.join(root, 'scripts'));
|
|
80
83
|
visit(path.join(root, 'deploy'));
|
|
81
84
|
visit(path.join(root, 'bin'));
|
|
85
|
+
visit(path.join(root, '.github', 'workflows'));
|
|
82
86
|
return files;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
export function findUnauthorizedPublishers(root = ROOT) {
|
|
86
|
-
|
|
90
|
+
const findings = sourceFiles(root).flatMap((absolute) => {
|
|
87
91
|
const relative = path.relative(root, absolute).split(path.sep).join('/');
|
|
88
92
|
return detectPublisherActions(relative, fs.readFileSync(absolute, 'utf8'));
|
|
89
93
|
});
|
|
94
|
+
const packageFile = path.join(root, 'package.json');
|
|
95
|
+
if (fs.existsSync(packageFile)) {
|
|
96
|
+
const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
97
|
+
for (const [name, command] of Object.entries(pkg.scripts || {})) {
|
|
98
|
+
findings.push(...detectPublisherActions(`package.json#scripts.${name}`, String(command)));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return findings;
|
|
90
102
|
}
|
|
91
103
|
|
|
92
104
|
export function main(root = ROOT) {
|
|
@@ -11,18 +11,16 @@
|
|
|
11
11
|
* for a month.
|
|
12
12
|
*
|
|
13
13
|
* WHAT IT WILL NOT DO. It does not publish. `scripts/self-update.mjs:56` refuses `--publish` and
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* still runs inside the workflow exactly as designed. Bypassing them to "get it done while nobody
|
|
18
|
-
* is looking" would be the worst possible reading of an instruction to finish the job.
|
|
14
|
+
* `release-candidate-preflight.yml` owns the one-time long lanes and `protected-release.yml` is the
|
|
15
|
+
* sole publication controller. This watchdog is report-only: it never dispatches either phase and
|
|
16
|
+
* cannot manufacture typed candidate evidence.
|
|
19
17
|
*
|
|
20
18
|
* IT IS A NO-OP UNLESS EVERY PRECONDITION HOLDS. It runs from the nightly, unattended, for weeks.
|
|
21
19
|
* A watchdog that acts on a partial picture is worse than no watchdog, so it refuses on anything
|
|
22
20
|
* unexpected and says why. The default outcome is "nothing happened, here is the reason".
|
|
23
21
|
*
|
|
24
22
|
* node scripts/release-convergence-watchdog.mjs # report only, never acts
|
|
25
|
-
* node scripts/release-convergence-watchdog.mjs --dispatch #
|
|
23
|
+
* node scripts/release-convergence-watchdog.mjs --dispatch # compatibility flag; still reports only
|
|
26
24
|
*/
|
|
27
25
|
import { execFileSync } from 'node:child_process';
|
|
28
26
|
import fs from 'node:fs';
|
|
@@ -99,21 +97,11 @@ const runsFor = (wf) => JSON.parse(tryShy(() => sh('gh', ['run', 'list', '--repo
|
|
|
99
97
|
const greenAt = (wf) => runsFor(wf).find((r) => r.headSha === originMain && r.conclusion === 'success');
|
|
100
98
|
|
|
101
99
|
const ci = greenAt('ci');
|
|
102
|
-
const aggregate = greenAt('release-aggregate');
|
|
103
100
|
if (!ci) stand_down(`no successful exact-SHA \`ci\` run at ${originMain.slice(0, 7)} yet`);
|
|
104
|
-
if (!aggregate) stand_down(`no successful exact-SHA \`release-aggregate\` run at ${originMain.slice(0, 7)} yet`);
|
|
105
101
|
|
|
106
|
-
log(`
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ── 6. Dispatch THE SANCTIONED PUBLISHER. Nothing here publishes; the workflow does. ─────────────
|
|
113
|
-
sh('gh', ['workflow', 'run', 'protected-release.yml', '--repo', REPO,
|
|
114
|
-
'-f', `candidate_sha=${originMain}`,
|
|
115
|
-
'-f', `version=${version}`,
|
|
116
|
-
'-f', `release_qe_run_id=${ci.databaseId}`,
|
|
117
|
-
'-f', `aggregate_run_id=${aggregate.databaseId}`]);
|
|
118
|
-
log(`DISPATCHED protected-release for ${version}. The workflow owns every safety gate from here.`);
|
|
119
|
-
log('Verify afterwards with: node scripts/published-surface-probe.mjs (D-version-coherence must PASS).');
|
|
102
|
+
log(`PRECONDITIONS VISIBLE — candidate=${originMain.slice(0, 7)} version=${version} ci=${ci.databaseId}`);
|
|
103
|
+
// ── 6. Never dispatch a publisher from the watchdog. ───────────────────────────────────────────
|
|
104
|
+
// Only protected-release may derive the complete typed evidence chain and cross Production.
|
|
105
|
+
// This unattended observer cannot turn visible preconditions into release authority.
|
|
106
|
+
log(`PRECONDITIONS ONLY — no candidate artifact inferred and no action taken for ${version} at ${originMain}`);
|
|
107
|
+
log('Run release-candidate-preflight on release/** once; fast-forward that exact SHA to main; then protected-release imports release-candidate-<SHA> without a run ID.');
|
|
@@ -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; }
|