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.
- package/README.md +15 -14
- package/bin/install.mjs +11 -2
- package/console/architecture.html +12 -14
- package/console/install-mockup.html +3 -3
- package/kb/zip-extract.mjs +8 -2
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/host-adapters/claude.json +11 -0
- package/plugin/host-adapters/codex.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +77 -10
- package/plugin/mcp/server.mjs +2 -1
- package/plugin/scripts/anticipate.sh +30 -1
- package/plugin/scripts/capability-claim-evidence.mjs +431 -0
- package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
- package/plugin/scripts/capability-registry.mjs +6 -0
- package/plugin/scripts/capability-routing.mjs +70 -0
- package/plugin/scripts/continuation-gate.mjs +68 -2
- package/plugin/scripts/coverage-integrity.mjs +575 -0
- package/plugin/scripts/ground-ruvnet.sh +2 -2
- package/plugin/scripts/nightly-controller.mjs +19 -2
- package/plugin/scripts/project-progression-contract.mjs +409 -0
- package/plugin/scripts/project-progression-hook.mjs +196 -0
- package/plugin/scripts/project-progression-outbox.mjs +95 -0
- package/plugin/scripts/project-progression-session-start.mjs +183 -0
- package/plugin/scripts/project-progression-store.mjs +248 -0
- package/plugin/scripts/project-store-resolver.mjs +106 -0
- package/plugin/scripts/session-snapshot-hook.mjs +29 -1
- package/plugin/scripts/session-start-core.mjs +22 -18
- package/plugin/scripts/spend-guard.mjs +6 -1
- package/scripts/adr-072-completion.mjs +98 -0
- package/scripts/behavioral-l1-l4.mjs +11 -8
- package/scripts/brain-score.mjs +7 -2
- package/scripts/build-bundle.mjs +82 -6
- package/scripts/card-from-source.mjs +32 -6
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +56 -36
- package/scripts/corpus-reconcile.mjs +295 -28
- package/scripts/corpus-seed-publish.mjs +3 -3
- package/scripts/coverage-integrity.mjs +3 -0
- package/scripts/gist-receipts.mjs +183 -0
- package/scripts/git-hooks/pre-push +21 -0
- package/scripts/host-registry.mjs +118 -0
- package/scripts/independent-review-receipt.mjs +499 -0
- package/scripts/ingest-gists.mjs +34 -5
- package/scripts/ingest-new-repos.mjs +5 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +75 -69
- package/scripts/product-integrity-contract.mjs +156 -0
- package/scripts/public-inventory.mjs +4 -0
- package/scripts/public-verification-aggregate.mjs +342 -0
- package/scripts/public-verification-finalizer.mjs +59 -0
- package/scripts/public-verification-inputs.mjs +473 -0
- package/scripts/public-verification-lane.mjs +244 -0
- package/scripts/publication-receipt.mjs +50 -6
- package/scripts/rebuild-gists-from-receipts.mjs +50 -5
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +139 -51
- package/scripts/release.mjs +15 -7
- package/scripts/restore-local-ingests.mjs +2 -2
- package/scripts/retrieval-canary.mjs +533 -0
- package/scripts/routing-flywheel.mjs +12 -1
- package/scripts/rvf-generation.mjs +9 -3
- package/scripts/self-update.mjs +19 -44
- package/scripts/source-coverage.mjs +415 -0
- package/scripts/source-scope-receipt.mjs +84 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +114 -24
- 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
|
-
|
|
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 =
|
|
297
|
+
run = defaultRunAsync,
|
|
298
|
+
concurrency = 5,
|
|
156
299
|
}) {
|
|
157
|
-
if (!Array.isArray(plan)
|
|
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
|
|
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
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const
|
|
263
|
-
|
|
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,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"
|