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,575 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
const HEX40 = /^[a-f0-9]{40}$/;
|
|
6
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
7
|
+
const HEX_GIST = /^[a-f0-9]{20,64}$/;
|
|
8
|
+
export const PUBLIC_GENERATIONS_FILE = 'PUBLIC-RVF-GENERATIONS.json';
|
|
9
|
+
export const RUNTIME_GENERATIONS_FILE = 'RVF-GENERATIONS.json';
|
|
10
|
+
export const INSTALLED_PROFILE_FILE = 'INSTALLED-PROFILE.json';
|
|
11
|
+
|
|
12
|
+
export function canonicalJson(value) {
|
|
13
|
+
if (Array.isArray(value)) return `[${value.map((entry) => canonicalJson(entry ?? null)).join(',')}]`;
|
|
14
|
+
if (value && typeof value === 'object') {
|
|
15
|
+
return `{${Object.keys(value).filter((key) => value[key] !== undefined).sort()
|
|
16
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
17
|
+
}
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function digest(value) {
|
|
22
|
+
return crypto.createHash('sha256').update(typeof value === 'string' ? value : canonicalJson(value)).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateLegacyGistAggregateReceipt({ receipt, passagesFile, expectedIds }) {
|
|
26
|
+
const ids = Object.keys(receipt?.gists || {});
|
|
27
|
+
if (receipt?.schemaVersion !== 2 || receipt?.owner !== 'ruvnet' || !ids.length
|
|
28
|
+
|| (expectedIds && canonicalJson([...ids].sort()) !== canonicalJson([...expectedIds].map(String).sort()))) {
|
|
29
|
+
throw new Error('legacy gist aggregate receipt is malformed or has the wrong gist set');
|
|
30
|
+
}
|
|
31
|
+
for (const id of ids) {
|
|
32
|
+
const row = receipt.gists[id];
|
|
33
|
+
const files = row?.files;
|
|
34
|
+
if (!HEX_GIST.test(id) || !/^[a-f0-9]{40}$/.test(String(row?.versionSha || ''))
|
|
35
|
+
|| !Array.isArray(files) || !files.length
|
|
36
|
+
|| files.some((file) => typeof file?.filename !== 'string' || !file.filename
|
|
37
|
+
|| (file.included === true
|
|
38
|
+
? !HEX64.test(String(file.sha256 || '')) || !Number.isSafeInteger(file.bytes) || file.bytes < 0
|
|
39
|
+
: file.included !== false || typeof file.reason !== 'string' || !file.reason
|
|
40
|
+
|| !Number.isSafeInteger(file.size) || file.size < 0))
|
|
41
|
+
// Schema 2 was generated with JSON.stringify(array), before the
|
|
42
|
+
// canonical-json contract was introduced for schema 3. Preserve that
|
|
43
|
+
// historical receipt format while keeping the stricter contract below.
|
|
44
|
+
|| row.contentDigest !== crypto.createHash('sha256').update(JSON.stringify(files)).digest('hex')) {
|
|
45
|
+
throw new Error(`legacy gist ${id} receipt is incomplete or internally inconsistent`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// v2 receipts predate shipping the source JSONL alongside the seed RVF.
|
|
49
|
+
// The recorded passage digest is retained as historical metadata only;
|
|
50
|
+
// schema 3 is the first format whose source bytes are a release gate.
|
|
51
|
+
return receipt;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function validateGistAggregateReceipt({ receipt, passagesFile, expectedIds = null,
|
|
55
|
+
sourceObservationSha256 = null }) {
|
|
56
|
+
if (receipt?.schemaVersion === 2 && sourceObservationSha256 === null) {
|
|
57
|
+
return validateLegacyGistAggregateReceipt({ receipt, passagesFile, expectedIds });
|
|
58
|
+
}
|
|
59
|
+
if (receipt?.schemaVersion !== 3 || receipt?.kind !== 'ruvnet-brain-gist-source-receipts'
|
|
60
|
+
|| !receipt.gists || typeof receipt.gists !== 'object' || Array.isArray(receipt.gists)
|
|
61
|
+
|| !HEX64.test(String(receipt.sourceObservationSha256 || ''))) {
|
|
62
|
+
throw new Error('gist aggregate receipt schema or observation identity is invalid');
|
|
63
|
+
}
|
|
64
|
+
if (sourceObservationSha256 !== null && receipt.sourceObservationSha256 !== sourceObservationSha256) {
|
|
65
|
+
throw new Error('gist aggregate receipt observation differs from coverage');
|
|
66
|
+
}
|
|
67
|
+
const ids = Object.keys(receipt.gists);
|
|
68
|
+
if (!ids.length || ids.some((id) => !HEX_GIST.test(id))
|
|
69
|
+
|| canonicalJson(ids) !== canonicalJson([...ids].sort())
|
|
70
|
+
|| (expectedIds && canonicalJson(ids) !== canonicalJson([...expectedIds].map(String).sort()))
|
|
71
|
+
|| receipt.gistSet?.count !== ids.length || receipt.gistSet?.idsSha256 !== digest(ids)
|
|
72
|
+
|| receipt.gistSet?.observationSha256 !== receipt.sourceObservationSha256) {
|
|
73
|
+
throw new Error('gist aggregate receipt does not bind the exact sorted gist set');
|
|
74
|
+
}
|
|
75
|
+
for (const id of ids) {
|
|
76
|
+
const row = receipt.gists[id];
|
|
77
|
+
const files = row?.files;
|
|
78
|
+
const filenames = Array.isArray(files) ? files.map((file) => file?.filename) : [];
|
|
79
|
+
if (row?.schemaVersion !== 3 || row?.kind !== 'ruvnet-brain-gist-source-receipt'
|
|
80
|
+
|| row.gistId !== id || row.complete !== true || !/^[a-f0-9]{40}$/.test(String(row.versionSha || ''))
|
|
81
|
+
|| !Array.isArray(files) || !files.length || row.fileCount !== files.length
|
|
82
|
+
|| canonicalJson(filenames) !== canonicalJson([...filenames].sort())
|
|
83
|
+
|| new Set(filenames).size !== filenames.length
|
|
84
|
+
|| files.some((file) => typeof file?.filename !== 'string' || !file.filename
|
|
85
|
+
|| (file.included === true
|
|
86
|
+
? !HEX64.test(String(file.sha256 || '')) || !Number.isSafeInteger(file.bytes) || file.bytes < 0
|
|
87
|
+
: file.included !== false || typeof file.reason !== 'string' || !file.reason
|
|
88
|
+
|| !(file.size === null || (Number.isSafeInteger(file.size) && file.size >= 0))))
|
|
89
|
+
|| row.contentDigest !== digest(files)) {
|
|
90
|
+
throw new Error(`gist ${id} receipt is incomplete or internally inconsistent`);
|
|
91
|
+
}
|
|
92
|
+
const { receiptSha256, ...payload } = row;
|
|
93
|
+
if (!HEX64.test(String(receiptSha256 || '')) || receiptSha256 !== digest(payload)) {
|
|
94
|
+
throw new Error(`gist ${id} receipt digest differs`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const sourceSet = ids.map((id) => ({ id, receiptSha256: receipt.gists[id].receiptSha256 }));
|
|
98
|
+
if (receipt.sourceSetSha256 !== digest(sourceSet)) throw new Error('gist source set digest differs');
|
|
99
|
+
let passagesStat = null;
|
|
100
|
+
try { passagesStat = fs.lstatSync(passagesFile); } catch { /* handled below */ }
|
|
101
|
+
if (!passagesFile || !passagesStat?.isFile() || passagesStat.isSymbolicLink()
|
|
102
|
+
|| !HEX64.test(String(receipt.passagesSha256 || ''))
|
|
103
|
+
|| receipt.passagesSha256 !== sha256File(passagesFile)) {
|
|
104
|
+
throw new Error('gist aggregate receipt does not bind its passage bytes');
|
|
105
|
+
}
|
|
106
|
+
const { receiptSha256, ...payload } = receipt;
|
|
107
|
+
if (!HEX64.test(String(receiptSha256 || '')) || receiptSha256 !== digest(payload)) {
|
|
108
|
+
throw new Error('gist aggregate receipt digest differs');
|
|
109
|
+
}
|
|
110
|
+
return receipt;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function sha256File(file) {
|
|
114
|
+
const hash = crypto.createHash('sha256');
|
|
115
|
+
const fd = fs.openSync(file, 'r');
|
|
116
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
117
|
+
try {
|
|
118
|
+
let bytes;
|
|
119
|
+
while ((bytes = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) hash.update(buffer.subarray(0, bytes));
|
|
120
|
+
} finally { fs.closeSync(fd); }
|
|
121
|
+
return hash.digest('hex');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const readJson = (file, label) => {
|
|
125
|
+
let stat;
|
|
126
|
+
try { stat = fs.lstatSync(file); } catch { throw new Error(`${label} is missing`); }
|
|
127
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${label} is not a trusted regular file`);
|
|
128
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
|
|
129
|
+
catch (error) { throw new Error(`${label} is unreadable: ${error.message}`); }
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function containedRegular(root, relative, label) {
|
|
133
|
+
if (typeof relative !== 'string' || !relative || path.isAbsolute(relative)
|
|
134
|
+
|| relative.split(/[\\/]/).includes('..')) throw new Error(`${label} path escapes the public inventory root`);
|
|
135
|
+
const resolved = path.resolve(root, relative);
|
|
136
|
+
const within = path.relative(root, resolved);
|
|
137
|
+
if (!within || within.startsWith('..') || path.isAbsolute(within)) {
|
|
138
|
+
throw new Error(`${label} path escapes the public inventory root`);
|
|
139
|
+
}
|
|
140
|
+
let stat;
|
|
141
|
+
try { stat = fs.lstatSync(resolved); } catch { throw new Error(`${label} is missing`); }
|
|
142
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${label} is not a trusted regular file`);
|
|
143
|
+
const realRoot = fs.realpathSync(root);
|
|
144
|
+
const realFile = fs.realpathSync(resolved);
|
|
145
|
+
const realWithin = path.relative(realRoot, realFile);
|
|
146
|
+
if (!realWithin || realWithin.startsWith('..') || path.isAbsolute(realWithin)) {
|
|
147
|
+
throw new Error(`${label} resolves outside the public inventory root`);
|
|
148
|
+
}
|
|
149
|
+
return resolved;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function evidenceIdentity(root, file, kind) {
|
|
153
|
+
return {
|
|
154
|
+
kind,
|
|
155
|
+
path: path.relative(root, file).split(path.sep).join('/'),
|
|
156
|
+
sha256: sha256File(file),
|
|
157
|
+
bytes: fs.statSync(file).size,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const canonicalStores = (root) => {
|
|
162
|
+
const stores = [];
|
|
163
|
+
for (const name of fs.readdirSync(root).filter((entry) => !entry.startsWith('._'))) {
|
|
164
|
+
const store = name.match(/^(.+)\.big\.rvf$/)?.[1];
|
|
165
|
+
if (!store) continue;
|
|
166
|
+
const stat = fs.lstatSync(path.join(root, name));
|
|
167
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${store} canonical RVF is not a trusted regular file`);
|
|
168
|
+
stores.push(store);
|
|
169
|
+
}
|
|
170
|
+
const folded = stores.map((store) => store.toLowerCase());
|
|
171
|
+
if (new Set(folded).size !== folded.length) throw new Error('canonical RVF store names have case-fold aliases');
|
|
172
|
+
return stores.sort();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export function projectPublicGenerationLedger({ ledger, publicStores, version, sourceSnapshot }) {
|
|
176
|
+
if (!ledger?.stores || typeof ledger.stores !== 'object' || Array.isArray(ledger.stores)) {
|
|
177
|
+
throw new Error('generation ledger stores are malformed');
|
|
178
|
+
}
|
|
179
|
+
if (typeof version !== 'string' || !version) throw new Error('public generation ledger version is missing');
|
|
180
|
+
if (!HEX40.test(String(sourceSnapshot || ''))) throw new Error('public generation ledger source snapshot is invalid');
|
|
181
|
+
const names = [...publicStores].map(String).sort();
|
|
182
|
+
if (!names.length || new Set(names).size !== names.length) {
|
|
183
|
+
throw new Error('public generation ledger store set is empty or duplicated');
|
|
184
|
+
}
|
|
185
|
+
const stores = {};
|
|
186
|
+
for (const name of names) {
|
|
187
|
+
if (!ledger.stores[name]) throw new Error(`generation ledger is missing public store ${name}`);
|
|
188
|
+
stores[name] = structuredClone(ledger.stores[name]);
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
schemaVersion: 2,
|
|
192
|
+
kind: 'ruvnet-brain-public-generation-ledger',
|
|
193
|
+
brainVersion: version,
|
|
194
|
+
releaseTag: `v${version}`,
|
|
195
|
+
sourceSnapshot,
|
|
196
|
+
stores,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function generationLedgerBytes(ledger) {
|
|
201
|
+
return Buffer.from(`${JSON.stringify(ledger, null, 2)}\n`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function validatePublicInventory({ assetsDir, coverage, ledger, installedPublicStores = null }) {
|
|
205
|
+
const root = path.resolve(assetsDir);
|
|
206
|
+
const selected = installedPublicStores === null ? null : [...installedPublicStores].map((store) => String(store).toLowerCase());
|
|
207
|
+
if (selected && (selected.some((store) => !store) || new Set(selected).size !== selected.length)) {
|
|
208
|
+
throw new Error('installed public store selection is missing or duplicated');
|
|
209
|
+
}
|
|
210
|
+
const selectedSet = selected === null ? null : new Set(selected);
|
|
211
|
+
if (!Array.isArray(coverage?.rows) || !ledger?.stores || typeof ledger.stores !== 'object' || Array.isArray(ledger.stores)) {
|
|
212
|
+
throw new Error('coverage rows or generation ledger stores are malformed');
|
|
213
|
+
}
|
|
214
|
+
const fence = readJson(path.join(root, 'PRIVATE-STORES.json'), 'private fence');
|
|
215
|
+
if (!Array.isArray(fence.privateStores)) throw new Error('private fence has no privateStores array');
|
|
216
|
+
const privateStores = fence.privateStores.map((store) => String(store).toLowerCase());
|
|
217
|
+
if (privateStores.some((store) => !store) || new Set(privateStores).size !== privateStores.length) {
|
|
218
|
+
throw new Error('private fence stores are missing or duplicated');
|
|
219
|
+
}
|
|
220
|
+
const privateSet = new Set(privateStores);
|
|
221
|
+
const evidenceFiles = [];
|
|
222
|
+
const uniqueStores = (rows, family) => {
|
|
223
|
+
const stores = rows.map((row) => String(row?.artifact?.store || '').toLowerCase());
|
|
224
|
+
if (stores.some((store) => !store) || new Set(stores).size !== stores.length) throw new Error(`${family} stores are missing or duplicated`);
|
|
225
|
+
return stores.sort();
|
|
226
|
+
};
|
|
227
|
+
const repos = coverage.rows.filter((row) => row.kind === 'repository' && row.disposition === 'eligible');
|
|
228
|
+
if (repos.some((row) => row.status !== 'CURRENT')) throw new Error('an eligible repository is not CURRENT');
|
|
229
|
+
const repositories = uniqueStores(repos, 'repository');
|
|
230
|
+
const excludedRepositories = uniqueStores(coverage.rows.filter((row) => row.kind === 'repository'
|
|
231
|
+
&& row.disposition !== 'eligible'), 'excluded repository');
|
|
232
|
+
const excludedSet = new Set(excludedRepositories);
|
|
233
|
+
if (repositories.some((store) => excludedSet.has(store))) throw new Error('eligible and excluded repository stores overlap');
|
|
234
|
+
const gists = coverage.rows.filter((row) => row.kind === 'gist' && row.disposition === 'eligible');
|
|
235
|
+
if (gists.some((row) => row.status !== 'CURRENT')) throw new Error('an eligible gist is not CURRENT');
|
|
236
|
+
let gistAggregate = null;
|
|
237
|
+
if (gists.length) {
|
|
238
|
+
const gistStores = new Set(gists.map((row) => String(row?.artifact?.store || '').toLowerCase()));
|
|
239
|
+
if (gistStores.size !== 1 || !gistStores.has('ruv-gists')) throw new Error('eligible gists must use the ruv-gists aggregate');
|
|
240
|
+
if (!selectedSet || selectedSet.has('ruv-gists')) {
|
|
241
|
+
const receiptFile = path.join(root, 'ruv-gists.sources.json');
|
|
242
|
+
const receipt = readJson(receiptFile, 'gist aggregate receipt');
|
|
243
|
+
const passages = path.join(root, 'ruv-gists.passages.jsonl');
|
|
244
|
+
const ids = gists.map((row) => String(row.key || '').replace(/^gist:/, ''));
|
|
245
|
+
if (ids.some((id) => !id)) throw new Error('gist coverage row identity is missing');
|
|
246
|
+
validateGistAggregateReceipt({ receipt, passagesFile: passages, expectedIds: ids,
|
|
247
|
+
sourceObservationSha256: coverage.sourceObservationSha256 });
|
|
248
|
+
if (receipt.schemaVersion === 3) evidenceFiles.push(evidenceIdentity(root, passages, 'gist-passages'));
|
|
249
|
+
evidenceFiles.push(evidenceIdentity(root, receiptFile, 'gist-receipt'));
|
|
250
|
+
}
|
|
251
|
+
gistAggregate = 'ruv-gists';
|
|
252
|
+
}
|
|
253
|
+
const classesFile = path.join(root, 'public-store-classes.json');
|
|
254
|
+
const classes = readJson(classesFile, 'public store classes');
|
|
255
|
+
evidenceFiles.push(evidenceIdentity(root, classesFile, 'class-registry'));
|
|
256
|
+
if (classes.schemaVersion !== 1 || !Array.isArray(classes.derived)) throw new Error('derived store classes are malformed');
|
|
257
|
+
const derived = classes.derived.map((entry) => String(entry?.store || '').toLowerCase()).sort();
|
|
258
|
+
if (derived.some((store) => !store) || new Set(derived).size !== derived.length) throw new Error('derived stores are missing or duplicated');
|
|
259
|
+
for (const entry of classes.derived) {
|
|
260
|
+
const store = String(entry.store || '').toLowerCase();
|
|
261
|
+
if (selectedSet && !selectedSet.has(store)) continue;
|
|
262
|
+
const receiptFile = containedRegular(root, entry.receipt, `derived ${store} receipt`);
|
|
263
|
+
const receipt = readJson(receiptFile, `derived ${store} receipt`);
|
|
264
|
+
const passages = containedRegular(root, `${store}.passages.jsonl`, `derived ${store} passages`);
|
|
265
|
+
if (receipt.schemaVersion !== 1 || receipt.kind !== 'ruvnet-brain-derived-store-receipt'
|
|
266
|
+
|| String(receipt.store || '').toLowerCase() !== store || !Array.isArray(receipt.inputs) || receipt.inputs.length === 0
|
|
267
|
+
|| !HEX64.test(String(receipt.passagesSha256 || '')) || receipt.passagesSha256 !== sha256File(passages)) {
|
|
268
|
+
throw new Error(`derived ${store} receipt does not bind its passage bytes`);
|
|
269
|
+
}
|
|
270
|
+
for (const input of receipt.inputs) {
|
|
271
|
+
let inputFile = null;
|
|
272
|
+
try { inputFile = containedRegular(root, input?.path, `derived ${store} input`); } catch { /* handled below */ }
|
|
273
|
+
if (!inputFile || !HEX64.test(String(input.sha256 || '')) || input.sha256 !== sha256File(inputFile)) {
|
|
274
|
+
throw new Error(`derived ${store} input receipt differs from ${input?.path || '(missing)'}`);
|
|
275
|
+
}
|
|
276
|
+
evidenceFiles.push(evidenceIdentity(root, inputFile, 'derived-input'));
|
|
277
|
+
}
|
|
278
|
+
evidenceFiles.push(evidenceIdentity(root, passages, 'derived-passages'));
|
|
279
|
+
evidenceFiles.push(evidenceIdentity(root, receiptFile, 'derived-receipt'));
|
|
280
|
+
}
|
|
281
|
+
const expected = [...repositories, ...(gistAggregate ? [gistAggregate] : []), ...derived].sort();
|
|
282
|
+
if (new Set(expected).size !== expected.length) throw new Error('public store classes overlap');
|
|
283
|
+
const excludedCollision = expected.filter((store) => excludedSet.has(store));
|
|
284
|
+
if (excludedCollision.length) throw new Error(`excluded/public store collision: ${excludedCollision.join(', ')}`);
|
|
285
|
+
const privateCollision = expected.filter((store) => privateSet.has(store));
|
|
286
|
+
if (privateCollision.length) throw new Error(`private/public store collision: ${privateCollision.join(', ')}`);
|
|
287
|
+
const installedExpected = selected === null ? expected : [...selected].sort();
|
|
288
|
+
if (installedExpected.some((store) => !expected.includes(store))) throw new Error('installed profile selects an unknown public store');
|
|
289
|
+
const actual = canonicalStores(root).filter((store) => !privateSet.has(store.toLowerCase())
|
|
290
|
+
&& !excludedSet.has(store.toLowerCase())).sort();
|
|
291
|
+
const missing = installedExpected.filter((store) => !actual.includes(store));
|
|
292
|
+
const extras = actual.filter((store) => !installedExpected.includes(store));
|
|
293
|
+
if (missing.length) throw new Error(`${missing.join(', ')} public store is missing`);
|
|
294
|
+
if (extras.length) throw new Error(`unclassified public stores: ${extras.join(', ')}`);
|
|
295
|
+
const ledgerStores = Object.keys(ledger.stores);
|
|
296
|
+
const foldedLedgerStores = ledgerStores.map((store) => store.toLowerCase());
|
|
297
|
+
if (new Set(foldedLedgerStores).size !== foldedLedgerStores.length) {
|
|
298
|
+
throw new Error('generation ledger store names have case-fold aliases');
|
|
299
|
+
}
|
|
300
|
+
const publicLedgerStores = ledgerStores.filter((store) => !privateSet.has(store.toLowerCase())
|
|
301
|
+
&& !excludedSet.has(store.toLowerCase())).sort();
|
|
302
|
+
if (canonicalJson(publicLedgerStores) !== canonicalJson(expected)) throw new Error('public generation ledger store set differs from the inventory partition');
|
|
303
|
+
for (const store of expected) {
|
|
304
|
+
const generation = ledger.stores[store];
|
|
305
|
+
const filename = `${store}.big.rvf`;
|
|
306
|
+
const file = path.join(root, filename);
|
|
307
|
+
const selected = installedExpected.includes(store);
|
|
308
|
+
if (generation?.file !== filename || (selected && (generation.bytes !== fs.statSync(file).size || generation.sha256 !== sha256File(file)))) {
|
|
309
|
+
throw new Error(`${store} generation record does not bind the RVF bytes`);
|
|
310
|
+
}
|
|
311
|
+
if (typeof generation.model !== 'string' || !generation.model.trim()
|
|
312
|
+
|| !Number.isInteger(generation.dimensions) || generation.dimensions <= 0
|
|
313
|
+
|| (generation.sourceCommit !== null && !/^[a-f0-9]{7,64}$/i.test(String(generation.sourceCommit || '')))
|
|
314
|
+
|| typeof generation.builtUtc !== 'string' || !Number.isFinite(Date.parse(generation.builtUtc))) {
|
|
315
|
+
throw new Error(`${store} generation record provenance is incomplete`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const publicStores = expected;
|
|
319
|
+
evidenceFiles.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
|
|
320
|
+
return { repositories, excludedRepositories, gistAggregate, derived, publicStores,
|
|
321
|
+
installedPublicStores: installedExpected,
|
|
322
|
+
evidenceFiles,
|
|
323
|
+
partitionSha256: digest({ repositories, excludedRepositories, gistAggregate, derived, publicStores, evidenceFiles }) };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function coverageGenerationFor({ generatorSourceSha, snapshotRoot, sourceObservationSha256 = null, rows, enumerationReceipt,
|
|
327
|
+
policyDispositionDigests = [], exemptionDigests = [] }) {
|
|
328
|
+
const orderedRows = [...rows].sort((a, b) => a.key.localeCompare(b.key));
|
|
329
|
+
return digest({ schemaVersion: 1, generatorSourceSha, snapshotRoot, sourceObservationSha256, orderedRows,
|
|
330
|
+
enumerationReceiptDigest: digest(enumerationReceipt), policyDispositionDigests, exemptionDigests });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function releaseCoverageGenerationFor(coverage) {
|
|
334
|
+
const orderedRows = [...(coverage.rows || [])].sort((a, b) => a.key.localeCompare(b.key));
|
|
335
|
+
return digest({
|
|
336
|
+
schemaVersion: 1,
|
|
337
|
+
releaseIdentity: coverage.releaseIdentity,
|
|
338
|
+
corpusSeed: coverage.corpusSeed,
|
|
339
|
+
corpusCoverage: coverage.corpusCoverage,
|
|
340
|
+
generationLedger: coverage.generationLedger,
|
|
341
|
+
publicInventoryPartitionSha256: coverage.publicInventoryPartitionSha256,
|
|
342
|
+
installedProjectionSchema: coverage.installedProjectionSchema,
|
|
343
|
+
generatorSourceSha: coverage.generatorSourceSha,
|
|
344
|
+
snapshotRoot: coverage.snapshotRoot,
|
|
345
|
+
sourceObservationSha256: coverage.sourceObservationSha256,
|
|
346
|
+
orderedRows,
|
|
347
|
+
totals: coverage.totals,
|
|
348
|
+
enumerationReceiptDigest: digest(coverage.enumerationReceipt),
|
|
349
|
+
policy: coverage.policy,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function validateRowsAndTotals(coverage, failures) {
|
|
354
|
+
const rows = Array.isArray(coverage?.rows) ? coverage.rows : [];
|
|
355
|
+
if (!Array.isArray(coverage?.rows)) failures.push('rows are missing');
|
|
356
|
+
const keys = rows.map((row) => row?.key);
|
|
357
|
+
if (keys.some((key) => typeof key !== 'string' || !key)) failures.push('a row key is missing');
|
|
358
|
+
if (new Set(keys).size !== keys.length) failures.push('row keys are duplicated');
|
|
359
|
+
if (rows.some((row) => !['repository', 'gist'].includes(row?.kind)
|
|
360
|
+
|| typeof row?.name !== 'string' || typeof row?.url !== 'string'
|
|
361
|
+
|| typeof row?.status !== 'string' || typeof row?.disposition !== 'string'
|
|
362
|
+
|| !row?.upstream || !row?.artifact || !Array.isArray(row?.reasons))) failures.push('row shape is invalid');
|
|
363
|
+
const repositoryCount = rows.filter((row) => row.kind === 'repository').length;
|
|
364
|
+
const gistCount = rows.filter((row) => row.kind === 'gist').length;
|
|
365
|
+
const byStatus = Object.fromEntries([...new Set(rows.map((row) => row.status))].sort()
|
|
366
|
+
.map((status) => [status, rows.filter((row) => row.status === status).length]));
|
|
367
|
+
if (coverage?.totals?.rows !== rows.length || coverage?.totals?.repositories !== repositoryCount
|
|
368
|
+
|| coverage?.totals?.gists !== gistCount || canonicalJson(coverage?.totals?.byStatus || {}) !== canonicalJson(byStatus)) {
|
|
369
|
+
failures.push('totals do not match rows');
|
|
370
|
+
}
|
|
371
|
+
return { rows, repositoryCount, gistCount };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function validateCoverageLedger(coverage) {
|
|
375
|
+
const failures = [];
|
|
376
|
+
if (coverage?.schemaVersion !== 1) failures.push('schemaVersion is unsupported');
|
|
377
|
+
const isCorpus = coverage?.kind === 'ruvnet-brain-corpus-coverage';
|
|
378
|
+
const isRelease = coverage?.kind === 'ruvnet-brain-release-coverage';
|
|
379
|
+
if ((isCorpus || isRelease) && !HEX64.test(String(coverage?.sourceObservationSha256 || ''))) {
|
|
380
|
+
failures.push('source observation digest is missing');
|
|
381
|
+
}
|
|
382
|
+
if (!HEX64.test(String(coverage?.generatorSourceSha || ''))) failures.push('generator source digest is missing');
|
|
383
|
+
if (!HEX64.test(String(coverage?.snapshotRoot || ''))) failures.push('snapshot root is missing');
|
|
384
|
+
const { rows, repositoryCount, gistCount } = validateRowsAndTotals(coverage, failures);
|
|
385
|
+
const enumeration = coverage?.enumerationReceipt;
|
|
386
|
+
if (enumeration?.schemaVersion !== 1 || enumeration?.terminal !== true || enumeration?.duplicateKeys !== 0
|
|
387
|
+
|| enumeration?.repositories?.expected !== repositoryCount || enumeration?.gists?.expected !== gistCount
|
|
388
|
+
|| !Array.isArray(enumeration?.repositories?.pages) || !Array.isArray(enumeration?.gists?.pages)) {
|
|
389
|
+
failures.push('enumeration receipt is incomplete');
|
|
390
|
+
}
|
|
391
|
+
const policyDispositionDigests = coverage?.policy?.policyDispositionDigests;
|
|
392
|
+
const exemptionDigests = coverage?.policy?.exemptionDigests;
|
|
393
|
+
if (!Array.isArray(policyDispositionDigests) || !Array.isArray(exemptionDigests)
|
|
394
|
+
|| [...policyDispositionDigests, ...exemptionDigests].some((value) => !HEX64.test(String(value)))) {
|
|
395
|
+
failures.push('policy digests are invalid');
|
|
396
|
+
}
|
|
397
|
+
if (isRelease) {
|
|
398
|
+
const identity = coverage.releaseIdentity;
|
|
399
|
+
if (!identity || !identity.version || identity.tag !== `v${identity.version}` || !HEX40.test(String(identity.sourceSnapshot || ''))) {
|
|
400
|
+
failures.push('release identity is invalid');
|
|
401
|
+
}
|
|
402
|
+
const seed = coverage.corpusSeed;
|
|
403
|
+
if (!seed || typeof seed.tag !== 'string' || !seed.tag || /latest/i.test(seed.tag)
|
|
404
|
+
|| !HEX64.test(String(seed.archiveSha256 || '')) || !Number.isSafeInteger(seed.archiveBytes) || seed.archiveBytes < 1
|
|
405
|
+
|| !HEX64.test(String(seed.receiptSha256 || ''))) failures.push('corpus seed identity is invalid');
|
|
406
|
+
if (!HEX64.test(String(coverage.corpusCoverage?.sha256 || ''))
|
|
407
|
+
|| !HEX64.test(String(coverage.corpusCoverage?.coverageGeneration || ''))) failures.push('corpus coverage link is invalid');
|
|
408
|
+
if (coverage.generationLedger?.file !== PUBLIC_GENERATIONS_FILE
|
|
409
|
+
|| !HEX64.test(String(coverage.generationLedger?.sha256 || ''))
|
|
410
|
+
|| !Number.isSafeInteger(coverage.generationLedger?.bytes) || coverage.generationLedger.bytes < 1
|
|
411
|
+
|| !Number.isSafeInteger(coverage.generationLedger?.storeCount) || coverage.generationLedger.storeCount < 1) {
|
|
412
|
+
failures.push('generation ledger identity is invalid');
|
|
413
|
+
}
|
|
414
|
+
if (!HEX64.test(String(coverage.publicInventoryPartitionSha256 || ''))) failures.push('public inventory partition is invalid');
|
|
415
|
+
if (coverage.installedProjectionSchema !== 2) failures.push('installed projection schema is unsupported');
|
|
416
|
+
if (!failures.length && coverage.releaseCoverageGeneration !== releaseCoverageGenerationFor(coverage)) {
|
|
417
|
+
failures.push('release coverage generation digest does not match the ledger');
|
|
418
|
+
}
|
|
419
|
+
} else if (!failures.length) {
|
|
420
|
+
const expected = coverageGenerationFor({
|
|
421
|
+
generatorSourceSha: coverage.generatorSourceSha,
|
|
422
|
+
snapshotRoot: coverage.snapshotRoot,
|
|
423
|
+
sourceObservationSha256: coverage.sourceObservationSha256 || null,
|
|
424
|
+
rows,
|
|
425
|
+
enumerationReceipt: enumeration,
|
|
426
|
+
policyDispositionDigests,
|
|
427
|
+
exemptionDigests,
|
|
428
|
+
});
|
|
429
|
+
if (coverage.coverageGeneration !== expected) failures.push('coverage generation digest does not match the ledger');
|
|
430
|
+
}
|
|
431
|
+
return { valid: failures.length === 0, failures };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function validateCoverageLink({ releaseCoverage, corpusCoverage, corpusCoverageSha256 }) {
|
|
435
|
+
const failures = [];
|
|
436
|
+
const release = validateCoverageLedger(releaseCoverage);
|
|
437
|
+
const corpus = validateCoverageLedger(corpusCoverage);
|
|
438
|
+
if (!release.valid) failures.push(...release.failures.map((failure) => `release: ${failure}`));
|
|
439
|
+
if (!corpus.valid || corpusCoverage?.kind !== 'ruvnet-brain-corpus-coverage') {
|
|
440
|
+
failures.push(...corpus.failures.map((failure) => `corpus: ${failure}`));
|
|
441
|
+
if (corpusCoverage?.kind !== 'ruvnet-brain-corpus-coverage') failures.push('corpus: wrong coverage kind');
|
|
442
|
+
}
|
|
443
|
+
if (releaseCoverage?.corpusCoverage?.sha256 !== corpusCoverageSha256) failures.push('corpus coverage byte digest differs');
|
|
444
|
+
if (releaseCoverage?.corpusCoverage?.coverageGeneration !== corpusCoverage?.coverageGeneration) {
|
|
445
|
+
failures.push('corpus coverage generation differs');
|
|
446
|
+
}
|
|
447
|
+
return { valid: failures.length === 0, failures };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Validate the exact installed/staged projection from disk. This is the single activation-boundary
|
|
452
|
+
* reader used by install, update, Console, and public proof; callers never reconstruct the linkage.
|
|
453
|
+
*/
|
|
454
|
+
export function validateCoverageDirectory(root, {
|
|
455
|
+
expectedVersion = null,
|
|
456
|
+
expectedSourceSnapshot = null,
|
|
457
|
+
requireCompleteProfile = false,
|
|
458
|
+
} = {}) {
|
|
459
|
+
const coverageFile = path.join(root, 'COVERAGE.json');
|
|
460
|
+
const corpusFile = path.join(root, 'CORPUS-COVERAGE.json');
|
|
461
|
+
const failures = [];
|
|
462
|
+
if (!fs.existsSync(coverageFile)) failures.push('COVERAGE.json is missing');
|
|
463
|
+
if (!fs.existsSync(corpusFile)) failures.push('CORPUS-COVERAGE.json is missing');
|
|
464
|
+
if (failures.length) return { valid: false, failures, coverageFile, corpusFile };
|
|
465
|
+
try {
|
|
466
|
+
const coverageBytes = fs.readFileSync(coverageFile);
|
|
467
|
+
const corpusBytes = fs.readFileSync(corpusFile);
|
|
468
|
+
const coverage = JSON.parse(coverageBytes);
|
|
469
|
+
const corpusCoverage = JSON.parse(corpusBytes);
|
|
470
|
+
if (coverage.kind !== 'ruvnet-brain-release-coverage') {
|
|
471
|
+
failures.push('COVERAGE.json is not bound to a product release identity');
|
|
472
|
+
}
|
|
473
|
+
const linked = validateCoverageLink({
|
|
474
|
+
releaseCoverage: coverage,
|
|
475
|
+
corpusCoverage,
|
|
476
|
+
corpusCoverageSha256: crypto.createHash('sha256').update(corpusBytes).digest('hex'),
|
|
477
|
+
});
|
|
478
|
+
failures.push(...linked.failures);
|
|
479
|
+
if (expectedVersion && coverage?.releaseIdentity?.version !== expectedVersion) {
|
|
480
|
+
failures.push(`release version ${coverage?.releaseIdentity?.version || '(missing)'} differs from ${expectedVersion}`);
|
|
481
|
+
}
|
|
482
|
+
if (expectedSourceSnapshot && coverage?.releaseIdentity?.sourceSnapshot !== expectedSourceSnapshot) {
|
|
483
|
+
failures.push(`release source snapshot ${coverage?.releaseIdentity?.sourceSnapshot || '(missing)'} differs from ${expectedSourceSnapshot}`);
|
|
484
|
+
}
|
|
485
|
+
let generationLedger = null;
|
|
486
|
+
let runtimeGenerationLedger = null;
|
|
487
|
+
let publicInventory = null;
|
|
488
|
+
let installedProfile = null;
|
|
489
|
+
let coverageState = 'COMPLETE';
|
|
490
|
+
try {
|
|
491
|
+
const ledgerFile = path.join(root, PUBLIC_GENERATIONS_FILE);
|
|
492
|
+
const ledgerBytes = fs.readFileSync(ledgerFile);
|
|
493
|
+
generationLedger = JSON.parse(ledgerBytes);
|
|
494
|
+
if (crypto.createHash('sha256').update(ledgerBytes).digest('hex') !== coverage?.generationLedger?.sha256
|
|
495
|
+
|| ledgerBytes.length !== coverage?.generationLedger?.bytes) failures.push('generation ledger bytes differ from ReleaseCoverage');
|
|
496
|
+
if (generationLedger.brainVersion !== coverage?.releaseIdentity?.version
|
|
497
|
+
|| generationLedger.releaseTag !== coverage?.releaseIdentity?.tag
|
|
498
|
+
|| generationLedger.sourceSnapshot !== coverage?.releaseIdentity?.sourceSnapshot
|
|
499
|
+
|| generationLedger.schemaVersion !== 2
|
|
500
|
+
|| generationLedger.kind !== 'ruvnet-brain-public-generation-ledger') {
|
|
501
|
+
failures.push('generation ledger release identity differs');
|
|
502
|
+
}
|
|
503
|
+
const installedProfileFile = path.join(root, INSTALLED_PROFILE_FILE);
|
|
504
|
+
if (fs.existsSync(installedProfileFile)) {
|
|
505
|
+
installedProfile = readJson(installedProfileFile, 'installed profile receipt');
|
|
506
|
+
const runtimeBytes = fs.readFileSync(path.join(root, RUNTIME_GENERATIONS_FILE));
|
|
507
|
+
const selected = [...(installedProfile.selectedPublicStores || [])].sort();
|
|
508
|
+
if (installedProfile.schemaVersion !== 1 || installedProfile.kind !== 'ruvnet-brain-installed-profile'
|
|
509
|
+
|| installedProfile.profile !== 'ruvector' || installedProfile.state !== 'PROFILED'
|
|
510
|
+
|| canonicalJson(selected) !== canonicalJson(['ruvector'])
|
|
511
|
+
|| installedProfile.immutablePublicLedger?.file !== PUBLIC_GENERATIONS_FILE
|
|
512
|
+
|| installedProfile.immutablePublicLedger?.sha256 !== crypto.createHash('sha256').update(ledgerBytes).digest('hex')
|
|
513
|
+
|| installedProfile.immutablePublicLedger?.bytes !== ledgerBytes.length
|
|
514
|
+
|| installedProfile.runtimeLedger?.file !== RUNTIME_GENERATIONS_FILE
|
|
515
|
+
|| installedProfile.runtimeLedger?.sha256 !== crypto.createHash('sha256').update(runtimeBytes).digest('hex')
|
|
516
|
+
|| installedProfile.runtimeLedger?.bytes !== runtimeBytes.length) {
|
|
517
|
+
failures.push('installed profile receipt is invalid or does not bind the installed ledgers');
|
|
518
|
+
} else {
|
|
519
|
+
coverageState = 'PROFILED';
|
|
520
|
+
if (requireCompleteProfile) failures.push('complete public corpus is required but the installed profile is ruvector');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const selectedPublicStores = installedProfile?.selectedPublicStores || null;
|
|
524
|
+
publicInventory = validatePublicInventory({ assetsDir: root, coverage, ledger: generationLedger, installedPublicStores: selectedPublicStores });
|
|
525
|
+
if (publicInventory.publicStores.length !== coverage?.generationLedger?.storeCount) failures.push('generation ledger public store count differs');
|
|
526
|
+
if (publicInventory.partitionSha256 !== coverage?.publicInventoryPartitionSha256) failures.push('installed public inventory partition digest differs');
|
|
527
|
+
|
|
528
|
+
const immutableNames = Object.keys(generationLedger.stores || {}).sort();
|
|
529
|
+
if (canonicalJson(immutableNames) !== canonicalJson(publicInventory.publicStores)) {
|
|
530
|
+
failures.push('immutable public generation ledger contains non-public stores');
|
|
531
|
+
}
|
|
532
|
+
runtimeGenerationLedger = readJson(path.join(root, RUNTIME_GENERATIONS_FILE), 'runtime generation ledger');
|
|
533
|
+
if (runtimeGenerationLedger.brainVersion !== coverage?.releaseIdentity?.version
|
|
534
|
+
|| runtimeGenerationLedger.releaseTag !== coverage?.releaseIdentity?.tag
|
|
535
|
+
|| runtimeGenerationLedger.sourceSnapshot !== coverage?.releaseIdentity?.sourceSnapshot
|
|
536
|
+
|| runtimeGenerationLedger.schemaVersion !== 2
|
|
537
|
+
|| runtimeGenerationLedger.kind !== 'ruvnet-brain-runtime-generation-ledger') {
|
|
538
|
+
failures.push('runtime generation ledger release identity differs');
|
|
539
|
+
}
|
|
540
|
+
const fence = readJson(path.join(root, 'PRIVATE-STORES.json'), 'private fence');
|
|
541
|
+
const privateSet = new Set((fence.privateStores || []).map((name) => String(name).toLowerCase()));
|
|
542
|
+
const runtimeNames = Object.keys(runtimeGenerationLedger.stores || {});
|
|
543
|
+
const runtimePublicNames = runtimeNames.filter((name) => !privateSet.has(name.toLowerCase())).sort();
|
|
544
|
+
const undeclaredExtras = runtimeNames.filter((name) => !immutableNames.includes(name) && !privateSet.has(name.toLowerCase()));
|
|
545
|
+
if (undeclaredExtras.length) failures.push(`runtime generation ledger has unclassified stores: ${undeclaredExtras.sort().join(', ')}`);
|
|
546
|
+
const expectedRuntimePublicNames = (selectedPublicStores || immutableNames).slice().sort();
|
|
547
|
+
if (canonicalJson(runtimePublicNames) !== canonicalJson(expectedRuntimePublicNames)) {
|
|
548
|
+
failures.push('runtime public generation store set differs from immutable public ledger');
|
|
549
|
+
} else {
|
|
550
|
+
for (const name of expectedRuntimePublicNames) {
|
|
551
|
+
if (canonicalJson(runtimeGenerationLedger.stores[name]) !== canonicalJson(generationLedger.stores[name])) {
|
|
552
|
+
failures.push(`runtime public generation record differs for ${name}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
} catch (error) { failures.push(`installed inventory: ${error.message}`); }
|
|
557
|
+
return {
|
|
558
|
+
valid: failures.length === 0,
|
|
559
|
+
failures,
|
|
560
|
+
coverage,
|
|
561
|
+
corpusCoverage,
|
|
562
|
+
generationLedger,
|
|
563
|
+
runtimeGenerationLedger,
|
|
564
|
+
publicInventory,
|
|
565
|
+
installedProfile,
|
|
566
|
+
coverageState,
|
|
567
|
+
coverageFile,
|
|
568
|
+
corpusFile,
|
|
569
|
+
coverageSha256: crypto.createHash('sha256').update(coverageBytes).digest('hex'),
|
|
570
|
+
corpusCoverageSha256: crypto.createHash('sha256').update(corpusBytes).digest('hex'),
|
|
571
|
+
};
|
|
572
|
+
} catch (error) {
|
|
573
|
+
return { valid: false, failures: [`coverage projection is unreadable: ${error.message}`], coverageFile, corpusFile };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -440,7 +440,7 @@ EOF
|
|
|
440
440
|
fi
|
|
441
441
|
|
|
442
442
|
# ── Gate 3 output — a ≤12-line pointer; the FULL playbook is injected once by session-start.sh ───
|
|
443
|
-
if
|
|
443
|
+
if false; then
|
|
444
444
|
cat <<'EOF'
|
|
445
445
|
[RuvNet Brain — build turn: APPLY THE PLAYBOOK injected at session start]
|
|
446
446
|
This is a build / change request — run THE PLAYBOOK (the standing build playbook from session start), beats A–D. DO FIRST, silently:
|
|
@@ -506,7 +506,7 @@ fi
|
|
|
506
506
|
# the user cannot tell grounding from a confident guess, which is the exact failure this whole
|
|
507
507
|
# project exists to kill. So the footer either names the source that was actually read, or openly
|
|
508
508
|
# says none was read. A fabricated path would be strictly worse than silence.
|
|
509
|
-
if
|
|
509
|
+
if false; then
|
|
510
510
|
cat <<EOF
|
|
511
511
|
[RuvNet Brain — engaged on this prompt]
|
|
512
512
|
End your response with exactly ONE dim line, nothing after it. Pick the form that is TRUE:
|
|
@@ -46,8 +46,25 @@ const INSTALLER = path.join(ROOT, 'bin', 'install.mjs');
|
|
|
46
46
|
*/
|
|
47
47
|
export const NIGHTLY_LABEL = 'com.ruvnet.brain-update';
|
|
48
48
|
|
|
49
|
+
function schedulerEnvironment(env) {
|
|
50
|
+
const fixtureRoot = env.RUVNET_CONSOLE_ROOT;
|
|
51
|
+
if (!fixtureRoot) return env;
|
|
52
|
+
if (env.RUVNET_BRAIN_TEST !== '1') {
|
|
53
|
+
throw new Error('RUVNET_CONSOLE_ROOT scheduler isolation requires RUVNET_BRAIN_TEST=1');
|
|
54
|
+
}
|
|
55
|
+
if (!path.isAbsolute(fixtureRoot)) {
|
|
56
|
+
throw new Error('RUVNET_CONSOLE_ROOT must be an absolute path');
|
|
57
|
+
}
|
|
58
|
+
const isolatedHome = path.resolve(fixtureRoot);
|
|
59
|
+
if (isolatedHome === path.resolve(os.homedir())) {
|
|
60
|
+
throw new Error('RUVNET_CONSOLE_ROOT must not be the real user home in test mode');
|
|
61
|
+
}
|
|
62
|
+
return { ...env, HOME: isolatedHome, USERPROFILE: isolatedHome };
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
export function nightlyArtifact({ env = process.env, platform = process.platform } = {}) {
|
|
50
|
-
const
|
|
66
|
+
const schedulerEnv = schedulerEnvironment(env);
|
|
67
|
+
const home = schedulerEnv.HOME || os.homedir();
|
|
51
68
|
return {
|
|
52
69
|
supported: platform === 'darwin',
|
|
53
70
|
platform,
|
|
@@ -71,7 +88,7 @@ export function nightlyStatus(options = {}) {
|
|
|
71
88
|
|
|
72
89
|
export function applyNightlyChoice(enabled, options = {}) {
|
|
73
90
|
if (typeof enabled !== 'boolean') return { ok: false, log: 'nightly must be true or false' };
|
|
74
|
-
const env = options.env || process.env;
|
|
91
|
+
const env = schedulerEnvironment(options.env || process.env);
|
|
75
92
|
const before = nightlyStatus({ ...options, env });
|
|
76
93
|
if (!before.artifact.supported) return { ok: false, state: before, log: before.evidence };
|
|
77
94
|
const run = spawnSync(process.execPath, [
|