ruvnet-brain 4.3.26 → 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 (45) hide show
  1. package/README.md +2 -2
  2. package/bin/install.mjs +19 -4
  3. package/data/model-catalog.json +1 -1
  4. package/docs/RELEASE-NOTES-4.0.md +4 -3
  5. package/kb/capability-only.mjs +27 -0
  6. package/kb/capability-summaries/cognitum-ruos/CAPABILITIES.md +26 -0
  7. package/kb/verify-citation.mjs +16 -4
  8. package/package.json +5 -1
  9. package/plugin/.claude-plugin/plugin.json +1 -1
  10. package/plugin/.codex-plugin/plugin.json +1 -1
  11. package/plugin/docs/RELEASE-NOTES-4.0.md +4 -3
  12. package/plugin/hooks/codex-hooks.json +6 -1
  13. package/plugin/hooks/hook-contracts.json +24 -2
  14. package/plugin/hooks/hooks.json +6 -1
  15. package/plugin/scripts/capacity-aware-parallel-work.mjs +200 -0
  16. package/plugin/scripts/codex-hook-adapter.mjs +37 -0
  17. package/plugin/scripts/continuity-hook-policy.mjs +4 -0
  18. package/plugin/scripts/coverage-integrity.mjs +17 -0
  19. package/plugin/scripts/hook-shim.mjs +1 -0
  20. package/plugin/scripts/lesson-gate.mjs +4 -1
  21. package/plugin/scripts/project-progression-reader.mjs +12 -1
  22. package/plugin/scripts/project-progression-session-start.mjs +24 -6
  23. package/plugin/scripts/project-progression-store.mjs +76 -4
  24. package/plugin/skills/release-proof/SKILL.md +28 -4
  25. package/plugin/skills/release-proof/scripts/release-proof.mjs +48 -24
  26. package/scripts/brain-novice-50.mjs +14 -16
  27. package/scripts/build-bundle.mjs +2 -0
  28. package/scripts/corpus-dispatch-receipt.mjs +22 -0
  29. package/scripts/corpus-reconcile.mjs +26 -4
  30. package/scripts/doc-currency.mjs +12 -4
  31. package/scripts/eval-brain.mjs +7 -5
  32. package/scripts/gist-git-transport.mjs +218 -0
  33. package/scripts/gist-receipts.mjs +168 -38
  34. package/scripts/ingest-gists.mjs +5 -2
  35. package/scripts/installed-brain-health.mjs +99 -0
  36. package/scripts/public-inputs.mjs +2 -1
  37. package/scripts/public-verification-inputs.mjs +14 -6
  38. package/scripts/refresh-capability-only-store.mjs +143 -0
  39. package/scripts/release-vector.mjs +44 -20
  40. package/scripts/run-operational-benchmark.mjs +151 -0
  41. package/scripts/run-operational-benchmark.v3.mjs +194 -0
  42. package/scripts/self-update.mjs +2 -0
  43. package/scripts/source-coverage.mjs +6 -1
  44. package/scripts/sync-version.mjs +10 -2
  45. package/scripts/wired-check.mjs +73 -45
@@ -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
+ }