ruvnet-brain 4.0.90-dev → 4.3.1-dev
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 +31 -7
- package/bin/install.mjs +153 -22
- package/console/architecture.html +12 -14
- package/console/install-mockup.html +3 -3
- package/kb/zip-extract.mjs +8 -2
- package/package.json +7 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/hooks/codex-hooks.json +1 -1
- package/plugin/hooks/hooks.json +10 -0
- package/plugin/host-adapters/claude.json +11 -0
- package/plugin/host-adapters/codex.json +11 -0
- package/plugin/mcp/managed-cli-interface.mjs +77 -10
- package/plugin/mcp/server.mjs +2 -1
- package/plugin/scripts/anticipate.sh +30 -1
- package/plugin/scripts/capability-claim-evidence.mjs +431 -0
- package/plugin/scripts/capability-inventory-receipt.mjs +225 -0
- package/plugin/scripts/capability-registry.mjs +6 -0
- package/plugin/scripts/capability-routing.mjs +70 -0
- package/plugin/scripts/continuation-gate.mjs +68 -2
- package/plugin/scripts/coverage-integrity.mjs +575 -0
- package/plugin/scripts/decision-outcomes.mjs +0 -0
- package/plugin/scripts/ground-ruvnet.sh +2 -2
- package/plugin/scripts/nightly-controller.mjs +19 -2
- package/plugin/scripts/project-progression-contract.mjs +409 -0
- package/plugin/scripts/project-progression-hook.mjs +196 -0
- package/plugin/scripts/project-progression-outbox.mjs +95 -0
- package/plugin/scripts/project-progression-session-start.mjs +183 -0
- package/plugin/scripts/project-progression-store.mjs +248 -0
- package/plugin/scripts/project-store-resolver.mjs +106 -0
- package/plugin/scripts/session-snapshot-hook.mjs +29 -1
- package/plugin/scripts/session-start-core.mjs +22 -18
- package/plugin/scripts/spend-guard.mjs +6 -1
- package/scripts/adr-072-completion.mjs +98 -0
- package/scripts/behavioral-l1-l4.mjs +11 -8
- package/scripts/brain-score.mjs +23 -9
- package/scripts/build-bundle.mjs +94 -8
- package/scripts/card-from-source.mjs +140 -0
- package/scripts/corpus-aggregates.mjs +194 -0
- package/scripts/corpus-candidate.mjs +314 -0
- package/scripts/corpus-reconcile.mjs +540 -0
- package/scripts/corpus-seed-publish.mjs +110 -0
- package/scripts/coverage-integrity.mjs +3 -0
- package/scripts/gist-receipts.mjs +183 -0
- package/scripts/git-hooks/pre-push +21 -0
- package/scripts/host-registry.mjs +118 -0
- package/scripts/independent-review-receipt.mjs +499 -0
- package/scripts/ingest-gists.mjs +34 -5
- package/scripts/ingest-new-repos.mjs +127 -0
- package/scripts/metaharness-gate.mjs +58 -0
- package/scripts/nightly-gists.sh +3 -3
- package/scripts/nightly-wrapper.sh +105 -64
- package/scripts/product-integrity-contract.mjs +156 -0
- package/scripts/public-inventory.mjs +4 -0
- package/scripts/public-verification-aggregate.mjs +342 -0
- package/scripts/public-verification-finalizer.mjs +59 -0
- package/scripts/public-verification-inputs.mjs +473 -0
- package/scripts/public-verification-lane.mjs +244 -0
- package/scripts/publication-receipt.mjs +50 -6
- package/scripts/rebuild-gists-from-receipts.mjs +291 -0
- package/scripts/release-projection.mjs +86 -0
- package/scripts/release-transaction-provider.mjs +43 -102
- package/scripts/release-transaction.mjs +165 -49
- package/scripts/release.mjs +158 -3
- package/scripts/restore-local-ingests.mjs +2 -2
- package/scripts/retrieval-canary.mjs +533 -0
- package/scripts/routing-flywheel.mjs +12 -1
- package/scripts/rvf-generation.mjs +50 -0
- package/scripts/self-update.mjs +19 -44
- package/scripts/source-coverage.mjs +415 -0
- package/scripts/source-scope-receipt.mjs +84 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/wired-check.mjs +114 -24
- package/scripts/worktree-integrity.mjs +123 -0
package/scripts/self-update.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// self-update.mjs —
|
|
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
|
-
//
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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
|
|
98
|
-
// "new repos rUv shipped never got ingested" — a brand-new repo is discovered and built the same
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
+
}
|
package/scripts/sync-census.mjs
CHANGED
|
Binary file
|