ruvnet-brain 4.3.35 → 4.3.37
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 +2 -2
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +6 -1
- package/plugin/hooks/hook-contracts.json +6 -4
- package/plugin/scripts/continuity-hook-policy.mjs +10 -3
- package/plugin/scripts/coverage-integrity.mjs +58 -2
- package/plugin/scripts/hook-shim.mjs +3 -1
- package/plugin/scripts/session-snapshot-hook.mjs +11 -1
- package/plugin/scripts/turn-outcome-capture.mjs +292 -0
- package/scripts/build-bundle.mjs +27 -1
- package/scripts/code-release-corpus.mjs +276 -0
- package/scripts/corpus-candidate.mjs +19 -3
- package/scripts/corpus-coverage-sidecar.mjs +202 -0
- package/scripts/corpus-currency.mjs +71 -0
- package/scripts/corpus-dispatch-decision.mjs +138 -0
- package/scripts/corpus-next-seed.mjs +69 -12
- package/scripts/corpus-reconcile.mjs +340 -61
- package/scripts/corpus-store-failure.mjs +82 -0
- package/scripts/corpus-watchdog.mjs +334 -0
- package/scripts/derive-passage-content-map.mjs +71 -0
- package/scripts/fixture-denominator.mjs +72 -0
- package/scripts/knowledge-input-digest.mjs +163 -0
- package/scripts/oracle/repo-recall.mjs +142 -21
- package/scripts/public-verification-inputs.mjs +42 -4
- package/scripts/rehearse-corpus-pipeline.mjs +350 -20
- package/scripts/release-transaction-provider.mjs +29 -6
- package/scripts/release.mjs +156 -18
- package/scripts/retrieval-canary.mjs +83 -24
- package/scripts/retrieval-passage-identity.mjs +58 -0
- package/scripts/source-coverage.mjs +19 -3
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +4 -0
package/scripts/release.mjs
CHANGED
|
@@ -66,6 +66,9 @@ const recallNotes = (receipt) => {
|
|
|
66
66
|
};
|
|
67
67
|
import { verifyBundle } from './verify-bundle.mjs';
|
|
68
68
|
import { CORPUS_GENERATION_FIELD, evaluateCorpusPromotion } from './corpus-promotion.mjs';
|
|
69
|
+
import { bindCoverageToReceipt, writeCoverageAssets } from './corpus-coverage-sidecar.mjs';
|
|
70
|
+
import { degradedPublication } from './corpus-store-failure.mjs';
|
|
71
|
+
import { assertNoNewerCorpusGeneration } from './code-release-corpus.mjs';
|
|
69
72
|
|
|
70
73
|
const ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
71
74
|
const PUBLISH = process.argv.includes('--publish');
|
|
@@ -114,6 +117,28 @@ function corpusFailure(message) {
|
|
|
114
117
|
throw new Error(`[corpus-seed] ${message}`);
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
/**
|
|
121
|
+
* A newer code release was published after this corpus was built at its approved runtime. Promoting
|
|
122
|
+
* it now would put an OLDER runtime on releases/latest over a newer live code release (fresh installs
|
|
123
|
+
* then fail on a version mismatch). That is not a broken night -- the next night builds at the newer
|
|
124
|
+
* runtime -- so it is a distinct, typed outcome: exit CORPUS_SUPERSEDED_EXIT, recorded as `superseded`.
|
|
125
|
+
*/
|
|
126
|
+
export const CORPUS_SUPERSEDED_EXIT = 4;
|
|
127
|
+
export class CorpusSuperseded extends Error {
|
|
128
|
+
constructor(message) {
|
|
129
|
+
super(`[corpus-seed] superseded: ${message}`);
|
|
130
|
+
this.name = 'CorpusSuperseded';
|
|
131
|
+
this.code = 'CORPUS_SUPERSEDED';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const CODE_TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
|
|
136
|
+
const compareCodeTags = (left, right) => {
|
|
137
|
+
const a = CODE_TAG.exec(left).slice(1).map(Number);
|
|
138
|
+
const b = CODE_TAG.exec(right).slice(1).map(Number);
|
|
139
|
+
return Math.sign(a[0] - b[0] || a[1] - b[1] || a[2] - b[2]);
|
|
140
|
+
};
|
|
141
|
+
|
|
117
142
|
export async function runProtectedCorpusSeed({
|
|
118
143
|
argv = process.argv.slice(2),
|
|
119
144
|
env = process.env,
|
|
@@ -135,13 +160,15 @@ export async function runProtectedCorpusSeed({
|
|
|
135
160
|
const tag = cliArg(argv, '--corpus-tag');
|
|
136
161
|
const bundleFile = cliArg(argv, '--corpus-bundle');
|
|
137
162
|
const receiptFile = cliArg(argv, '--corpus-receipt');
|
|
163
|
+
// ADR-0091 D6.2: the generation's sealed coverage (the prepared artifact's source-coverage.json).
|
|
164
|
+
const coverageFile = cliArg(argv, '--corpus-coverage');
|
|
138
165
|
const target = cliArg(argv, '--target');
|
|
139
166
|
const repo = cliArg(argv, '--repo') || env.GITHUB_REPOSITORY;
|
|
140
167
|
const digestMatch = String(tag || '').match(/^corpus-sha256-([a-f0-9]{64})$/);
|
|
141
168
|
if (!digestMatch) corpusFailure('corpus tag must be corpus-sha256- followed by 64 lowercase hex characters');
|
|
142
169
|
if (repo !== env.GITHUB_REPOSITORY || repo !== 'stuinfla/ruvnet-brain') corpusFailure('repository does not match the protected workflow');
|
|
143
170
|
|
|
144
|
-
for (const [label, file] of [['bundle', bundleFile], ['receipt', receiptFile]]) {
|
|
171
|
+
for (const [label, file] of [['bundle', bundleFile], ['receipt', receiptFile], ['coverage', coverageFile]]) {
|
|
145
172
|
if (!file || !path.isAbsolute(file)) corpusFailure(`${label} must be an absolute regular file`);
|
|
146
173
|
try {
|
|
147
174
|
const stat = fs.lstatSync(file);
|
|
@@ -164,14 +191,28 @@ export async function runProtectedCorpusSeed({
|
|
|
164
191
|
} catch (error) {
|
|
165
192
|
corpusFailure(`corpus receipt is unreadable/corrupt (${error.message})`);
|
|
166
193
|
}
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
// DECOUPLED FROM main HEAD (2026-09-29 nightly redesign). The corpus is built at the APPROVED
|
|
195
|
+
// runtime -- the newest code release with a verified install aggregate -- whose source is on main's
|
|
196
|
+
// history but is usually NOT main HEAD. The old rule (target === GITHUB_SHA) stood the nightly down
|
|
197
|
+
// whenever main was ahead of the newest verified release. The guard that rule was protecting
|
|
198
|
+
// (independent review of ADR-0091 D3: never promote an OLDER runtime over the live code release)
|
|
199
|
+
// is now enforced directly: target must be the checkout, the receipt's builder, an ancestor of this
|
|
200
|
+
// protected run's GITHUB_SHA, and -- for a customer promotion -- the commit of --approved-tag, which
|
|
201
|
+
// must still be the NEWEST code release at publish time (below; otherwise CorpusSuperseded).
|
|
202
|
+
// Format checks run first; no value reaches a subprocess unvalidated.
|
|
203
|
+
if (!isHex(target, 40) || target !== head || target !== receipt.builderSourceSha || !isHex(env.GITHUB_SHA, 40)) {
|
|
204
|
+
corpusFailure('target must exactly equal HEAD and the corpus receipt builderSourceSha (and GITHUB_SHA must be a commit)');
|
|
205
|
+
}
|
|
206
|
+
const ancestry = run('git', ['merge-base', '--is-ancestor', target, env.GITHUB_SHA], {
|
|
207
|
+
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
208
|
+
});
|
|
209
|
+
if (ancestry.error || ancestry.status !== 0) corpusFailure(`target ${target} is not an ancestor of this run's GITHUB_SHA ${env.GITHUB_SHA}`);
|
|
210
|
+
const approvedTag = cliArg(argv, '--approved-tag');
|
|
211
|
+
if (promoteLatest) {
|
|
212
|
+
if (!CODE_TAG.test(String(approvedTag || ''))) corpusFailure('customer promotion requires --approved-tag vX.Y.Z (the approved runtime this corpus was built at)');
|
|
213
|
+
if (receipt.archiveManifestReleaseTag !== approvedTag) {
|
|
214
|
+
corpusFailure(`the archive ships runtime ${receipt.archiveManifestReleaseTag}, not the approved runtime ${approvedTag}`);
|
|
215
|
+
}
|
|
175
216
|
}
|
|
176
217
|
|
|
177
218
|
// Schema 3 (ADR-086 Step 15 / A6): the receipt binds the full provenance closure shipped INSIDE
|
|
@@ -261,11 +302,16 @@ export async function runProtectedCorpusSeed({
|
|
|
261
302
|
|| fs.statSync(recallReportFile).size !== receipt.recallReport.bytes) {
|
|
262
303
|
corpusFailure('detached repo-recall report bytes do not match the corpus receipt');
|
|
263
304
|
}
|
|
305
|
+
// ADR-0091 D7.3: a claimed retirement is recomputed from THIS generation's sealed coverage (the bytes
|
|
306
|
+
// published beside the archive as CORPUS-COVERAGE.json), never taken from the report's own claim.
|
|
307
|
+
const recallFixture = loadFixture();
|
|
264
308
|
try {
|
|
265
309
|
readRecallReport({
|
|
266
310
|
reportFile: recallReportFile,
|
|
267
311
|
archive: archiveIdentity,
|
|
268
|
-
expectedFixtureSha256:
|
|
312
|
+
expectedFixtureSha256: recallFixture.fixtureSha256,
|
|
313
|
+
coverageBytes: fs.readFileSync(coverageFile),
|
|
314
|
+
fixtureStores: recallFixture.questions.map((question) => question.store),
|
|
269
315
|
});
|
|
270
316
|
} catch (error) {
|
|
271
317
|
corpusFailure(`retrieval does not qualify this corpus for publication (${error.message})`);
|
|
@@ -280,12 +326,39 @@ export async function runProtectedCorpusSeed({
|
|
|
280
326
|
// here. It runs before any `gh` call so an untrue candidate never reaches the network.
|
|
281
327
|
try {
|
|
282
328
|
await verifyCorpusReceipt({
|
|
283
|
-
receiptFile, bundleFile, accuracyReportFile, recallReportFile, expectedBuilderSha: target, expectedArchiveSha256: archiveSha256,
|
|
329
|
+
receiptFile, bundleFile, accuracyReportFile, recallReportFile, coverageFile, expectedBuilderSha: target, expectedArchiveSha256: archiveSha256,
|
|
284
330
|
});
|
|
285
331
|
} catch (error) {
|
|
286
332
|
corpusFailure(`corpus receipt does not verify against the sealed archive (${error.message})`);
|
|
287
333
|
}
|
|
288
334
|
|
|
335
|
+
// ADR-0091 D6.2 + D10. The archive carries no coverage and the schema-3 receipt binds none, so this
|
|
336
|
+
// is the one place the publisher can SEE whether the generation is degraded. The coverage must be
|
|
337
|
+
// the coverage of THIS archive (bound store by store to the receipt), it is published beside the
|
|
338
|
+
// archive as CORPUS-COVERAGE.json + coverage-receipt.json (no receipt schema bump), and a
|
|
339
|
+
// generation with any carried or missing store is refused while D10 has recorded no soaked
|
|
340
|
+
// tolerant-validator transition -- installed clients would reject it. Local, before any network.
|
|
341
|
+
let coverageAssets;
|
|
342
|
+
try {
|
|
343
|
+
coverageAssets = writeCoverageAssets({
|
|
344
|
+
dir: fs.mkdtempSync(path.join(os.tmpdir(), 'corpus-coverage-assets-')),
|
|
345
|
+
coverageFile, generationTag: tag, archiveSha256, archiveBytes: archiveIdentity.bytes,
|
|
346
|
+
});
|
|
347
|
+
const degraded = bindCoverageToReceipt({
|
|
348
|
+
coverage: JSON.parse(fs.readFileSync(coverageAssets.coverageFile, 'utf8')), receipt,
|
|
349
|
+
});
|
|
350
|
+
if (degraded.carried.length + degraded.missing.length > 0) {
|
|
351
|
+
const decision = degradedPublication();
|
|
352
|
+
if (!decision.allowed) {
|
|
353
|
+
corpusFailure(`degraded generation (${degraded.carried.length} carried, ${degraded.missing.length} missing) `
|
|
354
|
+
+ `must not be published: ${decision.reason}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
if (String(error.message).startsWith('[corpus-seed]')) throw error;
|
|
359
|
+
corpusFailure(`the generation's sealed coverage does not bind this archive (${error.message})`);
|
|
360
|
+
}
|
|
361
|
+
|
|
289
362
|
// EVERY local proof happens before the first network call. `gh` must never be reached by a
|
|
290
363
|
// candidate that is already known to be unpublishable — that is the same discipline the deep
|
|
291
364
|
// verifyCorpusReceipt above follows, and a customer release with an unusable signature is exactly
|
|
@@ -307,16 +380,48 @@ export async function runProtectedCorpusSeed({
|
|
|
307
380
|
if (!Number.isFinite(Date.parse(generation))) corpusFailure('corpus receipt createdAt is not a readable generation timestamp');
|
|
308
381
|
}
|
|
309
382
|
|
|
310
|
-
const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
|
|
311
383
|
const ghCommand = env.RUVNET_GH_COMMAND || 'gh';
|
|
312
384
|
const ghPrefix = env.RUVNET_GH_SCRIPT ? [env.RUVNET_GH_SCRIPT] : [];
|
|
313
|
-
const
|
|
385
|
+
const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
386
|
+
const ghJson = (args, label) => {
|
|
387
|
+
const result = gh(args);
|
|
388
|
+
if (result.error || result.status !== 0) {
|
|
389
|
+
corpusFailure(`cannot read ${label} (${String(result.error?.message || result.stderr || result.stdout || '').trim() || `gh exited ${result.status}`})`);
|
|
390
|
+
}
|
|
391
|
+
try { return JSON.parse(String(result.stdout || 'null')); }
|
|
392
|
+
catch (error) { corpusFailure(`cannot parse ${label} (${error.message})`); }
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
if (promoteLatest) {
|
|
396
|
+
// PUBLISH-TIME RE-RESOLVE, before anything is written. Preparation takes hours; a code release
|
|
397
|
+
// may have been published meanwhile. The newest code release is selected exactly as
|
|
398
|
+
// scripts/approved-runtime.mjs selects it (non-draft, non-prerelease vX.Y.Z, highest version).
|
|
399
|
+
// Its signed install aggregate was re-verified for --approved-tag by the workflow step that built
|
|
400
|
+
// the runtime pin moments ago; what can change after that is only WHICH release is newest.
|
|
401
|
+
const listed = ghJson(['release', 'list', '--repo', repo, '--limit', '200', '--json', 'tagName,isDraft,isPrerelease'], 'the code release list');
|
|
402
|
+
const [newest] = (Array.isArray(listed) ? listed : [])
|
|
403
|
+
.filter((row) => !row?.isDraft && !row?.isPrerelease && CODE_TAG.test(String(row?.tagName || '')))
|
|
404
|
+
.map((row) => row.tagName).sort((a, b) => compareCodeTags(b, a));
|
|
405
|
+
if (!newest) corpusFailure(`no published code release is listed on ${repo}; the approved runtime ${approvedTag} cannot be confirmed`);
|
|
406
|
+
const order = compareCodeTags(newest, approvedTag);
|
|
407
|
+
if (order > 0) {
|
|
408
|
+
throw new CorpusSuperseded(`code release ${newest} was published after this corpus was built at ${approvedTag}; `
|
|
409
|
+
+ 'promoting it would put an older runtime over the live code release. The next night builds at the newer runtime.');
|
|
410
|
+
}
|
|
411
|
+
if (order < 0) corpusFailure(`approved runtime ${approvedTag} is newer than every published code release (newest ${newest})`);
|
|
412
|
+
const commit = ghJson(['api', `repos/${repo}/commits/${approvedTag}`], `the commit of ${approvedTag}`);
|
|
413
|
+
if (String(commit?.sha || '').toLowerCase() !== target) {
|
|
414
|
+
corpusFailure(`target ${target} is not the source of the approved runtime ${approvedTag} (${commit?.sha || 'unknown'})`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const viewArgs = ['release', 'view', tag, '--json', 'tagName', '--repo', repo];
|
|
419
|
+
const view = gh(viewArgs);
|
|
314
420
|
if (!view.error && view.status === 0) corpusFailure(`release ${tag} already exists; refusing to overwrite immutable corpus seed`);
|
|
315
421
|
const viewError = String(view.error?.message || view.stderr || view.stdout || '');
|
|
316
422
|
if (!/(release not found|no release found)/i.test(viewError)) corpusFailure(`cannot prove ${tag} is absent (${viewError.trim() || `gh exited ${view.status}`})`);
|
|
317
423
|
|
|
318
424
|
const receiptSha256 = sha256File(receiptFile);
|
|
319
|
-
const gh = (args) => run(ghCommand, [...ghPrefix, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
320
425
|
|
|
321
426
|
if (!promoteLatest) {
|
|
322
427
|
// BOOTSTRAP/RECOVERY seeds stay exactly as ADR-086's original contract left them: an immutable
|
|
@@ -344,6 +449,7 @@ export async function runProtectedCorpusSeed({
|
|
|
344
449
|
'--title', `Immutable corpus seed ${archiveSha256.slice(0, 16)}`,
|
|
345
450
|
'--notes', notes,
|
|
346
451
|
bundleFile, receiptFile, accuracyReportFile, recallReportFile,
|
|
452
|
+
coverageAssets.coverageFile, coverageAssets.receiptFile,
|
|
347
453
|
];
|
|
348
454
|
const create = gh(createArgs);
|
|
349
455
|
if (create.error || create.status !== 0) {
|
|
@@ -395,7 +501,8 @@ export async function runProtectedCorpusSeed({
|
|
|
395
501
|
// (or the next night's dispatcher) that downloads the archive must be able to reverify it against
|
|
396
502
|
// the identity it was actually measured under — the blocking recall gate AND the C3 diagnostic it
|
|
397
503
|
// scored 59.0% on, so nobody has to take either number on trust.
|
|
398
|
-
const assetFiles = [bundleFile, signatureFile, digestFile, receiptFile, accuracyReportFile, recallReportFile
|
|
504
|
+
const assetFiles = [bundleFile, signatureFile, digestFile, receiptFile, accuracyReportFile, recallReportFile,
|
|
505
|
+
coverageAssets.coverageFile, coverageAssets.receiptFile];
|
|
399
506
|
const create = gh([
|
|
400
507
|
'release', 'create', tag,
|
|
401
508
|
'--draft',
|
|
@@ -425,13 +532,22 @@ export async function runProtectedCorpusSeed({
|
|
|
425
532
|
corpusFailure(`corpus promotion to latest failed (${String(promote.error?.message || promote.stderr || promote.stdout || '').trim()})`);
|
|
426
533
|
}
|
|
427
534
|
|
|
428
|
-
|
|
535
|
+
// `isLatest` is NOT a `gh release view` field (gh 2.101.0: "Unknown JSON field"; it exists only on
|
|
536
|
+
// `gh release list`), so asking for it made this confirmation fail against the real CLI every time.
|
|
537
|
+
// Latest-ness is read from the one authoritative endpoint instead: releases/latest must BE this tag.
|
|
538
|
+
// tests/unit/gh-json-fields.test.mjs checks every --json field list against the captured real CLI.
|
|
539
|
+
const finalView = gh(['release', 'view', tag, '--json', 'tagName,isDraft,isPrerelease,assets', '--repo', repo]);
|
|
429
540
|
if (finalView.error || finalView.status !== 0) corpusFailure('cannot confirm the promoted corpus release');
|
|
430
541
|
let promoted;
|
|
431
542
|
try { promoted = JSON.parse(String(finalView.stdout || 'null')); }
|
|
432
543
|
catch (error) { corpusFailure(`cannot read the promoted corpus release (${error.message})`); }
|
|
544
|
+
const latestNow = gh(['api', `repos/${repo}/releases/latest`]);
|
|
545
|
+
let latestTag = null;
|
|
546
|
+
if (!latestNow.error && latestNow.status === 0) {
|
|
547
|
+
try { latestTag = JSON.parse(String(latestNow.stdout || 'null'))?.tag_name ?? null; } catch { latestTag = null; }
|
|
548
|
+
}
|
|
433
549
|
const promotedAssets = (promoted?.assets || []).map((asset) => asset?.name).sort();
|
|
434
|
-
if (promoted?.tagName !== tag || promoted.isDraft !== false ||
|
|
550
|
+
if (promoted?.tagName !== tag || promoted.isDraft !== false || latestTag !== tag
|
|
435
551
|
|| promoted.isPrerelease !== false || JSON.stringify(promotedAssets) !== JSON.stringify(expectedAssets)) {
|
|
436
552
|
corpusFailure('corpus release did not reach a complete, non-draft, non-prerelease latest state');
|
|
437
553
|
}
|
|
@@ -448,7 +564,13 @@ if (CORPUS_SEED) {
|
|
|
448
564
|
console.log(JSON.stringify({ ok: true, mode: 'corpus-seed', ...result }, null, 2));
|
|
449
565
|
} catch (error) {
|
|
450
566
|
console.error(error.message);
|
|
451
|
-
|
|
567
|
+
if (error instanceof CorpusSuperseded) {
|
|
568
|
+
// Typed, not red: stdout carries the outcome the workflow records.
|
|
569
|
+
console.log(JSON.stringify({ ok: false, mode: 'corpus-seed', outcome: 'superseded', reason: error.message }));
|
|
570
|
+
process.exitCode = CORPUS_SUPERSEDED_EXIT;
|
|
571
|
+
} else {
|
|
572
|
+
process.exitCode = 1;
|
|
573
|
+
}
|
|
452
574
|
}
|
|
453
575
|
} else {
|
|
454
576
|
|
|
@@ -556,6 +678,22 @@ if (PUBLISH) {
|
|
|
556
678
|
process.exit(1);
|
|
557
679
|
}
|
|
558
680
|
}
|
|
681
|
+
// ADR-0091 D6.6 — THE BACKWARD-MOVE RACE. Release QE sealed the corpus generation this bundle was
|
|
682
|
+
// built from; publication happens later, after owner approval. Clients always accept a code release
|
|
683
|
+
// and drop their corpusGeneration marker when they install one (kb/forge-update.mjs), so publishing
|
|
684
|
+
// a bundle built from generation G after G+1 already shipped rolls every user back one night.
|
|
685
|
+
// Re-resolve with the SAME resolver, before any asset upload, and refuse on any difference -- or on
|
|
686
|
+
// any answer that could not prove there is no newer generation.
|
|
687
|
+
try {
|
|
688
|
+
const guard = await assertNoNewerCorpusGeneration({
|
|
689
|
+
sealedFile: assets.corpusSeedPath, repo: 'stuinfla/ruvnet-brain', runtimeRoot: ROOT,
|
|
690
|
+
});
|
|
691
|
+
console.log(` corpus seed still current at publish time: ${guard.origin} ${guard.tag}`);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
console.error(`\n${c.r('✗ GATE FAILED: corpus generation moved after release QE')} ${c.dim(error.message)}`);
|
|
694
|
+
console.error(`${c.r(' NOT shipped. Re-run release QE so this release is built from the newest generation.')}\n`);
|
|
695
|
+
process.exit(1);
|
|
696
|
+
}
|
|
559
697
|
const bundleSha256 = fs.readFileSync(assets.bundleDigestPath, 'utf8').trim().split(/\s+/)[0];
|
|
560
698
|
if (!/^[a-f0-9]{64}$/i.test(bundleSha256)) {
|
|
561
699
|
console.error(`\n${c.r('✗ GATE FAILED: release digest is not a SHA-256 value')}`);
|
|
@@ -5,10 +5,12 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import readline from 'node:readline';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { canonicalJson, digest, validateCoverageLedger } from './coverage-integrity.mjs';
|
|
8
|
+
import { canonicalJson, digest, eligibleRepositoryStanding, validateCoverageLedger } from './coverage-integrity.mjs';
|
|
9
|
+
import { fixtureDenominator } from './fixture-denominator.mjs';
|
|
10
|
+
import { passageMatches } from './retrieval-passage-identity.mjs';
|
|
9
11
|
|
|
10
12
|
// Both release phases resolve against an explicit installed context, never the checkout.
|
|
11
|
-
export async function resolveInstalledCanaryCitation({ kbDir, matched, expected, passageFileDigests = new Map() }) {
|
|
13
|
+
export async function resolveInstalledCanaryCitation({ kbDir, matched, expected, passageFileDigests = new Map(), contentMap }) {
|
|
12
14
|
if (!path.isAbsolute(kbDir || '')) throw new Error('installed canary KB path must be absolute');
|
|
13
15
|
if (String(matched?.repo || '').toLowerCase() !== expected.repo || matched?.path !== expected.path) return { resolved: false };
|
|
14
16
|
if (!/^[a-z0-9][a-z0-9._-]*$/i.test(expected.repo)) throw new Error('installed citation repository violates containment');
|
|
@@ -34,7 +36,7 @@ export async function resolveInstalledCanaryCitation({ kbDir, matched, expected,
|
|
|
34
36
|
let record;
|
|
35
37
|
try { record = JSON.parse(line); } catch { continue; }
|
|
36
38
|
if (record?.path !== expected.path) continue;
|
|
37
|
-
if (
|
|
39
|
+
if (!passageMatches(record, expected.passageSha256, contentMap)) continue;
|
|
38
40
|
const text = record.fullText || record.text;
|
|
39
41
|
if (typeof text !== 'string' || !text || typeof matched.text !== 'string' || !matched.text.includes(text)) continue;
|
|
40
42
|
passageSha256 = expected.passageSha256;
|
|
@@ -204,8 +206,10 @@ export function auditOracleCoverage({ coverage, queryEvidence, exemptions = null
|
|
|
204
206
|
const checked = validateCoverageLedger(coverage);
|
|
205
207
|
if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
|
|
206
208
|
validateRetrievalQueryEvidence(queryEvidence);
|
|
209
|
+
// ADR-0091 D5: a shipped store is CURRENT or STALE with a verified carry (its bytes ship). A
|
|
210
|
+
// MISSING-with-failure store ships nothing; the fixture-vs-available denominator is D6.4's change.
|
|
207
211
|
const eligibleRows = coverage.rows.filter((row) => row.kind === 'repository'
|
|
208
|
-
&& row.disposition === 'eligible' && row
|
|
212
|
+
&& row.disposition === 'eligible' && eligibleRepositoryStanding(row) === 'shipped');
|
|
209
213
|
const eligible = ordered(eligibleRows.map(storeOf));
|
|
210
214
|
if (!eligible.length || new Set(eligible).size !== eligible.length || eligible.some((store) => !store)) {
|
|
211
215
|
throw new Error('eligible coverage denominator is invalid');
|
|
@@ -310,9 +314,20 @@ export function validateRetrievalCanaryPlan(plan) {
|
|
|
310
314
|
'legacy population denominator');
|
|
311
315
|
checkedSet(plan.denominator.legacySelectedStores, plan.denominator.legacySelectedStoreSetSha256,
|
|
312
316
|
'legacy selected denominator');
|
|
317
|
+
// ADR-0091 D6.4: `eligibleStores` is the FROZEN FIXTURE denominator (it must equal the oracle's store
|
|
318
|
+
// set exactly, as before). Retired fixture stores are excluded from questioning; eligible stores the
|
|
319
|
+
// fixture does not cover are recorded, never questioned and never blocking.
|
|
320
|
+
checkedSet(plan.denominator.retiredFixtureStores, plan.denominator.retiredFixtureStoreSetSha256, 'retired fixture denominator');
|
|
321
|
+
checkedSet(plan.denominator.unfixturedEligibleStores, plan.denominator.unfixturedEligibleStoreSetSha256,
|
|
322
|
+
'unfixtured eligible record');
|
|
313
323
|
if (setDigest(plan.denominator.eligibleStores) !== plan.oracle.queryStoreSetSha256) {
|
|
314
324
|
throw new Error('oracle denominator differs from eligible coverage');
|
|
315
325
|
}
|
|
326
|
+
if (plan.denominator.retiredFixtureStores.some((store) => !plan.denominator.eligibleStores.includes(store))
|
|
327
|
+
|| plan.denominator.unfixturedEligibleStores.some((store) => plan.denominator.eligibleStores.includes(store))
|
|
328
|
+
|| plan.denominator.unfixturedEligibleCount !== plan.denominator.unfixturedEligibleStores.length) {
|
|
329
|
+
throw new Error('retrieval canary fixture denominator is inconsistent');
|
|
330
|
+
}
|
|
316
331
|
if (new Set(ids).size !== ids.length) throw new Error('retrieval canary plan has duplicate case ids');
|
|
317
332
|
const hasDelta = plan.cases.some(({ cohort }) => cohort === 'delta');
|
|
318
333
|
if ((!hasDelta && plan.noDelta !== true) || (hasDelta && plan.noDelta === true)
|
|
@@ -352,14 +367,18 @@ export function validatePlanAgainstCoverage(plan, coverage, { allowObservedBasel
|
|
|
352
367
|
if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
|
|
353
368
|
const generation = coverage.kind === 'ruvnet-brain-release-coverage'
|
|
354
369
|
? coverage.releaseCoverageGeneration : coverage.coverageGeneration;
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (
|
|
370
|
+
// Recomputed from the coverage and the plan's own sealed fixture, never trusted from the plan.
|
|
371
|
+
const denominator = fixtureDenominator({ coverage, fixtureStores: Object.keys(plan.oracle.evidence.queries) });
|
|
372
|
+
if (denominator.blocking.length) throw new Error(`fixture store(s) neither available nor retired: ${denominator.blocking.map(({ store }) => store).join(', ')}`);
|
|
373
|
+
const eligible = denominator.questioned;
|
|
374
|
+
if (!eligible.length) throw new Error('eligible coverage denominator is invalid');
|
|
358
375
|
const baseline = new Set(plan.baseline.stores);
|
|
359
376
|
const delta = eligible.filter((store) => !baseline.has(store));
|
|
360
377
|
const legacy = eligible.filter((store) => baseline.has(store));
|
|
361
378
|
if (generation !== plan.coverage.releaseCoverageGeneration
|
|
362
|
-
|| canonicalJson(
|
|
379
|
+
|| canonicalJson(denominator.fixture) !== canonicalJson(plan.denominator.eligibleStores)
|
|
380
|
+
|| canonicalJson(denominator.retired) !== canonicalJson(plan.denominator.retiredFixtureStores)
|
|
381
|
+
|| canonicalJson(denominator.unfixturedEligible) !== canonicalJson(plan.denominator.unfixturedEligibleStores)
|
|
363
382
|
|| canonicalJson(delta) !== canonicalJson(plan.denominator.deltaStores)
|
|
364
383
|
|| canonicalJson(legacy) !== canonicalJson(plan.denominator.legacyPopulationStores)
|
|
365
384
|
|| plan.denominator.legacySelectedStores.some((store) => !legacy.includes(store))) {
|
|
@@ -368,7 +387,8 @@ export function validatePlanAgainstCoverage(plan, coverage, { allowObservedBasel
|
|
|
368
387
|
return plan;
|
|
369
388
|
}
|
|
370
389
|
export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, coverageIdentity = null, queryEvidence, assetsDir = '.',
|
|
371
|
-
readPassages = defaultReadPassages, legacySampleSize, allowNoDelta = false
|
|
390
|
+
readPassages = defaultReadPassages, legacySampleSize, allowNoDelta = false, contentMap, knownHitStores = null,
|
|
391
|
+
notice = (message) => process.stderr.write(`${message}\n`) } = {}) {
|
|
372
392
|
const checked = validateCoverageLedger(coverage);
|
|
373
393
|
if (!checked.valid) throw new Error(`coverage ledger is invalid: ${checked.failures.join('; ')}`);
|
|
374
394
|
const coverageGeneration = coverage.kind === 'ruvnet-brain-release-coverage'
|
|
@@ -417,20 +437,19 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
|
|
|
417
437
|
}
|
|
418
438
|
validateRetrievalQueryEvidence(queryEvidence);
|
|
419
439
|
if (queryEvidence.sourceCommit === candidate.sourceSha) throw new Error('independent query source is not pre-candidate');
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
440
|
+
// ADR-0091 D6.4: fixture ⊆ available. The frozen fixture no longer has to EQUAL a living eligible
|
|
441
|
+
// set; every fixture store must ship (or be verified retired), and eligible stores the fixture does
|
|
442
|
+
// not cover are recorded as unfixturedEligible, never blocking.
|
|
443
|
+
let denominator;
|
|
444
|
+
try { denominator = fixtureDenominator({ coverage, fixtureStores: Object.keys(queryEvidence.queries) }); }
|
|
445
|
+
catch (error) { throw new Error(`eligible repository coverage is incomplete (${error.message})`); }
|
|
446
|
+
if (denominator.blocking.length) {
|
|
447
|
+
throw new Error(`independent query oracle names ${denominator.blocking.length} fixture store(s) that are neither shipped nor `
|
|
448
|
+
+ `verified retired: ${denominator.blocking.map(({ store, status }) => `${store}:${status ?? 'no-row'}`).join(', ')}`);
|
|
449
|
+
}
|
|
450
|
+
const eligibleStores = denominator.fixture;
|
|
451
|
+
const eligible = denominator.questionedRows;
|
|
452
|
+
if (!eligible.length) throw new Error('eligible repository coverage is incomplete');
|
|
434
453
|
const baselineStores = new Set(baseline.stores.map((name) => String(name).toLowerCase()));
|
|
435
454
|
const delta = eligible.filter((row) => !baselineStores.has(storeOf(row)));
|
|
436
455
|
const legacyPool = eligible.filter((row) => baselineStores.has(storeOf(row)));
|
|
@@ -438,12 +457,47 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
|
|
|
438
457
|
const passages = new Map(eligible.map((row) => [storeOf(row), readPassages(assetsDir, storeOf(row))]));
|
|
439
458
|
const rankedLegacy = legacyPool.map((row) => ({ row, count: passages.get(storeOf(row)).length }))
|
|
440
459
|
.sort((a, b) => a.count - b.count || storeOf(a.row).localeCompare(storeOf(b.row)));
|
|
460
|
+
// The legacy sample is drawn only from stores whose sealed passage still exists, unchanged, exactly
|
|
461
|
+
// once in the shipped store. When upstream edits the very file a fixture question was written against,
|
|
462
|
+
// that question can no longer identify its passage — the fixture is stale for that store, which says
|
|
463
|
+
// nothing about retrieval. Such stores stay in the sealed POPULATION (recomputed from coverage by
|
|
464
|
+
// validatePlanAgainstCoverage) but cannot be sampled; they are named below so it is never silent, and
|
|
465
|
+
// the nightly per-repository recall gate still exercises every one of them by file path.
|
|
466
|
+
const sealedPassageResolves = (store) => {
|
|
467
|
+
const evidence = queryEvidence.queries[store];
|
|
468
|
+
return Boolean(evidence) && expectedSources(evidence.expected).every((source) =>
|
|
469
|
+
passages.get(store).filter((row) => row.path === source.path && passageMatches(row, source.passageSha256, contentMap)).length === 1);
|
|
470
|
+
};
|
|
471
|
+
// INTEGRITY, NOT QUALITY. When the generation being shipped carries its own repo-recall measurement,
|
|
472
|
+
// `knownHitStores` names the stores that measurement retrieved. The release sample is then drawn from
|
|
473
|
+
// them, so the canary proves the SHIPPED, INSTALLED bundle reproduces what the generation measured on
|
|
474
|
+
// the same stores (a packaging, index, model or runtime break shows up as a miss on a store that hit).
|
|
475
|
+
// It deliberately does NOT re-judge stores the generation already missed: whole-corpus retrieval quality
|
|
476
|
+
// is the recall report's job and stays visible there (and in the corpus watchdog), because sampling ~19
|
|
477
|
+
// stores at an absolute 98% bar is a coin flip for any corpus below ~98% true recall (measured
|
|
478
|
+
// 2026-09-30: previous corpus 18/19, fresh corpus 17/19 on the same questions). Without a measurement
|
|
479
|
+
// (the committed bootstrap seed) nothing is filtered and the historical behaviour is unchanged.
|
|
480
|
+
const measuredHit = (store) => !knownHitStores || knownHitStores.has(store);
|
|
441
481
|
const strata = new Map();
|
|
482
|
+
const staleFixtureStores = [];
|
|
483
|
+
const generationMissStores = [];
|
|
442
484
|
rankedLegacy.forEach((entry, index) => {
|
|
443
485
|
const stratum = Math.min(3, Math.floor(index * 4 / rankedLegacy.length));
|
|
486
|
+
const store = storeOf(entry.row);
|
|
487
|
+
if (!sealedPassageResolves(store)) { staleFixtureStores.push(store); return; }
|
|
488
|
+
if (!measuredHit(store)) { generationMissStores.push(store); return; }
|
|
444
489
|
if (!strata.has(stratum)) strata.set(stratum, []);
|
|
445
490
|
strata.get(stratum).push(entry);
|
|
446
491
|
});
|
|
492
|
+
if (generationMissStores.length) {
|
|
493
|
+
notice(`[retrieval-canary] ${generationMissStores.length} of ${rankedLegacy.length} fixture store(s) not sampled: the generation's own `
|
|
494
|
+
+ `recall measurement did not retrieve their sealed file (retrieval-quality debt, tracked by the recall report, not re-judged here): `
|
|
495
|
+
+ `${ordered(generationMissStores).join(', ')}`);
|
|
496
|
+
}
|
|
497
|
+
if (staleFixtureStores.length) {
|
|
498
|
+
notice(`[retrieval-canary] ${staleFixtureStores.length} of ${rankedLegacy.length} fixture store(s) excluded from the legacy sample: `
|
|
499
|
+
+ `their sealed passage no longer exists unchanged in the shipped store: ${ordered(staleFixtureStores).join(', ')}`);
|
|
500
|
+
}
|
|
447
501
|
// Source-only releases retain the same corpus sample; the plan still seals exact release bytes.
|
|
448
502
|
const samplingGeneration = coverage.kind === 'ruvnet-brain-release-coverage'
|
|
449
503
|
? coverage.corpusCoverage.coverageGeneration : coverageGeneration;
|
|
@@ -470,7 +524,7 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
|
|
|
470
524
|
const observedPassageCount = passageCount ?? passages.get(store).length;
|
|
471
525
|
const evidence = queryEvidence.queries[store];
|
|
472
526
|
if (!evidence || expectedSources(evidence.expected).some((source) =>
|
|
473
|
-
passages.get(store).filter((row) => row.path === source.path &&
|
|
527
|
+
passages.get(store).filter((row) => row.path === source.path && passageMatches(row, source.passageSha256, contentMap)).length !== 1)) {
|
|
474
528
|
throw new Error(`${store} has no sealed independent query evidence`);
|
|
475
529
|
}
|
|
476
530
|
return {
|
|
@@ -502,6 +556,11 @@ export function buildRetrievalCanaryPlan({ coverage, baseline, candidate, covera
|
|
|
502
556
|
denominator: {
|
|
503
557
|
eligibleStores,
|
|
504
558
|
eligibleStoreSetSha256: setDigest(eligibleStores),
|
|
559
|
+
retiredFixtureStores: denominator.retired,
|
|
560
|
+
retiredFixtureStoreSetSha256: setDigest(denominator.retired),
|
|
561
|
+
unfixturedEligibleStores: denominator.unfixturedEligible,
|
|
562
|
+
unfixturedEligibleStoreSetSha256: setDigest(denominator.unfixturedEligible),
|
|
563
|
+
unfixturedEligibleCount: denominator.unfixturedEligible.length,
|
|
505
564
|
deltaStores: ordered(delta.map(storeOf)),
|
|
506
565
|
deltaStoreSetSha256: setDigest(delta.map(storeOf)),
|
|
507
566
|
legacyPopulationStores: ordered(legacyPool.map(storeOf)),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// How the retrieval fixture recognises "the expected passage" across store rebuilds.
|
|
2
|
+
//
|
|
3
|
+
// data/retrieval-query-evidence.json pins each expected passage by digest(row), where a row is
|
|
4
|
+
// { id, path, text, title }. Until 2026-09-29 the `id` was an ordinal ("2824"); the CI corpus builder
|
|
5
|
+
// now writes content-addressed ids ("chunk:<hash>"). The same passage — identical path, title and text —
|
|
6
|
+
// therefore changed digest with no content change, and every release that consumed a freshly built
|
|
7
|
+
// generation failed with "<store> has no sealed independent query evidence" (4.3.37 preflight, 2026-09-29).
|
|
8
|
+
//
|
|
9
|
+
// The fixture bytes are FROZEN on purpose: its sha256 is what corpus-next-seed judges a generation's
|
|
10
|
+
// recall report against, so editing it would make the newest generation an incompatible seed and force
|
|
11
|
+
// a full multi-hour rebuild. Instead this module adds a second, id-independent identity, looked up
|
|
12
|
+
// through a committed map (pinned digest -> content digest) derived once, mechanically, from the last
|
|
13
|
+
// corpus built with ordinal ids (scripts/derive-passage-content-map.mjs). The map can only ADD
|
|
14
|
+
// acceptance for a row whose path, title and text equal the row the fixture originally pinned.
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { digest } from './coverage-integrity.mjs';
|
|
19
|
+
|
|
20
|
+
export const CONTENT_MAP_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
|
21
|
+
'..', 'data', 'retrieval-passage-content-digests.json');
|
|
22
|
+
export const CONTENT_MAP_KIND = 'ruvnet-brain-retrieval-passage-content-digests';
|
|
23
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
24
|
+
|
|
25
|
+
/** Digest of everything about a passage row except its (build-dependent) id. */
|
|
26
|
+
export function passageContentDigest(row) {
|
|
27
|
+
const { id: _id, ...rest } = row ?? {};
|
|
28
|
+
return digest(rest);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** pinned digest -> content digest. A missing file is an empty map: legacy exact-digest matching only. */
|
|
32
|
+
export function loadContentMap(file = CONTENT_MAP_FILE) {
|
|
33
|
+
let parsed;
|
|
34
|
+
try { parsed = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (error) {
|
|
35
|
+
if (error.code === 'ENOENT') return new Map();
|
|
36
|
+
throw new Error(`passage content map is unreadable: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
if (parsed?.kind !== CONTENT_MAP_KIND || parsed.schemaVersion !== 1
|
|
39
|
+
|| !parsed.entries || typeof parsed.entries !== 'object' || Array.isArray(parsed.entries)) {
|
|
40
|
+
throw new Error('passage content map is malformed');
|
|
41
|
+
}
|
|
42
|
+
const map = new Map();
|
|
43
|
+
for (const [pinned, content] of Object.entries(parsed.entries)) {
|
|
44
|
+
if (!HEX64.test(pinned) || !HEX64.test(String(content))) throw new Error('passage content map holds a non-sha256 entry');
|
|
45
|
+
map.set(pinned, content);
|
|
46
|
+
}
|
|
47
|
+
return map;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let cachedMap = null;
|
|
51
|
+
const defaultMap = () => (cachedMap ??= loadContentMap());
|
|
52
|
+
|
|
53
|
+
/** Does this row satisfy a fixture pin: exact digest, or (when the map knows the pin) equal content. */
|
|
54
|
+
export function passageMatches(row, pinnedSha256, map = defaultMap()) {
|
|
55
|
+
if (digest(row) === pinnedSha256) return true;
|
|
56
|
+
const content = map.get(pinnedSha256);
|
|
57
|
+
return Boolean(content) && passageContentDigest(row) === content;
|
|
58
|
+
}
|
|
@@ -341,7 +341,13 @@ function assertExclusionEvidence(repo, exclusion, upstreamSha) {
|
|
|
341
341
|
}
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
|
|
344
|
+
// ADR-0091 D5: `outcome` is what reconciliation recorded for this store when its refresh FAILED this
|
|
345
|
+
// generation -- { carry } (previous bytes kept, already re-hashed against the ledger), { failure } (no
|
|
346
|
+
// prior bytes), or { integrity } (carried bytes failed that re-hash). The status token is always the
|
|
347
|
+
// one the evidence proves; the record is attached only where it agrees with that status, so a carry
|
|
348
|
+
// can never dress up a row the bytes do not support. Rows are annotated BEFORE sealing, so the
|
|
349
|
+
// coverage generation digest covers them.
|
|
350
|
+
export function classifyRepository(repo, evidence, exclusion = null, outcome = null) {
|
|
345
351
|
const upstreamSha = repo.defaultBranchRef?.target?.oid || null;
|
|
346
352
|
const activeExclusion = Boolean(exclusion && String(exclusion.pushedAt || '') !== ''
|
|
347
353
|
&& String(exclusion.pushedAt) === String(repo.pushedAt || ''));
|
|
@@ -373,6 +379,14 @@ export function classifyRepository(repo, evidence, exclusion = null) {
|
|
|
373
379
|
else if (evidence.receipt.sourceCommit !== upstreamSha) { status = 'STALE'; reasons.push('receipt sourceCommit differs from upstream HEAD'); }
|
|
374
380
|
else if (!evidence.bytesVerified) { status = 'FAILED'; reasons.push('RVF bytes do not match receipt'); }
|
|
375
381
|
else if (!evidence.passagesPresent) { status = 'FAILED'; reasons.push('passage inventory is absent'); }
|
|
382
|
+
const disposed = isIngestibleDisposition(disposition);
|
|
383
|
+
if (disposed && outcome?.integrity && ['CURRENT', 'STALE'].includes(status)) {
|
|
384
|
+
status = 'FAILED';
|
|
385
|
+
reasons.push(outcome.integrity);
|
|
386
|
+
}
|
|
387
|
+
const record = {};
|
|
388
|
+
if (disposed && outcome?.carry && status === 'STALE') record.carry = { ...outcome.carry };
|
|
389
|
+
if (disposed && outcome?.failure && status === 'MISSING') record.failure = { ...outcome.failure };
|
|
376
390
|
return {
|
|
377
391
|
key: repo.fullName ? `repo:${repo.fullName.toLowerCase()}` : `repo:${repo.databaseId}`,
|
|
378
392
|
kind: 'repository',
|
|
@@ -391,6 +405,7 @@ export function classifyRepository(repo, evidence, exclusion = null) {
|
|
|
391
405
|
cardPresent: evidence.cardPresent },
|
|
392
406
|
status,
|
|
393
407
|
reasons,
|
|
408
|
+
...record,
|
|
394
409
|
};
|
|
395
410
|
}
|
|
396
411
|
|
|
@@ -567,7 +582,7 @@ export function explainCoverageDrift(recorded, current) {
|
|
|
567
582
|
// where they are source-controlled (`<repo>/kb`) unless the caller names both directories, as the
|
|
568
583
|
// release path does with `--assets`.
|
|
569
584
|
export function buildCoverage({ owner = 'ruvnet', env = process.env, home = os.homedir(), kbDir = null, policyDir = null,
|
|
570
|
-
observation = null, gh = runGh, now = () => new Date().toISOString() } = {}) {
|
|
585
|
+
observation = null, gh = runGh, now = () => new Date().toISOString(), storeOutcomes = null } = {}) {
|
|
571
586
|
policyDir ??= kbDir ?? path.join(ROOT, 'kb');
|
|
572
587
|
kbDir ??= storeRoot(env, home);
|
|
573
588
|
if (rootNeverMaterialized(kbDir)) {
|
|
@@ -597,7 +612,8 @@ export function buildCoverage({ owner = 'ruvnet', env = process.env, home = os.h
|
|
|
597
612
|
const exclusions = fs.existsSync(exclusionsPath) ? JSON.parse(fs.readFileSync(exclusionsPath, 'utf8')) : {};
|
|
598
613
|
const rows = repositories.rows.map((repo) => {
|
|
599
614
|
const store = storeName(repo.storeName || repo.name);
|
|
600
|
-
return classifyRepository(repo, artifactEvidence(kbDir, ledger, cardStores, store), exclusions[store] || null
|
|
615
|
+
return classifyRepository(repo, artifactEvidence(kbDir, ledger, cardStores, store), exclusions[store] || null,
|
|
616
|
+
storeOutcomes?.[store.toLowerCase()] || null);
|
|
601
617
|
});
|
|
602
618
|
const gistEvidence = { ...artifactEvidence(kbDir, ledger, cardStores, 'ruv-gists'), sources: gistSources };
|
|
603
619
|
try {
|
package/scripts/sync-census.mjs
CHANGED
|
Binary file
|
package/scripts/wired-check.mjs
CHANGED
|
@@ -120,6 +120,10 @@ const STANDALONE = [
|
|
|
120
120
|
['self-update', 'author-run candidate rebuild; --apply is guarded by worktree-integrity.mjs and is not scheduled'],
|
|
121
121
|
['ingest-new-repos', 'author-run corpus expansion; --apply is guarded by worktree-integrity.mjs and is not scheduled'],
|
|
122
122
|
['count-chunks', 'human-run CLI — recount + restamp chunk surfaces (--check for drift); no scheduler'],
|
|
123
|
+
['derive-passage-content-map', 'human-run maintainer tool — regenerates data/retrieval-passage-content-digests.json '
|
|
124
|
+
+ 'from a corpus built with ordinal passage ids, only when the frozen fixture changes; '
|
|
125
|
+
+ 'tests/unit/retrieval-passage-identity.test.mjs fails if the committed map stops matching the fixture, '
|
|
126
|
+
+ 'so a stale map cannot go unnoticed and there is nothing to schedule'],
|
|
123
127
|
['brain-stamp', 'invoked by the author-run self-update.mjs candidate builder'],
|
|
124
128
|
['lesson-promote', 'human-run CLI — promotion is manual (--apply); no scheduler yet (automation is ADR-029 #4, open)'],
|
|
125
129
|
['behavioral-l1-l4', 'behavioural harness invoked by its own test file — not a product path'],
|