ruvnet-brain 4.2.2-dev → 4.3.1-dev

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
@@ -6,15 +6,29 @@
6
6
  import crypto from 'node:crypto';
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
- import { spawnSync } from 'node:child_process';
9
+ import { spawn, spawnSync } from 'node:child_process';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { extractZip } from '../kb/zip-extract.mjs';
12
+ import { FULL_HINTS, KEEP_DIRS } from './full-hints.mjs';
13
+ import { buildCoverage, observeSourceUniverse, sourceObservationDigest } from './source-coverage.mjs';
14
+ import { reconcileGistReceipts } from './gist-receipts.mjs';
15
+ import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
16
+ import { rebuildCorpusAggregates } from './corpus-aggregates.mjs';
17
+
18
+ export { rebuildCorpusAggregates };
12
19
 
13
20
  const HERE = path.dirname(fileURLToPath(import.meta.url));
14
21
  const DEFAULT_ROOT = path.resolve(HERE, '..');
15
22
  const HEX40 = /^[0-9a-f]{40}$/;
16
23
  const HEX64 = /^[0-9a-f]{64}$/;
17
24
  const SAFE_STORE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
25
+ const STORE_ARTIFACT_SUFFIXES = [
26
+ '.big.rvf', '.big.rvf.idmap.json', '.big.rvf.embed.json', '.big.passages.jsonl',
27
+ '.big.meta.json', '.passages.jsonl', '.meta.json',
28
+ ];
29
+ const REQUIRED_STORE_ARTIFACT_SUFFIXES = [
30
+ '.big.rvf', '.big.rvf.idmap.json', '.big.rvf.embed.json', '.passages.jsonl', '.meta.json',
31
+ ];
18
32
 
