@blamejs/exceptd-skills 0.12.2 → 0.12.5

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,103 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.5 — 2026-05-13
4
+
5
+ **Patch: root cause of the signature regression — a test was generating a fresh keypair mid-suite.**
6
+
7
+ ### The actual bug
8
+
9
+ `tests/operator-bugs.test.js:#87 doctor --fix is registered (smoke)` invoked `exceptd doctor --fix` directly. On any host where `.keys/private.pem` was missing (every CI run, every fresh clone), `--fix` synchronously spawned `lib/sign.js generate-keypair`, which OVERWRITES `keys/public.pem` with a fresh Ed25519 public key.
10
+
11
+ After that point in the test suite:
12
+ - `keys/public.pem` = new key generated by the test
13
+ - `manifest.json` skill signatures = unchanged, still reference the COMMITTED private key
14
+ - Every subsequent step ran against a state where signatures cover content signed by Key-A but the public key on disk is Key-B
15
+ - `npm pack` shipped the new public.pem + the old (committed) manifest signatures
16
+ - `verify` on the published tarball failed 0/38 because the keys don't match
17
+
18
+ The reason it was invisible across v0.11.x and v0.12.x:
19
+ - The CI verify gate (predeploy gate 1) ran BEFORE the test that overwrote the key
20
+ - The local maintainer always had `.keys/private.pem` present, so `--fix` was a no-op locally → local verify always passed
21
+ - npm-installed operators ran `exceptd doctor --signatures` and saw 0/38, but no CI gate caught the broken tarball before publish
22
+ - The new `verify-shipped-tarball` gate (v0.12.3) caught the symptom but the forensic logging in v0.12.4 was the first time we saw HEAD's public.pem fingerprint differ from the source-tree pubkey 19 seconds later in the same CI run
23
+
24
+ ### The fix
25
+
26
+ Pre-stage a dummy `.keys/private.pem` before invoking `doctor --fix` in the test, so `lib/sign.js generate-keypair` sees "private key already present" and exits before any key write. Restore the pre-test state in `finally{}`. The test still asserts the verb is registered + emits JSON, which is the only thing the smoke check needs to verify.
27
+
28
+ ### Why v0.12.3 and v0.12.4 didn't fix it
29
+
30
+ v0.12.3 added the `verify-shipped-tarball` gate which correctly BLOCKED the broken publish. v0.12.4 added per-file forensic logging which surfaced the exact divergence (source-tree fingerprint at gate 1 vs. gate 14). Neither release attempted to fix the root cause because we hadn't yet localized it to `doctor --fix` invocation inside a test. v0.12.5 is the actual fix.
31
+
32
+ ### Operator impact
33
+
34
+ This release SHOULD publish cleanly — the test no longer mutates `keys/public.pem` during the suite, so the post-test source tree matches the pre-test source tree, the packed tarball signatures verify against the packed public key, and the gate passes. Operators running `exceptd doctor --signatures` on v0.12.5 should see `38/38 skills passed Ed25519 verification` for the first time since v0.11.0.
35
+
36
+ ### Lessons codified in CLAUDE.md
37
+
38
+ - "Tests that invoke a real CLI verb that mutates filesystem state outside the test's tempdir are a CI-vs-local divergence engine." Always sandbox key-writing CLI invocations.
39
+ - "Smoke tests should not exercise mutating code paths." A test named `*is registered (smoke)` should only verify dispatch, not run the verb's side effects.
40
+
41
+ ## 0.12.4 — 2026-05-13
42
+
43
+ **Patch: forensic instrumentation for the signature-regression gate. v0.12.3 publish was blocked by the gate; v0.12.4 adds the diagnostic data needed to pinpoint the root cause on the next CI run.**
44
+
45
+ The v0.12.3 release was blocked at the new `verify-shipped-tarball` gate — exactly the behavior intended (better blocked publish than silent broken tarball). But the gate didn't log enough detail to pinpoint WHICH files diverge between source-tree and npm-packed tarball in CI. v0.12.4 adds per-file forensics + a working-tree drift dump.
46
+
47
+ ### What's new
48
+
49
+ - `scripts/verify-shipped-tarball.js`: on signature-fail, logs the size + sha256 of both the tarball-extracted content AND the source-tree content, plus whether the bytes are equal. Local pass-paths unchanged.
50
+ - `.github/workflows/release.yml`: new "Forensic — working-tree drift since checkout" step (runs `if: always()` so it fires even when prior gates fail). Dumps `git status --porcelain` + `git diff --stat HEAD` + `ls -la` of the case-mixed skill directory. The next CI failure surfaces the exact file-level divergence.
51
+
52
+ ### Why this isn't the root-cause fix
53
+
54
+ The bug is platform-specific: local `npm pack` on Windows produces a tarball that verifies 38/38. CI's `npm pack` on Ubuntu produces a tarball that verifies 0/38 — even though pubkey fingerprints match between source and tarball. The content drift has to be in a file the manifest signatures cover, but the signed bytes match between Windows and Linux (`.gitattributes` LF-normalizes). Forensics on the next run should make it obvious; this release ships the instrumentation, not the underlying fix.
55
+
56
+ ### Operator impact
57
+
58
+ v0.12.2 remains the latest npm-published version. Operators who ran `npm install -g @blamejs/exceptd-skills` see 0/38 verify on `exceptd doctor --signatures`. Until v0.12.4 (or later) publishes successfully, the integrity gate is open. Mitigations:
59
+
60
+ - `exceptd run`, `exceptd ci`, etc. do NOT block on signature verification — they continue to function with the catalog content as installed. The skill bytes themselves are intact (npm has its own tarball integrity check; only the per-skill Ed25519 attestation layer is broken).
61
+ - For audit purposes: the supply-chain trust anchor through npm provenance (OIDC + sigstore via `npm publish --provenance`) is unaffected. Confirm with `npm view @blamejs/exceptd-skills attestations`.
62
+
63
+ ### Shai-Hulud source audit (open question, not in this release)
64
+
65
+ The original Shai-Hulud campaign (2024) and Mini Shai-Hulud (CVE-2026-45321, 2026-05-11) are documented in public security research. v0.11.15 added CVE-2026-45321 to the catalog based on the description of the attack, not from a line-by-line reading of the published payload. Cross-referencing the actual payload source for IoCs we may have missed is scoped for v0.12.5:
66
+
67
+ - Walk the published worm source line-by-line; enumerate every credential path, every persistence vector, every C2 indicator.
68
+ - Compare against `data/cve-catalog.json:CVE-2026-45321.iocs` and the seven detect indicators in `data/playbooks/sbom.json` we ship.
69
+ - Add any missing patterns as additional indicators; update CHANGELOG with the line-level diff.
70
+
71
+ Same audit pattern should be applied to Copy Fail (CVE-2026-31431) and other open-sourced CVEs the catalog references — currently every CVE entry was assembled from secondary sources (advisories, NVD descriptions) rather than primary-source code review. v0.12.5 codifies the "primary-source review required before catalog entry" rule in AGENTS.md Hard Rule #14.
72
+
73
+ ## 0.12.3 — 2026-05-13
74
+
75
+ **Patch: critical signature-verification regression fix + 14th predeploy gate to prevent recurrence.**
76
+
77
+ ### The critical bug
78
+
79
+ Every release from v0.11.x through v0.12.2 shipped a tarball whose `keys/public.pem` did not match the Ed25519 signatures inside `manifest.json`. The result: `node lib/verify.js` against a fresh `npm install` reported `0/38 skills passed Ed25519 verification` and every skill listed as `TAMPERED`. Verification was silently bypassed by `exceptd run`, `exceptd ci`, etc. (which load skills without re-verifying), so the surface was only visible to operators running `exceptd doctor --signatures`.
80
+
81
+ ### What broke
82
+
83
+ The CI release workflow's `verify` step ran against the SOURCE tree (which had matching signatures + public key). It passed `38/38`. But the tarball that `npm publish` actually uploaded ended up with a different `public.pem` than the source tree. Verifying-on-source-tree is not the same as verifying-on-shipped-tarball. The mismatch went undetected for the entire v0.11.x and v0.12.x series.
84
+
85
+ ### The fix
86
+
87
+ - `scripts/verify-shipped-tarball.js` — packs the package via `npm pack`, extracts the tarball to a temp dir, and runs Ed25519 verify against the **extracted tree**. Catches any divergence between source-tree state and shipped-tarball state. Logs both fingerprints (source vs. tarball) so any future mismatch is forensically obvious.
88
+ - Wired in as **the 14th predeploy gate** so local maintainers + CI both run it. A release that produces a broken tarball now blocks before `npm publish` instead of shipping silently.
89
+ - v0.12.3 re-signs every skill against the current public key, then runs the new gate to confirm the round-trip is clean.
90
+
91
+ ### Other fixes
92
+
93
+ - **#137**: help text bumped from `v0.11.0 canonical surface` → `v0.12.0 canonical surface`.
94
+ - **#136 (text part)**: legacy-verb removal target moved from v0.12 → v0.13 in help text and deprecation banner. Actually removing the verbs is scope for a future release.
95
+ - **#135 (the run-with-no-evidence exit-0 case)**: deferred to v0.12.4. The fix is straightforward (have `run` exit 3 when classification: inconclusive AND no observations submitted, matching `ci`'s semantic) but changes the `run` verb's contract, which deserves a focused release that also documents the behavior change.
96
+
97
+ ### Lesson codified in CLAUDE.md
98
+
99
+ "Verify-on-source-tree is not verify-on-shipped-tarball." Any project that signs artifacts must verify the EXACT bytes that downstream consumers receive, after `npm pack` (or equivalent packaging step). The next-easiest place to lose integrity is the file-set transformation between `git checkout` and the registry upload — and that transformation runs in CI, where the maintainer has the least visibility.
100
+
3
101
  ## 0.12.2 — 2026-05-13
