ruvnet-brain 4.0.36 → 4.2.2-dev
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -5
- package/bin/install.mjs +405 -23
- package/data/model-catalog.json +104 -15
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/brain-console.md +72 -9
- package/plugin/commands/configure.md +67 -21
- package/plugin/commands/rvcb.md +72 -9
- package/plugin/hooks/codex-hooks.json +40 -33
- package/plugin/hooks/hook-contracts.json +14 -24
- package/plugin/hooks/hooks.json +7 -42
- package/plugin/mcp/server.mjs +23 -6
- package/plugin/scripts/adr-currency-gate.mjs +150 -0
- package/plugin/scripts/capability-registry.mjs +10 -1
- package/plugin/scripts/codex-hook-adapter.mjs +121 -19
- package/plugin/scripts/codex-hook-wrapper.mjs +61 -4
- package/plugin/scripts/continuation-gate.mjs +148 -8
- package/plugin/scripts/decision-gate.mjs +428 -0
- package/plugin/scripts/decision-outcomes.mjs +231 -0
- package/plugin/scripts/degradation-watch.mjs +271 -0
- package/plugin/scripts/ground-ruvnet.sh +51 -11
- package/plugin/scripts/hijack-ruvnet.sh +11 -3
- package/plugin/scripts/hook-registry.mjs +48 -3
- package/plugin/scripts/hook-shim.mjs +58 -3
- package/plugin/scripts/identifier-preflight.mjs +134 -0
- package/plugin/scripts/learn-capture.sh +50 -1
- package/plugin/scripts/learn-flush.mjs +5 -5
- package/plugin/scripts/lesson-bridge.mjs +343 -0
- package/plugin/scripts/lesson-hooks.sh +26 -0
- package/plugin/scripts/lesson-promote.mjs +50 -0
- package/plugin/scripts/lesson-store.mjs +6 -1
- package/plugin/scripts/mcp-readiness.mjs +107 -0
- package/plugin/scripts/protect-brain-state.sh +9 -0
- package/plugin/scripts/runtime-preferences.mjs +40 -0
- package/plugin/scripts/session-snapshot-hook.mjs +15 -6
- package/plugin/scripts/spend-guard.mjs +125 -0
- package/plugin/scripts/unprompted-runtime.mjs +12 -2
- package/plugin/scripts/update-apply.mjs +7 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +20 -5
- package/plugin/skills/ruvnet-brain/SKILL.md +3 -3
- package/scripts/brain-score.mjs +261 -0
- package/scripts/brain-stamp.mjs +5 -1
- package/scripts/build-bundle.mjs +38 -4
- package/scripts/card-from-source.mjs +114 -0
- package/scripts/console-engine.mjs +1 -1
- package/scripts/corpus-candidate.mjs +294 -0
- package/scripts/corpus-reconcile.mjs +273 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/health-repair.mjs +11 -2
- package/scripts/ingest-new-repos.mjs +122 -0
- package/scripts/ingest-repo.mjs +66 -6
- package/scripts/learning-replay-cli.mjs +8 -3
- package/scripts/learning-replay-fixture.mjs +25 -6
- package/scripts/learning-replay-proof.mjs +38 -0
- package/scripts/nightly-wrapper.sh +55 -7
- package/scripts/onboarding-console.mjs +21 -1
- package/scripts/org-repo-count.mjs +119 -0
- package/scripts/rebuild-gists-from-receipts.mjs +246 -0
- package/scripts/release-transaction.mjs +30 -2
- package/scripts/release.mjs +147 -0
- package/scripts/repo-count-detector.mjs +62 -0
- package/scripts/restore-local-ingests.mjs +116 -0
- package/scripts/rvf-generation.mjs +44 -0
- package/scripts/selfcheck.mjs +9 -1
- package/scripts/stabilization-receipt.mjs +11 -1
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-commands.mjs +117 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Seal and independently verify the exact public corpus bytes used by a release.
|
|
3
|
+
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const REQUIRED_SUFFIXES = [
|
|
13
|
+
'.big.rvf',
|
|
14
|
+
'.big.rvf.idmap.json',
|
|
15
|
+
'.big.rvf.embed.json',
|
|
16
|
+
'.passages.jsonl',
|
|
17
|
+
'.meta.json',
|
|
18
|
+
];
|
|
19
|
+
const OPTIONAL_SHIPPED_SUFFIXES = ['.big.passages.jsonl', '.big.meta.json'];
|
|
20
|
+
|
|
21
|
+
function fail(message) {
|
|
22
|
+
throw new Error(`[corpus-candidate] ${message}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readJson(file, label) {
|
|
26
|
+
if (!file || !fs.existsSync(file)) fail(`${label} missing (${file || 'no path supplied'})`);
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
fail(`${label} unreadable/corrupt (${error.message})`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sha256File(file) {
|
|
35
|
+
const hash = crypto.createHash('sha256');
|
|
36
|
+
const fd = fs.openSync(file, 'r');
|
|
37
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
38
|
+
try {
|
|
39
|
+
let bytes;
|
|
40
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) {
|
|
41
|
+
hash.update(buffer.subarray(0, bytes));
|
|
42
|
+
}
|
|
43
|
+
} finally {
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
}
|
|
46
|
+
return hash.digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fileIdentity(file) {
|
|
50
|
+
return { file: path.basename(file), sha256: sha256File(file), bytes: fs.statSync(file).size };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function canonicalJson(value) {
|
|
54
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
55
|
+
if (value && typeof value === 'object') {
|
|
56
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
57
|
+
}
|
|
58
|
+
return JSON.stringify(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function policyRows(policy) {
|
|
62
|
+
if (Array.isArray(policy)) return policy;
|
|
63
|
+
for (const key of ['sources', 'rows', 'repositories', 'repos']) {
|
|
64
|
+
if (Array.isArray(policy?.[key])) return policy[key];
|
|
65
|
+
}
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertPolicyCurrent(policy) {
|
|
70
|
+
const rows = policyRows(policy);
|
|
71
|
+
const stale = rows.filter((row) => row?.eligible !== false && row?.status !== 'CURRENT');
|
|
72
|
+
if (stale.length) fail(`eligibility policy has ${stale.length} eligible row(s) that are not CURRENT`);
|
|
73
|
+
return {
|
|
74
|
+
eligibleRepoCount: rows.filter((row) => row?.eligible !== false && row?.status === 'CURRENT').length,
|
|
75
|
+
gistCount: rows.filter((row) => /gist/i.test(String(row?.kind || row?.type || ''))).length,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function archiveEntries(bundleFile) {
|
|
80
|
+
const result = spawnSync('unzip', ['-Z1', bundleFile], { encoding: 'utf8' });
|
|
81
|
+
if (result.status !== 0) fail(`cannot list archive (${result.stderr.trim() || `exit ${result.status}`})`);
|
|
82
|
+
const entries = result.stdout.split(/\r?\n/).filter(Boolean);
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const normalized = entry.replaceAll('\\', '/');
|
|
85
|
+
if (normalized.startsWith('/') || normalized.split('/').includes('..')) {
|
|
86
|
+
fail(`archive contains unsafe path ${JSON.stringify(entry)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return entries;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function verifyArchive({ bundleFile, stores, privateStores }) {
|
|
93
|
+
if (!bundleFile || !fs.existsSync(bundleFile)) fail(`bundle missing (${bundleFile || 'no path supplied'})`);
|
|
94
|
+
const entries = archiveEntries(bundleFile);
|
|
95
|
+
for (const privateStore of privateStores) {
|
|
96
|
+
const escaped = privateStore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
97
|
+
const privateFile = new RegExp(`(?:^|/)${escaped}(?:\\.|$)`, 'i');
|
|
98
|
+
if (entries.some((entry) => privateFile.test(entry))) fail(`private store ${privateStore} is present in archive`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'corpus-candidate-'));
|
|
102
|
+
try {
|
|
103
|
+
const extraction = spawnSync('unzip', ['-qq', bundleFile, '-d', tmp], { encoding: 'utf8' });
|
|
104
|
+
if (extraction.status !== 0) fail(`cannot extract archive (${extraction.stderr.trim() || `exit ${extraction.status}`})`);
|
|
105
|
+
for (const store of stores) {
|
|
106
|
+
for (const expected of store.files) {
|
|
107
|
+
const matches = entries.filter((entry) => path.posix.basename(entry) === expected.file);
|
|
108
|
+
if (matches.length !== 1) fail(`archive must contain exactly one ${expected.file}; found ${matches.length}`);
|
|
109
|
+
const extracted = path.join(tmp, ...matches[0].replaceAll('\\', '/').split('/'));
|
|
110
|
+
if (sha256File(extracted) !== expected.sha256) fail(`archive ${expected.file} differs from canonical assets`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
return fileIdentity(bundleFile);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildReceipt({
|
|
120
|
+
assetsDir,
|
|
121
|
+
bundleFile,
|
|
122
|
+
policyFile,
|
|
123
|
+
builderSourceSha,
|
|
124
|
+
createdAt,
|
|
125
|
+
}) {
|
|
126
|
+
const assets = path.resolve(assetsDir || '');
|
|
127
|
+
if (!fs.existsSync(assets) || !fs.statSync(assets).isDirectory()) fail(`assets directory missing (${assets})`);
|
|
128
|
+
if (!/^[a-f0-9]{40,64}$/i.test(builderSourceSha || '')) fail('builderSourceSha must be a 40-64 hex source identity');
|
|
129
|
+
|
|
130
|
+
const fenceFile = path.join(assets, 'PRIVATE-STORES.json');
|
|
131
|
+
const fence = readJson(fenceFile, 'private fence');
|
|
132
|
+
if (!Array.isArray(fence.privateStores)) fail('private fence has no privateStores array');
|
|
133
|
+
const privateStores = [...new Set(fence.privateStores.map((name) => String(name).toLowerCase()))].sort();
|
|
134
|
+
const privateSet = new Set(privateStores);
|
|
135
|
+
const ledgerFile = path.join(assets, 'RVF-GENERATIONS.json');
|
|
136
|
+
const ledger = readJson(ledgerFile, 'RVF generation ledger');
|
|
137
|
+
if (!ledger.stores || typeof ledger.stores !== 'object' || Array.isArray(ledger.stores)) {
|
|
138
|
+
fail('RVF generation ledger has no stores object');
|
|
139
|
+
}
|
|
140
|
+
const policy = readJson(path.resolve(policyFile || ''), 'eligibility policy');
|
|
141
|
+
const counts = assertPolicyCurrent(policy);
|
|
142
|
+
|
|
143
|
+
const rvfFiles = fs.readdirSync(assets).filter((name) => /^.+\.big\.rvf$/.test(name)).sort();
|
|
144
|
+
const rvfByStore = new Map(rvfFiles.map((file) => [file.slice(0, -'.big.rvf'.length), file]));
|
|
145
|
+
const unreceipted = [...rvfByStore.keys()].filter((store) => !ledger.stores[store]);
|
|
146
|
+
if (unreceipted.length) fail(`unreceipted RVF files: ${unreceipted.join(', ')}`);
|
|
147
|
+
const orphanLedger = Object.entries(ledger.stores)
|
|
148
|
+
.filter(([store, generation]) => generation.file !== `${store}.big.rvf` || !rvfByStore.has(store))
|
|
149
|
+
.map(([store]) => store);
|
|
150
|
+
if (orphanLedger.length) fail(`ledger rows without RVFs: ${orphanLedger.join(', ')}`);
|
|
151
|
+
|
|
152
|
+
const digestOwners = new Map();
|
|
153
|
+
for (const [store, file] of rvfByStore) {
|
|
154
|
+
const digest = sha256File(path.join(assets, file));
|
|
155
|
+
const owners = digestOwners.get(digest) || [];
|
|
156
|
+
owners.push(store);
|
|
157
|
+
digestOwners.set(digest, owners);
|
|
158
|
+
}
|
|
159
|
+
const duplicateRvfDigests = [...digestOwners.entries()]
|
|
160
|
+
.filter(([, owners]) => owners.length > 1)
|
|
161
|
+
.map(([sha256, owners]) => ({ sha256, stores: owners.sort() }));
|
|
162
|
+
if (duplicateRvfDigests.length) fail(`duplicate RVF bytes: ${duplicateRvfDigests.map((row) => row.stores.join('/')).join(', ')}`);
|
|
163
|
+
|
|
164
|
+
const missingSidecars = [];
|
|
165
|
+
const stores = [];
|
|
166
|
+
for (const [store, rvfFile] of [...rvfByStore].sort(([a], [b]) => a.localeCompare(b))) {
|
|
167
|
+
const generation = ledger.stores[store];
|
|
168
|
+
const rvfPath = path.join(assets, rvfFile);
|
|
169
|
+
if (generation.file !== rvfFile || generation.sha256 !== sha256File(rvfPath) || generation.bytes !== fs.statSync(rvfPath).size) {
|
|
170
|
+
fail(`${store}: generation receipt does not match RVF bytes`);
|
|
171
|
+
}
|
|
172
|
+
if (!generation.sourceCommit || !generation.builtUtc || !generation.model || !Number.isInteger(generation.dimensions)) {
|
|
173
|
+
fail(`${store}: generation receipt lacks sourceCommit, builtUtc, model, or dimensions provenance`);
|
|
174
|
+
}
|
|
175
|
+
if (privateSet.has(store.toLowerCase())) continue;
|
|
176
|
+
const files = [];
|
|
177
|
+
for (const suffix of REQUIRED_SUFFIXES) {
|
|
178
|
+
const file = path.join(assets, `${store}${suffix}`);
|
|
179
|
+
if (!fs.existsSync(file)) missingSidecars.push(path.basename(file));
|
|
180
|
+
else files.push(fileIdentity(file));
|
|
181
|
+
}
|
|
182
|
+
for (const suffix of OPTIONAL_SHIPPED_SUFFIXES) {
|
|
183
|
+
const file = path.join(assets, `${store}${suffix}`);
|
|
184
|
+
if (fs.existsSync(file)) files.push(fileIdentity(file));
|
|
185
|
+
}
|
|
186
|
+
stores.push({
|
|
187
|
+
name: store,
|
|
188
|
+
sourceCommit: generation.sourceCommit,
|
|
189
|
+
builtUtc: generation.builtUtc,
|
|
190
|
+
model: generation.model,
|
|
191
|
+
dimensions: generation.dimensions,
|
|
192
|
+
files,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (missingSidecars.length) fail(`missing sidecars: ${missingSidecars.join(', ')}`);
|
|
196
|
+
if (!stores.length) fail('zero public corpus stores remain after the private fence');
|
|
197
|
+
const archive = verifyArchive({ bundleFile: path.resolve(bundleFile || ''), stores, privateStores });
|
|
198
|
+
const policyIdentity = fileIdentity(path.resolve(policyFile));
|
|
199
|
+
const fenceIdentity = fileIdentity(fenceFile);
|
|
200
|
+
const ledgerIdentity = fileIdentity(ledgerFile);
|
|
201
|
+
const coverageGeneration = String(policy.coverageGeneration || policy.generation || policy.generated || policyIdentity.sha256);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
schemaVersion: 1,
|
|
205
|
+
kind: 'ruvnet-brain-corpus-candidate',
|
|
206
|
+
builderSourceSha: builderSourceSha.toLowerCase(),
|
|
207
|
+
createdAt,
|
|
208
|
+
coverageGeneration,
|
|
209
|
+
eligibleRepoCount: counts.eligibleRepoCount,
|
|
210
|
+
gistCount: counts.gistCount,
|
|
211
|
+
storeCount: stores.length,
|
|
212
|
+
stores,
|
|
213
|
+
privateFence: fenceIdentity,
|
|
214
|
+
eligibilityPolicy: policyIdentity,
|
|
215
|
+
generationLedger: ledgerIdentity,
|
|
216
|
+
excludedPrivateStores: privateStores.filter((privateStore) =>
|
|
217
|
+
[...rvfByStore.keys()].some((store) => store.toLowerCase() === privateStore)),
|
|
218
|
+
duplicateRvfDigests,
|
|
219
|
+
unreceiptedRvfFiles: [],
|
|
220
|
+
missingSidecars: [],
|
|
221
|
+
archive,
|
|
222
|
+
generator: { corpusCandidateSha256: sha256File(fileURLToPath(import.meta.url)) },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function currentGitSha() {
|
|
227
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: path.resolve(HERE, '..'), encoding: 'utf8' });
|
|
228
|
+
if (result.status !== 0) fail('cannot determine builder source SHA');
|
|
229
|
+
return result.stdout.trim();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function createCorpusReceipt(options) {
|
|
233
|
+
const receiptFile = path.resolve(options.receiptFile || 'dist/corpus-receipt.json');
|
|
234
|
+
const receipt = buildReceipt({
|
|
235
|
+
...options,
|
|
236
|
+
builderSourceSha: options.builderSourceSha || currentGitSha(),
|
|
237
|
+
createdAt: options.createdAt || new Date().toISOString(),
|
|
238
|
+
});
|
|
239
|
+
fs.mkdirSync(path.dirname(receiptFile), { recursive: true });
|
|
240
|
+
fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`, { flag: options.overwrite === false ? 'wx' : 'w' });
|
|
241
|
+
return receipt;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function verifyCorpusReceipt(options) {
|
|
245
|
+
const receipt = readJson(path.resolve(options.receiptFile || ''), 'corpus receipt');
|
|
246
|
+
if (receipt.kind !== 'ruvnet-brain-corpus-candidate' || receipt.schemaVersion !== 1) fail('unsupported corpus receipt');
|
|
247
|
+
const exactFiles = [
|
|
248
|
+
['archive', path.resolve(options.bundleFile || '')],
|
|
249
|
+
['eligibility policy', path.resolve(options.policyFile || '')],
|
|
250
|
+
['private fence', path.join(path.resolve(options.assetsDir || ''), 'PRIVATE-STORES.json')],
|
|
251
|
+
['generation ledger', path.join(path.resolve(options.assetsDir || ''), 'RVF-GENERATIONS.json')],
|
|
252
|
+
];
|
|
253
|
+
for (const [label, file] of exactFiles) {
|
|
254
|
+
if (!fs.existsSync(file)) fail(`${label} missing (${file})`);
|
|
255
|
+
const expected = label === 'archive'
|
|
256
|
+
? receipt.archive
|
|
257
|
+
: label === 'eligibility policy'
|
|
258
|
+
? receipt.eligibilityPolicy
|
|
259
|
+
: label === 'private fence'
|
|
260
|
+
? receipt.privateFence
|
|
261
|
+
: receipt.generationLedger;
|
|
262
|
+
if (sha256File(file) !== expected?.sha256) fail(`${label} sha256 differs from receipt`);
|
|
263
|
+
if (fs.statSync(file).size !== expected?.bytes) fail(`${label} byte length differs from receipt`);
|
|
264
|
+
}
|
|
265
|
+
const actual = buildReceipt({
|
|
266
|
+
...options,
|
|
267
|
+
builderSourceSha: receipt.builderSourceSha,
|
|
268
|
+
createdAt: receipt.createdAt,
|
|
269
|
+
});
|
|
270
|
+
if (canonicalJson(actual) !== canonicalJson(receipt)) fail('receipt does not match the exact corpus inputs');
|
|
271
|
+
return receipt;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function arg(name, fallback) {
|
|
275
|
+
const index = process.argv.indexOf(name);
|
|
276
|
+
return index >= 0 ? process.argv[index + 1] : fallback;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function main() {
|
|
280
|
+
const mode = process.argv.includes('--verify') ? 'verify' : 'create';
|
|
281
|
+
const options = {
|
|
282
|
+
assetsDir: arg('--assets', 'kb'),
|
|
283
|
+
bundleFile: arg('--bundle', 'dist/ruvnet-brain.zip'),
|
|
284
|
+
policyFile: arg('--policy'),
|
|
285
|
+
receiptFile: arg('--receipt', arg('--out', 'dist/corpus-receipt.json')),
|
|
286
|
+
builderSourceSha: arg('--builder-source-sha'),
|
|
287
|
+
};
|
|
288
|
+
const receipt = mode === 'verify' ? verifyCorpusReceipt(options) : createCorpusReceipt(options);
|
|
289
|
+
console.log(JSON.stringify({ ok: true, mode, archive: receipt.archive, stores: receipt.storeCount }, null, 2));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
293
|
+
main().catch((error) => { console.error(error.message); process.exitCode = 1; });
|
|
294
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build a corpus candidate from one immutable seed and exact upstream repository SHAs.
|
|
3
|
+
// This module deliberately has no publication capability. The protected-release workflow owns
|
|
4
|
+
// the only legal call to corpus-seed-publish.mjs.
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { extractZip } from '../kb/zip-extract.mjs';
|
|
12
|
+
|
|
13
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const DEFAULT_ROOT = path.resolve(HERE, '..');
|
|
15
|
+
const HEX40 = /^[0-9a-f]{40}$/;
|
|
16
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
17
|
+
const SAFE_STORE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
18
|
+
|
|
19
|
+
function fail(message) {
|
|
20
|
+
throw new Error(`[corpus-reconcile] ${message}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function sha256File(file) {
|
|
24
|
+
if (!file || !fs.existsSync(file) || !fs.statSync(file).isFile()) fail(`seed archive missing (${file || 'no path supplied'})`);
|
|
25
|
+
const hash = crypto.createHash('sha256');
|
|
26
|
+
const fd = fs.openSync(file, 'r');
|
|
27
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
28
|
+
try {
|
|
29
|
+
let bytes;
|
|
30
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
|
|
31
|
+
} finally {
|
|
32
|
+
fs.closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
return hash.digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readJson(file, label) {
|
|
38
|
+
if (!file || !fs.existsSync(file)) fail(`${label} missing (${file || 'no path supplied'})`);
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
41
|
+
} catch (error) {
|
|
42
|
+
fail(`${label} unreadable (${error.message})`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function assertBootstrapIdentity({ archiveFile, tag, sha256, allowPinnedTag = false }) {
|
|
47
|
+
const expected = String(sha256 || '').toLowerCase();
|
|
48
|
+
if (!HEX64.test(expected) || (!allowPinnedTag && tag !== `corpus-sha256-${expected}`) || !tag || tag === 'latest') {
|
|
49
|
+
fail('bootstrap requires the exact digest-derived tag corpus-sha256-<configured sha256>; latest is forbidden');
|
|
50
|
+
}
|
|
51
|
+
const actual = sha256File(path.resolve(archiveFile || ''));
|
|
52
|
+
if (actual !== expected) fail(`downloaded archive sha256 ${actual} differs from configured ${expected}`);
|
|
53
|
+
return { tag, sha256: expected };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function filesNamed(root, wanted) {
|
|
57
|
+
const found = [];
|
|
58
|
+
const visit = (dir) => {
|
|
59
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
60
|
+
const file = path.join(dir, entry.name);
|
|
61
|
+
if (entry.isDirectory()) visit(file);
|
|
62
|
+
else if (entry.isFile() && entry.name === wanted) found.push(file);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
visit(root);
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function normalizeExtractedCorpus({ extractedDir, assetsDir }) {
|
|
70
|
+
const extracted = path.resolve(extractedDir || '');
|
|
71
|
+
const assets = path.resolve(assetsDir || '');
|
|
72
|
+
if (!fs.existsSync(extracted) || !fs.statSync(extracted).isDirectory()) {
|
|
73
|
+
fail(`extracted seed directory missing (${extracted})`);
|
|
74
|
+
}
|
|
75
|
+
if (fs.existsSync(assets) && fs.readdirSync(assets).length) fail(`bootstrap assets directory is not empty (${assets})`);
|
|
76
|
+
const ledgers = filesNamed(extracted, 'RVF-GENERATIONS.json');
|
|
77
|
+
if (ledgers.length !== 1) fail(`seed archive must contain exactly one RVF-GENERATIONS.json; found ${ledgers.length}`);
|
|
78
|
+
const corpusRoot = path.dirname(ledgers[0]);
|
|
79
|
+
if (fs.existsSync(path.join(corpusRoot, 'PRIVATE-STORES.json'))) {
|
|
80
|
+
fail('a published seed must not supply a private-store fence; the exact builder checkout owns that policy');
|
|
81
|
+
}
|
|
82
|
+
fs.mkdirSync(assets, { recursive: true });
|
|
83
|
+
for (const entry of fs.readdirSync(corpusRoot)) fs.renameSync(path.join(corpusRoot, entry), path.join(assets, entry));
|
|
84
|
+
return assets;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function repositorySlug(url) {
|
|
88
|
+
const match = String(url || '').match(/^https:\/\/github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?$/i);
|
|
89
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function planReconciliation({ coverage, ledger, assetsDir = null }) {
|
|
93
|
+
if (coverage?.schemaVersion !== 1 || !Array.isArray(coverage.rows) || !coverage.coverageGeneration) {
|
|
94
|
+
fail('coverage policy is missing a supported complete generation');
|
|
95
|
+
}
|
|
96
|
+
if (!ledger?.stores || typeof ledger.stores !== 'object' || Array.isArray(ledger.stores)) {
|
|
97
|
+
fail('RVF generation ledger has no stores object');
|
|
98
|
+
}
|
|
99
|
+
const eligible = coverage.rows.filter((row) => row?.kind === 'repository' && row?.disposition === 'eligible');
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const plan = [];
|
|
102
|
+
for (const row of eligible) {
|
|
103
|
+
const store = String(row?.artifact?.store || '');
|
|
104
|
+
if (!SAFE_STORE.test(store)) fail(`${row?.name || row?.key || 'eligible repository'} has an unsafe or missing store name`);
|
|
105
|
+
const folded = store.toLowerCase();
|
|
106
|
+
if (seen.has(folded)) fail(`duplicate eligible store ${store} in coverage policy`);
|
|
107
|
+
seen.add(folded);
|
|
108
|
+
const upstreamSha = String(row?.upstream?.sha || '').toLowerCase();
|
|
109
|
+
if (!HEX40.test(upstreamSha)) fail(`${row?.name || store} has a missing or malformed upstream SHA`);
|
|
110
|
+
if (!repositorySlug(row.url)) fail(`${row?.name || store} has no exact GitHub repository URL`);
|
|
111
|
+
const generation = Object.entries(ledger.stores).find(([name]) => name.toLowerCase() === folded)?.[1] || null;
|
|
112
|
+
const current = String(generation?.sourceCommit || '').toLowerCase();
|
|
113
|
+
let reason = generation?.sourceCommit ? 'sourceCommit differs' : 'missing ledger receipt';
|
|
114
|
+
if (current === upstreamSha) {
|
|
115
|
+
if (!assetsDir) continue;
|
|
116
|
+
const expectedFile = `${store}.big.rvf`;
|
|
117
|
+
const rvfFile = path.join(path.resolve(assetsDir), expectedFile);
|
|
118
|
+
const receiptMatches = generation?.file === expectedFile
|
|
119
|
+
&& fs.existsSync(rvfFile)
|
|
120
|
+
&& generation?.bytes === fs.statSync(rvfFile).size
|
|
121
|
+
&& generation?.sha256 === sha256File(rvfFile);
|
|
122
|
+
if (receiptMatches) continue;
|
|
123
|
+
reason = 'generation receipt differs from seed bytes';
|
|
124
|
+
}
|
|
125
|
+
plan.push({
|
|
126
|
+
name: String(row.name || store),
|
|
127
|
+
store,
|
|
128
|
+
url: row.url,
|
|
129
|
+
upstreamSha,
|
|
130
|
+
ledgerSourceCommit: generation?.sourceCommit || null,
|
|
131
|
+
reason,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return plan.sort((a, b) => a.store.localeCompare(b.store));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function defaultRun(command, args, options = {}) {
|
|
138
|
+
return spawnSync(command, args, { encoding: 'utf8', ...options });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function checked(run, command, args, options = {}) {
|
|
142
|
+
const result = run(command, args, options) || {};
|
|
143
|
+
if (result.error || result.status !== 0) {
|
|
144
|
+
const detail = String(result.stderr || result.stdout || result.error?.message || `exit ${result.status}`).trim();
|
|
145
|
+
fail(`${command} ${args.join(' ')} failed${detail ? ` (${detail})` : ''}`);
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function executeReconciliation({
|
|
151
|
+
plan,
|
|
152
|
+
assetsDir,
|
|
153
|
+
workspaceDir,
|
|
154
|
+
root = DEFAULT_ROOT,
|
|
155
|
+
run = defaultRun,
|
|
156
|
+
}) {
|
|
157
|
+
if (!Array.isArray(plan)) fail('reconciliation plan must be an array');
|
|
158
|
+
const assets = path.resolve(assetsDir || '');
|
|
159
|
+
const workspace = path.resolve(workspaceDir || '');
|
|
160
|
+
const ledgerFile = path.join(assets, 'RVF-GENERATIONS.json');
|
|
161
|
+
if (!fs.existsSync(ledgerFile)) fail(`RVF generation ledger missing (${ledgerFile})`);
|
|
162
|
+
if (fs.existsSync(workspace) && fs.readdirSync(workspace).length) fail(`fresh-clone workspace is not empty (${workspace})`);
|
|
163
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
164
|
+
const forge = path.join(path.resolve(root), 'kb', 'forge-refresh.mjs');
|
|
165
|
+
if (!fs.existsSync(forge)) fail(`forge-refresh missing (${forge})`);
|
|
166
|
+
const refreshed = [];
|
|
167
|
+
|
|
168
|
+
for (const item of plan) {
|
|
169
|
+
if (!SAFE_STORE.test(item.store) || !HEX40.test(item.upstreamSha) || !repositorySlug(item.url)) {
|
|
170
|
+
fail(`unsafe reconciliation item for ${item?.store || item?.name || 'unknown store'}`);
|
|
171
|
+
}
|
|
172
|
+
const cloneDir = path.join(workspace, item.store);
|
|
173
|
+
if (fs.existsSync(cloneDir)) fail(`fresh clone target already exists (${cloneDir})`);
|
|
174
|
+
checked(run, 'git', ['clone', '--no-checkout', '--filter=blob:none', item.url, cloneDir]);
|
|
175
|
+
checked(run, 'git', ['-C', cloneDir, 'fetch', '--depth=1', 'origin', item.upstreamSha]);
|
|
176
|
+
checked(run, 'git', ['-C', cloneDir, 'checkout', '--detach', 'FETCH_HEAD']);
|
|
177
|
+
const head = checked(run, 'git', ['-C', cloneDir, 'rev-parse', 'HEAD']);
|
|
178
|
+
if (String(head.stdout || '').trim().toLowerCase() !== item.upstreamSha) {
|
|
179
|
+
fail(`${item.store}: fresh clone did not resolve the exact upstream SHA`);
|
|
180
|
+
}
|
|
181
|
+
checked(run, process.execPath, [forge, '--repo', cloneDir, '--out', assets, '--name', item.store], { stdio: 'inherit' });
|
|
182
|
+
const ledger = readJson(ledgerFile, 'RVF generation ledger after forge-refresh');
|
|
183
|
+
const generation = Object.entries(ledger.stores || {}).find(([name]) => name.toLowerCase() === item.store.toLowerCase())?.[1];
|
|
184
|
+
if (String(generation?.sourceCommit || '').toLowerCase() !== item.upstreamSha) {
|
|
185
|
+
fail(`forge-refresh did not bind ${item.store} to the exact upstream SHA`);
|
|
186
|
+
}
|
|
187
|
+
refreshed.push(item.store);
|
|
188
|
+
}
|
|
189
|
+
return { refreshed };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function prepareCorpusCandidate({
|
|
193
|
+
root = DEFAULT_ROOT,
|
|
194
|
+
assetsDir,
|
|
195
|
+
owner = 'ruvnet',
|
|
196
|
+
builderSha,
|
|
197
|
+
candidateDir,
|
|
198
|
+
receiptFile,
|
|
199
|
+
coverageFile,
|
|
200
|
+
run = defaultRun,
|
|
201
|
+
}) {
|
|
202
|
+
const sourceRoot = path.resolve(root);
|
|
203
|
+
const assets = path.resolve(assetsDir || '');
|
|
204
|
+
const candidate = path.resolve(candidateDir || path.join(sourceRoot, 'dist', 'corpus-candidate'));
|
|
205
|
+
const receipt = path.resolve(receiptFile || path.join(sourceRoot, 'dist', 'corpus-receipt.json'));
|
|
206
|
+
const policy = path.resolve(coverageFile || path.join(sourceRoot, 'data', 'source-coverage.json'));
|
|
207
|
+
if (!HEX40.test(String(builderSha || '').toLowerCase())) fail('builder SHA must be exact 40-character lowercase hex');
|
|
208
|
+
const expectedPolicy = path.join(sourceRoot, 'data', 'source-coverage.json');
|
|
209
|
+
if (policy !== expectedPolicy) fail(`coverage policy must be the generator's canonical projection (${expectedPolicy})`);
|
|
210
|
+
const coverageScript = path.join(sourceRoot, 'scripts', 'source-coverage.mjs');
|
|
211
|
+
const buildScript = path.join(sourceRoot, 'scripts', 'build-bundle.mjs');
|
|
212
|
+
const receiptScript = path.join(sourceRoot, 'scripts', 'corpus-candidate.mjs');
|
|
213
|
+
for (const required of [coverageScript, buildScript, receiptScript]) {
|
|
214
|
+
if (!fs.existsSync(required)) fail(`required candidate builder missing (${required})`);
|
|
215
|
+
}
|
|
216
|
+
fs.mkdirSync(path.dirname(candidate), { recursive: true });
|
|
217
|
+
fs.mkdirSync(path.dirname(receipt), { recursive: true });
|
|
218
|
+
checked(run, process.execPath, [coverageScript, '--owner', owner, '--assets', assets, '--write'], { stdio: 'inherit' });
|
|
219
|
+
checked(run, process.execPath, [coverageScript, '--owner', owner, '--assets', assets, '--check', '--strict'], { stdio: 'inherit' });
|
|
220
|
+
checked(run, process.execPath, [buildScript, '--assets', assets, '--out', candidate], { stdio: 'inherit' });
|
|
221
|
+
const bundleFile = path.join(path.dirname(candidate), `${path.basename(candidate)}.zip`);
|
|
222
|
+
checked(run, process.execPath, [receiptScript, '--assets', assets, '--bundle', bundleFile,
|
|
223
|
+
'--policy', policy, '--receipt', receipt, '--builder-source-sha', builderSha], { stdio: 'inherit' });
|
|
224
|
+
checked(run, process.execPath, [receiptScript, '--verify', '--assets', assets, '--bundle', bundleFile,
|
|
225
|
+
'--policy', policy, '--receipt', receipt], { stdio: 'inherit' });
|
|
226
|
+
return { bundleFile, receiptFile: receipt, coverageFile: policy };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function arg(argv, name, fallback = null) {
|
|
230
|
+
const index = argv.indexOf(name);
|
|
231
|
+
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
235
|
+
const root = path.resolve(arg(argv, '--root', DEFAULT_ROOT));
|
|
236
|
+
const archiveFile = path.resolve(arg(argv, '--seed-archive', ''));
|
|
237
|
+
const seedTag = arg(argv, '--seed-tag');
|
|
238
|
+
const seedSha256 = arg(argv, '--seed-sha256');
|
|
239
|
+
const assetsDir = path.resolve(arg(argv, '--assets', ''));
|
|
240
|
+
const workspaceDir = path.resolve(arg(argv, '--workspace', ''));
|
|
241
|
+
const coverageFile = path.resolve(arg(argv, '--coverage', path.join(root, 'data', 'source-coverage.json')));
|
|
242
|
+
const candidateDir = path.resolve(arg(argv, '--candidate-out', path.join(root, 'dist', 'corpus-candidate')));
|
|
243
|
+
const receiptFile = path.resolve(arg(argv, '--receipt-out', path.join(root, 'dist', 'corpus-receipt.json')));
|
|
244
|
+
const builderSha = String(arg(argv, '--builder-sha', '')).toLowerCase();
|
|
245
|
+
const owner = arg(argv, '--owner', 'ruvnet');
|
|
246
|
+
|
|
247
|
+
assertBootstrapIdentity({ archiveFile, tag: seedTag, sha256: seedSha256, allowPinnedTag: process.argv.includes('--allow-pinned-seed-tag') });
|
|
248
|
+
if (fs.existsSync(assetsDir) && fs.readdirSync(assetsDir).length) fail(`bootstrap assets directory is not empty (${assetsDir})`);
|
|
249
|
+
fs.mkdirSync(path.dirname(assetsDir), { recursive: true });
|
|
250
|
+
const extractParent = fs.mkdtempSync(path.join(path.dirname(assetsDir), '.corpus-seed-extract-'));
|
|
251
|
+
await extractZip(archiveFile, extractParent);
|
|
252
|
+
normalizeExtractedCorpus({ extractedDir: extractParent, assetsDir });
|
|
253
|
+
const privateFence = path.join(root, 'kb', 'PRIVATE-STORES.json');
|
|
254
|
+
if (!fs.existsSync(privateFence)) fail(`canonical private-store fence missing (${privateFence})`);
|
|
255
|
+
fs.copyFileSync(privateFence, path.join(assetsDir, 'PRIVATE-STORES.json'), fs.constants.COPYFILE_EXCL);
|
|
256
|
+
fs.rmSync(extractParent, { recursive: true, force: true });
|
|
257
|
+
const coverageScript = path.join(root, 'scripts', 'source-coverage.mjs');
|
|
258
|
+
checked(defaultRun, process.execPath, [coverageScript, '--owner', owner, '--assets', assetsDir, '--write'], { stdio: 'inherit' });
|
|
259
|
+
const policy = readJson(coverageFile, 'source coverage policy');
|
|
260
|
+
const ledger = readJson(path.join(assetsDir, 'RVF-GENERATIONS.json'), 'bootstrap RVF generation ledger');
|
|
261
|
+
const plan = planReconciliation({ coverage: policy, ledger, assetsDir });
|
|
262
|
+
const reconciliation = executeReconciliation({ plan, assetsDir, workspaceDir, root });
|
|
263
|
+
const candidate = prepareCorpusCandidate({ root, assetsDir, owner, builderSha, candidateDir, receiptFile, coverageFile });
|
|
264
|
+
process.stdout.write(`${JSON.stringify({ ok: true, seedTag, seedSha256, plan, ...reconciliation, ...candidate }, null, 2)}\n`);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
269
|
+
main().then((code) => { process.exitCode = code; }).catch((error) => {
|
|
270
|
+
console.error(error.message);
|
|
271
|
+
process.exitCode = 1;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Publish a corpus candidate once, under a tag derived from its exact archive digest.
|
|
3
|
+
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { verifyCorpusReceipt } from './corpus-candidate.mjs';
|
|
10
|
+
|
|
11
|
+
function fail(message) {
|
|
12
|
+
throw new Error(`[corpus-seed-publish] ${message}`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sha256File(file) {
|
|
16
|
+
const hash = crypto.createHash('sha256');
|
|
17
|
+
const fd = fs.openSync(file, 'r');
|
|
18
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
19
|
+
try {
|
|
20
|
+
let bytes;
|
|
21
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
|
|
22
|
+
} finally {
|
|
23
|
+
fs.closeSync(fd);
|
|
24
|
+
}
|
|
25
|
+
return hash.digest('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function corpusSeedTag(receipt) {
|
|
29
|
+
if (!/^[a-f0-9]{64}$/.test(receipt?.archive?.sha256 || '')) fail('cannot derive tag without an archive sha256');
|
|
30
|
+
return `corpus-sha256-${receipt.archive.sha256}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function defaultRun(command, args, options) {
|
|
34
|
+
return spawnSync(command, args, { encoding: 'utf8', ...options });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function publishCorpusSeed({
|
|
38
|
+
receiptFile,
|
|
39
|
+
bundleFile,
|
|
40
|
+
assetsDir,
|
|
41
|
+
policyFile,
|
|
42
|
+
repo,
|
|
43
|
+
run = defaultRun,
|
|
44
|
+
}) {
|
|
45
|
+
const receiptPath = path.resolve(receiptFile || '');
|
|
46
|
+
const bundlePath = path.resolve(bundleFile || '');
|
|
47
|
+
if (!assetsDir || !policyFile) fail('publishing requires --assets and --policy for full receipt verification');
|
|
48
|
+
const receipt = verifyCorpusReceipt({
|
|
49
|
+
receiptFile: receiptPath,
|
|
50
|
+
bundleFile: bundlePath,
|
|
51
|
+
assetsDir,
|
|
52
|
+
policyFile,
|
|
53
|
+
});
|
|
54
|
+
if (!fs.existsSync(bundlePath)) fail(`bundle missing (${bundlePath})`);
|
|
55
|
+
const archiveSha256 = sha256File(bundlePath);
|
|
56
|
+
if (archiveSha256 !== receipt.archive.sha256) fail(`archive sha256=${archiveSha256}, receipt=${receipt.archive.sha256}`);
|
|
57
|
+
if (fs.statSync(bundlePath).size !== receipt.archive.bytes) fail('archive byte length differs from receipt');
|
|
58
|
+
|
|
59
|
+
const tag = corpusSeedTag(receipt);
|
|
60
|
+
const repoArgs = repo ? ['--repo', repo] : [];
|
|
61
|
+
const view = run('gh', ['release', 'view', tag, '--json', 'tagName', ...repoArgs], {
|
|
62
|
+
encoding: 'utf8',
|
|
63
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
64
|
+
});
|
|
65
|
+
if (view.status === 0) fail(`release ${tag} already exists; refusing to overwrite immutable corpus seed`);
|
|
66
|
+
const viewError = String(view.stderr || view.stdout || '');
|
|
67
|
+
if (!/(not found|404|no release found)/i.test(viewError)) {
|
|
68
|
+
fail(`cannot prove ${tag} is absent (${viewError.trim() || `gh exited ${view.status}`})`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Publication stays behind the repository's one-publisher authority. This wrapper validates and
|
|
72
|
+
// seals the corpus inputs; scripts/release.mjs owns the protected GitHub mutation and its receipt.
|
|
73
|
+
const publish = run(process.execPath, [
|
|
74
|
+
path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'release.mjs'),
|
|
75
|
+
'--corpus-seed',
|
|
76
|
+
'--corpus-tag', tag,
|
|
77
|
+
'--corpus-bundle', bundlePath,
|
|
78
|
+
'--corpus-receipt', receiptPath,
|
|
79
|
+
'--target', receipt.builderSourceSha,
|
|
80
|
+
...(repo ? ['--repo', repo] : []),
|
|
81
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
82
|
+
if (publish.status !== 0) fail(`protected corpus publication failed (${String(publish.stderr || publish.stdout || '').trim()})`);
|
|
83
|
+
return {
|
|
84
|
+
tag,
|
|
85
|
+
archiveSha256,
|
|
86
|
+
receiptSha256: sha256File(receiptPath),
|
|
87
|
+
draft: false,
|
|
88
|
+
prerelease: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function arg(name, fallback) {
|
|
93
|
+
const index = process.argv.indexOf(name);
|
|
94
|
+
return index >= 0 ? process.argv[index + 1] : fallback;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
const result = publishCorpusSeed({
|
|
99
|
+
receiptFile: arg('--receipt', 'dist/corpus-receipt.json'),
|
|
100
|
+
bundleFile: arg('--bundle', 'dist/ruvnet-brain.zip'),
|
|
101
|
+
assetsDir: arg('--assets'),
|
|
102
|
+
policyFile: arg('--policy'),
|
|
103
|
+
repo: arg('--repo'),
|
|
104
|
+
});
|
|
105
|
+
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
109
|
+
main().catch((error) => { console.error(error.message); process.exitCode = 1; });
|
|
110
|
+
}
|