ruvnet-brain 4.2.2-dev → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +15 -14
  2. package/bin/install.mjs +11 -2
  3. package/console/architecture.html +12 -14
  4. package/console/install-mockup.html +3 -3
  5. package/kb/zip-extract.mjs +8 -2
  6. package/package.json +7 -1
  7. package/plugin/.claude-plugin/plugin.json +1 -1
  8. package/plugin/.codex-plugin/plugin.json +1 -1
  9. package/plugin/hooks/codex-hooks.json +1 -1
  10. package/plugin/hooks/hooks.json +10 -0
  11. package/plugin/host-adapters/claude.json +11 -0
  12. package/plugin/host-adapters/codex.json +11 -0
  13. package/plugin/mcp/managed-cli-interface.mjs +77 -10
  14. package/plugin/mcp/server.mjs +2 -1
  15. package/plugin/scripts/anticipate.sh +30 -1
  16. package/plugin/scripts/capability-claim-evidence.mjs +431 -0
  17. package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
  18. package/plugin/scripts/capability-registry.mjs +6 -0
  19. package/plugin/scripts/capability-routing.mjs +70 -0
  20. package/plugin/scripts/continuation-gate.mjs +68 -2
  21. package/plugin/scripts/coverage-integrity.mjs +575 -0
  22. package/plugin/scripts/ground-ruvnet.sh +2 -2
  23. package/plugin/scripts/nightly-controller.mjs +19 -2
  24. package/plugin/scripts/project-progression-contract.mjs +409 -0
  25. package/plugin/scripts/project-progression-hook.mjs +196 -0
  26. package/plugin/scripts/project-progression-outbox.mjs +95 -0
  27. package/plugin/scripts/project-progression-session-start.mjs +183 -0
  28. package/plugin/scripts/project-progression-store.mjs +248 -0
  29. package/plugin/scripts/project-store-resolver.mjs +106 -0
  30. package/plugin/scripts/session-snapshot-hook.mjs +29 -1
  31. package/plugin/scripts/session-start-core.mjs +22 -18
  32. package/plugin/scripts/spend-guard.mjs +6 -1
  33. package/scripts/adr-072-completion.mjs +98 -0
  34. package/scripts/behavioral-l1-l4.mjs +11 -8
  35. package/scripts/brain-score.mjs +7 -2
  36. package/scripts/build-bundle.mjs +82 -6
  37. package/scripts/card-from-source.mjs +32 -6
  38. package/scripts/corpus-aggregates.mjs +194 -0
  39. package/scripts/corpus-candidate.mjs +56 -36
  40. package/scripts/corpus-reconcile.mjs +295 -28
  41. package/scripts/corpus-seed-publish.mjs +3 -3
  42. package/scripts/coverage-integrity.mjs +3 -0
  43. package/scripts/gist-receipts.mjs +183 -0
  44. package/scripts/git-hooks/pre-push +21 -0
  45. package/scripts/host-registry.mjs +118 -0
  46. package/scripts/independent-review-receipt.mjs +499 -0
  47. package/scripts/ingest-gists.mjs +34 -5
  48. package/scripts/ingest-new-repos.mjs +5 -0
  49. package/scripts/metaharness-gate.mjs +58 -0
  50. package/scripts/nightly-gists.sh +3 -3
  51. package/scripts/nightly-wrapper.sh +75 -69
  52. package/scripts/product-integrity-contract.mjs +156 -0
  53. package/scripts/public-inventory.mjs +4 -0
  54. package/scripts/public-verification-aggregate.mjs +342 -0
  55. package/scripts/public-verification-finalizer.mjs +59 -0
  56. package/scripts/public-verification-inputs.mjs +473 -0
  57. package/scripts/public-verification-lane.mjs +244 -0
  58. package/scripts/publication-receipt.mjs +50 -6
  59. package/scripts/rebuild-gists-from-receipts.mjs +50 -5
  60. package/scripts/release-projection.mjs +86 -0
  61. package/scripts/release-transaction-provider.mjs +43 -102
  62. package/scripts/release-transaction.mjs +139 -51
  63. package/scripts/release.mjs +15 -7
  64. package/scripts/restore-local-ingests.mjs +2 -2
  65. package/scripts/retrieval-canary.mjs +533 -0
  66. package/scripts/routing-flywheel.mjs +12 -1
  67. package/scripts/rvf-generation.mjs +9 -3
  68. package/scripts/self-update.mjs +19 -44
  69. package/scripts/source-coverage.mjs +415 -0
  70. package/scripts/source-scope-receipt.mjs +84 -0
  71. package/scripts/sync-census.mjs +0 -0
  72. package/scripts/wired-check.mjs +114 -24
  73. package/scripts/worktree-integrity.mjs +123 -0
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // self-update.mjs — nightly evergreen driver.
2
+ // self-update.mjs — author-side incremental candidate builder.
3
3
  // Compares each in-scope repo's live HEAD against the stamped manifest, then (re)builds the segments
4
4
  // that changed and re-stamps. Dry-run by default; pass --apply to actually (re)build.
5
5
  //
@@ -12,14 +12,16 @@
12
12
  // candidate bytes, but only the protected release workflow may publish them after release-proof
13
13
  // seals the exact clean SHA and packed-artifact digest.
14
14
  //
15
- // Designed to be invoked by deploy/com.ruvnet.brain-nightly.plist (LaunchAgent — NOT auto-installed).
15
+ // Author-side candidate builder. It is never scheduled against a developer checkout. The retired
16
+ // com.ruvnet.brain-nightly LaunchAgent did exactly that and accumulated generated changes beside
17
+ // live work. Run --apply only from a clean linked worktree outside the primary checkout.
16
18
  import fs from 'node:fs';