4
102
 
5
103
  **Patch: end-to-end scenario gate — staged-IoC harness in release workflow.**
package/bin/exceptd.js CHANGED
@@ -110,7 +110,7 @@ const PLAYBOOK_VERBS = new Set([
110
110
  // v0.11.0 canonical surface:
111
111
  "brief", "run", "ai-run", "attest", "discover", "doctor", "ci", "ask",
112
112
  "verify-attestation", "run-all", "lint",
113
- // v0.10.x legacy verbs — kept as aliases with deprecation banner, removed in v0.12+:
113
+ // v0.10.x legacy verbs — kept as aliases with deprecation banner, scheduled for removal in v0.13:
114
114
  "plan", "govern", "direct", "look", "ingest", "reattest", "list-attestations",
115
115
  ]);
116
116
 
@@ -176,7 +176,7 @@ function printHelp() {
176
176
  Usage: exceptd <command> [args]
177
177
  npx @blamejs/exceptd-skills <command> [args]
178
178
 
179
- v0.11.0 canonical surface
179
+ v0.12.0 canonical surface
180
180
  ─────────────────────────
181
181
 
182
182
  brief [playbook] Unified info doc — jurisdictions + threat context
@@ -267,7 +267,7 @@ v0.11.0 canonical surface
267
267
  Sources: kev|epss|nvd|rfc|pins|ghsa (v0.12.0).
268
268
  ghsa drafts pass validator as warnings.
269
269
 
270
- v0.10.x compatibility (will be removed in v0.12)
270
+ v0.10.x compatibility (will be removed in v0.13)
271
271
  ────────────────────────────────────────────────
272
272
 
273
273
  These verbs still work but emit a one-time deprecation banner. The
@@ -394,7 +394,7 @@ function main() {
394
394
  (haveBrief
395
395
  ? `Prefer \`${LEGACY_VERB_REPLACEMENTS[cmd]}\` (available in this install, v${ver}). `
396
396
  : `Upgrade to v0.11.0+ then use \`${LEGACY_VERB_REPLACEMENTS[cmd]}\` (currently installed: v${ver}). `) +
397
- `Legacy verbs remain functional through this release; they will be removed in v0.12. ` +
397
+ `Legacy verbs remain functional through this release; they will be removed in v0.13. ` +
398
398
  `Suppress: export EXCEPTD_DEPRECATION_SHOWN=1.\n`
399
399
  );
400
400
  process.env.EXCEPTD_DEPRECATION_SHOWN = "1";
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-13T02:49:16.936Z",
3
+ "generated_at": "2026-05-13T03:34:45.737Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "c607b3254ea45ed898b325a4bacbfc1076d2669e813e2d4dcbbd9d6ab0cf73ec",
7
+ "manifest.json": "694af5663344e76c17f8de1953aa388246a49502ad7b8d49b4d33c8ce8709610",
8
8
  "data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
9
9
  "data/cve-catalog.json": "e9a3a4ce988caa051e50a467f1cd9c0dcbf9e8f6f3e9522610baf196217b7bdc",
10
10
  "data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
package/keys/public.pem CHANGED
@@ -1,3 +1,3 @@
1
1
  -----BEGIN PUBLIC KEY-----
2
- MCowBQYDK2VwAyEALm7uQSdGjE9NSorvxoDnbqolbQaRXyGrgb82J5gUHhA=
2
+ MCowBQYDK2VwAyEAwEeCeTewS5TXFoRNRX4VKRHZL14bwdTVOZoMcedzq5s=
3
3
  -----END PUBLIC KEY-----
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "_comment": "Auto-generated by scripts/refresh-manifest-snapshot.js — do not hand-edit. Public skill surface used by check-manifest-snapshot.js to detect breaking removals.",
3
- "_generated_at": "2026-05-13T02:48:32.579Z",
3
+ "_generated_at": "2026-05-13T03:33:47.456Z",
4
4
  "atlas_version": "5.1.0",
5
5
  "skill_count": 38,
6
6
  "skills": [
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.12.2",
3
+ "version": "0.12.5",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
5
5
  "homepage": "https://exceptd.com",
6
6
  "license": "Apache-2.0",
@@ -52,7 +52,7 @@
52
52
  ],
53
53
  "last_threat_review": "2026-05-01",
54
54
  "signature": "Xk593pj7my6wPJbQBE47khpIUrPsp6N1lW7cE2T/VPPF5T+8C1yGKc9B8VphD7Q08yWFcbwF6HoWpA/+4uG9DA==",
55
- "signed_at": "2026-05-13T02:48:32.136Z",
55
+ "signed_at": "2026-05-13T03:33:37.992Z",
56
56
  "cwe_refs": [
57
57
  "CWE-125",
58
58
  "CWE-362",
@@ -116,7 +116,7 @@
116
116
  ],
117
117
  "last_threat_review": "2026-05-01",
118
118
  "signature": "nOgUu+LK9fy6ASTCoRGtx3ttgjZCl7WIkKu2wu06JEKVSpL2cKU3ex2tmVAvv11LBmpTH+b/0zvqXlzcxzHnCw==",
119
- "signed_at": "2026-05-13T02:48:32.138Z",
119
+ "signed_at": "2026-05-13T03:33:37.994Z",
120
120
  "cwe_refs": [
121
121
  "CWE-1039",
122
122
  "CWE-1426",
@@ -179,7 +179,7 @@
179
179
  ],
180
180
  "last_threat_review": "2026-05-01",
181
181
  "signature": "7FH1J9PlOyvcRCzRmggmenX9fIR0pi/veXihb3TeStcq1Rpuz1KHdOcJLqA9su4t2goYukKKCXHV6hx8hzplAA==",
182
- "signed_at": "2026-05-13T02:48:32.138Z",
182
+ "signed_at": "2026-05-13T03:33:37.995Z",
183
183
  "cwe_refs": [
184
184
  "CWE-22",
185
185
  "CWE-345",
@@ -225,7 +225,7 @@
225
225
  "framework_gaps": [],
226
226
  "last_threat_review": "2026-05-01",
227
227
  "signature": "FqTRjHfEgw56pyHnyWzNtnhzDMEePBtmuamtW/iyX+h4yqbvP4Fyr7NRjRs3EgqT4j7oHuEZhV9Jt6ZTBgN4AA==",
228
- "signed_at": "2026-05-13T02:48:32.138Z"
228
+ "signed_at": "2026-05-13T03:33:37.995Z"
229
229
  },
230
230
  {
231
231
  "name": "compliance-theater",
@@ -256,7 +256,7 @@
256
256
  ],
257
257
  "last_threat_review": "2026-05-01",
258
258
  "signature": "3fN4yotiIIq76PVTHwozCu28TzDZvWule6vX8SXUT3XXbIBSuvAO0M/euvc3pw3TdZ2UNf78dI18lOCNdJ0aAg==",
259
- "signed_at": "2026-05-13T02:48:32.139Z"
259
+ "signed_at": "2026-05-13T03:33:37.996Z"
260
260
  },
