ruvnet-brain 4.3.25 → 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/console/app.js +48 -0
- package/console/index.html +19 -0
- package/console/style.css +26 -0
- 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/onboarding-console.mjs +45 -1
- 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-transaction-provider.mjs +25 -5
- package/scripts/release-transaction.mjs +40 -11
- 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
|
@@ -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
|
+
}
|
|
@@ -21,6 +21,11 @@ const json = (name, args, options) => JSON.parse(command(name, args, options));
|
|
|
21
21
|
// night a corpus round shipped. Every question this provider asks is about the CODE generation, so
|
|
22
22
|
// it must resolve the latest CODE release rather than read a pointer that now answers differently.
|
|
23
23
|
const latestCodeTag = () => latestCodeReleaseTag(json('gh', ['api', `repos/${REPO}/releases?per_page=30`]));
|
|
24
|
+
// Two DIFFERENT questions, deliberately: latestCodeTag() is "which CODE generation is newest" (a
|
|
25
|
+
// corpus generation may hold the pointer — designed steady state); githubPointerTag() is "what does
|
|
26
|
+
// GitHub's releases/latest actually point at right now". The publish-github-nonlatest gate needs the
|
|
27
|
+
// second and was reading the first, which made it unsatisfiable for every freshly published release.
|
|
28
|
+
const githubPointerTag = () => maybe(() => json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name, null);
|
|
24
29
|
const maybe = (callback, fallback = null) => {
|
|
25
30
|
try { return callback(); } catch { return fallback; }
|
|
26
31
|
};
|
|
@@ -79,14 +84,26 @@ const sha256File = (file) => {
|
|
|
79
84
|
return hash.digest('hex');
|
|
80
85
|
};
|
|
81
86
|
const assetDigest = (asset) => withTempAsset(asset, sha256File);
|
|
87
|
+
// 4.3.25 run 35056636514: `npm publish` succeeded at ~04:47:31Z and the registry first showed the
|
|
88
|
+
// version at 04:53:40Z (time.modified) — roughly six minutes. The poll gave up at 156s, the job
|
|
89
|
+
// reported FAILURE, and an already-published release was left half-promoted with npm and GitHub
|
|
90
|
+
// naming different generations (the exact #77 split). The action was never the problem; the
|
|
91
|
+
// deadline was shorter than npm's propagation for this package. Ten minutes covers the observed
|
|
92
|
+
// ~6 with margin; the env overrides remain for a runner that knows better.
|
|
82
93
|
const OBSERVATION_POLICY = {
|
|
83
|
-
maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS ||
|
|
84
|
-
maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS ||
|
|
94
|
+
maxElapsedMs: Number(process.env.RUVNET_NPM_VISIBILITY_TIMEOUT_MS || 600_000),
|
|
95
|
+
maxAttempts: Number(process.env.RUVNET_NPM_VISIBILITY_ATTEMPTS || 40),
|
|
85
96
|
initialDelayMs: 1_000,
|
|
86
97
|
maxDelayMs: 15_000,
|
|
87
98
|
multiplier: 1.8,
|
|
88
99
|
jitter: (delay) => Math.floor(Math.random() * Math.min(1_000, delay * 0.2)),
|
|
89
100
|
};
|
|
101
|
+
// The one command in this file that mutates a public channel gets its stderr in the job log and a
|
|
102
|
+
// timeout sized for a real registry upload. command()'s defaults — stderr piped and dropped, 30s —
|
|
103
|
+
// meant 4.3.25's first run left NO diagnostic between "stage-npm" and the deadline error, and a slow
|
|
104
|
+
// registry would have killed a legitimate publish mid-upload. Named, so the publish call stays one
|
|
105
|
+
// pinnable line for tests/unit/release-lineage + publication-receipt-wiring (one publisher, one tag).
|
|
106
|
+
const PUBLISH_COMMAND_OPTIONS = Object.freeze({ stdio: ['ignore', 'pipe', 'inherit'], timeout: 600_000 });
|
|
90
107
|
|
|
91
108
|
export function selectCurrentReleaseBytes({ remoteBytes, localBytes, generatedBytes, identity }) {
|
|
92
109
|
const bytes = remoteBytes || localBytes || generatedBytes;
|
|
@@ -263,6 +280,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
263
280
|
try {
|
|
264
281
|
const release = draft?.id ? hydratedRelease(releaseById(draft.id)) : null;
|
|
265
282
|
const latestTag = latestCodeTag();
|
|
283
|
+
const pointerTag = githubPointerTag();
|
|
266
284
|
const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
|
|
267
285
|
const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
|
|
268
286
|
const latestVersion = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
@@ -295,8 +313,10 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
295
313
|
published: release.draft === false,
|
|
296
314
|
latest: latestTag === identity.tag,
|
|
297
315
|
latestTag,
|
|
316
|
+
// What releases/latest actually points at — distinct from `latest` (code recency) above.
|
|
317
|
+
pointerTag,
|
|
298
318
|
assetsExact: assetsExactFor(release, identity, forceAssets),
|
|
299
|
-
} : { tag: null, sha: null, draft: false, published: false, latest: false, assetsExact: false },
|
|
319
|
+
} : { tag: null, sha: null, draft: false, published: false, latest: false, pointerTag, assetsExact: false },
|
|
300
320
|
publicReceiptExact,
|
|
301
321
|
publicHostsExact,
|
|
302
322
|
};
|
|
@@ -400,7 +420,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
400
420
|
command('npm', ['dist-tag', 'add', `${PACKAGE}@${identity.version}`, `candidate-v${identity.version}`]);
|
|
401
421
|
return;
|
|
402
422
|
}
|
|
403
|
-
command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`]);
|
|
423
|
+
command('npm', ['publish', packagePath, '--tag', `candidate-v${identity.version}`], PUBLISH_COMMAND_OPTIONS);
|
|
404
424
|
},
|
|
405
425
|
|
|
406
426
|
async observeNpmCandidate(identity) {
|
|
@@ -437,7 +457,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
437
457
|
// legitimately holding `releases/latest` is NOT that — it is the designed steady state between
|
|
438
458
|
// code releases — so the comparison is made against the latest CODE release, and the pointer's
|
|
439
459
|
// own kind is recorded rather than treated as a conflict.
|
|
440
|
-
const pointer =
|
|
460
|
+
const pointer = githubPointerTag();
|
|
441
461
|
const current = latestCodeTag();
|
|
442
462
|
if (current !== expectedPrior && current !== identity.tag) {
|
|
443
463
|
throw new Error(`refusing GitHub promotion: latest code release is ${current}, expected ${expectedPrior}`);
|
|
@@ -194,6 +194,25 @@ const npmCandidateExact = (snapshot, identity) => snapshot?.npm?.candidateVersio
|
|
|
194
194
|
const npmLatestIsB = (snapshot, identity) => snapshot?.npm?.latestVersion === identity.version;
|
|
195
195
|
const githubIsB = (snapshot, identity) => snapshot?.github?.sha === identity.candidateSha
|
|
196
196
|
&& snapshot?.github?.tag === identity.tag;
|
|
197
|
+
// TWO DIFFERENT QUESTIONS ABOUT "LATEST", ANSWERED BY TWO DIFFERENT FIELDS (4.3.25, 2026-09-16).
|
|
198
|
+
//
|
|
199
|
+
// `snapshot.github.latest` is "is this the newest CODE release" — observeGithub derives it from
|
|
200
|
+
// latestCodeReleaseTag(), deliberately, because per ADR-086 S1 a corpus generation may legitimately
|
|
201
|
+
// hold GitHub's releases/latest pointer and a code release must not observe itself as demoted.
|
|
202
|
+
// `snapshot.github.pointerTag` is what releases/latest actually points at.
|
|
203
|
+
//
|
|
204
|
+
// Every promotion, recovery, verification and abort check in this file wants the SECOND question:
|
|
205
|
+
// "has the pointer been moved to B yet". Ten sites read the first. A freshly published code release
|
|
206
|
+
// is ALWAYS the newest code release, so `!latest` was unsatisfiable (publish gate, resume clause —
|
|
207
|
+
// the transaction wedged at github-promote-intent), `latest` was trivially true (promotion looked
|
|
208
|
+
// already done, the pointer never moved, converge and finalize passed with it unmoved), and abort
|
|
209
|
+
// refused because "latest" had moved. Ask the pointer. Snapshots without one (recorded receipts,
|
|
210
|
+
// older providers) fall back to code recency so history stays interpretable.
|
|
211
|
+
const githubPointerIsB = (snapshot, identity) => (snapshot?.github?.pointerTag !== undefined
|
|
212
|
+
? snapshot.github.pointerTag === identity.tag
|
|
213
|
+
: snapshot?.github?.latest === true);
|
|
214
|
+
const githubPointerTag = (snapshot) => (snapshot?.github?.pointerTag !== undefined
|
|
215
|
+
? snapshot.github.pointerTag : snapshot?.github?.latestTag);
|
|
197
216
|
|
|
198
217
|
/**
|
|
199
218
|
* Pure, total next-action selector. Receipt history is an audit chain, never permission to skip a
|
|
@@ -206,7 +225,7 @@ export function reduceReleaseState({ lastReceipt, snapshot, identity, prior }) {
|
|
|
206
225
|
const npmB = npmLatestIsB(snapshot, identity);
|
|
207
226
|
const githubB = githubIsB(snapshot, identity);
|
|
208
227
|
const githubPublished = githubB && snapshot.github?.published === true;
|
|
209
|
-
const githubLatest = githubPublished && snapshot
|
|
228
|
+
const githubLatest = githubPublished && githubPointerIsB(snapshot, identity);
|
|
210
229
|
|
|
211
230
|
if (snapshot.npm?.candidateVersion === identity.version && snapshot.npm?.candidateIntegrity
|
|
212
231
|
&& snapshot.npm.candidateIntegrity !== identity.packageIntegrity) {
|
|
@@ -338,8 +357,11 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
338
357
|
await transition('prepared', { recovered: true });
|
|
339
358
|
continue;
|
|
340
359
|
}
|
|
360
|
+
// 4.3.25 run 35109036116: run 2 died after writing github-promote-intent, run 3 found the draft
|
|
361
|
+
// published, the reducer asked for promote-npm — not a legal edge from here. This clause was the
|
|
362
|
+
// only legal exit and its `!latest` was unsatisfiable. Wedged.
|
|
341
363
|
if (current.state === 'github-promote-intent' && githubIsB(snapshot, identity)
|
|
342
|
-
&& snapshot.github?.published && !snapshot
|
|
364
|
+
&& snapshot.github?.published && !githubPointerIsB(snapshot, identity)) {
|
|
343
365
|
await transition('github-promoted-nonlatest', { github: snapshot.github, recovered: true });
|
|
344
366
|
continue;
|
|
345
367
|
}
|
|
@@ -348,7 +370,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
348
370
|
continue;
|
|
349
371
|
}
|
|
350
372
|
if (current.state === 'github-latest-intent' && npmLatestIsB(snapshot, identity)
|
|
351
|
-
&& githubIsB(snapshot, identity) && snapshot
|
|
373
|
+
&& githubIsB(snapshot, identity) && githubPointerIsB(snapshot, identity)) {
|
|
352
374
|
await transition('defaults-promoted', { npm: snapshot.npm, github: snapshot.github, recovered: true });
|
|
353
375
|
continue;
|
|
354
376
|
}
|
|
@@ -357,7 +379,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
357
379
|
throw new Error('npm compensation recovered; resume the same transaction');
|
|
358
380
|
}
|
|
359
381
|
if (current.state === 'finalize-intent' && npmCandidateExact(snapshot, identity) && npmLatestIsB(snapshot, identity)
|
|
360
|
-
&& githubIsB(snapshot, identity) && snapshot.github?.published && snapshot
|
|
382
|
+
&& githubIsB(snapshot, identity) && snapshot.github?.published && githubPointerIsB(snapshot, identity)
|
|
361
383
|
&& snapshot.github?.assetsExact) {
|
|
362
384
|
return append('channels-converged', { verdict: 'PUBLISHED_NOT_VERIFIED', recovered: true,
|
|
363
385
|
npm: snapshot.npm, github: snapshot.github });
|
|
@@ -425,7 +447,10 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
425
447
|
await transition('github-promote-intent');
|
|
426
448
|
await adapter.publishDraftNonLatest(draft, identity);
|
|
427
449
|
const observed = await adapter.observeSnapshot(identity, draft);
|
|
428
|
-
|
|
450
|
+
// Published as B, and the POINTER has not moved to B yet — it is promoted later, in
|
|
451
|
+
// make-github-latest. 4.3.25 run 35057738103 published this correctly and then threw here on
|
|
452
|
+
// `!latest`, which a just-published code release can never satisfy.
|
|
453
|
+
if (!(githubIsB(observed, identity) && observed.github.published && !githubPointerIsB(observed, identity))) {
|
|
429
454
|
throw new Error('GitHub non-latest publication not observed');
|
|
430
455
|
}
|
|
431
456
|
await transition('github-promoted-nonlatest', { github: observed.github });
|
|
@@ -446,7 +471,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
446
471
|
await transition('github-latest-intent');
|
|
447
472
|
await adapter.makeGithubLatest(draft, identity, prior.githubLatest);
|
|
448
473
|
const observed = await adapter.observeSnapshot(identity, draft);
|
|
449
|
-
if (!(npmLatestIsB(observed, identity) && observed
|
|
474
|
+
if (!(npmLatestIsB(observed, identity) && githubPointerIsB(observed, identity) && githubIsB(observed, identity))) {
|
|
450
475
|
throw new Error('provider defaults not jointly observed at candidate B');
|
|
451
476
|
}
|
|
452
477
|
await transition('defaults-promoted', { npm: observed.npm, github: observed.github });
|
|
@@ -469,7 +494,7 @@ export async function runReleaseTransaction({ identity, assets, adapter, private
|
|
|
469
494
|
}
|
|
470
495
|
await transition('finalize-intent');
|
|
471
496
|
const reobserved = await adapter.observeSnapshot(identity, draft, { forceAssets: true });
|
|
472
|
-
if (!(npmLatestIsB(reobserved, identity) && reobserved
|
|
497
|
+
if (!(npmLatestIsB(reobserved, identity) && githubPointerIsB(reobserved, identity) && githubIsB(reobserved, identity))) {
|
|
473
498
|
throw new Error('provider defaults drifted before channel convergence');
|
|
474
499
|
}
|
|
475
500
|
if (!reobserved.github?.assetsExact || !npmCandidateExact(reobserved, identity)) {
|
|
@@ -491,8 +516,12 @@ export async function abortReleaseTransaction({ identity, receipts, reason, auth
|
|
|
491
516
|
}
|
|
492
517
|
const snapshot = await adapter.observeSnapshot(identity);
|
|
493
518
|
const prior = chain[0]?.observation?.prior;
|
|
494
|
-
|
|
495
|
-
|
|
519
|
+
// "Both defaults still at the captured prior generation" is a question about the npm `latest`
|
|
520
|
+
// dist-tag and GitHub's releases/latest POINTER. A published-but-not-promoted candidate is the
|
|
521
|
+
// newest CODE release, so the old `latest === true` / `latestTag !== prior` reads refused to abort
|
|
522
|
+
// exactly the transaction that most needs aborting: one wedged after publish, before promotion.
|
|
523
|
+
if (!prior || snapshot.npm?.latestVersion === identity.version || githubPointerIsB(snapshot, identity)
|
|
524
|
+
|| snapshot.npm?.latestVersion !== prior.npmLatest || githubPointerTag(snapshot) !== prior.githubLatest) {
|
|
496
525
|
throw new Error('release abort cannot prove both defaults at captured prior generation');
|
|
497
526
|
}
|
|
498
527
|
const receipt = stateReceipt({
|
|
@@ -550,7 +579,7 @@ export async function finalizeReleaseTransaction({
|
|
|
550
579
|
}
|
|
551
580
|
const snapshot = await adapter.observeSnapshot(identity, discovered.matchingDrafts?.[0], { forceAssets: true });
|
|
552
581
|
if (snapshot.readError || !npmLatestIsB(snapshot, identity) || !npmCandidateExact(snapshot, identity)
|
|
553
|
-
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || snapshot
|
|
582
|
+
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || !githubPointerIsB(snapshot, identity)
|
|
554
583
|
|| snapshot.github?.assetsExact !== true) throw new Error('public channels drifted before install verification');
|
|
555
584
|
const materialized = await adapter.materializePublicVerificationAggregate({ identity, aggregate });
|
|
556
585
|
const aggregateAssetSha256 = crypto.createHash('sha256').update(canonicalJson(aggregate)).digest('hex');
|
|
@@ -642,7 +671,7 @@ export async function abandonPublicVerificationTransaction({ identity, expected,
|
|
|
642
671
|
}
|
|
643
672
|
const snapshot = await adapter.observeSnapshot(identity, discovered.matchingDrafts?.[0], { forceAssets: true });
|
|
644
673
|
if (snapshot.readError || !npmLatestIsB(snapshot, identity) || !npmCandidateExact(snapshot, identity)
|
|
645
|
-
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || snapshot
|
|
674
|
+
|| !githubIsB(snapshot, identity) || snapshot.github?.published !== true || !githubPointerIsB(snapshot, identity)
|
|
646
675
|
|| snapshot.github?.assetsExact !== true
|
|
647
676
|
|| canonicalJson(snapshot.npm) !== canonicalJson(current.observation.npm)
|
|
648
677
|
|| canonicalJson(snapshot.github) !== canonicalJson(current.observation.github)) {
|
|
@@ -21,6 +21,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
21
21
|
import fs from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { performance } from 'node:perf_hooks';
|
|
24
25
|
|
|
25
26
|
export const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
27
|
|
|
@@ -276,35 +277,58 @@ export function headSha() {
|
|
|
276
277
|
/** Strings a non-PASS verdict mechanically bans from release surfaces (ADR-058). */
|
|
277
278
|
export const BANNED_WHEN_DEGRADED = ['healthy', 'proven', 'all pass'];
|
|
278
279
|
|
|
279
|
-
export async function evaluate(invariants = INVARIANTS, options = {}) {
|
|
280
|
+
export async function evaluate(invariants = INVARIANTS, options = {}, { onInvariantStart, onInvariantComplete } = {}) {
|
|
280
281
|
const lineage = candidateLineage(options.root || ROOT);
|
|
281
282
|
const sha = lineage.sha;
|
|
282
283
|
const results = [];
|
|
283
284
|
for (const inv of invariants) {
|
|
285
|
+
onInvariantStart?.({ name: inv.name, dimension: inv.dimension });
|
|
286
|
+
const started = performance.now();
|
|
284
287
|
const r = await inv.detect(options);
|
|
285
|
-
|
|
288
|
+
const elapsedMs = Math.max(0, Math.round(performance.now() - started));
|
|
289
|
+
const result = { name: inv.name, dimension: inv.dimension, state: r.state, why: r.why, sha, elapsedMs };
|
|
290
|
+
results.push(result);
|
|
291
|
+
onInvariantComplete?.(result);
|
|
286
292
|
}
|
|
287
293
|
return { sha, lineage, results, verdict: verdictWithLineage(results, lineage) };
|
|
288
294
|
}
|
|
289
295
|
|
|
296
|
+
/** Map the vector verdict to the process contract: only PASS exits successfully. */
|
|
297
|
+
export function exitCodeForVerdict(verdict) {
|
|
298
|
+
return verdict === 'PASS' ? 0 : 1;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Render either CLI format from one already-evaluated result; rendering never reruns detectors. */
|
|
302
|
+
export function formatVectorOutput(result, { json = false } = {}) {
|
|
303
|
+
if (json) return JSON.stringify(result, null, 2);
|
|
304
|
+
const mark = { PASS: '✓', FAIL: '✗', UNKNOWN: '?' };
|
|
305
|
+
const rows = result.results.map((r) =>
|
|
306
|
+
` ${mark[r.state]} ${r.state.padEnd(7)} ${r.dimension.padEnd(3)} ${r.name.padEnd(22)} (${r.elapsedMs ?? '—'}ms) ${r.why}`);
|
|
307
|
+
const lines = [
|
|
308
|
+
'',
|
|
309
|
+
` release vector @ ${result.sha.slice(0, 7)}`,
|
|
310
|
+
'',
|
|
311
|
+
...rows,
|
|
312
|
+
` lineage: tree ${result.lineage.tree.slice(0, 12)} · ${result.lineage.dirty ? 'DIRTY (release-blocking)' : 'clean'}`,
|
|
313
|
+
'',
|
|
314
|
+
` verdict: ${result.verdict} (vector MINIMUM over ${result.results.length} invariants — never an average)`,
|
|
315
|
+
];
|
|
316
|
+
if (result.verdict !== 'PASS') {
|
|
317
|
+
lines.push(` release metadata must read DEGRADED; these strings are banned: ${BANNED_WHEN_DEGRADED.map((s) => `"${s}"`).join(', ')}`);
|
|
318
|
+
}
|
|
319
|
+
return `${lines.join('\n')}\n`;
|
|
320
|
+
}
|
|
321
|
+
|
|
290
322
|
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
291
|
-
const
|
|
323
|
+
const timings = process.argv.includes('--timings');
|
|
324
|
+
const emitTiming = (phase, data) => process.stderr.write(`${JSON.stringify({
|
|
325
|
+
schema: 'ruvnet-brain.release-vector.timing', phase, ...data,
|
|
326
|
+
})}\n`);
|
|
327
|
+
const result = await evaluate(INVARIANTS, {}, timings ? {
|
|
328
|
+
onInvariantStart: ({ name, dimension }) => emitTiming('start', { name, dimension }),
|
|
329
|
+
onInvariantComplete: ({ name, dimension, elapsedMs }) => emitTiming('complete', { name, dimension, elapsedMs }),
|
|
330
|
+
} : {});
|
|
292
331
|
const json = process.argv.includes('--json');
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
} else {
|
|
296
|
-
const mark = { PASS: '✓', FAIL: '✗', UNKNOWN: '?' };
|
|
297
|
-
console.log(`\n release vector @ ${sha.slice(0, 7)}\n`);
|
|
298
|
-
for (const r of results) {
|
|
299
|
-
console.log(` ${mark[r.state]} ${r.state.padEnd(7)} ${r.dimension.padEnd(3)} ${r.name.padEnd(22)} ${r.why}`);
|
|
300
|
-
}
|
|
301
|
-
console.log(` lineage: tree ${lineage.tree.slice(0, 12)} · ${lineage.dirty ? 'DIRTY (release-blocking)' : 'clean'}`);
|
|
302
|
-
console.log(`\n verdict: ${verdict} (vector MINIMUM over ${results.length} invariants — never an average)`);
|
|
303
|
-
if (verdict !== 'PASS') {
|
|
304
|
-
console.log(` release metadata must read DEGRADED; these strings are banned: ${BANNED_WHEN_DEGRADED.map((s) => `"${s}"`).join(', ')}\n`);
|
|
305
|
-
} else {
|
|
306
|
-
console.log('');
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
process.exitCode = verdict === 'PASS' ? 0 : 1;
|
|
332
|
+
console.log(formatVectorOutput(result, { json }));
|
|
333
|
+
process.exitCode = exitCodeForVerdict(result.verdict);
|
|
310
334
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Source-span operational benchmark. Each receipt preserves the exact query, raw customer-path
|
|
3
|
+
// output, citation resolution, oracle match, and latency. It never edits historical eval baselines.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { performance } from 'node:perf_hooks';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
+
import {
|
|
12
|
+
OPERATIONAL_FIXTURES,
|
|
13
|
+
gradeOperationalFixture,
|
|
14
|
+
latencyDistribution,
|
|
15
|
+
verifyFixtureSourceSupport,
|
|
16
|
+
} from '../evals/operational-benchmark.v2.mjs';
|
|
17
|
+
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
const KB = process.env.RUVNET_BRAIN_KB || path.join(os.homedir(), '.cache', 'ruvnet-brain', 'kb');
|
|
21
|
+
|
|
22
|
+
export async function runOperationalBenchmark({ fixtures = OPERATIONAL_FIXTURES, kb = KB, timeoutMs = 240_000 } = {}) {
|
|
23
|
+
const reader = path.join(kb, 'forge-ask-all.mjs');
|
|
24
|
+
const verifierPath = path.join(kb, 'verify-citation.mjs');
|
|
25
|
+
if (!fs.existsSync(reader) || !fs.existsSync(verifierPath)) throw new Error(`RuvNet Brain runtime or verifier missing under ${kb}`);
|
|
26
|
+
const { verifyGrounding } = await import(pathToFileURL(verifierPath).href);
|
|
27
|
+
const oracleFiles = new Map();
|
|
28
|
+
for (const fixture of fixtures) {
|
|
29
|
+
if (!fixture.expectedFact) continue;
|
|
30
|
+
const oracleFile = path.join(ROOT, fixture.oraclePath || 'kb/capability-cards.md');
|
|
31
|
+
if (!fs.existsSync(oracleFile)) throw new Error(`Operational source oracle missing for ${fixture.id}: ${oracleFile}`);
|
|
32
|
+
oracleFiles.set(oracleFile, true);
|
|
33
|
+
const oracleText = fs.readFileSync(oracleFile, 'utf8');
|
|
34
|
+
const normalize = (value) => String(value).replace(/\s+/g, ' ').trim();
|
|
35
|
+
if (fixture.expectedFact && !normalize(oracleText).includes(normalize(fixture.expectedFact))) {
|
|
36
|
+
throw new Error(`Source oracle drift for ${fixture.id}: expected fact is absent from ${oracleFile}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const receipts = [];
|
|
40
|
+
for (const fixture of fixtures) {
|
|
41
|
+
if (fixture.availability) {
|
|
42
|
+
receipts.push({ fixtureId: fixture.id, class: fixture.class, query: fixture.query, availability: fixture.availability, grade: { pass: false, reason: 'fixture-not-measurable-with-available-source-oracle' } });
|
|
43
|
+
process.stderr.write(`UNAVAILABLE ${fixture.id} ${fixture.availability}\n`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const started = performance.now();
|
|
47
|
+
let output = '';
|
|
48
|
+
let stderr = '';
|
|
49
|
+
let error = null;
|
|
50
|
+
let processOk = false;
|
|
51
|
+
let processExitCode = null;
|
|
52
|
+
try {
|
|
53
|
+
const args = [reader, '--dir', kb, '--q', fixture.query, '--k', '5'];
|
|
54
|
+
if (process.env.EVAL_FULL_CORPUS !== '1') args.push('--bounded');
|
|
55
|
+
const result = await execFileAsync(process.execPath, args, { cwd: kb, timeout: timeoutMs, maxBuffer: 64 * 1024 * 1024, env: process.env });
|
|
56
|
+
output = String(result.stdout ?? '');
|
|
57
|
+
stderr = String(result.stderr ?? '');
|
|
58
|
+
processOk = true;
|
|
59
|
+
processExitCode = 0;
|
|
60
|
+
} catch (caught) {
|
|
61
|
+
error = caught?.message ?? String(caught);
|
|
62
|
+
output = String(caught?.stdout ?? '');
|
|
63
|
+
stderr = String(caught?.stderr ?? '');
|
|
64
|
+
processExitCode = Number.isInteger(caught?.code) ? caught.code : null;
|
|
65
|
+
}
|
|
66
|
+
const elapsedMs = Math.round(performance.now() - started);
|
|
67
|
+
const verification = await verifyGrounding(output, kb);
|
|
68
|
+
const sourceSupport = processOk ? await verifyFixtureSourceSupport(fixture, verification, kb) : null;
|
|
69
|
+
const grade = gradeOperationalFixture(fixture, { output, verification, sourceSupport, processOk });
|
|
70
|
+
receipts.push({
|
|
71
|
+
fixtureId: fixture.id,
|
|
72
|
+
class: fixture.class,
|
|
73
|
+
query: fixture.query,
|
|
74
|
+
expectedRepos: fixture.expectedRepos,
|
|
75
|
+
expectedFact: fixture.expectedFact,
|
|
76
|
+
elapsedMs,
|
|
77
|
+
processOk,
|
|
78
|
+
processExitCode,
|
|
79
|
+
error,
|
|
80
|
+
stderr,
|
|
81
|
+
verification,
|
|
82
|
+
sourceSupport,
|
|
83
|
+
grade,
|
|
84
|
+
rawOutput: output,
|
|
85
|
+
});
|
|
86
|
+
process.stderr.write(`${grade.pass ? 'PASS' : 'FAIL'} ${fixture.id} ${elapsedMs}ms\n`);
|
|
87
|
+
}
|
|
88
|
+
const classes = Object.fromEntries([...new Set(fixtures.map((fixture) => fixture.class))].map((name) => [
|
|
89
|
+
name,
|
|
90
|
+
{
|
|
91
|
+
pass: receipts.filter((row) => row.class === name && !row.availability && row.grade.pass).length,
|
|
92
|
+
n: receipts.filter((row) => row.class === name && !row.availability).length,
|
|
93
|
+
unavailable: receipts.filter((row) => row.class === name && row.availability).length,
|
|
94
|
+
latency: latencyDistribution(receipts.filter((row) => row.class === name && !row.availability).map((row) => row.elapsedMs)),
|
|
95
|
+
},
|
|
96
|
+
]));
|
|
97
|
+
return {
|
|
98
|
+
schema: 'ruvnet-brain-operational-benchmark/v2',
|
|
99
|
+
generatedAt: new Date().toISOString(),
|
|
100
|
+
runtime: {
|
|
101
|
+
kb: path.resolve(kb),
|
|
102
|
+
nodeExecutable: process.execPath,
|
|
103
|
+
nodeVersion: process.version,
|
|
104
|
+
readerSha256: await hashFile(reader),
|
|
105
|
+
verifierSha256: await hashFile(verifierPath),
|
|
106
|
+
fixtureSha256: await hashFile(path.join(ROOT, 'evals', 'operational-benchmark.v2.mjs')),
|
|
107
|
+
oracleSha256: Object.fromEntries(await Promise.all([...oracleFiles.keys()].map(async (file) => [path.relative(ROOT, file), await hashFile(file)]))),
|
|
108
|
+
sourceManifestSha256: await hashExistingFiles(kb, ['SOURCE.json', 'RVF-GENERATIONS.json', 'PUBLIC-RVF-GENERATIONS.json']),
|
|
109
|
+
},
|
|
110
|
+
evaluationConfig: { lane: process.env.EVAL_FULL_CORPUS === '1' ? 'full-corpus' : 'bounded', k: 5, timeoutMs, sequential: true },
|
|
111
|
+
identityScope: 'Entry-point, verifier, checked manifests, capability-card oracle, and each evidence-bearing passage store are SHA-256 bound. This is not a signed release archive or a hash of every transitive package/model input.',
|
|
112
|
+
claimBoundary: { sourceSupportedRetrievalUtility: 'measured', generatedAnswerUsefulness: 'UNKNOWN; this retrieval tool does not provide a generated answer to grade' },
|
|
113
|
+
classes,
|
|
114
|
+
passed: receipts.filter((row) => row.grade.pass).length,
|
|
115
|
+
total: receipts.filter((row) => !row.availability).length,
|
|
116
|
+
unavailable: receipts.filter((row) => row.availability).map(({ fixtureId, availability }) => ({ fixtureId, availability })),
|
|
117
|
+
receipts,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function hashFile(file) {
|
|
122
|
+
const { createHash } = await import('node:crypto');
|
|
123
|
+
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function hashExistingFiles(dir, names) {
|
|
127
|
+
const result = {};
|
|
128
|
+
for (const name of names) {
|
|
129
|
+
const file = path.join(dir, name);
|
|
130
|
+
if (fs.existsSync(file)) result[name] = await hashFile(file);
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function main() {
|
|
136
|
+
const classIndex = process.argv.indexOf('--class');
|
|
137
|
+
const selectedClass = classIndex >= 0 ? process.argv[classIndex + 1] : null;
|
|
138
|
+
const fixtures = selectedClass ? OPERATIONAL_FIXTURES.filter((fixture) => fixture.class === selectedClass) : OPERATIONAL_FIXTURES;
|
|
139
|
+
if (!fixtures.length) throw new Error(`No operational fixtures for class ${selectedClass}`);
|
|
140
|
+
const report = await runOperationalBenchmark({ fixtures });
|
|
141
|
+
const outDir = path.join(ROOT, 'evals', 'operational-runs');
|
|
142
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
143
|
+
const out = path.join(outDir, `${report.generatedAt.replace(/[:.]/g, '-')}.json`);
|
|
144
|
+
fs.writeFileSync(out, `${JSON.stringify(report, null, 2)}\n`);
|
|
145
|
+
console.log(JSON.stringify({ pass: report.passed === report.total && report.unavailable.length === 0, unavailable: report.unavailable.length, passed: report.passed, total: report.total, classes: report.classes, receipt: out }, null, 2));
|
|
146
|
+
if (report.passed !== report.total || report.unavailable.length) process.exitCode = 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
150
|
+
main().catch((error) => { console.error(error.stack || error.message); process.exitCode = 2; });
|
|
151
|
+
}
|