17
- import os from 'node:os';
18
19
  import path from 'node:path';
19
20
  import { execFileSync, spawnSync } from 'node:child_process';
20
21
  import { fileURLToPath } from 'node:url';
21
22
  import { FULL_HINTS, KEEP_DIRS } from './full-hints.mjs';
22
23
  import { withSubmoduleSymlinksDetached } from './git-clone-refresh.mjs';
24
+ import { assertIsolatedMutationWorktree } from './worktree-integrity.mjs';
23
25
 
24
26
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
27
 
@@ -57,45 +59,18 @@ if (has('--publish')) {
57
59
  console.error('[release-authority] Use the protected release workflow after release-proof seals the exact clean SHA and packed-artifact digest.');
58
60
  process.exit(2);
59
61
  }
60
- // --publish's whole job is to end this run with `git commit` + `git push origin main` (see the
61
- // PUBLISH block far below) — and those two commands operate on whatever branch is CURRENTLY
62
- // CHECKED OUT in this working tree, not necessarily main. Real failure (2026-07-19): the nightly
63
- // fired at 03:15 while a developer had feat/meta-proxy-passthrough checked out, and commit 4a10833
64
- // "Nightly brain refresh v3.4.21-dev" landed silently on THAT branch — a version-bump commit no
65
- // release will ever be cut from, stranded on top of someone's unrelated in-progress work.
66
- // Guard here, before ANY of the (hours-long, CPU-bound) rebuild work below even starts, and before
67
- // the GitHub Release / npm publish steps run — checking branch only at commit-time would still
68
- // burn a full night's rebuild, or worse, leave a Release cut with no matching version-bump commit
69
- // to go with it. We deliberately do NOT `git checkout main` to self-correct: this machine is
70
- // routinely mid-task on a feature branch at 03:15 (that is exactly how this bug was found), and
71
- // switching branches out from under someone's uncommitted work is precisely the "clever and
72
- // destructive" move this repo has already been burned by once (see the -readonly/timeout lesson in
73
- // CLAUDE.md Rule 19). An aborted nightly that logs clearly and reruns tomorrow costs nothing; a
74
- // checkout that clobbers a developer's working tree cannot be undone.
75
- // ── THE BRANCH GUARD IS GONE, AND THAT IS THE FIX (2026-07-24) ──────────────────────────────────
76
- //
77
- // It used to FATAL here when ROOT was not on main. That guard was CORRECT for what it knew: these
78
- // git commands act on whatever branch is checked out, and on 2026-07-19 that stranded commit 4a10833
79
- // on feat/meta-proxy-passthrough. Refusing beat clobbering a developer's working tree.
80
- //
81
- // But refusing had a cost nobody was watching. This file is the ONLY thing that cuts GitHub Releases
82
- // (`release.mjs --publish` does npm and contains zero `gh release` calls). So on 2026-07-23 the
83
- // nightly hit this guard — the repo was on branch 4.0 — aborted correctly, and the release channel
84
- // froze at v3.9.18-dev while npm advanced to 3.9.50. Thirty-two versions of drift produced by a guard
85
- // working exactly as designed, invisible because the channel verifier only checked the bundle was
86
- // REACHABLE, never that it was CURRENT.
87
- //
88
- // The dilemma was false. It only existed because the version commit ran in ROOT. It now runs in a
89
- // worktree pinned to origin/main (see the PUBLISH block), so the developer's tree is never touched
90
- // AND the release is never blocked by what they have checked out. Neither horn, rather than a better
91
- // choice between them.
92
- //
93
- // Nothing replaces this check because nothing needs to: ROOT's branch is now irrelevant to publishing.
94
- // If the worktree cannot be prepared, THAT fails loudly and refuses to fall back to committing in
95
- // ROOT — the guarantee this guard existed to provide, kept, without the outage it caused.
62
+ if (APPLY) {
63
+ try { assertIsolatedMutationWorktree(ROOT, 'self-update --apply'); }
64
+ catch (error) { console.error(error?.message || String(error)); process.exit(2); }
65
+ }
66
+ // Historical note: this script once published and committed from whichever branch happened to be
67
+ // checked out. Publication authority has since moved to protected-release.yml and --publish is
68
+ // refused above. The remaining rebuild still writes generated candidate source, so the stronger
69
+ // boundary is now structural: assertIsolatedMutationWorktree refuses primary, nested, non-linked,
70
+ // and dirty worktrees before any expensive or mutating step begins.
96
71
  // --fresh-window <days>: LIVE-scan the org(s) and take every non-fork/non-archived repo pushed within
97
- // N days, bypassing the static registry.tiers.json AND the nightly T3 skip. This is THE fix for
98
- // "new repos rUv shipped never got ingested" — a brand-new repo is discovered and built the same night
72
+ // N days, bypassing the static registry.tiers.json AND the default T3 skip. This is THE fix for
73
+ // "new repos rUv shipped never got ingested" — a brand-new repo is discovered and built in the same run
99
74
  // it appears, but only within the rolling window, so the old "days of compute for all 173" risk that the
100
75
  // tier gate guarded against still cannot happen. Absent this flag, scoping is byte-for-byte unchanged.
101
76
  const FRESH_WINDOW = arg('--fresh-window', null);
