@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,33 +1,8 @@
1
1
  "use strict";
2
2
  /**
3
- * scripts/predeploy.js
4
- *
5
- * Local mirror of the CI pre-deployment gate sequence. Runs every gate
6
- * the `.github/workflows/ci.yml` workflow runs, in order. Each gate is
7
- * isolated — a failure does not short-circuit the rest, so a single run
8
- * surfaces all problems instead of just the first one (matches the CI
9
- * shape where each job runs independently).
10
- *
11
- * Run before pushing to main or opening a PR:
12
- * npm run predeploy
13
- *
14
- * Exit code:
15
- * 0 — all gates passed
16
- * 1 — one or more gates failed (per-gate output already printed)
17
- * 2 — runner-level error (missing script, fork failure, etc.)
18
- *
19
- * Single-source-of-truth: the GATES list below mirrors the job sequence
20
- * in .github/workflows/ci.yml. Test coverage in tests/predeploy.test.js
21
- * asserts the two stay in sync.
22
- *
23
- * when the manifest-snapshot gate fails, the fix is NOT to
24
- * run `npm run refresh-snapshot` blindly. The refresh script now refuses
25
- * unless the operator passes `--commit-only` or sets
26
- * EXCEPTD_SNAPSHOT_AUDIT_ACK=1. This is intentional: a failing snapshot
27
- * gate means a breaking change was detected, and an accidental refresh
28
- * would silently rewrite the baseline. Read the breaking-change list
29
- * first, then run `node scripts/refresh-manifest-snapshot.js --commit-only`
30
- * if the change is intentional.
3
+ * Local mirror of the CI pre-deployment gate sequence. Gates are isolated, so one
4
+ * failure does not short-circuit the rest and a single run surfaces every problem.
5
+ * Exit 0 all passed, 1 one or more failed, 2 runner-level error.
31
6
  */
32
7
 
33
8
  const { execFileSync } = require("child_process");
@@ -36,10 +11,7 @@ const fs = require("fs");
36
11
 
37
12
  const ROOT = path.join(__dirname, "..");
38
13
 