261
261
  {
262
262
  "name": "exploit-scoring",
@@ -285,7 +285,7 @@
285
285
  ],
286
286
  "last_threat_review": "2026-05-01",
287
287
  "signature": "yZfpk4lQMRXegj2ADWjMmZTchUN6Lxpv587O/0JMzbNkXQtD6FrSAQOBWjx8S7uQ/sTntxgGN7aQQDLxL9RWAA==",
288
- "signed_at": "2026-05-13T02:48:32.139Z"
288
+ "signed_at": "2026-05-13T03:33:37.996Z"
289
289
  },
290
290
  {
291
291
  "name": "rag-pipeline-security",
@@ -322,7 +322,7 @@
322
322
  ],
323
323
  "last_threat_review": "2026-05-01",
324
324
  "signature": "ABHkoqee67KdUyDZ3bvF+/DNxjGhPR/ehT6pfOnmUIMmkcQFHpZ0OUVXKiFUANaLgKLP1vg0VEmHOoxpNA3vAA==",
325
- "signed_at": "2026-05-13T02:48:32.140Z",
325
+ "signed_at": "2026-05-13T03:33:37.996Z",
326
326
  "cwe_refs": [
327
327
  "CWE-1395",
328
328
  "CWE-1426"
@@ -379,7 +379,7 @@
379
379
  ],
