@blamejs/exceptd-skills 0.19.32 → 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 (127) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +8 -8
  4. package/data/_indexes/activity-feed.json +2 -2
  5. package/data/_indexes/catalog-summaries.json +7 -7
  6. package/data/_indexes/chains.json +60118 -0
  7. package/data/attack-techniques.json +267 -7
  8. package/data/cve-catalog.json +9991 -3
  9. package/data/cwe-catalog.json +109 -2
  10. package/data/framework-control-gaps.json +578 -3
  11. package/data/zeroday-lessons.json +8330 -1
  12. package/lib/auto-discovery.js +56 -286
  13. package/lib/canonical-eq.js +7 -40
  14. package/lib/citation-resolve.js +22 -70
  15. package/lib/collectors/ai-api.js +20 -54
  16. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  17. package/lib/collectors/citation-hygiene.js +72 -210
  18. package/lib/collectors/containers.js +41 -130
  19. package/lib/collectors/cred-stores.js +31 -115
  20. package/lib/collectors/crypto-codebase.js +55 -138
  21. package/lib/collectors/crypto.js +24 -54
  22. package/lib/collectors/hardening.js +20 -78
  23. package/lib/collectors/kernel.js +16 -46
  24. package/lib/collectors/library-author.js +57 -206
  25. package/lib/collectors/mcp.js +24 -70
  26. package/lib/collectors/runtime.js +24 -86
  27. package/lib/collectors/sbom.js +34 -106
  28. package/lib/collectors/scan-excludes.js +31 -138
  29. package/lib/collectors/secrets.js +62 -178
  30. package/lib/cross-ref-api.js +39 -123
  31. package/lib/currency-severity.js +8 -27
  32. package/lib/cve-batch.js +13 -21
  33. package/lib/cve-cli.js +13 -20
  34. package/lib/cve-curation.js +72 -239
  35. package/lib/cve-regression-watcher.js +29 -152
  36. package/lib/cvss.js +13 -54
  37. package/lib/doctor-bucketing.js +3 -19
  38. package/lib/exit-codes.js +10 -42
  39. package/lib/flag-suggest.js +7 -25
  40. package/lib/framework-gap.js +35 -114
  41. package/lib/gap-detectors.js +37 -159
  42. package/lib/id-validation.js +9 -30
  43. package/lib/job-queue.js +13 -36
  44. package/lib/lint-skills.js +64 -232
  45. package/lib/playbook-runner.js +693 -2095
  46. package/lib/prefetch.js +100 -376
  47. package/lib/refresh-external.js +199 -627
  48. package/lib/refresh-network.js +75 -307
  49. package/lib/rfc-cli.js +23 -68
  50. package/lib/scoring.js +77 -145
  51. package/lib/sign.js +43 -229
  52. package/lib/source-advisories.js +43 -194
  53. package/lib/source-ghsa.js +37 -120
  54. package/lib/source-osv.js +94 -266
  55. package/lib/ttp-mapper.js +14 -24
  56. package/lib/upstream-check-cli.js +10 -28
  57. package/lib/upstream-check.js +19 -44
  58. package/lib/validate-catalog-meta.js +17 -61
  59. package/lib/validate-cve-catalog.js +43 -119
  60. package/lib/validate-indexes.js +25 -76
  61. package/lib/validate-package.js +16 -62
  62. package/lib/validate-playbooks.js +69 -275
  63. package/lib/validate-vendor.js +16 -49
  64. package/lib/verify.js +56 -286
  65. package/lib/version-pins.js +5 -34
  66. package/lib/worker-pool.js +11 -30
  67. package/lib/xml-tokenizer.js +47 -152
  68. package/manifest.json +53 -53
  69. package/orchestrator/dispatcher.js +17 -68
  70. package/orchestrator/event-bus.js +11 -74
  71. package/orchestrator/index.js +138 -412
  72. package/orchestrator/pipeline.js +28 -85
  73. package/orchestrator/scanner.js +34 -138
  74. package/orchestrator/scheduler.js +20 -84
  75. package/package.json +2 -2
  76. package/sbom.cdx.json +253 -253
  77. package/scripts/audit-catalog-gaps.js +9 -62
  78. package/scripts/audit-cross-skill.js +5 -31
  79. package/scripts/audit-perf.js +6 -16
  80. package/scripts/backfill-theater-test.js +7 -64
  81. package/scripts/bootstrap.js +12 -44
  82. package/scripts/build-indexes.js +40 -154
  83. package/scripts/builders/activity-feed.js +4 -14
  84. package/scripts/builders/catalog-summaries.js +3 -10
  85. package/scripts/builders/currency.js +7 -20
  86. package/scripts/builders/cwe-chains.js +7 -30
  87. package/scripts/builders/did-ladders.js +6 -13
  88. package/scripts/builders/frequency.js +5 -19
  89. package/scripts/builders/jurisdiction-clocks.js +6 -25
  90. package/scripts/builders/recipes.js +6 -14
  91. package/scripts/builders/section-offsets.js +13 -51
  92. package/scripts/builders/stale-content.js +7 -28
  93. package/scripts/builders/summary-cards.js +8 -29
  94. package/scripts/builders/theater-fingerprints.js +12 -27
  95. package/scripts/builders/token-budget.js +4 -31
  96. package/scripts/check-agents-md-collectors.js +11 -54
  97. package/scripts/check-catalog-gap-budget.js +15 -32
  98. package/scripts/check-changelog-extract.js +18 -48
  99. package/scripts/check-codebase-patterns-currency.js +6 -22
  100. package/scripts/check-codebase-patterns.js +50 -143
  101. package/scripts/check-epss-consistency.js +9 -64
  102. package/scripts/check-framework-gap-coverage.js +13 -31
  103. package/scripts/check-manifest-snapshot.js +13 -73
  104. package/scripts/check-sbom-currency.js +44 -142
  105. package/scripts/check-test-count.js +15 -52
  106. package/scripts/check-test-coverage.js +66 -197
  107. package/scripts/check-test-subjects.js +21 -62
  108. package/scripts/check-ttp-references.js +14 -38
  109. package/scripts/check-ttp-upstream.js +8 -40
  110. package/scripts/check-version-bump.js +9 -61
  111. package/scripts/check-version-tags.js +20 -121
  112. package/scripts/predeploy.js +38 -184
  113. package/scripts/refresh-manifest-snapshot.js +16 -38
  114. package/scripts/refresh-mitre-atlas.js +3 -8
  115. package/scripts/refresh-mitre-attack.js +1 -8
  116. package/scripts/refresh-mitre-d3fend.js +3 -9
  117. package/scripts/refresh-mitre-ics-attack.js +3 -8
  118. package/scripts/refresh-reverse-refs.js +27 -94
  119. package/scripts/refresh-rfc-index.js +2 -10
  120. package/scripts/refresh-sbom.js +31 -161
  121. package/scripts/refresh-upstream-catalogs.js +40 -137
  122. package/scripts/release.js +69 -232
  123. package/scripts/run-e2e-scenarios.js +24 -71
  124. package/scripts/sync-manifest-metadata.js +10 -34
  125. package/scripts/sync-package-description.js +8 -17
  126. package/scripts/validate-vendor-online.js +13 -44
  127. package/scripts/verify-shipped-tarball.js +35 -140
