ruvnet-brain 4.3.25 → 4.3.27

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 (51) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +19 -4
  3. package/console/app.js +48 -0
  4. package/console/index.html +19 -0
  5. package/console/style.css +26 -0
  6. package/data/model-catalog.json +1 -1
  7. package/docs/RELEASE-NOTES-4.0.md +4 -3
  8. package/kb/capability-only.mjs +27 -0
  9. package/kb/capability-summaries/cognitum-ruos/CAPABILITIES.md +26 -0
  10. package/kb/verify-citation.mjs +16 -4
  11. package/package.json +5 -1
  12. package/plugin/.claude-plugin/plugin.json +1 -1
  13. package/plugin/.codex-plugin/plugin.json +1 -1
  14. package/plugin/docs/RELEASE-NOTES-4.0.md +4 -3
  15. package/plugin/hooks/codex-hooks.json +6 -1
  16. package/plugin/hooks/hook-contracts.json +24 -2
  17. package/plugin/hooks/hooks.json +6 -1
  18. package/plugin/scripts/capacity-aware-parallel-work.mjs +200 -0
  19. package/plugin/scripts/codex-hook-adapter.mjs +37 -0
  20. package/plugin/scripts/continuity-hook-policy.mjs +4 -0
  21. package/plugin/scripts/coverage-integrity.mjs +17 -0
  22. package/plugin/scripts/hook-shim.mjs +1 -0
  23. package/plugin/scripts/lesson-gate.mjs +4 -1
  24. package/plugin/scripts/project-progression-reader.mjs +12 -1
  25. package/plugin/scripts/project-progression-session-start.mjs +24 -6
  26. package/plugin/scripts/project-progression-store.mjs +76 -4
  27. package/plugin/skills/release-proof/SKILL.md +28 -4
  28. package/plugin/skills/release-proof/scripts/release-proof.mjs +48 -24
  29. package/scripts/brain-novice-50.mjs +14 -16
  30. package/scripts/build-bundle.mjs +2 -0
  31. package/scripts/corpus-dispatch-receipt.mjs +22 -0
  32. package/scripts/corpus-reconcile.mjs +26 -4
  33. package/scripts/doc-currency.mjs +12 -4
  34. package/scripts/eval-brain.mjs +7 -5
  35. package/scripts/gist-git-transport.mjs +218 -0
  36. package/scripts/gist-receipts.mjs +168 -38
  37. package/scripts/ingest-gists.mjs +5 -2
  38. package/scripts/installed-brain-health.mjs +99 -0
  39. package/scripts/onboarding-console.mjs +45 -1
  40. package/scripts/public-inputs.mjs +2 -1
  41. package/scripts/public-verification-inputs.mjs +14 -6
  42. package/scripts/refresh-capability-only-store.mjs +143 -0
  43. package/scripts/release-transaction-provider.mjs +25 -5
  44. package/scripts/release-transaction.mjs +40 -11
  45. package/scripts/release-vector.mjs +44 -20
  46. package/scripts/run-operational-benchmark.mjs +151 -0
  47. package/scripts/run-operational-benchmark.v3.mjs +194 -0
  48. package/scripts/self-update.mjs +2 -0
  49. package/scripts/source-coverage.mjs +6 -1
  50. package/scripts/sync-version.mjs +10 -2
  51. package/scripts/wired-check.mjs +73 -45
