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.
Files changed (143) hide show
  1. package/README.md +5 -5
  2. package/bin/install.mjs +383 -78
  3. package/console/app.js +141 -9
  4. package/console/index.html +51 -24
  5. package/console/scope.css +137 -0
  6. package/console/scope.html +144 -0
  7. package/console/scope.js +209 -0
  8. package/console/tips.html +1 -0
  9. package/kb/corpus-release-identity.mjs +239 -0
  10. package/kb/update-storage-transaction.mjs +20 -3
  11. package/package.json +9 -2
  12. package/plugin/.claude-plugin/plugin.json +2 -2
  13. package/plugin/.codex-plugin/plugin.json +1 -1
  14. package/plugin/commands/checkpoint.md +61 -0
  15. package/plugin/hooks/codex-hooks.json +64 -1
  16. package/plugin/hooks/hook-contracts.json +299 -6
  17. package/plugin/hooks/hooks.json +81 -1
  18. package/plugin/mcp/server.mjs +23 -0
  19. package/plugin/scripts/advocacy-catalog.mjs +245 -0
  20. package/plugin/scripts/advocacy-route.mjs +460 -0
  21. package/plugin/scripts/continuation-gate.mjs +25 -2
  22. package/plugin/scripts/continuation-objective.mjs +7 -1
  23. package/plugin/scripts/continuity-hook-policy.mjs +190 -15
  24. package/plugin/scripts/coverage-integrity.mjs +7 -0
  25. package/plugin/scripts/gates.mjs +113 -10
  26. package/plugin/scripts/grounding-turn-gate.mjs +167 -0
  27. package/plugin/scripts/grounding-turn-mark.mjs +91 -0
  28. package/plugin/scripts/hook-shim.mjs +14 -0
  29. package/plugin/scripts/host-shell-boundary.mjs +43 -0
  30. package/plugin/scripts/nightly-scheduler.mjs +37 -4
  31. package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
  32. package/plugin/scripts/project-progression-contract.mjs +16 -0
  33. package/plugin/scripts/project-progression-hook.mjs +3 -0
  34. package/plugin/scripts/project-progression-producer.mjs +252 -0
  35. package/plugin/scripts/project-progression-reader.mjs +271 -0
  36. package/plugin/scripts/project-progression-session-start.mjs +93 -16
  37. package/plugin/scripts/project-progression-sources.mjs +220 -0
  38. package/plugin/scripts/project-progression-store.mjs +106 -13
  39. package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
  40. package/plugin/scripts/session-snapshot-hook.mjs +115 -7
  41. package/plugin/scripts/session-start-budget.mjs +59 -0
  42. package/plugin/scripts/session-start-core.mjs +234 -457
  43. package/plugin/scripts/session-start-fsutil.mjs +61 -0
  44. package/plugin/scripts/session-start-health.mjs +64 -0
  45. package/plugin/scripts/session-start-hook-description.mjs +45 -0
  46. package/plugin/scripts/session-start-issue-alert.mjs +77 -0
  47. package/plugin/scripts/session-start-repo-identity.mjs +54 -0
  48. package/plugin/scripts/session-start-signals.mjs +73 -0
  49. package/plugin/scripts/session-start-trace.mjs +86 -0
  50. package/plugin/scripts/session-start-update-plane.mjs +104 -0
  51. package/plugin/scripts/unprompted-runtime.mjs +32 -2
  52. package/plugin/scripts/update-apply.mjs +2 -32
  53. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
  54. package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
  55. package/scripts/adr-072-completion.mjs +1 -1
  56. package/scripts/agentdb-fleet-doctor.mjs +5 -1
  57. package/scripts/approved-runtime.mjs +197 -0
  58. package/scripts/brain-novice-50.mjs +16 -1
  59. package/scripts/brain-score.mjs +23 -5
  60. package/scripts/build-bundle.mjs +971 -530
  61. package/scripts/build-concepts.mjs +36 -116
  62. package/scripts/console-engine.test.mjs +8 -7
  63. package/scripts/console-runtime-identity.mjs +4 -0
  64. package/scripts/corpus-aggregates.mjs +94 -77
  65. package/scripts/corpus-candidate.mjs +475 -222
  66. package/scripts/corpus-next-seed.mjs +225 -0
  67. package/scripts/corpus-promotion.mjs +58 -0
  68. package/scripts/corpus-reconcile.mjs +411 -105
  69. package/scripts/doc-currency.mjs +16 -1
  70. package/scripts/dual-host-deliberation.mjs +25 -2
  71. package/scripts/dual-host-suggest.mjs +17 -1
  72. package/scripts/falsify.mjs +13 -3
  73. package/scripts/gist-receipts.mjs +482 -87
  74. package/scripts/github-health-watch.mjs +12 -2
  75. package/scripts/handoff-asset.mjs +34 -0
  76. package/scripts/hook-retirement-check.mjs +8 -1
  77. package/scripts/host-registry.mjs +1 -1
  78. package/scripts/ingest-gists.mjs +74 -101
  79. package/scripts/job-heartbeat.sh +77 -14
  80. package/scripts/learning-replay-execution.mjs +10 -4
  81. package/scripts/nightly-gists.sh +27 -13
  82. package/scripts/nightly-two-run-proof.mjs +1 -1
  83. package/scripts/nightly-watchdog.mjs +61 -4
  84. package/scripts/onboarding-console.mjs +319 -27
  85. package/scripts/oracle/produce-questions.mjs +293 -0
  86. package/scripts/oracle/producer-hosts.mjs +235 -0
  87. package/scripts/oracle/repo-recall.mjs +448 -0
  88. package/scripts/oracle/retrieval-accuracy.mjs +818 -0
  89. package/scripts/oracle/source-tree.mjs +165 -0
  90. package/scripts/oracle/source-units.mjs +391 -0
  91. package/scripts/oracle/spike-run.mjs +98 -0
  92. package/scripts/oracle/unit-inventory.mjs +141 -0
  93. package/scripts/oracle/unit-sampling.mjs +128 -0
  94. package/scripts/oracle/validate-labels.mjs +250 -0
  95. package/scripts/private-overlay.mjs +248 -0
  96. package/scripts/product-integrity-contract.mjs +1 -1
  97. package/scripts/proxy/claude-proxied.sh +6 -0
  98. package/scripts/proxy/proxy-revert.sh +5 -0
  99. package/scripts/proxy/proxy-up.sh +6 -0
  100. package/scripts/proxy/proxy-verify.mjs +4 -0
  101. package/scripts/public-inputs.mjs +409 -0
  102. package/scripts/public-verification-inputs.mjs +112 -26
  103. package/scripts/public-verification-lane.mjs +1 -1
  104. package/scripts/published-surface-probe.mjs +34 -4
  105. package/scripts/qe/card-lane-gate.mjs +16 -1
  106. package/scripts/qe/session-start-gate.mjs +16 -1
  107. package/scripts/rebuild-gists-from-receipts.mjs +58 -78
  108. package/scripts/record-lesson.mjs +4 -1
  109. package/scripts/rehearse-corpus-pipeline.mjs +994 -0
  110. package/scripts/release-abort-stale.mjs +5 -1
  111. package/scripts/release-authority.mjs +104 -12
  112. package/scripts/release-channel-kind.mjs +86 -0
  113. package/scripts/release-convergence-watchdog.mjs +7 -2
  114. package/scripts/release-projection.mjs +177 -72
  115. package/scripts/release-transaction-provider.mjs +23 -6
  116. package/scripts/release.mjs +252 -17
  117. package/scripts/retrieval-canary.mjs +87 -0
  118. package/scripts/rvf-index-audit.mjs +573 -13
  119. package/scripts/rvf-wire.mjs +269 -0
  120. package/scripts/seal-gist-receipt.mjs +65 -0
  121. package/scripts/selfcheck.mjs +42 -21
  122. package/scripts/source-coverage.mjs +253 -24
  123. package/scripts/status-honesty.mjs +25 -0
  124. package/scripts/sync-census.mjs +0 -0
  125. package/scripts/sync-version.mjs +2 -0
  126. package/scripts/trismart.mjs +42 -0
  127. package/scripts/updater-manifest.mjs +162 -0
  128. package/scripts/verify-channels.mjs +17 -5
  129. package/scripts/wired-check.mjs +48 -10
  130. package/tri-smart-skill/QUICKSTART.md +37 -0
  131. package/tri-smart-skill/README.md +92 -0
  132. package/tri-smart-skill/install.cmd +14 -0
  133. package/tri-smart-skill/install.command +13 -0
  134. package/tri-smart-skill/install.mjs +51 -0
  135. package/tri-smart-skill/install.sh +9 -0
  136. package/tri-smart-skill/tri-smart/SKILL.md +90 -0
  137. package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
  138. package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
  139. package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
  140. package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
  141. package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
  142. package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
  143. 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)) fail('baseline mode must be verified or observed');
388
- const baselineProof = baselineMode === 'observed'
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
- if (seed.receiptSha256 !== baselineProof.fileSha256) fail('baseline receipt differs from release coverage');
398
- if (seed.tag !== baselineProof.receipt.releaseTag) {
399
- fail('baseline release tag differs from release coverage');
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 (path.resolve(process.argv[1] || '') === fileURLToPath(import.meta.url)) process.exitCode = await main();
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();