@@ -2,30 +2,14 @@
2
2
  "use strict";
3
3
 
4
4
  /**
5
- * TTP reference-integrity gate.
5
+ * TTP reference-integrity gate. data/attack-techniques.json and
6
+ * data/atlas-ttps.json are the pinned copies of ATT&CK and ATLAS; every other file
7
+ * naming a technique refers into them, and this proves those references resolve.
8
+ * Resolution runs against the pins, not against MITRE:
9
+ * scripts/check-ttp-upstream.js is the network check on whether the pins
10
+ * themselves are current, and it cannot block a release. This one can.
6
11
  *
7
- * data/attack-techniques.json and data/atlas-ttps.json are the pinned copies of
8
- * ATT&CK and ATLAS. Every other file that names a technique — countermeasure
9
- * maps, DLP controls, playbooks, skill bodies, CVE entries — is referring INTO
10
- * those two catalogs. This gate proves those references resolve.
11
- *
12
- * The failure it exists for: MITRE retires and renumbers techniques between
13
- * releases. When a pin is bumped, the two source catalogs get remapped, but
14
- * references living in other files are easy to miss — nothing dereferences them
15
- * at runtime, so a stale id keeps rendering in operator output as if it were
16
- * current. It points at a MITRE page that no longer resolves, and any control
17
- * claiming to counter it is now mapped to nothing (AGENTS.md Hard Rule #4: no
18
- * orphaned controls). A bumped pin left exactly this residue in the D3FEND and
19
- * DLP maps, invisible to every other gate.
20
- *
21
- * The check is offline: it resolves references against the pinned catalogs, not
22
- * against MITRE. That is deliberate — scripts/check-ttp-upstream.js is the
23
- * network check that asks whether the PINS are current, and it cannot block a
24
- * release because it needs connectivity. This one can block, because a
25
- * reference that does not resolve against the pin we ship is broken no matter
26
- * what upstream says.
27
- *
28
- * Exit codes: 0 all references resolve, 1 unresolved references found.
12
+ * Exit 0 when every reference resolves, 1 when any does not.
29
13
  */