39
- // Ordered list of CI gates. Each entry: { name, command, args, ciJobName }.
40
- // ciJobName matches the `name:` field of the corresponding job in
41
- // .github/workflows/ci.yml (or scorecard.yml). Used by the workflow-sync
42
- // test to assert the two never drift.
14
+ // Ordered CI gates; `ciJobName` matches the job `name:` in .github/workflows/ci.yml.
43
15
  const GATES = [
44
16
  {
45
17
  name: "Verify skill signatures (Ed25519)",
@@ -51,39 +23,19 @@ const GATES = [
51
23
  {
52
24
  name: "Run tests (node:test)",
53
25
  command: process.execPath,
54
- // Glob form rather than a directory arg: Node 25.x on Windows
55
- // resolves a bare directory path through the module loader before
56
- // the test runner sees it, which fails for a working dir that
57
- // sits inside a path containing parentheses (e.g. Dropbox).
58
- //
59
- // --test-concurrency=1 forces sequential file execution. Several
60
- // test files (build-incremental, indexes-v070, refresh-*) touch
61
- // shared filesystem state under data/_indexes/ + refresh-report.json
62
- // + skill bodies; running in parallel produces flaky races. Sequential
63
- // is ~1.5s slower locally but eliminates the false negative we hit
64
- // on the Linux CI runner in the v0.9.0 release attempt.
26
+ // Glob, not a directory arg: on Windows a bare directory resolves through the
27
+ // module loader and fails under a path containing parentheses. --test-concurrency=1
28
+ // is required — build-incremental, indexes-v070 and refresh-* share state.
65
29
  args: ["--test", "--test-concurrency=1", "tests/*.test.js"],
66
30
  ciJobName: "Tests",
67
31
  },
68
32
  {
69
33
  name: "Validate CVE catalog schema + zero-day learning coverage",
70
34
  command: process.execPath,
71
- // --strict promotes the deferred warning checks (cross-catalog ref
72
- // resolution, strict CVSS-vector prefix, KEV-date-required, Hard-Rule-#14
73
- // IoCs) to hard failures so they block a release rather than scrolling
74
- // past. Auto-imported drafts stay exempt.
35
+ // --strict promotes deferred warnings to hard failures; drafts stay exempt.
75
36
  args: [path.join(ROOT, "lib", "validate-cve-catalog.js"), "--strict"],
76
37
  ciJobName: "Data integrity (catalog + manifest snapshot)",
77
38
  },
78
- // the "validate-cves --offline --no-fail" and
79
- // "validate-rfcs --offline --no-fail" gates were enumeration-only sanity
80
- // checks: `--no-fail` forced them to always exit 0, so they never blocked
81
- // a release on a real catalog problem. The deep catalog validation is
82
- // already performed by the gate above (`lib/validate-cve-catalog.js`),
83
- // including cross-catalog reference resolution after this same audit.
84
- // Keeping the no-op gates as predeploy steps inflated the gate count for
85
- // no marginal value and risked false confidence ("X gates passed"). They
86
- // are removed in v0.12.14; document the removal in CHANGELOG.
87
39
  {
88
40
  name: "Manifest snapshot gate (breaking-change detector)",
89
41
  command: process.execPath,
@@ -97,13 +49,7 @@ const GATES = [
97
49
  ciJobName: "Lint skill files",
98
50
  },
99
51
  {
100
- // Informational — surfaces the forward_watch horizon across all skills.
101
- // an exit code of 0 means "ok", 1 means "items present
102
- // (informational)", 2+ means a runtime error in the gate itself.
103
- // The runner now distinguishes the two: 0/1 stay informational, 2+
104
- // surface as a real failure. Pre-fix, any non-zero exit was rolled up
105
- // as informational, which hid crashes (a 137 OOM looked the same as
106
- // "found 12 items to review").
52
+ // Exit 0 ok, 1 "items present"; 2+ is a gate runtime error and fails the run.
107
53
  name: "Forward-watch aggregator (informational)",
108
54
  command: process.execPath,
109
55
  args: [
@@ -145,11 +91,7 @@ const GATES = [
145
91
  ciJobName: "Data integrity (catalog + manifest snapshot)",
146
92
  },
147
93
  {
148
- // v0.12.3 — packs the tarball, extracts it, runs Ed25519 verify on the
149
- // EXTRACTED tree. Catches the class of bug where verify-on-source-tree
150
- // passes (38/38) but verify-on-shipped-tarball fails (0/38) because
151
- // something between sign and pack swapped keys/public.pem. Every release
152
- // v0.11.x through v0.12.2 shipped this regression invisibly.
94
+ // Verifies the EXTRACTED tarball: a step between sign and pack can swap keys/public.pem.
153
95
  name: "Verify shipped tarball (sign + pack + extract + verify round-trip)",
154
96
  command: process.execPath,
155
97
  args: [path.join(ROOT, "scripts", "verify-shipped-tarball.js")],
@@ -157,170 +99,100 @@ const GATES = [
157
99
  requiresKeys: true,
158
100
  },
159
101
  {
160
- // AGENTS.md hard rule #15 (e2e no-MVP). Every diff that touches a
161
- // CLI verb, CLI flag, lib/orchestrator/scripts export, playbook
162
- // indicator, or CVE iocs field must land with a covering test
163
- // reference in the same PR. The analyzer parses git diff against
164
- // origin/main, classifies each change shape, and fails if a covered
165
- // surface lacks a test literal anywhere under tests/. Blocking — a
166
- // covered surface change without a covering test fails the gate.
102
+ // AGENTS.md Hard Rule #15: a CLI/export/indicator/iocs diff lands with a covering test.
167
103
  name: "Diff coverage (feature changes require test coverage)",
168
104
  command: process.execPath,
169
105
  args: [path.join(ROOT, "scripts", "check-test-coverage.js")],
170
106
  ciJobName: "Diff coverage",
171
107
  },
172
108
  {
173
- // Validate every playbook in data/playbooks/ against the JSON schema
174
- // + cross-playbook + cross-catalog references. v0.12.12 first wired
175
- // this as informational so the patch-class release could land without
176
- // retroactively breaking schema-drift cases; v0.13.0 flips it to
177
- // required because the 20-playbook canonical set (including the 4
178
- // v0.13.0 additions) all validate cleanly.
179
109
  name: "Validate playbooks (schema + cross-refs)",
180
110
  command: process.execPath,
181
111
  args: [path.join(ROOT, "lib", "validate-playbooks.js"), "--strict"],
182
112
  ciJobName: "Validate playbooks",
183
113
  },
184
114
  {
185
- // v0.13.2: refuse silent test-set shrinkage. Static-counts `test(`
186
- // declarations across tests/*.test.js and compares to the pinned
187
- // baseline in tests/.test-count-baseline.json. Catches the class
188
- // of regression where a test file gets accidentally deleted, a
189
- // skip-all lands without review, or a misnamed file slips through
190
- // the glob. The baseline is operator-refreshed on releases that
191
- // intentionally add many new tests; --update-baseline rewrites it.
115
+ // Refuses silent test-set shrinkage: a deleted file, a skip-all or a misnamed
116
+ // file cannot pass. --update-baseline rewrites the pinned baseline.
192
117
  name: "Test-count baseline (no silent shrinkage)",
193
118
  command: process.execPath,
194
119
  args: [path.join(ROOT, "scripts", "check-test-count.js")],
195
- // Folds under the existing Data integrity CI job rather than a
196
- // dedicated job — the check is fast (~70ms) static analysis and
197
- // shares the integrity-tier framing with manifest-snapshot etc.
198
120
  ciJobName: "Data integrity (catalog + manifest snapshot)",
199
121
  },
200
122
  {
201
- // v0.13.21: catalog-gap budget gate. Runs the seven extended
202
- // detection classes added in v0.13.21 (content-quality,
203
- // temporal-staleness, logical-consistency, cross-ref-completeness,
204
- // schema-evolution, operator-action-sla, unused-orphan) against
205
- // the shipped catalog and fails if any class regresses beyond its
206
- // documented budget. Mirrors the budget enforced by
207
- // tests/shipped-catalog-integrity.test.js so the regression
208
- // surfaces in BOTH the gate-summary table AND the test output.
123
+ // Fails if a detection class regresses past its budget. The same budget is in
124
+ // tests/shipped-catalog-integrity.test.js; the two must not drift.
209
125
  name: "Catalog-gap budget (v0.13.21 extended detection classes)",
210
126
  command: process.execPath,
211
127
  args: [path.join(ROOT, "scripts", "check-catalog-gap-budget.js")],
212
128
  ciJobName: "Data integrity (catalog + manifest snapshot)",
213
129
  },
214
130
  {
215
- // Global-first framework-gap coverage gate (AGENTS.md Hard Rule #5).
216
- // Every curated CVE must declare a framework_control_gaps statement for
217
- // all five jurisdiction buckets (NIST, EU, UK, AU, ISO). Drafts are
218
- // exempt. Prevents a US-centric subset from shipping in the offline
219
- // catalog's framework-gap output for multi-jurisdiction operators.
131
+ // AGENTS.md Hard Rule #5: every curated CVE declares framework_control_gaps for
132
+ // all five jurisdiction buckets (NIST, EU, UK, AU, ISO). Drafts exempt.
220
133
  name: "Framework-gap jurisdiction coverage (Hard Rule #5)",
221
134
  command: process.execPath,
222
135
  args: [path.join(ROOT, "scripts", "check-framework-gap-coverage.js")],
223
136
  ciJobName: "Data integrity (catalog + manifest snapshot)",
224
137
  },
225
138
  {
226
- // TTP reference-integrity gate. The two pinned MITRE catalogs define the
227
- // techniques; every other file naming one is referring into them. MITRE
228
- // retires and renumbers between releases, and a reference living outside
229
- // the source catalogs is never dereferenced at runtime — so a stale id
230
- // keeps rendering in operator output, pointing at a page that no longer
231
- // resolves and leaving any control mapped to it orphaned (Hard Rule #4).
232
- // Resolves against the pin rather than the network, so it can block.
139
+ // MITRE retires and renumbers technique ids, and a reference outside the two
140
+ // pinned catalogs is never dereferenced at runtime, orphaning its control.
233
141
  name: "TTP reference integrity (no orphaned technique ids)",
234
142
  command: process.execPath,
235
143
  args: [path.join(ROOT, "scripts", "check-ttp-references.js")],
236
144
  ciJobName: "Data integrity (catalog + manifest snapshot)",
237
145
  },
238
146
  {
239
- // EPSS pair-consistency gate. A CVE's epss_score and epss_percentile come
240
- // from the same daily publication, where the percentile is the score's
241
- // rank — so sorting a publication's entries by score must sort them by
242
- // percentile too. Refreshing one field without the other leaves an entry
243
- // that passes every range and type check while ranking a CVE on a score
244
- // that no longer supports it, which is precisely the input operators
245
- // prioritise by. The ordering check catches that offline.
147
+ // epss_percentile is the score's rank within one daily publication, so a
148
+ // publication's entries must sort the same way by both.
246
149
  name: "EPSS score/percentile consistency",
247
150
  command: process.execPath,
248
151
  args: [path.join(ROOT, "scripts", "check-epss-consistency.js")],
249
152
  ciJobName: "Data integrity (catalog + manifest snapshot)",
250
153
  },
251
154
  {
252
- // Version-tag drift gate. Compares the tracked tree against a
253
- // baseline snapshot of pre-existing `// vX.Y.Z` comments and
254
- // `*-vX_Y_Z.test.js` filenames. Fails on NEW additions outside
255
- // the authoritative version surfaces (package.json /
256
- // manifest.json / CHANGELOG headings / git tags). The full rule
257
- // is documented at the top of check-version-tags.js; refresh the
258
- // baseline after an organic cleanup via
259
- // `node scripts/check-version-tags.js --update-baseline`.
155
+ // Fails on NEW `// vX.Y.Z` comments and `*-vX_Y_Z.test.js` filenames: versions
156
+ // live in package.json, manifest.json, CHANGELOG headings and git tags.
260
157
  name: "Version-tag drift (no new phase residue)",
261
158
  command: process.execPath,
262
159
  args: [path.join(ROOT, "scripts", "check-version-tags.js")],
263
160
  ciJobName: "Data integrity (catalog + manifest snapshot)",
264
161
  },
265
162
  {
266
- // AGENTS.md collector enumeration drift gate. Catches the case
267
- // where lib/collectors/ gets a new module but AGENTS.md's
268
- // "<N> reference collectors ship today (...)" paragraph isn't
269
- // bumped (or vice versa). The paragraph is the canonical source
270
- // for AI-agent consumers; drift produces stale enumeration.
163
+ // Keeps lib/collectors/ and AGENTS.md's collector-enumeration paragraph in step.
271
164
  name: "AGENTS.md collector enumeration drift",
272
165
  command: process.execPath,
273
166
  args: [path.join(ROOT, "scripts", "check-agents-md-collectors.js")],
274
167
  ciJobName: "Data integrity (catalog + manifest snapshot)",
275
168
  },
276
169
  {
277
- // Codebase-pattern gate. Blocks the code-shape bug classes that
278
- // recurred across releases: a library-callable function that writes to
279
- // stdout then calls process.exit() (truncates the buffered write when
280
- // piped — the stdout-flush-truncation class), and a stale/typo'd `// allow:` marker.
281
- // dynamic-RegExp construction is surfaced warn-only this release. The
282
- // exception mechanism + the "owned elsewhere" boundary are documented in
283
- // the script header.
170
+ // Blocks a library-callable function that writes to stdout then calls
171
+ // process.exit(), and an orphaned allow marker. Dynamic RegExp is warn-only.
284
172
  name: "Codebase-pattern gates (stdout-flush, dynamic RegExp, bidi codepoints, orphan markers)",
285
173
  command: process.execPath,
286
174
  args: [path.join(ROOT, "scripts", "check-codebase-patterns.js")],
287
175
  ciJobName: "Data integrity (catalog + manifest snapshot)",
288
176
  },
289
177
  {
290
- // Test-subject coverage gate. Bidirectional: every tests/<x>.test.js must
291
- // be named after a real SUBJECT the codebase has (a module / CLI verb /
292
- // CVE id / playbook / data primitive / repo artifact), and every such
293
- // subject must have a test. Blocks the naming drift that lets a test be
294
- // filed under a version/finding label (where downstream readers can't find
295
- // it) and surfaces any module/playbook that ships without a test. Derived
296
- // dynamically from the source tree, so the list is never hand-maintained.
178
+ // Bidirectional: every tests/<x>.test.js names a real subject, and every subject
179
+ // has a test. Derived from the source tree, never hand-maintained.
297
180
  name: "Test-subject coverage (every test maps to a subject; every subject has a test)",
298
181
  command: process.execPath,
299
182
  args: [path.join(ROOT, "scripts", "check-test-subjects.js")],
300
183
  ciJobName: "Data integrity (catalog + manifest snapshot)",
301
184
  },
302
185
  {
303
- // Release-notes extract + quality gate. Runs the same `## <version>`
304
- // CHANGELOG extraction the release workflow publishes as the GitHub
305
- // Release body, and lints it for operator-facing quality (no internal
306
- // phase/pass/slice narrative, no agent-dispatch / conversation residue,
307
- // no tautological green claims). A malformed or internal-narrative section
308
- // fails here rather than shipping as the public release body / falling
309
- // back to the generic "Release of v<version>." line.
186
+ // Runs the same `## <version>` CHANGELOG extraction the release workflow
187
+ // publishes, then lints it, so a bad section fails here rather than publicly.
310
188
  name: "Release-notes extract + operator-facing lint (CHANGELOG section)",
311
189
  command: process.execPath,
312
190
  args: [path.join(ROOT, "scripts", "check-changelog-extract.js")],
313
191
  ciJobName: "Data integrity (catalog + manifest snapshot)",
314
192
  },
315
193
  {
316
- // Version-bump cadence gate. Patch is the ONLY default bump; a minor or
317
- // major requires an explicit, committed authorization
318
- // (tests/.version-bump-ack.json naming the exact target version).
319
- // Compares the top two `## X.Y.Z` CHANGELOG headings — hermetic, so it
320
- // enforces identically locally and in the release.yml validate job. A
321
- // hand-bumped minor without the ack fails here rather than shipping a
322
- // wrong version number (the class of error behind two mis-versioned
323
- // releases). Full contract at the top of check-version-bump.js.
194
+ // Patch is the ONLY default bump; a minor or major needs a committed
195
+ // tests/.version-bump-ack.json naming the exact target version.
324
196
  name: "Version-bump cadence (patch-only default)",
325
197
  command: process.execPath,
326
198
  args: [path.join(ROOT, "scripts", "check-version-bump.js")],
@@ -341,9 +213,7 @@ function runGate(gate) {
341
213
  }
342
214
  }
343
215
  const t0 = Date.now();
344
- // spawn the child with piped stdio + tee to the parent so we
345
- // can count `WARN ` lines for the summary table. We still want the live
346
- // output, so each chunk is forwarded as it arrives.
216
+ // Piped stdio, forwarded on: the summary needs a WARN count from the output.
347
217
  const { spawnSync } = require("child_process");
348
218
  const r = spawnSync(gate.command, gate.args, {
349
219
  cwd: ROOT,
@@ -353,9 +223,7 @@ function runGate(gate) {
353
223
  const durationMs = Date.now() - t0;
354
224
  if (r.stdout) process.stdout.write(r.stdout);
355
225
  if (r.stderr) process.stderr.write(r.stderr);
356
- // Count WARN-labelled lines in the combined stream so the summary table
357
- // can surface them. Lint / validate output uses "WARN " at line start;
358
- // count both the table form and an inline "[warn]" form.
226
+ // Both forms count: "WARN" at line start, and an inline "[warn]".
359
227
  const combined = (r.stdout || "") + (r.stderr || "");
360
228
  const warnCount = (
361
229
  combined.match(/^WARN\b/gm) || []
@@ -365,23 +233,13 @@ function runGate(gate) {
365
233
  if (r.status === 0) {
366
234
  return { status: "passed", durationMs, warnCount };
367
235
  }
368
- // gates may declare informationalMaxExitCode to distinguish
369
- // "soft signal" (exit codes 0..N) from "crash" (> N). Default behaviour
370
- // for an informational gate without that field stays the same.
236
+ // informationalMaxExitCode separates a soft signal (0..N) from a crash; absent = any.
371
237
  if (gate.informational) {
372
238
  const ceil = typeof gate.informationalMaxExitCode === "number"
373
239
  ? gate.informationalMaxExitCode
374
240
  : Infinity;
375
- // A spawn failure (spawnSync returns r.error set, status:null, signal:null —
376
- // e.g. the gate command is missing / ENOENT / EACCES) is a crash, not an
377
- // informational soft-signal. So is a signal kill (status:null with r.signal
378
- // set — e.g. a 137 OOM kill) and a status that exceeds the soft-signal
379
- // ceiling. Without surfacing the spawn-error case, an informational gate
380
- // that never even ran fell through to "informational" and the release
381
- // proceeded as if the gate had merely produced advisory output. The
382
- // status===null && !signal case (no error object, but the process never
383
- // produced an exit code) is treated the same way — a gate that did not
384
- // exit cleanly cannot be classified as a soft signal.
241
+ // A gate that never ran cleanly is a crash, not a soft signal: spawn failure,
242
+ // a signal kill (137 OOM), and a status above the ceiling all fail here.
385
243
  const spawnFailed = !!r.error || (r.status === null && !r.signal);
386
244
  if (r.error || r.signal || spawnFailed || (r.status !== null && r.status > ceil)) {
387
245
  return {
@@ -442,7 +300,6 @@ function main() {
442
300
  }
443
301
  }
444
302
 
445
- // Summary table.
446
303
  process.stdout.write("\n=== Pre-deploy summary ===\n");
447
304
  const widest = results.reduce(
448
305
  (n, r) => Math.max(n, r.gate.name.length),
@@ -459,10 +316,7 @@ function main() {
459
316
  : "✗";
460
317
  const timing = fmtMs(outcome.durationMs);
461
318
  const timingSuffix = timing ? ` (${timing})` : "";
462
- // F21 — surface WARN counts so a gate that "passed (3 warnings)" is
463
- // distinguishable from one that passed cleanly. Pre-fix, warnings
464
- // printed by individual gates (validate-cve-catalog, lint-skills,
465
- // validate-playbooks) scrolled past invisible in the summary.
319
+ // A gate that "passed (3 warnings)" must be distinguishable from a clean pass.
466
320
  const warnSuffix =
467
321
  outcome.warnCount && outcome.warnCount > 0
468
322
  ? ` (${outcome.warnCount} warning${outcome.warnCount === 1 ? "" : "s"})`
@@ -1,32 +1,16 @@
1
1
  "use strict";
2
2
  /**
3
- * scripts/refresh-manifest-snapshot.js
3
+ * Captures the public skill surface from manifest.json into
4
+ * manifest-snapshot.json. Run it AFTER an intentional surface change and commit
5
+ * the new snapshot alongside that change — never to "fix" a failing
6
+ * check-manifest-snapshot.js gate, whose breaking-change list is the thing to
7
+ * read first. A breaking change is a surface narrowing every downstream
8
+ * consumer needs to know about.
4
9
  *
5
- * Captures the current public skill surface from manifest.json and
6
- * writes it to manifest-snapshot.json. Run this AFTER an intentional
7
- * surface change (added skill, renamed trigger, refreshed framework
8
- * refs) and commit the new snapshot alongside the change.
9
- *
10
- * Do NOT run this to "fix" a failing check-manifest-snapshot.js gate
11
- * blindly — read the breaking-change list first. A breaking change is
12
- * a surface narrowing every downstream consumer needs to know about.
13
- *
14
- * commitOnly mode. Pass `--commit-only` (or set the env
15
- * EXCEPTD_SNAPSHOT_AUDIT_ACK=1) to acknowledge that the operator
16
- * deliberately wants to overwrite the committed snapshot. When neither
17
- * flag nor env is set AND the snapshot would actually change, the
18
- * script refuses and emits a structured diff hint. This stops an
19
- * accidental `npm run refresh-snapshot` (run as muscle-memory while
20
- * triaging a failing gate) from masking a real breaking change.
21
- *
22
- * Usage:
23
- * node scripts/refresh-manifest-snapshot.js # dry-shows the diff
24
- * EXCEPTD_SNAPSHOT_AUDIT_ACK=1 \
25
- * node scripts/refresh-manifest-snapshot.js # writes the new snapshot
26
- * node scripts/refresh-manifest-snapshot.js --commit-only # same thing, on argv
27
- *
28
- * The flag is documented in scripts/predeploy.js so contributors see it
29
- * the moment the snapshot gate fails.
10
+ * Overwriting an existing snapshot takes `--commit-only` or
11
+ * EXCEPTD_SNAPSHOT_AUDIT_ACK=1. Without either, a run whose capture differs
12
+ * refuses and prints a diff hint, so muscle memory cannot mask a real breaking
13
+ * change. scripts/predeploy.js names the flag where the gate fails.
30
14
  */
31
15
 
32
16
  const fs = require("fs");
@@ -64,18 +48,14 @@ const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
64
48
  const snapshot = captureSurface(manifest);
65
49
  const newJson = JSON.stringify(snapshot, null, 2) + "\n";
66
50
 
67
- // F5 — refuse to overwrite an existing snapshot unless the operator
68
- // has explicitly acknowledged the rewrite (env or --commit-only flag).
69
51
  const argv = process.argv.slice(2);
70
52
  const commitOnly =
71
53
  argv.includes("--commit-only") ||
72
54
  process.env.EXCEPTD_SNAPSHOT_AUDIT_ACK === "1";
73
55
 
74
- // Read the committed snapshot once and branch on the read RESULT rather than
75
- // an existsSync(SNAPSHOT_PATH)-then-readFileSync probe — the latter is a
76
- // check-then-use window (CodeQL js/file-system-race) where the existence the
77
- // guard decides on may not be the file it then reads. ENOENT IS the "no prior
78
- // snapshot, write a fresh one" signal.
56
+ // Branch on the read RESULT, never on an existsSync-then-read probe: that is a
57
+ // check-then-use window (CodeQL js/file-system-race) where the file the guard
58
+ // decided on need not be the one read. ENOENT IS the "no prior snapshot" signal.
79
59
  let current = null;
80
60
  try {
81
61
  current = fs.readFileSync(SNAPSHOT_PATH, "utf8");
@@ -83,8 +63,7 @@ try {
83
63
  if (e.code !== "ENOENT") throw e;
84
64
  }
85
65
  if (current !== null && !commitOnly) {
86
- // Normalise the _generated_at timestamp for comparison — that field
87
- // changes every run and shouldn't trigger the guard.
66
+ // _generated_at changes every run, so it must not trigger the guard.
88
67
  const stripGenerated = (s) => s.replace(
89
68
  /"_generated_at":\s*"[^"]+",?\s*\n?/, ""
90
69
  );
@@ -108,9 +87,8 @@ fs.writeFileSync(SNAPSHOT_PATH, newJson, "utf8");
108
87
  console.log(`[refresh-manifest-snapshot] wrote ${snapshot.skill_count} skills to manifest-snapshot.json`);
109
88
  console.log("[refresh-manifest-snapshot] commit this file alongside the surface change.");
110
89
 
111
- // write a tracked SHA-256 of the snapshot so the
112
- // check-manifest-snapshot.js gate can verify integrity (no hand edits
113
- // after refresh).
90
+ // A tracked SHA-256 lets check-manifest-snapshot.js catch a hand edit made
91
+ // after the refresh.
114
92
  const crypto = require("crypto");
115
93
  const snapshotSha = crypto.createHash("sha256").update(newJson).digest("hex");
116
94
  const snapshotShaPath = path.join(ROOT, "manifest-snapshot.sha256");
@@ -1,14 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-mitre-atlas.js
5
- *
6
- * Thin per-type wrapper for the MITRE ATLAS refresher. Logic lives in
7
- * scripts/refresh-upstream-catalogs.js#refreshAtlas.
8
- *
9
- * node scripts/refresh-mitre-atlas.js [--dry-run]
10
- *
11
- * Wired as `npm run refresh-mitre-atlas`.
4
+ * Thin per-type wrapper for the MITRE ATLAS refresher; the logic lives in
5
+ * scripts/refresh-upstream-catalogs.js#refreshAtlas. Wired as
6
+ * `npm run refresh-mitre-atlas`, and takes --dry-run.
12
7
  */
13
8
  const { refreshAtlas } = require("./refresh-upstream-catalogs.js");
14
9
  const dry = process.argv.includes("--dry-run");
@@ -1,15 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-mitre-attack.js
5
- *
6
- * Thin per-type wrapper for the MITRE ATT&CK refresher. Logic lives in
4
+ * Refreshes only the MITRE ATT&CK catalog. Logic lives in
7
5
  * scripts/refresh-upstream-catalogs.js#refreshAttack.
8
- *
9
- * node scripts/refresh-mitre-attack.js [--dry-run]
10
- * CAP=200 node scripts/refresh-mitre-attack.js
11
- *
12
- * Wired as `npm run refresh-mitre-attack`.
13
6
  */
14
7
  const { refreshAttack } = require("./refresh-upstream-catalogs.js");
15
8
  const dry = process.argv.includes("--dry-run");
@@ -1,15 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-mitre-d3fend.js
5
- *
6
- * Thin per-type wrapper for the MITRE D3FEND refresher. Logic lives in
7
- * scripts/refresh-upstream-catalogs.js#refreshD3fend.
8
- *
9
- * node scripts/refresh-mitre-d3fend.js [--dry-run]
10
- * CAP=120 node scripts/refresh-mitre-d3fend.js
11
- *
12
- * Wired as `npm run refresh-mitre-d3fend`.
4
+ * Thin wrapper for the MITRE D3FEND refresher; the logic lives in
5
+ * scripts/refresh-upstream-catalogs.js#refreshD3fend. Wired as
6
+ * `npm run refresh-mitre-d3fend`.
13
7
  */
14
8
  const { refreshD3fend } = require("./refresh-upstream-catalogs.js");
15
9
  const dry = process.argv.includes("--dry-run");
@@ -1,14 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-mitre-ics-attack.js
5
- *
6
- * Thin per-type wrapper for the MITRE ICS-attack STIX refresher. Logic
7
- * lives in scripts/refresh-upstream-catalogs.js#refreshIcsAttack.
8
- *
9
- * node scripts/refresh-mitre-ics-attack.js [--dry-run]
10
- *
11
- * Wired as `npm run refresh-mitre-ics-attack`.
4
+ * `npm run refresh-mitre-ics-attack [-- --dry-run]` — per-type wrapper for the
5
+ * MITRE ICS-attack STIX refresher; the logic lives in
6
+ * scripts/refresh-upstream-catalogs.js#refreshIcsAttack.
12
7
  */
13
8
  const { refreshIcsAttack } = require("./refresh-upstream-catalogs.js");
14
9
  const dry = process.argv.includes("--dry-run");