@@ -195,24 +195,42 @@ export function evaluatePublicationReceipt(candidate, publication) {
195
195
 
196
196
  export function evaluateLivePreflight(observed) {
197
197
  const failures = [];
198
+ const incomplete = [];
198
199
  if (observed?.dirty !== false) failures.push(fail('DIRTY_WORKTREE', 'local worktree is dirty'));
199
200
  if (observed?.localSha !== observed?.remoteSha) failures.push(fail('REMOTE_SHA_MISMATCH', 'local HEAD is not origin/main'));
200
- if (!Array.isArray(observed?.openIssues) || observed.openIssues.length > 0) failures.push(fail('OPEN_ISSUES', `${observed?.openIssues?.length ?? 'unknown'} issue(s) open`));
201
- if (!Array.isArray(observed?.failedRuns) || observed.failedRuns.length > 0) failures.push(fail('GITHUB_FAILURES', `${observed?.failedRuns?.length ?? 'unknown'} recent failed run(s)`));
201
+ const blockers = observed?.releaseBlockers;
202
+ if (!Array.isArray(blockers)) failures.push(fail('RELEASE_BLOCKERS_UNKNOWN', 'could not verify open issues labeled release-blocker'));
203
+ else if (blockers.length > 0) failures.push(fail('RELEASE_BLOCKERS_OPEN', `${blockers.length} release-blocker issue(s) remain open`));
204
+ const requiredChecks = observed?.requiredChecks;
205
+ if (!Array.isArray(requiredChecks) || requiredChecks.length === 0) {
206
+ failures.push(fail('REQUIRED_CHECKS_UNKNOWN', 'could not verify the required main-branch checks on the exact remote SHA'));
207
+ } else {
208
+ for (const check of requiredChecks) {
209
+ if (check?.status !== 'PASS') failures.push(fail('REQUIRED_CHECK_NOT_GREEN', `${check?.context || 'unknown'} is ${check?.status || 'UNKNOWN'} on the exact remote SHA`));
210
+ }
211
+ }
202
212
  if (observed?.activeSelfStore !== true) failures.push(fail('BRAIN_SELF_STORE_MISSING', 'installed active registry lacks ruvnet-brain'));
203
213
  if (observed?.branchEnforceAdmins !== true) failures.push(fail('ADMIN_BYPASS_ENABLED', 'main protection does not enforce required checks for admins'));
204
- if (observed?.productionProtected !== true) failures.push(fail('PRODUCTION_ENV_UNPROTECTED', 'production environment has no required reviewer protection'));
205
- if (observed?.releaseVector !== 'PASS') failures.push(fail('RELEASE_VECTOR_NOT_PASS', `release vector is ${observed?.releaseVector ?? 'UNKNOWN'}`));
206
- return { verdict: failures.length === 0 ? 'PASS' : 'FAIL', observed, failures };
214
+ if (observed?.productionProtected !== true) failures.push(fail('PRODUCTION_ENV_UNPROTECTED', 'production environment lacks an enforced protection rule'));
215
+ if (observed?.releaseVector === 'NOT_EVALUATED') incomplete.push(fail('RELEASE_VECTOR_NOT_EVALUATED', 'quick status skipped the release vector; run full status for that evidence'));
216
+ else if (observed?.releaseVector !== 'PASS') failures.push(fail('RELEASE_VECTOR_NOT_PASS', `release vector is ${observed?.releaseVector ?? 'UNKNOWN'}`));
217
+ return {
218
+ verdict: failures.length > 0 ? 'FAIL' : incomplete.length > 0 ? 'INCOMPLETE' : 'PASS',
219
+ observed,
220
+ failures,
221
+ incomplete,
222
+ };
207
223
  }
208
224
 
209
- export function latestRunsByWorkflow(runs) {
210
- const latest = new Map();
211
- for (const run of Array.isArray(runs) ? runs : []) {
212
- const prior = latest.get(run.workflowName);
213
- if (!prior || Number(run.databaseId || 0) > Number(prior.databaseId || 0)) latest.set(run.workflowName, run);
214
- }
215
- return [...latest.values()];
225
+ export function resolveRequiredChecks(requiredContexts, checkRuns, statuses) {
226
+ if (!Array.isArray(requiredContexts) || !Array.isArray(checkRuns) || !Array.isArray(statuses)) return null;
227
+ return requiredContexts.map(({ context, app_id: appId }) => {
228
+ const checkRun = checkRuns.find((row) => row.name === context && (appId == null || row.app?.id === appId));
229
+ const status = appId == null ? statuses.find((row) => row.context === context) : null;
230
+ const passed = (checkRun?.status === 'completed' && checkRun.conclusion === 'success') || status?.state === 'success';
231
+ const pending = checkRun?.status === 'queued' || checkRun?.status === 'in_progress' || status?.state === 'pending';
232
+ return { context, appId, status: passed ? 'PASS' : pending ? 'PENDING' : checkRun || status ? 'FAIL' : 'MISSING' };
233
+ });
216
234
  }
217
235
 
218
236
  function command(cmd, args, options = {}) {
@@ -235,15 +253,21 @@ export function collectLivePreflight({ root = process.cwd(), repo = 'stuinfla/ru
235
253
  const git = (args) => command('git', args, { cwd: root });
236
254
  const localSha = git(['rev-parse', 'HEAD']).stdout.trim();
237
255
  const dirty = Boolean(git(['status', '--porcelain']).stdout.trim());
238
- git(['fetch', '--quiet', 'origin', 'main']);
239
- const remoteSha = git(['rev-parse', 'origin/main']).stdout.trim();
240
- const openIssues = jsonCommand('gh', ['issue', 'list', '--repo', repo, '--state', 'open', '--limit', '100', '--json', 'number,title,url']) ?? null;
241
- const runs = jsonCommand('gh', ['run', 'list', '--repo', repo, '--limit', '50', '--json', 'databaseId,workflowName,headSha,status,conclusion,url']) ?? null;
242
- const failedRuns = Array.isArray(runs)
243
- ? latestRunsByWorkflow(runs.filter((run) => run.headSha === remoteSha))
244
- .filter((run) => run.status !== 'completed' || run.conclusion !== 'success')
245
- : null;
256
+ const fetched = git(['fetch', '--quiet', 'origin', 'main']);
257
+ const remoteSha = fetched.status === 0 ? git(['rev-parse', 'origin/main']).stdout.trim() : null;
258
+ const releaseBlockers = jsonCommand('gh', ['issue', 'list', '--repo', repo, '--state', 'open', '--label', 'release-blocker', '--limit', '100', '--json', 'number,title,url']) ?? null;
246
259
  const protection = jsonCommand('gh', ['api', `repos/${repo}/branches/main/protection`]);
260
+ const requiredChecksConfig = protection?.required_status_checks?.checks;
261
+ const requiredContexts = Array.isArray(requiredChecksConfig)
262
+ ? requiredChecksConfig
263
+ : protection?.required_status_checks?.contexts?.map((context) => ({ context, app_id: null }));
264
+ const checkRuns = remoteSha
265
+ ? jsonCommand('gh', ['api', `repos/${repo}/commits/${remoteSha}/check-runs?per_page=100`])?.check_runs
266
+ : null;
267
+ const statuses = remoteSha
268
+ ? jsonCommand('gh', ['api', `repos/${repo}/commits/${remoteSha}/status`])?.statuses
269
+ : null;
270
+ const requiredChecks = resolveRequiredChecks(requiredContexts, checkRuns, statuses);
247
271
  const environments = jsonCommand('gh', ['api', `repos/${repo}/environments`]);
248
272
  const production = environments?.environments?.find((environment) => environment.name === 'Production – ruvnet-brain');
249
273
  let activeSelfStore = false;
@@ -256,13 +280,13 @@ export function collectLivePreflight({ root = process.cwd(), repo = 'stuinfla/ru
256
280
  if (includeVector) {
257
281
  const vector = jsonCommand(process.execPath, [path.join(root, 'scripts/release-vector.mjs'), '--json'], { cwd: root });
258
282
  releaseVector = vector?.verdict || 'UNKNOWN';
259
- }
283
+ } else releaseVector = 'NOT_EVALUATED';
260
284
  return {
261
285
  localSha,
262
286
  remoteSha,
263
287
  dirty,
264
- openIssues,
265
- failedRuns,
288
+ releaseBlockers,
289
+ requiredChecks,
266
290
  activeSelfStore,
267
291
  branchEnforceAdmins: protection?.enforce_admins?.enabled === true,
268
292
  productionProtected: Array.isArray(production?.protection_rules) && production.protection_rules.length > 0 && production.can_admins_bypass === false,
@@ -280,7 +304,7 @@ export function main(args = process.argv.slice(2)) {
280
304
  if (args.includes('--status')) {
281
305
  const result = evaluateLivePreflight(collectLivePreflight({ includeVector: !args.includes('--quick') }));
282
306
  console.log(JSON.stringify(result, null, 2));
283
- return result.verdict === 'PASS' ? 0 : 1;
307
+ return result.verdict === 'PASS' ? 0 : result.verdict === 'INCOMPLETE' ? 2 : 1;
284
308
  }
285
309
  const candidatePath = argument(args, '--candidate');
286
310
  const publicationPath = argument(args, '--publication');
@@ -69,23 +69,21 @@ function percentile(values, fraction) {
69
69
  return sorted[Math.max(0, Math.ceil(sorted.length * fraction) - 1)];
70
70
  }
71
71
 
72
- function grade(spec, text, elapsedMs, transportOk) {
73
- const repo = spec.repo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
74
- const expectedRepoCited = new RegExp(
75
- `(?:repo\\s*=\\s*["']?${repo}\\b|path\\s*:\\s*${repo}/|capability-cards\\.md#${repo}\\b)`,
76
- 'i',
77
- ).test(text);
78
- const cited = /(?:#\d+\s+repo=[a-z0-9._-]+|path\s*:\s*[a-z0-9._-]+\/|capability-cards\.md#[a-z0-9._-]+)/i
79
- .test(text);
80
- const useful = spec.required.test(text);
72
+ export function grade(spec, text, elapsedMs, transportOk) {
73
+ const top = /^#1\s+repo=(\S+)(?:\s+ce=(-?[\d.]+))?/m.exec(text);
74
+ const expectedRepoCited = [...String(text).matchAll(/^#\d+\s+repo=(\S+)/gm)]
75
+ .some((match) => match[1].toLowerCase() === spec.repo.toLowerCase());
76
+ const cited = /^#\d+\s+repo=[a-z0-9._-]+/im.test(text);
77
+ const abstained = !top || (top[2] !== undefined && Number(top[2]) < 0);
78
+ // This regex is retained as a visible keyword signal only. It is not semantic accuracy and it
79
+ // cannot make a row effective without a citation from the independently expected owner.
80
+ const keywordSignal = spec.required.test(text);
81
81
  const unsupportedAbsence = /\b(?:does not exist|must be built|you need to build it)\b/i.test(text);
82
82
  const evidenceQualified = /EVIDENCE:\s*THIN|NOT PROVEN|PROPOSED|curated-capability-card|THIS QUERY found nothing/i.test(text);
83
83
  const honest = !unsupportedAbsence || evidenceQualified;
84
84
  const latencyPoints = elapsedMs <= 4000 ? 10 : elapsedMs <= 8000 ? 5 : 0;
85
- const score = (transportOk ? 20 : 0) + (text.length > 200 ? 20 : 0)
86
- + (cited ? 20 : 0) + (useful ? 20 : 0) + (honest ? 10 : 0) + latencyPoints;
87
- const effective = transportOk && cited && useful && honest && elapsedMs <= 4000;
88
- return { score, cited, expectedRepoCited, useful, honest, effective, latencyPoints };
85
+ const effective = transportOk && cited && expectedRepoCited && honest && !abstained && elapsedMs <= 4000;
86
+ return { cited, expectedRepoCited, keywordSignal, honest, abstained, effective, latencyPoints };
89
87
  }
90
88
 
91
89
  async function main() {
@@ -153,7 +151,7 @@ async function main() {
153
151
  answer: text,
154
152
  ...grade(spec, text, elapsedMs, transportOk),
155
153
  };
156
- process.stdout.write(`${String(spec.id).padStart(2, '0')} ${String(results[index].score).padStart(3)} ${String(elapsedMs).padStart(6)}ms ${spec.category.padEnd(14)} ${spec.query}\n`);
154
+ process.stdout.write(`${String(spec.id).padStart(2, '0')} ${results[index].effective ? 'ELIGIBLE' : 'MISS '} ${String(elapsedMs).padStart(6)}ms ${spec.category.padEnd(14)} ${spec.query}\n`);
157
155
  }
158
156
  }));
159
157
 
@@ -170,17 +168,17 @@ async function main() {
170
168
  readinessMs,
171
169
  passed: results.filter((result) => result.effective).length,
172
170
  under4s: results.filter((result) => result.elapsedMs <= 4000).length,
173
- averageScore: Math.round(results.reduce((sum, result) => sum + result.score, 0) / results.length),
174
171
  averageMs: Math.round(times.reduce((sum, value) => sum + value, 0) / times.length),
175
172
  medianMs: percentile(times, 0.5),
176
173
  p95Ms: percentile(times, 0.95),
177
174
  longestMs: Math.max(...times),
178
175
  stderr: stderr.trim(),
176
+ gradingScope: 'Operational eligibility only: transport, citation presence, expected-owner citation, honest uncertainty, and latency. keywordSignal is diagnostic only; this report does not measure semantic answer accuracy.',
179
177
  results,
180
178
  };
181
179
  const output = path.join(ROOT, 'data/novice-50-report.json');
182
180
  fs.writeFileSync(output, JSON.stringify(report, null, 2) + '\n');
183
- console.log(`SUMMARY ${report.passed}/50 effective · ${report.under4s}/50 under 4s · score ${report.averageScore}/100 · avg ${report.averageMs}ms · p95 ${report.p95Ms}ms · max ${report.longestMs}ms`);
181
+ console.log(`SUMMARY ${report.passed}/50 eligible · ${report.under4s}/50 under 4s · avg ${report.averageMs}ms · p95 ${report.p95Ms}ms · max ${report.longestMs}ms`);
184
182
  console.log(`REPORT ${output}`);
185
183
  }
186
184
 
@@ -66,6 +66,7 @@ import { validateCoverageLedger } from './coverage-integrity.mjs';
66
66
  // The org total is DERIVED, never a literal: it was hardcoded 248 in this file and in its
67
67
  // sibling while the account actually had 200 — one stale fact, restated twice (2026-08-12).
68
68
  import { orgRepoCount } from './org-repo-count.mjs';
69
+ import { assertCapabilityOnlyStore } from '../kb/capability-only.mjs';
69
70
 
70
71
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
71
72
 
@@ -742,6 +743,7 @@ async function assembleBundleImpl({ corpusDir, runtimeRoot, outDir, identity = {
742
743
  }
743
744
 
744
745
  const selectedResults = discovered.map((name) => {
746
+ assertCapabilityOnlyStore(corpus, name);
745
747
  const folded = name.toLowerCase();
746
748
  const generation = ledgerIn.stores?.[name]
747
749
  || Object.entries(ledgerIn.stores || {}).find(([key]) => key.toLowerCase() === folded)?.[1];
@@ -0,0 +1,22 @@
1
+ // Correlation is not authorization: protected-release still enforces its own release authority.
2
+ import fs from 'node:fs';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ export function selectCorpusDispatch(runs, { sha, dispatchId, notBefore }) {
6
+ if (!/^[a-f0-9]{40}$/.test(sha || '') || !/^corpus-\d+-\d+$/.test(dispatchId || '')
7
+ || !Number.isFinite(Date.parse(notBefore))) throw new Error('invalid corpus dispatch identity');
8
+ const matches = runs.filter(run => run.event === 'workflow_dispatch' && run.head_sha === sha
9
+ && run.head_branch === 'main' && run.display_title === `protected-release corpus ${dispatchId}`
10
+ && Date.parse(run.created_at) >= Date.parse(notBefore));
11
+ if (matches.length > 1) throw new Error('multiple runs claim the corpus dispatch identity');
12
+ return matches[0] || null;
13
+ }
14
+
15
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
16
+ const response = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
17
+ const run = selectCorpusDispatch(response.workflow_runs || [], {
18
+ sha: process.env.CANDIDATE_SHA, dispatchId: process.env.CORPUS_DISPATCH_ID,
19
+ notBefore: process.env.DISPATCHED_AT,
20
+ });
21
+ if (run) console.log(JSON.stringify(run));
22
+ }
@@ -15,8 +15,10 @@ import { FULL_HINTS, KEEP_DIRS } from './full-hints.mjs';
15
15
  import { buildCoverage, observeSourceUniverse, renderMarkdown } from './source-coverage.mjs';
16
16
  import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
17
17
  import { rebuildCorpusAggregates } from './corpus-aggregates.mjs';
18
+ import { assertCapabilityOnlyStore, isCapabilityOnly, CAPABILITY_RETIRED_SUFFIXES } from '../kb/capability-only.mjs';
18
19
  import { fileIdentity } from '../plugin/scripts/coverage-integrity.mjs';
19
20
  import { storeRoot } from '../kb/store-root.mjs';
21
+ import { captureGistSources } from './gist-receipts.mjs';
20
22
 
21
23
  export { rebuildCorpusAggregates };
22
24
 
@@ -177,7 +179,12 @@ export function planReconciliation({ coverage, ledger, assetsDir = null }) {
177
179
  const generation = Object.entries(ledger.stores).find(([name]) => name.toLowerCase() === folded)?.[1] || null;
178
180
  const current = String(generation?.sourceCommit || '').toLowerCase();
179
181
  let reason = generation?.sourceCommit ? 'sourceCommit differs' : 'missing ledger receipt';
180
- if (current === upstreamSha) {
182
+ let capabilityPolicyCurrent = !isCapabilityOnly(store);
183
+ if (!capabilityPolicyCurrent && assetsDir) {
184
+ try { assertCapabilityOnlyStore(assetsDir, store); capabilityPolicyCurrent = true; }
185
+ catch { reason = 'capability-only policy requires a clean rebuild'; }
186
+ }
187
+ if (current === upstreamSha && capabilityPolicyCurrent) {
181
188
  if (!assetsDir) continue;
182
189
  const expectedFile = `${store}.big.rvf`;
183
190
  const rvfFile = path.join(path.resolve(assetsDir), expectedFile);
@@ -243,7 +250,7 @@ async function measureFreshness({ closingObservation, observation }) {
243
250
  * generation; `latest` is never substituted, and an exhausted partial generation is never accepted.
244
251
  */
245
252
  export async function acquireSealedGeneration({ maxAttempts = 3, assetsDir = null, observe, build,
246
- readLedger: currentLedger, execute, prune, rebuild, closingObservation = null } = {}) {
253
+ readLedger: currentLedger, execute, prune, rebuild, preflight = null, closingObservation = null } = {}) {
247
254
  if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 10
248
255
  || [observe, build, currentLedger, execute, prune, rebuild].some((fn) => typeof fn !== 'function')) {
249
256
  fail('bounded acquisition configuration is invalid');
@@ -251,6 +258,10 @@ export async function acquireSealedGeneration({ maxAttempts = 3, assetsDir = nul
251
258
  // ONE discovery pass. This observation is the sealed manifest every later step consumes; it is never
252
259
  // re-taken, so upstream churn cannot restart or invalidate the generation.
253
260
  const observation = await observe();
261
+ // Validate/fetch the source most likely to fail late (gist detail/raw access) before any expensive
262
+ // repository clone and embedding work. Its verified bodies are the existing capture cache consumed
263
+ // by the later aggregate build, so preflight does not double-fetch or weaken source binding.
264
+ const preflightResult = typeof preflight === 'function' ? await preflight(observation) : null;
254
265
  const attempts = [];
255
266
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
256
267
  const coverage = await build(observation);
@@ -259,7 +270,7 @@ export async function acquireSealedGeneration({ maxAttempts = 3, assetsDir = nul
259
270
  const pruning = await prune(coverage, attempt);
260
271
  let aggregates;
261
272
  try {
262
- aggregates = await rebuild(coverage, observation, attempt);
273
+ aggregates = await rebuild(coverage, observation, attempt, preflightResult);
263
274
  } catch (error) {
264
275
  if (error?.code !== 'GIST_OBSERVATION_MOVED') throw error;
265
276
  // A gist moved between its list entry and its detail fetch. The remedy is to retry against the
@@ -414,6 +425,7 @@ function validateWorkerOutput({ output, item }) {
414
425
  fail(`${item.store}: worker generation does not bind exact source and RVF bytes`);
415
426
  }
416
427
  const source = readJson(path.join(output, 'SOURCE.json'), `${item.store} worker source manifest`);
428
+ assertCapabilityOnlyStore(output, item.store);
417
429
  if (Object.keys(source.stores || {}).length !== 1 || !source.stores[item.store]
418
430
  || String(source.stores[item.store].sourceCommit || '').toLowerCase() !== item.upstreamSha) {
419
431
  fail(`${item.store}: worker SOURCE manifest does not bind exact source`);
@@ -536,6 +548,12 @@ export async function executeReconciliation({
536
548
  writeJsonAtomic(path.join(merge, 'SOURCE.json'), mergedSource);
537
549
  promotedFiles.push('RVF-GENERATIONS.json', 'SOURCE.json');
538
550
  promoteArtifactSet({ liveDir: assets, candidateDir: merge, files: promotedFiles.sort() });
551
+ // Worker output replaces selected files, so explicitly retire old seed sidecars
552
+ // that are intentionally absent from a capability-only worker's output.
553
+ for (const { store } of results) if (isCapabilityOnly(store)) {
554
+ for (const suffix of CAPABILITY_RETIRED_SUFFIXES) fs.rmSync(path.join(assets, `${store}${suffix}`), { force: true });
555
+ assertCapabilityOnlyStore(assets, store);
556
+ }
539
557
  return { refreshed: results.map(({ store }) => store),
540
558
  workers: results.map(({ output: _output, ...receipt }) => receipt) };
541
559
  }
@@ -600,7 +618,10 @@ export async function acquireCorpusGeneration({ owner = 'ruvnet', assetsDir, wor
600
618
  // `coverage` is now threaded through (rule 8) rather than discarded: rebuildCorpusAggregates
601
619
  // asserts the concepts observation identity exactly equals coverage's own, instead of trusting an
602
620
  // accidental shared reference.
603
- rebuild = (coverage, observation) => rebuildCorpusAggregates({ assetsDir, observation, coverage, root }),
621
+ preflight = (observation) => captureGistSources({ observation }),
622
+ rebuild = (coverage, observation, _attempt, capturedGists) => rebuildCorpusAggregates({
623
+ assetsDir, observation, coverage, root, cache: capturedGists,
624
+ }),
604
625
  } = {}) {
605
626
  if (!assetsDir || !workspaceDir) fail('stable reconciliation requires explicit assets and workspace directories');
606
627
  const workspace = path.resolve(workspaceDir || '');
@@ -621,6 +642,7 @@ export async function acquireCorpusGeneration({ owner = 'ruvnet', assetsDir, wor
621
642
  }),
622
643
  prune,
623
644
  rebuild,
645
+ preflight,
624
646
  });
625
647
  }
626
648
 
@@ -291,11 +291,15 @@ export function findCallers(root, rel) {
291
291
  // ── impl derivation ─────────────────────────────────────────────────────────────────────────────
292
292
  // PER PATH, then the WEAKEST wins. Any-semantics ("one member is wired ⇒ wired") reports green on
293
293
  // exactly the built-but-unwired case the rung exists to catch.
294
- export function deriveImpl(root, governed, { checkWiring = true } = {}) {
294
+ export function deriveImpl(root, governed, { checkWiring = true, callerCache = new Map(), callerLookup = findCallers } = {}) {
295
295
  if (!governed.length) return { impl: 'unknown', perPath: [], unwired: [], reason: 'no governs: set' };
296
296
  const perPath = governed.map((g) => {
297
297
  if (!g.onDisk && !g.resolved) return { path: g.path, impl: 'unbuilt', type: g.type };
298
- const callers = checkWiring ? findCallers(root, g.path) : [];
298
+ let callers = [];
299
+ if (checkWiring) {
300
+ if (!callerCache.has(g.path)) callerCache.set(g.path, callerLookup(root, g.path));
301
+ callers = callerCache.get(g.path);
302
+ }
299
303
  return { path: g.path, impl: callers.length ? 'wired' : 'built', callers, type: g.type };
300
304
  });
301
305
  return {
@@ -630,7 +634,8 @@ export function evaluateDoc(root, rel, opts = {}) {
630
634
  }
631
635
 
632
636
  // ── impl (derived; the stored value is a claim to be checked, never an input) ──────────────────
633
- const derived = deriveImpl(root, governed, { checkWiring });
637
+ const derived = deriveImpl(root, governed, { checkWiring,
638
+ callerCache: opts.callerCache, callerLookup: opts.callerLookup });
634
639
  const digest = computeDigest(root, rel, text, governed);
635
640
  const storedDigest = doc.verifiedDigestStored;
636
641
  const digestMatch = digest.digest && storedDigest ? digest.digest === storedDigest : null;
@@ -739,7 +744,10 @@ export function evaluateDoc(root, rel, opts = {}) {
739
744
 
740
745
  export function evaluate(root = REPO_ROOT, opts = {}) {
741
746
  const dirs = opts.dirs ?? DEFAULT_DIRS;
742
- const docs = (opts.files ?? listDocs(root, dirs)).map((rel) => evaluateDoc(root, rel, opts));
747
+ // This cache belongs to exactly one evaluation. Each distinct governed path is searched once,
748
+ // even when several ADRs name it; a later call creates a fresh map and sees current source.
749
+ const invocation = { ...opts, callerCache: new Map(), callerLookup: opts.callerLookup ?? findCallers };
750
+ const docs = (opts.files ?? listDocs(root, dirs)).map((rel) => evaluateDoc(root, rel, invocation));
743
751
  return { root, docs };
744
752
  }
745
753
 
@@ -3,9 +3,9 @@
3
3
  // GROUND TRUTH rather than by a model's opinion of them. (ADR-0011 Phase 0.)
4
4
  //
5
5
  // FIVE STRATA, because a gate that only asks easy questions cannot fail:
6
- // named — the repo is named in the question pass = grounded AND routed
7
- // described — capability described, no names pass = grounded AND routed
8
- // scenario — a real-world situation, no names pass = grounded AND routed
6
+ // named — the repo is named in the question pass = grounded AND routed AND not abstained
7
+ // described — capability described, no names pass = grounded AND routed AND not abstained
8
+ // scenario — a real-world situation, no names pass = grounded AND routed AND not abstained
9
9
  // adversarial — the correct answer is NOT in this corpus pass = ABSTAINED (top ce < 0, or no hits)
10
10
  // provenance — gist-shaped content pass = grounded AND (if the top hit IS a
11
11
  // gist chunk, it must carry its GIST STATUS banner — better repo grounding also passes)
@@ -92,9 +92,11 @@ export function gradeQuestion(q, { grounded, citations, bannerPresent, receipt,
92
92
  case 'provenance':
93
93
  // The mechanism under test: IF a gist chunk wins, it must carry its own status banner.
94
94
  // A better hit from the real repo is not a failure — it is better grounding.
95
- return { grounded, routed: null, abstained, pass: !!grounded && (top?.repo !== 'ruv-gists' || bannerPresent) };
95
+ return { grounded, routed: null, abstained, pass: !!grounded && !abstained && (top?.repo !== 'ruv-gists' || bannerPresent) };
96
96
  default:
97
- return { grounded, routed, abstained, pass: !!grounded && routed };
97
+ // Abstention is correct only for the adversarial stratum. A negative cross-encoder score on
98
+ // an answerable question is an answer miss, even when an unrelated existing path resolves.
99
+ return { grounded, routed, abstained, pass: !!grounded && routed && !abstained };
98
100
  }
99
101
  }
100
102
 
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env node
2
+ // Public, no-token Git transport for complete gist snapshots. This reads Git objects only: it never
3
+ // checks out or executes gist content. API raw_url revisions are verified as real Git blob or
4
+ // commit objects; they are never inferred to be REST history.version values.
5
+ import fs from 'node:fs';
6
+ import os from 'node:os';
7
+ import path from 'node:path';
8
+ import { spawnSync } from 'node:child_process';
9
+
10
+ const HEX40 = /^[a-f0-9]{40}$/i;
11
+ const OWNER_RE = /^[A-Za-z0-9-]{1,39}$/;
12
+ const GIST_RE = /^[a-f0-9]{20,64}$/i;
13
+ const MAX_FILES = 10_000;
14
+ const MAX_TOTAL_BYTES = 256 * 1024 * 1024;
15
+ export function buildSafeGitEnv(source = process.env) {
16
+ const env = { ...source };
17
+ for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
18
+ return {
19
+ ...env,
20
+ GIT_CONFIG_NOSYSTEM: '1',
21
+ GIT_CONFIG_GLOBAL: os.devNull,
22
+ GIT_TERMINAL_PROMPT: '0',
23
+ GIT_ASKPASS: '/usr/bin/false',
24
+ GIT_LFS_SKIP_SMUDGE: '1',
25
+ };
26
+ }
27
+ const SAFE_GIT_ENV = buildSafeGitEnv();
28
+
29
+ function assertSafeFilename(filename) {
30
+ if (typeof filename !== 'string' || !filename || filename.includes('\0') || filename.includes('\\')
31
+ || filename.startsWith('/') || filename.split('/').some((part) => !part || part === '.' || part === '..')) {
32
+ throw new Error(`gist file has an unsafe path: ${String(filename)}`);
33
+ }
34
+ }
35
+
36
+ function runGit(args, { cwd, input, timeoutMs = 120_000, maxBuffer = MAX_TOTAL_BYTES + 1024 * 1024 } = {}) {
37
+ const result = spawnSync('git', ['-c', 'credential.helper=', '-c', 'core.hooksPath=/dev/null', ...args], {
38
+ cwd, input, env: SAFE_GIT_ENV, encoding: null, timeout: timeoutMs, maxBuffer, windowsHide: true,
39
+ });
40
+ if (result.error || result.status !== 0) {
41
+ throw new Error(`gist Git transport failed (${result.error?.message || `exit ${result.status}`}): ${Buffer.from(result.stderr || '').toString('utf8').slice(-1000)}`);
42
+ }
43
+ return Buffer.from(result.stdout || '');
44
+ }
45
+
46
+ function scalarGit(args, options) { return runGit(args, options).toString('ascii').trim(); }
47
+
48
+ function decodeTree(treeBytes) {
49
+ const entries = [];
50
+ for (const record of treeBytes.toString('utf8').split('\0').filter(Boolean)) {
51
+ const tab = record.indexOf('\t');
52
+ if (tab < 0) throw new Error('gist Git tree contains a malformed entry');
53
+ const [mode, type, oid] = record.slice(0, tab).split(' ');
54
+ const filename = record.slice(tab + 1);
55
+ assertSafeFilename(filename);
56
+ if (type !== 'blob' || !['100644', '100755'].includes(mode) || !HEX40.test(oid)) {
57
+ throw new Error(`gist Git tree contains an unsupported object or mode for ${filename}`);
58
+ }
59
+ entries.push({ filename, mode, blobSha: oid.toLowerCase() });
60
+ }
61
+ entries.sort((a, b) => a.filename < b.filename ? -1 : a.filename > b.filename ? 1 : 0);
62
+ if (entries.length > MAX_FILES || new Set(entries.map(({ filename }) => filename)).size !== entries.length) {
63
+ throw new Error('gist Git tree has too many or duplicate file paths');
64
+ }
65
+ return entries;
66
+ }
67
+
68
+ function readBlobs(repository, entries) {
69
+ const oids = [...new Set(entries.map(({ blobSha }) => blobSha))];
70
+ const response = runGit(['cat-file', '--batch'], { cwd: repository, input: `${oids.join('\n')}\n` });
71
+ const blobs = new Map();
72
+ let offset = 0;
73
+ for (const oid of oids) {
74
+ const newline = response.indexOf(0x0a, offset);
75
+ if (newline < 0) throw new Error(`gist Git object response for ${oid} is incomplete`);
76
+ const header = response.subarray(offset, newline).toString('ascii').split(' ');
77
+ if (header.length !== 3 || header[0] !== oid || header[1] !== 'blob' || !/^\d+$/.test(header[2])) {
78
+ throw new Error(`gist Git object ${oid} is missing or is not a blob`);
79
+ }
80
+ const size = Number(header[2]);
81
+ if (!Number.isSafeInteger(size) || size < 0 || newline + 1 + size >= response.length
82
+ || response[newline + 1 + size] !== 0x0a) throw new Error(`gist Git blob ${oid} has an invalid framed size`);
83
+ blobs.set(oid, response.subarray(newline + 1, newline + 1 + size));
84
+ offset = newline + 2 + size;
85
+ }
86
+ if (offset !== response.length) throw new Error('gist Git object response has unexpected trailing bytes');
87
+ return blobs;
88
+ }
89
+
90
+ function parseObservedRawUrl(file, owner, gistId, filename) {
91
+ const rawUrl = new URL(file?.raw_url || '');
92
+ if (rawUrl.protocol !== 'https:' || rawUrl.hostname !== 'gist.githubusercontent.com'
93
+ || rawUrl.username || rawUrl.password || rawUrl.port || rawUrl.search || rawUrl.hash) {
94
+ throw new Error(`gist observed raw URL is unsafe for ${filename}`);
95
+ }
96
+ const parts = rawUrl.pathname.split('/').filter(Boolean).map((part) => decodeURIComponent(part));
97
+ if (parts.length < 5 || parts[0] !== owner || parts[1] !== gistId || parts[2] !== 'raw'
98
+ || !HEX40.test(parts[3]) || parts.slice(4).join('/') !== filename) {
99
+ throw new Error(`gist observed raw URL identity does not match ${filename}`);
100
+ }
101
+ return parts[3].toLowerCase();
102
+ }
103
+
104
+ function bindObservedRevision(repository, revisionSha, filename, headBlobSha) {
105
+ const objectType = scalarGit(['cat-file', '-t', revisionSha], { cwd: repository });
106
+ let rawBlobSha;
107
+ if (objectType === 'blob') rawBlobSha = revisionSha;
108
+ else if (objectType === 'commit') {
109
+ const resolvedCommit = scalarGit(['rev-parse', '--verify', `${revisionSha}^{commit}`], { cwd: repository }).toLowerCase();
110
+ if (resolvedCommit !== revisionSha) throw new Error(`gist raw revision ${revisionSha} did not resolve to the pinned commit`);
111
+ rawBlobSha = scalarGit(['rev-parse', '--verify', `${revisionSha}:${filename}`], { cwd: repository }).toLowerCase();
112
+ if (scalarGit(['cat-file', '-t', rawBlobSha], { cwd: repository }) !== 'blob') {
113
+ throw new Error(`gist raw commit ${revisionSha} does not contain ${filename} as a blob`);
114
+ }
115
+ } else {
116
+ throw new Error(`gist raw revision ${revisionSha} is neither an actual Git blob nor commit`);
117
+ }
118
+ if (rawBlobSha !== headBlobSha) throw new Error(`gist raw revision for ${filename} does not resolve to the verified Git HEAD blob`);
119
+ return { kind: objectType, blobSha: rawBlobSha };
120
+ }
121
+
122
+ /** Read and bind an already-cloned bare gist repository to one REST list observation. */
123
+ export function inspectGistGitSnapshot({ owner, stub, repository }) {
124
+ if (!OWNER_RE.test(String(owner || '')) || !GIST_RE.test(String(stub?.id || '')) || !repository) {
125
+ throw new Error('gist Git snapshot identity is invalid');
126
+ }
127
+ const gistId = String(stub.id).toLowerCase();
128
+ const headSha = scalarGit(['rev-parse', '--verify', 'HEAD^{commit}'], { cwd: repository }).toLowerCase();
129
+ if (!HEX40.test(headSha)) throw new Error(`gist ${gistId} did not resolve to a verified Git commit`);
130
+ const treeSha = scalarGit(['rev-parse', '--verify', `${headSha}^{tree}`], { cwd: repository }).toLowerCase();
131
+ if (!HEX40.test(treeSha)) throw new Error(`gist ${gistId} did not resolve to a verified Git tree`);
132
+ if (scalarGit(['cat-file', '-t', headSha], { cwd: repository }) !== 'commit'
133
+ || scalarGit(['cat-file', '-t', treeSha], { cwd: repository }) !== 'tree') {
134
+ throw new Error(`gist ${gistId} Git HEAD/tree objects are unavailable`);
135
+ }
136
+ const entries = decodeTree(runGit(['ls-tree', '-r', '-z', '--full-tree', headSha], { cwd: repository }));
137
+ const blobs = readBlobs(repository, entries);
138
+ const entryByName = new Map(entries.map((entry) => [entry.filename, entry]));
139
+ const observedFiles = stub.files && typeof stub.files === 'object' ? stub.files : {};
140
+ const observedNames = Object.keys(observedFiles).sort();
141
+ const truncated = stub.truncated === true;
142
+ if (stub.truncated !== false && stub.truncated !== true) throw new Error(`gist ${gistId} has unknown top-level file inventory completeness`);
143
+ if (truncated) throw new Error(`gist ${gistId} API file inventory is truncated; Git-tree enrichment is not yet bound into the source observation`);
144
+ if (observedNames.length > 300 && !truncated) throw new Error(`gist ${gistId} reports more than 300 files without the truncation flag`);
145
+ if (!truncated && (observedNames.length !== entries.length || observedNames.some((name, i) => name !== entries[i]?.filename))) {
146
+ throw new Error(`gist ${gistId} complete API file inventory differs from the verified Git tree`);
147
+ }
148
+
149
+ let totalBytes = 0;
150
+ const files = entries.map((entry) => {
151
+ const body = blobs.get(entry.blobSha);
152
+ const size = body?.length;
153
+ if (!Number.isSafeInteger(size) || size < 0) throw new Error(`gist ${gistId}/${entry.filename} has an invalid Git blob size`);
154
+ totalBytes += size;
155
+ if (totalBytes > MAX_TOTAL_BYTES) throw new Error(`gist ${gistId} Git tree exceeds the ${MAX_TOTAL_BYTES}-byte capture limit`);
156
+ const observed = Object.hasOwn(observedFiles, entry.filename);
157
+ const apiFile = observedFiles[entry.filename];
158
+ let rawBlobSha = null;
159
+ let rawRevisionSha = null;
160
+ let rawRevisionKind = null;
161
+ if (observed) {
162
+ if (apiFile?.filename !== entry.filename || !Number.isSafeInteger(apiFile.size) || apiFile.size !== size) {
163
+ throw new Error(`gist ${gistId}/${entry.filename} size or filename differs between API observation and Git HEAD`);
164
+ }
165
+ rawRevisionSha = parseObservedRawUrl(apiFile, owner, gistId, entry.filename);
166
+ const rawBinding = bindObservedRevision(repository, rawRevisionSha, entry.filename, entry.blobSha);
167
+ rawBlobSha = rawBinding.blobSha;
168
+ rawRevisionKind = rawBinding.kind;
169
+ } else if (!truncated) {
170
+ throw new Error(`gist ${gistId}/${entry.filename} is absent from a supposedly complete API inventory`);
171
+ }
172
+ return {
173
+ ...entry,
174
+ size,
175
+ body,
176
+ observed,
177
+ observedRawBlobSha: rawBlobSha,
178
+ observedRawRevisionSha: rawRevisionSha,
179
+ observedRawRevisionKind: rawRevisionKind,
180
+ sourceGit: { headSha, treeSha, treeFileCount: entries.length, observedFileCount: observedNames.length,
181
+ observedTruncated: truncated, blobSha: entry.blobSha, observed, observedRawBlobSha: rawBlobSha,
182
+ observedRawRevisionSha: rawRevisionSha, observedRawRevisionKind: rawRevisionKind },
183
+ };
184
+ });
185
+ return {
186
+ gistId,
187
+ headSha,
188
+ treeSha,
189
+ treeFileCount: entries.length,
190
+ totalBytes,
191
+ observedFileCount: observedNames.length,
192
+ observedTruncated: truncated,
193
+ files,
194
+ };
195
+ }
196
+
197
+ /** Clone the public gist without credentials or checkout and inspect its immutable Git snapshot. */
198
+ export async function fetchGistGitSnapshot({ owner, stub, signal, tempRoot = os.tmpdir() } = {}) {
199
+ if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('gist capture aborted');
200
+ if (!OWNER_RE.test(String(owner || '')) || !GIST_RE.test(String(stub?.id || ''))) throw new Error('gist Git clone identity is invalid');
201
+ const gistId = String(stub.id).toLowerCase();
202
+ const remoteUrl = `https://gist.github.com/${gistId}.git`;
203
+ const stage = fs.mkdtempSync(path.join(tempRoot, 'ruvnet-gist-git-'));
204
+ const repository = path.join(stage, 'repo.git');
205
+ try {
206
+ // A public remote HEAD before and after clone detects movement while the tree is being captured.
207
+ const before = scalarGit(['ls-remote', remoteUrl, 'HEAD'], { cwd: stage }).split(/\s+/)[0]?.toLowerCase();
208
+ if (!HEX40.test(before || '')) throw new Error(`gist ${gistId} has no public Git HEAD`);
209
+ runGit(['clone', '--bare', '--no-tags', '--no-recurse-submodules', remoteUrl, repository], { cwd: stage });
210
+ if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error('gist capture aborted');
211
+ const snapshot = inspectGistGitSnapshot({ owner, stub, repository });
212
+ const after = scalarGit(['ls-remote', remoteUrl, 'HEAD'], { cwd: stage }).split(/\s+/)[0]?.toLowerCase();
213
+ if (before !== after || snapshot.headSha !== before) throw new Error(`gist ${gistId} moved while Git snapshot was captured`);
214
+ return snapshot;
215
+ } finally {
216
+ fs.rmSync(stage, { recursive: true, force: true });
217
+ }
218
+ }