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.
Files changed (74) hide show
  1. package/README.md +31 -7
  2. package/bin/install.mjs +153 -22
  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/decision-outcomes.mjs +0 -0
  23. package/plugin/scripts/ground-ruvnet.sh +2 -2
  24. package/plugin/scripts/nightly-controller.mjs +19 -2
  25. package/plugin/scripts/project-progression-contract.mjs +409 -0
  26. package/plugin/scripts/project-progression-hook.mjs +196 -0
  27. package/plugin/scripts/project-progression-outbox.mjs +95 -0
  28. package/plugin/scripts/project-progression-session-start.mjs +183 -0
  29. package/plugin/scripts/project-progression-store.mjs +248 -0
  30. package/plugin/scripts/project-store-resolver.mjs +106 -0
  31. package/plugin/scripts/session-snapshot-hook.mjs +29 -1
  32. package/plugin/scripts/session-start-core.mjs +22 -18
  33. package/plugin/scripts/spend-guard.mjs +6 -1
  34. package/scripts/adr-072-completion.mjs +98 -0
  35. package/scripts/behavioral-l1-l4.mjs +11 -8
  36. package/scripts/brain-score.mjs +23 -9
  37. package/scripts/build-bundle.mjs +94 -8
  38. package/scripts/card-from-source.mjs +140 -0
  39. package/scripts/corpus-aggregates.mjs +194 -0
  40. package/scripts/corpus-candidate.mjs +314 -0
  41. package/scripts/corpus-reconcile.mjs +540 -0
  42. package/scripts/corpus-seed-publish.mjs +110 -0
  43. package/scripts/coverage-integrity.mjs +3 -0
  44. package/scripts/gist-receipts.mjs +183 -0
  45. package/scripts/git-hooks/pre-push +21 -0
  46. package/scripts/host-registry.mjs +118 -0
  47. package/scripts/independent-review-receipt.mjs +499 -0
  48. package/scripts/ingest-gists.mjs +34 -5
  49. package/scripts/ingest-new-repos.mjs +127 -0
  50. package/scripts/metaharness-gate.mjs +58 -0
  51. package/scripts/nightly-gists.sh +3 -3
  52. package/scripts/nightly-wrapper.sh +105 -64
  53. package/scripts/product-integrity-contract.mjs +156 -0
  54. package/scripts/public-inventory.mjs +4 -0
  55. package/scripts/public-verification-aggregate.mjs +342 -0
  56. package/scripts/public-verification-finalizer.mjs +59 -0
  57. package/scripts/public-verification-inputs.mjs +473 -0
  58. package/scripts/public-verification-lane.mjs +244 -0
  59. package/scripts/publication-receipt.mjs +50 -6
  60. package/scripts/rebuild-gists-from-receipts.mjs +291 -0
  61. package/scripts/release-projection.mjs +86 -0
  62. package/scripts/release-transaction-provider.mjs +43 -102
  63. package/scripts/release-transaction.mjs +165 -49
  64. package/scripts/release.mjs +158 -3
  65. package/scripts/restore-local-ingests.mjs +2 -2
  66. package/scripts/retrieval-canary.mjs +533 -0
  67. package/scripts/routing-flywheel.mjs +12 -1
  68. package/scripts/rvf-generation.mjs +50 -0
  69. package/scripts/self-update.mjs +19 -44
  70. package/scripts/source-coverage.mjs +415 -0
  71. package/scripts/source-scope-receipt.mjs +84 -0
  72. package/scripts/sync-census.mjs +0 -0
  73. package/scripts/wired-check.mjs +114 -24
  74. 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
  }