380
380
  "last_threat_review": "2026-05-01",
381
381
  "signature": "+Nd/2tgBnW+mEGX84QvkgR2To2J7kA+lB63BsADDKeCXeebFv6Vo9H1P4vyUkKHfe4fP0ndpy3agIZcUO/e/Dg==",
382
- "signed_at": "2026-05-13T02:48:32.140Z",
382
+ "signed_at": "2026-05-13T03:33:37.996Z",
383
383
  "d3fend_refs": [
384
384
  "D3-CA",
385
385
  "D3-CSPP",
@@ -414,7 +414,7 @@
414
414
  "framework_gaps": [],
415
415
  "last_threat_review": "2026-05-01",
416
416
  "signature": "VMNGFvowXLbBjZp5nvWloKkqyqHKhnSzbVRU3gX9quOZJHH56w2M4id+oDsXIjR0CfRRb7eXl/so0Hq4xLBuBQ==",
417
- "signed_at": "2026-05-13T02:48:32.140Z",
417
+ "signed_at": "2026-05-13T03:33:37.997Z",
418
418
  "cwe_refs": [
419
419
  "CWE-1188"
420
420
  ]
@@ -442,7 +442,7 @@
442
442
  "framework_gaps": [],
443
443
  "last_threat_review": "2026-05-01",
444
444
  "signature": "5MaJs7gPCuFlK4oAttLulAPOA1noeV+xD/UqVWaVyRedXZgebBGKjnlE2t1qmTugvxlNIfeAnBZapk+Wz3VAAg==",
445
- "signed_at": "2026-05-13T02:48:32.141Z"
445
+ "signed_at": "2026-05-13T03:33:37.997Z"
446
446
  },
447
447
  {
448
448
  "name": "global-grc",
@@ -474,7 +474,7 @@
474
474
  "framework_gaps": [],
475
475
  "last_threat_review": "2026-05-01",
476
476
  "signature": "S/YXUpI/mcG2FpdUTgMsccWBtTaR5A4Ph4QFQw31S9w9Hn/z3sOFHLkb1B5YSwlg+mMOtSIxMdet1eLGSZkTDg==",
477
- "signed_at": "2026-05-13T02:48:32.141Z"
477
+ "signed_at": "2026-05-13T03:33:37.997Z"
478
478
  },
479
479
  {
480
480
  "name": "zeroday-gap-learn",
@@ -501,7 +501,7 @@
501
501
  "framework_gaps": [],
502
502
  "last_threat_review": "2026-05-01",
503
503
  "signature": "AKS+JsmhhBtytY2eIMuydjkZOYprWCmQ+RqxyxcVG9XcEI29ZSM/JbVIINQHozFl7OPPrOu1ouiTnk7LOJ86Bg==",
504
- "signed_at": "2026-05-13T02:48:32.141Z"
504
+ "signed_at": "2026-05-13T03:33:37.998Z"
505
505
  },
506
506
  {
507
507
  "name": "pqc-first",
@@ -553,7 +553,7 @@
553
553
  ],
554
554
  "last_threat_review": "2026-05-01",
555
555
  "signature": "oEkK5bLS/G5RIHnxlNFJYdzhTJbKZnkJv+W4iS9UJ/uszZHgZGoxygELPc4kn3FowV5eE988SQYG4WKlXtNzCg==",
556
- "signed_at": "2026-05-13T02:48:32.142Z",
556
+ "signed_at": "2026-05-13T03:33:37.998Z",
557
557
  "cwe_refs": [
558
558
  "CWE-327"
559
559
  ],
@@ -600,7 +600,7 @@
600
600
  ],
601
601
  "last_threat_review": "2026-05-01",
602
602
  "signature": "nPV6YTo1rsNH49qUnZpfoNLEQZXuLNyV05QMUOgXKHYeVDjotYpWhLgyVXlRhjV/fStiA2sWQ0MOnEJ4FBIfDg==",
603
- "signed_at": "2026-05-13T02:48:32.142Z"
603
+ "signed_at": "2026-05-13T03:33:37.999Z"
604
604
  },
605
605
  {
606
606
  "name": "security-maturity-tiers",
@@ -637,7 +637,7 @@
637
637
  ],
638
638
  "last_threat_review": "2026-05-01",
639
639
  "signature": "7rirSEONz6O9Yyf46eTyuwkGizCj9FRcNHe5p7Qz6nhJoZQRW5FwW7n9opL0WlbIw8FDBYn1f22zgNUV87L5AQ==",
640
- "signed_at": "2026-05-13T02:48:32.143Z",
640
+ "signed_at": "2026-05-13T03:33:37.999Z",
641
641
  "cwe_refs": [
642
642
  "CWE-1188"
643
643
  ]
@@ -672,7 +672,7 @@
672
672
  "framework_gaps": [],
673
673
  "last_threat_review": "2026-05-11",
674
674
  "signature": "+evehnd2wSBb8uMTlTr5/aTN4bfLjsKzZJk/+OMLMOJrjCt+OuMU7EQC6xMUGeSc4cPEGajghDvq3xVaacV2Dw==",
675
- "signed_at": "2026-05-13T02:48:32.143Z"
675
+ "signed_at": "2026-05-13T03:33:37.999Z"
676
676
  },
677
677
  {
678
678
  "name": "attack-surface-pentest",
@@ -743,7 +743,7 @@
743
743
  "PTES revision incorporating AI-surface enumeration"
744
744
  ],
