ruvnet-brain 4.2.2-dev → 4.3.1

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.
Files changed (73) hide show
  1. package/README.md +15 -14
  2. package/bin/install.mjs +11 -2
  3. package/console/architecture.html +12 -14
  4. package/console/install-mockup.html +3 -3
  5. package/kb/zip-extract.mjs +8 -2
  6. package/package.json +7 -1
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/hooks/codex-hooks.json +1 -1
  10. package/plugin/hooks/hooks.json +10 -0
  11. package/plugin/host-adapters/claude.json +11 -0
  12. package/plugin/host-adapters/codex.json +11 -0
  13. package/plugin/mcp/managed-cli-interface.mjs +77 -10
  14. package/plugin/mcp/server.mjs +2 -1
  15. package/plugin/scripts/anticipate.sh +30 -1
  16. package/plugin/scripts/capability-claim-evidence.mjs +431 -0
  17. package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
  18. package/plugin/scripts/capability-registry.mjs +6 -0
  19. package/plugin/scripts/capability-routing.mjs +70 -0
  20. package/plugin/scripts/continuation-gate.mjs +68 -2
  21. package/plugin/scripts/coverage-integrity.mjs +575 -0
  22. package/plugin/scripts/ground-ruvnet.sh +2 -2
  23. package/plugin/scripts/nightly-controller.mjs +19 -2
  24. package/plugin/scripts/project-progression-contract.mjs +409 -0
  25. package/plugin/scripts/project-progression-hook.mjs +196 -0
  26. package/plugin/scripts/project-progression-outbox.mjs +95 -0
  27. package/plugin/scripts/project-progression-session-start.mjs +183 -0
  28. package/plugin/scripts/project-progression-store.mjs +248 -0
  29. package/plugin/scripts/project-store-resolver.mjs +106 -0
  30. package/plugin/scripts/session-snapshot-hook.mjs +29 -1
  31. package/plugin/scripts/session-start-core.mjs +22 -18
  32. package/plugin/scripts/spend-guard.mjs +6 -1
  33. package/scripts/adr-072-completion.mjs +98 -0
  34. package/scripts/behavioral-l1-l4.mjs +11 -8
  35. package/scripts/brain-score.mjs +7 -2
  36. package/scripts/build-bundle.mjs +82 -6
  37. package/scripts/card-from-source.mjs +32 -6
  38. package/scripts/corpus-aggregates.mjs +194 -0
  39. package/scripts/corpus-candidate.mjs +56 -36
  40. package/scripts/corpus-reconcile.mjs +295 -28
  41. package/scripts/corpus-seed-publish.mjs +3 -3
  42. package/scripts/coverage-integrity.mjs +3 -0
  43. package/scripts/gist-receipts.mjs +183 -0
  44. package/scripts/git-hooks/pre-push +21 -0
  45. package/scripts/host-registry.mjs +118 -0
  46. package/scripts/independent-review-receipt.mjs +499 -0
  47. package/scripts/ingest-gists.mjs +34 -5
  48. package/scripts/ingest-new-repos.mjs +5 -0
  49. package/scripts/metaharness-gate.mjs +58 -0
  50. package/scripts/nightly-gists.sh +3 -3
  51. package/scripts/nightly-wrapper.sh +75 -69
  52. package/scripts/product-integrity-contract.mjs +156 -0
  53. package/scripts/public-inventory.mjs +4 -0
  54. package/scripts/public-verification-aggregate.mjs +342 -0
  55. package/scripts/public-verification-finalizer.mjs +59 -0
  56. package/scripts/public-verification-inputs.mjs +473 -0
  57. package/scripts/public-verification-lane.mjs +244 -0
  58. package/scripts/publication-receipt.mjs +50 -6
  59. package/scripts/rebuild-gists-from-receipts.mjs +50 -5
  60. package/scripts/release-projection.mjs +86 -0
  61. package/scripts/release-transaction-provider.mjs +43 -102
  62. package/scripts/release-transaction.mjs +139 -51
  63. package/scripts/release.mjs +15 -7
  64. package/scripts/restore-local-ingests.mjs +2 -2
  65. package/scripts/retrieval-canary.mjs +533 -0
  66. package/scripts/routing-flywheel.mjs +12 -1
  67. package/scripts/rvf-generation.mjs +9 -3
  68. package/scripts/self-update.mjs +19 -44
  69. package/scripts/source-coverage.mjs +415 -0
  70. package/scripts/source-scope-receipt.mjs +84 -0
  71. package/scripts/sync-census.mjs +0 -0
  72. package/scripts/wired-check.mjs +114 -24
  73. package/scripts/worktree-integrity.mjs +123 -0
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import osModule from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { canonicalJson, digest, validateCoverageLedger } from './coverage-integrity.mjs';
8
+ import { validateHostRegistry } from './host-registry.mjs';
9
+ import { RECEIPT_MODE_NAMES } from './host-install-matrix.mjs';
10
+ import { generatePublicationReceipt, livePublicationAdapter } from './publication-receipt.mjs';
11
+ import {
12
+ createPublicVerificationLeaf,
13
+ PUBLIC_VERIFICATION_MODES,
14
+ PUBLIC_VERIFICATION_OS,
15
+ } from './public-verification-aggregate.mjs';
16
+ import { transactionIdFor } from './release-transaction.mjs';
17
+ import {
18
+ runRetrievalCanaries,
19
+ validatePlanAgainstCoverage,
20
+ validateRetrievalCanaryReceipt,
21
+ } from './retrieval-canary.mjs';
22
+
23
+ const HEX40 = /^[a-f0-9]{40}$/;
24
+ const HEX64 = /^[a-f0-9]{64}$/;
25
+ const normalizedDigest = (value) => String(value || '').replace(/^sha256:/, '');
26
+
27
+ function validateReleaseIdentity(identity) {
28
+ if (identity?.repository !== 'stuinfla/ruvnet-brain' || identity?.package !== 'ruvnet-brain'
29
+ || !HEX40.test(String(identity.candidateSha || '')) || !HEX64.test(String(identity.payloadId || ''))
30
+ || !HEX64.test(String(identity.packageSha256 || '')) || !HEX64.test(String(identity.bundleSha256 || ''))
31
+ || typeof identity.version !== 'string' || !identity.version || identity.tag !== `v${identity.version}`) {
32
+ throw new Error('release transaction identity is malformed');
33
+ }
34
+ return identity;
35
+ }
36
+
37
+ function validatePublicEvidence({ candidate, publication, identity }) {
38
+ const candidateArtifact = normalizedDigest(candidate?.artifact?.sha256);
39
+ const surface = publication?.postPublicationChecks?.find(({ name }) => name === 'published-surface-probe');
40
+ if (candidate?.sha !== identity.candidateSha || candidate?.version !== identity.version || candidate?.tag !== identity.tag
41
+ || candidateArtifact !== identity.packageSha256 || publication?.sha !== identity.candidateSha
42
+ || publication?.version !== identity.version || publication?.payloadId !== identity.payloadId
43
+ || publication?.artifactSha256 !== identity.packageSha256
44
+ || publication?.bundleArtifactSha256 !== identity.bundleSha256
45
+ || publication?.npm?.version !== identity.version || publication?.npm?.sha !== identity.candidateSha
46
+ || publication?.npm?.artifactSha256 !== identity.packageSha256
47
+ || publication?.githubRelease?.tag !== identity.tag || publication?.githubRelease?.sha !== identity.candidateSha
48
+ || publication?.githubRelease?.artifactSha256 !== identity.packageSha256
49
+ || publication?.brain?.status !== 'PASS' || publication.brain.selfStore !== true
50
+ || surface?.status !== 'completed' || surface?.conclusion !== 'success' || surface?.sha !== identity.candidateSha) {
51
+ throw new Error('public byte identity or functional surface evidence differs from the release transaction');
52
+ }
53
+ return publication;
54
+ }
55
+
56
+ function coverageProjection(coverage) {
57
+ const checked = validateCoverageLedger(coverage);
58
+ if (!checked.valid || coverage.kind !== 'ruvnet-brain-release-coverage') {
59
+ throw new Error(`release coverage is invalid: ${checked.failures.join('; ')}`);
60
+ }
61
+ const repositories = coverage.rows.filter((row) => row.kind === 'repository' && row.disposition === 'eligible');
62
+ const gists = coverage.rows.filter((row) => row.kind === 'gist' && row.disposition === 'eligible');
63
+ const currentRepositories = repositories.filter(({ status }) => status === 'CURRENT').length;
64
+ const currentGists = gists.filter(({ status }) => status === 'CURRENT').length;
65
+ if (currentRepositories !== repositories.length || currentGists !== gists.length) {
66
+ throw new Error('release coverage contains a non-current eligible source');
67
+ }
68
+ return {
69
+ verified: true,
70
+ eligibleCurrent: currentRepositories,
71
+ eligibleTotal: repositories.length,
72
+ gistCurrent: currentGists,
73
+ gistTotal: gists.length,
74
+ };
75
+ }
76
+
77
+ export async function createPublicVerificationLane({
78
+ os,
79
+ candidate,
80
+ publication,
81
+ identity,
82
+ releaseCoverage,
83
+ coverageIdentity,
84
+ retrievalPlan,
85
+ hostRegistry,
86
+ adapter,
87
+ } = {}) {
88
+ if (!PUBLIC_VERIFICATION_OS.includes(os)) throw new Error(`unsupported public verification OS: ${os || '(missing)'}`);
89
+ validateReleaseIdentity(identity);
90
+ validatePublicEvidence({ candidate, publication, identity });
91
+ const registry = validateHostRegistry(hostRegistry);
92
+ const coverage = coverageProjection(releaseCoverage);
93
+ if (!coverageIdentity || retrievalPlan?.coverage?.sha256 !== coverageIdentity.sha256
94
+ || retrievalPlan.coverage.bytes !== coverageIdentity.bytes
95
+ || releaseCoverage.releaseCoverageGeneration !== retrievalPlan.coverage.releaseCoverageGeneration) {
96
+ throw new Error('retrieval plan coverage byte identity differs');
97
+ }
98
+ validatePlanAgainstCoverage(retrievalPlan, releaseCoverage, { allowObservedBaseline: true });
99
+ if (retrievalPlan.candidate.sourceSha !== identity.candidateSha
100
+ || retrievalPlan.candidate.packageSha256 !== identity.packageSha256
101
+ || retrievalPlan.candidate.archiveSha256 !== identity.bundleSha256) {
102
+ throw new Error('retrieval plan candidate identity differs from the release transaction');
103
+ }
104
+ if (!adapter || typeof adapter.searchInstalled !== 'function'
105
+ || typeof adapter.resolveInstalledCitation !== 'function') {
106
+ throw new Error('public verification lane adapter is incomplete');
107
+ }
108
+ const common = {
109
+ sourceSha: identity.candidateSha,
110
+ version: identity.version,
111
+ tag: identity.tag,
112
+ artifactSha256: identity.packageSha256,
113
+ bundleSha256: identity.bundleSha256,
114
+ payloadId: identity.payloadId,
115
+ hostRegistryDigest: registry.registrySha256,
116
+ coverageGeneration: releaseCoverage.releaseCoverageGeneration,
117
+ canaryPlanSha256: retrievalPlan.planSha256,
118
+ releaseTransactionId: transactionIdFor(identity),
119
+ };
120
+ const leaves = [];
121
+ for (const mode of PUBLIC_VERIFICATION_MODES) {
122
+ const installed = publication.installed?.[RECEIPT_MODE_NAMES[mode]];
123
+ if (installed?.status !== 'PASS' || installed.doctorExit !== 0 || installed.version !== identity.version
124
+ || installed.artifactSha256 !== identity.packageSha256 || installed.functionalSearch !== true) {
125
+ throw new Error(`${os}/${mode} installed public loader evidence is incomplete`);
126
+ }
127
+ const retrieval = await runRetrievalCanaries({
128
+ plan: retrievalPlan,
129
+ sourceSha: identity.candidateSha,
130
+ artifactSha256: identity.packageSha256,
131
+ candidateArchiveSha256: identity.bundleSha256,
132
+ search: ({ query, k }) => adapter.searchInstalled({ mode, query, k }),
133
+ citationResolver: (matched, expected) => adapter.resolveInstalledCitation({ mode, matched, expected }),
134
+ });
135
+ const verifiedRetrieval = validateRetrievalCanaryReceipt(
136
+ retrieval,
137
+ { plan: retrievalPlan, requireAcceptance: true },
138
+ );
139
+ leaves.push(createPublicVerificationLeaf({
140
+ ...common,
141
+ os,
142
+ mode,
143
+ status: verifiedRetrieval.receiptSha256 === retrieval.receiptSha256 ? 'completed' : 'failed',
144
+ verdict: 'PASS',
145
+ publicBytes: { npmExact: true, githubExact: true, bundleExact: true },
146
+ installed: { version: installed.version, loaderVerified: true },
147
+ coverage,
148
+ retrievalPlan,
149
+ retrieval,
150
+ untested: [],
151
+ skipped: 0,
152
+ unknown: 0,
153
+ }));
154
+ }
155
+ if (canonicalJson(leaves.map(({ mode }) => mode)) !== canonicalJson(PUBLIC_VERIFICATION_MODES)) {
156
+ throw new Error('public verification lane modes are incomplete');
157
+ }
158
+ return leaves;
159
+ }
160
+
161
+ const hostOperatingSystem = () => ({ linux: 'linux', darwin: 'macos', win32: 'windows' })[process.platform] || null;
162
+
163
+ function regularJson(file, label) {
164
+ const absolute = path.resolve(file || '');
165
+ let stat;
166
+ try { stat = fs.lstatSync(absolute); } catch { throw new Error(`${label} is missing`); }
167
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${label} is not a trusted regular file`);
168
+ return { absolute, bytes: fs.readFileSync(absolute), value: JSON.parse(fs.readFileSync(absolute, 'utf8')) };
169
+ }
170
+
171
+ export async function generatePublicVerificationLane({
172
+ os,
173
+ candidatePath,
174
+ identityPath,
175
+ coveragePath,
176
+ planPath,
177
+ registryPath,
178
+ outPath,
179
+ root = process.cwd(),
180
+ adapter = livePublicationAdapter({ root }),
181
+ } = {}) {
182
+ if (os !== hostOperatingSystem()) throw new Error(`runner OS ${hostOperatingSystem() || process.platform} cannot produce ${os || '(missing)'}`);
183
+ const candidate = regularJson(candidatePath, 'candidate receipt');
184
+ const identity = regularJson(identityPath, 'release identity');
185
+ const releaseCoverage = regularJson(coveragePath, 'release coverage');
186
+ const retrievalPlan = regularJson(planPath, 'retrieval plan');
187
+ const hostRegistry = regularJson(registryPath, 'host registry');
188
+ const output = path.resolve(outPath || '');
189
+ if (!outPath) throw new Error('--out is required');
190
+ if (fs.existsSync(output)) throw new Error(`refusing to overwrite public verification lane: ${output}`);
191
+ const temp = fs.mkdtempSync(path.join(osModule.tmpdir(), `ruvnet-public-${os}-`));
192
+ const publicationPath = path.join(temp, 'publication-receipt.json');
193
+ try {
194
+ await generatePublicationReceipt({ root, candidatePath: candidate.absolute, outPath: publicationPath,
195
+ adapter, disposeAdapter: false });
196
+ const publication = JSON.parse(fs.readFileSync(publicationPath, 'utf8'));
197
+ const leaves = await createPublicVerificationLane({
198
+ os,
199
+ candidate: candidate.value,
200
+ publication,
201
+ identity: identity.value,
202
+ releaseCoverage: releaseCoverage.value,
203
+ coverageIdentity: { sha256: crypto.createHash('sha256').update(releaseCoverage.bytes).digest('hex'),
204
+ bytes: releaseCoverage.bytes.length },
205
+ retrievalPlan: retrievalPlan.value,
206
+ hostRegistry: hostRegistry.value,
207
+ adapter,
208
+ });
209
+ const payload = { schemaVersion: 1, kind: 'ruvnet-brain-public-verification-os-lane', os, leaves };
210
+ const receipt = { ...payload, laneSha256: digest(payload) };
211
+ fs.mkdirSync(path.dirname(output), { recursive: true });
212
+ fs.writeFileSync(output, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
213
+ return receipt;
214
+ } finally {
215
+ await adapter.dispose?.();
216
+ fs.rmSync(temp, { recursive: true, force: true });
217
+ }
218
+ }
219
+
220
+ function argument(args, name) {
221
+ const index = args.indexOf(name);
222
+ return index >= 0 ? args[index + 1] : null;
223
+ }
224
+
225
+ export async function main(args = process.argv.slice(2)) {
226
+ try {
227
+ const receipt = await generatePublicVerificationLane({
228
+ os: argument(args, '--os'),
229
+ candidatePath: argument(args, '--candidate'),
230
+ identityPath: argument(args, '--identity'),
231
+ coveragePath: argument(args, '--coverage'),
232
+ planPath: argument(args, '--plan'),
233
+ registryPath: argument(args, '--registry'),
234
+ outPath: argument(args, '--out'),
235
+ });
236
+ process.stdout.write(`${JSON.stringify({ ok: true, os: receipt.os, laneSha256: receipt.laneSha256 })}\n`);
237
+ return 0;
238
+ } catch (error) {
239
+ console.error(`public-verification-lane: ${error.message}`);
240
+ return 1;
241
+ }
242
+ }
243
+
244
+ if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) process.exitCode = await main();
@@ -7,6 +7,7 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { pipeline } from 'node:stream/promises';
9
9
  import { Readable } from 'node:stream';
10
+ import readline from 'node:readline';
10
11
  import { spawn, spawnSync } from 'node:child_process';
11
12
  // ONE doctor rule and ONE mode vocabulary, shared with the staged-side check in
12
13
  // scripts/staged-host-verifier.mjs. This file kept its own copies, spelled claudeOnly/
@@ -18,6 +19,8 @@ import { HOST_MODES, RECEIPT_MODE_NAMES, MODE_FROM_RECEIPT_NAME, classifyDoctor,
18
19
  import { pathToFileURL } from 'node:url';
19
20
  import { evaluateCandidateReceipt, evaluatePublicationReceipt } from './release-proof.mjs';
20
21
  import { verifyPayload } from './release-payload.mjs';
22
+ import { digest, sha256File } from './coverage-integrity.mjs';
23
+ import { parseCitations } from '../kb/verify-citation.mjs';
21
24
 
22
25
  const REPO = 'stuinfla/ruvnet-brain';
23
26
  const PACKAGE = 'ruvnet-brain';
@@ -183,7 +186,8 @@ function rpcSearch(server, env, query, timeoutMs = DEADLINE_MS) {
183
186
  }
184
187
 
185
188
  export function livePublicationAdapter({ root = process.cwd() } = {}) {
186
- let installContext = null;
189
+ const installContexts = new Map();
190
+ const passageFileDigests = new Map();
187
191
  let installTemp = null;
188
192
  return {
189
193
  async downloadNpm({ version, destination }) {
@@ -272,7 +276,7 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
272
276
  functionalSearch: true, searchMs: searched.broadMs, hostsOnPath: mode,
273
277
  ...verified,
274
278
  };
275
- if (mode === 'dual') installContext = { temp, home, codexHome, brainHome, kb, env, packageRoot };
279
+ installContexts.set(MODE_FROM_RECEIPT_NAME[mode], { temp, home, codexHome, brainHome, kb, env, packageRoot });
276
280
  }
277
281
  return {
278
282
  ...results,
@@ -280,8 +284,9 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
280
284
  };
281
285
  },
282
286
 
283
- async probeBrain() {
284
- if (!installContext) throw new Error('public hosts must be installed before Brain proof');
287
+ async probeBrain({ mode = 'dual' } = {}) {
288
+ const installContext = installContexts.get(mode);
289
+ if (!installContext) throw new Error(`${mode} public host must be installed before Brain proof`);
285
290
  const rvfs = fs.readdirSync(installContext.kb).filter((name) => /ruvnet-brain.*\.rvf$/i.test(name));
286
291
  if (rvfs.length === 0) throw new Error('installed public Brain has no ruvnet-brain self RVF');
287
292
  const server = path.join(installContext.home, '.claude', 'ruvnet-brain', 'mcp', 'server.mjs');
@@ -292,6 +297,43 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
292
297
  return { status: 'PASS', selfStore: true, broadMs: result.broadMs, deadlineMs: DEADLINE_MS };
293
298
  },
294
299
 
300
+ async searchInstalled({ mode, query }) {
301
+ const context = installContexts.get(mode);
302
+ if (!context) throw new Error(`${mode} public host is not installed`);
303
+ const result = await rpcSearch(findMcpServer(context.home), context.env, query, DEADLINE_MS);
304
+ return parseCitations(result.text).map((citation) => ({
305
+ repo: citation.repo.toLowerCase(),
306
+ path: citation.docPath,
307
+ }));
308
+ },
309
+
310
+ async resolveInstalledCitation({ mode, matched, expected }) {
311
+ const context = installContexts.get(mode);
312
+ if (!context) throw new Error(`${mode} public host is not installed`);
313
+ if (String(matched?.repo || '').toLowerCase() !== expected.repo || matched?.path !== expected.path) {
314
+ return { resolved: false };
315
+ }
316
+ const files = [
317
+ path.join(context.kb, `${expected.repo}.passages.jsonl`),
318
+ path.join(context.kb, `${expected.repo}.big.passages.jsonl`),
319
+ ].filter((file) => fs.existsSync(file));
320
+ for (const file of files) {
321
+ const rows = readline.createInterface({ input: fs.createReadStream(file), crlfDelay: Infinity });
322
+ try {
323
+ for await (const line of rows) {
324
+ let record;
325
+ try { record = JSON.parse(line); } catch { continue; }
326
+ if (record?.path !== expected.path) continue;
327
+ const passageSha256 = digest(record);
328
+ if (passageSha256 !== expected.passageSha256) return { resolved: false };
329
+ if (!passageFileDigests.has(file)) passageFileDigests.set(file, sha256File(file));
330
+ return { resolved: true, evidence: { passageSha256, passageFileSha256: passageFileDigests.get(file) } };
331
+ }
332
+ } finally { rows.close(); }
333
+ }
334
+ return { resolved: false };
335
+ },
336
+
295
337
  async probePublishedSurface({ sha }) {
296
338
  const head = command('git', ['rev-parse', 'HEAD'], { cwd: root });
297
339
  if (head !== sha) throw new Error(`published-surface probe checkout ${head} does not match candidate ${sha}`);
@@ -307,7 +349,8 @@ export function livePublicationAdapter({ root = process.cwd() } = {}) {
307
349
  async dispose() {
308
350
  if (installTemp) fs.rmSync(installTemp, { recursive: true, force: true });
309
351
  installTemp = null;
310
- installContext = null;
352
+ installContexts.clear();
353
+ passageFileDigests.clear();
311
354
  },
312
355
  };
313
356
  }
@@ -317,6 +360,7 @@ export async function generatePublicationReceipt({
317
360
  candidatePath,
318
361
  outPath,
319
362
  adapter = livePublicationAdapter({ root }),
363
+ disposeAdapter = true,
320
364
  } = {}) {
321
365
  if (!candidatePath || !outPath) throw new Error('candidatePath and outPath are required');
322
366
  if (fs.existsSync(outPath)) throw new Error(`refusing to overwrite existing publication receipt: ${outPath}`);
@@ -386,7 +430,7 @@ export async function generatePublicationReceipt({
386
430
  fs.writeFileSync(outPath, `${JSON.stringify(publication, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
387
431
  return result;
388
432
  } finally {
389
- await adapter.dispose?.();
433
+ if (disposeAdapter) await adapter.dispose?.();
390
434
  fs.rmSync(temp, { recursive: true, force: true });
391
435
  }
392
436
  }
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // Deterministically reconstruct gist passages from tracked, exact-version source receipts.
2
+ // Deterministically reconstruct gist passages from durable, exact-version source receipts.
3
3
  // This intentionally does not call the GitHub API, build vectors, or publish artifacts.
4
4
 
5
5
  import crypto from 'node:crypto';
6
6
  import fs from 'node:fs';
7
+ import os from 'node:os';
7
8
  import path from 'node:path';
8
9
  import { fileURLToPath } from 'node:url';
10
+ import { sealGistReceiptSet, validateGistReceiptSet } from './gist-receipts.mjs';
9
11
 
10
12
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
11
13
  const NAME = 'ruv-gists';
@@ -32,6 +34,19 @@ function validateFilename(filename, label) {
32
34
  }
33
35
 
34
36
  export function validateSourceReceipts(source) {
37
+ if (source?.schemaVersion === 3) {
38
+ const observation = {
39
+ owner: source.owner,
40
+ observedAt: source.observedAt,
41
+ observationSha256: source.sourceObservationSha256,
42
+ gists: { rows: Object.entries(source.gists || {}).map(([id, receipt]) => ({
43
+ id, updated_at: receipt?.updatedAt,
44
+ })) },
45
+ };
46
+ try { validateGistReceiptSet(source, observation); }
47
+ catch (error) { fail(`schema-3 source receipt is invalid: ${error.message}`); }
48
+ return source;
49
+ }
35
50
  if (![1, 2].includes(source?.schemaVersion)) fail('source receipt schemaVersion must be 1 or 2');
36
51
  if (!/^[A-Za-z0-9-]{1,39}$/.test(String(source.owner || ''))) fail('source receipt owner is malformed');
37
52
  if (!validDate(source.generated)) fail('source receipt generated timestamp is malformed');
@@ -177,11 +192,14 @@ export async function reconstructGists(source, { fetchFn = globalThis.fetch, con
177
192
  note: "rUv's public gists — announcements and thinking, PROPOSED unless confirmed in repo source.",
178
193
  entries,
179
194
  };
195
+ const passagesSha256 = sha256(passageBody);
180
196
  return {
181
197
  passages,
182
198
  passageBody,
183
199
  meta,
184
- sources: { ...source, schemaVersion: 2, passagesSha256: sha256(passageBody), gists: source.gists },
200
+ sources: source.schemaVersion === 3
201
+ ? sealGistReceiptSet({ ...source, passagesSha256 })
202
+ : { ...source, schemaVersion: 2, passagesSha256, gists: source.gists },
185
203
  };
186
204
  }
187
205
 
@@ -218,12 +236,39 @@ function option(argv, name, fallback) {
218
236
  return index >= 0 && argv[index + 1] && !argv[index + 1].startsWith('--') ? argv[index + 1] : fallback;
219
237
  }
220
238
 
221
- export async function main(argv = process.argv.slice(2)) {
239
+ /**
240
+ * The receipt belongs beside the installed brain, because a published npm package intentionally
241
+ * does not contain the 300KB generated receipt. Prefer a source-checkout copy when one exists, then
242
+ * honor the canonical brain-root overrides, then use the standard installed cache. Returning the
243
+ * checkout candidate on a total miss preserves one precise error path instead of guessing.
244
+ */
245
+ export function resolveSourcesFile({
246
+ repoRoot = ROOT,
247
+ env = process.env,
248
+ home = os.homedir(),
249
+ exists = fs.existsSync,
250
+ } = {}) {
251
+ const local = path.join(repoRoot, 'kb', `${NAME}.sources.json`);
252
+ const candidates = [
253
+ local,
254
+ env.RUVNET_BRAIN_KB && path.join(env.RUVNET_BRAIN_KB, `${NAME}.sources.json`),
255
+ env.RUVNET_BRAIN_HOME && path.join(env.RUVNET_BRAIN_HOME, 'kb', `${NAME}.sources.json`),
256
+ path.join(home, '.cache', 'ruvnet-brain', 'kb', `${NAME}.sources.json`),
257
+ ].filter(Boolean).map((candidate) => path.resolve(candidate));
258
+ return candidates.find((candidate) => exists(candidate)) || local;
259
+ }
260
+
261
+ export async function main(argv = process.argv.slice(2), {
262
+ fetchFn = globalThis.fetch,
263
+ repoRoot = ROOT,
264
+ env = process.env,
265
+ home = os.homedir(),
266
+ } = {}) {
222
267
  const allowed = new Set(['--sources', '--out-dir', '--concurrency']);
223
268
  for (let index = 0; index < argv.length; index += 2) {
224
269
  if (!allowed.has(argv[index]) || !argv[index + 1] || argv[index + 1].startsWith('--')) fail(`unknown or incomplete option ${argv[index] || '(missing)'}`);
225
270
  }
226
- const sourcesFile = path.resolve(option(argv, '--sources', path.join(ROOT, 'kb', `${NAME}.sources.json`)));
271
+ const sourcesFile = path.resolve(option(argv, '--sources', resolveSourcesFile({ repoRoot, env, home })));
227
272
  const outDir = path.resolve(option(argv, '--out-dir', path.dirname(sourcesFile)));
228
273
  const concurrency = Number(option(argv, '--concurrency', '6'));
229
274
  let source;
@@ -232,7 +277,7 @@ export async function main(argv = process.argv.slice(2)) {
232
277
  } catch (error) {
233
278
  fail(`cannot read source receipts ${sourcesFile}: ${error.message}`);
234
279
  }
235
- const result = await reconstructGists(source, { concurrency });
280
+ const result = await reconstructGists(source, { concurrency, fetchFn });
236
281
  const written = writeReconstruction(result, { outDir });
237
282
  process.stdout.write(`${JSON.stringify({ ok: true, passages: result.passages.length, passagesSha256: result.sources.passagesSha256, ...written }, null, 2)}\n`);
238
283
  return 0;
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ // Build the release-bound coverage projection from an observed corpus ledger and exact bundle bytes.
3
+ // This is intentionally a producer, not a validator: public-verification-inputs.mjs remains the
4
+ // independent consumer that proves the emitted files agree with the archive.
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { digest, generationLedgerBytes, projectPublicGenerationLedger,
10
+ releaseCoverageGenerationFor } from '../plugin/scripts/coverage-integrity.mjs';
11
+ import { validatePublicInventory } from './public-inventory.mjs';
12
+ import { readRvfGenerations } from './rvf-generation.mjs';
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
+ const arg = (name, fallback = null) => { const i = process.argv.indexOf(name); return i >= 0 ? process.argv[i + 1] : fallback; };
16
+ const readJson = (file) => JSON.parse(fs.readFileSync(file, 'utf8'));
17
+ const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
18
+ const storeName = (value) => String(value || '').toLowerCase().replace(/\.big\.rvf$|\.rvf$/i, '');
19
+
20
+ export function createReleaseProjection({ corpusCoverage, assetsDir, version, sourceSnapshot,
21
+ corpusSeed, baselineReceiptSha256, outDir }) {
22
+ if (!corpusCoverage || corpusCoverage.kind !== 'ruvnet-brain-corpus-coverage') {
23
+ throw new Error('corpus coverage must be an observed ruvnet-brain-corpus-coverage ledger');
24
+ }
25
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version || '')) throw new Error('release version is invalid');
26
+ if (!/^[a-f0-9]{40}$/.test(sourceSnapshot || '')) throw new Error('release source snapshot is invalid');
27
+ if (!corpusSeed?.tag || !/^[a-f0-9]{64}$/.test(corpusSeed.archiveSha256 || '')
28
+ || !Number.isSafeInteger(corpusSeed.archiveBytes) || !/^[a-f0-9]{64}$/.test(baselineReceiptSha256 || '')) {
29
+ throw new Error('corpus seed identity is incomplete');
30
+ }
31
+ const assets = path.resolve(assetsDir);
32
+ const sourceLedger = readRvfGenerations(assets);
33
+ // The coverage ledger describes the full upstream universe. A release candidate contains
34
+ // only the immutable seed's asset set, so absent-but-eligible upstream rows are not release
35
+ // blockers; shipped stores must still all be CURRENT.
36
+ const assetStores = fs.readdirSync(assets)
37
+ .filter((name) => /\.rvf$/i.test(name)).map(storeName);
38
+ const availableStores = new Set([
39
+ ...Object.keys(sourceLedger.stores || {}).map(storeName),
40
+ ...assetStores,
41
+ ]);
42
+ const eligible = corpusCoverage.rows.filter((row) => row.disposition === 'eligible'
43
+ && availableStores.has(storeName(row.artifact?.store)));
44
+ if (!eligible.length || eligible.some((row) => row.status !== 'CURRENT')) throw new Error('eligible corpus rows are not all CURRENT');
45
+ const publicStores = [...new Set(eligible.map((row) => String(row.artifact.store).toLowerCase()))];
46
+ // Derived public stores are not corpus rows, but they are still part of the
47
+ // exact shipped generation ledger when the candidate asset set contains them.
48
+ if (sourceLedger.stores?.concepts && !publicStores.includes('concepts')) publicStores.push('concepts');
49
+ const ledger = projectPublicGenerationLedger({ ledger: sourceLedger, publicStores, version, sourceSnapshot });
50
+ const releaseBase = {
51
+ schemaVersion: 1, kind: 'ruvnet-brain-release-coverage', owner: corpusCoverage.owner,
52
+ observedAt: corpusCoverage.observedAt, generatorSourceSha: corpusCoverage.generatorSourceSha,
53
+ sourceObservationSha256: corpusCoverage.sourceObservationSha256, snapshotRoot: corpusCoverage.snapshotRoot,
54
+ releaseIdentity: { version, tag: `v${version}`, sourceSnapshot },
55
+ corpusSeed: { tag: corpusSeed.tag, archiveSha256: corpusSeed.archiveSha256,
56
+ archiveBytes: corpusSeed.archiveBytes, receiptSha256: baselineReceiptSha256 },
57
+ corpusCoverage: { sha256: sha256(Buffer.from(`${JSON.stringify(corpusCoverage, null, 2)}\n`)), coverageGeneration: corpusCoverage.coverageGeneration },
58
+ generationLedger: { file: 'PUBLIC-RVF-GENERATIONS.json', sha256: sha256(generationLedgerBytes(ledger)),
59
+ bytes: generationLedgerBytes(ledger).length, storeCount: Object.keys(ledger.stores).length },
60
+ installedProjectionSchema: 2, policy: corpusCoverage.policy, enumerationReceipt: corpusCoverage.enumerationReceipt,
61
+ rows: corpusCoverage.rows, totals: corpusCoverage.totals,
62
+ };
63
+ const inventory = validatePublicInventory({ assetsDir: assets, coverage: releaseBase, ledger });
64
+ releaseBase.publicInventoryPartitionSha256 = inventory.partitionSha256;
65
+ releaseBase.releaseCoverageGeneration = releaseCoverageGenerationFor(releaseBase);
66
+ const out = path.resolve(outDir);
67
+ 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
+ fs.writeFileSync(path.join(out, 'CORPUS-COVERAGE.json'), corpusBytes);
71
+ fs.writeFileSync(path.join(out, 'COVERAGE.json'), coverageBytes);
72
+ fs.writeFileSync(path.join(out, 'PUBLIC-RVF-GENERATIONS.json'), generationLedgerBytes(ledger));
73
+ return releaseBase;
74
+ }
75
+
76
+ if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
77
+ try {
78
+ const corpusFile = path.resolve(arg('--corpus', path.join(ROOT, 'data', 'source-coverage.json')));
79
+ const corpus = readJson(corpusFile);
80
+ const projection = createReleaseProjection({ corpusCoverage: corpus,
81
+ assetsDir: arg('--assets'), outDir: arg('--out', 'release-evidence'), version: arg('--version'),
82
+ sourceSnapshot: arg('--source-snapshot'), baselineReceiptSha256: arg('--baseline-receipt-sha256'),
83
+ corpusSeed: { tag: arg('--seed-tag'), archiveSha256: arg('--seed-sha256'), archiveBytes: Number(arg('--seed-bytes')) } });
84
+ console.log(JSON.stringify({ ok: true, releaseCoverageGeneration: projection.releaseCoverageGeneration }));
85
+ } catch (error) { console.error(`[release-projection] ${error.message}`); process.exitCode = 1; }
86
+ }