@@ -0,0 +1,291 @@
1
+ #!/usr/bin/env node
2
+ // Deterministically reconstruct gist passages from durable, exact-version source receipts.
3
+ // This intentionally does not call the GitHub API, build vectors, or publish artifacts.
4
+
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { sealGistReceiptSet, validateGistReceiptSet } from './gist-receipts.mjs';
11
+
12
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
+ const NAME = 'ruv-gists';
14
+ const HEX_GIST = /^[a-f0-9]{20,64}$/;
15
+ const HEX40 = /^[a-f0-9]{40}$/;
16
+ const HEX64 = /^[a-f0-9]{64}$/;
17
+
18
+ function fail(message) {
19
+ throw new Error(`[rebuild-gists-from-receipts] ${message}`);
20
+ }
21
+
22
+ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
23
+ const jsonLine = (value) => JSON.stringify(value).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
24
+
25
+ function validDate(value) {
26
+ return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T/.test(value) && Number.isFinite(Date.parse(value));
27
+ }
28
+
29
+ function validateFilename(filename, label) {
30
+ if (typeof filename !== 'string' || !filename || filename.includes('\0')
31
+ || filename.split(/[\\/]/).some((segment) => !segment || segment === '.' || segment === '..')) {
32
+ fail(`${label} has an unsafe or missing filename`);
33
+ }
34
+ }
35
+
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
+ }
50
+ if (![1, 2].includes(source?.schemaVersion)) fail('source receipt schemaVersion must be 1 or 2');
51
+ if (!/^[A-Za-z0-9-]{1,39}$/.test(String(source.owner || ''))) fail('source receipt owner is malformed');
52
+ if (!validDate(source.generated)) fail('source receipt generated timestamp is malformed');
53
+ if (!source.gists || typeof source.gists !== 'object' || Array.isArray(source.gists)) {
54
+ fail('source receipt gists must be an object');
55
+ }
56
+ if (Object.keys(source.gists).length === 0) fail('source receipt has no gist receipts');
57
+
58
+ for (const [gistId, receipt] of Object.entries(source.gists)) {
59
+ const label = `gist ${gistId}`;
60
+ if (!HEX_GIST.test(gistId)) fail(`${label} id is malformed`);
61
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) fail(`${label} receipt is malformed`);
62
+ if (receipt.complete !== true) fail(`${label} receipt is incomplete`);
63
+ if (!HEX40.test(String(receipt.versionSha || ''))) fail(`${label} versionSha is malformed`);
64
+ if (!validDate(receipt.updatedAt) || !validDate(receipt.ingestedAt)) fail(`${label} timestamps are malformed`);
65
+ if (!Array.isArray(receipt.files) || receipt.files.length === 0) fail(`${label} has no complete file inventory`);
66
+ const filenames = new Set();
67
+ for (const file of receipt.files) {
68
+ validateFilename(file?.filename, label);
69
+ if (filenames.has(file.filename)) fail(`${label} has duplicate filename ${file.filename}`);
70
+ filenames.add(file.filename);
71
+ if (file.included === true) {
72
+ if (!HEX64.test(String(file.sha256 || '')) || !Number.isSafeInteger(file.bytes) || file.bytes < 0) {
73
+ fail(`${label}/${file.filename} has malformed included-file identity`);
74
+ }
75
+ } else if (file.included !== false || typeof file.reason !== 'string' || !file.reason
76
+ || !(file.size === null || (Number.isSafeInteger(file.size) && file.size >= 0))) {
77
+ fail(`${label}/${file.filename} has malformed exclusion evidence`);
78
+ }
79
+ }
80
+ const expectedDigest = sha256(JSON.stringify(receipt.files));
81
+ if (!HEX64.test(String(receipt.contentDigest || '')) || receipt.contentDigest !== expectedDigest) {
82
+ fail(`${label} contentDigest does not match its file inventory`);
83
+ }
84
+ }
85
+ return source;
86
+ }
87
+
88
+ export function rawUrlFor({ owner, gistId, versionSha, filename }) {
89
+ if (!/^[A-Za-z0-9-]{1,39}$/.test(String(owner || '')) || !HEX_GIST.test(String(gistId || ''))
90
+ || !HEX40.test(String(versionSha || ''))) fail('cannot construct raw URL from malformed source identity');
91
+ validateFilename(filename, `gist ${gistId}`);
92
+ const encodedFile = filename.split('/').map(encodeURIComponent).join('/');
93
+ return `https://gist.githubusercontent.com/${owner}/${gistId}/raw/${versionSha}/${encodedFile}`;
94
+ }
95
+
96
+ export function paragraphChunks(text, size = 3200) {
97
+ if (typeof text !== 'string') fail('passage content must be text');
98
+ if (!Number.isSafeInteger(size) || size < 1) fail('chunk size must be a positive integer');
99
+ const out = [];
100
+ let buffer = '';
101
+ for (const paragraph of text.split(/\n\n+/)) {
102
+ if (buffer && buffer.length + paragraph.length + 2 > size) {
103
+ out.push(buffer);
104
+ buffer = '';
105
+ }
106
+ buffer = buffer ? `${buffer}\n\n${paragraph}` : paragraph;
107
+ }
108
+ if (buffer.trim()) out.push(buffer);
109
+ return out;
110
+ }
111
+
112
+ export function provenanceBanner({ owner, gistId, filename, updatedAt }) {
113
+ return `SOURCE: GitHub gist by @${owner} — "${filename.replace(/\s+/g, ' ').trim().slice(0, 160)}"\n`
114
+ + `GIST STATUS: rUv's own notes / release announcement — may describe PROPOSED or UNRELEASED work.\n`
115
+ + `Treat as intent, not as confirmed shipped behavior: verify against repo source before asserting.\n`
116
+ + `updated: ${updatedAt.slice(0, 10)} · https://gist.github.com/${owner}/${gistId}\n\n`;
117
+ }
118
+
119
+ async function mapBounded(items, concurrency, operation) {
120
+ const results = new Array(items.length);
121
+ let next = 0;
122
+ async function worker() {
123
+ while (next < items.length) {
124
+ const index = next++;
125
+ results[index] = await operation(items[index]);
126
+ }
127
+ }
128
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
129
+ return results;
130
+ }
131
+
132
+ export async function reconstructGists(source, { fetchFn = globalThis.fetch, concurrency = 6 } = {}) {
133
+ validateSourceReceipts(source);
134
+ if (typeof fetchFn !== 'function') fail('fetch implementation is unavailable');
135
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 32) {
136
+ fail('concurrency must be an integer from 1 through 32');
137
+ }
138
+
139
+ const jobs = [];
140
+ for (const [gistId, receipt] of Object.entries(source.gists)) {
141
+ for (const file of receipt.files) {
142
+ if (file.included) jobs.push({ gistId, receipt, file });
143
+ }
144
+ }
145
+ const bodies = await mapBounded(jobs, concurrency, async ({ gistId, receipt, file }) => {
146
+ const url = rawUrlFor({ owner: source.owner, gistId, versionSha: receipt.versionSha, filename: file.filename });
147
+ const response = await fetchFn(url, {
148
+ headers: { accept: 'text/plain', 'user-agent': 'ruvnet-brain-gist-receipt-rebuild' },
149
+ });
150
+ if (!response?.ok) fail(`${gistId}/${file.filename} raw fetch failed: HTTP ${response?.status ?? 'unknown'}`);
151
+ const bytes = Buffer.from(await response.arrayBuffer());
152
+ if (bytes.length !== file.bytes) fail(`${gistId}/${file.filename} byte count ${bytes.length} differs from receipt ${file.bytes}`);
153
+ const digest = sha256(bytes);
154
+ if (digest !== file.sha256) fail(`${gistId}/${file.filename} sha256 ${digest} differs from receipt ${file.sha256}`);
155
+ let body;
156
+ try {
157
+ body = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
158
+ } catch {
159
+ fail(`${gistId}/${file.filename} is not valid UTF-8 text`);
160
+ }
161
+ return body;
162
+ });
163
+
164
+ const passages = [];
165
+ const entries = {};
166
+ let bodyIndex = 0;
167
+ let id = 0;
168
+ for (const [gistId, receipt] of Object.entries(source.gists)) {
169
+ for (const file of receipt.files) {
170
+ if (!file.included) continue;
171
+ const body = bodies[bodyIndex++];
172
+ const chunks = paragraphChunks(body);
173
+ const title = file.filename.replace(/\s+/g, ' ').trim().slice(0, 180) || file.filename;
174
+ const banner = provenanceBanner({ owner: source.owner, gistId, filename: file.filename, updatedAt: receipt.updatedAt });
175
+ for (const [chunkIndex, chunk] of chunks.entries()) {
176
+ const passageId = String(id++);
177
+ const passagePath = `${gistId.slice(0, 8)}/${file.filename}${chunks.length > 1 ? `#${chunkIndex}` : ''}`;
178
+ const text = banner + chunk;
179
+ passages.push({ id: passageId, text, path: passagePath, title });
180
+ entries[passageId] = { path: passagePath, kind: 'doc', title, chunk: chunkIndex, preview: text.slice(0, 200) };
181
+ }
182
+ }
183
+ }
184
+ const passageBody = passages.map(jsonLine).join('\n') + '\n';
185
+ const meta = {
186
+ model: NAME,
187
+ dimensions: 0,
188
+ metric: 'cosine',
189
+ name: NAME,
190
+ generated: source.generated,
191
+ repo: `gists/${source.owner}`,
192
+ note: "rUv's public gists — announcements and thinking, PROPOSED unless confirmed in repo source.",
193
+ entries,
194
+ };
195
+ const passagesSha256 = sha256(passageBody);
196
+ return {
197
+ passages,
198
+ passageBody,
199
+ meta,
200
+ sources: source.schemaVersion === 3
201
+ ? sealGistReceiptSet({ ...source, passagesSha256 })
202
+ : { ...source, schemaVersion: 2, passagesSha256, gists: source.gists },
203
+ };
204
+ }
205
+
206
+ export function writeReconstruction(result, { outDir }) {
207
+ const output = path.resolve(outDir || '');
208
+ if (!result?.passageBody || !result.meta || !result.sources) fail('complete reconstruction result is required');
209
+ fs.mkdirSync(output, { recursive: true });
210
+ const files = {
211
+ passagesFile: path.join(output, `${NAME}.passages.jsonl`),
212
+ metaFile: path.join(output, `${NAME}.meta.json`),
213
+ sourcesFile: path.join(output, `${NAME}.sources.json`),
214
+ };
215
+ const contents = new Map([
216
+ [files.passagesFile, result.passageBody],
217
+ [files.metaFile, `${JSON.stringify(result.meta, null, 2)}\n`],
218
+ [files.sourcesFile, `${JSON.stringify(result.sources, null, 2)}\n`],
219
+ ]);
220
+ const staged = [];
221
+ try {
222
+ for (const [file, content] of contents) {
223
+ const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
224
+ fs.writeFileSync(temporary, content, { flag: 'wx' });
225
+ staged.push([temporary, file]);
226
+ }
227
+ for (const [temporary, file] of staged) fs.renameSync(temporary, file);
228
+ } finally {
229
+ for (const [temporary] of staged) fs.rmSync(temporary, { force: true });
230
+ }
231
+ return files;
232
+ }
233
+
234
+ function option(argv, name, fallback) {
235
+ const index = argv.indexOf(name);
236
+ return index >= 0 && argv[index + 1] && !argv[index + 1].startsWith('--') ? argv[index + 1] : fallback;
237
+ }
238
+
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
+ } = {}) {
267
+ const allowed = new Set(['--sources', '--out-dir', '--concurrency']);
268
+ for (let index = 0; index < argv.length; index += 2) {
269
+ if (!allowed.has(argv[index]) || !argv[index + 1] || argv[index + 1].startsWith('--')) fail(`unknown or incomplete option ${argv[index] || '(missing)'}`);
270
+ }
271
+ const sourcesFile = path.resolve(option(argv, '--sources', resolveSourcesFile({ repoRoot, env, home })));
272
+ const outDir = path.resolve(option(argv, '--out-dir', path.dirname(sourcesFile)));
273
+ const concurrency = Number(option(argv, '--concurrency', '6'));
274
+ let source;
275
+ try {
276
+ source = JSON.parse(fs.readFileSync(sourcesFile, 'utf8'));
277
+ } catch (error) {
278
+ fail(`cannot read source receipts ${sourcesFile}: ${error.message}`);
279
+ }
280
+ const result = await reconstructGists(source, { concurrency, fetchFn });
281
+ const written = writeReconstruction(result, { outDir });
282
+ process.stdout.write(`${JSON.stringify({ ok: true, passages: result.passages.length, passagesSha256: result.sources.passagesSha256, ...written }, null, 2)}\n`);
283
+ return 0;
284
+ }
285
+
286
+ if (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) {
287
+ main().then((code) => { process.exitCode = code; }).catch((error) => {
288
+ console.error(error.message);
289
+ process.exitCode = 1;
290
+ });
291
+ }