745
745
  "signature": "KHOXxloAYf7xqXjm2BaL3HVAZOmb7rMiMh20H/oaIkjN0WD1CnKCrRGPJn867uSFhCh/timkXolaiqD1L/h8Dg==",
746
- "signed_at": "2026-05-13T02:48:32.143Z"
746
+ "signed_at": "2026-05-13T03:33:38.000Z"
747
747
  },
748
748
  {
749
749
  "name": "fuzz-testing-strategy",
@@ -803,7 +803,7 @@
803
803
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
804
804
  ],
805
805
  "signature": "+ELdD+1AY5DymBitH7wU65CS60NY1nDoLowJAFn7cE5Gr/5jy9BTkyxsm7PEXaSlXWMOkTf/HQ+uyzyxUVD/Bw==",
806
- "signed_at": "2026-05-13T02:48:32.143Z"
806
+ "signed_at": "2026-05-13T03:33:38.000Z"
807
807
  },
808
808
  {
809
809
  "name": "dlp-gap-analysis",
@@ -878,7 +878,7 @@
878
878
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
879
879
  ],
880
880
  "signature": "8tFAhXAS8zZN3SUOdn+ZIu7lQ48JMOyBQ8SaObR3L/fDyFmDhufqleY2VzI3yigqlT/D4Y8FYxZHKmzXiALjDw==",
881
- "signed_at": "2026-05-13T02:48:32.144Z"
881
+ "signed_at": "2026-05-13T03:33:38.000Z"
882
882
  },
883
883
  {
884
884
  "name": "supply-chain-integrity",
@@ -955,7 +955,7 @@
955
955
  "OpenSSF model-signing — emerging Sigstore-based signing standard for ML model weights; track for production adoption"
956
956
  ],
957
957
  "signature": "YhvlD+6gdFGg7P6QtpWeb0n54/Ujlxc7I6o/bXtpkfPiy/JY4OJo5xdreb+mbytHkasmUErL5LsDtTCAVq0QAA==",
958
- "signed_at": "2026-05-13T02:48:32.144Z"
958
+ "signed_at": "2026-05-13T03:33:38.000Z"
959
959
  },
960
960
  {
961
961
  "name": "defensive-countermeasure-mapping",
@@ -1012,7 +1012,7 @@
1012
1012
  ],
1013
1013
  "last_threat_review": "2026-05-11",
1014
1014
  "signature": "AMdLkDx/e3ESI4NAnJhhcaas+Ru8VjrSn6v6RBbmmzoLCGo/vFxGraa1p/qF9udhVG+DdkbwHfbfKK5Im19KDw==",
1015
- "signed_at": "2026-05-13T02:48:32.145Z"
1015
+ "signed_at": "2026-05-13T03:33:38.001Z"
1016
1016
  },
1017
1017
  {
1018
1018
  "name": "identity-assurance",
@@ -1079,7 +1079,7 @@
1079
1079
  "d3fend_refs": [],
1080
1080
  "last_threat_review": "2026-05-11",
1081
1081
  "signature": "pSMHKkyWoZvRIuVtN7Vue51sP5MIy9lSaQa2YSAMhxjptx81cUnPt3S11/Tb9Ea1/eluMNQ+5F25eF2njr4mBQ==",
1082
- "signed_at": "2026-05-13T02:48:32.145Z"
1082
+ "signed_at": "2026-05-13T03:33:38.001Z"
1083
1083
  },
1084
1084
  {
1085
1085
  "name": "ot-ics-security",
@@ -1135,7 +1135,7 @@
1135
1135
  "d3fend_refs": [],
1136
1136
  "last_threat_review": "2026-05-11",
1137
1137
  "signature": "qjky+ZTX1DP7uRRMQZq7S7P9/uaJEoB1dy4RZ1l37Q4OO3k2ryfL+7o0Cgm/piuafJfH+dqUeNCRrVefj4r8Dw==",
1138
- "signed_at": "2026-05-13T02:48:32.146Z"
1138
+ "signed_at": "2026-05-13T03:33:38.001Z"
1139
1139
  },
1140
1140
  {
1141
1141
  "name": "coordinated-vuln-disclosure",
@@ -1187,7 +1187,7 @@
1187
1187
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1188
1188
  ],
1189
1189
  "signature": "F86Zl/I+dBzHYRUuGWsjDQI2F/I/vhzwZUFMqhNfKUzRbMf6mafOX2APCPYTp3eP1DvvvfL3Yc0hb1R5Q4nOAg==",
1190
- "signed_at": "2026-05-13T02:48:32.146Z"
1190
+ "signed_at": "2026-05-13T03:33:38.002Z"
1191
1191
  },
1192
1192
  {
1193
1193
  "name": "threat-modeling-methodology",
@@ -1237,7 +1237,7 @@
1237
1237
  "PASTA v2 updates incorporating AI/ML application threats"
1238
1238
  ],
1239
1239
  "signature": "D/4d5NcJScNH58ADXsSrVzTmLSWZpUZTdyhtDkJlC0twSMNczOiDsXgYFitBaZgGdv5nVd00viR45mNrsaZ4BQ==",
1240
- "signed_at": "2026-05-13T02:48:32.147Z"
1240
+ "signed_at": "2026-05-13T03:33:38.002Z"
1241
1241
  },
1242
1242
  {
1243
1243
  "name": "webapp-security",
@@ -1311,7 +1311,7 @@
1311
1311
  "d3fend_refs": [],
1312
1312
  "last_threat_review": "2026-05-11",
1313
1313
  "signature": "UOXaUtpcFjXyDQ70z2PaGu6K3pABtXp+7YzO6eGVGpN1CxXpPq/xW/CnTng6B7wk9WSsqD0OORBJp4VCjiVfAQ==",
1314
- "signed_at": "2026-05-13T02:48:32.147Z"
1314
+ "signed_at": "2026-05-13T03:33:38.002Z"
1315
1315
  },
