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
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Seal and independently verify the exact public corpus bytes used by a release.
|
|
3
|
+
//
|
|
4
|
+
// Schema 3 (2026-09-14, ADR-086 Step 15 / correction A6): the receipt additionally binds the
|
|
5
|
+
// DETACHED retrieval-accuracy report — `accuracyReport {file, sha256, bytes}` — that measured this
|
|
6
|
+
// exact archive through the customer query path. The report stays OUTSIDE the ZIP on purpose:
|
|
7
|
+
// inserting it into an already-measured archive would either change the archive's digest after it
|
|
8
|
+
// was measured or force a second assembly, and ADR-086 permits exactly one. Detached plus
|
|
9
|
+
// digest-bound preserves single assembly and exact-artifact identity at the same time.
|
|
10
|
+
//
|
|
11
|
+
// SCHEMA-2 CONSEQUENCE, stated plainly: a schema-2 seed carries no accuracy binding and is
|
|
12
|
+
// therefore UNPUBLISHABLE from this commit forward — verifyCorpusReceipt, runProtectedCorpusSeed
|
|
13
|
+
// and corpus-seed.yml's committed-seed loader all refuse it. data/corpus-seed.json must be
|
|
14
|
+
// re-pointed at a schema-3 seed in the same change that publishes one (ADR-086 Step 11's job).
|
|
15
|
+
//
|
|
16
|
+
// Schema 2 (2026-09-13): the receipt binds the FULL provenance/input closure that ships inside the
|
|
17
|
+
// sealed archive — ARCHIVE-MANIFEST.json, PRIVATE-STORES.json, RVF-GENERATIONS.json, SOURCE.json,
|
|
18
|
+
// the ruv-gists aggregate receipt, and every derived-store receipt (concepts, etc.) — instead of
|
|
19
|
+
// trusting a separate, unshipped assets/policy directory. Both createCorpusReceipt (writer) and
|
|
20
|
+
// verifyCorpusReceipt (reader) call the SAME deriveCorpusCandidate() so they can never drift from
|
|
21
|
+
// each other, and verification only ever trusts bytes actually extracted from the archive itself —
|
|
22
|
+
// there is no `--assets` escape hatch. The external, content-addressed corpus-sha256-<digest> tag is
|
|
23
|
+
// always derived OUTSIDE the archive from its own bytes; the archive's internal ARCHIVE-MANIFEST.json
|
|
24
|
+
// releaseTag/version is a completely separate identity domain and is never compared against it.
|
|
3
25
|
|
|
4
26
|
import crypto from 'node:crypto';
|
|
5
27
|
import fs from 'node:fs';
|
|
@@ -8,7 +30,10 @@ import path from 'node:path';
|
|
|
8
30
|
import { spawnSync } from 'node:child_process';
|
|
9
31
|
import { fileURLToPath } from 'node:url';
|
|
10
32
|
import { extractZip } from '../kb/zip-extract.mjs';
|
|
11
|
-
import {
|
|
33
|
+
import { canonicalJson, digest, fileIdentity, sha256File, validateGistAggregateReceipt } from '../plugin/scripts/coverage-integrity.mjs';
|
|
34
|
+
import { auditCorpusStores, isPassingState } from './rvf-index-audit.mjs';
|
|
35
|
+
import { readDiagnosticAccuracyReport } from './oracle/retrieval-accuracy.mjs';
|
|
36
|
+
import { loadFixture, readRecallReport } from './oracle/repo-recall.mjs';
|
|
12
37
|
|
|
13
38
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
14
39
|
const REQUIRED_SUFFIXES = [
|
|
@@ -19,6 +44,9 @@ const REQUIRED_SUFFIXES = [
|
|
|
19
44
|
'.meta.json',
|
|
20
45
|
];
|
|
21
46
|
const OPTIONAL_SHIPPED_SUFFIXES = ['.big.passages.jsonl', '.big.meta.json'];
|
|
47
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
48
|
+
const HEX_SOURCE = /^[a-f0-9]{40,64}$/i;
|
|
49
|
+
const HEX_COMMIT = /^[a-f0-9]{7,64}$/i;
|
|
22
50
|
|
|
23
51
|
function fail(message) {
|
|
24
52
|
throw new Error(`[corpus-candidate] ${message}`);
|
|
@@ -26,6 +54,8 @@ function fail(message) {
|
|
|
26
54
|
|
|
27
55
|
function readJson(file, label) {
|
|
28
56
|
if (!file || !fs.existsSync(file)) fail(`${label} missing (${file || 'no path supplied'})`);
|
|
57
|
+
const stat = fs.lstatSync(file);
|
|
58
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail(`${label} is not a trusted regular file`);
|
|
29
59
|
try {
|
|
30
60
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
31
61
|
} catch (error) {
|
|
@@ -33,214 +63,351 @@ function readJson(file, label) {
|
|
|
33
63
|
}
|
|
34
64
|
}
|
|
35
65
|
|
|
36
|
-
function
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
function findExactlyOne(root, name) {
|
|
67
|
+
const matches = [];
|
|
68
|
+
const visit = (dir) => {
|
|
69
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
70
|
+
const file = path.join(dir, entry.name);
|
|
71
|
+
if (entry.isSymbolicLink()) fail(`extracted archive contains a symbolic link: ${file}`);
|
|
72
|
+
if (entry.isDirectory()) visit(file);
|
|
73
|
+
else if (entry.isFile() && entry.name === name) matches.push(file);
|
|
44
74
|
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
return
|
|
75
|
+
};
|
|
76
|
+
visit(root);
|
|
77
|
+
if (matches.length !== 1) fail(`archive must contain exactly one ${name}; found ${matches.length}`);
|
|
78
|
+
return matches[0];
|
|
49
79
|
}
|
|
50
80
|
|
|
51
|
-
function
|
|
52
|
-
|
|
81
|
+
function filesUnder(root, prefix = '') {
|
|
82
|
+
const rows = [];
|
|
83
|
+
for (const entry of fs.readdirSync(path.join(root, prefix), { withFileTypes: true })
|
|
84
|
+
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
85
|
+
const relative = prefix ? path.join(prefix, entry.name) : entry.name;
|
|
86
|
+
const file = path.join(root, relative);
|
|
87
|
+
if (entry.isSymbolicLink()) fail(`archive tree contains a symbolic link: ${relative}`);
|
|
88
|
+
if (entry.isDirectory()) rows.push(...filesUnder(root, relative));
|
|
89
|
+
else if (entry.isFile()) rows.push({ path: relative.split(path.sep).join('/'), sha256: sha256File(file), bytes: fs.statSync(file).size });
|
|
90
|
+
else fail(`archive tree contains an unsupported entry: ${relative}`);
|
|
91
|
+
}
|
|
92
|
+
return rows;
|
|
53
93
|
}
|
|
54
94
|
|
|
55
|
-
function
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
95
|
+
function containedFile(root, relative, label) {
|
|
96
|
+
if (typeof relative !== 'string' || !relative || path.isAbsolute(relative) || relative.split(/[\\/]/).includes('..')) {
|
|
97
|
+
fail(`${label} path escapes the archive root`);
|
|
59
98
|
}
|
|
60
|
-
|
|
99
|
+
const resolved = path.resolve(root, relative);
|
|
100
|
+
const within = path.relative(root, resolved);
|
|
101
|
+
if (!within || within.startsWith('..') || path.isAbsolute(within)) fail(`${label} path escapes the archive root`);
|
|
102
|
+
if (!fs.existsSync(resolved)) fail(`${label} missing (${relative})`);
|
|
103
|
+
const stat = fs.lstatSync(resolved);
|
|
104
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail(`${label} is not a trusted regular file`);
|
|
105
|
+
return resolved;
|
|
61
106
|
}
|
|
62
107
|
|
|
63
|
-
function
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
108
|
+
function normalizeBootstrapIdentity(bootstrapIdentity) {
|
|
109
|
+
if (bootstrapIdentity === null || bootstrapIdentity === undefined) return null;
|
|
110
|
+
const { tag, sha256, privateFenceEvidence = null } = bootstrapIdentity;
|
|
111
|
+
if (typeof tag !== 'string' || !tag) fail('bootstrap identity tag is missing');
|
|
112
|
+
if (!HEX64.test(String(sha256 || '').toLowerCase())) fail('bootstrap identity sha256 is missing or malformed');
|
|
113
|
+
if (privateFenceEvidence !== null) {
|
|
114
|
+
if (typeof privateFenceEvidence.file !== 'string' || !privateFenceEvidence.file
|
|
115
|
+
|| !HEX64.test(String(privateFenceEvidence.sha256 || '')) || !Number.isSafeInteger(privateFenceEvidence.bytes)) {
|
|
116
|
+
fail('bootstrap private-fence evidence is malformed');
|
|
117
|
+
}
|
|
67
118
|
}
|
|
68
|
-
return
|
|
119
|
+
return { tag, sha256: sha256.toLowerCase(), privateFenceEvidence };
|
|
69
120
|
}
|
|
70
121
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
122
|
+
// The single derivation shared by the writer (createCorpusReceipt) and the reader
|
|
123
|
+
// (verifyCorpusReceipt): every field is either re-derived directly from bytes actually extracted
|
|
124
|
+
// from bundleFile, or is one of the three inputs that are not archive-derived (builderSourceSha,
|
|
125
|
+
// bootstrapIdentity, createdAt) — verification supplies those from the receipt being checked, so a
|
|
126
|
+
// receipt can never claim archive contents that were not really shipped.
|
|
127
|
+
async function deriveCorpusCandidate({ bundleFile, builderSourceSha, bootstrapIdentity, createdAt, accuracyReportFile, recallReportFile }) {
|
|
128
|
+
const bundle = path.resolve(bundleFile || '');
|
|
129
|
+
if (!fs.existsSync(bundle) || !fs.statSync(bundle).isFile()) fail(`bundle missing (${bundle || 'no path supplied'})`);
|
|
130
|
+
if (!HEX_SOURCE.test(builderSourceSha || '')) fail('builderSourceSha must be a 40-64 hex source identity');
|
|
131
|
+
const bootstrap = normalizeBootstrapIdentity(bootstrapIdentity);
|
|
132
|
+
const archive = fileIdentity(bundle);
|
|
133
|
+
|
|
134
|
+
// THE BLOCKING RETRIEVAL GATE (ADR-086 amendment, 2026-09-15). Both reports travel BESIDE the
|
|
135
|
+
// archive under fixed derived names, so any reader holding nothing but a downloaded release asset
|
|
136
|
+
// resolves the same files without the receipt carrying a path.
|
|
137
|
+
//
|
|
138
|
+
// What blocks: the frozen-fixture repo-recall gate. Every one of the 194 pre-existing human
|
|
139
|
+
// questions must complete without error, every repository must return content of its own, and the
|
|
140
|
+
// exact-file Hit@5 count may never fall below the accepted floor.
|
|
141
|
+
//
|
|
142
|
+
// What NO LONGER blocks: ADR-086's original C3 predicate. Measured against the real 4.3.25 archive
|
|
143
|
+
// it returns 680/1152 = 59.0% (diagnostic, c3Eligible:false) after both of its candidate
|
|
144
|
+
// explanations were tested and disproved. Holding publication on it shipped nothing. That swap is
|
|
145
|
+
// a DECLARED REDUCTION in release requirements, not a demonstration that C3 passed — so the C3
|
|
146
|
+
// report is still required to exist and still bound to these exact archive bytes, and is published
|
|
147
|
+
// alongside rather than quietly dropped.
|
|
148
|
+
const recall = readRecallReport({
|
|
149
|
+
reportFile: recallReportFile || `${bundle}.recall.json`,
|
|
150
|
+
archive,
|
|
151
|
+
expectedFixtureSha256: loadFixture().fixtureSha256,
|
|
152
|
+
});
|
|
153
|
+
const accuracyDiagnostic = readDiagnosticAccuracyReport({
|
|
154
|
+
reportFile: accuracyReportFile || `${bundle}.accuracy.json`,
|
|
155
|
+
archive,
|
|
156
|
+
});
|
|
81
157
|
|
|
82
|
-
async function verifyArchive({ bundleFile, stores, privateStores }) {
|
|
83
|
-
if (!bundleFile || !fs.existsSync(bundleFile)) fail(`bundle missing (${bundleFile || 'no path supplied'})`);
|
|
84
158
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'corpus-candidate-'));
|
|
85
159
|
try {
|
|
86
|
-
let extraction;
|
|
87
160
|
try {
|
|
88
|
-
|
|
161
|
+
await extractZip(bundle, tmp);
|
|
89
162
|
} catch (error) {
|
|
90
163
|
fail(`cannot extract archive (${error.message})`);
|
|
91
164
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
165
|
+
|
|
166
|
+
const manifestFile = findExactlyOne(tmp, 'ARCHIVE-MANIFEST.json');
|
|
167
|
+
const root = path.dirname(manifestFile);
|
|
168
|
+
const manifest = readJson(manifestFile, 'archive manifest');
|
|
169
|
+
if (manifest.schemaVersion !== 1 || manifest.kind !== 'ruvnet-brain-archive-manifest'
|
|
170
|
+
|| typeof manifest.version !== 'string' || !manifest.version
|
|
171
|
+
|| typeof manifest.releaseTag !== 'string' || !manifest.releaseTag
|
|
172
|
+
|| !Array.isArray(manifest.files)) fail('archive manifest schema is invalid');
|
|
173
|
+
|
|
174
|
+
const actualFiles = filesUnder(root).filter((row) => row.path !== 'ARCHIVE-MANIFEST.json');
|
|
175
|
+
const byPath = (left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
176
|
+
if (canonicalJson([...actualFiles].sort(byPath)) !== canonicalJson([...manifest.files].sort(byPath))) {
|
|
177
|
+
fail('archive file set or bytes differ from ARCHIVE-MANIFEST.json');
|
|
178
|
+
}
|
|
179
|
+
if (manifest.fileCount !== actualFiles.length
|
|
180
|
+
|| manifest.totalBytes !== actualFiles.reduce((total, row) => total + row.bytes, 0)) {
|
|
181
|
+
fail('archive manifest totals differ from its file rows');
|
|
99
182
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
183
|
+
|
|
184
|
+
const privateFenceFile = path.join(root, 'PRIVATE-STORES.json');
|
|
185
|
+
const fence = readJson(privateFenceFile, 'private fence');
|
|
186
|
+
if (!Array.isArray(fence.privateStores)) fail('private fence has no privateStores array');
|
|
187
|
+
const privateStores = [...new Set(fence.privateStores.map((name) => String(name).toLowerCase()))].sort();
|
|
188
|
+
const privateSet = new Set(privateStores);
|
|
189
|
+
|
|
190
|
+
const ledgerFile = path.join(root, 'RVF-GENERATIONS.json');
|
|
191
|
+
const ledger = readJson(ledgerFile, 'RVF generation ledger');
|
|
192
|
+
if (!ledger.stores || typeof ledger.stores !== 'object' || Array.isArray(ledger.stores)) {
|
|
193
|
+
fail('RVF generation ledger has no stores object');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const sourceFile = path.join(root, 'SOURCE.json');
|
|
197
|
+
const source = readJson(sourceFile, 'source manifest');
|
|
198
|
+
if (!source.stores || typeof source.stores !== 'object' || Array.isArray(source.stores)) {
|
|
199
|
+
fail('source manifest has no stores object');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const rvfFiles = fs.readdirSync(root).filter((name) => /^.+\.big\.rvf$/.test(name)).sort();
|
|
203
|
+
const rvfByStore = new Map(rvfFiles.map((file) => [file.slice(0, -'.big.rvf'.length), file]));
|
|
204
|
+
const foldedStores = [...rvfByStore.keys()].map((store) => store.toLowerCase());
|
|
205
|
+
if (new Set(foldedStores).size !== foldedStores.length) fail('archive RVF store names have case-fold aliases');
|
|
206
|
+
|
|
207
|
+
const leaked = [...rvfByStore.keys()].filter((store) => privateSet.has(store.toLowerCase()));
|
|
208
|
+
if (leaked.length) fail(`private store(s) present in archive: ${leaked.join(', ')}`);
|
|
209
|
+
|
|
210
|
+
const unreceipted = [...rvfByStore.keys()].filter((store) => !ledger.stores[store]);
|
|
211
|
+
if (unreceipted.length) fail(`unreceipted RVF files: ${unreceipted.join(', ')}`);
|
|
212
|
+
const orphanLedger = Object.entries(ledger.stores)
|
|
213
|
+
.filter(([store, generation]) => generation.file !== `${store}.big.rvf` || !rvfByStore.has(store))
|
|
214
|
+
.map(([store]) => store);
|
|
215
|
+
if (orphanLedger.length) fail(`ledger rows without RVFs: ${orphanLedger.join(', ')}`);
|
|
216
|
+
|
|
217
|
+
const digestOwners = new Map();
|
|
218
|
+
for (const [store, file] of rvfByStore) {
|
|
219
|
+
const rvfDigest = sha256File(path.join(root, file));
|
|
220
|
+
const owners = digestOwners.get(rvfDigest) || [];
|
|
221
|
+
owners.push(store);
|
|
222
|
+
digestOwners.set(rvfDigest, owners);
|
|
223
|
+
}
|
|
224
|
+
const duplicateRvfDigests = [...digestOwners.entries()]
|
|
225
|
+
.filter(([, owners]) => owners.length > 1)
|
|
226
|
+
.map(([sha256, owners]) => ({ sha256, stores: owners.sort() }));
|
|
227
|
+
if (duplicateRvfDigests.length) fail(`duplicate RVF bytes: ${duplicateRvfDigests.map((row) => row.stores.join('/')).join(', ')}`);
|
|
228
|
+
|
|
229
|
+
// Derived-store receipts (concepts, etc.) — the "concepts inputs" closure. Optional: a fresh
|
|
230
|
+
// bootstrap round may not have built any derived store yet.
|
|
231
|
+
const derivedStores = [];
|
|
232
|
+
const derivedNames = new Set();
|
|
233
|
+
const classesFile = path.join(root, 'public-store-classes.json');
|
|
234
|
+
if (fs.existsSync(classesFile)) {
|
|
235
|
+
const classes = readJson(classesFile, 'public store classes');
|
|
236
|
+
if (classes.schemaVersion !== 1 || !Array.isArray(classes.derived)) fail('derived store classes are malformed');
|
|
237
|
+
for (const entry of classes.derived) {
|
|
238
|
+
const store = String(entry?.store || '').toLowerCase();
|
|
239
|
+
if (!store) fail('derived store class entry is missing a store name');
|
|
240
|
+
derivedNames.add(store);
|
|
241
|
+
const receiptFile = containedFile(root, entry.receipt, `derived ${store} receipt`);
|
|
242
|
+
const dReceipt = readJson(receiptFile, `derived ${store} receipt`);
|
|
243
|
+
const passagesFile = containedFile(root, `${store}.passages.jsonl`, `derived ${store} passages`);
|
|
244
|
+
if (dReceipt.schemaVersion !== 1 || dReceipt.kind !== 'ruvnet-brain-derived-store-receipt'
|
|
245
|
+
|| String(dReceipt.store || '').toLowerCase() !== store || !Array.isArray(dReceipt.inputs) || !dReceipt.inputs.length
|
|
246
|
+
|| !HEX64.test(String(dReceipt.passagesSha256 || '')) || dReceipt.passagesSha256 !== sha256File(passagesFile)) {
|
|
247
|
+
fail(`derived ${store} receipt does not bind its passage bytes`);
|
|
248
|
+
}
|
|
249
|
+
for (const input of dReceipt.inputs) {
|
|
250
|
+
const inputFile = containedFile(root, input?.path, `derived ${store} input`);
|
|
251
|
+
if (!HEX64.test(String(input.sha256 || '')) || input.sha256 !== sha256File(inputFile)) {
|
|
252
|
+
fail(`derived ${store} input receipt differs from ${input?.path || '(missing)'}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
derivedStores.push({ store, receipt: fileIdentity(receiptFile) });
|
|
106
256
|
}
|
|
107
257
|
}
|
|
108
|
-
const selectedStores = new Map(stores.map(({ name }) => [name.toLowerCase(), name]));
|
|
109
|
-
const storeSuffixes = [...REQUIRED_SUFFIXES, ...OPTIONAL_SHIPPED_SUFFIXES]
|
|
110
|
-
.sort((left, right) => right.length - left.length);
|
|
111
|
-
const archiveFamilies = entries.map((entry) => path.posix.basename(entry.replaceAll('\\', '/')))
|
|
112
|
-
.map((name) => {
|
|
113
|
-
const suffix = storeSuffixes.find((candidate) => name.toLowerCase().endsWith(candidate));
|
|
114
|
-
return suffix ? { name, store: name.slice(0, -suffix.length) } : null;
|
|
115
|
-
}).filter(Boolean);
|
|
116
|
-
const unclassified = [...new Set(archiveFamilies.filter(({ store }) => !selectedStores.has(store.toLowerCase()))
|
|
117
|
-
.map(({ store }) => store))].sort();
|
|
118
|
-
if (unclassified.length) fail(`unclassified archive store(s): ${unclassified.join(', ')}`);
|
|
119
|
-
const aliases = archiveFamilies.filter(({ store }) => selectedStores.get(store.toLowerCase()) !== store)
|
|
120
|
-
.map(({ name }) => name).sort();
|
|
121
|
-
if (aliases.length) fail(`archive store-family alias differs by case: ${aliases.join(', ')}`);
|
|
122
|
-
} finally {
|
|
123
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
124
|
-
}
|
|
125
|
-
return fileIdentity(bundleFile);
|
|
126
|
-
}
|
|
127
258
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const fenceFile = path.join(assets, 'PRIVATE-STORES.json');
|
|
140
|
-
const fence = readJson(fenceFile, 'private fence');
|
|
141
|
-
if (!Array.isArray(fence.privateStores)) fail('private fence has no privateStores array');
|
|
142
|
-
const privateStores = [...new Set(fence.privateStores.map((name) => String(name).toLowerCase()))].sort();
|
|
143
|
-
const privateSet = new Set(privateStores);
|
|
144
|
-
const ledgerFile = path.join(assets, 'RVF-GENERATIONS.json');
|
|
145
|
-
const ledger = readJson(ledgerFile, 'RVF generation ledger');
|
|
146
|
-
if (!ledger.stores || typeof ledger.stores !== 'object' || Array.isArray(ledger.stores)) {
|
|
147
|
-
fail('RVF generation ledger has no stores object');
|
|
148
|
-
}
|
|
149
|
-
const policy = readJson(path.resolve(policyFile || ''), 'eligibility policy');
|
|
150
|
-
|
|
151
|
-
const rvfFiles = fs.readdirSync(assets).filter((name) => /^.+\.big\.rvf$/.test(name)).sort();
|
|
152
|
-
const rvfByStore = new Map(rvfFiles.map((file) => [file.slice(0, -'.big.rvf'.length), file]));
|
|
153
|
-
const unreceipted = [...rvfByStore.keys()].filter((store) => !ledger.stores[store]);
|
|
154
|
-
if (unreceipted.length) fail(`unreceipted RVF files: ${unreceipted.join(', ')}`);
|
|
155
|
-
const orphanLedger = Object.entries(ledger.stores)
|
|
156
|
-
.filter(([store, generation]) => generation.file !== `${store}.big.rvf` || !rvfByStore.has(store))
|
|
157
|
-
.map(([store]) => store);
|
|
158
|
-
if (orphanLedger.length) fail(`ledger rows without RVFs: ${orphanLedger.join(', ')}`);
|
|
159
|
-
|
|
160
|
-
const digestOwners = new Map();
|
|
161
|
-
for (const [store, file] of rvfByStore) {
|
|
162
|
-
const digest = sha256File(path.join(assets, file));
|
|
163
|
-
const owners = digestOwners.get(digest) || [];
|
|
164
|
-
owners.push(store);
|
|
165
|
-
digestOwners.set(digest, owners);
|
|
166
|
-
}
|
|
167
|
-
const duplicateRvfDigests = [...digestOwners.entries()]
|
|
168
|
-
.filter(([, owners]) => owners.length > 1)
|
|
169
|
-
.map(([sha256, owners]) => ({ sha256, stores: owners.sort() }));
|
|
170
|
-
if (duplicateRvfDigests.length) fail(`duplicate RVF bytes: ${duplicateRvfDigests.map((row) => row.stores.join('/')).join(', ')}`);
|
|
171
|
-
const publicInventory = validatePublicInventory({ assetsDir: assets, coverage: policy, ledger });
|
|
172
|
-
const publicSet = new Set(publicInventory.publicStores);
|
|
173
|
-
const counts = assertPolicyCurrent(policy);
|
|
174
|
-
|
|
175
|
-
const missingSidecars = [];
|
|
176
|
-
const stores = [];
|
|
177
|
-
for (const [store, rvfFile] of [...rvfByStore].sort(([a], [b]) => a.localeCompare(b))) {
|
|
178
|
-
const generation = ledger.stores[store];
|
|
179
|
-
const rvfPath = path.join(assets, rvfFile);
|
|
180
|
-
if (generation.file !== rvfFile || generation.sha256 !== sha256File(rvfPath) || generation.bytes !== fs.statSync(rvfPath).size) {
|
|
181
|
-
fail(`${store}: generation receipt does not match RVF bytes`);
|
|
259
|
+
// The ruv-gists aggregate — validated with the SAME reader every other consumer of this
|
|
260
|
+
// receipt schema uses (plugin/scripts/coverage-integrity.mjs), so the "individual source
|
|
261
|
+
// receipt digest, not the aggregate observation digest" contract lives in one place.
|
|
262
|
+
let gistAggregate = null;
|
|
263
|
+
if (rvfByStore.has('ruv-gists')) {
|
|
264
|
+
const gistReceiptFile = path.join(root, 'ruv-gists.sources.json');
|
|
265
|
+
const gistPassagesFile = path.join(root, 'ruv-gists.passages.jsonl');
|
|
266
|
+
if (!fs.existsSync(gistReceiptFile)) fail('ruv-gists store is present without ruv-gists.sources.json');
|
|
267
|
+
const gistReceipt = readJson(gistReceiptFile, 'gist aggregate receipt');
|
|
268
|
+
validateGistAggregateReceipt({ receipt: gistReceipt, passagesFile: gistPassagesFile, expectedIds: null });
|
|
269
|
+
gistAggregate = { name: 'ruv-gists', receipt: fileIdentity(gistReceiptFile) };
|
|
182
270
|
}
|
|
183
|
-
|
|
184
|
-
|
|
271
|
+
|
|
272
|
+
const missingSidecars = [];
|
|
273
|
+
const stores = [];
|
|
274
|
+
for (const [store, rvfFile] of [...rvfByStore].sort(([a], [b]) => a.localeCompare(b))) {
|
|
275
|
+
const generation = ledger.stores[store];
|
|
276
|
+
const rvfPath = path.join(root, rvfFile);
|
|
277
|
+
if (generation.file !== rvfFile || generation.sha256 !== sha256File(rvfPath) || generation.bytes !== fs.statSync(rvfPath).size) {
|
|
278
|
+
fail(`${store}: generation receipt does not match RVF bytes`);
|
|
279
|
+
}
|
|
280
|
+
if (typeof generation.builtUtc !== 'string' || !Number.isFinite(Date.parse(generation.builtUtc))
|
|
281
|
+
|| typeof generation.model !== 'string' || !generation.model || !Number.isInteger(generation.dimensions)
|
|
282
|
+
|| !(generation.sourceCommit === null || HEX_COMMIT.test(String(generation.sourceCommit || '')))) {
|
|
283
|
+
fail(`${store}: generation receipt lacks builtUtc, model, dimensions, or sourceCommit provenance`);
|
|
284
|
+
}
|
|
285
|
+
const folded = store.toLowerCase();
|
|
286
|
+
const kind = folded === 'ruv-gists' ? 'gist-aggregate' : derivedNames.has(folded) ? 'derived' : 'repository';
|
|
287
|
+
if (kind === 'repository') {
|
|
288
|
+
// The semantic cross-check: a repository store's ledger sourceCommit (the SELECTED
|
|
289
|
+
// generation) must equal the exact upstream SHA the worker that built it actually cloned
|
|
290
|
+
// and checked out (SOURCE.json) — never merely trusted from the ledger alone.
|
|
291
|
+
const sourceRow = source.stores[store];
|
|
292
|
+
const sourceCommit = String(sourceRow?.sourceCommit || '').toLowerCase();
|
|
293
|
+
const ledgerCommit = String(generation.sourceCommit || '').toLowerCase();
|
|
294
|
+
if (!sourceRow || !sourceCommit || sourceCommit !== ledgerCommit) {
|
|
295
|
+
fail(`${store}: SOURCE manifest sourceCommit does not equal the selected generation sourceCommit`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const files = [];
|
|
299
|
+
for (const suffix of REQUIRED_SUFFIXES) {
|
|
300
|
+
const file = path.join(root, `${store}${suffix}`);
|
|
301
|
+
if (!fs.existsSync(file)) missingSidecars.push(path.basename(file));
|
|
302
|
+
else files.push(fileIdentity(file));
|
|
303
|
+
}
|
|
304
|
+
for (const suffix of OPTIONAL_SHIPPED_SUFFIXES) {
|
|
305
|
+
const file = path.join(root, `${store}${suffix}`);
|
|
306
|
+
if (fs.existsSync(file)) files.push(fileIdentity(file));
|
|
307
|
+
}
|
|
308
|
+
stores.push({
|
|
309
|
+
name: store,
|
|
310
|
+
kind,
|
|
311
|
+
sourceCommit: generation.sourceCommit ?? null,
|
|
312
|
+
builtUtc: generation.builtUtc,
|
|
313
|
+
model: generation.model,
|
|
314
|
+
dimensions: generation.dimensions,
|
|
315
|
+
files,
|
|
316
|
+
});
|
|
185
317
|
}
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
318
|
+
if (missingSidecars.length) fail(`missing sidecars: ${missingSidecars.join(', ')}`);
|
|
319
|
+
if (!stores.length) fail('zero public corpus stores in the archive');
|
|
320
|
+
|
|
321
|
+
// C2 gate (scripts/rvf-index-audit.mjs auditCorpusStores): every shipped RVF is reopened read-only
|
|
322
|
+
// and must (1) carry a hash-verified persisted INDEX_SEG when it is large enough to need one,
|
|
323
|
+
// (2) answer persisted k-NN queries with measured recall@10 >= 0.95 against exact neighbours
|
|
324
|
+
// brute-forced from its own stored vectors, and (3) have an id map, passage rows and source
|
|
325
|
+
// mapping that correspond one-to-one. Segment presence alone is not accepted; stores under the
|
|
326
|
+
// engine's 1,024-vector HNSW threshold are reported as PASS-SMALL-STORE, never as PASS. The full
|
|
327
|
+
// measured result is written next to the archive as <bundle>.rvf-audit.json — NOT into this
|
|
328
|
+
// receipt: the receipt is schemaVersion 2 and any field addition is Step 15's coordinated bump.
|
|
329
|
+
let rvfAudit;
|
|
330
|
+
try {
|
|
331
|
+
rvfAudit = await auditCorpusStores({
|
|
332
|
+
dir: root,
|
|
333
|
+
rvfPaths: [...rvfByStore.values()].map((file) => path.join(root, file)),
|
|
334
|
+
privateStores: privateSet,
|
|
335
|
+
});
|
|
336
|
+
} catch (error) {
|
|
337
|
+
fail(`RVF index audit could not run (${error.message})`);
|
|
192
338
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
339
|
+
fs.writeFileSync(`${bundle}.rvf-audit.json`, `${JSON.stringify(rvfAudit, null, 2)}\n`);
|
|
340
|
+
const indexFailures = rvfAudit.stores.filter(({ state }) => !isPassingState(state));
|
|
341
|
+
if (indexFailures.length || rvfAudit.privateExclusion.leaks.length) {
|
|
342
|
+
fail(`RVF index audit failed: ${indexFailures
|
|
343
|
+
.map((row) => `${path.basename(row.path)} (vectors=${row.totalVectors ?? '?'}, recall@${row.recall?.kEffective ?? '?'}=${row.recall?.recallAtK ?? '?'}: ${[...new Set(row.failures.map((failure) => failure.kind))].join('+')})`)
|
|
344
|
+
.concat(rvfAudit.privateExclusion.leaks.map((name) => `${name} (private store present)`))
|
|
345
|
+
.join(', ')}`);
|
|
196
346
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
347
|
+
|
|
348
|
+
const finalByteFiles = stores.flatMap((store) => store.files.map((file) => ({ store: store.name, ...file })))
|
|
349
|
+
.sort((a, b) => a.file.localeCompare(b.file) || a.store.localeCompare(b.store));
|
|
350
|
+
const finalBytePartitionSha256 = digest({
|
|
351
|
+
archiveManifestSha256: digest(manifest),
|
|
352
|
+
privateFenceSha256: sha256File(privateFenceFile),
|
|
353
|
+
sourceManifestSha256: sha256File(sourceFile),
|
|
354
|
+
gistAggregate,
|
|
355
|
+
derivedStores,
|
|
356
|
+
files: finalByteFiles,
|
|
204
357
|
});
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
schemaVersion: 3,
|
|
361
|
+
kind: 'ruvnet-brain-corpus-candidate',
|
|
362
|
+
builderSourceSha: builderSourceSha.toLowerCase(),
|
|
363
|
+
createdAt,
|
|
364
|
+
archive,
|
|
365
|
+
archiveManifest: fileIdentity(manifestFile),
|
|
366
|
+
archiveManifestVersion: manifest.version,
|
|
367
|
+
archiveManifestReleaseTag: manifest.releaseTag,
|
|
368
|
+
fileCount: manifest.fileCount,
|
|
369
|
+
totalBytes: manifest.totalBytes,
|
|
370
|
+
privateFence: fileIdentity(privateFenceFile),
|
|
371
|
+
generationLedger: fileIdentity(ledgerFile),
|
|
372
|
+
sourceManifest: fileIdentity(sourceFile),
|
|
373
|
+
gistAggregate,
|
|
374
|
+
derivedStores,
|
|
375
|
+
storeCount: stores.length,
|
|
376
|
+
stores,
|
|
377
|
+
excludedPrivateStores: privateStores,
|
|
378
|
+
duplicateRvfDigests: [],
|
|
379
|
+
unreceiptedRvfFiles: [],
|
|
380
|
+
missingSidecars: [],
|
|
381
|
+
bootstrap,
|
|
382
|
+
finalBytePartitionSha256,
|
|
383
|
+
// A6, verbatim: "bump the corpus receipt to schemaVersion 3 with accuracyReport
|
|
384
|
+
// {file, sha256, bytes}". Exactly those three fields — the oracle and generator identities
|
|
385
|
+
// the publication gate needs are reached THROUGH this digest, by re-reading the bound report.
|
|
386
|
+
// The field keeps its name and shape so existing seed readers are unaffected; what changed is
|
|
387
|
+
// that this report is now a published DIAGNOSTIC, and `recallReport` is the blocking one.
|
|
388
|
+
accuracyReport: accuracyDiagnostic.identity,
|
|
389
|
+
accuracyDiagnostic: {
|
|
390
|
+
state: accuracyDiagnostic.state,
|
|
391
|
+
classification: accuracyDiagnostic.classification,
|
|
392
|
+
c3Eligible: accuracyDiagnostic.c3Eligible,
|
|
393
|
+
blocking: false,
|
|
394
|
+
note: 'ADR-086 C3 was NOT met and is NOT demonstrated by this release. Published for inspection.',
|
|
395
|
+
},
|
|
396
|
+
recallReport: recall.identity,
|
|
397
|
+
recallSummary: {
|
|
398
|
+
fixtureSha256: recall.report.fixture.sha256,
|
|
399
|
+
questions: recall.report.totals.questions,
|
|
400
|
+
repositoriesAnswering: recall.report.totals.repoCoverage,
|
|
401
|
+
exactFileTop1: recall.report.totals.hitTop1,
|
|
402
|
+
exactFileTop5: recall.report.totals.hitTop5,
|
|
403
|
+
floor: recall.report.floor.value,
|
|
404
|
+
blocking: true,
|
|
405
|
+
},
|
|
406
|
+
generator: { corpusCandidateSha256: sha256File(fileURLToPath(import.meta.url)) },
|
|
407
|
+
};
|
|
408
|
+
} finally {
|
|
409
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
205
410
|
}
|
|
206
|
-
if (missingSidecars.length) fail(`missing sidecars: ${missingSidecars.join(', ')}`);
|
|
207
|
-
if (!stores.length) fail('zero public corpus stores remain after the private fence');
|
|
208
|
-
const finalByteFiles = stores.flatMap((store) => store.files.map((file) => ({ store: store.name, ...file })))
|
|
209
|
-
.sort((a, b) => a.file.localeCompare(b.file) || a.store.localeCompare(b.store));
|
|
210
|
-
const finalBytePartitionSha256 = crypto.createHash('sha256').update(canonicalJson({
|
|
211
|
-
publicInventoryPartitionSha256: publicInventory.partitionSha256,
|
|
212
|
-
publicStores: publicInventory.publicStores,
|
|
213
|
-
files: finalByteFiles,
|
|
214
|
-
})).digest('hex');
|
|
215
|
-
const archive = await verifyArchive({ bundleFile: path.resolve(bundleFile || ''), stores, privateStores });
|
|
216
|
-
const policyIdentity = fileIdentity(path.resolve(policyFile));
|
|
217
|
-
const fenceIdentity = fileIdentity(fenceFile);
|
|
218
|
-
const ledgerIdentity = fileIdentity(ledgerFile);
|
|
219
|
-
const coverageGeneration = String(policy.coverageGeneration || policy.generation || policy.generated || policyIdentity.sha256);
|
|
220
|
-
|
|
221
|
-
return {
|
|
222
|
-
schemaVersion: 1,
|
|
223
|
-
kind: 'ruvnet-brain-corpus-candidate',
|
|
224
|
-
builderSourceSha: builderSourceSha.toLowerCase(),
|
|
225
|
-
createdAt,
|
|
226
|
-
coverageGeneration,
|
|
227
|
-
eligibleRepoCount: counts.eligibleRepoCount,
|
|
228
|
-
gistCount: counts.gistCount,
|
|
229
|
-
storeCount: stores.length,
|
|
230
|
-
stores,
|
|
231
|
-
privateFence: fenceIdentity,
|
|
232
|
-
eligibilityPolicy: policyIdentity,
|
|
233
|
-
generationLedger: ledgerIdentity,
|
|
234
|
-
publicInventory,
|
|
235
|
-
finalBytePartitionSha256,
|
|
236
|
-
excludedPrivateStores: privateStores.filter((privateStore) =>
|
|
237
|
-
[...rvfByStore.keys()].some((store) => store.toLowerCase() === privateStore)),
|
|
238
|
-
duplicateRvfDigests,
|
|
239
|
-
unreceiptedRvfFiles: [],
|
|
240
|
-
missingSidecars: [],
|
|
241
|
-
archive,
|
|
242
|
-
generator: { corpusCandidateSha256: sha256File(fileURLToPath(import.meta.url)) },
|
|
243
|
-
};
|
|
244
411
|
}
|
|
245
412
|
|
|
246
413
|
function currentGitSha() {
|
|
@@ -249,48 +416,94 @@ function currentGitSha() {
|
|
|
249
416
|
return result.stdout.trim();
|
|
250
417
|
}
|
|
251
418
|
|
|
252
|
-
export async function createCorpusReceipt(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
419
|
+
export async function createCorpusReceipt({
|
|
420
|
+
bundleFile, builderSourceSha, bootstrapIdentity = null, receiptFile, createdAt, accuracyReportFile = null, recallReportFile = null,
|
|
421
|
+
} = {}) {
|
|
422
|
+
const resolvedReceiptFile = path.resolve(receiptFile || 'dist/corpus-receipt.json');
|
|
423
|
+
const receipt = await deriveCorpusCandidate({
|
|
424
|
+
bundleFile,
|
|
425
|
+
builderSourceSha: builderSourceSha || currentGitSha(),
|
|
426
|
+
bootstrapIdentity,
|
|
427
|
+
createdAt: createdAt || new Date().toISOString(),
|
|
428
|
+
accuracyReportFile,
|
|
429
|
+
recallReportFile,
|
|
258
430
|
});
|
|
259
|
-
fs.mkdirSync(path.dirname(
|
|
260
|
-
fs.writeFileSync(
|
|
431
|
+
fs.mkdirSync(path.dirname(resolvedReceiptFile), { recursive: true });
|
|
432
|
+
fs.writeFileSync(resolvedReceiptFile, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
261
433
|
return receipt;
|
|
262
434
|
}
|
|
263
435
|
|
|
264
|
-
export async function verifyCorpusReceipt(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
: label === 'eligibility policy'
|
|
278
|
-
? receipt.eligibilityPolicy
|
|
279
|
-
: label === 'private fence'
|
|
280
|
-
? receipt.privateFence
|
|
281
|
-
: receipt.generationLedger;
|
|
282
|
-
if (sha256File(file) !== expected?.sha256) fail(`${label} sha256 differs from receipt`);
|
|
283
|
-
if (fs.statSync(file).size !== expected?.bytes) fail(`${label} byte length differs from receipt`);
|
|
436
|
+
export async function verifyCorpusReceipt({
|
|
437
|
+
bundleFile, receiptFile, expectedBuilderSha, expectedArchiveSha256, expectedReceiptSha256, accuracyReportFile = null, recallReportFile = null,
|
|
438
|
+
} = {}) {
|
|
439
|
+
const resolvedReceiptFile = path.resolve(receiptFile || '');
|
|
440
|
+
const resolvedBundleFile = path.resolve(bundleFile || '');
|
|
441
|
+
const receipt = readJson(resolvedReceiptFile, 'corpus receipt');
|
|
442
|
+
// Schema 3 only. A schema-2 receipt has no accuracyReport binding, so accepting one here would
|
|
443
|
+
// silently publish a corpus that was never measured — the exact hole A6 closes.
|
|
444
|
+
if (receipt.schemaVersion !== 3 || receipt.kind !== 'ruvnet-brain-corpus-candidate') {
|
|
445
|
+
fail('unsupported corpus receipt (schema downgrade or wrong kind)');
|
|
446
|
+
}
|
|
447
|
+
if (expectedReceiptSha256 != null && sha256File(resolvedReceiptFile) !== expectedReceiptSha256) {
|
|
448
|
+
fail('receipt file bytes differ from the expected receipt sha256');
|
|
284
449
|
}
|
|
285
|
-
|
|
286
|
-
|
|
450
|
+
if (expectedBuilderSha != null && receipt.builderSourceSha !== String(expectedBuilderSha).toLowerCase()) {
|
|
451
|
+
fail('receipt builderSourceSha differs from the expected builder SHA');
|
|
452
|
+
}
|
|
453
|
+
if (!fs.existsSync(resolvedBundleFile)) fail(`bundle missing (${resolvedBundleFile})`);
|
|
454
|
+
const actualArchiveSha256 = sha256File(resolvedBundleFile);
|
|
455
|
+
if (actualArchiveSha256 !== receipt.archive?.sha256 || fs.statSync(resolvedBundleFile).size !== receipt.archive?.bytes) {
|
|
456
|
+
fail('archive sha256 or byte length differs from the corpus receipt');
|
|
457
|
+
}
|
|
458
|
+
if (expectedArchiveSha256 != null && actualArchiveSha256 !== expectedArchiveSha256) {
|
|
459
|
+
fail('archive sha256 differs from the expected archive sha256');
|
|
460
|
+
}
|
|
461
|
+
const derived = await deriveCorpusCandidate({
|
|
462
|
+
bundleFile: resolvedBundleFile,
|
|
287
463
|
builderSourceSha: receipt.builderSourceSha,
|
|
464
|
+
bootstrapIdentity: receipt.bootstrap,
|
|
288
465
|
createdAt: receipt.createdAt,
|
|
466
|
+
accuracyReportFile,
|
|
467
|
+
recallReportFile,
|
|
289
468
|
});
|
|
290
|
-
if (canonicalJson(
|
|
469
|
+
if (canonicalJson(derived) !== canonicalJson(receipt)) fail('receipt does not match the exact corpus archive contents');
|
|
291
470
|
return receipt;
|
|
292
471
|
}
|
|
293
472
|
|
|
473
|
+
// Verify a downloaded seed archive against its EXTERNAL, content-addressed descriptor
|
|
474
|
+
// (corpus-sha256-<digest>) and its accompanying schema-2 candidate receipt. This never compares
|
|
475
|
+
// the external tag against the archive's own internal ARCHIVE-MANIFEST releaseTag/version — those
|
|
476
|
+
// are two independent identity domains and conflating them was the historical bug this fixes.
|
|
477
|
+
export async function verifySeedBaseline({ seedDescriptor, bundleFile, receiptFile, accuracyReportFile = null, recallReportFile = null } = {}) {
|
|
478
|
+
if (!seedDescriptor || typeof seedDescriptor !== 'object') fail('seed descriptor is required');
|
|
479
|
+
const { tag, sha256, bytes, sourceCommit = null, allowPinnedTag = false } = seedDescriptor;
|
|
480
|
+
const expectedSha256 = String(sha256 || '').toLowerCase();
|
|
481
|
+
if (!HEX64.test(expectedSha256)) fail('seed descriptor sha256 is missing or malformed');
|
|
482
|
+
if (!tag || tag === 'latest') fail('seed descriptor tag is missing or forbidden');
|
|
483
|
+
const contentAddressed = tag === `corpus-sha256-${expectedSha256}`;
|
|
484
|
+
if (!contentAddressed && !allowPinnedTag) {
|
|
485
|
+
fail('seed descriptor tag must be the exact digest-derived tag corpus-sha256-<sha256> unless explicitly pinned');
|
|
486
|
+
}
|
|
487
|
+
const resolvedBundleFile = path.resolve(bundleFile || '');
|
|
488
|
+
if (!fs.existsSync(resolvedBundleFile)) fail(`seed bundle missing (${resolvedBundleFile})`);
|
|
489
|
+
const actualSha256 = sha256File(resolvedBundleFile);
|
|
490
|
+
if (actualSha256 !== expectedSha256) fail(`seed bundle sha256 ${actualSha256} differs from configured ${expectedSha256}`);
|
|
491
|
+
if (Number.isSafeInteger(bytes) && fs.statSync(resolvedBundleFile).size !== bytes) {
|
|
492
|
+
fail('seed bundle byte length differs from the configured seed descriptor');
|
|
493
|
+
}
|
|
494
|
+
// "Reverify the downloaded final artifact against the measured identity" — a downloaded seed is
|
|
495
|
+
// re-measured against its own detached accuracy report, which must have travelled with it.
|
|
496
|
+
const receipt = await verifyCorpusReceipt({
|
|
497
|
+
bundleFile: resolvedBundleFile,
|
|
498
|
+
receiptFile,
|
|
499
|
+
accuracyReportFile,
|
|
500
|
+
recallReportFile,
|
|
501
|
+
expectedArchiveSha256: expectedSha256,
|
|
502
|
+
...(sourceCommit ? { expectedBuilderSha: sourceCommit } : {}),
|
|
503
|
+
});
|
|
504
|
+
return { tag, sha256: expectedSha256, bytes: fs.statSync(resolvedBundleFile).size, receipt };
|
|
505
|
+
}
|
|
506
|
+
|
|
294
507
|
function arg(name, fallback) {
|
|
295
508
|
const index = process.argv.indexOf(name);
|
|
296
509
|
return index >= 0 ? process.argv[index + 1] : fallback;
|
|
@@ -298,17 +511,57 @@ function arg(name, fallback) {
|
|
|
298
511
|
|
|
299
512
|
async function main() {
|
|
300
513
|
const mode = process.argv.includes('--verify') ? 'verify' : 'create';
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
514
|
+
const bundleFile = arg('--bundle', 'dist/ruvnet-brain.zip');
|
|
515
|
+
const receiptFile = arg('--receipt', arg('--out', 'dist/corpus-receipt.json'));
|
|
516
|
+
const accuracyReportFile = arg('--accuracy-report');
|
|
517
|
+
const recallReportFile = arg('--recall-report');
|
|
518
|
+
if (mode === 'create') {
|
|
519
|
+
const bootstrapTag = arg('--bootstrap-tag');
|
|
520
|
+
const bootstrapSha256 = arg('--bootstrap-sha256');
|
|
521
|
+
const bootstrapIdentity = bootstrapTag && bootstrapSha256 ? { tag: bootstrapTag, sha256: bootstrapSha256 } : null;
|
|
522
|
+
const receipt = await createCorpusReceipt({
|
|
523
|
+
bundleFile,
|
|
524
|
+
builderSourceSha: arg('--builder-source-sha'),
|
|
525
|
+
bootstrapIdentity,
|
|
526
|
+
receiptFile,
|
|
527
|
+
accuracyReportFile,
|
|
528
|
+
recallReportFile,
|
|
529
|
+
});
|
|
530
|
+
console.log(JSON.stringify({
|
|
531
|
+
ok: true, mode, archive: receipt.archive, stores: receipt.storeCount, accuracyReport: receipt.accuracyReport, recall: receipt.recallSummary,
|
|
532
|
+
}, null, 2));
|
|
533
|
+
} else {
|
|
534
|
+
const receipt = await verifyCorpusReceipt({
|
|
535
|
+
bundleFile,
|
|
536
|
+
receiptFile,
|
|
537
|
+
accuracyReportFile,
|
|
538
|
+
recallReportFile,
|
|
539
|
+
expectedBuilderSha: arg('--expected-builder-sha'),
|
|
540
|
+
expectedArchiveSha256: arg('--expected-archive-sha256'),
|
|
541
|
+
expectedReceiptSha256: arg('--expected-receipt-sha256'),
|
|
542
|
+
});
|
|
543
|
+
console.log(JSON.stringify({
|
|
544
|
+
ok: true, mode, archive: receipt.archive, stores: receipt.storeCount, accuracyReport: receipt.accuracyReport, recall: receipt.recallSummary,
|
|
545
|
+
}, null, 2));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// REALPATH BOTH SIDES, or this CLI silently no-ops. argv[1] is whatever the caller typed, symlinks
|
|
550
|
+
// and all, while node resolves a module URL THROUGH symlinks before it reaches import.meta.url — so a
|
|
551
|
+
// symlinked invocation compares a link path against a real path, decides it is not the entry point,
|
|
552
|
+
// runs nothing, and EXITS 0. On macOS every os.tmpdir() path is symlinked (/var/folders -> /private/
|
|
553
|
+
// var/folders), so any caller staging work in a temp directory hits this. Measured 2026-09-14:
|
|
554
|
+
// build-bundle.mjs and corpus-candidate.mjs both no-opped and prepareCorpusCandidate reported SUCCESS
|
|
555
|
+
// with no archive and no receipt on disk. Same defect, same fix as plugin/scripts/hook-input.mjs:518.
|
|
556
|
+
function isMain() {
|
|
557
|
+
try {
|
|
558
|
+
if (!process.argv[1]) return false;
|
|
559
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
|
|
560
|
+
} catch {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
310
563
|
}
|
|
311
564
|
|
|
312
|
-
if (
|
|
565
|
+
if (isMain()) {
|
|
313
566
|
main().catch((error) => { console.error(error.message); process.exitCode = 1; });
|
|
314
567
|
}
|