@blamejs/exceptd-skills 0.19.33 → 0.19.34

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 (119) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +2 -2
  4. package/lib/auto-discovery.js +56 -286
  5. package/lib/canonical-eq.js +7 -40
  6. package/lib/citation-resolve.js +22 -70
  7. package/lib/collectors/ai-api.js +20 -54
  8. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  9. package/lib/collectors/citation-hygiene.js +72 -210
  10. package/lib/collectors/containers.js +41 -130
  11. package/lib/collectors/cred-stores.js +31 -115
  12. package/lib/collectors/crypto-codebase.js +55 -138
  13. package/lib/collectors/crypto.js +24 -54
  14. package/lib/collectors/hardening.js +20 -78
  15. package/lib/collectors/kernel.js +16 -46
  16. package/lib/collectors/library-author.js +57 -206
  17. package/lib/collectors/mcp.js +24 -70
  18. package/lib/collectors/runtime.js +24 -86
  19. package/lib/collectors/sbom.js +34 -106
  20. package/lib/collectors/scan-excludes.js +31 -138
  21. package/lib/collectors/secrets.js +62 -178
  22. package/lib/cross-ref-api.js +39 -123
  23. package/lib/currency-severity.js +8 -27
  24. package/lib/cve-batch.js +13 -21
  25. package/lib/cve-cli.js +13 -20
  26. package/lib/cve-curation.js +72 -239
  27. package/lib/cve-regression-watcher.js +29 -152
  28. package/lib/cvss.js +13 -54
  29. package/lib/doctor-bucketing.js +3 -19
  30. package/lib/exit-codes.js +10 -42
  31. package/lib/flag-suggest.js +7 -25
  32. package/lib/framework-gap.js +35 -114
  33. package/lib/gap-detectors.js +37 -159
  34. package/lib/id-validation.js +9 -30
  35. package/lib/job-queue.js +13 -36
  36. package/lib/lint-skills.js +64 -232
  37. package/lib/playbook-runner.js +693 -2095
  38. package/lib/prefetch.js +100 -376
  39. package/lib/refresh-external.js +199 -627
  40. package/lib/refresh-network.js +75 -307
  41. package/lib/rfc-cli.js +23 -68
  42. package/lib/scoring.js +77 -145
  43. package/lib/sign.js +43 -229
  44. package/lib/source-advisories.js +43 -194
  45. package/lib/source-ghsa.js +37 -120
  46. package/lib/source-osv.js +94 -266
  47. package/lib/ttp-mapper.js +14 -24
  48. package/lib/upstream-check-cli.js +10 -28
  49. package/lib/upstream-check.js +19 -44
  50. package/lib/validate-catalog-meta.js +17 -61
  51. package/lib/validate-cve-catalog.js +43 -119
  52. package/lib/validate-indexes.js +25 -76
  53. package/lib/validate-package.js +16 -62
  54. package/lib/validate-playbooks.js +69 -275
  55. package/lib/validate-vendor.js +16 -49
  56. package/lib/verify.js +56 -286
  57. package/lib/version-pins.js +5 -34
  58. package/lib/worker-pool.js +11 -30
  59. package/lib/xml-tokenizer.js +47 -152
  60. package/manifest.json +53 -53
  61. package/orchestrator/dispatcher.js +17 -68
  62. package/orchestrator/event-bus.js +11 -74
  63. package/orchestrator/index.js +138 -412
  64. package/orchestrator/pipeline.js +28 -85
  65. package/orchestrator/scanner.js +34 -138
  66. package/orchestrator/scheduler.js +20 -84
  67. package/package.json +1 -1
  68. package/sbom.cdx.json +241 -241
  69. package/scripts/audit-catalog-gaps.js +9 -62
  70. package/scripts/audit-cross-skill.js +5 -31
  71. package/scripts/audit-perf.js +6 -16
  72. package/scripts/backfill-theater-test.js +7 -64
  73. package/scripts/bootstrap.js +12 -44
  74. package/scripts/build-indexes.js +40 -154
  75. package/scripts/builders/activity-feed.js +4 -14
  76. package/scripts/builders/catalog-summaries.js +3 -10
  77. package/scripts/builders/currency.js +7 -20
  78. package/scripts/builders/cwe-chains.js +7 -30
  79. package/scripts/builders/did-ladders.js +6 -13
  80. package/scripts/builders/frequency.js +5 -19
  81. package/scripts/builders/jurisdiction-clocks.js +6 -25
  82. package/scripts/builders/recipes.js +6 -14
  83. package/scripts/builders/section-offsets.js +13 -51
  84. package/scripts/builders/stale-content.js +7 -28
  85. package/scripts/builders/summary-cards.js +8 -29
  86. package/scripts/builders/theater-fingerprints.js +12 -27
  87. package/scripts/builders/token-budget.js +4 -31
  88. package/scripts/check-agents-md-collectors.js +11 -54
  89. package/scripts/check-catalog-gap-budget.js +15 -32
  90. package/scripts/check-changelog-extract.js +18 -48
  91. package/scripts/check-codebase-patterns-currency.js +6 -22
  92. package/scripts/check-codebase-patterns.js +50 -143
  93. package/scripts/check-epss-consistency.js +9 -64
  94. package/scripts/check-framework-gap-coverage.js +13 -31
  95. package/scripts/check-manifest-snapshot.js +13 -73
  96. package/scripts/check-sbom-currency.js +44 -142
  97. package/scripts/check-test-count.js +15 -52
  98. package/scripts/check-test-coverage.js +66 -197
  99. package/scripts/check-test-subjects.js +21 -62
  100. package/scripts/check-ttp-references.js +14 -38
  101. package/scripts/check-ttp-upstream.js +8 -40
  102. package/scripts/check-version-bump.js +9 -61
  103. package/scripts/check-version-tags.js +20 -121
  104. package/scripts/predeploy.js +38 -184
  105. package/scripts/refresh-manifest-snapshot.js +16 -38
  106. package/scripts/refresh-mitre-atlas.js +3 -8
  107. package/scripts/refresh-mitre-attack.js +1 -8
  108. package/scripts/refresh-mitre-d3fend.js +3 -9
  109. package/scripts/refresh-mitre-ics-attack.js +3 -8
  110. package/scripts/refresh-reverse-refs.js +27 -94
  111. package/scripts/refresh-rfc-index.js +2 -10
  112. package/scripts/refresh-sbom.js +31 -161
  113. package/scripts/refresh-upstream-catalogs.js +40 -137
  114. package/scripts/release.js +69 -232
  115. package/scripts/run-e2e-scenarios.js +24 -71
  116. package/scripts/sync-manifest-metadata.js +10 -34
  117. package/scripts/sync-package-description.js +8 -17
  118. package/scripts/validate-vendor-online.js +13 -44
  119. package/scripts/verify-shipped-tarball.js +35 -140
