ruvnet-brain 4.3.26 → 4.3.27
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 +2 -2
- package/bin/install.mjs +19 -4
- package/data/model-catalog.json +1 -1
- package/docs/RELEASE-NOTES-4.0.md +4 -3
- package/kb/capability-only.mjs +27 -0
- package/kb/capability-summaries/cognitum-ruos/CAPABILITIES.md +26 -0
- package/kb/verify-citation.mjs +16 -4
- package/package.json +5 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/docs/RELEASE-NOTES-4.0.md +4 -3
- package/plugin/hooks/codex-hooks.json +6 -1
- package/plugin/hooks/hook-contracts.json +24 -2
- package/plugin/hooks/hooks.json +6 -1
- package/plugin/scripts/capacity-aware-parallel-work.mjs +200 -0
- package/plugin/scripts/codex-hook-adapter.mjs +37 -0
- package/plugin/scripts/continuity-hook-policy.mjs +4 -0
- package/plugin/scripts/coverage-integrity.mjs +17 -0
- package/plugin/scripts/hook-shim.mjs +1 -0
- package/plugin/scripts/lesson-gate.mjs +4 -1
- package/plugin/scripts/project-progression-reader.mjs +12 -1
- package/plugin/scripts/project-progression-session-start.mjs +24 -6
- package/plugin/scripts/project-progression-store.mjs +76 -4
- package/plugin/skills/release-proof/SKILL.md +28 -4
- package/plugin/skills/release-proof/scripts/release-proof.mjs +48 -24
- package/scripts/brain-novice-50.mjs +14 -16
- package/scripts/build-bundle.mjs +2 -0
- package/scripts/corpus-dispatch-receipt.mjs +22 -0
- package/scripts/corpus-reconcile.mjs +26 -4
- package/scripts/doc-currency.mjs +12 -4
- package/scripts/eval-brain.mjs +7 -5
- package/scripts/gist-git-transport.mjs +218 -0
- package/scripts/gist-receipts.mjs +168 -38
- package/scripts/ingest-gists.mjs +5 -2
- package/scripts/installed-brain-health.mjs +99 -0
- package/scripts/public-inputs.mjs +2 -1
- package/scripts/public-verification-inputs.mjs +14 -6
- package/scripts/refresh-capability-only-store.mjs +143 -0
- package/scripts/release-vector.mjs +44 -20
- package/scripts/run-operational-benchmark.mjs +151 -0
- package/scripts/run-operational-benchmark.v3.mjs +194 -0
- package/scripts/self-update.mjs +2 -0
- package/scripts/source-coverage.mjs +6 -1
- package/scripts/sync-version.mjs +10 -2
- package/scripts/wired-check.mjs +73 -45
|
@@ -33,6 +33,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
33
33
|
import { canonicalJson, digest, validateGistAggregateReceipt } from './coverage-integrity.mjs';
|
|
34
34
|
import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
|
|
35
35
|
import { writeRvfGeneration } from './rvf-generation.mjs';
|
|
36
|
+
import { fetchGistGitSnapshot } from './gist-git-transport.mjs';
|
|
36
37
|
|
|
37
38
|
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
39
|
const TEXT_EXT = new Set(['.md', '.markdown', '.txt', '.rst']);
|
|
@@ -45,6 +46,7 @@ const HEX_GIST = /^[a-f0-9]{20,64}$/;
|
|
|
45
46
|
const OWNER_RE = /^[A-Za-z0-9-]{1,39}$/;
|
|
46
47
|
const compareCanonicalText = (left, right) => String(left) < String(right) ? -1
|
|
47
48
|
: String(left) > String(right) ? 1 : 0;
|
|
49
|
+
const VERIFIED_CAPTURE_SETS = new WeakMap();
|
|
48
50
|
|
|
49
51
|
function validDate(value) {
|
|
50
52
|
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
@@ -122,38 +124,67 @@ const RATE_LIMIT_RE = /rate limit/i;
|
|
|
122
124
|
const NOT_FOUND_RE = /\bnot found\b|HTTP 404/i;
|
|
123
125
|
const FORBIDDEN_INTEGRATION_RE = /resource not accessible by integration/i;
|
|
124
126
|
const TRANSIENT_RE = /timeout|timed out|TLS handshake|ECONNRESET|ECONNREFUSED|EAI_AGAIN|temporary failure|HTTP 5\d\d|HTTP 429|socket hang up/i;
|
|
127
|
+
const MAX_RATE_LIMIT_WAIT_MS = 15 * 60 * 1000;
|
|
128
|
+
|
|
129
|
+
function retryDelay(error, attempt, retryDelayMs) {
|
|
130
|
+
if (error?.code !== 'GIST_RATE_LIMITED') return retryDelayMs * attempt;
|
|
131
|
+
const retryAfter = Number(error.headers?.['retry-after']);
|
|
132
|
+
const resetAt = Number(error.headers?.['x-ratelimit-reset']);
|
|
133
|
+
const requested = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000
|
|
134
|
+
: Number.isFinite(resetAt) && resetAt > 0 ? Math.max(0, resetAt * 1000 - Date.now()) : retryDelayMs * attempt;
|
|
135
|
+
return requested > MAX_RATE_LIMIT_WAIT_MS ? null : Math.max(retryDelayMs * attempt, requested);
|
|
136
|
+
}
|
|
125
137
|
|
|
126
138
|
export class GistFetchError extends Error {
|
|
127
|
-
constructor(message, { code, gistId, status, retryable = false } = {}) {
|
|
139
|
+
constructor(message, { code, gistId, status, headers = {}, retryable = false } = {}) {
|
|
128
140
|
super(message);
|
|
129
141
|
this.name = 'GistFetchError';
|
|
130
142
|
this.code = code;
|
|
131
143
|
this.gistId = gistId;
|
|
132
144
|
if (status !== undefined) this.status = status;
|
|
145
|
+
if (headers && Object.keys(headers).length) this.headers = headers;
|
|
133
146
|
this.retryable = retryable;
|
|
134
147
|
}
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
function classifyGistFetchFailure(stderr, gistId) {
|
|
150
|
+
function classifyGistFetchFailure(stderr, gistId, { status, headers = {} } = {}) {
|
|
138
151
|
const message = String(stderr || '').trim();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
152
|
+
const responseStatus = status ?? (Number(message.match(/\bHTTP\s+(\d{3})\b/i)?.[1]) || undefined);
|
|
153
|
+
const safeHeaders = Object.fromEntries(['x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after', 'x-ratelimit-resource', 'x-github-request-id']
|
|
154
|
+
.flatMap((key) => headers[key] == null ? [] : [[key, String(headers[key])]]));
|
|
155
|
+
const responseDetail = [responseStatus ? `HTTP ${responseStatus}` : '', Object.keys(safeHeaders).length
|
|
156
|
+
? `headers=${JSON.stringify(safeHeaders)}` : '', message].filter(Boolean).join('; ');
|
|
157
|
+
if (responseStatus === 404 || (!responseStatus && NOT_FOUND_RE.test(message))) {
|
|
158
|
+
return new GistFetchError(`gist ${gistId} was moved or deleted: ${responseDetail}`,
|
|
159
|
+
{ code: 'GIST_NOT_FOUND', gistId, status: responseStatus, headers: safeHeaders, retryable: false });
|
|
142
160
|
}
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
161
|
+
if (RATE_LIMIT_RE.test(message) || (responseStatus === 403 && headers['x-ratelimit-remaining'] === '0')
|
|
162
|
+
|| (responseStatus === 429 && Boolean(headers['retry-after']))) {
|
|
163
|
+
return new GistFetchError(`gist ${gistId} fetch rate-limited: ${responseDetail}`,
|
|
164
|
+
{ code: 'GIST_RATE_LIMITED', gistId, status: responseStatus, headers: safeHeaders, retryable: true });
|
|
146
165
|
}
|
|
147
|
-
if (
|
|
148
|
-
return new GistFetchError(`gist ${gistId} fetch
|
|
149
|
-
{ code: '
|
|
166
|
+
if (responseStatus === 403 && (FORBIDDEN_INTEGRATION_RE.test(message) || !headers['x-ratelimit-remaining'])) {
|
|
167
|
+
return new GistFetchError(`gist ${gistId} fetch rejected (HTTP 403): ${responseDetail}`,
|
|
168
|
+
{ code: 'GIST_FORBIDDEN', gistId, status: responseStatus, headers: safeHeaders, retryable: false });
|
|
150
169
|
}
|
|
151
|
-
if (TRANSIENT_RE.test(message)) {
|
|
152
|
-
return new GistFetchError(`gist ${gistId} fetch failed transiently: ${
|
|
153
|
-
{ code: 'GIST_TRANSIENT_FAILURE', gistId, retryable: true });
|
|
170
|
+
if (TRANSIENT_RE.test(message) || (responseStatus >= 500 && responseStatus < 600) || responseStatus === 429) {
|
|
171
|
+
return new GistFetchError(`gist ${gistId} fetch failed transiently: ${responseDetail}`,
|
|
172
|
+
{ code: 'GIST_TRANSIENT_FAILURE', gistId, status: responseStatus, headers: safeHeaders, retryable: true });
|
|
154
173
|
}
|
|
155
|
-
return new GistFetchError(`gist ${gistId} fetch failed: ${
|
|
156
|
-
{ code: 'GIST_FETCH_FAILED', gistId, retryable: false });
|
|
174
|
+
return new GistFetchError(`gist ${gistId} fetch failed: ${responseDetail}`,
|
|
175
|
+
{ code: 'GIST_FETCH_FAILED', gistId, status: responseStatus, headers: safeHeaders, retryable: false });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseGhResponse(output) {
|
|
179
|
+
const text = String(output || '');
|
|
180
|
+
const match = text.match(/^HTTP\/\S+\s+(\d+)\r?\n([\s\S]*?)\r?\n\r?\n([\s\S]*)$/m);
|
|
181
|
+
if (!match) return { status: undefined, headers: {}, body: text };
|
|
182
|
+
const headers = {};
|
|
183
|
+
for (const line of match[2].split(/\r?\n/)) {
|
|
184
|
+
const split = line.indexOf(':');
|
|
185
|
+
if (split > 0) headers[line.slice(0, split).trim().toLowerCase()] = line.slice(split + 1).trim();
|
|
186
|
+
}
|
|
187
|
+
return { status: Number(match[1]), headers, body: match[3] };
|
|
157
188
|
}
|
|
158
189
|
|
|
159
190
|
function defaultSleep(ms) {
|
|
@@ -172,11 +203,14 @@ export async function defaultFetchGist(id, { spawn = spawnSync, retries = 3, ret
|
|
|
172
203
|
const attempts = Math.max(1, retries);
|
|
173
204
|
let lastError;
|
|
174
205
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
175
|
-
const result = spawn('gh', ['api', `gists/${id}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
176
|
-
|
|
177
|
-
|
|
206
|
+
const result = spawn('gh', ['api', '--include', `gists/${id}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
207
|
+
const response = parseGhResponse(result.stdout);
|
|
208
|
+
if (result.status === 0) return JSON.parse(response.body);
|
|
209
|
+
lastError = classifyGistFetchFailure(`${result.stderr || ''}\n${response.body || ''}`, id, response);
|
|
178
210
|
if (!lastError.retryable || attempt === attempts) throw lastError;
|
|
179
|
-
|
|
211
|
+
const delay = retryDelay(lastError, attempt, retryDelayMs);
|
|
212
|
+
if (delay === null) throw lastError;
|
|
213
|
+
await sleep(delay);
|
|
180
214
|
if (signal?.aborted) throw abortError(signal);
|
|
181
215
|
}
|
|
182
216
|
throw lastError;
|
|
@@ -185,17 +219,34 @@ export async function defaultFetchGist(id, { spawn = spawnSync, retries = 3, ret
|
|
|
185
219
|
// The unauthenticated fallback used ONLY for the specific 403 "no gist scope" rejection. Mirrors
|
|
186
220
|
// source-coverage.mjs's listGistsUnauthenticated: same env var, same accept header, same
|
|
187
221
|
// unauthenticated-quota reasoning. `fetchImpl` is a test seam (default: global fetch).
|
|
188
|
-
async function fetchPublicGistDetail(id, { fetchImpl = globalThis.fetch,
|
|
222
|
+
async function fetchPublicGistDetail(id, { fetchImpl = globalThis.fetch, retries = 3, retryDelayMs = 300,
|
|
223
|
+
sleep = defaultSleep, signal } = {}) {
|
|
189
224
|
const apiBase = process.env.RUVNET_GISTS_API || 'https://api.github.com';
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
225
|
+
const attempts = Math.max(1, retries);
|
|
226
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
227
|
+
const response = await fetchImpl(`${apiBase}/gists/${id}`, {
|
|
228
|
+
headers: { accept: 'application/vnd.github+json', 'user-agent': 'ruvnet-brain-gist-receipts' },
|
|
229
|
+
signal,
|
|
230
|
+
});
|
|
231
|
+
if (response.ok) return response.json();
|
|
232
|
+
const headers = {};
|
|
233
|
+
for (const key of ['x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after', 'x-ratelimit-resource', 'x-github-request-id']) {
|
|
234
|
+
const value = response.headers?.get?.(key);
|
|
235
|
+
if (value != null) headers[key] = value;
|
|
236
|
+
}
|
|
237
|
+
const body = await response.json?.().catch?.(() => null);
|
|
238
|
+
const classified = classifyGistFetchFailure(`public detail fetch failed: HTTP ${response.status} ${body?.message || ''}`, id,
|
|
239
|
+
{ status: response.status, headers });
|
|
240
|
+
const error = new GistFetchError(`gist ${id} public detail fetch failed: ${classified.message}`,
|
|
241
|
+
{ code: classified.code, gistId: id, status: response.status, headers: classified.headers,
|
|
242
|
+
retryable: classified.retryable });
|
|
243
|
+
if (!error.retryable || attempt === attempts) throw error;
|
|
244
|
+
const delay = retryDelay(error, attempt, retryDelayMs);
|
|
245
|
+
if (delay === null) throw error;
|
|
246
|
+
await sleep(delay);
|
|
247
|
+
if (signal?.aborted) throw abortError(signal);
|
|
197
248
|
}
|
|
198
|
-
|
|
249
|
+
throw new Error(`gist ${id} public detail retries exhausted`);
|
|
199
250
|
}
|
|
200
251
|
|
|
201
252
|
// The transport `captureGistSources` uses by default for per-gist DETAIL. Wraps `defaultFetchGist`
|
|
@@ -229,9 +280,18 @@ export async function defaultFetchRaw(file, { fetchImpl = globalThis.fetch, retr
|
|
|
229
280
|
headers: { 'user-agent': 'ruvnet-brain-gist-receipts' }, signal,
|
|
230
281
|
});
|
|
231
282
|
if (response.ok) return Buffer.from(await response.arrayBuffer());
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
283
|
+
const headers = {};
|
|
284
|
+
for (const key of ['x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after', 'x-ratelimit-resource', 'x-github-request-id']) {
|
|
285
|
+
const value = response.headers?.get?.(key);
|
|
286
|
+
if (value != null) headers[key] = value;
|
|
287
|
+
}
|
|
288
|
+
const rateLimited = response.status === 403 && (headers['x-ratelimit-remaining'] === '0'
|
|
289
|
+
|| RATE_LIMIT_RE.test(response.statusText || '')) || response.status === 429;
|
|
290
|
+
const transient = rateLimited || (response.status >= 500 && response.status < 600);
|
|
291
|
+
lastError = new GistFetchError(`raw gist fetch failed: HTTP ${response.status}; headers=${JSON.stringify(
|
|
292
|
+
Object.fromEntries(Object.entries(headers).filter(([key]) => ['x-ratelimit-remaining', 'x-ratelimit-reset', 'retry-after', 'x-ratelimit-resource', 'x-github-request-id'].includes(key))))}`,
|
|
293
|
+
{ code: rateLimited ? 'GIST_RATE_LIMITED' : transient ? 'GIST_RAW_TRANSIENT_FAILURE' : 'GIST_RAW_FETCH_FAILED',
|
|
294
|
+
status: response.status, headers, retryable: transient });
|
|
235
295
|
if (!transient) throw lastError;
|
|
236
296
|
} catch (error) {
|
|
237
297
|
if (error instanceof GistFetchError) { lastError = error; if (!error.retryable) throw error; }
|
|
@@ -243,7 +303,9 @@ export async function defaultFetchRaw(file, { fetchImpl = globalThis.fetch, retr
|
|
|
243
303
|
}
|
|
244
304
|
}
|
|
245
305
|
if (attempt === attempts) throw lastError;
|
|
246
|
-
|
|
306
|
+
const delay = retryDelay(lastError, attempt, retryDelayMs);
|
|
307
|
+
if (delay === null) throw lastError;
|
|
308
|
+
await sleep(delay);
|
|
247
309
|
if (signal?.aborted) throw abortError(signal);
|
|
248
310
|
}
|
|
249
311
|
throw lastError;
|
|
@@ -340,18 +402,49 @@ function reusableCachedGist(cached, stub) {
|
|
|
340
402
|
return 'reuse';
|
|
341
403
|
}
|
|
342
404
|
|
|
343
|
-
export async function captureGistSources({ observation, cache = null, fetchDetail =
|
|
344
|
-
fetchRaw = defaultFetchRaw,
|
|
405
|
+
export async function captureGistSources({ observation, cache = null, fetchDetail = null,
|
|
406
|
+
fetchRaw = defaultFetchRaw, fetchGitSnapshot = fetchGistGitSnapshot, signal,
|
|
407
|
+
now = () => new Date().toISOString() } = {}) {
|
|
345
408
|
const stubs = observation?.gists?.rows;
|
|
346
409
|
if (!Array.isArray(stubs) || stubs.some(({ id }) => !HEX_GIST.test(String(id || '')))
|
|
347
410
|
|| new Set(stubs.map(({ id }) => id)).size !== stubs.length) {
|
|
348
411
|
throw new Error('source observation has missing or duplicate gist ids');
|
|
349
412
|
}
|
|
413
|
+
const observedRowsSha256 = digest({ owner: observation.owner, observedAt: observation.observedAt, rows: stubs });
|
|
350
414
|
const gists = {};
|
|
351
415
|
const reused = [];
|
|
352
416
|
const fetched = [];
|
|
353
417
|
for (const stub of [...stubs].sort((a, b) => String(a.id).localeCompare(String(b.id)))) {
|
|
354
418
|
if (signal?.aborted) throw abortError(signal);
|
|
419
|
+
if (!fetchDetail) {
|
|
420
|
+
// The production/no-key route never spends one REST detail request per gist and never trusts
|
|
421
|
+
// a body-free cache or timestamps as content identity. A full bare clone gives one immutable
|
|
422
|
+
// Git commit/tree; every observed raw_url's blob identity must match its HEAD file bytes.
|
|
423
|
+
const snapshot = await fetchGitSnapshot({ owner: observation.owner, stub, signal });
|
|
424
|
+
const files = [];
|
|
425
|
+
for (const source of snapshot.files) {
|
|
426
|
+
const filename = source.filename;
|
|
427
|
+
if (!TEXT_EXT.has(path.extname(filename).toLowerCase())) {
|
|
428
|
+
files.push({ filename, included: false, reason: 'non-text policy exclusion', size: source.size,
|
|
429
|
+
sourceGit: { ...source.sourceGit, captureMethod: 'public-bare-git-v1', revisionKind: 'git-commit',
|
|
430
|
+
observedAt: observation.observedAt, observedRowsSha256,
|
|
431
|
+
sourceObservationSha256: observation.observationSha256 } });
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
let text;
|
|
435
|
+
try { text = new TextDecoder('utf-8', { fatal: true }).decode(source.body); }
|
|
436
|
+
catch { throw new Error(`gist ${stub.id}/${filename} is not valid UTF-8 text`); }
|
|
437
|
+
files.push({ filename, included: true, sha256: sha256(source.body), bytes: source.size,
|
|
438
|
+
body: text, sourceGit: { ...source.sourceGit, captureMethod: 'public-bare-git-v1', revisionKind: 'git-commit',
|
|
439
|
+
observedAt: observation.observedAt, observedRowsSha256,
|
|
440
|
+
sourceObservationSha256: observation.observationSha256 } });
|
|
441
|
+
}
|
|
442
|
+
if (files.length !== snapshot.treeFileCount) throw new Error(`gist ${stub.id} Git inventory changed during capture`);
|
|
443
|
+
gists[stub.id] = { gistId: stub.id, versionSha: snapshot.headSha, updatedAt: stub.updated_at,
|
|
444
|
+
ingestedAt: now(), complete: true, files };
|
|
445
|
+
fetched.push(stub.id);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
355
448
|
const cached = cache?.gists?.[stub.id];
|
|
356
449
|
const verdict = cached ? reusableCachedGist(cached, stub) : 'miss';
|
|
357
450
|
if (verdict === 'tampered') {
|
|
@@ -386,14 +479,31 @@ export async function captureGistSources({ observation, cache = null, fetchDetai
|
|
|
386
479
|
complete: files.length === Object.keys(full.files || {}).length, files };
|
|
387
480
|
fetched.push(stub.id);
|
|
388
481
|
}
|
|
389
|
-
|
|
482
|
+
const captured = {
|
|
390
483
|
owner: observation.owner,
|
|
484
|
+
observedRowsSha256,
|
|
391
485
|
observedAt: observation.observedAt,
|
|
392
486
|
sourceObservationSha256: observation.observationSha256,
|
|
393
487
|
generatedAt: now(),
|
|
394
488
|
gists,
|
|
395
489
|
reuseEvidence: { reused, fetched },
|
|
396
490
|
};
|
|
491
|
+
VERIFIED_CAPTURE_SETS.set(captured, {
|
|
492
|
+
fingerprint: digest(captured),
|
|
493
|
+
owner: observation.owner,
|
|
494
|
+
observedRowsSha256,
|
|
495
|
+
sourceObservationSha256: observation.observationSha256,
|
|
496
|
+
});
|
|
497
|
+
return captured;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function isCurrentVerifiedCapture(captured, observation) {
|
|
501
|
+
const proof = captured && typeof captured === 'object' ? VERIFIED_CAPTURE_SETS.get(captured) : null;
|
|
502
|
+
return Boolean(proof && proof.owner === observation?.owner
|
|
503
|
+
&& proof.observedRowsSha256 === digest({ owner: observation?.owner,
|
|
504
|
+
observedAt: observation?.observedAt, rows: observation?.gists?.rows })
|
|
505
|
+
&& proof.sourceObservationSha256 === observation?.observationSha256
|
|
506
|
+
&& proof.fingerprint === digest(captured));
|
|
397
507
|
}
|
|
398
508
|
|
|
399
509
|
// ── stage 2: render ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -462,6 +572,25 @@ export function validateGistReceipt({ receipt, observation = null, passagesFile,
|
|
|
462
572
|
if (!validDate(row?.updatedAt) || !validDate(row?.ingestedAt)) {
|
|
463
573
|
throw new Error(`gist ${gistId} has an invalid updatedAt/ingestedAt timestamp`);
|
|
464
574
|
}
|
|
575
|
+
const sourceProofs = (row?.files || []).map((file) => file?.sourceGit).filter(Boolean);
|
|
576
|
+
if (sourceProofs.length && sourceProofs.length !== row.files.length) {
|
|
577
|
+
throw new Error(`gist ${gistId} mixes Git-bound and unbound file evidence`);
|
|
578
|
+
}
|
|
579
|
+
if (sourceProofs.length && sourceProofs.some((proof) => !HEX40.test(String(proof.headSha || ''))
|
|
580
|
+
|| !HEX40.test(String(proof.treeSha || '')) || !HEX40.test(String(proof.blobSha || ''))
|
|
581
|
+
|| proof.captureMethod !== 'public-bare-git-v1' || proof.revisionKind !== 'git-commit'
|
|
582
|
+
|| !validDate(proof.observedAt) || proof.sourceObservationSha256 !== receipt.sourceObservationSha256
|
|
583
|
+
|| !HEX64.test(String(proof.observedRowsSha256 || ''))
|
|
584
|
+
|| !Number.isSafeInteger(proof.treeFileCount) || proof.treeFileCount !== row.files.length
|
|
585
|
+
|| proof.observedTruncated !== false || proof.observedFileCount !== proof.treeFileCount
|
|
586
|
+
|| proof.headSha !== row.versionSha || proof.observed !== true
|
|
587
|
+
|| !HEX40.test(String(proof.observedRawRevisionSha || ''))
|
|
588
|
+
|| !['blob', 'commit'].includes(proof.observedRawRevisionKind)
|
|
589
|
+
|| proof.observedRawBlobSha !== proof.blobSha
|
|
590
|
+
|| sourceProofs.some((other) => other.treeSha !== proof.treeSha || other.headSha !== proof.headSha
|
|
591
|
+
|| other.observedRowsSha256 !== proof.observedRowsSha256))) {
|
|
592
|
+
throw new Error(`gist ${gistId} has an incomplete Git snapshot proof`);
|
|
593
|
+
}
|
|
465
594
|
for (const file of row?.files || []) {
|
|
466
595
|
if (!validateFilename(file?.filename)) throw new Error(`gist ${gistId} has an unsafe or missing filename`);
|
|
467
596
|
}
|
|
@@ -517,8 +646,9 @@ export async function buildGistAggregate({ observation, cache = null, outDir, ro
|
|
|
517
646
|
sourceMetadata: null, files: [], sourceReceipt: null, reuseEvidence: { reused: [], fetched: [] } };
|
|
518
647
|
}
|
|
519
648
|
|
|
520
|
-
const captured = await captureGistSources({
|
|
521
|
-
observation, cache, fetchDetail: transport.fetchDetail, fetchRaw: transport.fetchRaw,
|
|
649
|
+
const captured = isCurrentVerifiedCapture(cache, observation) ? cache : await captureGistSources({
|
|
650
|
+
observation, cache, fetchDetail: transport.fetchDetail, fetchRaw: transport.fetchRaw,
|
|
651
|
+
fetchGitSnapshot: transport.fetchGitSnapshot, signal, now,
|
|
522
652
|
});
|
|
523
653
|
const { passageBytes, metadata, gistRecords } = renderGistPassages({ captured, generatedAt: captured.generatedAt });
|
|
524
654
|
const includedFileCount = Object.values(captured.gists)
|
package/scripts/ingest-gists.mjs
CHANGED
|
@@ -194,10 +194,13 @@ async function main() {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
const observedAt = new Date().toISOString();
|
|
197
|
-
const rows = gists.map((g) => ({ id: g.id, updated_at: g.updated_at,
|
|
197
|
+
const rows = gists.map((g) => ({ id: g.id, updated_at: g.updated_at, html_url: g.html_url,
|
|
198
|
+
truncated: g.truncated, files: g.files }));
|
|
198
199
|
const observation = {
|
|
199
200
|
owner: OWNER, observedAt,
|
|
200
|
-
|
|
201
|
+
// Bind the exact listed file inventory and its truncation indicator. A timestamp-only digest
|
|
202
|
+
// could otherwise reuse a capture after the API changes a file's pinned raw blob or completeness.
|
|
203
|
+
observationSha256: digest({ owner: OWNER, rows }),
|
|
201
204
|
gists: { rows },
|
|
202
205
|
};
|
|
203
206
|
const now = () => observedAt;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ADR-089: describe installed identities without confusing a validator pin with search-engine proof.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { readInstalledRuntime, isCorpusReleaseTag } from '../kb/corpus-release-identity.mjs';
|
|
6
|
+
import { cmpVersion } from './stack-sync.mjs';
|
|
7
|
+
import { isRuntimeFile } from './approved-runtime.mjs';
|
|
8
|
+
|
|
9
|
+
const SEARCH_FILES = ['forge-mcp-all.mjs', 'forge-ask-all.mjs', 'forge-rerank.mjs', 'card-lane.mjs'];
|
|
10
|
+
// Keep the doctor probe answerable and bounded. A health probe must exercise a real indexed
|
|
11
|
+
// source/citation path; it must not turn an unclear broad question into an all-corpus rerank.
|
|
12
|
+
export const DOCTOR_SMOKE_QUERY = 'What package name is declared in the RuvNet Brain KB package manifest?';
|
|
13
|
+
export function doctorSmokeArgs(cacheDir) {
|
|
14
|
+
return ['forge-ask-all.mjs', '--dir', cacheDir, '--q', DOCTOR_SMOKE_QUERY,
|
|
15
|
+
'--repos', 'ruvnet-brain', '--k', '3', '--pool', '8', '--bounded'];
|
|
16
|
+
}
|
|
17
|
+
const version = value => typeof value === 'string' && /^v?\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(value)
|
|
18
|
+
? value.replace(/^v/, '') : null;
|
|
19
|
+
|
|
20
|
+
export function inspectInstalledBrain(kbDir, packageVersion) {
|
|
21
|
+
const result = { healthy: false, packageVersion, searchVersion: null, corpusTag: null,
|
|
22
|
+
validatorVersion: null, searchFiles: {}, issues: [] };
|
|
23
|
+
let source;
|
|
24
|
+
try { source = JSON.parse(fs.readFileSync(path.join(kbDir, 'SOURCE.json'), 'utf8')); }
|
|
25
|
+
catch { result.issues.push('SOURCE.json is missing or unreadable'); }
|
|
26
|
+
result.searchVersion = version(source?.brainVersion) || version(source?.releaseTag);
|
|
27
|
+
result.corpusTag = isCorpusReleaseTag(source?.corpusReleaseTag) ? source.corpusReleaseTag : null;
|
|
28
|
+
if (!result.searchVersion) result.issues.push('the search engine version is unverified');
|
|
29
|
+
if (version(source?.brainVersion) && version(source?.releaseTag)
|
|
30
|
+
&& version(source.brainVersion) !== version(source.releaseTag)) {
|
|
31
|
+
result.issues.push('SOURCE.json runtime version and release tag disagree');
|
|
32
|
+
}
|
|
33
|
+
const pin = readInstalledRuntime(kbDir);
|
|
34
|
+
result.validatorVersion = pin.brainVersion;
|
|
35
|
+
if (!pin.ok) result.issues.push(pin.reason);
|
|
36
|
+
if (pin.ok && result.searchVersion && version(pin.brainVersion) !== result.searchVersion) {
|
|
37
|
+
result.issues.push(`validator runtime ${pin.brainVersion} and search engine ${result.searchVersion} differ`);
|
|
38
|
+
}
|
|
39
|
+
const expected = version(packageVersion);
|
|
40
|
+
if (expected && result.searchVersion && cmpVersion(result.searchVersion, expected) < 0) {
|
|
41
|
+
result.issues.push(`search engine ${result.searchVersion} is behind package ${packageVersion}`);
|
|
42
|
+
}
|
|
43
|
+
for (const file of SEARCH_FILES) {
|
|
44
|
+
try {
|
|
45
|
+
const bytes = fs.readFileSync(path.join(kbDir, file));
|
|
46
|
+
result.searchFiles[file] = { bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') };
|
|
47
|
+
if (!bytes.length) result.issues.push(`search executable ${file} is empty`);
|
|
48
|
+
} catch { result.issues.push(`search executable ${file} is missing`); }
|
|
49
|
+
}
|
|
50
|
+
// The installed archive manifest lets doctor detect code drift even if SOURCE's version stayed
|
|
51
|
+
// unchanged. This is a consistency check, not independent authentication of a local manifest.
|
|
52
|
+
try {
|
|
53
|
+
const archive = JSON.parse(fs.readFileSync(path.join(kbDir, 'ARCHIVE-MANIFEST.json'), 'utf8'));
|
|
54
|
+
if (archive.kind !== 'ruvnet-brain-archive-manifest' || !Array.isArray(archive.files)) {
|
|
55
|
+
throw new Error('invalid archive manifest shape');
|
|
56
|
+
}
|
|
57
|
+
if (version(archive.version) !== result.searchVersion) {
|
|
58
|
+
result.issues.push('archive manifest and search engine versions differ');
|
|
59
|
+
}
|
|
60
|
+
const runtimeFiles = new Set([...SEARCH_FILES, ...archive.files.filter(row => isRuntimeFile(row?.path)).map(row => row.path)]);
|
|
61
|
+
for (const file of runtimeFiles) {
|
|
62
|
+
if (typeof file !== 'string' || path.isAbsolute(file) || file.includes('\\')
|
|
63
|
+
|| file.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
64
|
+
result.issues.push('archive manifest contains an unsafe runtime path');
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const rows = archive.files.filter(row => row?.path === file);
|
|
68
|
+
let actual = result.searchFiles[file];
|
|
69
|
+
if (!actual) {
|
|
70
|
+
try {
|
|
71
|
+
const bytes = fs.readFileSync(path.join(kbDir, file));
|
|
72
|
+
actual = { bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') };
|
|
73
|
+
result.searchFiles[file] = actual;
|
|
74
|
+
} catch { result.issues.push(`runtime file ${file} is missing`); }
|
|
75
|
+
}
|
|
76
|
+
if (rows.length !== 1 || !/^[a-f0-9]{64}$/.test(rows[0]?.sha256 || '')
|
|
77
|
+
|| !Number.isSafeInteger(rows[0]?.bytes) || rows[0].bytes < 0) {
|
|
78
|
+
result.issues.push(`archive manifest has no unique valid identity for ${file}`);
|
|
79
|
+
} else if (actual && (actual.sha256 !== rows[0].sha256 || actual.bytes !== rows[0].bytes)) {
|
|
80
|
+
result.issues.push(`search executable ${file} differs from the installed archive manifest`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
result.issues.push(`installed archive manifest is missing or unreadable: ${error.message}`);
|
|
85
|
+
}
|
|
86
|
+
// These hashes and the local manifest are NOT independent release approval receipts.
|
|
87
|
+
result.healthy = result.issues.length === 0;
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function classifySmokeEvidence(verification, answer) {
|
|
92
|
+
if (!verification?.grounded) return { usable: false, reason: verification?.reason || 'citation-not-verified' };
|
|
93
|
+
const weak = /EVIDENCE:\s*(?:INSUFFICIENT_EVIDENCE|THIN)\b/i.test(answer);
|
|
94
|
+
if (weak) return { usable: false, reason: 'retrieval-evidence-insufficient' };
|
|
95
|
+
if (/BUILT\/SHIPPED CLAIM:\s*NOT PROVEN/i.test(answer)) {
|
|
96
|
+
return { usable: false, reason: 'required-implementation-unproven' };
|
|
97
|
+
}
|
|
98
|
+
return { usable: true, reason: null };
|
|
99
|
+
}
|
|
@@ -36,6 +36,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
36
36
|
import { digest, fileIdentity } from './coverage-integrity.mjs';
|
|
37
37
|
import { isPrivate, loadPrivateFence, loadPrivateSlugs, shouldFenceL2 } from './private-fence.mjs';
|
|
38
38
|
import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
|
|
39
|
+
import { isCapabilityOnly } from '../kb/capability-only.mjs';
|
|
39
40
|
|
|
40
41
|
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
41
42
|
const HEX40 = /^[a-f0-9]{40}$/;
|
|
@@ -299,7 +300,7 @@ export function materializePublicInputs({ builderRoot = DEFAULT_ROOT, policy = {
|
|
|
299
300
|
try {
|
|
300
301
|
for (const repo of allRepos) {
|
|
301
302
|
const src = path.join(kb, `${repo}-primer.md`);
|
|
302
|
-
if (isPrivate(privateSet, repo)) {
|
|
303
|
+
if (isPrivate(privateSet, repo) || isCapabilityOnly(repo)) {
|
|
303
304
|
excluded.primers.push({ repo, ...excludedIdentity(src) });
|
|
304
305
|
continue;
|
|
305
306
|
}
|
|
@@ -113,6 +113,18 @@ function exactNamedFile(root, name, label) {
|
|
|
113
113
|
return trustedFile(path.join(root, name), label);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function validHistoricalGenerationLedger(ledger, names) {
|
|
117
|
+
const legacy = ledger?.schemaVersion === 1;
|
|
118
|
+
const runtime = ledger?.schemaVersion === 2
|
|
119
|
+
&& ledger?.kind === 'ruvnet-brain-runtime-generation-ledger'
|
|
120
|
+
&& HEX40.test(String(ledger?.sourceSnapshot || ''));
|
|
121
|
+
return (legacy || runtime)
|
|
122
|
+
&& typeof ledger?.brainVersion === 'string' && Boolean(ledger.brainVersion)
|
|
123
|
+
&& typeof ledger?.releaseTag === 'string' && Boolean(ledger.releaseTag)
|
|
124
|
+
&& names.length > 0
|
|
125
|
+
&& new Set(names.map((name) => name.toLowerCase())).size === names.length;
|
|
126
|
+
}
|
|
127
|
+
|
|
116
128
|
function validateCandidate({ root, bundleFile, packageFile }) {
|
|
117
129
|
const coverageFile = trustedFile(path.join(root, 'COVERAGE.json'), 'candidate release coverage');
|
|
118
130
|
const coverageBytes = fs.readFileSync(coverageFile);
|
|
@@ -176,9 +188,7 @@ function retrospectiveBaselineFromTree({ extractedRoot, bundleFile, expectedTag,
|
|
|
176
188
|
const root = path.dirname(ledgerFile);
|
|
177
189
|
const { value: ledger } = readJson(ledgerFile, 'historical baseline generation ledger');
|
|
178
190
|
const names = Object.keys(ledger?.stores || {}).sort();
|
|
179
|
-
if (ledger
|
|
180
|
-
|| typeof ledger.releaseTag !== 'string' || !ledger.releaseTag || !names.length
|
|
181
|
-
|| new Set(names.map((name) => name.toLowerCase())).size !== names.length) {
|
|
191
|
+
if (!validHistoricalGenerationLedger(ledger, names)) {
|
|
182
192
|
fail('historical baseline generation ledger is malformed');
|
|
183
193
|
}
|
|
184
194
|
const archive = namedIdentity(bundleFile);
|
|
@@ -245,9 +255,7 @@ function observedBaselineFromTree({ extractedRoot, bundleFile, expectedTag, expe
|
|
|
245
255
|
const root = path.dirname(ledgerFile);
|
|
246
256
|
const { value: ledger } = readJson(ledgerFile, 'historical baseline generation ledger');
|
|
247
257
|
const ledgerNames = Object.keys(ledger?.stores || {}).sort();
|
|
248
|
-
if (ledger
|
|
249
|
-
|| typeof ledger.releaseTag !== 'string' || !ledger.releaseTag || !ledgerNames.length
|
|
250
|
-
|| new Set(ledgerNames.map((name) => name.toLowerCase())).size !== ledgerNames.length) {
|
|
258
|
+
if (!validHistoricalGenerationLedger(ledger, ledgerNames)) {
|
|
251
259
|
fail('historical baseline generation ledger is malformed');
|
|
252
260
|
}
|
|
253
261
|
const archive = namedIdentity(bundleFile);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Rebuild ruOS's release store from the curated capability summary, never from repository source.
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
|
+
import { buildCorpus } from '../kb/forge-corpus.mjs';
|
|
9
|
+
import { loadRvf } from '../kb/resolve-deps.mjs';
|
|
10
|
+
import { CAPABILITY_RETIRED_SUFFIXES, isCapabilityOnly } from '../kb/capability-only.mjs';
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const NAME = 'cognitum-ruos';
|
|
14
|
+
const MODEL = 'Xenova/bge-base-en-v1.5';
|
|
15
|
+
const DIMENSIONS = 768;
|
|
16
|
+
|
|
17
|
+
function writeJson(file, value) {
|
|
18
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function prepareCapabilityOnlyInputs(assetsDir) {
|
|
22
|
+
const dir = path.resolve(assetsDir);
|
|
23
|
+
if (!fs.statSync(dir).isDirectory()) throw new Error(`assets directory is invalid: ${dir}`);
|
|
24
|
+
const corpus = buildCorpus({ repo: dir, name: NAME });
|
|
25
|
+
if (corpus.chunks.length !== 1 || corpus.chunks[0].path !== 'CAPABILITIES.md'
|
|
26
|
+
|| corpus.chunks[0].kind !== 'doc') {
|
|
27
|
+
throw new Error('curated ruOS source must produce exactly one CAPABILITIES.md document');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for (const suffix of CAPABILITY_RETIRED_SUFFIXES) fs.rmSync(path.join(dir, `${NAME}${suffix}`), { force: true });
|
|
31
|
+
for (const file of fs.readdirSync(dir)) {
|
|
32
|
+
if (file.startsWith(`${NAME}.big.vecs.`) || file.startsWith(`${NAME}.big.progress.`)) {
|
|
33
|
+
fs.rmSync(path.join(dir, file), { force: true });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const chunk = corpus.chunks[0];
|
|
37
|
+
writeJsonLines(path.join(dir, `${NAME}.passages.jsonl`), [{
|
|
38
|
+
id: chunk.id, text: chunk.text, path: chunk.path, title: chunk.title,
|
|
39
|
+
}]);
|
|
40
|
+
writeJson(path.join(dir, `${NAME}.meta.json`), {
|
|
41
|
+
model: MODEL, dimensions: DIMENSIONS, metric: 'cosine', generated: new Date().toISOString(),
|
|
42
|
+
entries: { [chunk.id]: { path: chunk.path, kind: chunk.kind, title: chunk.title, chunk: '1/1', preview: chunk.preview } },
|
|
43
|
+
});
|
|
44
|
+
return { dir, sourceText: chunk.text, id: chunk.id };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Historical v4.3.26 Brain self-store embedded the implementation primer. Remove those
|
|
48
|
+
// vectors from the copied seed and bind the rewritten bytes before packaging.
|
|
49
|
+
export async function pruneCapabilityOnlySelfStore(assetsDir, { RvfDatabase = null } = {}) {
|
|
50
|
+
const dir = path.resolve(assetsDir);
|
|
51
|
+
const passagesFile = path.join(dir, 'ruvnet-brain.passages.jsonl');
|
|
52
|
+
const metaFile = path.join(dir, 'ruvnet-brain.meta.json');
|
|
53
|
+
const rvfFile = path.join(dir, 'ruvnet-brain.big.rvf');
|
|
54
|
+
const ledgerFile = path.join(dir, 'RVF-GENERATIONS.json');
|
|
55
|
+
const rows = fs.readFileSync(passagesFile, 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
|
|
56
|
+
const removed = rows.filter(row => /(?:^|\/)cognitum-ruos-primer\.md$/i.test(row.path || ''));
|
|
57
|
+
if (!removed.length) return { removed: 0 };
|
|
58
|
+
if (!RvfDatabase) ({ mod: { RvfDatabase } } = loadRvf());
|
|
59
|
+
const db = await RvfDatabase.open(rvfFile);
|
|
60
|
+
try {
|
|
61
|
+
await db.delete(removed.map(row => row.id));
|
|
62
|
+
await db.compact();
|
|
63
|
+
} finally { await db.close(); }
|
|
64
|
+
const kept = rows.filter(row => !removed.some(item => item.id === row.id));
|
|
65
|
+
writeJsonLines(passagesFile, kept);
|
|
66
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'));
|
|
67
|
+
const removedIds = new Set(removed.map(row => row.id));
|
|
68
|
+
for (const row of removed) {
|
|
69
|
+
const entry = meta.entries?.[row.path];
|
|
70
|
+
if (!entry) continue;
|
|
71
|
+
entry.chunkIds = (entry.chunkIds || []).filter(id => !removedIds.has(id));
|
|
72
|
+
if (!entry.chunkIds.length) delete meta.entries[row.path];
|
|
73
|
+
}
|
|
74
|
+
writeJson(metaFile, meta);
|
|
75
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerFile, 'utf8'));
|
|
76
|
+
const bytes = fs.readFileSync(rvfFile);
|
|
77
|
+
ledger.stores['ruvnet-brain'] = {
|
|
78
|
+
...ledger.stores['ruvnet-brain'],
|
|
79
|
+
sha256: crypto.createHash('sha256').update(bytes).digest('hex'), bytes: bytes.length,
|
|
80
|
+
builtUtc: new Date().toISOString(),
|
|
81
|
+
};
|
|
82
|
+
writeJson(ledgerFile, ledger);
|
|
83
|
+
return { removed: removed.length };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeJsonLines(file, rows) {
|
|
87
|
+
fs.writeFileSync(file, `${rows.map((row) => JSON.stringify(row)).join('\n')}\n`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function bindCapabilityOnlyGeneration(assetsDir, { builtUtc = new Date().toISOString() } = {}) {
|
|
91
|
+
const dir = path.resolve(assetsDir);
|
|
92
|
+
const rvfFile = path.join(dir, `${NAME}.big.rvf`);
|
|
93
|
+
const metaFile = path.join(dir, `${NAME}.meta.json`);
|
|
94
|
+
const ledgerFile = path.join(dir, 'RVF-GENERATIONS.json');
|
|
95
|
+
const embedFile = path.join(dir, `${NAME}.big.rvf.embed.json`);
|
|
96
|
+
const rvf = fs.readFileSync(rvfFile);
|
|
97
|
+
const embed = JSON.parse(fs.readFileSync(embedFile, 'utf8'));
|
|
98
|
+
const meta = JSON.parse(fs.readFileSync(metaFile, 'utf8'));
|
|
99
|
+
if (embed.model !== MODEL || embed.dimensions !== DIMENSIONS || Object.keys(meta.entries || {}).length !== 1) {
|
|
100
|
+
throw new Error('rebuilt ruOS store does not match the 768-dim, one-summary contract');
|
|
101
|
+
}
|
|
102
|
+
const ledger = JSON.parse(fs.readFileSync(ledgerFile, 'utf8'));
|
|
103
|
+
if (!ledger || ![1, 2].includes(ledger.schemaVersion) || typeof ledger.stores !== 'object' || !ledger.stores) {
|
|
104
|
+
throw new Error('seed runtime generation ledger is malformed');
|
|
105
|
+
}
|
|
106
|
+
const previous = ledger.stores[NAME] || {};
|
|
107
|
+
ledger.stores[NAME] = {
|
|
108
|
+
file: `${NAME}.big.rvf`,
|
|
109
|
+
sha256: crypto.createHash('sha256').update(rvf).digest('hex'),
|
|
110
|
+
bytes: rvf.length,
|
|
111
|
+
model: MODEL,
|
|
112
|
+
dimensions: DIMENSIONS,
|
|
113
|
+
sourceCommit: previous.sourceCommit ?? null,
|
|
114
|
+
builtUtc,
|
|
115
|
+
};
|
|
116
|
+
writeJson(ledgerFile, ledger);
|
|
117
|
+
return ledger.stores[NAME];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function refreshCapabilityOnlyStore(assetsDir) {
|
|
121
|
+
const { dir } = prepareCapabilityOnlyInputs(assetsDir);
|
|
122
|
+
const selfStore = await pruneCapabilityOnlySelfStore(dir);
|
|
123
|
+
const script = path.join(ROOT, 'kb/forge-big.mjs');
|
|
124
|
+
const result = spawnSync(process.execPath, [script, 'both', '--dir', dir, '--name', NAME], {
|
|
125
|
+
cwd: ROOT, encoding: 'utf8', stdio: 'inherit', env: process.env,
|
|
126
|
+
});
|
|
127
|
+
if (result.error) throw result.error;
|
|
128
|
+
if (result.status !== 0) throw new Error(`ruOS capability-only RVF build exited ${result.status}`);
|
|
129
|
+
return { ...bindCapabilityOnlyGeneration(dir), selfStore };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function main(argv) {
|
|
133
|
+
const index = argv.indexOf('--assets');
|
|
134
|
+
const assetsDir = index >= 0 ? argv[index + 1] : null;
|
|
135
|
+
if (!assetsDir) throw new Error('Usage: refresh-capability-only-store.mjs --assets <seed-kb-directory>');
|
|
136
|
+
if (!isCapabilityOnly(NAME)) throw new Error('configured store is not capability-only');
|
|
137
|
+
const result = await refreshCapabilityOnlyStore(assetsDir);
|
|
138
|
+
console.log(JSON.stringify({ store: NAME, ...result }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
|
|
142
|
+
try { await main(process.argv.slice(2)); } catch (error) { console.error(`[capability-store] ${error.message}`); process.exitCode = 1; }
|
|
143
|
+
}
|