@blamejs/exceptd-skills 0.19.33 → 0.19.35

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 +28 -0
  2. package/bin/exceptd.js +895 -2828
  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 +170 -76
  8. package/lib/collectors/cicd-pipeline-compromise.js +113 -136
  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 +198 -211
  17. package/lib/collectors/mcp.js +24 -70
  18. package/lib/collectors/runtime.js +24 -86
  19. package/lib/collectors/sbom.js +130 -118
  20. package/lib/collectors/scan-excludes.js +33 -139
  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 -155
  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 +39 -113
  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 +88 -236
  37. package/lib/playbook-runner.js +759 -2107
  38. package/lib/prefetch.js +101 -376
  39. package/lib/refresh-external.js +199 -633
  40. package/lib/refresh-network.js +78 -311
  41. package/lib/rfc-cli.js +23 -68
  42. package/lib/scoring.js +85 -146
  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 +28 -27
  48. package/lib/upstream-check-cli.js +36 -29
  49. package/lib/upstream-check.js +19 -44
  50. package/lib/validate-catalog-meta.js +17 -61
  51. package/lib/validate-cve-catalog.js +52 -121
  52. package/lib/validate-indexes.js +25 -76
  53. package/lib/validate-package.js +16 -62
  54. package/lib/validate-playbooks.js +78 -286
  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 -413
  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 +242 -242
  69. package/scripts/audit-catalog-gaps.js +9 -62
  70. package/scripts/audit-cross-skill.js +5 -31
  71. package/scripts/audit-perf.js +29 -28
  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 +21 -31
  87. package/scripts/builders/token-budget.js +4 -31
  88. package/scripts/check-agents-md-collectors.js +26 -57
  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 +63 -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 +62 -81
  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 +83 -198
  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 +7 -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 +7 -8
  110. package/scripts/refresh-reverse-refs.js +27 -94
  111. package/scripts/refresh-rfc-index.js +7 -10
  112. package/scripts/refresh-sbom.js +31 -161
  113. package/scripts/refresh-upstream-catalogs.js +63 -148
  114. package/scripts/release.js +69 -234
  115. package/scripts/run-e2e-scenarios.js +26 -73
  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 -141
@@ -2,43 +2,11 @@
2
2
  "use strict";
3
3
 
4
4
  /**
5
- * scripts/check-version-tags.js
6
- *
7
- * Refuses NEW version-stamped lines / filenames in the tracked source
8
- * tree. "Line" is deliberate and broader than "comment": a 0.x version
9
- * stamp is residue wherever a stranger reads it, which per the
10
- * operator-facing-surface rules includes string literals that ship to
11
- * operators — CLI `--help` text, error messages, and test descriptions —
12
- * not just `//` comments. The scan therefore tests the WHOLE line, so a
13
- * `version: '0.18.7'` data literal or a `--flag (v0.18.7)` help string
14
- * counts the same as a `// v0.18.7` comment. Genuinely-load-bearing
15
- * version references (real test fixtures, deprecation timelines) get the
16
- * file added to COMMENT_EXEMPT below. The authoritative version surfaces
17
- * are:
18
- *
19
- * 1. package.json / manifest.json `"version"` field
20
- * 2. CHANGELOG.md `## X.Y.Z` headings
21
- * 3. git tags
22
- * 4. CLI `version` verb output (reads from package.json)
23
- *
24
- * Anywhere else, `// v0.13.22` / `Pre-v0.13.22` / `*-v0_13_22.test.js`
25
- * is phase residue — operators don't have the roadmap, version tags
26
- * rot the moment the next release lands, and `git clone` ships every
27
- * comment to operators along with the code.
28
- *
29
- * The check uses a baseline snapshot (`tests/.version-tag-baseline.
30
- * json`) capturing current violation counts per file. Future scans
31
- * compare against the baseline:
32
- *
33
- * - Filename violations beyond baseline → fail.
34
- * - Line violations beyond baseline (in any file) → fail.
35
- * - Violations strictly within baseline → ok.
36
- * - Violations below baseline (drift reduced) → ok +
37
- * suggestion to refresh the baseline.
38
- *
39
- * Refresh: `node scripts/check-version-tags.js --update-baseline`.
40
- *
41
- * Wired into `npm run predeploy` as a gate.
5
+ * Predeploy gate refusing new version-stamped lines and filenames in the tracked
6
+ * source tree. Whole lines, not just comments: a stamp in a data literal or a
7
+ * help string is residue too, and a load-bearing one is exempted by path in
8
+ * COMMENT_EXEMPT rather than by narrowing the scan. Counts must not rise above
9
+ * tests/.version-tag-baseline.json; refresh it with `--update-baseline`.
42
10
  */
