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.
- package/README.md +2 -2
- package/bin/install.mjs +19 -4
- package/console/app.js +48 -0
- package/console/index.html +19 -0
- package/console/style.css +26 -0
- package/data/model-catalog.json +1 -1
- package/docs/RELEASE-NOTES-4.0.md +4 -3
- package/kb/capability-only.mjs +27 -0
- package/kb/capability-summaries/cognitum-ruos/CAPABILITIES.md +26 -0
- package/kb/verify-citation.mjs +16 -4
- package/package.json +5 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/docs/RELEASE-NOTES-4.0.md +4 -3
- package/plugin/hooks/codex-hooks.json +6 -1
- package/plugin/hooks/hook-contracts.json +24 -2
- package/plugin/hooks/hooks.json +6 -1
- package/plugin/scripts/capacity-aware-parallel-work.mjs +200 -0
- package/plugin/scripts/codex-hook-adapter.mjs +37 -0
- package/plugin/scripts/continuity-hook-policy.mjs +4 -0
- package/plugin/scripts/coverage-integrity.mjs +17 -0
- package/plugin/scripts/hook-shim.mjs +1 -0
- package/plugin/scripts/lesson-gate.mjs +4 -1
- package/plugin/scripts/project-progression-reader.mjs +12 -1
- package/plugin/scripts/project-progression-session-start.mjs +24 -6
- package/plugin/scripts/project-progression-store.mjs +76 -4
- package/plugin/skills/release-proof/SKILL.md +28 -4
- package/plugin/skills/release-proof/scripts/release-proof.mjs +48 -24
- package/scripts/brain-novice-50.mjs +14 -16
- package/scripts/build-bundle.mjs +2 -0
- package/scripts/corpus-dispatch-receipt.mjs +22 -0
- package/scripts/corpus-reconcile.mjs +26 -4
- package/scripts/doc-currency.mjs +12 -4
- package/scripts/eval-brain.mjs +7 -5
- package/scripts/gist-git-transport.mjs +218 -0
- package/scripts/gist-receipts.mjs +168 -38
- package/scripts/ingest-gists.mjs +5 -2
- package/scripts/installed-brain-health.mjs +99 -0
- package/scripts/onboarding-console.mjs +45 -1
- package/scripts/public-inputs.mjs +2 -1
- package/scripts/public-verification-inputs.mjs +14 -6
- package/scripts/refresh-capability-only-store.mjs +143 -0
- package/scripts/release-transaction-provider.mjs +25 -5
- package/scripts/release-transaction.mjs +40 -11
- package/scripts/release-vector.mjs +44 -20
- package/scripts/run-operational-benchmark.mjs +151 -0
- package/scripts/run-operational-benchmark.v3.mjs +194 -0
- package/scripts/self-update.mjs +2 -0
- package/scripts/source-coverage.mjs +6 -1
- package/scripts/sync-version.mjs +10 -2
- 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
|
-
|
|
201
|
-
if (!Array.isArray(
|
|
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
|
|
205
|
-
if (observed?.releaseVector
|
|
206
|
-
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
265
|
-
|
|
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
|
|
74
|
-
const expectedRepoCited =
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
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
|
|
86
|
-
|
|
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')} ${
|
|
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
|
|
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
|
|
package/scripts/build-bundle.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/scripts/doc-currency.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
package/scripts/eval-brain.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|