1316
1316
  {
1317
1317
  "name": "ai-risk-management",
@@ -1361,7 +1361,7 @@
1361
1361
  "d3fend_refs": [],
1362
1362
  "last_threat_review": "2026-05-11",
1363
1363
  "signature": "IVKygsrFjiM64fQVbd2PT6jDjs6fm5nKwJSqGfK53gG0S9wdHC4QYuh+LWlI/2ftvIKjjedLQ6FRyTrqpDEuDw==",
1364
- "signed_at": "2026-05-13T02:48:32.147Z"
1364
+ "signed_at": "2026-05-13T03:33:38.003Z"
1365
1365
  },
1366
1366
  {
1367
1367
  "name": "sector-healthcare",
@@ -1421,7 +1421,7 @@
1421
1421
  "d3fend_refs": [],
1422
1422
  "last_threat_review": "2026-05-11",
1423
1423
  "signature": "P+CdSu8ZJCNUU4nTa09Voh2PcYF3y/AFJn4v7cjVIGo9FbbqO7MwvGN7cJ+aSRs2/3NMUXX4eupcODslxYyJDw==",
1424
- "signed_at": "2026-05-13T02:48:32.148Z"
1424
+ "signed_at": "2026-05-13T03:33:38.003Z"
1425
1425
  },
1426
1426
  {
1427
1427
  "name": "sector-financial",
@@ -1502,7 +1502,7 @@
1502
1502
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1503
1503
  ],
1504
1504
  "signature": "zpEfh181Sc0b0cvRf/31Ir1f8lD4V5tehTogO3TJMxdKmXu06IAK7hrhBcLA/jFBv3xDDwrWW3sHzChVhWDeDA==",
1505
- "signed_at": "2026-05-13T02:48:32.148Z"
1505
+ "signed_at": "2026-05-13T03:33:38.003Z"
1506
1506
  },
1507
1507
  {
1508
1508
  "name": "sector-federal-government",
@@ -1571,7 +1571,7 @@
1571
1571
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1572
1572
  ],
1573
1573
  "signature": "7NpQlPu1DkpY9f+Frv/LLBHWUUe/qTM80c+xeYDxOzweXhvJGE/dnDCjglYHTjxT82L9cVxzBezvLEne20UpBg==",
1574
- "signed_at": "2026-05-13T02:48:32.148Z"
1574
+ "signed_at": "2026-05-13T03:33:38.004Z"
1575
1575
  },
1576
1576
  {
1577
1577
  "name": "sector-energy",
@@ -1636,7 +1636,7 @@
1636
1636
  "ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
1637
1637
  ],
1638
1638
  "signature": "4rhyHN5HykK7MQUmhvaTeDGj6Qf5swDd5ry8foh4KBvTkRKxTI/XyxconFGm5FASnySGPLMxX6m4JZAq5wiNBg==",
1639
- "signed_at": "2026-05-13T02:48:32.149Z"
1639
+ "signed_at": "2026-05-13T03:33:38.004Z"
1640
1640
  },
1641
1641
  {
1642
1642
  "name": "api-security",
@@ -1705,7 +1705,7 @@
1705
1705
  "d3fend_refs": [],
1706
1706
  "last_threat_review": "2026-05-11",
1707
1707
  "signature": "hS1izPhETclITK7fp6R67dhy+wFDti/YsJ2M5I1gDjeWZYK41WuxeYSyt5xEHbCr3WCGDFJe77jkK1MWkxk2BA==",
1708
- "signed_at": "2026-05-13T02:48:32.149Z"
1708
+ "signed_at": "2026-05-13T03:33:38.004Z"
1709
1709
  },
1710
1710
  {
1711
1711
  "name": "cloud-security",
@@ -1786,7 +1786,7 @@
1786
1786
  "CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
1787
1787
  ],
1788
1788
  "signature": "kuatqNZoRnv+oeyrxbnk+m37JRBIgRAWnDp0/IYLnoBOybiG09RzLILJraxjhvdSNCgo7WXTeBO3Y6a3Ji9MAA==",
1789
- "signed_at": "2026-05-13T02:48:32.149Z"
1789
+ "signed_at": "2026-05-13T03:33:38.005Z"
1790
1790
  },
1791
1791
  {
1792
1792
  "name": "container-runtime-security",
@@ -1848,7 +1848,7 @@
1848
1848
  "d3fend_refs": [],
1849
1849
  "last_threat_review": "2026-05-11",
1850
1850
  "signature": "Btb3/7fjPFopFVdxP7+E6n322gnAAwd7OPrnuqatq6c1rXTD9aXKxiBeCmWxs8zYbIbE/lFoe9R2g6uTp8ZDBg==",
1851
- "signed_at": "2026-05-13T02:48:32.150Z"
1851
+ "signed_at": "2026-05-13T03:33:38.005Z"
1852
1852
  },
1853
1853
  {
1854
1854
  "name": "mlops-security",
@@ -1919,7 +1919,7 @@
1919
1919
  "MITRE ATLAS v5.2 — track AML.T0010 sub-technique expansion and any new MLOps-pipeline-specific TTPs"
1920
1920
  ],
1921
1921
  "signature": "TBWnlgdllW7K1F10HCJ7p4dbLeS3lyNWm+7mNNtyZu7jB1V5AauG1P7sb1nLLqwKqeGlHS1F0eh/BNiuAvkABg==",
1922
- "signed_at": "2026-05-13T02:48:32.151Z"
1922
+ "signed_at": "2026-05-13T03:33:38.005Z"
1923
1923
  },
1924
1924
  {
1925
1925
  "name": "incident-response-playbook",
@@ -1981,7 +1981,7 @@
1981
1981
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
1982
1982
  ],
1983
1983
  "signature": "FVAXpD6sIoOLQSPtZSLLsXQnc2o2hRwiFj4xK8zEWJVkUWGqvAWRrngie7O2DRKIbWqjO5h9EevVYSzhwYHCAA==",