19
33
  function fail(message) {
20
34
  throw new Error(`[corpus-reconcile] ${message}`);
@@ -134,6 +148,47 @@ export function planReconciliation({ coverage, ledger, assetsDir = null }) {
134
148
  return plan.sort((a, b) => a.store.localeCompare(b.store));
135
149
  }
136
150
 
151
+ export async function reconcileUntilStable({ maxRounds = 3, assetsDir = null, observe, build, readLedger: currentLedger,
152
+ execute, prune, rebuild } = {}) {
153
+ if (!Number.isSafeInteger(maxRounds) || maxRounds < 1 || maxRounds > 10
154
+ || [observe, build, currentLedger, execute, prune, rebuild].some((fn) => typeof fn !== 'function')) {
155
+ fail('bounded reconciliation loop configuration is invalid');
156
+ }
157
+ const rounds = [];
158
+ let observation = await observe();
159
+ for (let round = 1; round <= maxRounds; round += 1) {
160
+ const preliminary = await build(observation);
161
+ const plan = planReconciliation({ coverage: preliminary, ledger: currentLedger(), assetsDir });
162
+ const reconciliation = await execute(plan, round);
163
+ const pruning = await prune(preliminary, round);
164
+ let aggregates;
165
+ try {
166
+ aggregates = await rebuild(preliminary, observation, round);
167
+ } catch (error) {
168
+ if (error?.code !== 'GIST_OBSERVATION_MOVED') throw error;
169
+ const nextObservation = await observe();
170
+ rounds.push({ round, before: observation.observationSha256, after: nextObservation.observationSha256,
171
+ plan, ...reconciliation, ...pruning, rebuilt: [], invalidated: {
172
+ reason: 'gist observation moved during exact detail fetch', gistId: error.gistId || null } });
173
+ observation = nextObservation;
174
+ continue;
175
+ }
176
+ const nextObservation = await observe();
177
+ rounds.push({ round, before: observation.observationSha256, after: nextObservation.observationSha256,
178
+ plan, ...reconciliation, ...pruning, ...aggregates });
179
+ if (nextObservation.observationSha256 === observation.observationSha256) {
180
+ const coverage = await build(nextObservation);
181
+ const remaining = planReconciliation({ coverage, ledger: currentLedger(), assetsDir });
182
+ if (remaining.length) fail(`reconciliation stabilized with ${remaining.length} unresolved repository artifact(s)`);
183
+ const unresolved = coverage.rows.filter((row) => row.disposition === 'eligible' && row.status !== 'CURRENT');
184
+ if (unresolved.length) fail(`reconciliation stabilized with ${unresolved.length} unresolved eligible source(s)`);
185
+ return { observation: nextObservation, coverage, rounds };
186
+ }
187
+ observation = nextObservation;
188
+ }
189
+ fail(`source observation did not stabilize within ${maxRounds} reconciliation rounds`);
190
+ }
191
+
137
192
  function defaultRun(command, args, options = {}) {
138
193
  return spawnSync(command, args, { encoding: 'utf8', ...options });
139
194
  }
@@ -147,14 +202,104 @@ function checked(run, command, args, options = {}) {
147
202
  return result;
148
203
  }
149
204
 
150
- export function executeReconciliation({
205
+ function defaultRunAsync(command, args, options = {}) {
206
+ return new Promise((resolve) => {
207
+ const inherited = options.stdio === 'inherit';
208
+ const child = spawn(command, args, { ...options, encoding: undefined,
209
+ stdio: inherited ? 'inherit' : ['ignore', 'pipe', 'pipe'] });
210
+ let stdout = '';
211
+ let stderr = '';
212
+ if (!inherited) {
213
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
214
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
215
+ }
216
+ child.on('error', (error) => resolve({ status: null, error, stdout, stderr }));
217
+ child.on('close', (status) => resolve({ status, stdout, stderr }));
218
+ });
219
+ }
220
+
221
+ async function checkedAsync(run, command, args, options = {}) {
222
+ const result = await run(command, args, options) || {};
223
+ if (result.error || result.status !== 0) {
224
+ const detail = String(result.stderr || result.stdout || result.error?.message || `exit ${result.status}`).trim();
225
+ fail(`${command} ${args.join(' ')} failed${detail ? ` (${detail})` : ''}`);
226
+ }
227
+ return result;
228
+ }
229
+
230
+ const storeArtifacts = (store) => STORE_ARTIFACT_SUFFIXES.map((suffix) => `${store}${suffix}`);
231
+
232
+ function writeJsonAtomic(file, value) {
233
+ const temporary = `${file}.tmp-${process.pid}-${crypto.randomBytes(6).toString('hex')}`;
234
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
235
+ fs.renameSync(temporary, file);
236
+ }
237
+
238
+ function seedWorkerAssets({ assets, output, store, ledger, source }) {
239
+ fs.mkdirSync(output, { recursive: true });
240
+ for (const name of storeArtifacts(store)) {
241
+ const input = path.join(assets, name);
242
+ if (!fs.existsSync(input)) continue;
243
+ const stat = fs.lstatSync(input);
244
+ if (!stat.isFile() || stat.isSymbolicLink()) fail(`${store}: canonical seed artifact is not a regular file (${name})`);
245
+ fs.copyFileSync(input, path.join(output, name));
246
+ }
247
+ writeJsonAtomic(path.join(output, 'RVF-GENERATIONS.json'), {
248
+ ...ledger, stores: ledger.stores?.[store] ? { [store]: ledger.stores[store] } : {},
249
+ });
250
+ writeJsonAtomic(path.join(output, 'SOURCE.json'), {
251
+ ...(source || { builder: 'rvf-kb-forge' }), stores: source?.stores?.[store] ? { [store]: source.stores[store] } : {},
252
+ });
253
+ }
254
+
255
+ function validateWorkerOutput({ output, item }) {
256
+ const allowed = new Set([...storeArtifacts(item.store), 'RVF-GENERATIONS.json', 'SOURCE.json']);
257
+ const names = fs.readdirSync(output).filter((name) => !name.startsWith('._')).sort();
258
+ const unexpected = names.filter((name) => !allowed.has(name));
259
+ if (unexpected.length) fail(`${item.store}: worker emitted unexpected artifact(s): ${unexpected.join(', ')}`);
260
+ const caseFolded = names.map((name) => name.toLowerCase());
261
+ if (new Set(caseFolded).size !== names.length) fail(`${item.store}: worker emitted case-fold aliases`);
262
+ for (const name of names) {
263
+ const stat = fs.lstatSync(path.join(output, name));
264
+ if (!stat.isFile() || stat.isSymbolicLink()) fail(`${item.store}: worker artifact is not a regular file (${name})`);
265
+ }
266
+ for (const suffix of REQUIRED_STORE_ARTIFACT_SUFFIXES) {
267
+ if (!names.includes(`${item.store}${suffix}`)) fail(`${item.store}: worker artifact family is incomplete (${suffix})`);
268
+ }
269
+ const ledger = readJson(path.join(output, 'RVF-GENERATIONS.json'), `${item.store} worker ledger`);
270
+ const ledgerStores = Object.keys(ledger.stores || {});
271
+ if (ledgerStores.length !== 1 || ledgerStores[0] !== item.store) fail(`${item.store}: worker ledger must contain exactly its own store`);
272
+ const generation = ledger.stores[item.store];
273
+ const expectedRvf = `${item.store}.big.rvf`;
274
+ if (generation?.file !== expectedRvf || String(generation.sourceCommit || '').toLowerCase() !== item.upstreamSha
275
+ || !fs.existsSync(path.join(output, expectedRvf))
276
+ || generation.sha256 !== sha256File(path.join(output, expectedRvf))
277
+ || generation.bytes !== fs.statSync(path.join(output, expectedRvf)).size) {
278
+ fail(`${item.store}: worker generation does not bind exact source and RVF bytes`);
279
+ }
280
+ const source = readJson(path.join(output, 'SOURCE.json'), `${item.store} worker source manifest`);
281
+ if (Object.keys(source.stores || {}).length !== 1 || !source.stores[item.store]
282
+ || String(source.stores[item.store].sourceCommit || '').toLowerCase() !== item.upstreamSha) {
283
+ fail(`${item.store}: worker SOURCE manifest does not bind exact source`);
284
+ }
285
+ const files = names.filter((name) => !['RVF-GENERATIONS.json', 'SOURCE.json'].includes(name))
286
+ .map((name) => ({ name, sha256: sha256File(path.join(output, name)), bytes: fs.statSync(path.join(output, name)).size }));
287
+ const payload = { schemaVersion: 1, kind: 'ruvnet-brain-corpus-worker-result', store: item.store,
288
+ sourceCommit: item.upstreamSha, generation, source: source.stores[item.store], files };
289
+ return { ...payload, receiptSha256: crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex'), output };
290
+ }
291
+
292
+ export async function executeReconciliation({
151
293
  plan,
152
294
  assetsDir,
153
295
  workspaceDir,
154
296
  root = DEFAULT_ROOT,
155
- run = defaultRun,
297
+ run = defaultRunAsync,
298
+ concurrency = 5,
156
299
  }) {
157
- if (!Array.isArray(plan)) fail('reconciliation plan must be an array');
300
+ if (!Array.isArray(plan) || !Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 10) {
301
+ fail('reconciliation plan or worker concurrency is invalid');
302
+ }
158
303
  const assets = path.resolve(assetsDir || '');
159
304
  const workspace = path.resolve(workspaceDir || '');
160
305
  const ledgerFile = path.join(assets, 'RVF-GENERATIONS.json');
@@ -163,30 +308,152 @@ export function executeReconciliation({
163
308
  fs.mkdirSync(workspace, { recursive: true });
164
309
  const forge = path.join(path.resolve(root), 'kb', 'forge-refresh.mjs');
165
310
  if (!fs.existsSync(forge)) fail(`forge-refresh missing (${forge})`);
166
- const refreshed = [];
311
+ const canonicalLedger = readJson(ledgerFile, 'RVF generation ledger');
312
+ const sourceFile = path.join(assets, 'SOURCE.json');
313
+ const canonicalSource = fs.existsSync(sourceFile) ? readJson(sourceFile, 'SOURCE manifest') : { builder: 'rvf-kb-forge', stores: {} };
314
+ const orderedPlan = [...plan].sort((a, b) => a.store.localeCompare(b.store));
315
+ const lowerStores = orderedPlan.map(({ store }) => store.toLowerCase());
316
+ if (new Set(lowerStores).size !== lowerStores.length) fail('reconciliation plan has duplicate or case-fold-colliding stores');
167
317
 
168
- for (const item of plan) {
318
+ const worker = async (item) => {
169
319
  if (!SAFE_STORE.test(item.store) || !HEX40.test(item.upstreamSha) || !repositorySlug(item.url)) {
170
320
  fail(`unsafe reconciliation item for ${item?.store || item?.name || 'unknown store'}`);
171
321
  }
172
- const cloneDir = path.join(workspace, item.store);
173
- if (fs.existsSync(cloneDir)) fail(`fresh clone target already exists (${cloneDir})`);
174
- checked(run, 'git', ['clone', '--no-checkout', '--filter=blob:none', item.url, cloneDir]);
175
- checked(run, 'git', ['-C', cloneDir, 'fetch', '--depth=1', 'origin', item.upstreamSha]);
176
- checked(run, 'git', ['-C', cloneDir, 'checkout', '--detach', 'FETCH_HEAD']);
177
- const head = checked(run, 'git', ['-C', cloneDir, 'rev-parse', 'HEAD']);
322
+ const workerRoot = path.join(workspace, 'workers', item.store);
323
+ const cloneDir = path.join(workerRoot, 'clone');
324
+ const output = path.join(workerRoot, 'assets');
325
+ fs.mkdirSync(workerRoot, { recursive: true });
326
+ seedWorkerAssets({ assets, output, store: item.store, ledger: canonicalLedger, source: canonicalSource });
327
+ await checkedAsync(run, 'git', ['clone', '--no-checkout', '--filter=blob:none', item.url, cloneDir]);
328
+ await checkedAsync(run, 'git', ['-C', cloneDir, 'fetch', '--depth=1', 'origin', item.upstreamSha]);
329
+ await checkedAsync(run, 'git', ['-C', cloneDir, 'checkout', '--detach', 'FETCH_HEAD']);
330
+ const head = await checkedAsync(run, 'git', ['-C', cloneDir, 'rev-parse', 'HEAD']);
178
331
  if (String(head.stdout || '').trim().toLowerCase() !== item.upstreamSha) {
179
332
  fail(`${item.store}: fresh clone did not resolve the exact upstream SHA`);
180
333
  }
181
- checked(run, process.execPath, [forge, '--repo', cloneDir, '--out', assets, '--name', item.store], { stdio: 'inherit' });
182
- const ledger = readJson(ledgerFile, 'RVF generation ledger after forge-refresh');
183
- const generation = Object.entries(ledger.stores || {}).find(([name]) => name.toLowerCase() === item.store.toLowerCase())?.[1];
184
- if (String(generation?.sourceCommit || '').toLowerCase() !== item.upstreamSha) {
185
- fail(`forge-refresh did not bind ${item.store} to the exact upstream SHA`);
334
+ await checkedAsync(run, process.execPath, [forge, '--repo', cloneDir, '--out', output, '--name', item.store,
335
+ ...(FULL_HINTS[item.store] ? ['--full', FULL_HINTS[item.store]] : []),
336
+ ...(KEEP_DIRS[item.store] ? ['--keep', KEEP_DIRS[item.store]] : []),
337
+ ], { stdio: 'inherit', env: { ...process.env, RUVNET_BIG_SHARDS: '1' } });
338
+ return validateWorkerOutput({ output, item });
339
+ };
340
+
341
+ const results = new Array(orderedPlan.length);
342
+ let next = 0;
343
+ await Promise.all(Array.from({ length: Math.min(concurrency, orderedPlan.length) }, async () => {
344
+ while (next < orderedPlan.length) {
345
+ const index = next++;
346
+ results[index] = await worker(orderedPlan[index]);
347
+ }
348
+ }));
349
+ if (!results.length) return { refreshed: [], workers: [] };
350
+
351
+ const merge = path.join(workspace, 'merge-candidate');
352
+ fs.mkdirSync(merge);
353
+ const mergedLedger = structuredClone(canonicalLedger);
354
+ const mergedSource = structuredClone(canonicalSource);
355
+ mergedLedger.stores ||= {};
356
+ mergedSource.stores ||= {};
357
+ const promotedFiles = [];
358
+ for (const result of results) {
359
+ for (const file of result.files) {
360
+ fs.copyFileSync(path.join(result.output, file.name), path.join(merge, file.name), fs.constants.COPYFILE_EXCL);
361
+ promotedFiles.push(file.name);
362
+ }
363
+ mergedLedger.stores[result.store] = result.generation;
364
+ mergedSource.stores[result.store] = result.source;
365
+ }
366
+ mergedLedger.stores = Object.fromEntries(Object.entries(mergedLedger.stores).sort(([a], [b]) => a.localeCompare(b)));
367
+ mergedSource.stores = Object.fromEntries(Object.entries(mergedSource.stores).sort(([a], [b]) => a.localeCompare(b)));
368
+ writeJsonAtomic(path.join(merge, 'RVF-GENERATIONS.json'), mergedLedger);
369
+ writeJsonAtomic(path.join(merge, 'SOURCE.json'), mergedSource);
370
+ promotedFiles.push('RVF-GENERATIONS.json', 'SOURCE.json');
371
+ promoteArtifactSet({ liveDir: assets, candidateDir: merge, files: promotedFiles.sort() });
372
+ return { refreshed: results.map(({ store }) => store),
373
+ workers: results.map(({ output: _output, ...receipt }) => receipt) };
374
+ }
375
+
376
+ export function syncCorpusInputs({ root = DEFAULT_ROOT, assetsDir }) {
377
+ const sourceKb = path.join(path.resolve(root), 'kb');
378
+ const assets = path.resolve(assetsDir || '');
379
+ const required = ['capability-cards.md', 'external-sources.json', 'no-corpus-repos.json',
380
+ 'public-store-classes.json'];
381
+ for (const name of required) {
382
+ const source = path.join(sourceKb, name);
383
+ if (!fs.existsSync(source) || !fs.statSync(source).isFile()) fail(`canonical corpus input missing (${source})`);
384
+ fs.copyFileSync(source, path.join(assets, name));
385
+ }
386
+ const sourceL2 = path.join(sourceKb, 'l2');
387
+ if (!fs.existsSync(sourceL2) || !fs.statSync(sourceL2).isDirectory()) fail(`canonical L2 input missing (${sourceL2})`);
388
+ fs.rmSync(path.join(assets, 'l2'), { recursive: true, force: true });
389
+ fs.cpSync(sourceL2, path.join(assets, 'l2'), { recursive: true });
390
+ for (const entry of fs.readdirSync(sourceKb)) {
391
+ if (entry.endsWith('-primer.md') || /^l2-topics\..+\.json$/.test(entry)) {
392
+ fs.copyFileSync(path.join(sourceKb, entry), path.join(assets, entry));
186
393
  }
187
- refreshed.push(item.store);
188
394
  }
189
- return { refreshed };
395
+ return { copied: required };
396
+ }
397
+
398
+ export async function materializeGistReceipts({ observation, assetsDir, fetchGist, fetchBody, now } = {}) {
399
+ if (observation?.observationSha256 !== sourceObservationDigest(observation)) {
400
+ fail('gist receipts require an exact sealed source observation');
401
+ }
402
+ const sourceFile = path.join(path.resolve(assetsDir || ''), 'ruv-gists.sources.json');
403
+ const existing = fs.existsSync(sourceFile) ? readJson(sourceFile, 'existing gist receipts') : null;
404
+ const receipt = await reconcileGistReceipts({ observation, existing, fetchGist, fetchBody, now });
405
+ if (receipt.sourceObservationSha256 !== observation.observationSha256) {
406
+ fail('gist receipts differ from the sealed source observation');
407
+ }
408
+ writeJsonAtomic(sourceFile, receipt);
409
+ return { sourceFile, receipt };
410
+ }
411
+
412
+ export async function observeAndMaterializeGistReceipts({ owner = 'ruvnet', assetsDir,
413
+ observe = observeSourceUniverse, fetchGist, fetchBody, now } = {}) {
414
+ const assets = path.resolve(assetsDir || '');
415
+ const externalFile = path.join(assets, 'external-sources.json');
416
+ const policy = fs.existsSync(externalFile) ? readJson(externalFile, 'external source policy') : { sources: [] };
417
+ if (!Array.isArray(policy.sources)) fail('external source policy has no sources array');
418
+ const observation = await observe({ owner, externalSources: policy.sources });
419
+ const materialized = await materializeGistReceipts({ observation, assetsDir: assets, fetchGist, fetchBody, now });
420
+ return { observation, ...materialized };
421
+ }
422
+
423
+ export async function reconcileCorpusUntilStable({ owner = 'ruvnet', assetsDir, workspaceDir,
424
+ root = DEFAULT_ROOT, maxRounds = 3,
425
+ observeAndMaterialize = null,
426
+ build = (observation) => buildCoverage({ owner, kbDir: assetsDir, policyDir: assetsDir, observation }),
427
+ readLedger = () => readJson(path.join(path.resolve(assetsDir || ''), 'RVF-GENERATIONS.json'),
428
+ 'RVF generation ledger'),
429
+ execute = executeReconciliation,
430
+ prune = () => ({ pruned: [] }),
431
+ rebuild = (_coverage, observation) => rebuildCorpusAggregates({ assetsDir, observation, root }),
432
+ } = {}) {
433
+ if (!assetsDir || !workspaceDir) fail('stable reconciliation requires explicit assets and workspace directories');
434
+ const workspace = path.resolve(workspaceDir || '');
435
+ return reconcileUntilStable({
436
+ maxRounds,
437
+ assetsDir,
438
+ observe: async () => (await (observeAndMaterialize
439
+ ? observeAndMaterialize({ owner, assetsDir })
440
+ : observeAndMaterializeGistReceipts({ owner, assetsDir }))).observation,
441
+ build,
442
+ readLedger,
443
+ execute: (plan, round) => execute({
444
+ plan, assetsDir, workspaceDir: path.join(workspace, `round-${round}`), root,
445
+ }),
446
+ prune,
447
+ rebuild,
448
+ });
449
+ }
450
+
451
+ export async function reconcileAndPrepareCorpusCandidate({ plan, assetsDir, workspaceDir, root = DEFAULT_ROOT,
452
+ owner = 'ruvnet', builderSha, candidateDir, receiptFile, coverageFile,
453
+ execute = executeReconciliation, prepare = prepareCorpusCandidate } = {}) {
454
+ const reconciliation = await execute({ plan, assetsDir, workspaceDir, root });
455
+ const candidate = await prepare({ root, assetsDir, owner, builderSha, candidateDir, receiptFile, coverageFile });
456
+ return { reconciliation, candidate };
190
457
  }
191
458
 
192
459
  export function prepareCorpusCandidate({
@@ -217,7 +484,8 @@ export function prepareCorpusCandidate({
217
484
  fs.mkdirSync(path.dirname(receipt), { recursive: true });
218
485
  checked(run, process.execPath, [coverageScript, '--owner', owner, '--assets', assets, '--write'], { stdio: 'inherit' });
219
486
  checked(run, process.execPath, [coverageScript, '--owner', owner, '--assets', assets, '--check', '--strict'], { stdio: 'inherit' });
220
- checked(run, process.execPath, [buildScript, '--assets', assets, '--out', candidate], { stdio: 'inherit' });
487
+ checked(run, process.execPath, [buildScript, '--assets', assets, '--out', candidate,
488
+ '--coverage', policy], { stdio: 'inherit' });
221
489
  const bundleFile = path.join(path.dirname(candidate), `${path.basename(candidate)}.zip`);
222
490
  checked(run, process.execPath, [receiptScript, '--assets', assets, '--bundle', bundleFile,
223
491
  '--policy', policy, '--receipt', receipt, '--builder-source-sha', builderSha], { stdio: 'inherit' });
@@ -254,14 +522,13 @@ export async function main(argv = process.argv.slice(2)) {
254
522
  if (!fs.existsSync(privateFence)) fail(`canonical private-store fence missing (${privateFence})`);
255
523
  fs.copyFileSync(privateFence, path.join(assetsDir, 'PRIVATE-STORES.json'), fs.constants.COPYFILE_EXCL);
256
524
  fs.rmSync(extractParent, { recursive: true, force: true });
257
- const coverageScript = path.join(root, 'scripts', 'source-coverage.mjs');
258
- checked(defaultRun, process.execPath, [coverageScript, '--owner', owner, '--assets', assetsDir, '--write'], { stdio: 'inherit' });
259
- const policy = readJson(coverageFile, 'source coverage policy');
260
- const ledger = readJson(path.join(assetsDir, 'RVF-GENERATIONS.json'), 'bootstrap RVF generation ledger');
261
- const plan = planReconciliation({ coverage: policy, ledger, assetsDir });
262
- const reconciliation = executeReconciliation({ plan, assetsDir, workspaceDir, root });
263
- const candidate = prepareCorpusCandidate({ root, assetsDir, owner, builderSha, candidateDir, receiptFile, coverageFile });
264
- process.stdout.write(`${JSON.stringify({ ok: true, seedTag, seedSha256, plan, ...reconciliation, ...candidate }, null, 2)}\n`);
525
+ syncCorpusInputs({ root, assetsDir });
526
+ const { reconciliation, candidate } = await reconcileAndPrepareCorpusCandidate({
527
+ plan: [], assetsDir, workspaceDir, root, owner, builderSha, candidateDir, receiptFile, coverageFile,
528
+ execute: () => reconcileCorpusUntilStable({ owner, assetsDir, workspaceDir, root }),
529
+ });
530
+ const plan = reconciliation.rounds.flatMap((round) => round.plan);
531
+ process.stdout.write(`${JSON.stringify({ ok: true, seedTag, seedSha256, plan, reconciliation, ...candidate }, null, 2)}\n`);
265
532
  return 0;
266
533
  }
267
534
 
@@ -34,7 +34,7 @@ function defaultRun(command, args, options) {
34
34
  return spawnSync(command, args, { encoding: 'utf8', ...options });
35
35
  }
36
36
 
37
- export function publishCorpusSeed({
37
+ export async function publishCorpusSeed({
38
38
  receiptFile,
39
39
  bundleFile,
40
40
  assetsDir,
@@ -45,7 +45,7 @@ export function publishCorpusSeed({
45
45
  const receiptPath = path.resolve(receiptFile || '');
46
46
  const bundlePath = path.resolve(bundleFile || '');
47
47
  if (!assetsDir || !policyFile) fail('publishing requires --assets and --policy for full receipt verification');
48
- const receipt = verifyCorpusReceipt({
48
+ const receipt = await verifyCorpusReceipt({
49
49
  receiptFile: receiptPath,
50
50
  bundleFile: bundlePath,
51
51
  assetsDir,
@@ -95,7 +95,7 @@ function arg(name, fallback) {
95
95
  }
96
96
 
97
97
  async function main() {
98
- const result = publishCorpusSeed({
98
+ const result = await publishCorpusSeed({
99
99
  receiptFile: arg('--receipt', 'dist/corpus-receipt.json'),
100
100
  bundleFile: arg('--bundle', 'dist/ruvnet-brain.zip'),
101
101
  assetsDir: arg('--assets'),
@@ -0,0 +1,3 @@
1
+ // Compatibility import surface. The installed plugin owns coverage validation so `/coverage`,
2
+ // Console, build, update, and release tooling all execute the same implementation.
3
+ export * from '../plugin/scripts/coverage-integrity.mjs';
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { canonicalJson, digest } from './coverage-integrity.mjs';
6
+
7
+ const TEXT_EXT = new Set(['.md', '.markdown', '.txt', '.rst']);
8
+ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex');
9
+ const HEX40 = /^[a-f0-9]{40}$/;
10
+ const HEX64 = /^[a-f0-9]{64}$/;
11
+ const compareCanonicalText = (left, right) => String(left) < String(right) ? -1
12
+ : String(left) > String(right) ? 1 : 0;
13
+
14
+ const withoutReceiptDigest = ({ receiptSha256: _receiptSha256, ...payload }) => payload;
15
+
16
+ function validDate(value) {
17
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
18
+ }
19
+
20
+ export function sealGistReceipt(receipt) {
21
+ const files = [...(receipt.files || [])].sort((a, b) => compareCanonicalText(a.filename, b.filename));
22
+ const payload = {
23
+ schemaVersion: 3,
24
+ kind: 'ruvnet-brain-gist-source-receipt',
25
+ gistId: String(receipt.gistId || ''),
26
+ versionSha: receipt.versionSha,
27
+ updatedAt: receipt.updatedAt,
28
+ ingestedAt: receipt.ingestedAt,
29
+ fileCount: files.length,
30
+ files,
31
+ contentDigest: digest(files),
32
+ complete: receipt.complete === true,
33
+ };
34
+ return { ...payload, receiptSha256: digest(payload) };
35
+ }
36
+
37
+ export function sealGistReceiptSet(receipt) {
38
+ const ids = Object.keys(receipt.gists || {}).sort();
39
+ const gists = Object.fromEntries(ids.map((id) => [id, receipt.gists[id]]));
40
+ const sourceSet = ids.map((id) => ({ id, receiptSha256: gists[id]?.receiptSha256 }));
41
+ const payload = {
42
+ schemaVersion: 3,
43
+ kind: 'ruvnet-brain-gist-source-receipts',
44
+ owner: receipt.owner,
45
+ generated: receipt.generated,
46
+ observedAt: receipt.observedAt,
47
+ sourceObservationSha256: receipt.sourceObservationSha256,
48
+ gistSet: {
49
+ count: ids.length,
50
+ idsSha256: digest(ids),
51
+ observationSha256: receipt.sourceObservationSha256,
52
+ },
53
+ sourceSetSha256: digest(sourceSet),
54
+ passagesSha256: receipt.passagesSha256 ?? null,
55
+ gists,
56
+ };
57
+ return { ...payload, receiptSha256: digest(payload) };
58
+ }
59
+
60
+ function defaultFetchGist(id) {
61
+ const result = spawnSync('gh', ['api', `gists/${id}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
62
+ if (result.status !== 0) throw new Error(`gist ${id} fetch failed: ${String(result.stderr || '').trim()}`);
63
+ return JSON.parse(result.stdout);
64
+ }
65
+
66
+ async function defaultFetchBody(file) {
67
+ if (!file.truncated) return Buffer.from(file.content || '', 'utf8');
68
+ const response = await fetch(file.raw_url, { headers: { 'user-agent': 'ruvnet-brain-gist-receipts' } });
69
+ if (!response.ok) throw new Error(`raw gist fetch failed: HTTP ${response.status}`);
70
+ return Buffer.from(await response.arrayBuffer());
71
+ }
72
+
73
+ function listedFileIdentity(gist) {
74
+ return Object.entries(gist?.files || {}).sort(([a], [b]) => compareCanonicalText(a, b)).map(([key, file]) => ({
75
+ key, filename: file?.filename, rawUrl: file?.raw_url, size: file?.size, type: file?.type,
76
+ language: file?.language ?? null,
77
+ }));
78
+ }
79
+
80
+ function exactFileIdentity(file) {
81
+ if (!file || typeof file.filename !== 'string' || !file.filename) return false;
82
+ const keys = Object.keys(file).sort();
83
+ if (file.included === true) {
84
+ return canonicalJson(keys) === canonicalJson(['bytes', 'filename', 'included', 'sha256'])
85
+ && HEX64.test(String(file.sha256 || '')) && Number.isSafeInteger(file.bytes) && file.bytes >= 0;
86
+ }
87
+ return file.included === false
88
+ && canonicalJson(keys) === canonicalJson(['filename', 'included', 'reason', 'size'])
89
+ && typeof file.reason === 'string' && Boolean(file.reason)
90
+ && (file.size === null || (Number.isSafeInteger(file.size) && file.size >= 0));
91
+ }
92
+
93
+ function reusableReceipt(row, { gistId, updatedAt }) {
94
+ if (row?.schemaVersion !== 3 || row?.kind !== 'ruvnet-brain-gist-source-receipt'
95
+ || row.gistId !== gistId || row.complete !== true || row.updatedAt !== updatedAt
96
+ || !validDate(row.ingestedAt)
97
+ || !Array.isArray(row.files) || !row.files.length || row.fileCount !== row.files.length
98
+ || row.files.some((file) => !exactFileIdentity(file))) return false;
99
+ const filenames = row.files.map(({ filename }) => filename);
100
+ return new Set(filenames).size === filenames.length
101
+ && canonicalJson(filenames) === canonicalJson([...filenames].sort(compareCanonicalText))
102
+ && canonicalJson(row) === canonicalJson(sealGistReceipt(row));
103
+ }
104
+
105
+ export function validateGistReceiptSet(receipt, observation) {
106
+ const ids = observation?.gists?.rows?.map(({ id }) => String(id)).sort() || [];
107
+ const received = Object.keys(receipt?.gists || {}).sort();
108
+ if (receipt?.schemaVersion !== 3 || receipt?.kind !== 'ruvnet-brain-gist-source-receipts'
109
+ || receipt.owner !== observation?.owner || !validDate(receipt.generated)
110
+ || !validDate(receipt.observedAt)
111
+ || canonicalJson(ids) !== canonicalJson(received)
112
+ || receipt.gistSet?.count !== ids.length || receipt.gistSet?.idsSha256 !== digest(ids)
113
+ || receipt.gistSet?.observationSha256 !== observation?.observationSha256
114
+ || receipt.sourceObservationSha256 !== observation?.observationSha256) {
115
+ throw new Error('gist receipt set does not exactly match the sealed source observation');
116
+ }
117
+ for (const id of ids) {
118
+ const row = receipt.gists[id];
119
+ const stub = observation.gists.rows.find((gist) => String(gist.id) === id);
120
+ if (row?.schemaVersion !== 3 || row?.kind !== 'ruvnet-brain-gist-source-receipt'
121
+ || row?.gistId !== id || row?.complete !== true || row.updatedAt !== stub?.updated_at
122
+ || !HEX40.test(String(row.versionSha || ''))
123
+ || !validDate(row.updatedAt) || !validDate(row.ingestedAt)
124
+ || !Array.isArray(row.files) || row.fileCount !== row.files.length || row.files.length === 0
125
+ || row.files.some((file) => !exactFileIdentity(file))) throw new Error(`gist ${id} receipt is incomplete`);
126
+ const filenames = row.files.map(({ filename }) => filename);
127
+ if (filenames.some((filename) => typeof filename !== 'string' || !filename)
128
+ || new Set(filenames).size !== filenames.length
129
+ || canonicalJson(filenames) !== canonicalJson([...filenames].sort(compareCanonicalText))) {
130
+ throw new Error(`gist ${id} file inventory is not unique and sorted`);
131
+ }
132
+ if (row.contentDigest !== digest(row.files)) throw new Error(`gist ${id} content digest differs`);
133
+ if (row.receiptSha256 !== digest(withoutReceiptDigest(row))) throw new Error(`gist ${id} receipt digest differs`);
134
+ }
135
+ const sourceSet = ids.map((id) => ({ id, receiptSha256: receipt.gists[id].receiptSha256 }));
136
+ if (receipt.sourceSetSha256 !== digest(sourceSet)) throw new Error('gist source set digest differs');
137
+ if (!(receipt.passagesSha256 === null || HEX64.test(String(receipt.passagesSha256 || '')))) {
138
+ throw new Error('gist passage digest is malformed');
139
+ }
140
+ if (receipt.receiptSha256 !== digest(withoutReceiptDigest(receipt))) throw new Error('gist receipt set digest differs');
141
+ return receipt;
142
+ }
143
+
144
+ export async function reconcileGistReceipts({ observation, existing = null, fetchGist = defaultFetchGist,
145
+ fetchBody = defaultFetchBody, now = () => new Date().toISOString() } = {}) {
146
+ const stubs = observation?.gists?.rows;
147
+ if (!Array.isArray(stubs) || stubs.some(({ id }) => !/^[a-f0-9]{20,64}$/.test(String(id || '')))
148
+ || new Set(stubs.map(({ id }) => id)).size !== stubs.length) {
149
+ throw new Error('source observation has missing or duplicate gist ids');
150
+ }
151
+ const gists = {};
152
+ for (const stub of [...stubs].sort((a, b) => String(a.id).localeCompare(String(b.id)))) {
153
+ const prior = existing?.gists?.[stub.id];
154
+ const reusable = reusableReceipt(prior, { gistId: stub.id, updatedAt: stub.updated_at });
155
+ if (reusable) { gists[stub.id] = prior; continue; }
156
+ const full = await fetchGist(stub.id);
157
+ const versionSha = full?.history?.[0]?.version;
158
+ if (full?.id !== stub.id || full.updated_at !== stub.updated_at || !HEX40.test(String(versionSha || ''))
159
+ || canonicalJson(listedFileIdentity(full)) !== canonicalJson(listedFileIdentity(stub))) {
160
+ const error = new Error(`gist ${stub.id} individual identity differs from the list observation`);
161
+ error.code = 'GIST_OBSERVATION_MOVED';
162
+ error.gistId = stub.id;
163
+ throw error;
164
+ }
165
+ const files = [];
166
+ for (const [filename, file] of Object.entries(full.files || {}).sort(([a], [b]) => a.localeCompare(b))) {
167
+ if (!TEXT_EXT.has(path.extname(filename).toLowerCase())) {
168
+ files.push({ filename, included: false, reason: 'non-text policy exclusion', size: file.size ?? null });
169
+ continue;
170
+ }
171
+ const fetched = await fetchBody(file);
172
+ const body = Buffer.isBuffer(fetched) ? fetched : Buffer.from(String(fetched), 'utf8');
173
+ try { new TextDecoder('utf-8', { fatal: true }).decode(body); }
174
+ catch { throw new Error(`gist ${stub.id}/${filename} is not valid UTF-8 text`); }
175
+ files.push({ filename, included: true, sha256: sha256(body), bytes: body.length });
176
+ }
177
+ gists[stub.id] = sealGistReceipt({ gistId: stub.id, versionSha, updatedAt: full.updated_at,
178
+ ingestedAt: now(), files, complete: files.length === Object.keys(full.files || {}).length });
179
+ }
180
+ return validateGistReceiptSet(sealGistReceiptSet({ owner: observation.owner, generated: now(),
181
+ observedAt: observation.observedAt, sourceObservationSha256: observation.observationSha256,
182
+ passagesSha256: null, gists }), observation);
183
+ }
@@ -93,6 +93,27 @@ if [ "$status" -ne 0 ]; then
93
93
  exit 1
94
94
  fi
95
95
 
96
+ # Generated public census and command aliases have one source of truth each. Both writers have a
97
+ # `--check` mode specifically for CI/pre-push; until 2026-08-22 nothing invoked either check, so the
98
+ # writers were built and tested but their promised gate did not exist.
99
+ node "$ROOT/scripts/sync-census.mjs" --check
100
+ census_status=$?
101
+ if [ "$census_status" -ne 0 ]; then
102
+ echo ""
103
+ echo " ⛔ pre-push gate refused this push — public census claims have drifted."
104
+ echo " Run: node scripts/sync-census.mjs"
105
+ exit 1
106
+ fi
107
+
108
+ node "$ROOT/scripts/sync-commands.mjs" --check
109
+ commands_status=$?
110
+ if [ "$commands_status" -ne 0 ]; then
111
+ echo ""
112
+ echo " ⛔ pre-push gate refused this push — command aliases no longer share one body."
113
+ echo " Run: node scripts/sync-commands.mjs"
114
+ exit 1
115
+ fi
116
+
96
117
  # ── DOCUMENT CURRENCY (ADR-056 §5) ─────────────────────────────────────────────────────────────────
97
118
  # The tool was written and tested on 2026-07-22 and then invoked by NOTHING for five days — absent
98
119
  # from this hook, gate.sh, gates.mjs and every workflow, while `wired-check` reported it "wired"