43
11
 
44
12
  const fs = require("node:fs");
@@ -48,24 +16,14 @@ const { execFileSync } = require("node:child_process");
48
16
  const ROOT = path.join(__dirname, "..");
49
17
  const BASELINE_PATH = path.join(ROOT, "tests", ".version-tag-baseline.json");
50
18
 
51
- // Directories we do not walk at all.
52
19
  const SKIP_DIRS = new Set([
53
20
  "node_modules", ".git", ".keys", ".cache", ".scratch",
54
21
  "data", "vendor", ".husky",
55
22
  ]);
56
23
 
57
- // File extensions we scan for comment violations.
58
24
  const SCAN_EXTS = new Set([".js", ".cjs", ".mjs", ".md"]);
59
25
 
60
- // Paths that the project intentionally version-stamps:
61
- // - CHANGELOG headings are how operators navigate the file
62
- // - package.json / manifest.json carry the canonical version field
63
- // - manifest-snapshot.json + sbom.cdx.json contain version-pinned
64
- // metadata (the SBOM IS a version-stamped manifest)
65
- // - lib/version-pins.js is a version-constant lookup table
66
- // - This checker itself documents what it forbids
67
- // - .git-blame-ignore-revs carries commit hashes, not version tags,
68
- // but is conventional config the user maintains
26
+ // Paths where a version reference is load-bearing.
69
27
  const COMMENT_EXEMPT = new Set([
70
28
  "package.json",
71
29
  "manifest.json",
@@ -74,45 +32,20 @@ const COMMENT_EXEMPT = new Set([
74
32
  "CHANGELOG.md",
75
33
  "lib/version-pins.js",
76
34
  "scripts/check-version-tags.js",
77
- // The release-notes-extract gate test asserts version-based CHANGELOG
78
- // extraction + the shorter-vs-longer prefix-collision guard, so its fixtures
79
- // MUST embed real `## X.Y.Z` headings (e.g. 0.15.5 vs 0.15.50) — load-bearing
80
- // test data, not sprinkled release tags.
35
+ // Fixtures embed real `## X.Y.Z` headings, including a prefix collision.
81
36
  "tests/check-changelog-extract.test.js",
82
- // The extract gate's orphan-tag allowlist must name the exact versions of
83
- // tags that exist with no published release (outage-recovery bumps), so the
84
- // heading-completeness check can skip them — load-bearing references to git
85
- // tags, an authoritative version surface.
37
+ // Allowlists the exact versions of tags with no published release.
86
38
  "scripts/check-changelog-extract.js",
87
- // The version-bump cadence gate's subject IS version comparison: its doc
88
- // shows an example ack naming a target version, and its test compares real
89
- // X.Y.Z transitions (patch vs minor vs major vs downgrade). Those version
90
- // literals are load-bearing data, not sprinkled release tags.
39
+ // Version comparison is the subject: real X.Y.Z transitions under test.
91
40
  "scripts/check-version-bump.js",
92
41
  "tests/version-bump-cadence.test.js",
93
- // The version-tag gate's own regression test asserts the trailing-period /
94
- // IPv4 / longer-run boundaries and the PHASE_RESIDUE_RES / FILENAME_VERSION_RE
95
- // / countLineViolations exports, so it MUST embed literal stamps like
96
- // `0.18.9.`, `0.18.99`, `Pre-0.13.22`, and `foo-v0_13_2.test.js` as the inputs
97
- // under test — load-bearing data for the detector's boundary cases, not
98
- // sprinkled release tags.
42
+ // The detector's own boundary cases appear literally as the inputs under test.
99
43
  "tests/check-version-tags.test.js",
100
44
  ]);
101
45
 
102
- // Git-ignored files (a contributor's local-only working docs, scratch) are
103
- // never scanned — the gate enforces on the would-be-shipped surface, with no
104
- // need to name individual local-only files. Untracked-but-NOT-ignored files
105
- // ARE still scanned: a new file a contributor is about to commit is exactly
106
- // what the gate must catch. Computed via `git check-ignore` over the walked set.
107
- // Returns the ignored subset, or NULL when git cannot answer.
108
- //
109
- // "No path matched" and "the question could not be asked" are different
110
- // results and must not collapse into the same empty set. Without a repository
111
- // — a build context that omits .git/, or git not installed — an empty set
112
- // silently reclassifies every local-only file as part of the shipped surface,
113
- // so the gate reports violations in files a clone never contains. Returning
114
- // null lets the caller say it could not determine the surface instead of
115
- // asserting a wrong one.
46
+ // The ignored subset of `relPaths`, or null when git cannot answer. "No path
47
+ // matched" and "the question could not be asked" must not collapse into the same
48
+ // empty set, which would reclassify every local-only file as shipped surface.
116
49
  function gitIgnoredSet(relPaths) {
117
50
  if (!relPaths.length) return new Set();
118
51
  try {
@@ -122,9 +55,7 @@ function gitIgnoredSet(relPaths) {
122
55
  });
123
56
  return new Set(out.split(/\r?\n/).filter(Boolean));
124
57
  } catch (e) {
125
- // Exit 1 with no stderr is git's way of saying "no path matched" — a real
126
- // answer, and an empty set is correct. Anything else (git missing, not a
127
- // repository, .git absent) means the question went unanswered.
58
+ // Exit 1 with no stderr is git's "no path matched" — a real answer, not a failure.
128
59
  const status = e && typeof e.status === "number" ? e.status : null;
129
60
  const stderr = e && e.stderr ? String(e.stderr).trim() : "";
130
61
  const out = e && e.stdout ? String(e.stdout) : "";
@@ -133,20 +64,11 @@ function gitIgnoredSet(relPaths) {
133
64
  }
134
65
  }
135
66
 
136
- // Pattern: project version like `v0.13.22` or bare `0.13.22`. Matches
137
- // our pre-1.0 release range. External package versions like ATLAS
138
- // `v5.6.0` or CycloneDX `1.6` don't match because the major is 0.
139
- // The trailing lookahead rejects a longer minor/patch digit (so `0.18.99`
140
- // still matches, but the stamp can't be part of a wider number) and a
141
- // dot-followed-by-digit (an IPv4 next octet / longer dotted-numeric run, e.g.
142
- // `127.0.0.1`, whose `0.0.1` tail would otherwise register). A sentence-ending
143
- // period after the patch (dot followed by non-digit / end-of-line, e.g.
144
- // `// fixed in 0.18.9.`) is NOT excluded — that is exactly the version residue
145
- // the gate must catch. The leading `(?<![\d.])` lookbehind keeps the IPv4
146
- // suppression on the other side.
67
+ // A pre-1.0 project version, `v0.13.22` or bare; a non-0.x external version such
68
+ // as CycloneDX `1.6` misses. The lookarounds keep the stamp out of a wider number
69
+ // or a dotted run like `127.0.0.1`, whose tail would otherwise register.
147
70
  const VERSION_TAG_RE = /(?<![\d.])v?0\.\d+\.\d+(?!\d)(?!\.\d)/;
148
71
 
149
- // Phase residue patterns — broader than just version tags.
150
72
  const PHASE_RESIDUE_RES = [
151
73
  /\bcycle\s+\d+\b/i, // "cycle 13 P3 F3"
152
74
  /\bphase\s+\d+(\.\d+)+\b/i,// "phase 9.11k"
@@ -173,12 +95,6 @@ function walk(dir, results = []) {
173
95
  return results;
174
96
  }
175
97
 
176
- // Counts version-stamp lines in a file. Intentionally WHOLE-LINE, not
177
- // comment-only: a 0.x stamp inside a shipped string literal (CLI --help text,
178
- // an error message, a test description) is operator-readable residue just like
179
- // a `//` comment, so it counts the same. A file with a genuinely load-bearing
180
- // version literal (real test fixture, deprecation timeline) is exempted by path
181
- // in COMMENT_EXEMPT, not by narrowing the scan.
182
98
  function countLineViolations(rel) {
183
99
  if (COMMENT_EXEMPT.has(rel)) return 0;
184
100
  const ext = path.extname(rel);
@@ -199,16 +115,11 @@ function countLineViolations(rel) {
199
115
  function scanCurrent() {
200
116
  const files = walk(ROOT);
201
117
  const ignored = gitIgnoredSet(files);
202
- // Without git the shipped surface is unknowable: local-only files are
203
- // indistinguishable from tracked ones, so any result would be a guess.
204
- // Report that rather than emit findings the baseline cannot be compared to.
118
+ // Without git, local-only files are indistinguishable from tracked ones.
205
119
  if (ignored === null) return { byFile: {}, filenameViolations: [], surfaceUnknown: true };
206
120
  const byFile = {};
207
121
  const filenameViolations = [];
208
122
  for (const rel of files) {
209
- // Skip git-ignored, local-only files that `git clone` never ships.
210
- // Untracked-but-not-ignored files are still scanned — a new file about to
211
- // be committed is exactly what the gate guards.
212
123
  if (ignored.has(rel)) continue;
213
124
  if (FILENAME_VERSION_RE.test(rel)) filenameViolations.push(rel);
214
125
  const n = countLineViolations(rel);
@@ -248,15 +159,8 @@ function main() {
248
159
  const current = scanCurrent();
249
160
 
250
161
  if (current.surfaceUnknown) {
251
- // Never write a baseline from a scan that could not tell shipped files from
252
- // local ones — that would bake the wrong surface in permanently.
253
- //
254
- // Automation is the one place this must not degrade to a skip. This gate
255
- // runs inside predeploy, and predeploy guards the publish job, so a
256
- // silently-skipped run there stops enforcing on exactly the path that
257
- // ships. Locally — a container built without .git, a tarball inspection —
258
- // skipping is the honest answer, because the shipped surface genuinely is
259
- // not knowable there and failing would only punish the harness.
162
+ // A baseline written from this scan would bake in the wrong surface. In
163
+ // automation this fails rather than skips: predeploy guards publishing.
260
164
  const inAutomation = process.env.CI === "true" || !!process.env.GITHUB_ACTIONS;
261
165
  if (inAutomation) {
262
166
  console.error("[check-version-tags] FAIL — no git repository available, so the shipped");
@@ -291,8 +195,6 @@ function main() {
291
195
 
292
196
  const regressions = [];
293
197
 
294
- // Filename regressions: any new filename matching the pattern that
295
- // wasn't in the baseline.
296
198
  for (const rel of current.filenameViolations) {
297
199
  if (!baseline.filenameViolations.includes(rel)) {
298
200
  regressions.push({
@@ -303,7 +205,6 @@ function main() {
303
205
  }
304
206
  }
305
207
 
306
- // Comment regressions: per-file count grew.
307
208
  for (const [rel, n] of Object.entries(current.byFile)) {
308
209
  const prior = baseline.byFile[rel] || 0;
309
210
  if (n > prior) {
@@ -317,11 +218,9 @@ function main() {
317
218
  }
318
219
  }
319
220
 
320
- // Files newly added to the violation set (not in baseline at all).
321
221
  for (const rel of Object.keys(current.byFile)) {
322
222
  if (!(rel in baseline.byFile)) {
323
223
  const n = current.byFile[rel];
324
- // Skip if already captured as a count regression above.
325
224
  if (regressions.some(r => r.path === rel)) continue;
326
225
  regressions.push({
327
226
  kind: "comment",
@@ -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"})`