1984
- "signed_at": "2026-05-13T02:48:32.151Z"
1984
+ "signed_at": "2026-05-13T03:33:38.006Z"
1985
1985
  },
1986
1986
  {
1987
1987
  "name": "email-security-anti-phishing",
@@ -2034,7 +2034,7 @@
2034
2034
  "d3fend_refs": [],
2035
2035
  "last_threat_review": "2026-05-11",
2036
2036
  "signature": "0HDt3Qklee4FQeKoZfwr+8qdq2pVDS0a+c7JxVw1hV/bl8+YTPaPjPTAhQUnbhUCa5cGo7G4MBQ1AifQTMJdDA==",
2037
- "signed_at": "2026-05-13T02:48:32.152Z"
2037
+ "signed_at": "2026-05-13T03:33:38.006Z"
2038
2038
  },
2039
2039
  {
2040
2040
  "name": "age-gates-child-safety",
@@ -2102,7 +2102,7 @@
2102
2102
  "US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
2103
2103
  ],
2104
2104
  "signature": "UyPSKUztZI/daHCRTnAh6ryoKLX4xyjuG+EaNMPRVuCz2gANGl1F/NozDsw7R2koMUwSFoiYTzwqDvo1tpuKAg==",
2105
- "signed_at": "2026-05-13T02:48:32.152Z"
2105
+ "signed_at": "2026-05-13T03:33:38.006Z"
2106
2106
  }
2107
2107
  ]