30
14
 
31
15
  const fs = require("node:fs");
@@ -34,11 +18,10 @@ const path = require("node:path");
34
18
  const ROOT = path.resolve(__dirname, "..");
35
19
 
36
20
  /**
37
- * ATT&CK ids are TNNNN[.NNN]; ATLAS ids are AML.TNNNN[.NNN]. The lookbehind
38
- * matters: without it the ATT&CK alternative matches the "T0017" inside
39
- * "AML.T0017" and reports a phantom bare-ATT&CK reference for every ATLAS id in
40
- * the tree. It covers the hyphen as well, because ATLAS ids also appear in
41
- * filenames, where the dot is not a legal separator.
21
+ * ATT&CK ids are TNNNN[.NNN]; ATLAS ids are AML.TNNNN[.NNN]. Without the
22
+ * lookbehind the ATT&CK alternative matches the "T0017" inside "AML.T0017" and
23
+ * reports a phantom bare-ATT&CK reference for every ATLAS id. It covers the
24
+ * hyphen too, since ATLAS ids also appear in filenames.
42
25
  */
43
26
  const TTP_PATTERN = /\bAML\.T\d{4}(?:\.\d{3})?\b|(?<!AML[.-])\bT\d{4}(?:\.\d{3})?\b/g;
44
27
 
@@ -50,17 +33,10 @@ const SEARCH_EXTS = new Set([".json", ".md", ".js"]);
50
33
  const SKIP_DIRS = new Set(["node_modules", "_indexes", "vendor", ".git"]);
51
34
 
52
35
  /**
53
- * Tokens that match the pattern without being references to a technique.
54
- * Every entry needs a reason: an unexplained allowlist is how a real stale id
55
- * eventually gets parked here to make the gate green.
36
+ * Tokens that match the pattern without referencing a technique. Every entry needs
37
+ * a reason: an unexplained allowlist is how a real stale id gets parked here.
56
38
  */
57
- const NOT_REFERENCES = [
58
- {
59
- id: "T1234",
60
- file: "lib/gap-detectors.js",
61
- why: "placeholder in a comment describing the reference-extraction pattern itself, not a claim about a technique",
62
- },
63
- ];
39
+ const NOT_REFERENCES = [];
64
40
 
