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.
- package/README.md +15 -14
- package/bin/install.mjs +11 -2
- 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/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 +7 -2
- package/scripts/build-bundle.mjs +82 -6
- package/scripts/card-from-source.mjs +32 -6
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +56 -36
- package/scripts/corpus-reconcile.mjs +295 -28
- package/scripts/corpus-seed-publish.mjs +3 -3
- 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 +5 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +75 -69
- 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 +50 -5
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +139 -51
- package/scripts/release.mjs +15 -7
- 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 +9 -3
- 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,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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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',
|
|
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
|
+
}
|