ruvnet-brain 4.0.90-dev → 4.3.1-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 +31 -7
- package/bin/install.mjs +153 -22
- package/console/architecture.html +12 -14
- package/console/install-mockup.html +3 -3
- package/kb/zip-extract.mjs +8 -2
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/host-adapters/claude.json +11 -0
- package/plugin/host-adapters/codex.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +77 -10
- package/plugin/mcp/server.mjs +2 -1
- package/plugin/scripts/anticipate.sh +30 -1
- package/plugin/scripts/capability-claim-evidence.mjs +431 -0
- package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
- package/plugin/scripts/capability-registry.mjs +6 -0
- package/plugin/scripts/capability-routing.mjs +70 -0
- package/plugin/scripts/continuation-gate.mjs +68 -2
- package/plugin/scripts/coverage-integrity.mjs +575 -0
- package/plugin/scripts/decision-outcomes.mjs +0 -0
- package/plugin/scripts/ground-ruvnet.sh +2 -2
- package/plugin/scripts/nightly-controller.mjs +19 -2
- package/plugin/scripts/project-progression-contract.mjs +409 -0
- package/plugin/scripts/project-progression-hook.mjs +196 -0
- package/plugin/scripts/project-progression-outbox.mjs +95 -0
- package/plugin/scripts/project-progression-session-start.mjs +183 -0
- package/plugin/scripts/project-progression-store.mjs +248 -0
- package/plugin/scripts/project-store-resolver.mjs +106 -0
- package/plugin/scripts/session-snapshot-hook.mjs +29 -1
- package/plugin/scripts/session-start-core.mjs +22 -18
- package/plugin/scripts/spend-guard.mjs +6 -1
- package/scripts/adr-072-completion.mjs +98 -0
- package/scripts/behavioral-l1-l4.mjs +11 -8
- package/scripts/brain-score.mjs +23 -9
- package/scripts/build-bundle.mjs +94 -8
- package/scripts/card-from-source.mjs +140 -0
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +314 -0
- package/scripts/corpus-reconcile.mjs +540 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/coverage-integrity.mjs +3 -0
- package/scripts/gist-receipts.mjs +183 -0
- package/scripts/git-hooks/pre-push +21 -0
- package/scripts/host-registry.mjs +118 -0
- package/scripts/independent-review-receipt.mjs +499 -0
- package/scripts/ingest-gists.mjs +34 -5
- package/scripts/ingest-new-repos.mjs +127 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +105 -64
- package/scripts/product-integrity-contract.mjs +156 -0
- package/scripts/public-inventory.mjs +4 -0
- package/scripts/public-verification-aggregate.mjs +342 -0
- package/scripts/public-verification-finalizer.mjs +59 -0
- package/scripts/public-verification-inputs.mjs +473 -0
- package/scripts/public-verification-lane.mjs +244 -0
- package/scripts/publication-receipt.mjs +50 -6
- package/scripts/rebuild-gists-from-receipts.mjs +291 -0
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +165 -49
- package/scripts/release.mjs +158 -3
- package/scripts/restore-local-ingests.mjs +2 -2
- package/scripts/retrieval-canary.mjs +533 -0
- package/scripts/routing-flywheel.mjs +12 -1
- package/scripts/rvf-generation.mjs +50 -0
- package/scripts/self-update.mjs +19 -44
- package/scripts/source-coverage.mjs +415 -0
- package/scripts/source-scope-receipt.mjs +84 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +114 -24
- package/scripts/worktree-integrity.mjs +123 -0
|
@@ -0,0 +1,540 @@
|
|
|
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 { spawn, spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { extractZip } from '../kb/zip-extract.mjs';
|
|
12
|
+
import { FULL_HINTS, KEEP_DIRS } from './full-hints.mjs';
|
|
13
|
+
import { buildCoverage, observeSourceUniverse, sourceObservationDigest } from './source-coverage.mjs';
|
|
14
|
+
import { reconcileGistReceipts } from './gist-receipts.mjs';
|
|
15
|
+
import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
|
|
16
|
+
import { rebuildCorpusAggregates } from './corpus-aggregates.mjs';
|
|
17
|
+
|
|
18
|
+
export { rebuildCorpusAggregates };
|
|
19
|
+
|
|
20
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const DEFAULT_ROOT = path.resolve(HERE, '..');
|
|
22
|
+
const HEX40 = /^[0-9a-f]{40}$/;
|
|
23
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
24
|
+
const SAFE_STORE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
25
|
+
const STORE_ARTIFACT_SUFFIXES = [
|
|
26
|
+
'.big.rvf', '.big.rvf.idmap.json', '.big.rvf.embed.json', '.big.passages.jsonl',
|
|
27
|
+
'.big.meta.json', '.passages.jsonl', '.meta.json',
|
|
28
|
+
];
|
|
29
|
+
const REQUIRED_STORE_ARTIFACT_SUFFIXES = [
|
|
30
|
+
'.big.rvf', '.big.rvf.idmap.json', '.big.rvf.embed.json', '.passages.jsonl', '.meta.json',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function fail(message) {
|
|
34
|
+
throw new Error(`[corpus-reconcile] ${message}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sha256File(file) {
|
|
38
|
+
if (!file || !fs.existsSync(file) || !fs.statSync(file).isFile()) fail(`seed archive missing (${file || 'no path supplied'})`);
|
|
39
|
+
const hash = crypto.createHash('sha256');
|
|
40
|
+
const fd = fs.openSync(file, 'r');
|
|
41
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
42
|
+
try {
|
|
43
|
+
let bytes;
|
|
44
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
|
|
45
|
+
} finally {
|
|
46
|
+
fs.closeSync(fd);
|
|
47
|
+
}
|
|
48
|
+
return hash.digest('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readJson(file, label) {
|
|
52
|
+
if (!file || !fs.existsSync(file)) fail(`${label} missing (${file || 'no path supplied'})`);
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
fail(`${label} unreadable (${error.message})`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function assertBootstrapIdentity({ archiveFile, tag, sha256, allowPinnedTag = false }) {
|
|
61
|
+
const expected = String(sha256 || '').toLowerCase();
|
|
62
|
+
if (!HEX64.test(expected) || (!allowPinnedTag && tag !== `corpus-sha256-${expected}`) || !tag || tag === 'latest') {
|
|
63
|
+
fail('bootstrap requires the exact digest-derived tag corpus-sha256-<configured sha256>; latest is forbidden');
|
|
64
|
+
}
|
|
65
|
+
const actual = sha256File(path.resolve(archiveFile || ''));
|
|
66
|
+
if (actual !== expected) fail(`downloaded archive sha256 ${actual} differs from configured ${expected}`);
|
|
67
|
+
return { tag, sha256: expected };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function filesNamed(root, wanted) {
|
|
71
|
+
const found = [];
|
|
72
|
+
const visit = (dir) => {
|
|
73
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
74
|
+
const file = path.join(dir, entry.name);
|
|
75
|
+
if (entry.isDirectory()) visit(file);
|
|
76
|
+
else if (entry.isFile() && entry.name === wanted) found.push(file);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
visit(root);
|
|
80
|
+
return found;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeExtractedCorpus({ extractedDir, assetsDir }) {
|
|
84
|
+
const extracted = path.resolve(extractedDir || '');
|
|
85
|
+
const assets = path.resolve(assetsDir || '');
|
|
86
|
+
if (!fs.existsSync(extracted) || !fs.statSync(extracted).isDirectory()) {
|
|
87
|
+
fail(`extracted seed directory missing (${extracted})`);
|
|
88
|
+
}
|
|
89
|
+
if (fs.existsSync(assets) && fs.readdirSync(assets).length) fail(`bootstrap assets directory is not empty (${assets})`);
|
|
90
|
+
const ledgers = filesNamed(extracted, 'RVF-GENERATIONS.json');
|
|
91
|
+
if (ledgers.length !== 1) fail(`seed archive must contain exactly one RVF-GENERATIONS.json; found ${ledgers.length}`);
|
|
92
|
+
const corpusRoot = path.dirname(ledgers[0]);
|
|
93
|
+
if (fs.existsSync(path.join(corpusRoot, 'PRIVATE-STORES.json'))) {
|
|
94
|
+
fail('a published seed must not supply a private-store fence; the exact builder checkout owns that policy');
|
|
95
|
+
}
|
|
96
|
+
fs.mkdirSync(assets, { recursive: true });
|
|
97
|
+
for (const entry of fs.readdirSync(corpusRoot)) fs.renameSync(path.join(corpusRoot, entry), path.join(assets, entry));
|
|
98
|
+
return assets;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function repositorySlug(url) {
|
|
102
|
+
const match = String(url || '').match(/^https:\/\/github\.com\/([^/]+)\/([^/#?]+?)(?:\.git)?$/i);
|
|
103
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function planReconciliation({ coverage, ledger, assetsDir = null }) {
|
|
107
|
+
if (coverage?.schemaVersion !== 1 || !Array.isArray(coverage.rows) || !coverage.coverageGeneration) {
|
|
108
|
+
fail('coverage policy is missing a supported complete generation');
|
|
109
|
+
}
|
|
110
|
+
if (!ledger?.stores || typeof ledger.stores !== 'object' || Array.isArray(ledger.stores)) {
|
|
111
|
+
fail('RVF generation ledger has no stores object');
|
|
112
|
+
}
|
|
113
|
+
const eligible = coverage.rows.filter((row) => row?.kind === 'repository' && row?.disposition === 'eligible');
|
|
114
|
+
const seen = new Set();
|
|
115
|
+
const plan = [];
|
|
116
|
+
for (const row of eligible) {
|
|
117
|
+
const store = String(row?.artifact?.store || '');
|
|
118
|
+
if (!SAFE_STORE.test(store)) fail(`${row?.name || row?.key || 'eligible repository'} has an unsafe or missing store name`);
|
|
119
|
+
const folded = store.toLowerCase();
|
|
120
|
+
if (seen.has(folded)) fail(`duplicate eligible store ${store} in coverage policy`);
|
|
121
|
+
seen.add(folded);
|
|
122
|
+
const upstreamSha = String(row?.upstream?.sha || '').toLowerCase();
|
|
123
|
+
if (!HEX40.test(upstreamSha)) fail(`${row?.name || store} has a missing or malformed upstream SHA`);
|
|
124
|
+
if (!repositorySlug(row.url)) fail(`${row?.name || store} has no exact GitHub repository URL`);
|
|
125
|
+
const generation = Object.entries(ledger.stores).find(([name]) => name.toLowerCase() === folded)?.[1] || null;
|
|
126
|
+
const current = String(generation?.sourceCommit || '').toLowerCase();
|
|
127
|
+
let reason = generation?.sourceCommit ? 'sourceCommit differs' : 'missing ledger receipt';
|
|
128
|
+
if (current === upstreamSha) {
|
|
129
|
+
if (!assetsDir) continue;
|
|
130
|
+
const expectedFile = `${store}.big.rvf`;
|
|
131
|
+
const rvfFile = path.join(path.resolve(assetsDir), expectedFile);
|
|
132
|
+
const receiptMatches = generation?.file === expectedFile
|
|
133
|
+
&& fs.existsSync(rvfFile)
|
|
134
|
+
&& generation?.bytes === fs.statSync(rvfFile).size
|
|
135
|
+
&& generation?.sha256 === sha256File(rvfFile);
|
|
136
|
+
if (receiptMatches) continue;
|
|
137
|
+
reason = 'generation receipt differs from seed bytes';
|
|
138
|
+
}
|
|
139
|
+
plan.push({
|
|
140
|
+
name: String(row.name || store),
|
|
141
|
+
store,
|
|
142
|
+
url: row.url,
|
|
143
|
+
upstreamSha,
|
|
144
|
+
ledgerSourceCommit: generation?.sourceCommit || null,
|
|
145
|
+
reason,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return plan.sort((a, b) => a.store.localeCompare(b.store));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function reconcileUntilStable({ maxRounds = 3, assetsDir = null, observe, build, readLedger: currentLedger,
|
|
152
|
+
execute, prune, rebuild } = {}) {
|
|
153
|
+
if (!Number.isSafeInteger(maxRounds) || maxRounds < 1 || maxRounds > 10
|
|
154
|
+
|| [observe, build, currentLedger, execute, prune, rebuild].some((fn) => typeof fn !== 'function')) {
|
|
155
|
+
fail('bounded reconciliation loop configuration is invalid');
|
|
156
|
+
}
|
|
157
|
+
const rounds = [];
|
|
158
|
+
let observation = await observe();
|
|
159
|
+
for (let round = 1; round <= maxRounds; round += 1) {
|
|
160
|
+
const preliminary = await build(observation);
|
|
161
|
+
const plan = planReconciliation({ coverage: preliminary, ledger: currentLedger(), assetsDir });
|
|
162
|
+
const reconciliation = await execute(plan, round);
|
|
163
|
+
const pruning = await prune(preliminary, round);
|
|
164
|
+
let aggregates;
|
|
165
|
+
try {
|
|
166
|
+
aggregates = await rebuild(preliminary, observation, round);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
if (error?.code !== 'GIST_OBSERVATION_MOVED') throw error;
|
|
169
|
+
const nextObservation = await observe();
|
|
170
|
+
rounds.push({ round, before: observation.observationSha256, after: nextObservation.observationSha256,
|
|
171
|
+
plan, ...reconciliation, ...pruning, rebuilt: [], invalidated: {
|
|
172
|
+
reason: 'gist observation moved during exact detail fetch', gistId: error.gistId || null } });
|
|
173
|
+
observation = nextObservation;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const nextObservation = await observe();
|
|
177
|
+
rounds.push({ round, before: observation.observationSha256, after: nextObservation.observationSha256,
|
|
178
|
+
plan, ...reconciliation, ...pruning, ...aggregates });
|
|
179
|
+
if (nextObservation.observationSha256 === observation.observationSha256) {
|
|
180
|
+
const coverage = await build(nextObservation);
|
|
181
|
+
const remaining = planReconciliation({ coverage, ledger: currentLedger(), assetsDir });
|
|
182
|
+
if (remaining.length) fail(`reconciliation stabilized with ${remaining.length} unresolved repository artifact(s)`);
|
|
183
|
+
const unresolved = coverage.rows.filter((row) => row.disposition === 'eligible' && row.status !== 'CURRENT');
|
|
184
|
+
if (unresolved.length) fail(`reconciliation stabilized with ${unresolved.length} unresolved eligible source(s)`);
|
|
185
|
+
return { observation: nextObservation, coverage, rounds };
|
|
186
|
+
}
|
|
187
|
+
observation = nextObservation;
|
|
188
|
+
}
|
|
189
|
+
fail(`source observation did not stabilize within ${maxRounds} reconciliation rounds`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function defaultRun(command, args, options = {}) {
|
|
193
|
+
return spawnSync(command, args, { encoding: 'utf8', ...options });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function checked(run, command, args, options = {}) {
|
|
197
|
+
const result = run(command, args, options) || {};
|
|
198
|
+
if (result.error || result.status !== 0) {
|
|
199
|
+
const detail = String(result.stderr || result.stdout || result.error?.message || `exit ${result.status}`).trim();
|
|
200
|
+
fail(`${command} ${args.join(' ')} failed${detail ? ` (${detail})` : ''}`);
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function defaultRunAsync(command, args, options = {}) {
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
const inherited = options.stdio === 'inherit';
|
|
208
|
+
const child = spawn(command, args, { ...options, encoding: undefined,
|
|
209
|
+
stdio: inherited ? 'inherit' : ['ignore', 'pipe', 'pipe'] });
|
|
210
|
+
let stdout = '';
|
|
211
|
+
let stderr = '';
|
|
212
|
+
if (!inherited) {
|
|
213
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
214
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
215
|
+
}
|
|
216
|
+
child.on('error', (error) => resolve({ status: null, error, stdout, stderr }));
|
|
217
|
+
child.on('close', (status) => resolve({ status, stdout, stderr }));
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function checkedAsync(run, command, args, options = {}) {
|
|
222
|
+
const result = await run(command, args, options) || {};
|
|
223
|
+
if (result.error || result.status !== 0) {
|
|
224
|
+
const detail = String(result.stderr || result.stdout || result.error?.message || `exit ${result.status}`).trim();
|
|
225
|
+
fail(`${command} ${args.join(' ')} failed${detail ? ` (${detail})` : ''}`);
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const storeArtifacts = (store) => STORE_ARTIFACT_SUFFIXES.map((suffix) => `${store}${suffix}`);
|
|
231
|
+
|
|
232
|
+
function writeJsonAtomic(file, value) {
|
|
233
|
+
const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
|
|
234
|
+
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
235
|
+
fs.renameSync(temporary, file);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function seedWorkerAssets({ assets, output, store, ledger, source }) {
|
|
239
|
+
fs.mkdirSync(output, { recursive: true });
|
|
240
|
+
for (const name of storeArtifacts(store)) {
|
|
241
|
+
const input = path.join(assets, name);
|
|
242
|
+
if (!fs.existsSync(input)) continue;
|
|
243
|
+
const stat = fs.lstatSync(input);
|
|
244
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail(`${store}: canonical seed artifact is not a regular file (${name})`);
|
|
245
|
+
fs.copyFileSync(input, path.join(output, name));
|
|
246
|
+
}
|
|
247
|
+
writeJsonAtomic(path.join(output, 'RVF-GENERATIONS.json'), {
|
|
248
|
+
...ledger, stores: ledger.stores?.[store] ? { [store]: ledger.stores[store] } : {},
|
|
249
|
+
});
|
|
250
|
+
writeJsonAtomic(path.join(output, 'SOURCE.json'), {
|
|
251
|
+
...(source || { builder: 'rvf-kb-forge' }), stores: source?.stores?.[store] ? { [store]: source.stores[store] } : {},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function validateWorkerOutput({ output, item }) {
|
|
256
|
+
const allowed = new Set([...storeArtifacts(item.store), 'RVF-GENERATIONS.json', 'SOURCE.json']);
|
|
257
|
+
const names = fs.readdirSync(output).filter((name) => !name.startsWith('._')).sort();
|
|
258
|
+
const unexpected = names.filter((name) => !allowed.has(name));
|
|
259
|
+
if (unexpected.length) fail(`${item.store}: worker emitted unexpected artifact(s): ${unexpected.join(', ')}`);
|
|
260
|
+
const caseFolded = names.map((name) => name.toLowerCase());
|
|
261
|
+
if (new Set(caseFolded).size !== names.length) fail(`${item.store}: worker emitted case-fold aliases`);
|
|
262
|
+
for (const name of names) {
|
|
263
|
+
const stat = fs.lstatSync(path.join(output, name));
|
|
264
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail(`${item.store}: worker artifact is not a regular file (${name})`);
|
|
265
|
+
}
|
|
266
|
+
for (const suffix of REQUIRED_STORE_ARTIFACT_SUFFIXES) {
|
|
267
|
+
if (!names.includes(`${item.store}${suffix}`)) fail(`${item.store}: worker artifact family is incomplete (${suffix})`);
|
|
268
|
+
}
|
|
269
|
+
const ledger = readJson(path.join(output, 'RVF-GENERATIONS.json'), `${item.store} worker ledger`);
|
|
270
|
+
const ledgerStores = Object.keys(ledger.stores || {});
|
|
271
|
+
if (ledgerStores.length !== 1 || ledgerStores[0] !== item.store) fail(`${item.store}: worker ledger must contain exactly its own store`);
|
|
272
|
+
const generation = ledger.stores[item.store];
|
|
273
|
+
const expectedRvf = `${item.store}.big.rvf`;
|
|
274
|
+
if (generation?.file !== expectedRvf || String(generation.sourceCommit || '').toLowerCase() !== item.upstreamSha
|
|
275
|
+
|| !fs.existsSync(path.join(output, expectedRvf))
|
|
276
|
+
|| generation.sha256 !== sha256File(path.join(output, expectedRvf))
|
|
277
|
+
|| generation.bytes !== fs.statSync(path.join(output, expectedRvf)).size) {
|
|
278
|
+
fail(`${item.store}: worker generation does not bind exact source and RVF bytes`);
|
|
279
|
+
}
|
|
280
|
+
const source = readJson(path.join(output, 'SOURCE.json'), `${item.store} worker source manifest`);
|
|
281
|
+
if (Object.keys(source.stores || {}).length !== 1 || !source.stores[item.store]
|
|
282
|
+
|| String(source.stores[item.store].sourceCommit || '').toLowerCase() !== item.upstreamSha) {
|
|
283
|
+
fail(`${item.store}: worker SOURCE manifest does not bind exact source`);
|
|
284
|
+
}
|
|
285
|
+
const files = names.filter((name) => !['RVF-GENERATIONS.json', 'SOURCE.json'].includes(name))
|
|
286
|
+
.map((name) => ({ name, sha256: sha256File(path.join(output, name)), bytes: fs.statSync(path.join(output, name)).size }));
|
|
287
|
+
const payload = { schemaVersion: 1, kind: 'ruvnet-brain-corpus-worker-result', store: item.store,
|
|
288
|
+
sourceCommit: item.upstreamSha, generation, source: source.stores[item.store], files };
|
|
289
|
+
return { ...payload, receiptSha256: crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex'), output };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export async function executeReconciliation({
|
|
293
|
+
plan,
|
|
294
|
+
assetsDir,
|
|
295
|
+
workspaceDir,
|
|
296
|
+
root = DEFAULT_ROOT,
|
|
297
|
+
run = defaultRunAsync,
|
|
298
|
+
concurrency = 5,
|
|
299
|
+
}) {
|
|
300
|
+
if (!Array.isArray(plan) || !Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 10) {
|
|
301
|
+
fail('reconciliation plan or worker concurrency is invalid');
|
|
302
|
+
}
|
|
303
|
+
const assets = path.resolve(assetsDir || '');
|
|
304
|
+
const workspace = path.resolve(workspaceDir || '');
|
|
305
|
+
const ledgerFile = path.join(assets, 'RVF-GENERATIONS.json');
|
|
306
|
+
if (!fs.existsSync(ledgerFile)) fail(`RVF generation ledger missing (${ledgerFile})`);
|
|
307
|
+
if (fs.existsSync(workspace) && fs.readdirSync(workspace).length) fail(`fresh-clone workspace is not empty (${workspace})`);
|
|
308
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
309
|
+
const forge = path.join(path.resolve(root), 'kb', 'forge-refresh.mjs');
|
|
310
|
+
if (!fs.existsSync(forge)) fail(`forge-refresh missing (${forge})`);
|
|
311
|
+
const canonicalLedger = readJson(ledgerFile, 'RVF generation ledger');
|
|
312
|
+
const sourceFile = path.join(assets, 'SOURCE.json');
|
|
313
|
+
const canonicalSource = fs.existsSync(sourceFile) ? readJson(sourceFile, 'SOURCE manifest') : { builder: 'rvf-kb-forge', stores: {} };
|
|
314
|
+
const orderedPlan = [...plan].sort((a, b) => a.store.localeCompare(b.store));
|
|
315
|
+
const lowerStores = orderedPlan.map(({ store }) => store.toLowerCase());
|
|
316
|
+
if (new Set(lowerStores).size !== lowerStores.length) fail('reconciliation plan has duplicate or case-fold-colliding stores');
|
|
317
|
+
|
|
318
|
+
const worker = async (item) => {
|
|
319
|
+
if (!SAFE_STORE.test(item.store) || !HEX40.test(item.upstreamSha) || !repositorySlug(item.url)) {
|
|
320
|
+
fail(`unsafe reconciliation item for ${item?.store || item?.name || 'unknown store'}`);
|
|
321
|
+
}
|
|
322
|
+
const workerRoot = path.join(workspace, 'workers', item.store);
|
|
323
|
+
const cloneDir = path.join(workerRoot, 'clone');
|
|
324
|
+
const output = path.join(workerRoot, 'assets');
|
|
325
|
+
fs.mkdirSync(workerRoot, { recursive: true });
|
|
326
|
+
seedWorkerAssets({ assets, output, store: item.store, ledger: canonicalLedger, source: canonicalSource });
|
|
327
|
+
await checkedAsync(run, 'git', ['clone', '--no-checkout', '--filter=blob:none', item.url, cloneDir]);
|
|
328
|
+
await checkedAsync(run, 'git', ['-C', cloneDir, 'fetch', '--depth=1', 'origin', item.upstreamSha]);
|
|
329
|
+
await checkedAsync(run, 'git', ['-C', cloneDir, 'checkout', '--detach', 'FETCH_HEAD']);
|
|
330
|
+
const head = await checkedAsync(run, 'git', ['-C', cloneDir, 'rev-parse', 'HEAD']);
|
|
331
|
+
if (String(head.stdout || '').trim().toLowerCase() !== item.upstreamSha) {
|
|
332
|
+
fail(`${item.store}: fresh clone did not resolve the exact upstream SHA`);
|
|
333
|
+
}
|
|
334
|
+
await checkedAsync(run, process.execPath, [forge, '--repo', cloneDir, '--out', output, '--name', item.store,
|
|
335
|
+
...(FULL_HINTS[item.store] ? ['--full', FULL_HINTS[item.store]] : []),
|
|
336
|
+
...(KEEP_DIRS[item.store] ? ['--keep', KEEP_DIRS[item.store]] : []),
|
|
337
|
+
], { stdio: 'inherit', env: { ...process.env, RUVNET_BIG_SHARDS: '1' } });
|
|
338
|
+
return validateWorkerOutput({ output, item });
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const results = new Array(orderedPlan.length);
|
|
342
|
+
let next = 0;
|
|
343
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, orderedPlan.length) }, async () => {
|
|
344
|
+
while (next < orderedPlan.length) {
|
|
345
|
+
const index = next++;
|
|
346
|
+
results[index] = await worker(orderedPlan[index]);
|
|
347
|
+
}
|
|
348
|
+
}));
|
|
349
|
+
if (!results.length) return { refreshed: [], workers: [] };
|
|
350
|
+
|
|
351
|
+
const merge = path.join(workspace, 'merge-candidate');
|
|
352
|
+
fs.mkdirSync(merge);
|
|
353
|
+
const mergedLedger = structuredClone(canonicalLedger);
|
|
354
|
+
const mergedSource = structuredClone(canonicalSource);
|
|
355
|
+
mergedLedger.stores ||= {};
|
|
356
|
+
mergedSource.stores ||= {};
|
|
357
|
+
const promotedFiles = [];
|
|
358
|
+
for (const result of results) {
|
|
359
|
+
for (const file of result.files) {
|
|
360
|
+
fs.copyFileSync(path.join(result.output, file.name), path.join(merge, file.name), fs.constants.COPYFILE_EXCL);
|
|
361
|
+
promotedFiles.push(file.name);
|
|
362
|
+
}
|
|
363
|
+
mergedLedger.stores[result.store] = result.generation;
|
|
364
|
+
mergedSource.stores[result.store] = result.source;
|
|
365
|
+
}
|
|
366
|
+
mergedLedger.stores = Object.fromEntries(Object.entries(mergedLedger.stores).sort(([a], [b]) => a.localeCompare(b)));
|
|
367
|
+
mergedSource.stores = Object.fromEntries(Object.entries(mergedSource.stores).sort(([a], [b]) => a.localeCompare(b)));
|
|
368
|
+
writeJsonAtomic(path.join(merge, 'RVF-GENERATIONS.json'), mergedLedger);
|
|
369
|
+
writeJsonAtomic(path.join(merge, 'SOURCE.json'), mergedSource);
|
|
370
|
+
promotedFiles.push('RVF-GENERATIONS.json', 'SOURCE.json');
|
|
371
|
+
promoteArtifactSet({ liveDir: assets, candidateDir: merge, files: promotedFiles.sort() });
|
|
372
|
+
return { refreshed: results.map(({ store }) => store),
|
|
373
|
+
workers: results.map(({ output: _output, ...receipt }) => receipt) };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function syncCorpusInputs({ root = DEFAULT_ROOT, assetsDir }) {
|
|
377
|
+
const sourceKb = path.join(path.resolve(root), 'kb');
|
|
378
|
+
const assets = path.resolve(assetsDir || '');
|
|
379
|
+
const required = ['capability-cards.md', 'external-sources.json', 'no-corpus-repos.json',
|
|
380
|
+
'public-store-classes.json'];
|
|
381
|
+
for (const name of required) {
|
|
382
|
+
const source = path.join(sourceKb, name);
|
|
383
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isFile()) fail(`canonical corpus input missing (${source})`);
|
|
384
|
+
fs.copyFileSync(source, path.join(assets, name));
|
|
385
|
+
}
|
|
386
|
+
const sourceL2 = path.join(sourceKb, 'l2');
|
|
387
|
+
if (!fs.existsSync(sourceL2) || !fs.statSync(sourceL2).isDirectory()) fail(`canonical L2 input missing (${sourceL2})`);
|
|
388
|
+
fs.rmSync(path.join(assets, 'l2'), { recursive: true, force: true });
|
|
389
|
+
fs.cpSync(sourceL2, path.join(assets, 'l2'), { recursive: true });
|
|
390
|
+
for (const entry of fs.readdirSync(sourceKb)) {
|
|
391
|
+
if (entry.endsWith('-primer.md') || /^l2-topics\..+\.json$/.test(entry)) {
|
|
392
|
+
fs.copyFileSync(path.join(sourceKb, entry), path.join(assets, entry));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return { copied: required };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function materializeGistReceipts({ observation, assetsDir, fetchGist, fetchBody, now } = {}) {
|
|
399
|
+
if (observation?.observationSha256 !== sourceObservationDigest(observation)) {
|
|
400
|
+
fail('gist receipts require an exact sealed source observation');
|
|
401
|
+
}
|
|
402
|
+
const sourceFile = path.join(path.resolve(assetsDir || ''), 'ruv-gists.sources.json');
|
|
403
|
+
const existing = fs.existsSync(sourceFile) ? readJson(sourceFile, 'existing gist receipts') : null;
|
|
404
|
+
const receipt = await reconcileGistReceipts({ observation, existing, fetchGist, fetchBody, now });
|
|
405
|
+
if (receipt.sourceObservationSha256 !== observation.observationSha256) {
|
|
406
|
+
fail('gist receipts differ from the sealed source observation');
|
|
407
|
+
}
|
|
408
|
+
writeJsonAtomic(sourceFile, receipt);
|
|
409
|
+
return { sourceFile, receipt };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export async function observeAndMaterializeGistReceipts({ owner = 'ruvnet', assetsDir,
|
|
413
|
+
observe = observeSourceUniverse, fetchGist, fetchBody, now } = {}) {
|
|
414
|
+
const assets = path.resolve(assetsDir || '');
|
|
415
|
+
const externalFile = path.join(assets, 'external-sources.json');
|
|
416
|
+
const policy = fs.existsSync(externalFile) ? readJson(externalFile, 'external source policy') : { sources: [] };
|
|
417
|
+
if (!Array.isArray(policy.sources)) fail('external source policy has no sources array');
|
|
418
|
+
const observation = await observe({ owner, externalSources: policy.sources });
|
|
419
|
+
const materialized = await materializeGistReceipts({ observation, assetsDir: assets, fetchGist, fetchBody, now });
|
|
420
|
+
return { observation, ...materialized };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export async function reconcileCorpusUntilStable({ owner = 'ruvnet', assetsDir, workspaceDir,
|
|
424
|
+
root = DEFAULT_ROOT, maxRounds = 3,
|
|
425
|
+
observeAndMaterialize = null,
|
|
426
|
+
build = (observation) => buildCoverage({ owner, kbDir: assetsDir, policyDir: assetsDir, observation }),
|
|
427
|
+
readLedger = () => readJson(path.join(path.resolve(assetsDir || ''), 'RVF-GENERATIONS.json'),
|
|
428
|
+
'RVF generation ledger'),
|
|
429
|
+
execute = executeReconciliation,
|
|
430
|
+
prune = () => ({ pruned: [] }),
|
|
431
|
+
rebuild = (_coverage, observation) => rebuildCorpusAggregates({ assetsDir, observation, root }),
|
|
432
|
+
} = {}) {
|
|
433
|
+
if (!assetsDir || !workspaceDir) fail('stable reconciliation requires explicit assets and workspace directories');
|
|
434
|
+
const workspace = path.resolve(workspaceDir || '');
|
|
435
|
+
return reconcileUntilStable({
|
|
436
|
+
maxRounds,
|
|
437
|
+
assetsDir,
|
|
438
|
+
observe: async () => (await (observeAndMaterialize
|
|
439
|
+
? observeAndMaterialize({ owner, assetsDir })
|
|
440
|
+
: observeAndMaterializeGistReceipts({ owner, assetsDir }))).observation,
|
|
441
|
+
build,
|
|
442
|
+
readLedger,
|
|
443
|
+
execute: (plan, round) => execute({
|
|
444
|
+
plan, assetsDir, workspaceDir: path.join(workspace, `round-${round}`), root,
|
|
445
|
+
}),
|
|
446
|
+
prune,
|
|
447
|
+
rebuild,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export async function reconcileAndPrepareCorpusCandidate({ plan, assetsDir, workspaceDir, root = DEFAULT_ROOT,
|
|
452
|
+
owner = 'ruvnet', builderSha, candidateDir, receiptFile, coverageFile,
|
|
453
|
+
execute = executeReconciliation, prepare = prepareCorpusCandidate } = {}) {
|
|
454
|
+
const reconciliation = await execute({ plan, assetsDir, workspaceDir, root });
|
|
455
|
+
const candidate = await prepare({ root, assetsDir, owner, builderSha, candidateDir, receiptFile, coverageFile });
|
|
456
|
+
return { reconciliation, candidate };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function prepareCorpusCandidate({
|
|
460
|
+
root = DEFAULT_ROOT,
|
|
461
|
+
assetsDir,
|
|
462
|
+
owner = 'ruvnet',
|
|
463
|
+
builderSha,
|
|
464
|
+
candidateDir,
|
|
465
|
+
receiptFile,
|
|
466
|
+
coverageFile,
|
|
467
|
+
run = defaultRun,
|
|
468
|
+
}) {
|
|
469
|
+
const sourceRoot = path.resolve(root);
|
|
470
|
+
const assets = path.resolve(assetsDir || '');
|
|
471
|
+
const candidate = path.resolve(candidateDir || path.join(sourceRoot, 'dist', 'corpus-candidate'));
|
|
472
|
+
const receipt = path.resolve(receiptFile || path.join(sourceRoot, 'dist', 'corpus-receipt.json'));
|
|
473
|
+
const policy = path.resolve(coverageFile || path.join(sourceRoot, 'data', 'source-coverage.json'));
|
|
474
|
+
if (!HEX40.test(String(builderSha || '').toLowerCase())) fail('builder SHA must be exact 40-character lowercase hex');
|
|
475
|
+
const expectedPolicy = path.join(sourceRoot, 'data', 'source-coverage.json');
|
|
476
|
+
if (policy !== expectedPolicy) fail(`coverage policy must be the generator's canonical projection (${expectedPolicy})`);
|
|
477
|
+
const coverageScript = path.join(sourceRoot, 'scripts', 'source-coverage.mjs');
|
|
478
|
+
const buildScript = path.join(sourceRoot, 'scripts', 'build-bundle.mjs');
|
|
479
|
+
const receiptScript = path.join(sourceRoot, 'scripts', 'corpus-candidate.mjs');
|
|
480
|
+
for (const required of [coverageScript, buildScript, receiptScript]) {
|
|
481
|
+
if (!fs.existsSync(required)) fail(`required candidate builder missing (${required})`);
|
|
482
|
+
}
|
|
483
|
+
fs.mkdirSync(path.dirname(candidate), { recursive: true });
|
|
484
|
+
fs.mkdirSync(path.dirname(receipt), { recursive: true });
|
|
485
|
+
checked(run, process.execPath, [coverageScript, '--owner', owner, '--assets', assets, '--write'], { stdio: 'inherit' });
|
|
486
|
+
checked(run, process.execPath, [coverageScript, '--owner', owner, '--assets', assets, '--check', '--strict'], { stdio: 'inherit' });
|
|
487
|
+
checked(run, process.execPath, [buildScript, '--assets', assets, '--out', candidate,
|
|
488
|
+
'--coverage', policy], { stdio: 'inherit' });
|
|
489
|
+
const bundleFile = path.join(path.dirname(candidate), `${path.basename(candidate)}.zip`);
|
|
490
|
+
checked(run, process.execPath, [receiptScript, '--assets', assets, '--bundle', bundleFile,
|
|
491
|
+
'--policy', policy, '--receipt', receipt, '--builder-source-sha', builderSha], { stdio: 'inherit' });
|
|
492
|
+
checked(run, process.execPath, [receiptScript, '--verify', '--assets', assets, '--bundle', bundleFile,
|
|
493
|
+
'--policy', policy, '--receipt', receipt], { stdio: 'inherit' });
|
|
494
|
+
return { bundleFile, receiptFile: receipt, coverageFile: policy };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function arg(argv, name, fallback = null) {
|
|
498
|
+
const index = argv.indexOf(name);
|
|
499
|
+
return index >= 0 && argv[index + 1] ? argv[index + 1] : fallback;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
503
|
+
const root = path.resolve(arg(argv, '--root', DEFAULT_ROOT));
|
|
504
|
+
const archiveFile = path.resolve(arg(argv, '--seed-archive', ''));
|
|
505
|
+
const seedTag = arg(argv, '--seed-tag');
|
|
506
|
+
const seedSha256 = arg(argv, '--seed-sha256');
|
|
507
|
+
const assetsDir = path.resolve(arg(argv, '--assets', ''));
|
|
508
|
+
const workspaceDir = path.resolve(arg(argv, '--workspace', ''));
|
|
509
|
+
const coverageFile = path.resolve(arg(argv, '--coverage', path.join(root, 'data', 'source-coverage.json')));
|
|
510
|
+
const candidateDir = path.resolve(arg(argv, '--candidate-out', path.join(root, 'dist', 'corpus-candidate')));
|
|
511
|
+
const receiptFile = path.resolve(arg(argv, '--receipt-out', path.join(root, 'dist', 'corpus-receipt.json')));
|
|
512
|
+
const builderSha = String(arg(argv, '--builder-sha', '')).toLowerCase();
|
|
513
|
+
const owner = arg(argv, '--owner', 'ruvnet');
|
|
514
|
+
|
|
515
|
+
assertBootstrapIdentity({ archiveFile, tag: seedTag, sha256: seedSha256, allowPinnedTag: process.argv.includes('--allow-pinned-seed-tag') });
|
|
516
|
+
if (fs.existsSync(assetsDir) && fs.readdirSync(assetsDir).length) fail(`bootstrap assets directory is not empty (${assetsDir})`);
|
|
517
|
+
fs.mkdirSync(path.dirname(assetsDir), { recursive: true });
|
|
518
|
+
const extractParent = fs.mkdtempSync(path.join(path.dirname(assetsDir), '.corpus-seed-extract-'));
|
|
519
|
+
await extractZip(archiveFile, extractParent);
|
|
520
|
+
normalizeExtractedCorpus({ extractedDir: extractParent, assetsDir });
|
|
521
|
+
const privateFence = path.join(root, 'kb', 'PRIVATE-STORES.json');
|
|
522
|
+
if (!fs.existsSync(privateFence)) fail(`canonical private-store fence missing (${privateFence})`);
|
|
523
|
+
fs.copyFileSync(privateFence, path.join(assetsDir, 'PRIVATE-STORES.json'), fs.constants.COPYFILE_EXCL);
|
|
524
|
+
fs.rmSync(extractParent, { recursive: true, force: true });
|
|
525
|
+
syncCorpusInputs({ root, assetsDir });
|
|
526
|
+
const { reconciliation, candidate } = await reconcileAndPrepareCorpusCandidate({
|
|
527
|
+
plan: [], assetsDir, workspaceDir, root, owner, builderSha, candidateDir, receiptFile, coverageFile,
|
|
528
|
+
execute: () => reconcileCorpusUntilStable({ owner, assetsDir, workspaceDir, root }),
|
|
529
|
+
});
|
|
530
|
+
const plan = reconciliation.rounds.flatMap((round) => round.plan);
|
|
531
|
+
process.stdout.write(`${JSON.stringify({ ok: true, seedTag, seedSha256, plan, reconciliation, ...candidate }, null, 2)}\n`);
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
|
|
536
|
+
main().then((code) => { process.exitCode = code; }).catch((error) => {
|
|
537
|
+
console.error(error.message);
|
|
538
|
+
process.exitCode = 1;
|
|
539
|
+
});
|
|
540
|
+
}
|
|
@@ -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 async 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 = await 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 = await 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
|
+
}
|