@@ -158,7 +133,7 @@ if (FRESH_WINDOW) {
158
133
  if (TIER && t !== TIER) continue;
159
134
  for (const r of tiers.tiers[t].repos) {
160
135
  if (ONLY && r.name !== ONLY) continue;
161
- if (t === 'T3' && !ONLY) continue; // T3 is deep-walked on demand, not nightly
136
+ if (t === 'T3' && !ONLY) continue; // T3 is deep-walked only when named explicitly
162
137
  inScope.push({ ...r, tier: t });
163
138
  }
164
139
  }
@@ -205,9 +180,9 @@ if (typeof orgFailures !== 'undefined' && orgFailures > 0 && inScope.length ===
205
180
 
206
181
  console.log(`self-update ${APPLY ? '(APPLY)' : '(dry-run)'} — ${plan.length} repos in scope\n`);
207
182
  for (const p of plan) console.log(` ${p.action.padEnd(20)} ${p.tier} ${p.name.padEnd(24)} built:${p.built} live:${p.live}`);
208
- // SAFE NIGHTLY SCOPE: by default only REBUILD already-built repos whose upstream changed (keeps the
183
+ // SAFE DEFAULT SCOPE: only REBUILD already-built repos whose upstream changed (keeps the
209
184
  // shipped bundle current). Building brand-new repos is a supervised, multi-hour scaling effort, NOT an
210
- // unattended nightly job — gate it behind --include-new so the cron can't silently try to deep-walk 40+
185
+ // implicit default — gate it behind --include-new so an ordinary author run cannot silently deep-walk 40+
211
186
  // repos (days of compute) on its first run.
212
187
  // Fresh-window mode exists precisely to discover + build brand-new repos, so default INCLUDE_NEW on
213
188
  // there (still bounded to the N-day set). Static-tier mode keeps the original opt-in gate untouched.
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { readRvfGenerations, sha256File } from './rvf-generation.mjs';
8
+ import { canonicalJson, coverageGenerationFor, digest, validateGistAggregateReceipt } from './coverage-integrity.mjs';
9
+
10
+ export { canonicalJson, digest } from './coverage-integrity.mjs';
11
+
12
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
13
+ const REPO_QUERY = `query($login:String!,$cursor:String){
14
+ user(login:$login){
15
+ publicRepositories:repositories(privacy:PUBLIC){totalCount}
16
+ repositories(first:100,after:$cursor,privacy:PUBLIC,ownerAffiliations:OWNER,orderBy:{field:NAME,direction:ASC}){
17
+ pageInfo{hasNextPage endCursor}
18
+ nodes{databaseId name url description homepageUrl isFork isArchived isDisabled diskUsage updatedAt pushedAt
19
+ defaultBranchRef{name target{... on Commit{oid committedDate}}}}
20
+ }
21
+ }
22
+ }`;
23
+
24
+ function runGh(args) {
25
+ const result = spawnSync('gh', args, { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
26
+ if (result.status !== 0) throw new Error(`gh ${args.join(' ')} failed: ${(result.stderr || '').trim()}`);
27
+ return result.stdout;
28
+ }
29
+
30
+ export function observeRepositories(owner, { gh = runGh } = {}) {
31
+ const rows = [];
32
+ const pages = [];
33
+ let cursor = '';
34
+ do {
35
+ const raw = gh(['api', 'graphql', '-f', `query=${REPO_QUERY}`, '-F', `login=${owner}`,
36
+ ...(cursor ? ['-F', `cursor=${cursor}`] : [])]);
37
+ const body = JSON.parse(raw);
38
+ const connection = body?.data?.user?.repositories;
39
+ if (!connection) throw new Error('GitHub repository enumeration returned no repository connection');
40
+ pages.push({ cursor: cursor || null, responseDigest: digest(body), count: connection.nodes.length,
41
+ endCursor: connection.pageInfo.endCursor, hasNextPage: connection.pageInfo.hasNextPage });
42
+ rows.push(...connection.nodes);
43
+ cursor = connection.pageInfo.hasNextPage ? connection.pageInfo.endCursor : '';
44
+ } while (cursor);
45
+ const expected = rows.length ? Number(JSON.parse(gh(['api', `users/${owner}`])).public_repos) : 0;
46
+ if (rows.length !== expected) throw new Error(`repository enumeration incomplete: ${rows.length}/${expected}`);
47
+ return { rows, expected, pages };
48
+ }
49
+
50
+ export function observeExternalRepositories(sources, { gh = runGh } = {}) {
51
+ const rows = [];
52
+ const pages = [];
53
+ for (const source of sources) {
54
+ if (!source?.store || !/^[^/]+\/[^/]+$/.test(source?.repository || '')) {
55
+ throw new Error('external source requires a store and owner/repository');
56
+ }
57
+ const repo = JSON.parse(gh(['api', `repos/${source.repository}`]));
58
+ const commit = repo.default_branch
59
+ ? JSON.parse(gh(['api', `repos/${source.repository}/commits/${repo.default_branch}`]))
60
+ : null;
61
+ const row = {
62
+ databaseId: repo.id, name: repo.name, fullName: repo.full_name, storeName: source.store,
63
+ url: repo.html_url, description: repo.description || null, homepageUrl: repo.homepage || null,
64
+ isFork: false, upstreamIsFork: repo.fork,
65
+ isArchived: repo.archived, isDisabled: repo.disabled,
66
+ diskUsage: repo.size, updatedAt: repo.updated_at, pushedAt: repo.pushed_at,
67
+ defaultBranchRef: commit ? { name: repo.default_branch, target: {
68
+ oid: commit.sha, committedDate: commit.commit?.committer?.date || commit.commit?.author?.date || null,
69
+ } } : null,
70
+ };
71
+ rows.push(row);
72
+ pages.push({ repository: source.repository, responseDigest: digest({ repo, commit }), count: 1, terminal: true });
73
+ }
74
+ return { rows, expected: rows.length, pages };
75
+ }
76
+
77
+ export function observeSourceUniverse({
78
+ owner = 'ruvnet',
79
+ externalSources = [],
80
+ gh = runGh,
81
+ observedAt = new Date().toISOString(),
82
+ } = {}) {
83
+ const primary = observeRepositories(owner, { gh });
84
+ const external = observeExternalRepositories(externalSources, { gh });
85
+ const repositoryRows = canonicalRepositoryRows([...primary.rows, ...external.rows]);
86
+ const repositories = sourceSetObservation(repositoryRows, primary.expected + external.expected);
87
+ const repositoryKeys = new Set();
88
+ const storeKeys = new Set();
89
+ for (const repo of repositories.rows) {
90
+ const repositoryKey = String(repo.fullName || `${owner}/${repo.name}`).toLowerCase();
91
+ const storeKey = storeName(repo.storeName || repo.name);
92
+ if (repositoryKeys.has(repositoryKey)) throw new Error(`source observation has duplicate repository: ${repositoryKey}`);
93
+ if (storeKeys.has(storeKey)) throw new Error(`source observation has colliding store name: ${storeKey}`);
94
+ repositoryKeys.add(repositoryKey);
95
+ storeKeys.add(storeKey);
96
+ }
97
+ const observedGists = observeGists(owner, { gh });
98
+ const gists = sourceSetObservation(canonicalGistRows(observedGists.rows), observedGists.expected);
99
+ return canonicalSourceObservation({
100
+ schemaVersion: 1,
101
+ kind: 'ruvnet-brain-source-observation',
102
+ owner,
103
+ observedAt,
104
+ repositories,
105
+ gists,
106
+ });
107
+ }
108
+
109
+ export function sourceObservationDigest(observation) {
110
+ const stable = canonicalSourceObservation(observation, { seal: false });
111
+ return digest({ schemaVersion: stable.schemaVersion, kind: stable.kind,
112
+ owner: stable.owner, repositories: stable.repositories, gists: stable.gists });
113
+ }
114
+
115
+ function sourceSetObservation(rows, expected) {
116
+ return { rows, expected, pages: [{ index: 1, responseDigest: digest(rows), count: rows.length, terminal: true }] };
117
+ }
118
+
119
+ export function canonicalRepositoryRows(rows = []) {
120
+ return rows.map((repo) => ({
121
+ databaseId: repo?.databaseId,
122
+ name: repo?.name,
123
+ ...(repo?.fullName === undefined ? {} : { fullName: repo.fullName }),
124
+ ...(repo?.storeName === undefined ? {} : { storeName: repo.storeName }),
125
+ url: repo?.url,
126
+ description: repo?.description ?? null,
127
+ homepageUrl: repo?.homepageUrl ?? null,
128
+ isFork: repo?.isFork === true,
129
+ ...(repo?.upstreamIsFork === undefined ? {} : { upstreamIsFork: repo.upstreamIsFork === true }),
130
+ isArchived: repo?.isArchived === true,
131
+ isDisabled: repo?.isDisabled === true,
132
+ diskUsage: repo?.diskUsage,
133
+ updatedAt: repo?.updatedAt ?? null,
134
+ pushedAt: repo?.pushedAt ?? null,
135
+ defaultBranchRef: repo?.defaultBranchRef ? {
136
+ name: repo.defaultBranchRef.name,
137
+ target: repo.defaultBranchRef.target ? {
138
+ oid: repo.defaultBranchRef.target.oid,
139
+ committedDate: repo.defaultBranchRef.target.committedDate ?? null,
140
+ } : null,
141
+ } : null,
142
+ })).sort((a, b) => {
143
+ const identity = (row) => String(row.fullName || row.name || '').toLowerCase();
144
+ return identity(a).localeCompare(identity(b))
145
+ || String(a.storeName || '').toLowerCase().localeCompare(String(b.storeName || '').toLowerCase())
146
+ || Number(a.databaseId || 0) - Number(b.databaseId || 0);
147
+ });
148
+ }
149
+
150
+ export function canonicalGistRows(rows = []) {
151
+ return rows.map((gist) => ({
152
+ id: gist?.id,
153
+ updated_at: gist?.updated_at ?? null,
154
+ html_url: gist?.html_url,
155
+ files: Object.fromEntries(Object.entries(gist?.files || {}).sort(([a], [b]) => a.localeCompare(b))
156
+ .map(([key, file]) => [key, {
157
+ filename: file?.filename,
158
+ raw_url: file?.raw_url,
159
+ size: file?.size,
160
+ type: file?.type,
161
+ language: file?.language ?? null,
162
+ }])),
163
+ })).sort((a, b) => String(a.id || '').localeCompare(String(b.id || '')));
164
+ }
165
+
166
+ export function canonicalSourceObservation(observation, { seal = true } = {}) {
167
+ const repositories = sourceSetObservation(canonicalRepositoryRows(observation?.repositories?.rows),
168
+ observation?.repositories?.expected);
169
+ const gists = sourceSetObservation(canonicalGistRows(observation?.gists?.rows), observation?.gists?.expected);
170
+ const stable = {
171
+ schemaVersion: observation?.schemaVersion,
172
+ kind: observation?.kind,
173
+ owner: observation?.owner,
174
+ observedAt: observation?.observedAt,
175
+ repositories,
176
+ gists,
177
+ };
178
+ if (!seal) return stable;
179
+ return { ...stable, observationSha256: sourceObservationDigest(stable) };
180
+ }
181
+
182
+ export function gistVersion(gist) {
183
+ const versions = Object.values(gist.files || {}).flatMap((file) => {
184
+ const match = String(file.raw_url || '').match(/\/raw\/([0-9a-f]{7,64})\//i);
185
+ return match ? [match[1]] : [];
186
+ });
187
+ return versions.length && new Set(versions).size === 1 ? versions[0] : null;
188
+ }
189
+
190
+ export function observeGists(owner, { gh = runGh } = {}) {
191
+ const raw = gh(['api', `users/${owner}/gists?per_page=100`, '--paginate', '--slurp']);
192
+ const parsed = JSON.parse(raw);
193
+ const pages = Array.isArray(parsed[0]) ? parsed : [parsed];
194
+ const rows = pages.flat().filter((gist) => gist?.id);
195
+ const expected = Number(JSON.parse(gh(['api', `users/${owner}`])).public_gists);
196
+ if (rows.length !== expected) throw new Error(`gist enumeration incomplete: ${rows.length}/${expected}`);
197
+ return {
198
+ rows,
199
+ expected,
200
+ pages: pages.map((page, index) => ({ index: index + 1, responseDigest: digest(page), count: page.length,
201
+ terminal: index === pages.length - 1 })),
202
+ };
203
+ }
204
+
205
+ function storeName(name) { return String(name).toLowerCase(); }
206
+
207
+ export function classifyRepository(repo, evidence, exclusion = null) {
208
+ const activeExclusion = exclusion && String(exclusion.pushedAt || '') !== ''
209
+ && String(exclusion.pushedAt) === String(repo.pushedAt || '');
210
+ const disposition = activeExclusion ? 'excluded-no-corpus' : repo.isFork ? 'fork' : repo.isArchived ? 'archived' :
211
+ repo.isDisabled ? 'disabled' : !repo.defaultBranchRef ? 'empty' : 'eligible';
212
+ const upstreamSha = repo.defaultBranchRef?.target?.oid || null;
213
+ const reasons = [];
214
+ let status = 'CURRENT';
215
+ if (disposition !== 'eligible') {
216
+ status = 'INELIGIBLE';
217
+ if (activeExclusion && exclusion?.reason) reasons.push(exclusion.reason);
218
+ }
219
+ else if (!evidence.rvfPresent) { status = 'MISSING'; reasons.push('canonical RVF is absent'); }
220
+ else if (!evidence.receipt?.sourceCommit) { status = 'UNVERIFIED'; reasons.push('RVF receipt has no sourceCommit'); }
221
+ else if (evidence.receipt.sourceCommit !== upstreamSha) { status = 'STALE'; reasons.push('receipt sourceCommit differs from upstream HEAD'); }
222
+ else if (!evidence.bytesVerified) { status = 'FAILED'; reasons.push('RVF bytes do not match receipt'); }
223
+ else if (!evidence.passagesPresent) { status = 'FAILED'; reasons.push('passage inventory is absent'); }
224
+ return {
225
+ key: repo.fullName ? `repo:${repo.fullName.toLowerCase()}` : `repo:${repo.databaseId}`,
226
+ kind: 'repository',
227
+ name: repo.name,
228
+ url: repo.url,
229
+ routing: { description: repo.description || null, homepageUrl: repo.homepageUrl || null,
230
+ capabilityCardPresent: evidence.cardPresent === true },
231
+ disposition,
232
+ upstream: { sha: upstreamSha, committedAt: repo.defaultBranchRef?.target?.committedDate || null,
233
+ pushedAt: repo.pushedAt, updatedAt: repo.updatedAt },
234
+ artifact: { store: storeName(repo.storeName || repo.name), sourceCommit: evidence.receipt?.sourceCommit || null,
235
+ ingestedAt: evidence.receipt?.builtUtc || null, rvfSha256: evidence.receipt?.sha256 || null,
236
+ bytesVerified: evidence.bytesVerified, passagesPresent: evidence.passagesPresent,
237
+ cardPresent: evidence.cardPresent },
238
+ status,
239
+ reasons,
240
+ };
241
+ }
242
+
243
+ export function classifyGist(gist, evidence) {
244
+ const source = evidence.sources?.gists?.[gist.id] || null;
245
+ const ingestedAt = source?.ingestedAt || evidence.cache?.[gist.id] || null;
246
+ // The list API does not expose gist history.version. A just-fetched individual source receipt is
247
+ // still the authoritative version when the live list's updated_at is unchanged; otherwise the
248
+ // list proves drift and the row is stale until the individual gist is refreshed.
249
+ const version = source?.updatedAt === gist.updated_at ? source.versionSha : gistVersion(gist);
250
+ const currentByDate = source?.updatedAt === gist.updated_at || evidence.cache?.[gist.id] === gist.updated_at;
251
+ let status = 'CURRENT';
252
+ const reasons = [];
253
+ if (!evidence.rvfPresent) { status = 'MISSING'; reasons.push('ruv-gists RVF is absent'); }
254
+ else if (!evidence.receipt || !source || !version) { status = 'UNVERIFIED'; reasons.push('per-gist source receipt is absent'); }
255
+ else if (!evidence.bytesVerified) { status = 'FAILED'; reasons.push('ruv-gists RVF bytes do not match the generation receipt'); }
256
+ else if (!evidence.passagesBound) { status = 'FAILED'; reasons.push('gist passages do not match the source receipt'); }
257
+ else if (!source.complete) { status = 'FAILED'; reasons.push('per-gist source receipt is incomplete'); }
258
+ else if (source.versionSha !== version || !currentByDate) { status = 'STALE'; reasons.push('ingested gist identity differs from upstream'); }
259
+ const filenames = Object.values(gist.files || {}).map((file) => file?.filename).filter(Boolean).sort();
260
+ return {
261
+ key: `gist:${gist.id}`,
262
+ kind: 'gist',
263
+ name: Object.values(gist.files || {})[0]?.filename || gist.id,
264
+ url: gist.html_url,
265
+ disposition: 'eligible',
266
+ upstream: { sha: version, updatedAt: gist.updated_at, fileCount: filenames.length, files: filenames },
267
+ artifact: { store: 'ruv-gists', sourceCommit: source?.versionSha || null, ingestedAt: source?.ingestedAt || ingestedAt,
268
+ contentDigest: source?.contentDigest || null, fileCount: source?.files?.length || null,
269
+ rvfSha256: evidence.receipt?.sha256 || null, bytesVerified: evidence.bytesVerified },
270
+ status,
271
+ reasons,
272
+ };
273
+ }
274
+
275
+ function artifactEvidence(kbDir, ledger, cardStores, name) {
276
+ const store = storeName(name);
277
+ const receipt = Object.entries(ledger.stores).find(([key]) => key.toLowerCase() === store)?.[1] || null;
278
+ const rvfFile = receipt?.file || `${store}.big.rvf`;
279
+ const rvfPath = path.join(kbDir, rvfFile);
280
+ const rvfPresent = fs.existsSync(rvfPath) && fs.lstatSync(rvfPath).isFile();
281
+ return {
282
+ receipt,
283
+ rvfPresent,
284
+ bytesVerified: Boolean(rvfPresent && receipt?.sha256 && sha256File(rvfPath) === receipt.sha256),
285
+ passagesPresent: fs.existsSync(path.join(kbDir, `${store}.passages.jsonl`))
286
+ && fs.statSync(path.join(kbDir, `${store}.passages.jsonl`)).size > 0,
287
+ cardPresent: cardStores.has(store),
288
+ };
289
+ }
290
+
291
+ export function renderMarkdown(coverage) {
292
+ const counts = coverage.totals.byStatus;
293
+ const lines = [
294
+ '# RuvNet Brain source coverage', '',
295
+ `Generated: ${coverage.observedAt} `,
296
+ `Coverage generation: \`${coverage.coverageGeneration}\` `,
297
+ `Repositories: ${coverage.totals.repositories} · Gists: ${coverage.totals.gists} · ` +
298
+ Object.entries(counts).sort().map(([state, count]) => `${state} ${count}`).join(' · '), '',
299
+ '> `CURRENT` is artifact-bound. Clone state and timestamps alone never establish freshness.', '',
300
+ '## Repositories', '',
301
+ '| Repository | Upstream updated | Upstream SHA | Ingested | Ingested SHA | State | Reason |',
302
+ '|---|---:|---|---:|---|---|---|',
303
+ ];
304
+ for (const row of coverage.rows.filter((entry) => entry.kind === 'repository')) {
305
+ lines.push(`| [${row.name}](${row.url}) | ${row.upstream.committedAt || row.upstream.updatedAt || '—'} | ${row.upstream.sha || '—'} | ${row.artifact.ingestedAt || '—'} | ${row.artifact.sourceCommit || '—'} | ${row.status} | ${row.reasons.join('; ') || row.disposition} |`);
306
+ }
307
+ lines.push('', '## Public gists', '',
308
+ '| Gist | Upstream updated | Version SHA | Ingested update | State | Reason |',
309
+ '|---|---:|---|---:|---|---|');
310
+ for (const row of coverage.rows.filter((entry) => entry.kind === 'gist')) {
311
+ lines.push(`| [${row.name}](${row.url}) | ${row.upstream.updatedAt || '—'} | ${row.upstream.sha || '—'} | ${row.artifact.ingestedAt || '—'} | ${row.status} | ${row.reasons.join('; ') || '—'} |`);
312
+ }
313
+ return `${lines.join('\n')}\n`;
314
+ }
315
+
316
+ export function sealCoverage({ owner, repositories, gists, rows, generatorSourceSha, snapshotRoot, observedAt,
317
+ sourceObservationSha256 = null,
318
+ policyDispositionDigests = [], exemptionDigests = [] }) {
319
+ const enumerationReceipt = {
320
+ schemaVersion: 1, owner, observedAt, requestParameters: { repositoryPageSize: 100, gistPageSize: 100 },
321
+ repositories: { expected: repositories.expected, pages: repositories.pages },
322
+ gists: { expected: gists.expected, pages: gists.pages },
323
+ duplicateKeys: rows.length - new Set(rows.map((row) => row.key)).size,
324
+ terminal: true,
325
+ };
326
+ const orderedRows = [...rows].sort((a, b) => a.key.localeCompare(b.key));
327
+ const byStatus = Object.fromEntries([...new Set(orderedRows.map((row) => row.status))].sort()
328
+ .map((status) => [status, orderedRows.filter((row) => row.status === status).length]));
329
+ return { schemaVersion: 1, kind: 'ruvnet-brain-corpus-coverage', owner, observedAt, generatorSourceSha,
330
+ sourceObservationSha256, snapshotRoot,
331
+ policy: { policyDispositionDigests, exemptionDigests },
332
+ coverageGeneration: coverageGenerationFor({ generatorSourceSha, snapshotRoot, sourceObservationSha256, rows: orderedRows,
333
+ enumerationReceipt, policyDispositionDigests, exemptionDigests }), enumerationReceipt, rows: orderedRows,
334
+ totals: { repositories: repositories.expected, gists: gists.expected, rows: orderedRows.length, byStatus } };
335
+ }
336
+
337
+ export function buildCoverage({ owner = 'ruvnet', kbDir = path.join(ROOT, 'kb'), policyDir = kbDir,
338
+ observation = null, gh = runGh, now = () => new Date().toISOString() } = {}) {
339
+ const externalPath = path.join(policyDir, 'external-sources.json');
340
+ const externalPolicy = fs.existsSync(externalPath) ? JSON.parse(fs.readFileSync(externalPath, 'utf8')) : { sources: [] };
341
+ if (!Array.isArray(externalPolicy.sources)) throw new Error('external-sources.json has no sources array');
342
+ const suppliedObservation = observation || observeSourceUniverse({
343
+ owner, externalSources: externalPolicy.sources, gh, observedAt: now(),
344
+ });
345
+ if (suppliedObservation?.kind !== 'ruvnet-brain-source-observation'
346
+ || suppliedObservation.owner !== owner
347
+ || suppliedObservation.observationSha256 !== sourceObservationDigest(suppliedObservation)) {
348
+ throw new Error('source observation identity is missing or invalid');
349
+ }
350
+ const sourceObservation = canonicalSourceObservation(suppliedObservation);
351
+ const repositories = sourceObservation.repositories;
352
+ const gists = sourceObservation.gists;
353
+ const ledger = readRvfGenerations(kbDir);
354
+ const cards = fs.readFileSync(path.join(kbDir, 'capability-cards.md'), 'utf8');
355
+ const cardStores = new Set([...cards.matchAll(/^##\s+(.+?)\s*$/gm)].map((match) => storeName(match[1])));
356
+ const gistCachePath = path.join(kbDir, '.ruv-gists.cache.json');
357
+ const gistCache = fs.existsSync(gistCachePath) ? JSON.parse(fs.readFileSync(gistCachePath, 'utf8')) : {};
358
+ const gistSourcesPath = path.join(kbDir, 'ruv-gists.sources.json');
359
+ const gistSources = fs.existsSync(gistSourcesPath) ? JSON.parse(fs.readFileSync(gistSourcesPath, 'utf8')) : null;
360
+ const exclusionsPath = path.join(policyDir, 'no-corpus-repos.json');
361
+ const exclusions = fs.existsSync(exclusionsPath) ? JSON.parse(fs.readFileSync(exclusionsPath, 'utf8')) : {};
362
+ const rows = repositories.rows.map((repo) => {
363
+ const store = storeName(repo.storeName || repo.name);
364
+ return classifyRepository(repo, artifactEvidence(kbDir, ledger, cardStores, store), exclusions[store] || null);
365
+ });
366
+ const gistEvidence = { ...artifactEvidence(kbDir, ledger, cardStores, 'ruv-gists'), cache: gistCache, sources: gistSources };
367
+ try {
368
+ validateGistAggregateReceipt({ receipt: gistSources,
369
+ passagesFile: path.join(kbDir, 'ruv-gists.passages.jsonl'),
370
+ expectedIds: gists.rows.map(({ id }) => id),
371
+ sourceObservationSha256: sourceObservation.observationSha256 });
372
+ gistEvidence.passagesBound = true;
373
+ } catch {
374
+ gistEvidence.passagesBound = false;
375
+ }
376
+ rows.push(...gists.rows.map((gist) => classifyGist(gist, gistEvidence)));
377
+ const generatorSourceSha = sha256File(fileURLToPath(import.meta.url));
378
+ const snapshotRoot = digest({ stores: ledger.stores, files: rows.filter((row) => row.kind === 'repository')
379
+ .map((row) => ({ store: row.artifact.store, sha256: row.artifact.rvfSha256 })) });
380
+ return sealCoverage({ owner, repositories, gists, rows, generatorSourceSha, snapshotRoot,
381
+ observedAt: sourceObservation.observedAt, sourceObservationSha256: sourceObservation.observationSha256,
382
+ policyDispositionDigests: fs.existsSync(externalPath) ? [sha256File(externalPath)] : [],
383
+ exemptionDigests: fs.existsSync(exclusionsPath) ? [sha256File(exclusionsPath)] : [] });
384
+ }
385
+
386
+ export async function main(argv = process.argv.slice(2)) {
387
+ const ownerIndex = argv.indexOf('--owner');
388
+ const owner = ownerIndex >= 0 ? argv[ownerIndex + 1] : 'ruvnet';
389
+ const assetsIndex = argv.indexOf('--assets');
390
+ const kbDir = path.resolve(assetsIndex >= 0 ? argv[assetsIndex + 1] : path.join(ROOT, 'kb'));
391
+ const jsonPath = path.join(ROOT, 'data', 'source-coverage.json');
392
+ const markdownPath = path.join(ROOT, 'docs', 'RUVNET-COVERAGE.md');
393
+ const recorded = argv.includes('--check') && fs.existsSync(jsonPath)
394
+ ? JSON.parse(fs.readFileSync(jsonPath, 'utf8')) : null;
395
+ const coverage = buildCoverage({ owner, kbDir,
396
+ now: recorded?.observedAt ? () => recorded.observedAt : () => new Date().toISOString() });
397
+ const json = `${JSON.stringify(coverage, null, 2)}\n`;
398
+ const markdown = renderMarkdown(coverage);
399
+ if (argv.includes('--check')) {
400
+ const matches = fs.existsSync(jsonPath) && fs.readFileSync(jsonPath, 'utf8') === json &&
401
+ fs.existsSync(markdownPath) && fs.readFileSync(markdownPath, 'utf8') === markdown;
402
+ const blockers = coverage.rows.filter((row) => row.disposition === 'eligible' && row.status !== 'CURRENT');
403
+ if (!matches) console.error('source coverage projections differ from live observation');
404
+ if (argv.includes('--strict') && blockers.length) console.error(`strict coverage: ${blockers.length} eligible row(s) are not CURRENT`);
405
+ return matches && (!argv.includes('--strict') || blockers.length === 0) ? 0 : 1;
406
+ }
407
+ fs.writeFileSync(jsonPath, json);
408
+ fs.writeFileSync(markdownPath, markdown);
409
+ console.log(`wrote ${path.relative(ROOT, markdownPath)} (${coverage.rows.length} rows, ${coverage.coverageGeneration})`);
410
+ return 0;
411
+ }
412
+
413
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
414
+ process.exitCode = await main();
415
+ }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import crypto from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ export function canonicalJson(value) {
8
+ if (Array.isArray(value)) {
9
+ return `[${value.map((entry) => canonicalJson(entry === undefined ? null : entry)).join(',')}]`;
10
+ }
11
+ if (value && typeof value === 'object') {
12
+ return `{${Object.keys(value).sort().filter((key) => value[key] !== undefined)
13
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
14
+ }
15
+ return JSON.stringify(value);
16
+ }
17
+
18
+ export const digest = (value) => crypto.createHash('sha256')
19
+ .update(typeof value === 'string' ? value : canonicalJson(value)).digest('hex');
20
+
21
+ const sha256File = (file) => {
22
+ const hash = crypto.createHash('sha256');
23
+ const bytes = fs.readFileSync(file);
24
+ hash.update(bytes);
25
+ return { sha256: hash.digest('hex'), bytes: bytes.length };
26
+ };
27
+
28
+ function gitInventory(root) {
29
+ const result = spawnSync('git', ['ls-files', '-co', '--exclude-standard', '-z'], {
30
+ cwd: root, encoding: 'buffer', maxBuffer: 128 * 1024 * 1024,
31
+ });
32
+ if (result.status !== 0) throw new Error(`repository inventory failed: ${String(result.stderr || '').trim()}`);
33
+ return result.stdout.toString('utf8').split('\0').filter(Boolean).sort();
34
+ }
35
+
36
+ function validateRelativeFile(root, relative) {
37
+ if (typeof relative !== 'string' || !relative || path.isAbsolute(relative)
38
+ || relative.split(/[\\/]/).includes('..')) throw new Error(`unsafe source path: ${relative || '(missing)'}`);
39
+ const absolute = path.join(root, relative);
40
+ const stat = fs.lstatSync(absolute);
41
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`source path is not a regular file: ${relative}`);
42
+ return absolute;
43
+ }
44
+
45
+ function payload(receipt) {
46
+ const { receiptSha256: _receiptSha256, ...rest } = receipt;
47
+ return rest;
48
+ }
49
+
50
+ export function buildSourceScopeReceipt({ root, governedPaths, sourceSha = null, inventory = gitInventory } = {}) {
51
+ const repositoryRoot = path.resolve(root || '.');
52
+ const paths = inventory(repositoryRoot);
53
+ if (!paths.length || new Set(paths).size !== paths.length) throw new Error('repository inventory is empty or duplicated');
54
+ const files = paths.map((relative) => ({ path: relative, ...sha256File(validateRelativeFile(repositoryRoot, relative)) }));
55
+ const governed = [...new Set(governedPaths || [])].sort();
56
+ if (!governed.length) throw new Error('governed source scope is empty');
57
+ const inventorySet = new Set(paths);
58
+ const missing = governed.filter((relative) => !inventorySet.has(relative));
59
+ if (missing.length) throw new Error(`governed source is outside repository inventory: ${missing.join(', ')}`);
60
+ const byPath = new Map(files.map((row) => [row.path, row]));
61
+ const governedFiles = governed.map((relative) => ({ ...byPath.get(relative), readComplete: true }));
62
+ const body = {
63
+ schemaVersion: 1,
64
+ kind: 'ruvnet-brain-source-scope-receipt',
65
+ sourceSha,
66
+ repository: { fileCount: files.length, inventorySha256: digest(files) },
67
+ governed: { fileCount: governedFiles.length, files: governedFiles },
68
+ };
69
+ return { ...body, receiptSha256: digest(body) };
70
+ }
71
+
72
+ export function validateSourceScopeReceipt(receipt, { root, inventory = gitInventory } = {}) {
73
+ if (receipt?.schemaVersion !== 1 || receipt?.kind !== 'ruvnet-brain-source-scope-receipt'
74
+ || !Number.isInteger(receipt.repository?.fileCount) || !Array.isArray(receipt.governed?.files)
75
+ || receipt.governed.files.some((row) => row.readComplete !== true)) throw new Error('source scope receipt is malformed');
76
+ if (digest(payload(receipt)) !== receipt.receiptSha256) throw new Error('source scope receipt digest mismatch');
77
+ const rebuilt = buildSourceScopeReceipt({ root, sourceSha: receipt.sourceSha,
78
+ governedPaths: receipt.governed.files.map(({ path: relative }) => relative), inventory });
79
+ if (canonicalJson(rebuilt.repository) !== canonicalJson(receipt.repository)
80
+ || canonicalJson(rebuilt.governed) !== canonicalJson(receipt.governed)) {
81
+ throw new Error('source scope receipt differs from the current complete repository inventory');
82
+ }
83
+ return receipt;
84
+ }
Binary file