ruvnet-brain 4.2.2-dev → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +15 -14
  2. package/bin/install.mjs +11 -2
  3. package/console/architecture.html +12 -14
  4. package/console/install-mockup.html +3 -3
  5. package/kb/zip-extract.mjs +8 -2
  6. package/package.json +7 -1
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/hooks/codex-hooks.json +1 -1
  10. package/plugin/hooks/hooks.json +10 -0
  11. package/plugin/host-adapters/claude.json +11 -0
  12. package/plugin/host-adapters/codex.json +11 -0
  13. package/plugin/mcp/managed-cli-interface.mjs +77 -10
  14. package/plugin/mcp/server.mjs +2 -1
  15. package/plugin/scripts/anticipate.sh +30 -1
  16. package/plugin/scripts/capability-claim-evidence.mjs +431 -0
  17. package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
  18. package/plugin/scripts/capability-registry.mjs +6 -0
  19. package/plugin/scripts/capability-routing.mjs +70 -0
  20. package/plugin/scripts/continuation-gate.mjs +68 -2
  21. package/plugin/scripts/coverage-integrity.mjs +575 -0
  22. package/plugin/scripts/ground-ruvnet.sh +2 -2
  23. package/plugin/scripts/nightly-controller.mjs +19 -2
  24. package/plugin/scripts/project-progression-contract.mjs +409 -0
  25. package/plugin/scripts/project-progression-hook.mjs +196 -0
  26. package/plugin/scripts/project-progression-outbox.mjs +95 -0
  27. package/plugin/scripts/project-progression-session-start.mjs +183 -0
  28. package/plugin/scripts/project-progression-store.mjs +248 -0
  29. package/plugin/scripts/project-store-resolver.mjs +106 -0
  30. package/plugin/scripts/session-snapshot-hook.mjs +29 -1
  31. package/plugin/scripts/session-start-core.mjs +22 -18
  32. package/plugin/scripts/spend-guard.mjs +6 -1
  33. package/scripts/adr-072-completion.mjs +98 -0
  34. package/scripts/behavioral-l1-l4.mjs +11 -8
  35. package/scripts/brain-score.mjs +7 -2
  36. package/scripts/build-bundle.mjs +82 -6
  37. package/scripts/card-from-source.mjs +32 -6
  38. package/scripts/corpus-aggregates.mjs +194 -0
  39. package/scripts/corpus-candidate.mjs +56 -36
  40. package/scripts/corpus-reconcile.mjs +295 -28
  41. package/scripts/corpus-seed-publish.mjs +3 -3
  42. package/scripts/coverage-integrity.mjs +3 -0
  43. package/scripts/gist-receipts.mjs +183 -0
  44. package/scripts/git-hooks/pre-push +21 -0
  45. package/scripts/host-registry.mjs +118 -0
  46. package/scripts/independent-review-receipt.mjs +499 -0
  47. package/scripts/ingest-gists.mjs +34 -5
  48. package/scripts/ingest-new-repos.mjs +5 -0
  49. package/scripts/metaharness-gate.mjs +58 -0
  50. package/scripts/nightly-gists.sh +3 -3
  51. package/scripts/nightly-wrapper.sh +75 -69
  52. package/scripts/product-integrity-contract.mjs +156 -0
  53. package/scripts/public-inventory.mjs +4 -0
  54. package/scripts/public-verification-aggregate.mjs +342 -0
  55. package/scripts/public-verification-finalizer.mjs +59 -0
  56. package/scripts/public-verification-inputs.mjs +473 -0
  57. package/scripts/public-verification-lane.mjs +244 -0
  58. package/scripts/publication-receipt.mjs +50 -6
  59. package/scripts/rebuild-gists-from-receipts.mjs +50 -5
  60. package/scripts/release-projection.mjs +86 -0
  61. package/scripts/release-transaction-provider.mjs +43 -102
  62. package/scripts/release-transaction.mjs +139 -51
  63. package/scripts/release.mjs +15 -7
  64. package/scripts/restore-local-ingests.mjs +2 -2
  65. package/scripts/retrieval-canary.mjs +533 -0
  66. package/scripts/routing-flywheel.mjs +12 -1
  67. package/scripts/rvf-generation.mjs +9 -3
  68. package/scripts/self-update.mjs +19 -44
  69. package/scripts/source-coverage.mjs +415 -0
  70. package/scripts/source-scope-receipt.mjs +84 -0
  71. package/scripts/sync-census.mjs +0 -0
  72. package/scripts/wired-check.mjs +114 -24
  73. package/scripts/worktree-integrity.mjs +123 -0
