ruvnet-brain 4.3.21 → 4.3.25
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 +5 -5
- package/bin/install.mjs +275 -60
- package/console/app.js +141 -9
- package/console/index.html +51 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +319 -27
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +23 -6
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
|
@@ -4,6 +4,7 @@ import fs from 'node:fs';
|
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { canonicalJson } from './coverage-integrity.mjs';
|
|
7
|
+
import { isCorpusReleaseTag, latestCodeReleaseTag } from './release-channel-kind.mjs';
|
|
7
8
|
import {
|
|
8
9
|
pollObservation, RECEIPT_PREFIX, receiptDisposition, transactionIdFor,
|
|
9
10
|
} from './release-transaction.mjs';
|
|
@@ -16,6 +17,10 @@ const command = (name, args, options = {}) => execFileSync(name, args, {
|
|
|
16
17
|
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30_000, ...options,
|
|
17
18
|
}).trim();
|
|
18
19
|
const json = (name, args, options) => JSON.parse(command(name, args, options));
|
|
20
|
+
// ADR-086 S1: `releases/latest` is the customer download pointer and is a corpus generation on any
|
|
21
|
+
// night a corpus round shipped. Every question this provider asks is about the CODE generation, so
|
|
22
|
+
// it must resolve the latest CODE release rather than read a pointer that now answers differently.
|
|
23
|
+
const latestCodeTag = () => latestCodeReleaseTag(json('gh', ['api', `repos/${REPO}/releases?per_page=30`]));
|
|
19
24
|
const maybe = (callback, fallback = null) => {
|
|
20
25
|
try { return callback(); } catch { return fallback; }
|
|
21
26
|
};
|
|
@@ -216,7 +221,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
216
221
|
}
|
|
217
222
|
}
|
|
218
223
|
const npmLatest = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
219
|
-
const githubLatest =
|
|
224
|
+
const githubLatest = latestCodeTag();
|
|
220
225
|
const legacySettled = [];
|
|
221
226
|
const pending = [];
|
|
222
227
|
for (const { receipt, release } of latestByTransaction.values()) {
|
|
@@ -257,7 +262,7 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
257
262
|
async observeSnapshot(identity, draft = activeDraft, { forceAssets = false } = {}) {
|
|
258
263
|
try {
|
|
259
264
|
const release = draft?.id ? hydratedRelease(releaseById(draft.id)) : null;
|
|
260
|
-
const latestTag =
|
|
265
|
+
const latestTag = latestCodeTag();
|
|
261
266
|
const candidate = maybe(() => json('npm', ['view', `${PACKAGE}@candidate-v${identity.version}`, '--json']), null);
|
|
262
267
|
const exactVersion = maybe(() => json('npm', ['view', `${PACKAGE}@${identity.version}`, '--json']), null);
|
|
263
268
|
const latestVersion = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
|
@@ -409,7 +414,10 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
409
414
|
|
|
410
415
|
async observeGithub(identity) {
|
|
411
416
|
const release = json('gh', ['api', `repos/${REPO}/releases/tags/${identity.tag}`]);
|
|
412
|
-
|
|
417
|
+
// "Is this release the current CODE generation", not "does it hold the latest pointer". Once a
|
|
418
|
+
// corpus generation takes the pointer — which is the designed steady state — a genuinely
|
|
419
|
+
// published code release would otherwise start observing itself as no longer latest.
|
|
420
|
+
const latest = maybe(() => latestCodeTag(), null);
|
|
413
421
|
return { tag: release.tag_name, sha: tagSha(identity.tag, root), latest: latest === identity.tag };
|
|
414
422
|
},
|
|
415
423
|
|
|
@@ -424,14 +432,23 @@ export function liveReleaseProvider({ root = process.cwd() } = {}) {
|
|
|
424
432
|
return { version: command('npm', ['view', `${PACKAGE}@latest`, 'version']) };
|
|
425
433
|
},
|
|
426
434
|
async makeGithubLatest(draft, identity, expectedPrior) {
|
|
427
|
-
|
|
435
|
+
// The prior-generation check is about the CODE lineage: it exists to catch "someone promoted a
|
|
436
|
+
// different product generation while this transaction was in flight". A corpus generation
|
|
437
|
+
// legitimately holding `releases/latest` is NOT that — it is the designed steady state between
|
|
438
|
+
// code releases — so the comparison is made against the latest CODE release, and the pointer's
|
|
439
|
+
// own kind is recorded rather than treated as a conflict.
|
|
440
|
+
const pointer = json('gh', ['api', `repos/${REPO}/releases/latest`]).tag_name;
|
|
441
|
+
const current = latestCodeTag();
|
|
428
442
|
if (current !== expectedPrior && current !== identity.tag) {
|
|
429
|
-
throw new Error(`refusing GitHub promotion: latest is ${current}, expected ${expectedPrior}`);
|
|
443
|
+
throw new Error(`refusing GitHub promotion: latest code release is ${current}, expected ${expectedPrior}`);
|
|
444
|
+
}
|
|
445
|
+
if (isCorpusReleaseTag(pointer)) {
|
|
446
|
+
process.stdout.write(`[release] promoting over corpus generation ${pointer} — the code bundle carries its own corpus\n`);
|
|
430
447
|
}
|
|
431
448
|
command('gh', ['api', '-X', 'PATCH', `repos/${REPO}/releases/${draft.id}`, '-f', 'make_latest=true']);
|
|
432
449
|
},
|
|
433
450
|
async observeGithubLatest() {
|
|
434
|
-
return { tag:
|
|
451
|
+
return { tag: latestCodeTag() };
|
|
435
452
|
},
|
|
436
453
|
async restoreNpmLatest(prior, expected) {
|
|
437
454
|
const current = command('npm', ['view', `${PACKAGE}@latest`, 'version']);
|
package/scripts/release.mjs
CHANGED
|
@@ -35,6 +35,29 @@ import { materializePublicationHandoff, resolvePublicationHandoffPaths } from '.
|
|
|
35
35
|
import { liveReleaseProvider } from './release-transaction-provider.mjs';
|
|
36
36
|
import { stagedHostVerifier } from './staged-host-verifier.mjs';
|
|
37
37
|
import { verifyPayload } from './release-payload.mjs';
|
|
38
|
+
import { verifyCorpusReceipt } from './corpus-candidate.mjs';
|
|
39
|
+
import { readDiagnosticAccuracyReport } from './oracle/retrieval-accuracy.mjs';
|
|
40
|
+
import { loadFixture, readRecallReport } from './oracle/repo-recall.mjs';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The measured retrieval numbers, stated in the release notes themselves rather than left behind a
|
|
44
|
+
* digest. Both halves go in together on purpose: the number that qualified the release, and the one
|
|
45
|
+
* it did NOT meet. A reader who sees only the first would reasonably assume the second was fine.
|
|
46
|
+
*/
|
|
47
|
+
const recallNotes = (receipt) => {
|
|
48
|
+
const r = receipt.recallSummary;
|
|
49
|
+
if (!r) return [];
|
|
50
|
+
return [
|
|
51
|
+
`Retrieval (blocking): ${r.repositoriesAnswering}/${r.questions} repositories answer a real question`
|
|
52
|
+
+ ` about themselves from their own content; ${r.exactFileTop5}/${r.questions} return the exact`
|
|
53
|
+
+ ` labeled file in the top 5 (floor ${r.floor}).`,
|
|
54
|
+
'NOT measured: generated-answer correctness, citation support, or unscoped whole-corpus discovery.',
|
|
55
|
+
`ADR-086 C3 was NOT met and is NOT claimed — its measurement ships as ${receipt.accuracyReport.file}`
|
|
56
|
+
+ ' for inspection.',
|
|
57
|
+
];
|
|
58
|
+
};
|
|
59
|
+
import { verifyBundle } from './verify-bundle.mjs';
|
|
60
|
+
import { CORPUS_GENERATION_FIELD, evaluateCorpusPromotion } from './corpus-promotion.mjs';
|
|
38
61
|
|
|
39
62
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
40
63
|
const PUBLISH = process.argv.includes('--publish');
|
|
@@ -83,7 +106,7 @@ function corpusFailure(message) {
|
|
|
83
106
|
throw new Error(`[corpus-seed] ${message}`);
|
|
84
107
|
}
|
|
85
108
|
|
|
86
|
-
export function runProtectedCorpusSeed({
|
|
109
|
+
export async function runProtectedCorpusSeed({
|
|
87
110
|
argv = process.argv.slice(2),
|
|
88
111
|
env = process.env,
|
|
89
112
|
root = ROOT,
|
|
@@ -94,6 +117,13 @@ export function runProtectedCorpusSeed({
|
|
|
94
117
|
corpusFailure(environmentFailures.join('; '));
|
|
95
118
|
}
|
|
96
119
|
|
|
120
|
+
// The corpus route may never enter product publication. This is belt to the workflow's braces: the
|
|
121
|
+
// corpus job binds an environment that holds no NPM_TOKEN at all, so npm is unreachable from it by
|
|
122
|
+
// construction; this refuses the combined invocation outright so the two modes can never share one
|
|
123
|
+
// process even if a future workflow edit put them in the same job.
|
|
124
|
+
if (argv.includes('--publish')) corpusFailure('--corpus-seed cannot be combined with --publish; corpus routing must never enter product publication');
|
|
125
|
+
const promoteLatest = argv.includes('--promote-latest');
|
|
126
|
+
|
|
97
127
|
const tag = cliArg(argv, '--corpus-tag');
|
|
98
128
|
const bundleFile = cliArg(argv, '--corpus-bundle');
|
|
99
129
|
const receiptFile = cliArg(argv, '--corpus-receipt');
|
|
@@ -130,11 +160,20 @@ export function runProtectedCorpusSeed({
|
|
|
130
160
|
corpusFailure('target must exactly equal HEAD, GITHUB_SHA, and the corpus receipt builderSourceSha');
|
|
131
161
|
}
|
|
132
162
|
|
|
133
|
-
|
|
163
|
+
// Schema 3 (ADR-086 Step 15 / A6): the receipt binds the full provenance closure shipped INSIDE
|
|
164
|
+
// the sealed archive (ARCHIVE-MANIFEST.json, PRIVATE-STORES.json, RVF-GENERATIONS.json,
|
|
165
|
+
// SOURCE.json) AND the detached, digest-bound retrieval-accuracy report that measured this exact
|
|
166
|
+
// archive. Schema 2 is refused outright: a schema-2 seed carries no accuracy binding, so it is
|
|
167
|
+
// UNPUBLISHABLE from here forward and data/corpus-seed.json must be re-pointed at a schema-3 seed
|
|
168
|
+
// in the same change that publishes one.
|
|
169
|
+
const boundaryIdentities = [receipt.privateFence, receipt.generationLedger, receipt.sourceManifest, receipt.archiveManifest];
|
|
134
170
|
const storeBindingsValid = Number.isSafeInteger(receipt.storeCount) && receipt.storeCount > 0
|
|
135
171
|
&& Array.isArray(receipt.stores) && receipt.stores.length === receipt.storeCount
|
|
136
172
|
&& receipt.stores.every((store) => typeof store?.name === 'string' && store.name.length > 0
|
|
137
|
-
&&
|
|
173
|
+
&& ['repository', 'gist-aggregate', 'derived'].includes(store.kind)
|
|
174
|
+
&& (store.kind === 'repository'
|
|
175
|
+
? /^[a-f0-9]{7,64}$/.test(String(store.sourceCommit || ''))
|
|
176
|
+
: store.sourceCommit === null || /^[a-f0-9]{7,64}$/i.test(String(store.sourceCommit || '')))
|
|
138
177
|
&& typeof store.builtUtc === 'string' && Number.isFinite(Date.parse(store.builtUtc))
|
|
139
178
|
&& typeof store.model === 'string' && store.model.length > 0
|
|
140
179
|
&& Number.isSafeInteger(store.dimensions) && store.dimensions > 0
|
|
@@ -146,9 +185,10 @@ export function runProtectedCorpusSeed({
|
|
|
146
185
|
const generatorFile = path.join(root, 'scripts/corpus-candidate.mjs');
|
|
147
186
|
const generatorValid = fs.existsSync(generatorFile)
|
|
148
187
|
&& receipt.generator?.corpusCandidateSha256 === sha256File(generatorFile);
|
|
149
|
-
if (receipt.schemaVersion !==
|
|
150
|
-
|| !receipt.createdAt || !
|
|
188
|
+
if (receipt.schemaVersion !== 3 || receipt.kind !== 'ruvnet-brain-corpus-candidate'
|
|
189
|
+
|| !receipt.createdAt || !storeBindingsValid || !emptyFailureArrays
|
|
151
190
|
|| !privateExclusionsValid || !boundaryIdentities.every(exactFileIdentity) || !exactFileIdentity(receipt.archive)
|
|
191
|
+
|| !exactFileIdentity(receipt.accuracyReport)
|
|
152
192
|
|| !generatorValid
|
|
153
193
|
|| receipt.archive.file !== path.basename(bundleFile)) {
|
|
154
194
|
corpusFailure('corpus receipt bindings are incomplete or invalid');
|
|
@@ -160,6 +200,98 @@ export function runProtectedCorpusSeed({
|
|
|
160
200
|
}
|
|
161
201
|
if (digestMatch[1] !== archiveSha256) corpusFailure('corpus tag digest does not match the receipt and archive');
|
|
162
202
|
|
|
203
|
+
// ADR-086 Step 15's second binding. The detached accuracy report travels beside the archive; this
|
|
204
|
+
// proves (a) the file the receipt names is the file present here, byte for byte, (b) the report
|
|
205
|
+
// was measured against THESE archive bytes, (c) every partition in both query modes passed
|
|
206
|
+
// 20x>=19x with no timeouts and no bounded sampling, and (d) it was produced by the committed
|
|
207
|
+
// benchmark against the committed oracle — so a swapped oracle or a patched benchmark is caught
|
|
208
|
+
// here even though the receipt itself carries only {file, sha256, bytes}.
|
|
209
|
+
const accuracyReportFile = `${bundleFile}.accuracy.json`;
|
|
210
|
+
if (receipt.accuracyReport.file !== path.basename(accuracyReportFile)) {
|
|
211
|
+
corpusFailure('corpus receipt names an accuracy report that is not the one beside this archive');
|
|
212
|
+
}
|
|
213
|
+
if (!fs.existsSync(accuracyReportFile) || !fs.statSync(accuracyReportFile).isFile()) {
|
|
214
|
+
corpusFailure(`detached retrieval-accuracy report missing beside the archive (${path.basename(accuracyReportFile)})`);
|
|
215
|
+
}
|
|
216
|
+
if (sha256File(accuracyReportFile) !== receipt.accuracyReport.sha256
|
|
217
|
+
|| fs.statSync(accuracyReportFile).size !== receipt.accuracyReport.bytes) {
|
|
218
|
+
corpusFailure('detached retrieval-accuracy report bytes do not match the corpus receipt');
|
|
219
|
+
}
|
|
220
|
+
const committedOracleFile = path.join(root, 'data/retrieval-accuracy-oracle.json');
|
|
221
|
+
const accuracyGeneratorFile = path.join(root, 'scripts/oracle/retrieval-accuracy.mjs');
|
|
222
|
+
if (!fs.existsSync(committedOracleFile)) corpusFailure('committed retrieval-accuracy oracle is missing from the release checkout');
|
|
223
|
+
if (!fs.existsSync(accuracyGeneratorFile)) corpusFailure('committed retrieval-accuracy benchmark is missing from the release checkout');
|
|
224
|
+
// The BLOCKING retrieval predicate at publication is the frozen-fixture repo-recall gate, read
|
|
225
|
+
// through the same module candidate acceptance used so the two can never drift apart. ADR-086's
|
|
226
|
+
// C3 report still has to exist and still has to be bound to these exact archive bytes — an
|
|
227
|
+
// unbound diagnostic looks like evidence and is worse than none — but its score no longer refuses
|
|
228
|
+
// publication. That reduction is declared in docs/adr/0086 and in the published report itself.
|
|
229
|
+
const archiveIdentity = { file: receipt.archive.file, sha256: archiveSha256, bytes: fs.statSync(bundleFile).size };
|
|
230
|
+
try {
|
|
231
|
+
readDiagnosticAccuracyReport({
|
|
232
|
+
reportFile: accuracyReportFile,
|
|
233
|
+
archive: archiveIdentity,
|
|
234
|
+
expectedOracleSha256: sha256File(committedOracleFile),
|
|
235
|
+
expectedGeneratorSha256: sha256File(accuracyGeneratorFile),
|
|
236
|
+
});
|
|
237
|
+
} catch (error) {
|
|
238
|
+
corpusFailure(`the published C3 diagnostic is not bound to this archive (${error.message})`);
|
|
239
|
+
}
|
|
240
|
+
const recallReportFile = `${bundleFile}.recall.json`;
|
|
241
|
+
if (!fs.existsSync(recallReportFile)) {
|
|
242
|
+
corpusFailure(`detached repo-recall report missing beside the archive (${path.basename(recallReportFile)})`);
|
|
243
|
+
}
|
|
244
|
+
if (!receipt.recallReport
|
|
245
|
+
|| sha256File(recallReportFile) !== receipt.recallReport.sha256
|
|
246
|
+
|| fs.statSync(recallReportFile).size !== receipt.recallReport.bytes) {
|
|
247
|
+
corpusFailure('detached repo-recall report bytes do not match the corpus receipt');
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
readRecallReport({
|
|
251
|
+
reportFile: recallReportFile,
|
|
252
|
+
archive: archiveIdentity,
|
|
253
|
+
expectedFixtureSha256: loadFixture().fixtureSha256,
|
|
254
|
+
});
|
|
255
|
+
} catch (error) {
|
|
256
|
+
corpusFailure(`retrieval does not qualify this corpus for publication (${error.message})`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Deep re-verification — moved here 2026-09-13 from the deleted scripts/corpus-seed-publish.mjs
|
|
260
|
+
// (ADR-085). Everything above proves the receipt is well-FORMED and that the archive's outer
|
|
261
|
+
// digest matches it; none of it proves the receipt is TRUE. verifyCorpusReceipt re-extracts the
|
|
262
|
+
// sealed archive and re-derives the entire candidate from its own bytes — per-store file digests,
|
|
263
|
+
// private-store fence, generation ledger, RVF index audit — and requires canonical equality with
|
|
264
|
+
// the receipt. A receipt with a single forged store digest passes every check above and fails
|
|
265
|
+
// here. It runs before any `gh` call so an untrue candidate never reaches the network.
|
|
266
|
+
try {
|
|
267
|
+
await verifyCorpusReceipt({
|
|
268
|
+
receiptFile, bundleFile, accuracyReportFile, recallReportFile, expectedBuilderSha: target, expectedArchiveSha256: archiveSha256,
|
|
269
|
+
});
|
|
270
|
+
} catch (error) {
|
|
271
|
+
corpusFailure(`corpus receipt does not verify against the sealed archive (${error.message})`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// EVERY local proof happens before the first network call. `gh` must never be reached by a
|
|
275
|
+
// candidate that is already known to be unpublishable — that is the same discipline the deep
|
|
276
|
+
// verifyCorpusReceipt above follows, and a customer release with an unusable signature is exactly
|
|
277
|
+
// as unpublishable as an untrue receipt.
|
|
278
|
+
const signatureFile = `${bundleFile}.sig`;
|
|
279
|
+
const digestFile = `${bundleFile}.sha256`;
|
|
280
|
+
const generation = String(receipt.createdAt || '');
|
|
281
|
+
if (promoteLatest) {
|
|
282
|
+
for (const [label, file] of [['detached signature', signatureFile], ['sha256 sidecar', digestFile]]) {
|
|
283
|
+
if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
|
|
284
|
+
corpusFailure(`customer corpus promotion requires a ${label} beside the archive (${path.basename(file)} missing) — the updater fails closed without it`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
// The real verifier, against the trust root that ships inside the npm package. Signing happens in
|
|
288
|
+
// the workflow with the environment-scoped key; this proves the bytes about to be published
|
|
289
|
+
// verify with the key kb/forge-update.mjs actually carries.
|
|
290
|
+
const signature = verifyBundle(bundleFile, signatureFile);
|
|
291
|
+
if (!signature.ok) corpusFailure(`detached signature does not verify against the shipped trust root (${signature.reason})`);
|
|
292
|
+
if (!Number.isFinite(Date.parse(generation))) corpusFailure('corpus receipt createdAt is not a readable generation timestamp');
|
|
293
|
+
}
|
|
294
|
+
|
|
163
295
|
const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
|
|
164
296
|
const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
|
|
165
297
|
const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
|
|
@@ -168,33 +300,136 @@ export function runProtectedCorpusSeed({
|
|
|
168
300
|
const viewError = String(view.error?.message || view.stderr || view.stdout || '');
|
|
169
301
|
if (!/(release not found|no release found)/i.test(viewError)) corpusFailure(`cannot prove ${tag} is absent (${viewError.trim() || `gh exited ${view.status}`})`);
|
|
170
302
|
|
|
303
|
+
const receiptSha256 = sha256File(receiptFile);
|
|
304
|
+
const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
305
|
+
|
|
306
|
+
if (!promoteLatest) {
|
|
307
|
+
// BOOTSTRAP/RECOVERY seeds stay exactly as ADR-086's original contract left them: an immutable
|
|
308
|
+
// prerelease that never touches releases/latest. Dual's C4 resolution (S1) narrows the change to
|
|
309
|
+
// CUSTOMER releases — "Bootstrap-only releases may remain prereleases."
|
|
310
|
+
const notes = [
|
|
311
|
+
'Content-addressed RuvNet Brain corpus seed.',
|
|
312
|
+
`Archive SHA-256: ${archiveSha256}`,
|
|
313
|
+
`Receipt SHA-256: ${receiptSha256}`,
|
|
314
|
+
`Accuracy report SHA-256: ${receipt.accuracyReport.sha256}`,
|
|
315
|
+
`Recall report SHA-256: ${receipt.recallReport.sha256}`,
|
|
316
|
+
...recallNotes(receipt),
|
|
317
|
+
`Stores: ${receipt.storeCount}`,
|
|
318
|
+
`Builder source SHA: ${receipt.builderSourceSha}`,
|
|
319
|
+
'This published prerelease is immutable and must never be replaced.',
|
|
320
|
+
].join('\n');
|
|
321
|
+
// The detached accuracy report ships AS AN ASSET. Without it a downloader holds an archive it
|
|
322
|
+
// cannot re-verify — "reverify the downloaded final artifact against the measured identity"
|
|
323
|
+
// requires the measurement to travel with the artifact it measured.
|
|
324
|
+
const createArgs = [
|
|
325
|
+
'release', 'create', tag,
|
|
326
|
+
'--prerelease', '--latest=false',
|
|
327
|
+
'--target', target,
|
|
328
|
+
'--repo', repo,
|
|
329
|
+
'--title', `Immutable corpus seed ${archiveSha256.slice(0, 16)}`,
|
|
330
|
+
'--notes', notes,
|
|
331
|
+
bundleFile, receiptFile, accuracyReportFile, recallReportFile,
|
|
332
|
+
];
|
|
333
|
+
const create = gh(createArgs);
|
|
334
|
+
if (create.error || create.status !== 0) {
|
|
335
|
+
corpusFailure(`protected corpus publication failed (${String(create.error?.message || create.stderr || create.stdout || '').trim()})`);
|
|
336
|
+
}
|
|
337
|
+
return { tag, target, repository: repo, archiveSha256, receiptSha256, promoted: false };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── CUSTOMER CORPUS RELEASE (ADR-086 step 17 / C4 resolution S1) ───────────────────────────────
|
|
341
|
+
// The old path was invisible AND unusable to a customer, for two independent reasons, and fixing
|
|
342
|
+
// only one leaves the channel dead. `--prerelease --latest=false` means kb/forge-update.mjs's
|
|
343
|
+
// releases/latest poll never sees it; and with no detached .sig the updater fails closed anyway
|
|
344
|
+
// (kb/forge-update.mjs:1275 fetches `${url}.sig`, :1284-1285 exits 4 when verification fails).
|
|
345
|
+
// scripts/verify-channels.mjs checks exactly these two things (checks 3 and 4) against the live
|
|
346
|
+
// endpoints, and is the owner's post-publish acceptance gate.
|
|
347
|
+
const latestView = gh(['release', 'view', '--json', 'tagName,body', '--repo', repo]);
|
|
348
|
+
let currentLatest = null;
|
|
349
|
+
if (!latestView.error && latestView.status === 0) {
|
|
350
|
+
try { currentLatest = JSON.parse(String(latestView.stdout || 'null')); }
|
|
351
|
+
catch (error) { corpusFailure(`cannot read the current latest release (${error.message})`); }
|
|
352
|
+
if (!currentLatest || typeof currentLatest.tagName !== 'string') corpusFailure('current latest release carries no tag name');
|
|
353
|
+
} else {
|
|
354
|
+
const latestError = String(latestView.error?.message || latestView.stderr || latestView.stdout || '');
|
|
355
|
+
if (!/(release not found|no release found)/i.test(latestError)) {
|
|
356
|
+
corpusFailure(`cannot determine the current latest release (${latestError.trim() || `gh exited ${latestView.status}`})`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const promotion = evaluateCorpusPromotion({ tag, generation, currentLatest });
|
|
360
|
+
if (!promotion.allowed) corpusFailure(promotion.reason);
|
|
361
|
+
|
|
171
362
|
const notes = [
|
|
172
|
-
'
|
|
363
|
+
'RuvNet Brain corpus generation — signed, content-addressed, and promoted to latest.',
|
|
364
|
+
`${CORPUS_GENERATION_FIELD} ${generation}`,
|
|
173
365
|
`Archive SHA-256: ${archiveSha256}`,
|
|
174
|
-
`Receipt SHA-256: ${
|
|
366
|
+
`Receipt SHA-256: ${receiptSha256}`,
|
|
175
367
|
`Stores: ${receipt.storeCount}`,
|
|
176
|
-
`
|
|
177
|
-
|
|
368
|
+
`Builder source SHA: ${receipt.builderSourceSha}`,
|
|
369
|
+
`Shipped runtime: ${receipt.archiveManifestReleaseTag}`,
|
|
370
|
+
...recallNotes(receipt),
|
|
371
|
+
'Immutable: this tag is the archive digest and must never be replaced.',
|
|
178
372
|
].join('\n');
|
|
179
|
-
|
|
373
|
+
|
|
374
|
+
// ASSETS COMPLETE BEFORE PROMOTION. `gh release create` uploads assets AFTER the release exists, so
|
|
375
|
+
// creating a non-draft release directly opens a window in which releases/latest resolves to a
|
|
376
|
+
// release with no archive — every polling client in that window fails or, worse, half-downloads.
|
|
377
|
+
// Create as a draft (invisible to releases/latest), prove all four assets landed, and only then
|
|
378
|
+
// flip draft off and claim latest in one edit.
|
|
379
|
+
// Both reports ride with every corpus release for the same reason they ride with a seed: a customer
|
|
380
|
+
// (or the next night's dispatcher) that downloads the archive must be able to reverify it against
|
|
381
|
+
// the identity it was actually measured under — the blocking recall gate AND the C3 diagnostic it
|
|
382
|
+
// scored 59.0% on, so nobody has to take either number on trust.
|
|
383
|
+
const assetFiles = [bundleFile, signatureFile, digestFile, receiptFile, accuracyReportFile, recallReportFile];
|
|
384
|
+
const create = gh([
|
|
180
385
|
'release', 'create', tag,
|
|
181
|
-
'--
|
|
386
|
+
'--draft',
|
|
182
387
|
'--target', target,
|
|
183
388
|
'--repo', repo,
|
|
184
|
-
'--title', `
|
|
389
|
+
'--title', `RuvNet Brain corpus ${archiveSha256.slice(0, 16)}`,
|
|
185
390
|
'--notes', notes,
|
|
186
|
-
|
|
187
|
-
];
|
|
188
|
-
const create = run(ghCommand, [...ghPrefix, ...createArgs], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
391
|
+
...assetFiles,
|
|
392
|
+
]);
|
|
189
393
|
if (create.error || create.status !== 0) {
|
|
190
394
|
corpusFailure(`protected corpus publication failed (${String(create.error?.message || create.stderr || create.stdout || '').trim()})`);
|
|
191
395
|
}
|
|
192
|
-
|
|
396
|
+
|
|
397
|
+
const expectedAssets = assetFiles.map((file) => path.basename(file)).sort();
|
|
398
|
+
const draftView = gh(['release', 'view', tag, '--json', 'isDraft,assets', '--repo', repo]);
|
|
399
|
+
if (draftView.error || draftView.status !== 0) corpusFailure('cannot confirm the draft corpus release before promotion');
|
|
400
|
+
let draft;
|
|
401
|
+
try { draft = JSON.parse(String(draftView.stdout || 'null')); }
|
|
402
|
+
catch (error) { corpusFailure(`cannot read the draft corpus release (${error.message})`); }
|
|
403
|
+
const uploaded = (draft?.assets || []).filter((asset) => asset?.state === 'uploaded' && Number.isSafeInteger(asset.size) && asset.size > 0);
|
|
404
|
+
if (draft?.isDraft !== true || JSON.stringify(uploaded.map((asset) => asset.name).sort()) !== JSON.stringify(expectedAssets)) {
|
|
405
|
+
corpusFailure(`refusing to promote an incomplete corpus release; expected ${expectedAssets.join(', ')} fully uploaded on a draft`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const promote = gh(['release', 'edit', tag, '--repo', repo, '--draft=false', '--latest', '--prerelease=false']);
|
|
409
|
+
if (promote.error || promote.status !== 0) {
|
|
410
|
+
corpusFailure(`corpus promotion to latest failed (${String(promote.error?.message || promote.stderr || promote.stdout || '').trim()})`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isLatest,isPrerelease,assets', '--repo', repo]);
|
|
414
|
+
if (finalView.error || finalView.status !== 0) corpusFailure('cannot confirm the promoted corpus release');
|
|
415
|
+
let promoted;
|
|
416
|
+
try { promoted = JSON.parse(String(finalView.stdout || 'null')); }
|
|
417
|
+
catch (error) { corpusFailure(`cannot read the promoted corpus release (${error.message})`); }
|
|
418
|
+
const promotedAssets = (promoted?.assets || []).map((asset) => asset?.name).sort();
|
|
419
|
+
if (promoted?.tagName !== tag || promoted.isDraft !== false || promoted.isLatest !== true
|
|
420
|
+
|| promoted.isPrerelease !== false || JSON.stringify(promotedAssets) !== JSON.stringify(expectedAssets)) {
|
|
421
|
+
corpusFailure('corpus release did not reach a complete, non-draft, non-prerelease latest state');
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
tag, target, repository: repo, archiveSha256, receiptSha256, promoted: true,
|
|
426
|
+
generation, supersededLatest: currentLatest?.tagName || null,
|
|
427
|
+
};
|
|
193
428
|
}
|
|
194
429
|
|
|
195
430
|
if (CORPUS_SEED) {
|
|
196
431
|
try {
|
|
197
|
-
const result = runProtectedCorpusSeed();
|
|
432
|
+
const result = await runProtectedCorpusSeed();
|
|
198
433
|
console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
|
|
199
434
|
} catch (error) {
|
|
200
435
|
console.error(error.message);
|
|
@@ -169,6 +169,72 @@ export function validateRetrievalQueryEvidence(evidence) {
|
|
|
169
169
|
return evidence;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// An exemption is a RECORDED, EVIDENCED admission that one eligible store's upstream bytes cannot
|
|
173
|
+
// support a source-grounded question — never a bypass. It satisfies the inventory obligation only;
|
|
174
|
+
// buildRetrievalCanaryPlan still demands a row for every eligible store, so an exempted store must
|
|
175
|
+
// also leave the eligible corpus before a canary plan can be sealed. Recording one is a coverage
|
|
176
|
+
// policy decision that someone has to make in the open, which is the entire point.
|
|
177
|
+
export function validateOracleExemptions(exemptions) {
|
|
178
|
+
if (exemptions === null || exemptions === undefined) return [];
|
|
179
|
+
if (canonicalJson(Object.keys(exemptions || {}).sort()) !== canonicalJson(['exemptions', 'kind', 'schemaVersion'])
|
|
180
|
+
|| exemptions.schemaVersion !== 1 || exemptions.kind !== 'ruvnet-brain-retrieval-oracle-exemptions'
|
|
181
|
+
|| !Array.isArray(exemptions.exemptions)) {
|
|
182
|
+
throw new Error('retrieval oracle exemptions are malformed');
|
|
183
|
+
}
|
|
184
|
+
for (const row of exemptions.exemptions) {
|
|
185
|
+
if (canonicalJson(Object.keys(row || {}).sort()) !== canonicalJson(['evidencePaths', 'reason', 'store', 'upstreamSha'])
|
|
186
|
+
|| typeof row.store !== 'string' || !/^[a-z0-9][a-z0-9._-]*$/.test(row.store)
|
|
187
|
+
|| typeof row.reason !== 'string' || row.reason.trim().length < 24
|
|
188
|
+
|| !HEX40.test(String(row.upstreamSha || ''))
|
|
189
|
+
|| !Array.isArray(row.evidencePaths) || row.evidencePaths.length < 1
|
|
190
|
+
|| row.evidencePaths.some((file) => typeof file !== 'string' || !file || path.isAbsolute(file)
|
|
191
|
+
|| file.split(/[\\/]/).includes('..'))) {
|
|
192
|
+
throw new Error(`retrieval oracle exemption for ${row?.store || '(missing)'} is malformed`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const stores = exemptions.exemptions.map((row) => row.store);
|
|
196
|
+
if (new Set(stores).size !== stores.length) throw new Error('retrieval oracle exemptions name a store twice');
|
|
197
|
+
return exemptions.exemptions;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// The cheap, standalone form of the denominator gate buildRetrievalCanaryPlan enforces at release
|
|
201
|
+
// time against a 567MB seed. Missing evidence is a FAILURE here, named store by store, seconds after
|
|
202
|
+
// a new repository lands in coverage — not six minutes into release-qe on a runner.
|
|
203
|
+
export function auditOracleCoverage({ coverage, queryEvidence, exemptions = null } = {}) {
|
|
204
|
+
const checked = validateCoverageLedger(coverage);
|
|
205
|
+
if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
|
|
206
|
+
validateRetrievalQueryEvidence(queryEvidence);
|
|
207
|
+
const eligibleRows = coverage.rows.filter((row) => row.kind === 'repository'
|
|
208
|
+
&& row.disposition === 'eligible' && row.status === 'CURRENT');
|
|
209
|
+
const eligible = ordered(eligibleRows.map(storeOf));
|
|
210
|
+
if (!eligible.length || new Set(eligible).size !== eligible.length || eligible.some((store) => !store)) {
|
|
211
|
+
throw new Error('eligible coverage denominator is invalid');
|
|
212
|
+
}
|
|
213
|
+
const oracle = new Set(Object.keys(queryEvidence.queries));
|
|
214
|
+
const exempted = validateOracleExemptions(exemptions);
|
|
215
|
+
const exemptStores = new Set(exempted.map((row) => row.store));
|
|
216
|
+
const uncovered = eligible.filter((store) => !oracle.has(store));
|
|
217
|
+
const missing = uncovered.filter((store) => !exemptStores.has(store));
|
|
218
|
+
const exempt = uncovered.filter((store) => exemptStores.has(store));
|
|
219
|
+
const extra = ordered([...oracle].filter((store) => !eligible.includes(store)));
|
|
220
|
+
const stale = ordered(exempted.map((row) => row.store).filter((store) => !uncovered.includes(store)));
|
|
221
|
+
const result = { eligible: eligible.length, covered: eligible.length - uncovered.length,
|
|
222
|
+
missing, exempt, extra, staleExemptions: stale };
|
|
223
|
+
if (missing.length) {
|
|
224
|
+
throw new Error(`independent retrieval oracle has no source-grounded row for ${missing.length} of `
|
|
225
|
+
+ `${eligible.length} eligible stores: ${missing.join(', ')}`);
|
|
226
|
+
}
|
|
227
|
+
if (extra.length) {
|
|
228
|
+
throw new Error(`independent retrieval oracle covers ${extra.length} store(s) outside the eligible `
|
|
229
|
+
+ `denominator: ${extra.join(', ')}`);
|
|
230
|
+
}
|
|
231
|
+
if (stale.length) {
|
|
232
|
+
throw new Error(`retrieval oracle exemption is stale for ${stale.join(', ')}; the store is covered `
|
|
233
|
+
+ 'or no longer eligible, so the recorded admission must be removed');
|
|
234
|
+
}
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
172
238
|
export function verifyQueryOracleSource(queryEvidence, candidateSourceSha, {
|
|
173
239
|
cwd = process.cwd(), run = spawnSync, allowSquashedSource = false,
|
|
174
240
|
} = {}) {
|
|
@@ -600,13 +666,34 @@ export function buildPlanFromFiles({ coverageFile, baselineFile, candidateFile,
|
|
|
600
666
|
return plan;
|
|
601
667
|
}
|
|
602
668
|
|
|
669
|
+
export function auditOracleCoverageFromFiles({ coverageFile, oracleFile, exemptionsFile = null,
|
|
670
|
+
candidateSourceSha = null, cwd = process.cwd() }) {
|
|
671
|
+
const coverage = JSON.parse(fs.readFileSync(regular(coverageFile, 'coverage'), 'utf8'));
|
|
672
|
+
const queryEvidence = JSON.parse(fs.readFileSync(regular(oracleFile, 'query oracle'), 'utf8'));
|
|
673
|
+
const exemptions = exemptionsFile
|
|
674
|
+
? JSON.parse(fs.readFileSync(regular(exemptionsFile, 'oracle exemptions'), 'utf8')) : null;
|
|
675
|
+
const inventory = auditOracleCoverage({ coverage, queryEvidence, exemptions });
|
|
676
|
+
// The second half of step 7's proof: the oracle's own commit must be a strict ancestor of the
|
|
677
|
+
// candidate that consumes it, and its tracked bytes must be identical at both.
|
|
678
|
+
const strictAncestor = candidateSourceSha
|
|
679
|
+
? Boolean(verifyQueryOracleSource(queryEvidence, candidateSourceSha, { cwd })) : null;
|
|
680
|
+
return { ...inventory, oracleSourceCommit: queryEvidence.sourceCommit, strictAncestor };
|
|
681
|
+
}
|
|
682
|
+
|
|
603
683
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
604
684
|
try {
|
|
605
685
|
const argv = process.argv.slice(2);
|
|
686
|
+
if (argv.includes('--audit')) {
|
|
687
|
+
const report = auditOracleCoverageFromFiles({ coverageFile: arg(argv, '--coverage'),
|
|
688
|
+
oracleFile: arg(argv, '--oracle'), exemptionsFile: arg(argv, '--exemptions'),
|
|
689
|
+
candidateSourceSha: arg(argv, '--candidate-sha'), cwd: arg(argv, '--repo') || process.cwd() });
|
|
690
|
+
console.log(JSON.stringify({ ok: true, ...report }));
|
|
691
|
+
} else {
|
|
606
692
|
const plan = buildPlanFromFiles({ coverageFile: arg(argv, '--coverage'), baselineFile: arg(argv, '--baseline'),
|
|
607
693
|
candidateFile: arg(argv, '--candidate'), oracleFile: arg(argv, '--oracle'), assetsDir: arg(argv, '--assets'),
|
|
608
694
|
outFile: arg(argv, '--out'), cwd: arg(argv, '--repo') || process.cwd() });
|
|
609
695
|
console.log(JSON.stringify({ ok: true, planSha256: plan.planSha256, cases: plan.cases.length }));
|
|
696
|
+
}
|
|
610
697
|
} catch (error) {
|
|
611
698
|
console.error(`[retrieval-canary] ${error.message}`);
|
|
612
699
|
process.exitCode = 1;
|