65
41
  function loadKnownIds() {
66
42
  const known = new Set();
@@ -2,31 +2,11 @@
2
2
  "use strict";
3
3
 
4
4
  /**
5
- * scripts/check-ttp-upstream.js — validates every shipped TTP id against the
6
- * pinned upstream release.
5
+ * Validates every shipped TTP id in data/attack-techniques.json and
6
+ * data/atlas-ttps.json against the pinned upstream MITRE release. A network
7
+ * check, so it runs in the refresh workflow rather than in offline predeploy.
7
8
  *
8
- * Why this exists. The catalogs in data/attack-techniques.json and
9
- * data/atlas-ttps.json are curated mirrors of MITRE releases, and nothing
10
- * checked that the ids in them are ids MITRE actually publishes. The ATLAS
11
- * 2026.07 / ATT&CK 19.2 pin bump found eleven that were not: five ATT&CK
12
- * techniques revoked upstream — already revoked in 19.1, the version pinned at
13
- * the time, so they had been shipping revoked for a full release cycle — and
14
- * six that appear in no upstream domain at any version. Every one of them was
15
- * reachable by a skill telling an operator to map a finding to it, which is
16
- * exactly what Hard Rule #4 (no orphaned controls — every control maps to a
17
- * real TTP) exists to prevent.
18
- *
19
- * This is a NETWORK check, so it runs in the refresh workflow beside the pin
20
- * checker rather than in predeploy, which is offline. Report-only: it prints
21
- * findings and exits non-zero, leaving the decision to a maintainer, because a
22
- * revocation needs a judgement call about the successor rather than an
23
- * automatic rewrite.
24
- *
25
- * node scripts/check-ttp-upstream.js validate against the pins
26
- * node scripts/check-ttp-upstream.js --json machine-readable output
27
- *
28
- * Exit: 0 clean, 1 findings, 2 upstream unreachable (never a silent pass —
29
- * an unreachable upstream must not read as "all ids valid").
9
+ * Exit 0 clean, 1 findings, 2 upstream unreachable — never a silent pass.
30
10
  */
31
11
 
32
12
  const fs = require("node:fs");
@@ -41,14 +21,8 @@ function readJson(p) {
41
21
  }
42
22
 
43
23
  /**
44
- * A pinned version read out of a catalog is file data, and it is about to be
45
- * interpolated into an upstream URL. Constrain it to the exact shape each
46
- * project publishes before it can reach a request, so a malformed or tampered
47
- * `_meta` cannot steer the fetch — and so a typo'd pin fails here with a clear
48
- * message instead of as a puzzling 404 later.
49
- *
50
- * ATT&CK semver-ish major.minor 19.2
51
- * ATLAS CalVer YYYY.MM with optional .N 2026.07, 2025.11.2
24
+ * A pin is catalog data interpolated into an upstream URL, so its shape is
25
+ * bounded here: ATT&CK is major.minor, ATLAS is CalVer YYYY.MM with optional .N.
52
26
  */
53
27
  const VERSION_SHAPES = {
54
28
  attack: /^\d{1,3}\.\d{1,3}$/,
@@ -66,13 +40,8 @@ function assertPinShape(kind, value) {
66
40
  }
67
41
 
68
42
  /**
69
- * The only hosts this checker may ever contact. `assertPinShape` already bounds
70
- * the catalog-sourced version to a digit-and-dot shape before it reaches a URL,
71
- * but that guard sits a long way from the request — a later edit could add a
72
- * caller that skips it, and static analysis reasonably flags file data reaching
73
- * an outbound request when the barrier is that distant. Re-check the resolved
74
- * origin at the sink so the property is enforced where it matters and is
75
- * verifiable by reading ten lines rather than tracing the whole module.
43
+ * The only hosts this checker may contact. Re-checked at the sink even though
44
+ * `assertPinShape` bounds the version, so a later caller cannot route past it.
76
45
  */
77
46
  const ALLOWED_ORIGINS = new Set(["https://raw.githubusercontent.com"]);
78
47
 
@@ -89,7 +58,6 @@ function assertAllowedUrl(rawUrl) {
89
58
  `${[...ALLOWED_ORIGINS].join(", ")}`
90
59
  );
91
60
  }
92
- // Reject anything that could smuggle credentials or redirect the path.
93
61
  if (parsed.username || parsed.password || parsed.search || parsed.hash) {
94
62
  throw new Error(`refusing to fetch a URL carrying credentials or a query/fragment: ${parsed.origin}${parsed.pathname}`);
95
63
  }
@@ -2,45 +2,9 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * scripts/check-version-bump.js — patch-only-cadence predeploy gate.
6
- *
7
- * Why this exists. The project cadence is "patch is the only default bump; a
8
- * minor or major requires explicit human authorization." That rule lived in
9
- * the contributor guide and in maintainer memory — and was violated anyway
10
- * (two releases shipped as minors that should have been patches). A written
11
- * rule the tooling does not enforce is one a tired/automated contributor will
12
- * eventually skip. This gate makes the rule mechanical: an unauthorized
13
- * minor/major version bump fails predeploy and cannot ship.
14
- *
15
- * Hermetic by design. Authorization is a COMMITTED artifact
16
- * (tests/.version-bump-ack.json), not an environment variable, because
17
- * `npm run predeploy` runs in the release.yml validate job as well as locally.
18
- * An env-var scheme would either false-fail a legitimately-authorized minor in
19
- * CI or require per-release CI config. A committed ack file travels with the
20
- * checkout, so the gate enforces identically everywhere — and a minor bump
21
- * becomes a loud, reviewable line in the PR diff instead of a silent change to
22
- * a version string.
23
- *
24
- * Mechanism:
25
- * prev = the most recent OTHER `## X.Y.Z` heading in CHANGELOG.md
26
- * cur = package.json version (== the top CHANGELOG heading; the version-sync
27
- * gate enforces that match separately)
28
- * classify prev -> cur as patch | minor | major | none | downgrade
29
- * - patch / none -> pass (the default; zero ceremony)
30
- * - downgrade / bad -> fail (versions only move forward)
31
- * - minor / major -> pass ONLY if tests/.version-bump-ack.json names
32
- * the exact `cur` version with the matching type;
33
- * otherwise fail with remediation.
34
- *
35
- * To authorize a minor (only after the user explicitly asks for one):
36
- * echo '{"version":"0.19.0","type":"minor"}' > tests/.version-bump-ack.json
37
- * and commit it. The ack is version-specific, so a stale ack cannot authorize
38
- * a different future bump.
39
- *
40
- * Output:
41
- * stdout: structured JSON when --json, else a one-line summary
42
- * exit 0: patch/none, or an authorized minor/major
43
- * exit 1: unauthorized minor/major, downgrade, or unparseable version
5
+ * Patch-only-cadence gate. A minor or major fails unless the committed
6
+ * tests/.version-bump-ack.json names that exact version, so an ack travels with
7
+ * the checkout and a stale one cannot authorize a later bump.
44
8
  */
45
9
 
46
10
  const fs = require('fs');
@@ -57,7 +21,6 @@ function parseSemver(v) {
57
21
  return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
58
22
  }
59
23
 
60
- // Classify the transition prev -> cur. Pure; exported for unit tests.
61
24
  function classifyBump(prev, cur) {
62
25
  const a = parseSemver(prev);
63
26
  const b = parseSemver(cur);
@@ -68,8 +31,7 @@ function classifyBump(prev, cur) {
68
31
  return 'none';
69
32
  }
70
33
 
71
- // Decide whether a bump is allowed given the committed ack (or null). Pure;
72
- // exported for unit tests. ack = { version, type } | null.
34
+ // `ack` is { version, type } or null.
73
35
  function checkBump(prev, cur, ack) {
74
36
  if (!prev) return { ok: true, bump: 'initial', reason: 'no previous version recorded' };
75
37
  const bump = classifyBump(prev, cur);
@@ -82,7 +44,6 @@ function checkBump(prev, cur, ack) {
82
44
  if (bump === 'patch' || bump === 'none') {
83
45
  return { ok: true, bump, reason: `${bump} bump (${prev} -> ${cur})` };
84
46
  }
85
- // minor or major — requires explicit committed authorization for this exact version.
86
47
  if (ack && ack.version === cur && ack.type === bump) {
87
48
  return { ok: true, bump, reason: `${bump} bump authorized for ${cur} via tests/.version-bump-ack.json` };
88
49
  }
@@ -93,7 +54,7 @@ function checkBump(prev, cur, ack) {
93
54
  };
94
55
  }
95
56
 
96
- // Extract `## X.Y.Z` version headings from CHANGELOG.md, in document order.
57
+ // Document order — newest first, which determinePrevious relies on.
97
58
  function changelogVersions(text) {
98
59
  const out = [];
99
60
  const re = /^##\s+(\d+\.\d+\.\d+)\b/gm;
@@ -107,14 +68,8 @@ function suggestPatch(prev) {
107
68
  return a ? `${a.major}.${a.minor}.${a.patch + 1}` : null;
108
69
  }
109
70
 
110
- // Resolve the previous version from CHANGELOG text, or fail closed. Pure;
111
- // exported for unit tests. A release gate must NOT treat an unreadable or
112
- // heading-less CHANGELOG as a "first release" — doing so silently authorized
113
- // ANY bump (including an unapproved major), because checkBump(null, ...) takes
114
- // the initial-release pass. `text` is the CHANGELOG contents, or null when the
115
- // read failed. Returns { ok, prev?, reason? }: ok:false fails the gate; ok:true
116
- // yields prev, which is null ONLY for a genuine first release whose sole
117
- // `## X.Y.Z` heading equals cur (versions === [cur]).
71
+ // Fails closed: an unreadable or heading-less CHANGELOG must not look like a
72
+ // first release, because checkBump(null, ...) then authorizes any bump.
118
73
  function determinePrevious(text, cur) {
119
74
  if (text == null) {
120
75
  return { ok: false, reason: 'CHANGELOG.md could not be read' };
@@ -149,12 +104,7 @@ function main() {
149
104
  }
150
105
  const cur = pkg.version;
151
106
 
152
- // Fail closed when CHANGELOG is unreadable or carries no version headings.
153
- // Swallowing a read error to '' previously made versions=[] -> prev=null ->
154
- // checkBump's initial-release pass, so a missing/malformed CHANGELOG silently
155
- // authorized any bump. A genuine first release still passes: its sole
156
- // `## X.Y.Z` heading equals cur, so determinePrevious returns prev=null with
157
- // ok:true and checkBump takes the legitimate initial path.
107
+ // null on a read failure, never '' — determinePrevious fails closed on null.
158
108
  let changelogText = null;
159
109
  try { changelogText = fs.readFileSync(CHANGELOG_PATH, 'utf8'); } catch (_e) { changelogText = null; }
160
110
  const prevRes = determinePrevious(changelogText, cur);
@@ -163,7 +113,6 @@ function main() {
163
113
  process.exitCode = 1;
164
114
  return;
165
115
  }
166
- // prev = the most recent heading that differs from the current version.
167
116
  const prev = prevRes.prev;
168
117
 
169
118
  const ack = readAck();
@@ -191,8 +140,7 @@ function main() {
191
140
  if (patch) process.stderr.write(`[check-version-bump] If this should be a patch, set the version to ${patch}.\n`);
192
141
  process.stderr.write(`[check-version-bump] If the user explicitly authorized a ${res.bump}, commit tests/.version-bump-ack.json = {"version":"${cur}","type":"${res.bump}"}.\n`);
193
142
  }
194
- // process.exitCode (not process.exit) so the buffered stdout write above
195
- // is not truncated when stdout is piped (the stdout-flush-truncation class).
143
+ // `process.exitCode`, not process.exit(): exit truncates the buffered stdout write.
196
144
  process.exitCode = 1;
197
145
  return;
198
146
  }
@@ -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",