ruvnet-brain 4.3.20 → 4.3.25
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 +5 -5
- package/bin/install.mjs +383 -78
- package/console/app.js +141 -9
- package/console/index.html +51 -24
- package/console/scope.css +137 -0
- package/console/scope.html +144 -0
- package/console/scope.js +209 -0
- package/console/tips.html +1 -0
- package/kb/corpus-release-identity.mjs +239 -0
- package/kb/update-storage-transaction.mjs +20 -3
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/commands/checkpoint.md +61 -0
- package/plugin/hooks/codex-hooks.json +64 -1
- package/plugin/hooks/hook-contracts.json +299 -6
- package/plugin/hooks/hooks.json +81 -1
- package/plugin/mcp/server.mjs +23 -0
- package/plugin/scripts/advocacy-catalog.mjs +245 -0
- package/plugin/scripts/advocacy-route.mjs +460 -0
- package/plugin/scripts/continuation-gate.mjs +25 -2
- package/plugin/scripts/continuation-objective.mjs +7 -1
- package/plugin/scripts/continuity-hook-policy.mjs +190 -15
- package/plugin/scripts/coverage-integrity.mjs +7 -0
- package/plugin/scripts/gates.mjs +113 -10
- package/plugin/scripts/grounding-turn-gate.mjs +167 -0
- package/plugin/scripts/grounding-turn-mark.mjs +91 -0
- package/plugin/scripts/hook-shim.mjs +14 -0
- package/plugin/scripts/host-shell-boundary.mjs +43 -0
- package/plugin/scripts/nightly-scheduler.mjs +37 -4
- package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
- package/plugin/scripts/project-progression-contract.mjs +16 -0
- package/plugin/scripts/project-progression-hook.mjs +3 -0
- package/plugin/scripts/project-progression-producer.mjs +252 -0
- package/plugin/scripts/project-progression-reader.mjs +271 -0
- package/plugin/scripts/project-progression-session-start.mjs +93 -16
- package/plugin/scripts/project-progression-sources.mjs +220 -0
- package/plugin/scripts/project-progression-store.mjs +106 -13
- package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
- package/plugin/scripts/session-snapshot-hook.mjs +115 -7
- package/plugin/scripts/session-start-budget.mjs +59 -0
- package/plugin/scripts/session-start-core.mjs +234 -457
- package/plugin/scripts/session-start-fsutil.mjs +61 -0
- package/plugin/scripts/session-start-health.mjs +64 -0
- package/plugin/scripts/session-start-hook-description.mjs +45 -0
- package/plugin/scripts/session-start-issue-alert.mjs +77 -0
- package/plugin/scripts/session-start-repo-identity.mjs +54 -0
- package/plugin/scripts/session-start-signals.mjs +73 -0
- package/plugin/scripts/session-start-trace.mjs +86 -0
- package/plugin/scripts/session-start-update-plane.mjs +104 -0
- package/plugin/scripts/unprompted-runtime.mjs +32 -2
- package/plugin/scripts/update-apply.mjs +2 -32
- package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
- package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
- package/scripts/adr-072-completion.mjs +1 -1
- package/scripts/agentdb-fleet-doctor.mjs +5 -1
- package/scripts/approved-runtime.mjs +197 -0
- package/scripts/brain-novice-50.mjs +16 -1
- package/scripts/brain-score.mjs +23 -5
- package/scripts/build-bundle.mjs +971 -530
- package/scripts/build-concepts.mjs +36 -116
- package/scripts/console-engine.test.mjs +8 -7
- package/scripts/console-runtime-identity.mjs +4 -0
- package/scripts/corpus-aggregates.mjs +94 -77
- package/scripts/corpus-candidate.mjs +475 -222
- package/scripts/corpus-next-seed.mjs +225 -0
- package/scripts/corpus-promotion.mjs +58 -0
- package/scripts/corpus-reconcile.mjs +411 -105
- package/scripts/doc-currency.mjs +16 -1
- package/scripts/dual-host-deliberation.mjs +25 -2
- package/scripts/dual-host-suggest.mjs +17 -1
- package/scripts/falsify.mjs +13 -3
- package/scripts/gist-receipts.mjs +482 -87
- package/scripts/github-health-watch.mjs +12 -2
- package/scripts/handoff-asset.mjs +34 -0
- package/scripts/hook-retirement-check.mjs +8 -1
- package/scripts/host-registry.mjs +1 -1
- package/scripts/ingest-gists.mjs +74 -101
- package/scripts/job-heartbeat.sh +77 -14
- package/scripts/learning-replay-execution.mjs +10 -4
- package/scripts/nightly-gists.sh +27 -13
- package/scripts/nightly-two-run-proof.mjs +1 -1
- package/scripts/nightly-watchdog.mjs +61 -4
- package/scripts/onboarding-console.mjs +319 -27
- package/scripts/oracle/produce-questions.mjs +293 -0
- package/scripts/oracle/producer-hosts.mjs +235 -0
- package/scripts/oracle/repo-recall.mjs +448 -0
- package/scripts/oracle/retrieval-accuracy.mjs +818 -0
- package/scripts/oracle/source-tree.mjs +165 -0
- package/scripts/oracle/source-units.mjs +391 -0
- package/scripts/oracle/spike-run.mjs +98 -0
- package/scripts/oracle/unit-inventory.mjs +141 -0
- package/scripts/oracle/unit-sampling.mjs +128 -0
- package/scripts/oracle/validate-labels.mjs +250 -0
- package/scripts/private-overlay.mjs +248 -0
- package/scripts/product-integrity-contract.mjs +1 -1
- package/scripts/proxy/claude-proxied.sh +6 -0
- package/scripts/proxy/proxy-revert.sh +5 -0
- package/scripts/proxy/proxy-up.sh +6 -0
- package/scripts/proxy/proxy-verify.mjs +4 -0
- package/scripts/public-inputs.mjs +409 -0
- package/scripts/public-verification-inputs.mjs +112 -26
- package/scripts/public-verification-lane.mjs +1 -1
- package/scripts/published-surface-probe.mjs +34 -4
- package/scripts/qe/card-lane-gate.mjs +16 -1
- package/scripts/qe/session-start-gate.mjs +16 -1
- package/scripts/rebuild-gists-from-receipts.mjs +58 -78
- package/scripts/record-lesson.mjs +4 -1
- package/scripts/rehearse-corpus-pipeline.mjs +994 -0
- package/scripts/release-abort-stale.mjs +5 -1
- package/scripts/release-authority.mjs +104 -12
- package/scripts/release-channel-kind.mjs +86 -0
- package/scripts/release-convergence-watchdog.mjs +7 -2
- package/scripts/release-projection.mjs +177 -72
- package/scripts/release-transaction-provider.mjs +23 -6
- package/scripts/release.mjs +252 -17
- package/scripts/retrieval-canary.mjs +87 -0
- package/scripts/rvf-index-audit.mjs +573 -13
- package/scripts/rvf-wire.mjs +269 -0
- package/scripts/seal-gist-receipt.mjs +65 -0
- package/scripts/selfcheck.mjs +42 -21
- package/scripts/source-coverage.mjs +253 -24
- package/scripts/status-honesty.mjs +25 -0
- package/scripts/sync-census.mjs +0 -0
- package/scripts/sync-version.mjs +2 -0
- package/scripts/trismart.mjs +42 -0
- package/scripts/updater-manifest.mjs +162 -0
- package/scripts/verify-channels.mjs +17 -5
- package/scripts/wired-check.mjs +48 -10
- package/tri-smart-skill/QUICKSTART.md +37 -0
- package/tri-smart-skill/README.md +92 -0
- package/tri-smart-skill/install.cmd +14 -0
- package/tri-smart-skill/install.command +13 -0
- package/tri-smart-skill/install.mjs +51 -0
- package/tri-smart-skill/install.sh +9 -0
- package/tri-smart-skill/tri-smart/SKILL.md +90 -0
- package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
- package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
- package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
- package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
- package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
- package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
- package/scripts/corpus-seed-publish.mjs +0 -110
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// public-inputs.mjs — the ONE canonical public-prose selection pipeline (Step 3 of the
|
|
3
|
+
// corpus-seed/release pipeline consolidation, 2026-09-13).
|
|
4
|
+
//
|
|
5
|
+
// Before this, "what prose ships to end users" was decided independently in at least three places
|
|
6
|
+
// with three different filters: scripts/build-concepts.mjs (its own chunker/fence),
|
|
7
|
+
// scripts/corpus-aggregates.mjs's buildConceptAggregate (a second, different transformation that
|
|
8
|
+
// ALSO re-derived the private fence from PRIVATE-STORES.json/l2-topics.*.json), and
|
|
9
|
+
// scripts/build-bundle.mjs (a THIRD independent card filter plus wholesale, unfenced-by-itself
|
|
10
|
+
// copies of kb/l2 and kb/repo-aliases.json straight from the checkout). Two more bugs rode along
|
|
11
|
+
// for free: scripts/corpus-reconcile.mjs's syncCorpusInputs OVERLAID primers/l2-topics files onto
|
|
12
|
+
// whatever a prior round had left behind (a deleted seed input never disappeared), and
|
|
13
|
+
// build-bundle.mjs shipped a circular, self-referencing concepts.sources.json whenever the real one
|
|
14
|
+
// was absent (its only declared "input" was its own packaged output).
|
|
15
|
+
//
|
|
16
|
+
// materializePublicInputs is now the single place that decides what public prose exists. It builds
|
|
17
|
+
// a FRESH tree every call (positive selection — nothing is retained just because it was there last
|
|
18
|
+
// time), reuses scripts/private-fence.mjs's already-tested fence decisions directly (never
|
|
19
|
+
// reimplements them), and returns a PUBLIC selection receipt that proves what was excluded — by
|
|
20
|
+
// digest only, never by content — plus a resolved topic-ownership map every derived-store builder
|
|
21
|
+
// can trust instead of re-deriving.
|
|
22
|
+
//
|
|
23
|
+
// Two policies, kept deliberately separate (rule 5 of the Step 3 spec): this file decides PUBLIC
|
|
24
|
+
// PROSE eligibility (what text ships). scripts/source-coverage.mjs / scripts/corpus-reconcile.mjs's
|
|
25
|
+
// planReconciliation decide CODE-INGESTION eligibility (which repos/gists enter the corpus at all).
|
|
26
|
+
// Neither derives the other.
|
|
27
|
+
//
|
|
28
|
+
// SAFE TO CALL WITH outDir POINTING AT builderRoot's OWN kb/ (a plain local `node
|
|
29
|
+
// scripts/build-concepts.mjs` or `node scripts/build-bundle.mjs` run with no --assets uses exactly
|
|
30
|
+
// this). Every selected file is copied into a fresh STAGE directory first; nothing under `outDir` is
|
|
31
|
+
// removed until the entire stage is built and sealed, so a self-materializing call reads its full
|
|
32
|
+
// source before it ever deletes anything.
|
|
33
|
+
import fs from 'node:fs';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import { fileURLToPath } from 'node:url';
|
|
36
|
+
import { digest, fileIdentity } from './coverage-integrity.mjs';
|
|
37
|
+
import { isPrivate, loadPrivateFence, loadPrivateSlugs, shouldFenceL2 } from './private-fence.mjs';
|
|
38
|
+
import { promoteArtifactSet } from '../kb/incremental-refresh.mjs';
|
|
39
|
+
|
|
40
|
+
const DEFAULT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
41
|
+
const HEX40 = /^[a-f0-9]{40}$/;
|
|
42
|
+
|
|
43
|
+
// The sealed selection receipt's filename -- exported so a caller can detect "did reconciliation
|
|
44
|
+
// already seal a public-input tree here?" WITHOUT re-deriving one to find out. This is exactly the
|
|
45
|
+
// signal build-bundle.mjs uses to decide between trusting an already-reconciled ASSETS directory
|
|
46
|
+
// byte-for-byte and materializing fresh in the genuine local/standalone case.
|
|
47
|
+
export const SELECTION_FILE = 'PUBLIC-INPUT-SELECTION.json';
|
|
48
|
+
export const SELECTION_RECEIPT_KIND = 'ruvnet-brain-public-input-selection-receipt';
|
|
49
|
+
// Schema 2 (Step 5 remediation, 2026-09-13): the receipt binds every included file's BYTES
|
|
50
|
+
// (`files[]` = relative path + sha256 + size), not just its NAME. Schema 1 sealed names only, so a
|
|
51
|
+
// consumer could verify that "alpha-primer.md" was selected while shipping any bytes at all under
|
|
52
|
+
// that name -- exactly the "checks exists, then trusts blindly" trap Dual's review named.
|
|
53
|
+
export const SELECTION_RECEIPT_SCHEMA = 2;
|
|
54
|
+
|
|
55
|
+
/** Digest+size of an excluded file, WITHOUT its content -- the private-exclusion evidence never
|
|
56
|
+
* carries the bytes it proves were excluded. */
|
|
57
|
+
function excludedIdentity(file) {
|
|
58
|
+
const { sha256, bytes } = fileIdentity(file);
|
|
59
|
+
return { sha256, bytes };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The ONE definition of "a managed public-prose entry" -- shared by the producer's positive-selection
|
|
63
|
+
// cleanup below and by validateSelectionReceipt's leak check, so the two can never disagree about
|
|
64
|
+
// which files this selection owns. Directory-level: `l2/` is owned wholesale.
|
|
65
|
+
const MANAGED_PRIMER = /^.+-primer\.md$/;
|
|
66
|
+
const MANAGED_TOPICS = /^l2-topics\..+\.json$/;
|
|
67
|
+
const MANAGED_STATIC = ['capability-cards.md', 'repo-aliases.json'];
|
|
68
|
+
const isManagedTopLevel = (name) => MANAGED_PRIMER.test(name) || MANAGED_TOPICS.test(name)
|
|
69
|
+
|| MANAGED_STATIC.includes(name) || name === 'l2' || name === SELECTION_FILE;
|
|
70
|
+
|
|
71
|
+
/** Every managed public-prose FILE currently on disk under `dir`, as relative POSIX paths (the
|
|
72
|
+
* receipt itself excluded). `l2/` is walked recursively: anything under it is owned by this
|
|
73
|
+
* selection, so a stray subdirectory there is a leak, not an exemption. */
|
|
74
|
+
export function managedPublicProseFiles(dir) {
|
|
75
|
+
const out = [];
|
|
76
|
+
if (!fs.existsSync(dir)) return out;
|
|
77
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
78
|
+
if (entry.name === SELECTION_FILE) continue;
|
|
79
|
+
if (entry.name === 'l2') {
|
|
80
|
+
const walk = (abs, rel) => {
|
|
81
|
+
for (const child of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
82
|
+
const childRel = `${rel}/${child.name}`;
|
|
83
|
+
if (child.isDirectory()) walk(path.join(abs, child.name), childRel);
|
|
84
|
+
else out.push(childRel);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
if (entry.isDirectory()) walk(path.join(dir, 'l2'), 'l2');
|
|
88
|
+
else out.push('l2');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (isManagedTopLevel(entry.name)) out.push(entry.name);
|
|
92
|
+
}
|
|
93
|
+
return out.sort();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Relative-path identity rows for every regular file under `dir` except the receipt itself. */
|
|
97
|
+
function sealedFileRows(dir) {
|
|
98
|
+
const rows = [];
|
|
99
|
+
const walk = (abs, rel) => {
|
|
100
|
+
for (const entry of fs.readdirSync(abs, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
101
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
102
|
+
if (childRel === SELECTION_FILE) continue;
|
|
103
|
+
if (entry.isDirectory()) walk(path.join(abs, entry.name), childRel);
|
|
104
|
+
else {
|
|
105
|
+
const { sha256, bytes } = fileIdentity(path.join(abs, entry.name));
|
|
106
|
+
rows.push({ path: childRel, sha256, bytes });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
walk(dir, '');
|
|
111
|
+
return rows;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* validateSelectionReceipt({ receipt, dir }) -> receipt
|
|
116
|
+
*
|
|
117
|
+
* The fail-closed READER for SELECTION_FILE, owned by the same module that writes it (the same
|
|
118
|
+
* producer-owns-validator discipline gist-receipts.mjs uses). Every consumer that wants to trust a
|
|
119
|
+
* sealed public-input tree calls this instead of checking that the file merely exists:
|
|
120
|
+
* - kind / schemaVersion are exactly this module's;
|
|
121
|
+
* - receiptSha256 is recomputed from the receipt's own content and must match;
|
|
122
|
+
* - every sealed file exists under `dir` as a regular file with the exact sha256 and byte count;
|
|
123
|
+
* - every `included` name is backed by a sealed file, and every sealed file is named by `included`;
|
|
124
|
+
* - no managed public-prose file exists on disk that the receipt does not seal (an extra managed
|
|
125
|
+
* file is a LEAK -- unfenced prose riding along under a "sealed" label).
|
|
126
|
+
*/
|
|
127
|
+
export function validateSelectionReceipt({ receipt, dir } = {}) {
|
|
128
|
+
const root = path.resolve(dir || '');
|
|
129
|
+
const fail = (message) => { throw new Error(`public-input selection receipt: ${message}`); };
|
|
130
|
+
if (!receipt || typeof receipt !== 'object') fail('missing or not an object');
|
|
131
|
+
if (receipt.kind !== SELECTION_RECEIPT_KIND) fail(`kind is ${JSON.stringify(receipt.kind)}, expected ${SELECTION_RECEIPT_KIND}`);
|
|
132
|
+
if (receipt.schemaVersion !== SELECTION_RECEIPT_SCHEMA) fail(`schemaVersion is ${JSON.stringify(receipt.schemaVersion)}, expected ${SELECTION_RECEIPT_SCHEMA}`);
|
|
133
|
+
const { receiptSha256, ...payload } = receipt;
|
|
134
|
+
if (typeof receiptSha256 !== 'string' || receiptSha256 !== digest(payload)) fail('receiptSha256 does not match the receipt content');
|
|
135
|
+
const included = receipt.included;
|
|
136
|
+
if (!included || !Array.isArray(receipt.files) || !Array.isArray(included.primers) || !Array.isArray(included.topics)
|
|
137
|
+
|| !Array.isArray(included.l2) || !Array.isArray(included.cards) || typeof included.aliases !== 'boolean') {
|
|
138
|
+
fail('included/files sections are malformed');
|
|
139
|
+
}
|
|
140
|
+
const sealed = new Map();
|
|
141
|
+
// FILESYSTEM CONTAINMENT (P2, Dual 2026-09-14): the previous check "validates slash-separated
|
|
142
|
+
// traversal and the final file only. It does not reject backslash traversal on Windows or symlink
|
|
143
|
+
// ancestors." Both are closed here: `\` is rejected outright as a path separator on any host, and
|
|
144
|
+
// every row's resolved PARENT must still be inside the real root, so a symlinked directory
|
|
145
|
+
// component cannot carry the read outside the sealed tree even though the final entry is a
|
|
146
|
+
// regular, non-symlink file.
|
|
147
|
+
let rootReal;
|
|
148
|
+
try { rootReal = fs.realpathSync(root); } catch { fail(`sealed directory ${root} does not exist`); }
|
|
149
|
+
const containedIn = (target) => target === rootReal || target.startsWith(rootReal + path.sep);
|
|
150
|
+
for (const row of receipt.files) {
|
|
151
|
+
if (typeof row?.path !== 'string' || !row.path || path.isAbsolute(row.path) || row.path.includes('\\')
|
|
152
|
+
|| row.path.split('/').some((segment) => segment === '..' || segment === '' || segment === '.')
|
|
153
|
+
|| !/^[a-f0-9]{64}$/.test(String(row.sha256 || '')) || !Number.isSafeInteger(row.bytes) || row.bytes < 0) {
|
|
154
|
+
fail(`sealed file row is malformed (${JSON.stringify(row?.path)})`);
|
|
155
|
+
}
|
|
156
|
+
if (sealed.has(row.path)) fail(`sealed file ${row.path} is listed twice`);
|
|
157
|
+
sealed.set(row.path, row);
|
|
158
|
+
const abs = path.join(root, row.path);
|
|
159
|
+
if (!fs.existsSync(abs)) fail(`sealed file ${row.path} is missing from ${root}`);
|
|
160
|
+
let parentReal;
|
|
161
|
+
try { parentReal = fs.realpathSync(path.dirname(abs)); } catch { fail(`sealed file ${row.path} has no resolvable parent directory`); }
|
|
162
|
+
if (!containedIn(parentReal)) fail(`sealed file ${row.path} resolves outside the sealed directory (${parentReal})`);
|
|
163
|
+
const stat = fs.lstatSync(abs);
|
|
164
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail(`sealed file ${row.path} is not a regular file`);
|
|
165
|
+
const { sha256, bytes } = fileIdentity(abs);
|
|
166
|
+
if (sha256 !== row.sha256 || bytes !== row.bytes) fail(`sealed file ${row.path} bytes differ from the receipt`);
|
|
167
|
+
}
|
|
168
|
+
const expected = new Set([
|
|
169
|
+
...included.primers.map((repo) => `${repo}-primer.md`),
|
|
170
|
+
...included.topics.map((repo) => `l2-topics.${repo}.json`),
|
|
171
|
+
...included.l2.map((slug) => `l2/${slug}.md`),
|
|
172
|
+
...(included.cards.length ? ['capability-cards.md'] : []),
|
|
173
|
+
...(included.aliases ? ['repo-aliases.json'] : []),
|
|
174
|
+
]);
|
|
175
|
+
for (const name of expected) if (!sealed.has(name)) fail(`included name ${name} has no sealed file row`);
|
|
176
|
+
// capability-cards.md may legitimately be sealed with zero card sections (header-only file);
|
|
177
|
+
// every OTHER sealed file must be accounted for by an included name.
|
|
178
|
+
for (const name of sealed.keys()) {
|
|
179
|
+
if (!expected.has(name) && name !== 'capability-cards.md') fail(`sealed file ${name} is not named by any included entry`);
|
|
180
|
+
}
|
|
181
|
+
const extras = managedPublicProseFiles(root).filter((name) => !sealed.has(name));
|
|
182
|
+
if (extras.length) fail(`managed public-prose file(s) on disk are not sealed by the receipt (unfenced leak): ${extras.join(', ')}`);
|
|
183
|
+
return receipt;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Legacy slug ownership that predates per-repo l2-topics.<repo>.json files. Treated exactly like an
|
|
187
|
+
// explicit claim by "ruflo" made before anyone else's -- a later repo declaring the SAME slug is
|
|
188
|
+
// still a genuine ownership conflict, not a silent override.
|
|
189
|
+
const SEED_OWNERSHIP = [
|
|
190
|
+
['guidance-mechanism', 'ruflo'],
|
|
191
|
+
['memory-end-to-end', 'ruflo'],
|
|
192
|
+
['adr-coverage', 'ruflo'],
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
function readTopics(kbDir, repo) {
|
|
196
|
+
const file = path.join(kbDir, `l2-topics.${repo}.json`);
|
|
197
|
+
if (!fs.existsSync(file)) return null;
|
|
198
|
+
let topics;
|
|
199
|
+
try {
|
|
200
|
+
topics = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
201
|
+
} catch (error) {
|
|
202
|
+
throw new Error(`materializePublicInputs: ${repo} topics file is corrupt (${error.message})`);
|
|
203
|
+
}
|
|
204
|
+
if (!Array.isArray(topics)) throw new Error(`materializePublicInputs: ${repo} topics file is malformed (expected an array)`);
|
|
205
|
+
return { file, topics };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolve slug -> owning-repo across every repo's topics file, and FAIL on any slug two different
|
|
210
|
+
* repos both claim (rule 4). Runs over EVERY discovered repo, public and private alike, because an
|
|
211
|
+
* ambiguous claim between a public and a private repo is exactly the kind of attribution bug the
|
|
212
|
+
* fence depends on getting right -- it must not be silently resolved by iteration order.
|
|
213
|
+
*/
|
|
214
|
+
export function resolveTopicOwnership(kbDir, repos) {
|
|
215
|
+
const ownership = new Map(SEED_OWNERSHIP);
|
|
216
|
+
for (const repo of [...repos].sort()) {
|
|
217
|
+
const loaded = readTopics(kbDir, repo);
|
|
218
|
+
if (!loaded) continue;
|
|
219
|
+
for (const topic of loaded.topics) {
|
|
220
|
+
const slug = topic?.slug;
|
|
221
|
+
if (!slug) continue;
|
|
222
|
+
const owner = ownership.get(slug);
|
|
223
|
+
if (owner !== undefined && owner !== repo) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`materializePublicInputs: conflicting topic ownership -- slug "${slug}" is claimed by both `
|
|
226
|
+
+ `"${owner}" and "${repo}"`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
ownership.set(slug, repo);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return ownership;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function sealSelectionReceipt({ builderSha, generatedAt, included, excluded, ownership, files }) {
|
|
236
|
+
const payload = {
|
|
237
|
+
schemaVersion: SELECTION_RECEIPT_SCHEMA,
|
|
238
|
+
kind: SELECTION_RECEIPT_KIND,
|
|
239
|
+
builderSha: builderSha || null,
|
|
240
|
+
generatedAt,
|
|
241
|
+
// Byte binding (schema 2): every file this selection actually wrote, by relative path.
|
|
242
|
+
files: [...files].sort((a, b) => a.path.localeCompare(b.path)),
|
|
243
|
+
included: {
|
|
244
|
+
primers: [...included.primers].sort(),
|
|
245
|
+
topics: [...included.topics].sort(),
|
|
246
|
+
l2: [...included.l2].sort(),
|
|
247
|
+
cards: [...included.cards].sort(),
|
|
248
|
+
aliases: included.aliases === true,
|
|
249
|
+
},
|
|
250
|
+
excluded: {
|
|
251
|
+
primers: [...excluded.primers].sort((a, b) => a.repo.localeCompare(b.repo)),
|
|
252
|
+
topics: [...excluded.topics].sort((a, b) => a.repo.localeCompare(b.repo)),
|
|
253
|
+
l2: [...excluded.l2].sort((a, b) => a.slug.localeCompare(b.slug)),
|
|
254
|
+
cards: [...excluded.cards].sort((a, b) => a.repo.localeCompare(b.repo)),
|
|
255
|
+
},
|
|
256
|
+
ownership: Object.fromEntries([...ownership.entries()].sort(([a], [b]) => a.localeCompare(b))),
|
|
257
|
+
};
|
|
258
|
+
return { ...payload, receiptSha256: digest(payload) };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* materializePublicInputs({ builderRoot, policy, outDir, builderSha, now }) -> PublicInputSet
|
|
263
|
+
*
|
|
264
|
+
* Builds a FRESH public-prose tree under `outDir`: capability-cards.md (card-filtered exactly
|
|
265
|
+
* once, here), repo-aliases.json, every non-private `<repo>-primer.md`, every non-private
|
|
266
|
+
* `l2-topics.<repo>.json`, and kb/l2/*.md minus anything shouldFenceL2 excludes -- using
|
|
267
|
+
* scripts/private-fence.mjs's own loadPrivateFence/loadPrivateSlugs/isPrivate/shouldFenceL2
|
|
268
|
+
* directly, never a re-derived copy of that logic.
|
|
269
|
+
*
|
|
270
|
+
* `policy.allowNoFence` is the same documented ALLOW_NO_PRIVATE_FENCE escape hatch private-fence.mjs
|
|
271
|
+
* already defines -- passed straight through.
|
|
272
|
+
*/
|
|
273
|
+
export function materializePublicInputs({ builderRoot = DEFAULT_ROOT, policy = {}, outDir,
|
|
274
|
+
builderSha = null, now = () => new Date().toISOString() } = {}) {
|
|
275
|
+
const kb = path.join(path.resolve(builderRoot), 'kb');
|
|
276
|
+
const out = path.resolve(outDir || '');
|
|
277
|
+
if (!fs.existsSync(kb) || !fs.statSync(kb).isDirectory()) {
|
|
278
|
+
throw new Error(`materializePublicInputs: builder kb directory is missing (${kb})`);
|
|
279
|
+
}
|
|
280
|
+
if (builderSha !== null && !HEX40.test(String(builderSha))) {
|
|
281
|
+
throw new Error('materializePublicInputs: builderSha must be an exact 40-character commit sha, or null');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const fence = loadPrivateFence(kb, { allowNoFence: policy.allowNoFence === true });
|
|
285
|
+
if (!fence.ok) throw new Error(`materializePublicInputs: ${fence.reason}`);
|
|
286
|
+
const privateSet = fence.privateSet;
|
|
287
|
+
const slugFence = loadPrivateSlugs(kb, privateSet);
|
|
288
|
+
if (!slugFence.ok) throw new Error(`materializePublicInputs: ${slugFence.reason}`);
|
|
289
|
+
const privateSlugs = slugFence.slugs;
|
|
290
|
+
|
|
291
|
+
const allRepos = fs.readdirSync(kb).filter((f) => f.endsWith('-primer.md'))
|
|
292
|
+
.map((f) => f.slice(0, -'-primer.md'.length)).sort();
|
|
293
|
+
const ownership = resolveTopicOwnership(kb, allRepos);
|
|
294
|
+
|
|
295
|
+
fs.mkdirSync(out, { recursive: true });
|
|
296
|
+
const stage = fs.mkdtempSync(path.join(path.dirname(out), '.public-inputs-'));
|
|
297
|
+
const included = { primers: [], topics: [], l2: [], cards: [], aliases: false };
|
|
298
|
+
const excluded = { primers: [], topics: [], l2: [], cards: [] };
|
|
299
|
+
try {
|
|
300
|
+
for (const repo of allRepos) {
|
|
301
|
+
const src = path.join(kb, `${repo}-primer.md`);
|
|
302
|
+
if (isPrivate(privateSet, repo)) {
|
|
303
|
+
excluded.primers.push({ repo, ...excludedIdentity(src) });
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
fs.copyFileSync(src, path.join(stage, `${repo}-primer.md`));
|
|
307
|
+
included.primers.push(repo);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const repo of allRepos) {
|
|
311
|
+
const file = path.join(kb, `l2-topics.${repo}.json`);
|
|
312
|
+
if (!fs.existsSync(file)) continue;
|
|
313
|
+
if (isPrivate(privateSet, repo)) {
|
|
314
|
+
excluded.topics.push({ repo, ...excludedIdentity(file) });
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
fs.copyFileSync(file, path.join(stage, `l2-topics.${repo}.json`));
|
|
318
|
+
included.topics.push(repo);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fs.mkdirSync(path.join(stage, 'l2'), { recursive: true });
|
|
322
|
+
const l2Src = path.join(kb, 'l2');
|
|
323
|
+
if (fs.existsSync(l2Src)) {
|
|
324
|
+
for (const name of fs.readdirSync(l2Src).filter((f) => f.endsWith('.md')).sort()) {
|
|
325
|
+
const slug = name.slice(0, -3);
|
|
326
|
+
const repo = ownership.get(slug) || 'ruvnet';
|
|
327
|
+
const src = path.join(l2Src, name);
|
|
328
|
+
if (shouldFenceL2({ repo, slug }, privateSet, privateSlugs)) {
|
|
329
|
+
excluded.l2.push({ slug, repo, ...excludedIdentity(src) });
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
fs.copyFileSync(src, path.join(stage, 'l2', name));
|
|
333
|
+
included.l2.push(slug);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const cardsSrc = path.join(kb, 'capability-cards.md');
|
|
338
|
+
if (fs.existsSync(cardsSrc)) {
|
|
339
|
+
const raw = fs.readFileSync(cardsSrc, 'utf8');
|
|
340
|
+
const parts = raw.split(/^##\s+/m);
|
|
341
|
+
const kept = [];
|
|
342
|
+
for (const section of parts.slice(1)) {
|
|
343
|
+
const newline = section.indexOf('\n');
|
|
344
|
+
const repo = newline < 0 ? '' : section.slice(0, newline).trim();
|
|
345
|
+
const rendered = `## ${section}`;
|
|
346
|
+
if (repo && isPrivate(privateSet, repo)) {
|
|
347
|
+
excluded.cards.push({ repo, sha256: digest(rendered), bytes: Buffer.byteLength(rendered, 'utf8') });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (repo) included.cards.push(repo);
|
|
351
|
+
kept.push(rendered);
|
|
352
|
+
}
|
|
353
|
+
fs.writeFileSync(path.join(stage, 'capability-cards.md'), parts[0] + kept.join(''));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const aliasesSrc = path.join(kb, 'repo-aliases.json');
|
|
357
|
+
if (fs.existsSync(aliasesSrc)) {
|
|
358
|
+
fs.copyFileSync(aliasesSrc, path.join(stage, 'repo-aliases.json'));
|
|
359
|
+
included.aliases = true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Seal the BYTES actually staged (schema 2), never a claim about them: identities are read back
|
|
363
|
+
// from the stage directory after every file has been written.
|
|
364
|
+
const selectionReceipt = sealSelectionReceipt({
|
|
365
|
+
builderSha, generatedAt: now(), included, excluded, ownership, files: sealedFileRows(stage),
|
|
366
|
+
});
|
|
367
|
+
fs.writeFileSync(path.join(stage, SELECTION_FILE), `${JSON.stringify(selectionReceipt, null, 2)}\n`);
|
|
368
|
+
|
|
369
|
+
// Positive selection (rule 3): remove every previously-managed entry that this round did NOT
|
|
370
|
+
// reproduce -- a primer/topics file whose source disappeared or went private simply never lands
|
|
371
|
+
// in `stage`, and is removed here rather than left behind by an overlay. `stage` is fully built
|
|
372
|
+
// (every file read from `kb` already) before anything under `out` is touched. "Managed" is the
|
|
373
|
+
// SAME predicate validateSelectionReceipt uses for its leak check (isManagedTopLevel).
|
|
374
|
+
const stageEntries = new Set(fs.readdirSync(stage));
|
|
375
|
+
const stale = fs.existsSync(out)
|
|
376
|
+
? fs.readdirSync(out).filter((name) => isManagedTopLevel(name) && !stageEntries.has(name))
|
|
377
|
+
: [];
|
|
378
|
+
for (const name of stale) fs.rmSync(path.join(out, name), { recursive: true, force: true });
|
|
379
|
+
promoteArtifactSet({ liveDir: out, candidateDir: stage, files: [...stageEntries] });
|
|
380
|
+
} finally {
|
|
381
|
+
fs.rmSync(stage, { recursive: true, force: true });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const files = [];
|
|
385
|
+
for (const repo of included.primers) files.push({ name: `${repo}-primer.md`, ...fileIdentity(path.join(out, `${repo}-primer.md`)) });
|
|
386
|
+
for (const repo of included.topics) files.push({ name: `l2-topics.${repo}.json`, ...fileIdentity(path.join(out, `l2-topics.${repo}.json`)) });
|
|
387
|
+
if (fs.existsSync(path.join(out, 'capability-cards.md'))) files.push({ name: 'capability-cards.md', ...fileIdentity(path.join(out, 'capability-cards.md')) });
|
|
388
|
+
if (included.aliases) files.push({ name: 'repo-aliases.json', ...fileIdentity(path.join(out, 'repo-aliases.json')) });
|
|
389
|
+
|
|
390
|
+
// The producer READS BACK its own output through the same fail-closed validator every consumer
|
|
391
|
+
// uses (P2, Dual 2026-09-14: "scripts/public-inputs.mjs:385 reads its output without calling
|
|
392
|
+
// validateSelectionReceipt"). The stage is built, promoted, and only then proven — so a promotion
|
|
393
|
+
// that landed different bytes, dropped a file, or left an unsealed managed file behind is caught
|
|
394
|
+
// HERE, by the producer, instead of becoming a consumer's problem one call later.
|
|
395
|
+
const promotedReceipt = validateSelectionReceipt({
|
|
396
|
+
receipt: JSON.parse(fs.readFileSync(path.join(out, SELECTION_FILE), 'utf8')), dir: out,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
kind: 'public-input-set',
|
|
401
|
+
dir: out,
|
|
402
|
+
publicRepos: allRepos.filter((repo) => !isPrivate(privateSet, repo)),
|
|
403
|
+
ownership: Object.fromEntries(ownership),
|
|
404
|
+
included,
|
|
405
|
+
excluded,
|
|
406
|
+
files,
|
|
407
|
+
selectionReceipt: promotedReceipt,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
@@ -10,6 +10,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import { extractZip } from '../kb/zip-extract.mjs';
|
|
11
11
|
import { canonicalJson, digest, validateCoverageLedger, validateCoverageLink } from './coverage-integrity.mjs';
|
|
12
12
|
import { validatePublicInventory } from './public-inventory.mjs';
|
|
13
|
+
import { verifySeedBaseline } from './corpus-candidate.mjs';
|
|
13
14
|
import {
|
|
14
15
|
buildRetrievalCanaryPlan,
|
|
15
16
|
validateRetrievalQueryEvidence,
|
|
@@ -305,6 +306,48 @@ function observedBaselineFromTree({ extractedRoot, bundleFile, expectedTag, expe
|
|
|
305
306
|
return { receipt, bytes, fileSha256: crypto.createHash('sha256').update(bytes).digest('hex'), root, archiveManifest };
|
|
306
307
|
}
|
|
307
308
|
|
|
309
|
+
// The external, content-addressed seed tag (corpus-sha256-<digest>) and the archive's own internal
|
|
310
|
+
// ARCHIVE-MANIFEST release tag/version are two independent identity domains. Unlike the legacy
|
|
311
|
+
// retrospective/observed baseline readers above — which predate the schema-3 candidate receipt and
|
|
312
|
+
// compare a seed's tag directly against its internal releaseTag — this reader never makes that
|
|
313
|
+
// comparison: it verifies the seed purely through its schema-3 candidate receipt.
|
|
314
|
+
async function receiptedBaselineFromSeed({ seedDescriptor, bundleFile, receiptFile }) {
|
|
315
|
+
const verified = await verifySeedBaseline({ seedDescriptor, bundleFile, receiptFile });
|
|
316
|
+
const payload = {
|
|
317
|
+
schemaVersion: 1,
|
|
318
|
+
kind: 'ruvnet-brain-receipted-public-baseline',
|
|
319
|
+
verificationMode: 'candidate-receipt-verification',
|
|
320
|
+
historicalCorpusReceipt: true,
|
|
321
|
+
tag: verified.tag,
|
|
322
|
+
archive: { sha256: verified.sha256, bytes: verified.bytes },
|
|
323
|
+
archiveManifestSha256: verified.receipt.archiveManifest.sha256,
|
|
324
|
+
internalArchiveVersion: verified.receipt.archiveManifestVersion,
|
|
325
|
+
internalArchiveReleaseTag: verified.receipt.archiveManifestReleaseTag,
|
|
326
|
+
builderSourceSha: verified.receipt.builderSourceSha,
|
|
327
|
+
candidateReceiptSha256: sha256File(path.resolve(receiptFile)),
|
|
328
|
+
storeCount: verified.receipt.storeCount,
|
|
329
|
+
storeSetSha256: digest(verified.receipt.stores.map(({ name }) => name)),
|
|
330
|
+
stores: verified.receipt.stores.map(({ name, kind, sourceCommit, builtUtc, model, dimensions }) =>
|
|
331
|
+
({ name, kind, sourceCommit, builtUtc, model, dimensions })),
|
|
332
|
+
limitations: [
|
|
333
|
+
"the external content-addressed seed tag is an independent identity from the archive's "
|
|
334
|
+
+ 'internal ARCHIVE-MANIFEST release tag/version and is never compared against it',
|
|
335
|
+
],
|
|
336
|
+
};
|
|
337
|
+
const receipt = { ...payload, receiptSha256: digest(payload) };
|
|
338
|
+
const bytes = encoded(receipt);
|
|
339
|
+
return { receipt, bytes, fileSha256: crypto.createHash('sha256').update(bytes).digest('hex') };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function createReceiptedBaselineVerification({ seedDescriptor, baselineBundle, baselineReceipt,
|
|
343
|
+
outFile = 'release-evidence/baseline-verification-receipt.json' } = {}) {
|
|
344
|
+
const archive = trustedFile(baselineBundle, 'baseline archive');
|
|
345
|
+
const receiptFile = trustedFile(baselineReceipt, 'baseline candidate receipt');
|
|
346
|
+
const result = await receiptedBaselineFromSeed({ seedDescriptor, bundleFile: archive, receiptFile });
|
|
347
|
+
writeExactFile(outFile, result.bytes, 'baseline-verification-receipt.json');
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
308
351
|
function writeExactFile(file, bytes, label) {
|
|
309
352
|
const output = path.resolve(file);
|
|
310
353
|
if (fs.existsSync(output)) {
|
|
@@ -365,7 +408,8 @@ function writeExactOutputs(outDir, outputs) {
|
|
|
365
408
|
}
|
|
366
409
|
|
|
367
410
|
export async function createPublicVerificationInputs({ baselineBundle, candidateBundle,
|
|
368
|
-
candidatePackage, oracleFile, repo = process.cwd(), outDir = 'release-evidence', baselineMode = 'verified'
|
|
411
|
+
candidatePackage, oracleFile, repo = process.cwd(), outDir = 'release-evidence', baselineMode = 'verified',
|
|
412
|
+
baselineReceipt = null } = {}) {
|
|
369
413
|
const baselineArchive = trustedFile(baselineBundle, 'baseline archive');
|
|
370
414
|
const candidateArchive = trustedFile(candidateBundle, 'candidate archive');
|
|
371
415
|
const packageFile = trustedFile(candidatePackage, 'candidate package');
|
|
@@ -384,32 +428,62 @@ export async function createPublicVerificationInputs({ baselineBundle, candidate
|
|
|
384
428
|
const candidateTree = validateArchive(candidateExtracted);
|
|
385
429
|
const candidateResult = validateCandidate({ root: candidateTree.root, bundleFile: candidateArchive, packageFile });
|
|
386
430
|
const seed = candidateResult.coverage.corpusSeed;
|
|
387
|
-
if (!['verified', 'observed'].includes(baselineMode))
|
|
388
|
-
|
|
389
|
-
? observedBaselineFromTree({ extractedRoot: baselineExtracted, bundleFile: baselineArchive,
|
|
390
|
-
expectedTag: seed.tag, expectedSha256: seed.archiveSha256, expectedBytes: seed.archiveBytes })
|
|
391
|
-
: retrospectiveBaselineFromTree({ extractedRoot: baselineExtracted, bundleFile: baselineArchive,
|
|
392
|
-
expectedTag: seed.tag, expectedSha256: seed.archiveSha256, expectedBytes: seed.archiveBytes });
|
|
393
|
-
if (seed.archiveSha256 !== baselineProof.receipt.archive.sha256
|
|
394
|
-
|| seed.archiveBytes !== baselineProof.receipt.archive.bytes) {
|
|
395
|
-
fail('baseline archive bytes differ from release coverage');
|
|
431
|
+
if (!['verified', 'observed', 'receipted'].includes(baselineMode)) {
|
|
432
|
+
fail('baseline mode must be verified, observed, or receipted');
|
|
396
433
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
434
|
+
let baselineProof;
|
|
435
|
+
let baseline;
|
|
436
|
+
if (baselineMode === 'receipted') {
|
|
437
|
+
// The new seed-type path (task 3): route through verifySeedBaseline's schema-2 candidate
|
|
438
|
+
// receipt instead of the legacy retrospective/observed historical-baseline readers, which
|
|
439
|
+
// remain untouched below for genuinely historical (receipt-less) seeds only.
|
|
440
|
+
if (!baselineReceipt) fail('receipted baseline mode requires the seed candidate receipt file');
|
|
441
|
+
baselineProof = await receiptedBaselineFromSeed({
|
|
442
|
+
seedDescriptor: { tag: seed.tag, sha256: seed.archiveSha256, bytes: seed.archiveBytes, allowPinnedTag: true },
|
|
443
|
+
bundleFile: baselineArchive,
|
|
444
|
+
receiptFile: trustedFile(baselineReceipt, 'baseline candidate receipt'),
|
|
445
|
+
});
|
|
446
|
+
if (seed.archiveSha256 !== baselineProof.receipt.archive.sha256
|
|
447
|
+
|| seed.archiveBytes !== baselineProof.receipt.archive.bytes) {
|
|
448
|
+
fail('baseline archive bytes differ from release coverage');
|
|
449
|
+
}
|
|
450
|
+
if (seed.receiptSha256 !== baselineProof.fileSha256) fail('baseline receipt differs from release coverage');
|
|
451
|
+
// NEVER: seed.tag !== baselineProof.receipt.internalArchiveReleaseTag — the external
|
|
452
|
+
// content-addressed tag and the archive's internal release tag are independent identities.
|
|
453
|
+
const baselineStores = baselineProof.receipt.stores.map(({ name }) => name);
|
|
454
|
+
baseline = { schemaVersion: 1, kind: 'ruvnet-brain-receipted-public-baseline', tag: seed.tag,
|
|
455
|
+
archiveSha256: seed.archiveSha256, archiveBytes: seed.archiveBytes,
|
|
456
|
+
archiveManifestSha256: baselineProof.receipt.archiveManifestSha256,
|
|
457
|
+
candidateReceiptSha256: baselineProof.receipt.candidateReceiptSha256,
|
|
458
|
+
verificationReceiptSha256: baselineProof.fileSha256,
|
|
459
|
+
stores: baselineStores, storeCount: baselineStores.length };
|
|
460
|
+
} else {
|
|
461
|
+
baselineProof = baselineMode === 'observed'
|
|
462
|
+
? observedBaselineFromTree({ extractedRoot: baselineExtracted, bundleFile: baselineArchive,
|
|
463
|
+
expectedTag: seed.tag, expectedSha256: seed.archiveSha256, expectedBytes: seed.archiveBytes })
|
|
464
|
+
: retrospectiveBaselineFromTree({ extractedRoot: baselineExtracted, bundleFile: baselineArchive,
|
|
465
|
+
expectedTag: seed.tag, expectedSha256: seed.archiveSha256, expectedBytes: seed.archiveBytes });
|
|
466
|
+
if (seed.archiveSha256 !== baselineProof.receipt.archive.sha256
|
|
467
|
+
|| seed.archiveBytes !== baselineProof.receipt.archive.bytes) {
|
|
468
|
+
fail('baseline archive bytes differ from release coverage');
|
|
469
|
+
}
|
|
470
|
+
if (seed.receiptSha256 !== baselineProof.fileSha256) fail('baseline receipt differs from release coverage');
|
|
471
|
+
if (seed.tag !== baselineProof.receipt.releaseTag) {
|
|
472
|
+
fail('baseline release tag differs from release coverage');
|
|
473
|
+
}
|
|
474
|
+
const baselineStores = baselineProof.receipt.stores.map(({ name }) => name);
|
|
475
|
+
baseline = { schemaVersion: 1,
|
|
476
|
+
kind: baselineMode === 'observed' ? 'ruvnet-brain-observed-failed-public-baseline' : 'ruvnet-brain-verified-public-baseline',
|
|
477
|
+
...(baselineMode === 'observed' ? { integrity: 'DEGRADED', historicalCorpusReceipt: false,
|
|
478
|
+
candidateVerificationEligible: false, observationReceiptSha256: baselineProof.fileSha256,
|
|
479
|
+
discrepancyDigest: baselineProof.receipt.discrepancyDigest,
|
|
480
|
+
ledgerDiscrepancies: baselineProof.receipt.ledgerDiscrepancies,
|
|
481
|
+
provenanceGaps: baselineProof.receipt.provenanceGaps } : {}), tag: seed.tag,
|
|
482
|
+
archiveSha256: seed.archiveSha256, archiveBytes: seed.archiveBytes,
|
|
483
|
+
archiveManifestSha256: baselineProof.receipt.archiveManifestSha256,
|
|
484
|
+
...(baselineMode === 'verified' ? { verificationReceiptSha256: baselineProof.fileSha256 } : {}),
|
|
485
|
+
stores: baselineStores, storeCount: baselineStores.length };
|
|
400
486
|
}
|
|
401
|
-
const baselineStores = baselineProof.receipt.stores.map(({ name }) => name);
|
|
402
|
-
const baseline = { schemaVersion: 1,
|
|
403
|
-
kind: baselineMode === 'observed' ? 'ruvnet-brain-observed-failed-public-baseline' : 'ruvnet-brain-verified-public-baseline',
|
|
404
|
-
...(baselineMode === 'observed' ? { integrity: 'DEGRADED', historicalCorpusReceipt: false,
|
|
405
|
-
candidateVerificationEligible: false, observationReceiptSha256: baselineProof.fileSha256,
|
|
406
|
-
discrepancyDigest: baselineProof.receipt.discrepancyDigest,
|
|
407
|
-
ledgerDiscrepancies: baselineProof.receipt.ledgerDiscrepancies,
|
|
408
|
-
provenanceGaps: baselineProof.receipt.provenanceGaps } : {}), tag: seed.tag,
|
|
409
|
-
archiveSha256: seed.archiveSha256, archiveBytes: seed.archiveBytes,
|
|
410
|
-
archiveManifestSha256: baselineProof.receipt.archiveManifestSha256,
|
|
411
|
-
...(baselineMode === 'verified' ? { verificationReceiptSha256: baselineProof.fileSha256 } : {}),
|
|
412
|
-
stores: baselineStores, storeCount: baselineStores.length };
|
|
413
487
|
const { value: queryEvidence } = readJson(oraclePath, 'independent strict-ancestor query oracle');
|
|
414
488
|
validateRetrievalQueryEvidence(queryEvidence);
|
|
415
489
|
verifyQueryOracleSource(queryEvidence, candidateResult.candidate.sourceSha, {
|
|
@@ -456,6 +530,17 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
456
530
|
provenanceGaps: result.receipt.provenanceGaps.length, receiptFileSha256: result.fileSha256 }));
|
|
457
531
|
return result;
|
|
458
532
|
}
|
|
533
|
+
if (argv[0] === 'receipted-baseline') {
|
|
534
|
+
const result = await createReceiptedBaselineVerification({
|
|
535
|
+
seedDescriptor: { tag: arg(argv, '--expected-tag'), sha256: arg(argv, '--expected-sha256'),
|
|
536
|
+
bytes: Number(arg(argv, '--expected-bytes')), allowPinnedTag: argv.includes('--allow-pinned-tag') },
|
|
537
|
+
baselineBundle: arg(argv, '--baseline-bundle'),
|
|
538
|
+
baselineReceipt: arg(argv, '--baseline-receipt'),
|
|
539
|
+
outFile: arg(argv, '--out') || 'release-evidence/baseline-verification-receipt.json' });
|
|
540
|
+
console.log(JSON.stringify({ ok: true, mode: 'receipted-baseline', tag: result.receipt.tag,
|
|
541
|
+
stores: result.receipt.storeCount, receiptFileSha256: result.fileSha256 }));
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
459
544
|
const result = await createPublicVerificationInputs({
|
|
460
545
|
baselineBundle: arg(argv, '--baseline-bundle'),
|
|
461
546
|
candidateBundle: arg(argv, '--candidate-bundle'),
|
|
@@ -463,7 +548,8 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
463
548
|
oracleFile: arg(argv, '--oracle'),
|
|
464
549
|
repo: arg(argv, '--repo') || process.cwd(),
|
|
465
550
|
outDir: arg(argv, '--out-dir') || 'release-evidence',
|
|
466
|
-
baselineMode: argv.includes('--observed-baseline') ? 'observed' : 'verified',
|
|
551
|
+
baselineMode: argv.includes('--receipted-baseline') ? 'receipted' : argv.includes('--observed-baseline') ? 'observed' : 'verified',
|
|
552
|
+
baselineReceipt: arg(argv, '--baseline-receipt'),
|
|
467
553
|
});
|
|
468
554
|
console.log(JSON.stringify({ ok: true, sourceSha: result.candidate.sourceSha,
|
|
469
555
|
coverageGeneration: result.coverage.releaseCoverageGeneration, cases: result.plan.cases.length }));
|
|
@@ -353,4 +353,4 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
355
|
|
|
356
|
-
if (
|
|
356
|
+
if (((() => { try { return process.argv[1] && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)); } catch { return false; } })())) process.exitCode = await main();
|