2108
2108
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.12.2",
3
+ "version": "0.12.5",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
5
5
  "keywords": [
6
6
  "ai-security",
package/sbom.cdx.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:f4ef44ca-296e-4961-bf84-1231c24d3c05",
4
+ "serialNumber": "urn:uuid:9dc98664-f302-4563-875e-0b2a119304d0",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2026-05-13T02:48:32.992Z",
7
+ "timestamp": "2026-05-13T03:33:47.860Z",
8
8
  "tools": [
9
9
  {
10
10
  "name": "hand-written",
@@ -13,10 +13,10 @@
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.12.2",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.12.5",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.12.2",
19
+ "version": "0.12.5",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 38 skills, 10 catalogs, 34 jurisdictions, pre-computed indexes, Ed25519-signed.",
21
21
  "licenses": [
22
22
  {
@@ -25,11 +25,11 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.12.2",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.12.5",
29
29
  "externalReferences": [
30
30
  {
31
31
  "type": "distribution",
32
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.12.2"
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.12.5"
33
33
  },
34
34
  {
35
35
  "type": "vcs",
@@ -139,6 +139,18 @@ const GATES = [
139
139
  args: [path.join(ROOT, "lib", "validate-package.js")],
140
140
  ciJobName: "Data integrity (catalog + manifest snapshot)",
141
141
  },
142
+ {
143
+ // v0.12.3 — packs the tarball, extracts it, runs Ed25519 verify on the
144
+ // EXTRACTED tree. Catches the class of bug where verify-on-source-tree
145
+ // passes (38/38) but verify-on-shipped-tarball fails (0/38) because
146
+ // something between sign and pack swapped keys/public.pem. Every release
147
+ // v0.11.x through v0.12.2 shipped this regression invisibly.
148
+ name: "Verify shipped tarball (sign + pack + extract + verify round-trip)",
149
+ command: process.execPath,
150
+ args: [path.join(ROOT, "scripts", "verify-shipped-tarball.js")],
151
+ ciJobName: "Data integrity (catalog + manifest snapshot)",
152
+ requiresKeys: true,
153
+ },
142
154
  ];
143
155
 
144
156
  /* Inline checker, run as `node -e`, so the predeploy gate stays one
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * scripts/verify-shipped-tarball.js
6
+ *
7
+ * Pack the package with `npm pack`, extract the tarball to a temp dir,
8
+ * then run lib/verify.js against the EXTRACTED tree (not the source
9
+ * working tree). This catches the class of bug where:
10
+ *
11
+ * - CI's verify step against the source tree passes (38/38)
12
+ * - The tarball that npm publish actually uploads has different
13
+ * content (e.g. keys/public.pem swapped) and verify-on-tarball fails
14
+ *
15
+ * Every release v0.11.x through v0.12.2 shipped a tarball whose
16
+ * keys/public.pem did not match the Ed25519 signatures in manifest.json.
17
+ * Operators installing from npm saw 0/38 verify on every fresh install.
18
+ * The bug was invisible because CI's verify ran against the SOURCE tree,
19
+ * not the shipped tarball. This gate closes that gap.
20
+ *
21
+ * Exit codes:
22
+ * 0 verify passed against the packed tarball
23
+ * 1 verify failed against the packed tarball (the bug class above)
24
+ * 2 pack or extract failed (infrastructure error)
25
+ *
26
+ * Zero npm deps. Node 24 stdlib only.
27
+ */
28
+
29
+ const fs = require("fs");
30
+ const path = require("path");
31
+ const os = require("os");
32
+ const { spawnSync } = require("child_process");
33
+
34
+ const ROOT = path.resolve(__dirname, "..");
35
+
36
+ function emit(msg) { process.stdout.write(`[verify-shipped-tarball] ${msg}\n`); }
37
+ function fail(msg, code = 1) {
38
+ process.stderr.write(`[verify-shipped-tarball] FAIL: ${msg}\n`);
39
+ process.exit(code);
40
+ }
41
+
42
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "verify-shipped-"));
43
+ try {
44
+ emit(`packing into ${tmpRoot} ...`);
45
+ const pack = spawnSync("npm", ["pack", "--pack-destination", tmpRoot], {
46
+ cwd: ROOT,
47
+ encoding: "utf8",
48
+ shell: process.platform === "win32",
49
+ });
50
+ if (pack.status !== 0) {
51
+ fail(`npm pack failed (exit ${pack.status}): ${pack.stderr || pack.stdout}`, 2);
52
+ }
53
+ const tarballName = pack.stdout.trim().split(/\r?\n/).filter(Boolean).pop();
54
+ const tarballPath = path.join(tmpRoot, tarballName);
55
+ if (!fs.existsSync(tarballPath)) fail(`expected tarball at ${tarballPath}, not found`, 2);
56
+ emit(`tarball: ${tarballPath} (${fs.statSync(tarballPath).size} bytes)`);
57
+
58
+ // Extract via Node — bypasses GNU tar's "C:..." path quirk on Windows
59
+ // where it interprets the colon as a remote-host separator.
60
+ const extractDir = path.join(tmpRoot, "extract");
61
+ fs.mkdirSync(extractDir, { recursive: true });
62
+ const zlib = require("zlib");
63
+ const { parseTar } = require(path.join(ROOT, "lib", "refresh-network.js"));
64
+ const tgz = fs.readFileSync(tarballPath);
65
+ const tarBuf = zlib.gunzipSync(tgz);
66
+ const entries = parseTar(tarBuf);
67
+ for (const e of entries) {
68
+ if (!e.name) continue;
69
+ const dst = path.join(extractDir, e.name);
70
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
71
+ fs.writeFileSync(dst, e.body);
72
+ }
73
+
74
+ const pkgRoot = path.join(extractDir, "package");
75
+ if (!fs.existsSync(path.join(pkgRoot, "lib", "verify.js"))) {
76
+ fail(`extracted tree missing lib/verify.js at ${pkgRoot}`, 2);
77
+ }
78
+ emit(`extracted to ${pkgRoot}`);
79
+
80
+ // Run the verifier inline against the extracted package tree. This avoids
81
+ // having to spawn a separate process whose cwd resolution differs across
82
+ // platforms.
83
+ const crypto = require("crypto");
84
+ const manifestPath = path.join(pkgRoot, "manifest.json");
85
+ const pubKeyPath = path.join(pkgRoot, "keys", "public.pem");
86
+ if (!fs.existsSync(manifestPath)) fail(`extracted tree missing manifest.json`, 2);
87
+ if (!fs.existsSync(pubKeyPath)) fail(`extracted tree missing keys/public.pem`, 2);
88
+
89
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
90
+ const pubPem = fs.readFileSync(pubKeyPath, "utf8");
91
+ const pubKey = crypto.createPublicKey(pubPem);
92
+ const pubFp = crypto.createHash("sha256")
93
+ .update(pubKey.export({ type: "spki", format: "der" }))
94
+ .digest("base64");
95
+
96
+ // Compute the same fingerprint for the SOURCE-tree public.pem so the log
97
+ // shows the divergence explicitly.
98
+ const sourcePubPem = fs.readFileSync(path.join(ROOT, "keys", "public.pem"), "utf8");
99
+ const sourcePubKey = crypto.createPublicKey(sourcePubPem);
100
+ const sourcePubFp = crypto.createHash("sha256")
101
+ .update(sourcePubKey.export({ type: "spki", format: "der" }))
102
+ .digest("base64");
103
+
104
+ emit(`source-tree public.pem fingerprint: SHA256:${sourcePubFp}`);
105
+ emit(`tarball public.pem fingerprint: SHA256:${pubFp}`);
106
+ if (pubFp !== sourcePubFp) {
107
+ emit(`*** WARNING: tarball public.pem differs from source-tree public.pem ***`);
108
+ emit(`*** Something between sign and pack is swapping the key. Verify will fail below. ***`);
109
+ }
110
+
111
+ let pass = 0, miss = 0, fail_count = 0;
112
+ const failures = [];
113
+ for (const s of (manifest.skills || [])) {
114
+ const skillPath = path.join(pkgRoot, s.path);
115
+ const sourceSkillPath = path.join(ROOT, s.path);
116
+ if (!fs.existsSync(skillPath)) {
117
+ miss++;
118
+ failures.push(`${s.name}: file not found at ${s.path}`);
119
+ continue;
120
+ }
121
+ const content = fs.readFileSync(skillPath);
122
+ const ok = crypto.verify(null, content, pubKey, Buffer.from(s.signature, "base64"));
123
+ if (ok) pass++;
124
+ else {
125
+ fail_count++;
126
+ // Forensic detail: log size + sha256 of tarball-extracted content vs source-tree content
127
+ // so we can pinpoint which bytes changed between npm pack and what was signed.
128
+ const tarSha = crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
129
+ let srcSha = "<missing>", srcSize = 0, srcContent;
130
+ if (fs.existsSync(sourceSkillPath)) {
131
+ srcContent = fs.readFileSync(sourceSkillPath);
132
+ srcSize = srcContent.length;
133
+ srcSha = crypto.createHash("sha256").update(srcContent).digest("hex").slice(0, 16);
134
+ }
135
+ const equal = srcContent && content.equals(srcContent) ? "equal" : "DIFFER";
136
+ failures.push(`${s.name}: signature did not verify (tarball size=${content.length} sha=${tarSha}; source size=${srcSize} sha=${srcSha}; bytes ${equal})`);
137
+ }
138
+ }
139
+
140
+ const total = (manifest.skills || []).length;
141
+ emit(`tarball verify result: ${pass}/${total} pass, ${fail_count} fail, ${miss} missing`);
142
+ if (fail_count === 0 && miss === 0 && pass === total) {
143
+ emit(`PASS — shipped tarball is internally consistent`);
144
+ process.exit(0);
145
+ }
146
+ for (const f of failures.slice(0, 10)) emit(` - ${f}`);
147
+ if (failures.length > 10) emit(` ... and ${failures.length - 10} more`);
148
+ emit(`FAIL — shipped tarball would be broken on every fresh install. Refusing to publish.`);
149
+ process.exit(1);
150
+ } finally {
151
+ // Best-effort cleanup; leave on failure for diagnostics.
152
+ if (process.exitCode === 0) {
153
+ try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
154
+ } else {
155
+ emit(`temp dir preserved for inspection: ${tmpRoot}`);
156
+ }
157
+ }