@@ -0,0 +1,194 @@
1
+ // Rebuild the two source-derived public aggregate stores as one atomic artifact set.
2
+
3
+ import crypto from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
9
+ import { reconstructGists, writeReconstruction } from './rebuild-gists-from-receipts.mjs';
10
+ import { writeRvfGeneration } from './rvf-generation.mjs';
11
+
12
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
+ const HEX64 = /^[a-f0-9]{64}$/;
14
+ const MODEL = 'Xenova/bge-base-en-v1.5';
15
+ const DIMENSIONS = 768;
16
+ const AGGREGATES = ['ruv-gists', 'concepts'];
17
+
18
+ function fail(message) {
19
+ throw new Error(`[corpus-aggregates] ${message}`);
20
+ }
21
+
22
+ function readJson(file, label) {
23
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
24
+ catch (error) { fail(`${label} is missing or unreadable (${error.message})`); }
25
+ }
26
+
27
+ function sha256File(file) {
28
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
29
+ }
30
+
31
+ function chunks(text, size = 3200) {
32
+ const result = [];
33
+ let buffer = '';
34
+ for (const paragraph of String(text).split(/\n\n+/)) {
35
+ if (buffer && buffer.length + paragraph.length + 2 > size) {
36
+ result.push(buffer);
37
+ buffer = '';
38
+ }
39
+ buffer = buffer ? `${buffer}\n\n${paragraph}` : paragraph;
40
+ }
41
+ if (buffer.trim()) result.push(buffer);
42
+ return result;
43
+ }
44
+
45
+ export function buildConceptAggregate({ inputDir, outDir, sourceObservationSha256,
46
+ now = () => new Date().toISOString() } = {}) {
47
+ const input = path.resolve(inputDir || '');
48
+ const output = path.resolve(outDir || '');
49
+ if (!HEX64.test(String(sourceObservationSha256 || ''))) fail('concepts require an exact source observation');
50
+ const fenceFile = path.join(input, 'PRIVATE-STORES.json');
51
+ const fence = readJson(fenceFile, 'private fence');
52
+ if (!Array.isArray(fence.privateStores)) fail('private fence has no privateStores array');
53
+ const privateStores = new Set(fence.privateStores.map((store) => String(store).toLowerCase()));
54
+ const privateSlugs = new Set();
55
+ for (const store of privateStores) {
56
+ const file = path.join(input, `l2-topics.${store}.json`);
57
+ if (!fs.existsSync(file)) continue;
58
+ const topics = readJson(file, `${store} private topic fence`);
59
+ if (!Array.isArray(topics)) fail(`${store} private topic fence is malformed`);
60
+ for (const topic of topics) if (topic?.slug) privateSlugs.add(String(topic.slug));
61
+ }
62
+
63
+ const repositories = fs.readdirSync(input)
64
+ .filter((file) => file.endsWith('-primer.md'))
65
+ .map((file) => file.slice(0, -'-primer.md'.length))
66
+ .filter((store) => !privateStores.has(store.toLowerCase()))
67
+ .sort();
68
+ const slugRepository = new Map([
69
+ ['guidance-mechanism', 'ruflo'], ['memory-end-to-end', 'ruflo'], ['adr-coverage', 'ruflo'],
70
+ ]);
71
+ const inputFiles = new Set(['PRIVATE-STORES.json']);
72
+ for (const repository of repositories) {
73
+ const relative = `l2-topics.${repository}.json`;
74
+ const file = path.join(input, relative);
75
+ if (!fs.existsSync(file)) continue;
76
+ const topics = readJson(file, `${repository} topics`);
77
+ if (!Array.isArray(topics)) fail(`${repository} topics are malformed`);
78
+ inputFiles.add(relative);
79
+ for (const topic of topics) if (topic?.slug) slugRepository.set(String(topic.slug), repository);
80
+ }
81
+
82
+ const passages = [];
83
+ const entries = {};
84
+ let nextId = 0;
85
+ const add = (repository, kind, slug, title, body) => {
86
+ const parts = chunks(body);
87
+ for (const [index, text] of parts.entries()) {
88
+ const id = String(nextId++);
89
+ const passagePath = `${repository}/${kind}/${slug}${parts.length > 1 ? `#${index}` : ''}`;
90
+ passages.push({ id, text, path: passagePath, title });
91
+ entries[id] = { path: passagePath, kind: 'doc', title, chunk: index, preview: text.slice(0, 200) };
92
+ }
93
+ };
94
+
95
+ const l2Dir = path.join(input, 'l2');
96
+ if (!fs.existsSync(l2Dir) || !fs.statSync(l2Dir).isDirectory()) fail('concept L2 input directory is missing');
97
+ for (const name of fs.readdirSync(l2Dir).filter((file) => file.endsWith('.md')).sort()) {
98
+ const slug = name.slice(0, -3);
99
+ const repository = slugRepository.get(slug) || 'ruvnet';
100
+ if (privateStores.has(repository.toLowerCase()) || privateSlugs.has(slug)) continue;
101
+ const relative = `l2/${name}`;
102
+ const body = fs.readFileSync(path.join(input, relative), 'utf8');
103
+ inputFiles.add(relative);
104
+ add(repository, 'L2', slug, body.match(/^#\s+(.+)/m)?.[1] || slug, body);
105
+ }
106
+ for (const repository of repositories) {
107
+ const relative = `${repository}-primer.md`;
108
+ const body = fs.readFileSync(path.join(input, relative), 'utf8');
109
+ inputFiles.add(relative);
110
+ add(repository, 'PRIMER', `${repository}-primer`, `${repository} — Primer`, body);
111
+ }
112
+ const cardsRelative = 'capability-cards.md';
113
+ const cardsFile = path.join(input, cardsRelative);
114
+ if (fs.existsSync(cardsFile)) {
115
+ inputFiles.add(cardsRelative);
116
+ const sections = fs.readFileSync(cardsFile, 'utf8').split(/^##\s+/m).slice(1);
117
+ for (const section of sections) {
118
+ const newline = section.indexOf('\n');
119
+ if (newline < 0) continue;
120
+ const repository = section.slice(0, newline).trim();
121
+ const body = section.slice(newline + 1).trim();
122
+ if (!repository || !body || privateStores.has(repository.toLowerCase())) continue;
123
+ add(repository, 'CARD', `${repository}-card`, `${repository} — Capability`, `${repository} — ${body}`);
124
+ }
125
+ }
126
+ if (!passages.length) fail('concept aggregate produced zero public passages');
127
+
128
+ fs.mkdirSync(output, { recursive: true });
129
+ const passageBody = `${passages.map((row) => JSON.stringify(row)).join('\n')}\n`;
130
+ fs.writeFileSync(path.join(output, 'concepts.passages.jsonl'), passageBody);
131
+ fs.writeFileSync(path.join(output, 'concepts.meta.json'), `${JSON.stringify({
132
+ model: 'concepts', dimensions: 0, metric: 'cosine', name: 'concepts', generated: now(),
133
+ repo: 'ruvnet-concepts', note: 'Public L2 synthesis, primers, and capability cards.', entries,
134
+ }, null, 2)}\n`);
135
+ const receipt = {
136
+ schemaVersion: 1,
137
+ kind: 'ruvnet-brain-derived-store-receipt',
138
+ store: 'concepts',
139
+ sourceObservationSha256,
140
+ inputs: [...inputFiles].sort().map((relative) => ({ path: relative, sha256: sha256File(path.join(input, relative)) })),
141
+ passagesSha256: crypto.createHash('sha256').update(passageBody).digest('hex'),
142
+ };
143
+ fs.writeFileSync(path.join(output, 'concepts.sources.json'), `${JSON.stringify(receipt, null, 2)}\n`);
144
+ const classesFile = path.join(input, 'public-store-classes.json');
145
+ const classes = fs.existsSync(classesFile) ? readJson(classesFile, 'public store classes') : { schemaVersion: 1, derived: [] };
146
+ if (classes.schemaVersion !== 1 || !Array.isArray(classes.derived)) fail('public store classes are malformed');
147
+ classes.derived = [...classes.derived.filter((entry) => String(entry?.store || '').toLowerCase() !== 'concepts'),
148
+ { store: 'concepts', receipt: 'concepts.sources.json' }]
149
+ .sort((a, b) => String(a.store).localeCompare(String(b.store)));
150
+ fs.writeFileSync(path.join(output, 'public-store-classes.json'), `${JSON.stringify(classes, null, 2)}\n`);
151
+ return { passages: passages.length, receipt };
152
+ }
153
+
154
+ function defaultBuildVector({ root, assetsDir, store }) {
155
+ const script = path.join(path.resolve(root || ROOT), 'kb', 'forge-big.mjs');
156
+ const result = spawnSync(process.execPath, [script, 'both', '--dir', assetsDir, '--name', store], {
157
+ encoding: 'utf8', stdio: 'inherit', env: { ...process.env },
158
+ });
159
+ if (result.error || result.status !== 0) fail(`${store} vector build failed (${result.error?.message || `exit ${result.status}`})`);
160
+ }
161
+
162
+ export async function rebuildCorpusAggregates({ assetsDir, observation, root = ROOT,
163
+ fetchFn = globalThis.fetch, buildVector = defaultBuildVector, now = () => new Date().toISOString() } = {}) {
164
+ const assets = path.resolve(assetsDir || '');
165
+ const observationSha256 = String(observation?.observationSha256 || '');
166
+ if (!HEX64.test(observationSha256)) fail('aggregate rebuild requires an exact source observation');
167
+ const sourceFile = path.join(assets, 'ruv-gists.sources.json');
168
+ const source = readJson(sourceFile, 'gist source receipt');
169
+ if (source.sourceObservationSha256 !== observationSha256) fail('gist source receipt observation differs from the stable observation');
170
+ if (typeof buildVector !== 'function') fail('aggregate vector builder is unavailable');
171
+
172
+ const stage = fs.mkdtempSync(path.join(path.dirname(assets), '.corpus-aggregates-'));
173
+ try {
174
+ const gists = await reconstructGists(source, { fetchFn });
175
+ writeReconstruction(gists, { outDir: stage });
176
+ buildConceptAggregate({ inputDir: assets, outDir: stage, sourceObservationSha256: observationSha256, now });
177
+ for (const store of AGGREGATES) await buildVector({ root, assetsDir: stage, store });
178
+ writeRvfGeneration({ dir: stage, previousDir: assets, store: 'ruv-gists', model: MODEL,
179
+ dimensions: DIMENSIONS, sourceCommit: observationSha256, builtUtc: now() });
180
+ writeRvfGeneration({ dir: stage, previousDir: stage, store: 'concepts', model: MODEL,
181
+ dimensions: DIMENSIONS, sourceCommit: observationSha256, builtUtc: now() });
182
+ const files = [
183
+ ...AGGREGATES.flatMap((store) => [
184
+ `${store}.passages.jsonl`, `${store}.meta.json`, `${store}.big.rvf`,
185
+ `${store}.big.rvf.idmap.json`, `${store}.big.rvf.embed.json`,
186
+ ]),
187
+ 'ruv-gists.sources.json', 'concepts.sources.json', 'public-store-classes.json', 'RVF-GENERATIONS.json',
188
+ ];
189
+ promoteArtifactSet({ liveDir: assets, candidateDir: stage, files });
190
+ return { rebuilt: [...AGGREGATES].sort(), sourceObservationSha256: observationSha256 };
191
+ } finally {
192
+ fs.rmSync(stage, { recursive: true, force: true });
193
+ }
194
+ }
@@ -7,6 +7,8 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { spawnSync } from 'node:child_process';
9
9
  import { fileURLToPath } from 'node:url';
10
+ import { extractZip } from '../kb/zip-extract.mjs';
11
+ import { validatePublicInventory } from './public-inventory.mjs';
10
12
 
11
13
  const HERE = path.dirname(fileURLToPath(import.meta.url));
12
14
  const REQUIRED_SUFFIXES = [
@@ -68,55 +70,62 @@ function policyRows(policy) {
68
70
 
69
71
  function assertPolicyCurrent(policy) {
70
72
  const rows = policyRows(policy);
71
- const stale = rows.filter((row) => row?.eligible !== false && row?.status !== 'CURRENT');
73
+ const eligible = (row) => row?.eligible !== false && (!row?.disposition || row.disposition === 'eligible');
74
+ const stale = rows.filter((row) => eligible(row) && row?.status !== 'CURRENT');
72
75
  if (stale.length) fail(`eligibility policy has ${stale.length} eligible row(s) that are not CURRENT`);
73
76
  return {
74
- eligibleRepoCount: rows.filter((row) => row?.eligible !== false && row?.status === 'CURRENT').length,
75
- gistCount: rows.filter((row) => /gist/i.test(String(row?.kind || row?.type || ''))).length,
77
+ eligibleRepoCount: rows.filter((row) => eligible(row) && row?.kind === 'repository' && row?.status === 'CURRENT').length,
78
+ gistCount: rows.filter((row) => eligible(row) && /gist/i.test(String(row?.kind || row?.type || ''))).length,
76
79
  };
77
80
  }
78
81
 
79
- function archiveEntries(bundleFile) {
80
- const result = spawnSync('unzip', ['-Z1', bundleFile], { encoding: 'utf8' });
81
- if (result.status !== 0) fail(`cannot list archive (${result.stderr.trim() || `exit ${result.status}`})`);
82
- const entries = result.stdout.split(/\r?\n/).filter(Boolean);
83
- for (const entry of entries) {
84
- const normalized = entry.replaceAll('\\', '/');
85
- if (normalized.startsWith('/') || normalized.split('/').includes('..')) {
86
- fail(`archive contains unsafe path ${JSON.stringify(entry)}`);
87
- }
88
- }
89
- return entries;
90
- }
91
-
92
- function verifyArchive({ bundleFile, stores, privateStores }) {
82
+ async function verifyArchive({ bundleFile, stores, privateStores }) {
93
83
  if (!bundleFile || !fs.existsSync(bundleFile)) fail(`bundle missing (${bundleFile || 'no path supplied'})`);
94
- const entries = archiveEntries(bundleFile);
95
- for (const privateStore of privateStores) {
96
- const escaped = privateStore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
- const privateFile = new RegExp(`(?:^|/)${escaped}(?:\\.|$)`, 'i');
98
- if (entries.some((entry) => privateFile.test(entry))) fail(`private store ${privateStore} is present in archive`);
99
- }
100
-
101
84
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'corpus-candidate-'));
102
85
  try {
103
- const extraction = spawnSync('unzip', ['-qq', bundleFile, '-d', tmp], { encoding: 'utf8' });
104
- if (extraction.status !== 0) fail(`cannot extract archive (${extraction.stderr.trim() || `exit ${extraction.status}`})`);
86
+ let extraction;
87
+ try {
88
+ extraction = await extractZip(bundleFile, tmp);
89
+ } catch (error) {
90
+ fail(`cannot extract archive (${error.message})`);
91
+ }
92
+ const entries = extraction.entryNames;
93
+ for (const privateStore of privateStores) {
94
+ const escaped = privateStore.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
95
+ const privateFile = new RegExp(`(?:^|/)${escaped}(?:\\.|$)`, 'i');
96
+ if (entries.some((entry) => privateFile.test(entry.replaceAll('\\', '/')))) {
97
+ fail(`private store ${privateStore} is present in archive`);
98
+ }
99
+ }
105
100
  for (const store of stores) {
106
101
  for (const expected of store.files) {
107
- const matches = entries.filter((entry) => path.posix.basename(entry) === expected.file);
102
+ const matches = entries.filter((entry) => path.posix.basename(entry.replaceAll('\\', '/')) === expected.file);
108
103
  if (matches.length !== 1) fail(`archive must contain exactly one ${expected.file}; found ${matches.length}`);
109
104
  const extracted = path.join(tmp, ...matches[0].replaceAll('\\', '/').split('/'));
110
105
  if (sha256File(extracted) !== expected.sha256) fail(`archive ${expected.file} differs from canonical assets`);
111
106
  }
112
107
  }
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(', ')}`);
113
122
  } finally {
114
123
  fs.rmSync(tmp, { recursive: true, force: true });
115
124
  }
116
125
  return fileIdentity(bundleFile);
117
126
  }
118
127
 
119
- function buildReceipt({
128
+ async function buildReceipt({
120
129
  assetsDir,
121
130
  bundleFile,
122
131
  policyFile,
@@ -138,7 +147,6 @@ function buildReceipt({
138
147
  fail('RVF generation ledger has no stores object');
139
148
  }
140
149
  const policy = readJson(path.resolve(policyFile || ''), 'eligibility policy');
141
- const counts = assertPolicyCurrent(policy);
142
150
 
143
151
  const rvfFiles = fs.readdirSync(assets).filter((name) => /^.+\.big\.rvf$/.test(name)).sort();
144
152
  const rvfByStore = new Map(rvfFiles.map((file) => [file.slice(0, -'.big.rvf'.length), file]));
@@ -160,6 +168,9 @@ function buildReceipt({
160
168
  .filter(([, owners]) => owners.length > 1)
161
169
  .map(([sha256, owners]) => ({ sha256, stores: owners.sort() }));
162
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);
163
174
 
164
175
  const missingSidecars = [];
165
176
  const stores = [];
@@ -172,7 +183,7 @@ function buildReceipt({
172
183
  if (!generation.sourceCommit || !generation.builtUtc || !generation.model || !Number.isInteger(generation.dimensions)) {
173
184
  fail(`${store}: generation receipt lacks sourceCommit, builtUtc, model, or dimensions provenance`);
174
185
  }
175
- if (privateSet.has(store.toLowerCase())) continue;
186
+ if (privateSet.has(store.toLowerCase()) || !publicSet.has(store.toLowerCase())) continue;
176
187
  const files = [];
177
188
  for (const suffix of REQUIRED_SUFFIXES) {
178
189
  const file = path.join(assets, `${store}${suffix}`);
@@ -194,7 +205,14 @@ function buildReceipt({
194
205
  }
195
206
  if (missingSidecars.length) fail(`missing sidecars: ${missingSidecars.join(', ')}`);
196
207
  if (!stores.length) fail('zero public corpus stores remain after the private fence');
197
- const archive = verifyArchive({ bundleFile: path.resolve(bundleFile || ''), stores, privateStores });
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 });
198
216
  const policyIdentity = fileIdentity(path.resolve(policyFile));
199
217
  const fenceIdentity = fileIdentity(fenceFile);
200
218
  const ledgerIdentity = fileIdentity(ledgerFile);
@@ -213,6 +231,8 @@ function buildReceipt({
213
231
  privateFence: fenceIdentity,
214
232
  eligibilityPolicy: policyIdentity,
215
233
  generationLedger: ledgerIdentity,
234
+ publicInventory,
235
+ finalBytePartitionSha256,
216
236
  excludedPrivateStores: privateStores.filter((privateStore) =>
217
237
  [...rvfByStore.keys()].some((store) => store.toLowerCase() === privateStore)),
218
238
  duplicateRvfDigests,
@@ -229,9 +249,9 @@ function currentGitSha() {
229
249
  return result.stdout.trim();
230
250
  }
231
251
 
232
- export function createCorpusReceipt(options) {
252
+ export async function createCorpusReceipt(options) {
233
253
  const receiptFile = path.resolve(options.receiptFile || 'dist/corpus-receipt.json');
234
- const receipt = buildReceipt({
254
+ const receipt = await buildReceipt({
235
255
  ...options,
236
256
  builderSourceSha: options.builderSourceSha || currentGitSha(),
237
257
  createdAt: options.createdAt || new Date().toISOString(),
@@ -241,7 +261,7 @@ export function createCorpusReceipt(options) {
241
261
  return receipt;
242
262
  }
243
263
 
244
- export function verifyCorpusReceipt(options) {
264
+ export async function verifyCorpusReceipt(options) {
245
265
  const receipt = readJson(path.resolve(options.receiptFile || ''), 'corpus receipt');
246
266
  if (receipt.kind !== 'ruvnet-brain-corpus-candidate' || receipt.schemaVersion !== 1) fail('unsupported corpus receipt');
247
267
  const exactFiles = [
@@ -262,7 +282,7 @@ export function verifyCorpusReceipt(options) {
262
282
  if (sha256File(file) !== expected?.sha256) fail(`${label} sha256 differs from receipt`);
263
283
  if (fs.statSync(file).size !== expected?.bytes) fail(`${label} byte length differs from receipt`);
264
284
  }
265
- const actual = buildReceipt({
285
+ const actual = await buildReceipt({
266
286
  ...options,
267
287
  builderSourceSha: receipt.builderSourceSha,
268
288
  createdAt: receipt.createdAt,
@@ -285,7 +305,7 @@ async function main() {
285
305
  receiptFile: arg('--receipt', arg('--out', 'dist/corpus-receipt.json')),
286
306
  builderSourceSha: arg('--builder-source-sha'),
287
307
  };
288
- const receipt = mode === 'verify' ? verifyCorpusReceipt(options) : createCorpusReceipt(options);
308
+ const receipt = mode === 'verify' ? await verifyCorpusReceipt(options) : await createCorpusReceipt(options);
289
309
  console.log(JSON.stringify({ ok: true, mode, archive: receipt.archive, stores: receipt.storeCount }, null, 2));
290
310
  }
291
311