ruvnet-brain 4.3.21 → 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 (141) hide show
  1. package/README.md +5 -5
  2. package/bin/install.mjs +275 -60
  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/nightly-scheduler.mjs +37 -4
  30. package/plugin/scripts/project-progression-checkpoint.mjs +145 -0
  31. package/plugin/scripts/project-progression-contract.mjs +16 -0
  32. package/plugin/scripts/project-progression-hook.mjs +3 -0
  33. package/plugin/scripts/project-progression-producer.mjs +252 -0
  34. package/plugin/scripts/project-progression-reader.mjs +271 -0
  35. package/plugin/scripts/project-progression-session-start.mjs +93 -16
  36. package/plugin/scripts/project-progression-sources.mjs +220 -0
  37. package/plugin/scripts/project-progression-store.mjs +106 -13
  38. package/plugin/scripts/ruvnet-gate1-pattern.mjs +29 -0
  39. package/plugin/scripts/session-snapshot-hook.mjs +115 -7
  40. package/plugin/scripts/session-start-budget.mjs +59 -0
  41. package/plugin/scripts/session-start-core.mjs +234 -457
  42. package/plugin/scripts/session-start-fsutil.mjs +61 -0
  43. package/plugin/scripts/session-start-health.mjs +64 -0
  44. package/plugin/scripts/session-start-hook-description.mjs +45 -0
  45. package/plugin/scripts/session-start-issue-alert.mjs +77 -0
  46. package/plugin/scripts/session-start-repo-identity.mjs +54 -0
  47. package/plugin/scripts/session-start-signals.mjs +73 -0
  48. package/plugin/scripts/session-start-trace.mjs +86 -0
  49. package/plugin/scripts/session-start-update-plane.mjs +104 -0
  50. package/plugin/scripts/unprompted-runtime.mjs +32 -2
  51. package/plugin/skills/ruvnet-brain/PLAYBOOK.md +26 -2
  52. package/plugin/skills/ruvnet-brain/SKILL.md +67 -2
  53. package/scripts/adr-072-completion.mjs +1 -1
  54. package/scripts/agentdb-fleet-doctor.mjs +5 -1
  55. package/scripts/approved-runtime.mjs +197 -0
  56. package/scripts/brain-novice-50.mjs +16 -1
  57. package/scripts/brain-score.mjs +23 -5
  58. package/scripts/build-bundle.mjs +971 -530
  59. package/scripts/build-concepts.mjs +36 -116
  60. package/scripts/console-engine.test.mjs +8 -7
  61. package/scripts/console-runtime-identity.mjs +4 -0
  62. package/scripts/corpus-aggregates.mjs +94 -77
  63. package/scripts/corpus-candidate.mjs +475 -222
  64. package/scripts/corpus-next-seed.mjs +225 -0
  65. package/scripts/corpus-promotion.mjs +58 -0
  66. package/scripts/corpus-reconcile.mjs +411 -105
  67. package/scripts/doc-currency.mjs +16 -1
  68. package/scripts/dual-host-deliberation.mjs +25 -2
  69. package/scripts/dual-host-suggest.mjs +17 -1
  70. package/scripts/falsify.mjs +13 -3
  71. package/scripts/gist-receipts.mjs +482 -87
  72. package/scripts/github-health-watch.mjs +12 -2
  73. package/scripts/handoff-asset.mjs +34 -0
  74. package/scripts/hook-retirement-check.mjs +8 -1
  75. package/scripts/host-registry.mjs +1 -1
  76. package/scripts/ingest-gists.mjs +74 -101
  77. package/scripts/job-heartbeat.sh +77 -14
  78. package/scripts/learning-replay-execution.mjs +10 -4
  79. package/scripts/nightly-gists.sh +27 -13
  80. package/scripts/nightly-two-run-proof.mjs +1 -1
  81. package/scripts/nightly-watchdog.mjs +61 -4
  82. package/scripts/onboarding-console.mjs +319 -27
  83. package/scripts/oracle/produce-questions.mjs +293 -0
  84. package/scripts/oracle/producer-hosts.mjs +235 -0
  85. package/scripts/oracle/repo-recall.mjs +448 -0
  86. package/scripts/oracle/retrieval-accuracy.mjs +818 -0
  87. package/scripts/oracle/source-tree.mjs +165 -0
  88. package/scripts/oracle/source-units.mjs +391 -0
  89. package/scripts/oracle/spike-run.mjs +98 -0
  90. package/scripts/oracle/unit-inventory.mjs +141 -0
  91. package/scripts/oracle/unit-sampling.mjs +128 -0
  92. package/scripts/oracle/validate-labels.mjs +250 -0
  93. package/scripts/private-overlay.mjs +248 -0
  94. package/scripts/product-integrity-contract.mjs +1 -1
  95. package/scripts/proxy/claude-proxied.sh +6 -0
  96. package/scripts/proxy/proxy-revert.sh +5 -0
  97. package/scripts/proxy/proxy-up.sh +6 -0
  98. package/scripts/proxy/proxy-verify.mjs +4 -0
  99. package/scripts/public-inputs.mjs +409 -0
  100. package/scripts/public-verification-inputs.mjs +112 -26
  101. package/scripts/public-verification-lane.mjs +1 -1
  102. package/scripts/published-surface-probe.mjs +34 -4
  103. package/scripts/qe/card-lane-gate.mjs +16 -1
  104. package/scripts/qe/session-start-gate.mjs +16 -1
  105. package/scripts/rebuild-gists-from-receipts.mjs +58 -78
  106. package/scripts/record-lesson.mjs +4 -1
  107. package/scripts/rehearse-corpus-pipeline.mjs +994 -0
  108. package/scripts/release-abort-stale.mjs +5 -1
  109. package/scripts/release-authority.mjs +104 -12
  110. package/scripts/release-channel-kind.mjs +86 -0
  111. package/scripts/release-convergence-watchdog.mjs +7 -2
  112. package/scripts/release-projection.mjs +177 -72
  113. package/scripts/release-transaction-provider.mjs +23 -6
  114. package/scripts/release.mjs +252 -17
  115. package/scripts/retrieval-canary.mjs +87 -0
  116. package/scripts/rvf-index-audit.mjs +573 -13
  117. package/scripts/rvf-wire.mjs +269 -0
  118. package/scripts/seal-gist-receipt.mjs +65 -0
  119. package/scripts/selfcheck.mjs +42 -21
  120. package/scripts/source-coverage.mjs +253 -24
  121. package/scripts/status-honesty.mjs +25 -0
  122. package/scripts/sync-census.mjs +0 -0
  123. package/scripts/sync-version.mjs +2 -0
  124. package/scripts/trismart.mjs +42 -0
  125. package/scripts/updater-manifest.mjs +162 -0
  126. package/scripts/verify-channels.mjs +17 -5
  127. package/scripts/wired-check.mjs +48 -10
  128. package/tri-smart-skill/QUICKSTART.md +37 -0
  129. package/tri-smart-skill/README.md +92 -0
  130. package/tri-smart-skill/install.cmd +14 -0
  131. package/tri-smart-skill/install.command +13 -0
  132. package/tri-smart-skill/install.mjs +51 -0
  133. package/tri-smart-skill/install.sh +9 -0
  134. package/tri-smart-skill/tri-smart/SKILL.md +90 -0
  135. package/tri-smart-skill/tri-smart/evals/evals.json +25 -0
  136. package/tri-smart-skill/tri-smart/references/protocol.md +25 -0
  137. package/tri-smart-skill/tri-smart/references/provider-cli.md +18 -0
  138. package/tri-smart-skill/tri-smart/scripts/review.mjs +154 -0
  139. package/tri-smart-skill/tri-smart/scripts/setup.mjs +97 -0
  140. package/tri-smart-skill/tri-smart/scripts/verify-access.mjs +107 -0
  141. package/scripts/corpus-seed-publish.mjs +0 -110
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env node
2
+ // scripts/corpus-next-seed.mjs — resolve night N+1's seed from night N's published generation.
3
+ //
4
+ // ADR-086 step 18, verbatim: "Resolve the previous compatible, verified corpus generation at runtime
5
+ // as the next seed; retain the committed exact seed as bootstrap/recovery input. Do not commit a new
6
+ // pointer every night."
7
+ //
8
+ // So this NEVER writes to the repository. It reads the published release list, picks the newest
9
+ // corpus generation that is genuinely usable as a seed, and prints a descriptor for the preparation
10
+ // workflow to consume. When nothing published qualifies — first run, an incompatible runtime, an
11
+ // unsigned or half-uploaded release — it falls back to the committed data/corpus-seed.json, which is
12
+ // the bootstrap/recovery input and is content-addressed independently.
13
+ //
14
+ // "Compatible" is decided against the SAME approved runtime pin that gates promotion
15
+ // (scripts/approved-runtime.mjs): a generation whose archive shipped a different brainVersion is a
16
+ // different runtime, and seeding from it would drag unapproved executables forward through reuse.
17
+ //
18
+ // "Verified" is decided by evidence that is checkable without downloading 500 MB here: the tag must
19
+ // be the content-addressed corpus-sha256-<digest> form, the release must not be a draft, and it must
20
+ // carry all three of ruvnet-brain.zip, its detached .sig, and corpus-receipt.json, with the receipt's
21
+ // own archive digest equal to the digest in the tag. The full byte-level proof still happens
22
+ // downstream where the archive is actually fetched (corpus-seed.yml re-checks sha256 and byte length
23
+ // before reconciliation, and corpus-candidate.mjs re-derives the whole candidate from the bytes).
24
+ //
25
+ // Usage:
26
+ // node scripts/corpus-next-seed.mjs --repo owner/name [--pin data/approved-runtime.json]
27
+ // [--bootstrap data/corpus-seed.json] [--out <file>]
28
+
29
+ import fs from 'node:fs';
30
+ import os from 'node:os';
31
+ import path from 'node:path';
32
+ import { spawnSync } from 'node:child_process';
33
+ import { fileURLToPath, pathToFileURL } from 'node:url';
34
+ import { readApprovedRuntime, validateApprovedRuntime } from './approved-runtime.mjs';
35
+
36
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
37
+ const HEX64 = /^[0-9a-f]{64}$/;
38
+ const CORPUS_TAG = /^corpus-sha256-([0-9a-f]{64})$/;
39
+ const ARCHIVE_ASSET = 'ruvnet-brain.zip';
40
+ const SIGNATURE_ASSET = 'ruvnet-brain.zip.sig';
41
+ const RECEIPT_ASSET = 'corpus-receipt.json';
42
+
43
+ const defaultRun = (command, args, options) => spawnSync(command, args, { encoding: 'utf8', ...options });
44
+
45
+ function ghJson(run, args) {
46
+ const result = run('gh', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
47
+ if (result.error || result.status !== 0) {
48
+ throw new Error(String(result.error?.message || result.stderr || result.stdout || `gh exited ${result.status}`).trim());
49
+ }
50
+ return JSON.parse(String(result.stdout || 'null'));
51
+ }
52
+
53
+ export function validateBootstrapSeed(seed) {
54
+ const failures = [];
55
+ if (!seed || typeof seed !== 'object') return ['committed bootstrap seed is not an object'];
56
+ if (seed.schemaVersion !== 1) failures.push('committed bootstrap seed schemaVersion must be 1');
57
+ if (!seed.tag || seed.tag === 'latest') failures.push('committed bootstrap seed tag is missing or forbidden');
58
+ if (seed.asset !== ARCHIVE_ASSET) failures.push(`committed bootstrap seed asset must be ${ARCHIVE_ASSET}`);
59
+ if (!HEX64.test(String(seed.sha256 || ''))) failures.push('committed bootstrap seed sha256 is malformed');
60
+ if (!Number.isSafeInteger(seed.bytes) || seed.bytes < 1) failures.push('committed bootstrap seed bytes is malformed');
61
+ return failures;
62
+ }
63
+
64
+ /**
65
+ * One published release, judged. Returns null when it cannot serve as a seed, with the reason
66
+ * recorded on `rejected` so a no-op night is explainable rather than silent.
67
+ */
68
+ function judgeRelease({ run, repo, tag, digest, approved, rejected }) {
69
+ let view;
70
+ try {
71
+ view = ghJson(run, ['release', 'view', tag, '--repo', repo, '--json', 'tagName,isDraft,assets']);
72
+ } catch (error) {
73
+ rejected.push({ tag, reason: `release view failed (${error.message})` });
74
+ return null;
75
+ }
76
+ if (view?.tagName !== tag || view.isDraft) {
77
+ rejected.push({ tag, reason: 'release is a draft or names another tag' });
78
+ return null;
79
+ }
80
+ const assets = Array.isArray(view.assets) ? view.assets : [];
81
+ const named = (name) => assets.filter((asset) => asset?.name === name);
82
+ for (const name of [ARCHIVE_ASSET, SIGNATURE_ASSET, RECEIPT_ASSET]) {
83
+ if (named(name).length !== 1) {
84
+ rejected.push({ tag, reason: `unverified: expected exactly one ${name} asset` });
85
+ return null;
86
+ }
87
+ }
88
+ const archive = named(ARCHIVE_ASSET)[0];
89
+ if (!Number.isSafeInteger(archive.size) || archive.size < 1) {
90
+ rejected.push({ tag, reason: 'unverified: archive asset has no usable byte length' });
91
+ return null;
92
+ }
93
+
94
+ const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'corpus-next-seed-'));
95
+ try {
96
+ const download = run('gh', ['release', 'download', tag, '--repo', repo, '--pattern', RECEIPT_ASSET, '--dir', scratch],
97
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
98
+ if (download.error || download.status !== 0) {
99
+ rejected.push({ tag, reason: 'unverified: corpus receipt could not be downloaded' });
100
+ return null;
101
+ }
102
+ let receipt;
103
+ try { receipt = JSON.parse(fs.readFileSync(path.join(scratch, RECEIPT_ASSET), 'utf8')); }
104
+ catch (error) { rejected.push({ tag, reason: `unverified: corpus receipt unreadable (${error.message})` }); return null; }
105
+
106
+ // ADR-086 Step 15 / A6 moved the corpus receipt to schemaVersion 3 (it now binds the detached
107
+ // retrieval-accuracy report). This reader has to move with it: left at 2 it would reject every
108
+ // schema-3 generation as unverified and silently fall back to the committed bootstrap seed every
109
+ // night — a degradation that looks exactly like "no new generation yet".
110
+ if (receipt.schemaVersion !== 3 || receipt.kind !== 'ruvnet-brain-corpus-candidate') {
111
+ rejected.push({ tag, reason: 'unverified: receipt schema or kind is not a schema-3 corpus candidate' });
112
+ return null;
113
+ }
114
+ if (!receipt.accuracyReport?.file || !/^[a-f0-9]{64}$/.test(String(receipt.accuracyReport.sha256 || ''))
115
+ || !Number.isSafeInteger(receipt.accuracyReport.bytes)) {
116
+ rejected.push({ tag, reason: 'unverified: receipt carries no retrieval-accuracy binding' });
117
+ return null;
118
+ }
119
+ if (receipt.archive?.sha256 !== digest || receipt.archive?.bytes !== archive.size) {
120
+ rejected.push({ tag, reason: 'unverified: receipt archive identity disagrees with the content-addressed tag' });
121
+ return null;
122
+ }
123
+ if (receipt.archiveManifestVersion !== approved.brainVersion || receipt.archiveManifestReleaseTag !== approved.releaseTag) {
124
+ rejected.push({ tag, reason: `incompatible: generation shipped runtime ${receipt.archiveManifestReleaseTag}, approved runtime is ${approved.releaseTag}` });
125
+ return null;
126
+ }
127
+ return {
128
+ origin: 'published-generation',
129
+ tag,
130
+ asset: ARCHIVE_ASSET,
131
+ sha256: digest,
132
+ bytes: archive.size,
133
+ sourceCommit: typeof receipt.builderSourceSha === 'string' ? receipt.builderSourceSha : null,
134
+ brainVersion: receipt.archiveManifestVersion,
135
+ };
136
+ } finally {
137
+ fs.rmSync(scratch, { recursive: true, force: true });
138
+ }
139
+ }
140
+
141
+ export function resolveNextCorpusSeed({
142
+ repo, run = defaultRun, root = ROOT, pinFile, bootstrapFile, limit = 100,
143
+ } = {}) {
144
+ if (!/^[^/\s]+\/[^/\s]+$/.test(String(repo || ''))) throw new Error('--repo must be owner/name');
145
+
146
+ const approved = readApprovedRuntime(pinFile || path.join(root, 'data/approved-runtime.json'));
147
+ const pinFailures = validateApprovedRuntime(approved);
148
+ if (pinFailures.length) throw new Error(`approved runtime pin is invalid: ${pinFailures.join('; ')}`);
149
+
150
+ const bootstrapPath = path.resolve(bootstrapFile || path.join(root, 'data/corpus-seed.json'));
151
+ const bootstrap = JSON.parse(fs.readFileSync(bootstrapPath, 'utf8'));
152
+ const bootstrapFailures = validateBootstrapSeed(bootstrap);
153
+ if (bootstrapFailures.length) throw new Error(`committed bootstrap seed is invalid: ${bootstrapFailures.join('; ')}`);
154
+
155
+ const rejected = [];
156
+ let listed = [];
157
+ try {
158
+ listed = ghJson(run, ['release', 'list', '--repo', repo, '--limit', String(limit), '--json', 'tagName,isDraft,createdAt']) || [];
159
+ } catch (error) {
160
+ rejected.push({ tag: null, reason: `release list failed (${error.message})` });
161
+ }
162
+
163
+ // Every corpus-shaped tag that is NOT usable gets an explicit reason. A silently skipped row is
164
+ // indistinguishable from "there were none", and this project has already paid for one diagnostic
165
+ // that reported a defect nobody read — a skip nobody can see is worse.
166
+ const corpusShaped = (Array.isArray(listed) ? listed : [])
167
+ .filter((row) => row && CORPUS_TAG.test(String(row.tagName || '')));
168
+ const candidates = [];
169
+ for (const row of corpusShaped) {
170
+ if (row.isDraft) { rejected.push({ tag: row.tagName, reason: 'unverified: release is still a draft' }); continue; }
171
+ const createdAt = Date.parse(row.createdAt);
172
+ if (!Number.isFinite(createdAt)) { rejected.push({ tag: row.tagName, reason: 'unverified: release has no readable publication time' }); continue; }
173
+ candidates.push({ tag: row.tagName, digest: CORPUS_TAG.exec(row.tagName)[1], createdAt });
174
+ }
175
+ candidates.sort((left, right) => right.createdAt - left.createdAt);
176
+
177
+ for (const candidate of candidates) {
178
+ const resolved = judgeRelease({ run, repo, tag: candidate.tag, digest: candidate.digest, approved, rejected });
179
+ if (resolved) return { seed: resolved, rejected, approvedRuntime: approved.releaseTag };
180
+ }
181
+
182
+ return {
183
+ seed: {
184
+ origin: 'committed-bootstrap',
185
+ tag: bootstrap.tag,
186
+ asset: bootstrap.asset,
187
+ sha256: String(bootstrap.sha256).toLowerCase(),
188
+ bytes: bootstrap.bytes,
189
+ sourceCommit: typeof bootstrap.sourceCommit === 'string' ? bootstrap.sourceCommit : null,
190
+ brainVersion: null,
191
+ },
192
+ rejected,
193
+ approvedRuntime: approved.releaseTag,
194
+ };
195
+ }
196
+
197
+ const arg = (name, fallback) => {
198
+ const index = process.argv.indexOf(name);
199
+ return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
200
+ };
201
+
202
+ function main() {
203
+ let result;
204
+ try {
205
+ result = resolveNextCorpusSeed({
206
+ repo: arg('--repo', process.env.GITHUB_REPOSITORY),
207
+ pinFile: arg('--pin'),
208
+ bootstrapFile: arg('--bootstrap'),
209
+ });
210
+ } catch (error) {
211
+ console.error(`[corpus-next-seed] ${error.message}`);
212
+ return 1;
213
+ }
214
+ for (const row of result.rejected) console.error(`[corpus-next-seed] skipped ${row.tag || '(list)'}: ${row.reason}`);
215
+ const out = arg('--out');
216
+ const serialized = `${JSON.stringify(result.seed, null, 2)}\n`;
217
+ if (out) fs.writeFileSync(path.resolve(out), serialized);
218
+ process.stdout.write(serialized);
219
+ console.error(`[corpus-next-seed] ${result.seed.origin} ${result.seed.tag} (approved runtime ${result.approvedRuntime})`);
220
+ return 0;
221
+ }
222
+
223
+ if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
224
+ process.exitCode = main();
225
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // scripts/corpus-promotion.mjs — the ordering rule for customer corpus releases (ADR-086 step 17).
3
+ //
4
+ // This lives in its own module rather than inside scripts/release.mjs for one concrete reason:
5
+ // release.mjs runs its whole gate flow at import time (`npm test`, vitest, version sync), so a test
6
+ // that imported it to check one pure function would execute a full release preflight as a side
7
+ // effect. A decision this load-bearing must be directly testable.
8
+ //
9
+ // WHAT IT DECIDES. A corpus tag is the archive's own digest, so two tags carry no ordering between
10
+ // them — nothing in `corpus-sha256-<a>` versus `corpus-sha256-<b>` says which came later. Promotion
11
+ // to `releases/latest` therefore needs an explicit, PUBLISHED ordering key, and the rule is strict
12
+ // monotonicity in it: a generation may take latest only if it is strictly newer than the corpus
13
+ // generation already there. Everything ambiguous fails closed, because "cannot prove we are newer"
14
+ // and "we are older" have the same correct answer.
15
+
16
+ export const CORPUS_GENERATION_FIELD = 'Corpus generation:';
17
+ export const CORPUS_TAG_PATTERN = /^corpus-sha256-[0-9a-f]{64}$/;
18
+
19
+ export function parseCorpusGeneration(body) {
20
+ const line = String(body || '').split('\n').map((row) => row.trim())
21
+ .find((row) => row.startsWith(CORPUS_GENERATION_FIELD));
22
+ if (!line) return null;
23
+ const value = line.slice(CORPUS_GENERATION_FIELD.length).trim();
24
+ const parsed = Date.parse(value);
25
+ return Number.isFinite(parsed) ? { value, epoch: parsed } : null;
26
+ }
27
+
28
+ export function evaluateCorpusPromotion({ tag, generation, currentLatest } = {}) {
29
+ if (!currentLatest) return { allowed: true, reason: 'no published latest release to move' };
30
+ if (currentLatest.tagName === tag) {
31
+ return { allowed: false, reason: `${tag} is already the published latest release` };
32
+ }
33
+ if (!CORPUS_TAG_PATTERN.test(String(currentLatest.tagName || ''))) {
34
+ // A code release holds latest. Corpus promotion does not regress the runtime, because
35
+ // scripts/approved-runtime.mjs has already proved this archive's executables ARE the
36
+ // owner-approved shipped runtime, byte for byte.
37
+ return {
38
+ allowed: true,
39
+ reason: `current latest ${currentLatest.tagName} is a code release; runtime equality is enforced by the approved runtime pin`,
40
+ };
41
+ }
42
+ const published = parseCorpusGeneration(currentLatest.body);
43
+ if (!published) {
44
+ return {
45
+ allowed: false,
46
+ reason: `current latest ${currentLatest.tagName} is a corpus release with no readable "${CORPUS_GENERATION_FIELD}" ordering key`,
47
+ };
48
+ }
49
+ const mine = Date.parse(generation);
50
+ if (!Number.isFinite(mine)) return { allowed: false, reason: 'this candidate has no readable corpus generation timestamp' };
51
+ if (mine <= published.epoch) {
52
+ return {
53
+ allowed: false,
54
+ reason: `refusing to move customers backward: ${currentLatest.tagName} published generation ${published.value} is not older than ${generation}`,
55
+ };
56
+ }
57
+ return { allowed: true, reason: `newer than ${currentLatest.tagName} (${published.value})` };
58
+ }