@@ -1,44 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * release.js — orchestrate the exceptd release flow as a sequence of
5
- * idempotent subcommands. Each subcommand performs ONE phase, prints what
6
- * it did, and exits with a code that's safe to script against in a terminal
7
- * or CI runner. It codifies the flow that CONTRIBUTING.md / the repo's
8
- * release notes describe step by step, so a release can't skip the
9
- * load-bearing ordering (CHANGELOG entry first, gates before tag, CI green
10
- * before tag push, GUARD before tag).
11
- *
12
- * Usage:
13
- * node scripts/release.js prepare [--minor] [--with-content]
14
- * # bump + sign + indexes + snapshot + sbom + baseline
15
- * # --with-content: this release ships uncommitted work
16
- * node scripts/release.js gates # npm test + 20-gate predeploy
17
- * node scripts/release.js commit # release branch + signed commit
18
- * node scripts/release.js push # push branch + open PR
19
- * node scripts/release.js watch # CI watch + flag unresolved review threads
20
- * node scripts/release.js merge # admin squash-merge if CLEAN + zero unresolved
21
- * node scripts/release.js tag # GUARD + signed tag + push tag + verify
22
- * node scripts/release.js release # watch release.yml + npm/global/tarball verify
23
- * node scripts/release.js all [--minor] # all eight in sequence
24
- * node scripts/release.js status # what phase the current branch is in
25
- * node scripts/release.js help # this banner
26
- *
27
- * Pre-conditions the script enforces rather than assumes:
28
- * - prepare runs only on a clean `main`, and refuses unless CHANGELOG.md
29
- * already carries a `## <next-version>` heading (the operator writes the
30
- * behavior-framed notes by hand; they don't auto-generate from a diff).
31
- * - The three-version invariant (package.json == manifest.json ==
32
- * CHANGELOG top heading) is established by prepare and re-checked by tag.
33
- * - tag refuses unless local HEAD == origin/main and the version matches
34
- * and no such tag exists (the GUARD that prevents tag-on-stale-HEAD).
35
- *
36
- * Patch is the default bump. --minor requires the explicit flag AND is a
37
- * deliberate choice — the project default is patch-only.
38
- *
39
- * The judgment-requiring parts stay manual: writing the CHANGELOG entry,
40
- * reviewing/fixing CI-surfaced review-thread findings (watch flags them and
41
- * stops), and choosing patch vs minor.
4
+ * Orchestrates the release as idempotent per-phase subcommands, each enforcing
5
+ * its own preconditions so the load-bearing ordering cannot be skipped. `help`
6
+ * lists them.
42
7
  */
43
8
 
44
9
  var fs = require("node:fs");
@@ -49,27 +14,18 @@ var ROOT = path.resolve(__dirname, "..");
49
14
  var REPO = "blamejs/exceptd-skills";
50
15
  var PKG_NAME = "@blamejs/exceptd-skills";
51
16
 
52
- // Known-flaky CI jobs that warrant an auto-rerun rather than a hard fail:
53
- // the macOS playbook-runner job and the offline-CLI F1 check both flake on
54
- // fresh runners. watch reruns them up to twice before surfacing a failure.
55
17
  var RERUN_LIMIT = 2;
56
18
 
57
- // ---- Helpers -------------------------------------------------------------
58
-
59
- // Windows resolves `npm` / `npx` as `.cmd` shims, which child_process can
60
- // only invoke through a shell. `git`, `gh`, `node` are native exes that
61
- // spawn directly — keeping shell off avoids the implicit arg-quoting risk.
19
+ // Windows resolves `npm` / `npx` as `.cmd` shims, which child_process can only
20
+ // invoke through a shell; `git`, `gh` and `node` are native exes that spawn directly.
62
21
  function _needsShell(cmd) {
63
22
  if (process.platform !== "win32") return false;
64
23
  return cmd === "npm" || cmd === "npx";
65
24
  }
66
25
 
67
26
  // spawnSync with shell:true AND an args array concatenates the args without
68
- // escaping (Node DEP0190 — a real injection surface). When a shell is needed
69
- // (npm/npx on Windows) we instead pass the whole invocation as one command
70
- // string with no args array, which is the correct shell-invocation form. The
71
- // only commands routed through the shell here are npm with static token args
72
- // (verb names + flags, no spaces), so the single-string join is unambiguous.
27
+ // escaping (Node DEP0190 — an injection surface), so the shell path passes the
28
+ // whole invocation as one command string. Only npm with static token args goes there.
73
29
  function _spawn(cmd, args, opts) {
74
30
  opts = opts || {};
75
31
  var useShell = _needsShell(cmd);
@@ -114,9 +70,8 @@ function _readJsonVersion(file) {
114
70
  return JSON.parse(fs.readFileSync(path.join(ROOT, file), "utf8")).version;
115
71
  }
116
72
 
117
- // Rewrite only the top-level "version" line so formatting/key-order of the
118
- // rest of the file is untouched (a full JSON.stringify would reflow
119
- // manifest.json's hand-maintained shape).
73
+ // Rewrites only the "version" line: a full JSON.stringify would reflow
74
+ // manifest.json's hand-maintained key order and formatting.
120
75
  function _writeJsonVersion(file, next) {
121
76
  var p = path.join(ROOT, file);
122
77
  var content = fs.readFileSync(p, "utf8");
@@ -136,7 +91,6 @@ function _bump(version, kind) {
136
91
  return parts[0] + "." + parts[1] + "." + (parts[2] + 1);
137
92
  }
138
93
 
139
- // Topmost `## X.Y.Z` heading in CHANGELOG.md.
140
94
  function _changelogTopVersion() {
141
95
  var lines = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8").split(/\r?\n/);
142
96
  for (var i = 0; i < lines.length; i++) {
@@ -146,8 +100,6 @@ function _changelogTopVersion() {
146
100
  return null;
147
101
  }
148
102
 
149
- // Extract the body of a CHANGELOG section (between its `## X.Y.Z` heading
150
- // and the next `## ` heading) — used to compose the commit + PR body.
151
103
  function _changelogSection(version) {
152
104
  var lines = fs.readFileSync(path.join(ROOT, "CHANGELOG.md"), "utf8").split(/\r?\n/);
153
105
  var out = [];
@@ -164,10 +116,8 @@ function _changelogSection(version) {
164
116
  return out.join("\n").trim();
165
117
  }
166
118
 
167
- // Derive a concise "vX.Y.Z: <subject>" commit/PR title from a CHANGELOG
168
- // section. exceptd's entries lead with a prose paragraph (no dedicated
169
- // headline field), so take the first SENTENCE and cap the length rather than
170
- // dump the whole paragraph as the subject. The operator can always amend.
119
+ // "vX.Y.Z: <subject>" for the commit and PR title. CHANGELOG entries carry no
120
+ // headline field, so the subject is the first sentence, length-capped.
171
121
  function _releaseSubject(version, section) {
172
122
  var firstLine = (section.split(/\r?\n/).find(function (l) { return l.trim(); }) || "").trim();
173
123
  var firstSentence = firstLine.split(/(?<=[.!?])\s/)[0] || firstLine;
@@ -182,10 +132,8 @@ function _gitOnMain() { return _gitBranch() === "main"; }
182
132
  function _gitOnRelease() { return /^release-v\d+\.\d+\.\d+$/.test(_gitBranch()); }
183
133
  function _releaseBranchFor(version) { return "release-v" + version; }
184
134
 
185
- // Verify HEAD's commit signature two independent ways: `git verify-commit`
186
- // (the canonical boolean GitHub's required_signatures ruleset checks) and a
187
- // human-readable `%G? %GS` line. main is under required_signatures, so an
188
- // unsigned commit can't be pushed — fail loudly here rather than at push.
135
+ // `git verify-commit` is the boolean GitHub's required_signatures ruleset checks;
136
+ // main is under that ruleset, so fail here rather than at push.
189
137
  function _verifyCommitSignature(label) {
190
138
  var verify = _capture("git", ["verify-commit", "HEAD"]);
191
139
  if (verify.status !== 0) {
@@ -205,9 +153,8 @@ function _openPrNumber(branch) {
205
153
  "--json", "number", "--jq", ".[0].number"]).stdout;
206
154
  }
207
155
 
208
- // Unresolved review threads on the PR. Codex (chatgpt-codex-connector) posts
209
- // review threads with P-badge findings; an unresolved thread is a hard
210
- // branch-protection merge block (conversation-resolution required).
156
+ // Conversation resolution is branch-protection-required, so an unresolved
157
+ // thread is a hard merge block.
211
158
  function _unresolvedThreads(prNum) {
212
159
  var q = 'query { repository(owner:"blamejs",name:"exceptd-skills") { pullRequest(number:' +
213
160
  prNum + ') { reviewThreads(first:50) { nodes { isResolved comments(first:1) ' +
@@ -217,32 +164,11 @@ function _unresolvedThreads(prNum) {
217
164
  try { return JSON.parse(rv.stdout || "[]"); } catch (_e) { return []; }
218
165
  }
219
166
 
220
- // Open CodeQL code-scanning alerts on the PR's merge ref. CodeQL SAST runs on
221
- // every PR (.github/workflows/codeql.yml); an open, un-triaged alert is a
222
- // per-release gate on par with the codex review-thread gate. Returns the alert
223
- // array (possibly empty) on success, or null when the query can't run (code
224
- // scanning unavailable, API error, or the GET-param form not supported) so a
225
- // transient failure fails OPEN rather than blocking a release — the pre-flight
226
- // checklist is the human backstop.
227
- // Two refs, and BOTH are load-bearing — each alone has a blind spot that has
228
- // already bitten:
229
- //
230
- // refs/pull/<N>/merge answers "did this PR introduce an alert". Scoping to
231
- // it ALONE let two medium alerts sitting on refs/heads/main since
232
- // 2026-08-08 return zero, riding through eight consecutive releases while
233
- // the phase printed "zero open CodeQL alerts" — GitHub only annotates a PR
234
- // with alerts that PR introduces.
235
- // (no ref) answers against the DEFAULT BRANCH, catching exactly
236
- // those pre-existing findings — but on its own it misses an alert this PR
237
- // introduces that is not on main yet, which is the case the PR-scoped query
238
- // was there for.
239
- //
240
- // So the gate blocks on the union. Replacing one query with the other just
241
- // trades which blind spot you have.
242
- //
243
- // Filtered to tool_name=CodeQL deliberately: OpenSSF Scorecard writes to the
244
- // same code-scanning surface, and its accepted-out-of-scope policy alerts would
245
- // otherwise make this list permanently non-empty and therefore useless.
167
+ // Open CodeQL alerts. Returns the alert array, or null when the query cannot run
168
+ // at all, so a transient API failure fails OPEN rather than blocking a release.
169
+ // The union of two refs is required: refs/pull/<N>/merge misses an alert already
170
+ // sitting on main, and the default-branch query misses one this PR introduces.
171
+ // tool_name=CodeQL excludes Scorecard's accepted-out-of-scope policy alerts.
246
172
  function _codeqlAlertsForRef(ref) {
247
173
  var args = ["api", "repos/:owner/:repo/code-scanning/alerts", "-X", "GET",
248
174
  "-f", "state=open", "-f", "tool_name=CodeQL", "-f", "per_page=100"];
@@ -258,9 +184,7 @@ function _codeqlAlertsForRef(ref) {
258
184
  function _openCodeqlAlerts(prNum) {
259
185
  var onDefault = _codeqlAlertsForRef(null);
260
186
  var onPr = _codeqlAlertsForRef("refs/pull/" + prNum + "/merge");
261
- // Either query failing means the question is unanswered. Never let a failed
262
- // lookup read as "no alerts" — that is the same absent-input-passes shape
263
- // this whole gate exists to close.
187
+ // A failed lookup must never read as "no alerts".
264
188
  if (onDefault === null || onPr === null) return null;
265
189
 
266
190
  var byNumber = new Map();
@@ -272,19 +196,11 @@ function _openCodeqlAlerts(prNum) {
272
196
  return [...byNumber.values()];
273
197
  }
274
198
 
275
- // ---- Subcommands ---------------------------------------------------------
276
-
277
- // The derived-artifact regeneration, in the one order that is correct. Shared
278
- // by `prepare` and `regen` so the two cannot drift: any source edit after a
279
- // prepare — a review finding fixed on the release branch, most often a data
280
- // correction — invalidates the signatures, indexes and SBOM hashes, and
281
- // re-running the four commands from memory is where the ordering gets lost.
199
+ // Shared by `prepare` and `regen` so the two cannot drift.
282
200
  function _regenArtifacts() {
283
201
  _section("regen artifacts");
284
- // Order matters: sign first (re-signs the manifest), then the snapshot/
285
- // index/SBOM derivations. refresh-sbom runs LAST because it hashes the
286
- // shipped tree (incl. README) — regenerating it before a later source edit
287
- // strands the hashes (the recurring "refresh-sbom last" lesson).
202
+ // sign-all first, since it rewrites the manifest; refresh-sbom LAST, since it
203
+ // hashes the shipped tree (README included) and any later edit strands the hashes.
288
204
  _run("node", ["lib/sign.js", "sign-all"]);
289
205
  _run("npm", ["run", "build-indexes"]);
290
206
  _run("npm", ["run", "refresh-snapshot"]);
@@ -292,16 +208,12 @@ function _regenArtifacts() {
292
208
  _ok("signed + indexes + snapshot + sbom regenerated");
293
209
  }
294
210
 
295
- // Re-derive the artifacts after editing source on an already-prepared release
296
- // branch. No version bump, no CHANGELOG requirement, no clean-tree demand —
297
- // this phase exists precisely for a dirty tree. It refuses on main, where the
298
- // bump belongs to `prepare`.
211
+ // Re-derives artifacts after editing an already-prepared release branch: no
212
+ // bump, no CHANGELOG requirement, and a dirty tree is the point.
299
213
  function cmdRegen() {
300
214
  _section("regen");
301
- // Positively require a release branch rather than merely rejecting main: this
302
- // re-signs the manifest and rewrites checked-in derived artifacts, so an
303
- // accidental run from a feature branch or a detached HEAD would produce
304
- // release artifacts from a tree that is not the release.
215
+ // A release branch is positively required rather than main merely rejected: a
216
+ // feature branch or detached HEAD would build artifacts out of the wrong tree.
305
217
  if (!_gitOnRelease()) {
306
218
  throw new Error("release: regen must run on a release-vX.Y.Z branch (on " + _gitBranch() + "). " +
307
219
  "On main the regeneration belongs to `prepare`.");
@@ -319,22 +231,10 @@ function cmdRegen() {
319
231
  function cmdPrepare(opts) {
320
232
  _section("prepare");
321
233
  if (!_gitOnMain()) throw new Error("release: prepare must run on main (on " + _gitBranch() + ")");
322
- // The documented flow is: write the `## <next>` CHANGELOG entry by hand,
323
- // THEN run prepare. That edit makes the tree dirty, so requiring a fully
324
- // clean tree here would make the first phase unusable as documented. Allow
325
- // a CHANGELOG.md-only dirty tree; refuse if anything else is uncommitted
326
- // (prepare is about to bump versions + regenerate artifacts — it must start
327
- // from an otherwise-clean main so the release commit captures only the
328
- // intended change set).
329
- //
330
- // `--with-content` widens that to a release which SHIPS an uncommitted change
331
- // — a curation batch written into data/, a fix folded in alongside the bump.
332
- // Without it, such a release cannot use this phase at all and gets hand-run
333
- // instead, which is how the steps below drift: a hand-rolled sequence dropped
334
- // the shrinkage gate two lines above the baseline refresh and rebaselined
335
- // without ever checking. The flag keeps the phase authoritative for that flow
336
- // rather than leaving it to memory. It still prints what it is carrying, so
337
- // an unintended file in the tree is visible rather than silently released.
234
+ // The `## <next>` CHANGELOG entry is written by hand before prepare runs, so a
235
+ // CHANGELOG.md-only dirty tree is allowed and anything else is refused.
236
+ // `--with-content` widens that to a release which SHIPS uncommitted work, and
237
+ // prints what it carries so an unintended file is visible rather than released.
338
238
  var dirty = _capture("git", ["status", "--porcelain"]).stdout
339
239
  .split(/\r?\n/)
340
240
  .filter(function (l) { return l.trim() && !/\bCHANGELOG\.md$/.test(l); });
@@ -352,24 +252,18 @@ function cmdPrepare(opts) {
352
252
  var next = _bump(current, opts.minor ? "minor" : "patch");
353
253
  console.log("current: " + current + " next: " + next + " (" + (opts.minor ? "minor" : "patch") + ")");
354
254
 
355
- // The CHANGELOG entry is written by hand (behavior-framed, no internal
356
- // narrative). Refuse if it isn't there — the three-version invariant the
357
- // bootstrap-mode test enforces would otherwise fail at gates time.
255
+ // Without the entry, tests/bootstrap-mode's three-version invariant fails at gates.
358
256
  var top = _changelogTopVersion();
359
257
  if (top !== next) {
360
- // Throw (not process.exit) so a stdout write earlier in this phase can't be
361
- // truncated when piped, and so `release all` aborts the whole sequence here
362
- // rather than continuing past a failed prepare. Matches the throw-style
363
- // guards above (clean-tree / on-main); the dispatcher maps it to exit 1.
258
+ // Throw, never process.exit: the exit can truncate a piped stdout write, and
259
+ // `release all` must abort here. The dispatcher maps it to exit 1.
364
260
  throw new Error(
365
261
  "CHANGELOG.md top heading is '## " + top + "', expected '## " + next + "'. " +
366
262
  "Write the " + next + " entry first (terse, behavior-change framed, no internal " +
367
263
  "narrative), then re-run prepare. Example heading: ## " + next + " — <YYYY-MM-DD>");
368
264
  }
369
265
 
370
- // The `## <next>` heading exists; confirm the section extracts cleanly and
371
- // passes the operator-facing lint (the release workflow publishes it verbatim
372
- // as the GitHub Release body). Fail fast here rather than at the gates phase.
266
+ // The release workflow publishes this section verbatim as the GitHub Release body.
373
267
  _run("node", ["scripts/check-changelog-extract.js", next]);
374
268
 
375
269
  _writeJsonVersion("package.json", next);
@@ -379,22 +273,14 @@ function cmdPrepare(opts) {
379
273
  _regenArtifacts();
380
274
 
381
275
  _section("test-count baseline");
382
- // Check BEFORE refreshing. `--update-baseline` writes whatever it observes,
383
- // so calling it unconditionally rebaselined a shrunken suite downward and the
384
- // shrinkage gate — which runs later, in `gates` — then compared the new count
385
- // against itself and passed. Releasing was the one path that disarmed the
386
- // guard, which is the path it exists to guard. Run the gate first so a drop
387
- // stops the release here; refresh only once it has agreed nothing was lost,
388
- // which still captures growth. A deliberate removal is re-baselined by hand
389
- // with the command the gate's own failure message prints.
276
+ // Check BEFORE refreshing: `--update-baseline` writes whatever it observes, so
277
+ // refreshing first rebaselines a shrunken suite downward and the shrinkage gate
278
+ // in `gates` then compares the new count against itself.
390
279
  _run("node", ["scripts/check-test-count.js"]);
391
280
  _run("node", ["scripts/check-test-count.js", "--update-baseline"]);
392
281
 
393
282
  _section("codebase-patterns currency (advisory)");
394
- // Flag when the upstream pattern catalog (the sibling blamejs codebase-
395
- // patterns test) has grown a class exceptd hasn't triaged yet — the same
396
- // forcing function the actions/vendor currency checks give those surfaces.
397
- // Advisory: never blocks; skips cleanly when the sibling repo is absent.
283
+ // Flags a pattern class the sibling blamejs catalog grew; never blocks.
398
284
  _run("node", ["scripts/check-codebase-patterns-currency.js"], { allowFail: true });
399
285
 
400
286
  console.log("\nnext: node scripts/release.js gates");
@@ -402,9 +288,6 @@ function cmdPrepare(opts) {
402
288
 
403
289
  function cmdGates() {
404
290
  _section("gates");
405
- // predeploy runs the full suite + every publish gate (signatures, catalog
406
- // schema, snapshot, lint, sbom currency, indexes, tarball verify, diff
407
- // coverage, ...). It is the authoritative pre-publish check.
408
291
  _run("npm", ["run", "predeploy"]);
409
292
  _ok("predeploy gates passed");
410
293
  console.log("\nnext: node scripts/release.js commit");
@@ -416,8 +299,7 @@ function cmdCommit() {
416
299
  var branch = _releaseBranchFor(next);
417
300
  var current = _gitBranch();
418
301
 
419
- // Resumable: a prior commit that failed after `checkout -b` leaves the
420
- // branch in place — switch to it instead of refusing.
302
+ // Resumable: a failed commit leaves the branch in place, so switch to it.
421
303
  if (current === branch) {
422
304
  _ok("already on " + branch + " (resume mode)");
423
305
  } else if (current === "main") {
@@ -433,8 +315,7 @@ function cmdCommit() {
433
315
  throw new Error("release: commit must run on main or " + branch + " (on " + current + ")");
434
316
  }
435
317
 
436
- // If HEAD already carries this release's commit, don't double-commit —
437
- // just verify the signature.
318
+ // HEAD already carrying this release's commit means verify, not re-commit.
438
319
  var headSubject = _capture("git", ["log", "-1", "--pretty=%s"]).stdout;
439
320
  if (headSubject.indexOf("v" + next + ":") === 0) {
440
321
  _ok("HEAD already carries a v" + next + " commit (resume mode)");
@@ -443,8 +324,6 @@ function cmdCommit() {
443
324
  return;
444
325
  }
445
326
 
446
- // Compose the commit body from the CHANGELOG section — the operator can
447
- // amend, but the default mirrors the shipped notes.
448
327
  var section = _changelogSection(next);
449
328
  var subject = _releaseSubject(next, section);
450
329
  var bodyPath = path.join(ROOT, ".scratch");
@@ -487,24 +366,16 @@ function cmdWatch() {
487
366
  if (!prNum) throw new Error("release: no open PR for " + branch);
488
367
  console.log("PR #" + prNum);
489
368
 
490
- // gh pr checks --watch blocks until checks settle. allowFail so a flaky
491
- // run doesn't throw before we get to inspect + rerun it.
369
+ // Blocks until the checks settle; allowFail so failures are inspected below.
492
370
  _run("gh", ["pr", "checks", prNum, "--watch"], { allowFail: true });
493
371
 
494
- // Gate on check CONCLUSIONS, not only review threads. A red required check
495
- // leaves the PR BLOCKED at merge, so surfacing failures here (the whole
496
- // point of the watch phase) beats advancing to "next: merge" and letting
497
- // cmdMerge reject it. Bucket is gh's normalized verdict: pass / fail /
498
- // pending / skipping / cancel.
372
+ // Gate on check CONCLUSIONS, not only review threads. Bucket is gh's normalized
373
+ // verdict — pass / fail / pending / skipping / cancel.
499
374
  var checksRaw = _capture("gh", ["pr", "checks", prNum, "--json", "name,bucket,link"]).stdout;
500
375
  var checks = [];
501
376
  try { checks = JSON.parse(checksRaw || "[]"); } catch (_e) { checks = []; }
502
- // An empty or still-pending check set is NOT a pass. `gh pr checks --watch`
503
- // returns immediately when no check has registered yet — the workflows are
504
- // still being scheduled — and the filters below then find nothing wrong,
505
- // so the phase printed "next: merge" for a PR whose CI had not started.
506
- // Absence of a failure is not evidence of success; require that checks exist
507
- // and have settled before reading anything into them.
377
+ // An empty or still-pending check set is NOT a pass: `gh pr checks --watch`
378
+ // returns immediately while the workflows are still being scheduled.
508
379
  if (checks.length === 0) {
509
380
  console.log("\nno checks have registered on this PR yet — they are probably still scheduling.");
510
381
  console.log("Wait a moment, then re-run: node scripts/release.js watch");
@@ -526,8 +397,7 @@ function cmdWatch() {
526
397
  process.exit(3); // allow:process-exit-after-stdout-write — maintainer-run release orchestrator; the guidance line above is human-read on a TTY, not a piped result channel
527
398
  }
528
399
 
529
- // CodeQL SAST gate — a standing per-release step. An open, un-triaged CodeQL
530
- // alert blocks the release the same way an unresolved codex thread does.
400
+ // An open CodeQL alert blocks the release like an unresolved review thread.
531
401
  var codeqlAlerts = _openCodeqlAlerts(prNum);
532
402
  if (codeqlAlerts === null) {
533
403
  console.log("\nnote: could not query CodeQL alerts (code scanning unavailable / API error) — " +
@@ -578,15 +448,13 @@ function cmdMerge() {
578
448
  throw new Error("release: PR #" + prNum + " not mergeable (state=" +
579
449
  state.mergeStateStatus + " mergeable=" + state.mergeable + ")");
580
450
  }
581
- // Re-check threads right before merge — a reviewer (or Codex) can open one
582
- // between watch and merge.
451
+ // A reviewer can open a thread between watch and merge.
583
452
  var unresolved = _unresolvedThreads(prNum);
584
453
  if (unresolved.length > 0) {
585
454
  throw new Error("release: refusing to merge PR #" + prNum + " — " +
586
455
  unresolved.length + " unresolved review thread(s); run watch again");
587
456
  }
588
- // Solo-maintainer protection requires 0 approvals; --admin satisfies the
589
- // remaining required checks gate without a second reviewer.
457
+ // Solo-maintainer protection requires 0 approvals; --admin covers required checks.
590
458
  _run("gh", ["pr", "merge", prNum, "--squash", "--admin", "--delete-branch"]);
591
459
  _ok("PR #" + prNum + " squash-merged");
592
460
 
@@ -601,10 +469,8 @@ function cmdTag() {
601
469
  var next = _readJsonVersion("package.json");
602
470
  var tag = "v" + next;
603
471
 
604
- // GUARD against tag-on-stale-HEAD: a transient git index lock can leave
605
- // local HEAD behind origin/main after a merge, so a tag would land on the
606
- // wrong commit and the release workflow's version-match gate would reject
607
- // it (burning a version slot, since the v* ruleset blocks tag rewrites).
472
+ // GUARD against tag-on-stale-HEAD: a tag on the wrong commit burns a version
473
+ // slot, since the v* ruleset blocks tag rewrites.
608
474
  try { fs.rmSync(path.join(ROOT, ".git", "index.lock"), { force: true }); } catch (_e) { /* ignore */ }
609
475
  _run("git", ["fetch", "origin", "main"]);
610
476
  var local = _capture("git", ["rev-parse", "HEAD"]).stdout;
@@ -628,12 +494,9 @@ function cmdTag() {
628
494
  }
629
495
  _ok("GUARD passed (HEAD==origin/main, 3-version match, no existing tag)");
630
496
 
631
- // `-s` forces a signed tag regardless of whether tag.gpgsign is set in
632
- // config; `-a` would silently produce an UNSIGNED annotated tag when the
633
- // config is absent, and main's tag ruleset / the release provenance both
634
- // expect a signature. Verify BEFORE pushing so an unsigned tag never
635
- // reaches origin (the v* ruleset blocks tag rewrites, so a bad push would
636
- // burn the version slot).
497
+ // `-s` forces a signed tag whatever tag.gpgsign is set to; `-a` silently
498
+ // produces an UNSIGNED annotated tag when the config is absent. Verify before
499
+ // pushing — an unsigned tag on origin burns the version slot.
637
500
  _run("git", ["tag", "-s", tag, "-m", tag]);
638
501
  var verify = _capture("git", ["tag", "-v", tag]);
639
502
  if (verify.stderr.indexOf("Good") === -1 && verify.stdout.indexOf("Good") === -1) {
@@ -653,13 +516,9 @@ function cmdRelease() {
653
516
  var next = _readJsonVersion("package.json");
654
517
 
655
518
  _section("release workflow");
656
- // Select the release.yml run created by THIS tag push — not merely the
657
- // newest run. gh exposes the tag ref as headBranch for tag-triggered runs;
658
- // event=="push" excludes workflow_dispatch runs. Filtering by headBranch==tag
659
- // uniquely identifies this run even when a prior tag points at the same
660
- // commit (so it's preferable to headSha matching, and needs no extra git
661
- // call). Bounded retries cover the few-seconds window GitHub needs to
662
- // register the run after the tag push.
519
+ // Selects the release.yml run created by THIS tag push: gh reports the tag ref
520
+ // as headBranch for tag-triggered runs, and event=="push" excludes
521
+ // workflow_dispatch. The bounded retries cover GitHub registering the run.
663
522
  var tag = "v" + next;
664
523
  var runId = "";
665
524
  for (var _i = 0; _i < 30 && !runId; _i++) {
@@ -667,20 +526,13 @@ function cmdRelease() {
667
526
  "--event=push", "--json", "databaseId,headBranch,event",
668
527
  "--jq", '[.[] | select(.headBranch=="' + tag + '")] | sort_by(.databaseId) | last | .databaseId']).stdout;
669
528
  if (!runId && _i < 29) {
670
- // Short bounded wait between polls — the run usually appears within
671
- // a few seconds of the tag push.
672
529
  _spawn(process.execPath, ["-e", "setTimeout(function(){},2000)"], { stdio: "ignore" });
673
530
  }
674
531
  }
675
532
  if (runId) {
676
533
  _run("gh", ["run", "watch", runId, "--exit-status"], { allowFail: true });
677
- // Read the conclusion, distinguishing "the workflow failed" from "the
678
- // lookup failed". Conflating them reported conclusion=(unknown) on a
679
- // release whose three jobs had all succeeded and whose package was already
680
- // on npm — a false alarm on a good publish, twice, because an empty stdout
681
- // (transient API error, or a run still settling) was treated as a verdict.
682
- // Retry a few times before concluding anything: the value we want is a
683
- // terminal state, and asking again is cheap next to a wrong answer.
534
+ // "The workflow failed" and "the lookup failed" are different answers: an
535
+ // empty stdout is the second, not a verdict. Retry for a terminal state.
684
536
  var concl = "";
685
537
  var lookupOk = false;
686
538
  for (var _c = 0; _c < 5; _c++) {
@@ -688,11 +540,8 @@ function cmdRelease() {
688
540
  "--jq", ".status + \"|\" + (.conclusion // \"\")"]);
689
541
  if (rv.status === 0 && rv.stdout) {
690
542
  var parts = rv.stdout.split("|");
691
- // Accept only a completed run WITH a non-empty conclusion. An
692
- // in-progress run legitimately reports an empty one, and a completed
693
- // run can briefly report one too while the value settles — taking
694
- // either as an answer reproduces the false failed-publish this retry
695
- // exists to prevent. Keep polling until a real verdict appears.
543
+ // Only a completed run WITH a non-empty conclusion counts: an in-progress
544
+ // run reports an empty one, as does a completed one before it settles.
696
545
  if (parts[0] === "completed" && parts[1]) { concl = parts[1]; lookupOk = true; break; }
697
546
  }
698
547
  if (_c < 4) _spawn(process.execPath, ["-e", "setTimeout(function(){},3000)"], { stdio: "ignore" });
@@ -716,20 +565,14 @@ function cmdRelease() {
716
565
  _section("verify npm");
717
566
  var npmVersion = _capture("npm", ["view", PKG_NAME, "version"]).stdout;
718
567
  console.log("npm " + PKG_NAME + ": " + (npmVersion || "(unable to query)") + " (expected " + next + ")");
719
- // Require a POSITIVE confirmation: the queried npm version must equal `next`.
720
- // The hard failure is asserted at the end of the phase (after the tarball
721
- // verify). An empty stdout (registry/auth/network failure) is treated as a
722
- // mismatch — an unconfirmable publish is a failure, not a success.
568
+ // Positive confirmation only: an empty stdout is a mismatch, not a pass. The
569
+ // hard failure is asserted at the end of the phase, after the tarball verify.
723
570
  if (npmVersion === next) _ok("npm matches " + next);
724
571
 
725
572
  _section("fresh-tarball signature verify");
726
- // Verify against the EXACT bytes a downstream consumer installs — the
727
- // source-tree verify is necessary-but-insufficient (the v0.11.x signature
728
- // regression was invisible until a fresh install). Packs, extracts, and
729
- // runs lib/verify.js against the extracted tree. This is the load-bearing
730
- // post-publish check: a broken artifact/signature here means the release
731
- // is broken, so it is a HARD gate — _run (no allowFail) throws on failure
732
- // and the phase exits non-zero rather than reporting a clean release.
573
+ // Verifies the EXACT bytes a downstream consumer installs. A source-tree verify
574
+ // is necessary but not sufficient — a signature can diverge at pack time. HARD
575
+ // gate: _run throws rather than let the phase call the release clean.
733
576
  var wrapper = path.join(ROOT, "scripts", "verify-shipped-tarball.js");
734
577
  if (fs.existsSync(wrapper)) {
735
578
  _run("node", [wrapper]);
@@ -738,12 +581,8 @@ function cmdRelease() {
738
581
  throw new Error("release: scripts/verify-shipped-tarball.js missing — cannot verify the shipped artifact");
739
582
  }
740
583
 
741
- // Require a positive npm confirmation after the workflow finished. A version
742
- // that is empty (query failed) OR != next is not mere propagation lag — fail
743
- // so a stalled/failed/unconfirmable publish can't read as a completed
744
- // release. (A genuinely in-flight publish is caught by the workflow-
745
- // conclusion check above; by the time we query npm post-watch the version
746
- // should be live.) The message reports the value actually queried.
584
+ // The workflow has finished by now, so an empty or mismatched version is not
585
+ // propagation lag — it must not read as a completed release.
747
586
  if (npmVersion !== next) {
748
587
  throw new Error("release: npm shows " + (npmVersion || "(unable to query)") + " but expected " + next +
749
588
  " — publish did not complete or could not be confirmed; re-check release.yml before treating the release as done");
@@ -797,8 +636,6 @@ function cmdHelp() {
797
636
  console.log("Patch is the default. --minor is a deliberate, explicit choice.");
798
637
  }
799
638
 
800
- // ---- Dispatch ------------------------------------------------------------
801
-
802
639
  var sub = process.argv[2] || "help";
803
640
  var opts = {
804
641
  minor: process.argv.slice(3).indexOf("--minor") !== -1,