@blamejs/exceptd-skills 0.9.1

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 (136) hide show
  1. package/AGENTS.md +232 -0
  2. package/ARCHITECTURE.md +267 -0
  3. package/CHANGELOG.md +616 -0
  4. package/CONTEXT.md +203 -0
  5. package/LICENSE +200 -0
  6. package/NOTICE +82 -0
  7. package/README.md +307 -0
  8. package/SECURITY.md +73 -0
  9. package/agents/README.md +81 -0
  10. package/agents/report-generator.md +156 -0
  11. package/agents/skill-updater.md +102 -0
  12. package/agents/source-validator.md +119 -0
  13. package/agents/threat-researcher.md +149 -0
  14. package/bin/exceptd.js +183 -0
  15. package/data/_indexes/_meta.json +88 -0
  16. package/data/_indexes/activity-feed.json +362 -0
  17. package/data/_indexes/catalog-summaries.json +229 -0
  18. package/data/_indexes/chains.json +7135 -0
  19. package/data/_indexes/currency.json +359 -0
  20. package/data/_indexes/did-ladders.json +451 -0
  21. package/data/_indexes/frequency.json +2072 -0
  22. package/data/_indexes/handoff-dag.json +476 -0
  23. package/data/_indexes/jurisdiction-clocks.json +967 -0
  24. package/data/_indexes/jurisdiction-map.json +536 -0
  25. package/data/_indexes/recipes.json +319 -0
  26. package/data/_indexes/section-offsets.json +3656 -0
  27. package/data/_indexes/stale-content.json +14 -0
  28. package/data/_indexes/summary-cards.json +1736 -0
  29. package/data/_indexes/theater-fingerprints.json +381 -0
  30. package/data/_indexes/token-budget.json +2137 -0
  31. package/data/_indexes/trigger-table.json +1374 -0
  32. package/data/_indexes/xref.json +818 -0
  33. package/data/atlas-ttps.json +282 -0
  34. package/data/cve-catalog.json +496 -0
  35. package/data/cwe-catalog.json +1017 -0
  36. package/data/d3fend-catalog.json +738 -0
  37. package/data/dlp-controls.json +1039 -0
  38. package/data/exploit-availability.json +67 -0
  39. package/data/framework-control-gaps.json +1255 -0
  40. package/data/global-frameworks.json +2913 -0
  41. package/data/rfc-references.json +324 -0
  42. package/data/zeroday-lessons.json +377 -0
  43. package/keys/public.pem +3 -0
  44. package/lib/framework-gap.js +328 -0
  45. package/lib/job-queue.js +195 -0
  46. package/lib/lint-skills.js +536 -0
  47. package/lib/prefetch.js +372 -0
  48. package/lib/refresh-external.js +713 -0
  49. package/lib/schemas/cve-catalog.schema.json +151 -0
  50. package/lib/schemas/manifest.schema.json +106 -0
  51. package/lib/schemas/skill-frontmatter.schema.json +113 -0
  52. package/lib/scoring.js +149 -0
  53. package/lib/sign.js +197 -0
  54. package/lib/ttp-mapper.js +80 -0
  55. package/lib/validate-catalog-meta.js +198 -0
  56. package/lib/validate-cve-catalog.js +213 -0
  57. package/lib/validate-indexes.js +83 -0
  58. package/lib/validate-package.js +162 -0
  59. package/lib/validate-vendor.js +85 -0
  60. package/lib/verify.js +216 -0
  61. package/lib/worker-pool.js +84 -0
  62. package/manifest-snapshot.json +1833 -0
  63. package/manifest.json +2108 -0
  64. package/orchestrator/README.md +124 -0
  65. package/orchestrator/dispatcher.js +140 -0
  66. package/orchestrator/event-bus.js +146 -0
  67. package/orchestrator/index.js +874 -0
  68. package/orchestrator/pipeline.js +201 -0
  69. package/orchestrator/scanner.js +327 -0
  70. package/orchestrator/scheduler.js +137 -0
  71. package/package.json +113 -0
  72. package/sbom.cdx.json +158 -0
  73. package/scripts/audit-cross-skill.js +261 -0
  74. package/scripts/audit-perf.js +160 -0
  75. package/scripts/bootstrap.js +205 -0
  76. package/scripts/build-indexes.js +721 -0
  77. package/scripts/builders/activity-feed.js +79 -0
  78. package/scripts/builders/catalog-summaries.js +67 -0
  79. package/scripts/builders/currency.js +109 -0
  80. package/scripts/builders/cwe-chains.js +105 -0
  81. package/scripts/builders/did-ladders.js +149 -0
  82. package/scripts/builders/frequency.js +89 -0
  83. package/scripts/builders/jurisdiction-clocks.js +126 -0
  84. package/scripts/builders/recipes.js +159 -0
  85. package/scripts/builders/section-offsets.js +162 -0
  86. package/scripts/builders/stale-content.js +171 -0
  87. package/scripts/builders/summary-cards.js +166 -0
  88. package/scripts/builders/theater-fingerprints.js +198 -0
  89. package/scripts/builders/token-budget.js +96 -0
  90. package/scripts/check-manifest-snapshot.js +217 -0
  91. package/scripts/predeploy.js +267 -0
  92. package/scripts/refresh-manifest-snapshot.js +57 -0
  93. package/scripts/refresh-sbom.js +222 -0
  94. package/skills/age-gates-child-safety/skill.md +456 -0
  95. package/skills/ai-attack-surface/skill.md +282 -0
  96. package/skills/ai-c2-detection/skill.md +440 -0
  97. package/skills/ai-risk-management/skill.md +311 -0
  98. package/skills/api-security/skill.md +287 -0
  99. package/skills/attack-surface-pentest/skill.md +381 -0
  100. package/skills/cloud-security/skill.md +384 -0
  101. package/skills/compliance-theater/skill.md +365 -0
  102. package/skills/container-runtime-security/skill.md +379 -0
  103. package/skills/coordinated-vuln-disclosure/skill.md +473 -0
  104. package/skills/defensive-countermeasure-mapping/skill.md +300 -0
  105. package/skills/dlp-gap-analysis/skill.md +337 -0
  106. package/skills/email-security-anti-phishing/skill.md +206 -0
  107. package/skills/exploit-scoring/skill.md +331 -0
  108. package/skills/framework-gap-analysis/skill.md +374 -0
  109. package/skills/fuzz-testing-strategy/skill.md +313 -0
  110. package/skills/global-grc/skill.md +564 -0
  111. package/skills/identity-assurance/skill.md +272 -0
  112. package/skills/incident-response-playbook/skill.md +546 -0
  113. package/skills/kernel-lpe-triage/skill.md +303 -0
  114. package/skills/mcp-agent-trust/skill.md +326 -0
  115. package/skills/mlops-security/skill.md +325 -0
  116. package/skills/ot-ics-security/skill.md +340 -0
  117. package/skills/policy-exception-gen/skill.md +437 -0
  118. package/skills/pqc-first/skill.md +546 -0
  119. package/skills/rag-pipeline-security/skill.md +294 -0
  120. package/skills/researcher/skill.md +310 -0
  121. package/skills/sector-energy/skill.md +409 -0
  122. package/skills/sector-federal-government/skill.md +302 -0
  123. package/skills/sector-financial/skill.md +398 -0
  124. package/skills/sector-healthcare/skill.md +373 -0
  125. package/skills/security-maturity-tiers/skill.md +464 -0
  126. package/skills/skill-update-loop/skill.md +463 -0
  127. package/skills/supply-chain-integrity/skill.md +318 -0
  128. package/skills/threat-model-currency/skill.md +404 -0
  129. package/skills/threat-modeling-methodology/skill.md +312 -0
  130. package/skills/webapp-security/skill.md +281 -0
  131. package/skills/zeroday-gap-learn/skill.md +350 -0
  132. package/vendor/blamejs/LICENSE +201 -0
  133. package/vendor/blamejs/README.md +54 -0
  134. package/vendor/blamejs/_PROVENANCE.json +54 -0
  135. package/vendor/blamejs/retry.js +335 -0
  136. package/vendor/blamejs/worker-pool.js +418 -0
package/package.json ADDED
@@ -0,0 +1,113 @@
1
+ {
2
+ "name": "@blamejs/exceptd-skills",
3
+ "version": "0.9.1",
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
+ "keywords": [
6
+ "ai-security",
7
+ "ai-skills",
8
+ "atlas",
9
+ "att-ck",
10
+ "compliance",
11
+ "cve",
12
+ "cwe",
13
+ "d3fend",
14
+ "framework-gap",
15
+ "grc",
16
+ "kev",
17
+ "llm-security",
18
+ "mcp",
19
+ "nist",
20
+ "nvd",
21
+ "prompt-injection",
22
+ "rwep",
23
+ "security-research",
24
+ "skills",
25
+ "supply-chain",
26
+ "threat-intelligence",
27
+ "threat-modeling"
28
+ ],
29
+ "homepage": "https://exceptd.com",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/blamejs/exceptd-skills.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/blamejs/exceptd-skills/issues"
36
+ },
37
+ "license": "Apache-2.0",
38
+ "author": "blamejs contributors",
39
+ "engines": {
40
+ "node": ">=24.0.0 <25.0.0"
41
+ },
42
+ "bin": {
43
+ "exceptd": "bin/exceptd.js"
44
+ },
45
+ "main": "orchestrator/index.js",
46
+ "files": [
47
+ "bin/",
48
+ "lib/",
49
+ "orchestrator/",
50
+ "scripts/",
51
+ "vendor/",
52
+ "agents/",
53
+ "data/",
54
+ "skills/",
55
+ "keys/public.pem",
56
+ "manifest.json",
57
+ "manifest-snapshot.json",
58
+ "sbom.cdx.json",
59
+ "AGENTS.md",
60
+ "ARCHITECTURE.md",
61
+ "CONTEXT.md",
62
+ "CHANGELOG.md",
63
+ "LICENSE",
64
+ "NOTICE",
65
+ "README.md",
66
+ "SECURITY.md"
67
+ ],
68
+ "publishConfig": {
69
+ "access": "public",
70
+ "provenance": true,
71
+ "registry": "https://registry.npmjs.org/"
72
+ },
73
+ "scripts": {
74
+ "bootstrap": "node scripts/bootstrap.js",
75
+ "verify": "node lib/verify.js",
76
+ "verify:update": "node lib/verify.js update",
77
+ "test": "node --test --test-concurrency=1 \"tests/*.test.js\"",
78
+ "lint": "node lib/lint-skills.js",
79
+ "check-snapshot": "node scripts/check-manifest-snapshot.js",
80
+ "refresh-snapshot": "node scripts/refresh-manifest-snapshot.js",
81
+ "validate-catalog": "node lib/validate-cve-catalog.js",
82
+ "validate-catalog-meta": "node lib/validate-catalog-meta.js",
83
+ "validate-package": "node lib/validate-package.js",
84
+ "refresh-sbom": "node scripts/refresh-sbom.js",
85
+ "predeploy": "node scripts/predeploy.js",
86
+ "prepublishOnly": "node scripts/predeploy.js && node lib/validate-package.js",
87
+ "test:docker": "docker build --target predeploy -t exceptd-test:predeploy -f docker/test.Dockerfile . && docker run --rm exceptd-test:predeploy",
88
+ "test:docker:fresh": "docker build --target fresh-bootstrap -t exceptd-test:fresh-bootstrap -f docker/test.Dockerfile . && docker run --rm exceptd-test:fresh-bootstrap",
89
+ "scan": "node orchestrator/index.js scan",
90
+ "dispatch": "node orchestrator/index.js dispatch",
91
+ "currency": "node orchestrator/index.js currency",
92
+ "report": "node orchestrator/index.js report",
93
+ "watch": "node orchestrator/index.js watch",
94
+ "validate-cves": "node orchestrator/index.js validate-cves",
95
+ "validate-rfcs": "node orchestrator/index.js validate-rfcs",
96
+ "watchlist": "node orchestrator/index.js watchlist",
97
+ "build-indexes": "node scripts/build-indexes.js",
98
+ "validate-indexes": "node lib/validate-indexes.js",
99
+ "validate-vendor": "node lib/validate-vendor.js",
100
+ "audit-perf": "node scripts/audit-perf.js",
101
+ "audit-cross-skill": "node scripts/audit-cross-skill.js",
102
+ "refresh": "node lib/refresh-external.js",
103
+ "refresh:dry": "node lib/refresh-external.js",
104
+ "refresh:apply": "node lib/refresh-external.js --apply",
105
+ "refresh:offline": "node lib/refresh-external.js --from-fixture tests/fixtures/refresh",
106
+ "refresh:from-cache": "node lib/refresh-external.js --from-cache .cache/upstream",
107
+ "refresh:swarm": "node lib/refresh-external.js --swarm",
108
+ "prefetch": "node lib/prefetch.js",
109
+ "prefetch:dry": "node lib/prefetch.js --no-network"
110
+ },
111
+ "dependencies": {},
112
+ "devDependencies": {}
113
+ }
package/sbom.cdx.json ADDED
@@ -0,0 +1,158 @@
1
+ {
2
+ "bomFormat": "CycloneDX",
3
+ "specVersion": "1.6",
4
+ "serialNumber": "urn:uuid:986fb119-b9a5-4395-9a40-62fdd3ab0a7b",
5
+ "version": 1,
6
+ "metadata": {
7
+ "timestamp": "2026-05-11T23:30:00.623Z",
8
+ "tools": [
9
+ {
10
+ "name": "hand-written",
11
+ "version": "0.1.0",
12
+ "description": "SBOM generated from package.json + manual review (scripts/refresh-sbom.js)."
13
+ }
14
+ ],
15
+ "component": {
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.9.1",
17
+ "type": "application",
18
+ "name": "@blamejs/exceptd-skills",
19
+ "version": "0.9.1",
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
+ "licenses": [
22
+ {
23
+ "license": {
24
+ "id": "Apache-2.0"
25
+ }
26
+ }
27
+ ],
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.9.1",
29
+ "externalReferences": [
30
+ {
31
+ "type": "distribution",
32
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.9.1"
33
+ },
34
+ {
35
+ "type": "vcs",
36
+ "url": "git+https://github.com/blamejs/exceptd-skills.git"
37
+ }
38
+ ]
39
+ },
40
+ "properties": [
41
+ {
42
+ "name": "cyclonedx:dataflow:input",
43
+ "value": "data/atlas-ttps.json,data/cve-catalog.json,data/cwe-catalog.json,data/d3fend-catalog.json,data/dlp-controls.json,data/exploit-availability.json,data/framework-control-gaps.json,data/global-frameworks.json,data/rfc-references.json,data/zeroday-lessons.json"
44
+ },
45
+ {
46
+ "name": "exceptd:catalog:count",
47
+ "value": "10"
48
+ },
49
+ {
50
+ "name": "exceptd:skill:count",
51
+ "value": "38"
52
+ },
53
+ {
54
+ "name": "exceptd:integrity:method",
55
+ "value": "Ed25519 per-skill (lib/sign.js)"
56
+ },
57
+ {
58
+ "name": "exceptd:runtime:dependency:count",
59
+ "value": "0"
60
+ },
61
+ {
62
+ "name": "exceptd:devDependency:count",
63
+ "value": "0"
64
+ },
65
+ {
66
+ "name": "exceptd:vendor:count",
67
+ "value": "2"
68
+ },
69
+ {
70
+ "name": "exceptd:vendor:pin",
71
+ "value": "https://github.com/blamejs/blamejs@1442f17758a4bd511c63877561c0ffa759f66a87"
72
+ }
73
+ ]
74
+ },
75
+ "components": [
76
+ {
77
+ "bom-ref": "vendor:blamejs:retry.js",
78
+ "type": "library",
79
+ "name": "blamejs/retry.js",
80
+ "version": "1442f17758a4",
81
+ "description": "Vendored from blamejs/lib/retry.js (flattened + stripped). See vendor/blamejs/README.md.",
82
+ "licenses": [
83
+ {
84
+ "license": {
85
+ "id": "Apache-2.0"
86
+ }
87
+ }
88
+ ],
89
+ "hashes": [
90
+ {
91
+ "alg": "SHA-256",
92
+ "content": "2b9a7d99a8477740dade4e4490e436817c250a3d63e9d91898a9cdaaeff82ac4"
93
+ }
94
+ ],
95
+ "externalReferences": [
96
+ {
97
+ "type": "vcs",
98
+ "url": "https://github.com/blamejs/blamejs"
99
+ },
100
+ {
101
+ "type": "distribution",
102
+ "url": "https://github.com/blamejs/blamejs/blob/1442f17758a4bd511c63877561c0ffa759f66a87/lib/retry.js"
103
+ }
104
+ ],
105
+ "properties": [
106
+ {
107
+ "name": "exceptd:vendor:upstream_sha256_at_pin",
108
+ "value": "e307f19a9012265d71f25fcf8e43e595479d38d1358f4b607b052d0ce053abb0"
109
+ },
110
+ {
111
+ "name": "exceptd:vendor:strip_summary",
112
+ "value": "observability event sink (_emitEvent → no-op); audit chain hooks; lazy-require for safeAsync (replaced with stdlib AbortSignal-aware sleep); numeric-checks dep (replaced with two inline predicates); constants C.TIME.seconds(...) (replaced with literal ms values)"
113
+ }
114
+ ]
115
+ },
116
+ {
117
+ "bom-ref": "vendor:blamejs:worker-pool.js",
118
+ "type": "library",
119
+ "name": "blamejs/worker-pool.js",
120
+ "version": "1442f17758a4",
121
+ "description": "Vendored from blamejs/lib/worker-pool.js (flattened + stripped). See vendor/blamejs/README.md.",
122
+ "licenses": [
123
+ {
124
+ "license": {
125
+ "id": "Apache-2.0"
126
+ }
127
+ }
128
+ ],
129
+ "hashes": [
130
+ {
131
+ "alg": "SHA-256",
132
+ "content": "57d8f5c3bca23cd4ef2a16adf4107bc60c4da3fbc9bb861580edc76ed9a2d132"
133
+ }
134
+ ],
135
+ "externalReferences": [
136
+ {
137
+ "type": "vcs",
138
+ "url": "https://github.com/blamejs/blamejs"
139
+ },
140
+ {
141
+ "type": "distribution",
142
+ "url": "https://github.com/blamejs/blamejs/blob/1442f17758a4bd511c63877561c0ffa759f66a87/lib/worker-pool.js"
143
+ }
144
+ ],
145
+ "properties": [
146
+ {
147
+ "name": "exceptd:vendor:upstream_sha256_at_pin",
148
+ "value": "262f99e9cc3d4a8f4eba9ad3e28401e8a1f47f78040afa63c9b17eb998437171"
149
+ },
150
+ {
151
+ "name": "exceptd:vendor:strip_summary",
152
+ "value": "WorkerPoolError class (replaced with vanilla Error with `code` field); validate-opts dep (replaced with inline whitelist + type checks); numeric-bounds dep (replaced with inline predicates); audit event sink (_emitAudit → no-op stub); constants C.BYTES.kib / C.TIME.minutes (replaced with literal int values)"
153
+ }
154
+ ]
155
+ }
156
+ ],
157
+ "dependencies": []
158
+ }
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ /**
3
+ * scripts/audit-cross-skill.js
4
+ *
5
+ * Comprehensive cross-skill accuracy / bug audit. Run after any
6
+ * skill add / rename / dispatch-rewire. Surfaces:
7
+ *
8
+ * - manifest paths that don't exist on disk
9
+ * - skill directories on disk with no manifest entry
10
+ * - frontmatter `name` drift from manifest `name`
11
+ * - skills missing from the researcher dispatch table
12
+ * - skills missing from AGENTS.md Quick Skill Reference
13
+ * - version drift between package.json / manifest.json / CHANGELOG.md
14
+ * - manifest-snapshot.json drift from manifest.json
15
+ * - sbom.cdx.json drift from live skill / catalog counts
16
+ * - broken ref: any cwe_refs / d3fend_refs / framework_gaps / atlas_refs /
17
+ * rfc_refs / dlp_refs that doesn't resolve in its catalog
18
+ * - RFC catalog reverse-references that drift from manifest forward-refs
19
+ * - skill-update-loop "Affected skills" blocks referencing nonexistent skills
20
+ * - stale references to renamed skills in any tracked file
21
+ * - trigger collisions between skills (informational)
22
+ * - README badge count drift
23
+ *
24
+ * Exit non-zero on any finding (excluding trigger collisions which are informational).
25
+ *
26
+ * Usage: node scripts/audit-cross-skill.js
27
+ */
28
+
29
+ const fs = require("fs");
30
+ const path = require("path");
31
+
32
+ const ROOT = path.join(__dirname, "..");
33
+ const ABS = (p) => path.join(ROOT, p);
34
+
35
+ const issues = [];
36
+ const note = (s) => issues.push(s);
37
+
38
+ const manifest = JSON.parse(fs.readFileSync(ABS("manifest.json"), "utf8"));
39
+ const skills = manifest.skills;
40
+ const skillNames = new Set(skills.map((s) => s.name));
41
+
42
+ // 1. Manifest paths exist
43
+ for (const s of skills) {
44
+ if (!fs.existsSync(ABS(s.path))) {
45
+ note(`MANIFEST PATH MISSING: ${s.name} → ${s.path}`);
46
+ }
47
+ }
48
+
49
+ // 2. Disk skill dirs have manifest entries
50
+ const skillDirs = fs
51
+ .readdirSync(ABS("skills"))
52
+ .filter((d) => fs.existsSync(ABS(`skills/${d}/skill.md`)));
53
+ for (const d of skillDirs) {
54
+ if (!skillNames.has(d)) note(`ORPHAN SKILL FILE: skills/${d}/skill.md has no manifest entry`);
55
+ }
56
+
57
+ // 3. Frontmatter name == manifest name
58
+ for (const s of skills) {
59
+ const c = fs.readFileSync(ABS(s.path), "utf8");
60
+ const m = c.match(/^name:\s*(\S+)/m);
61
+ if (m && m[1] !== s.name) {
62
+ note(`NAME DRIFT: manifest "${s.name}" vs frontmatter "${m[1]}" in ${s.path}`);
63
+ }
64
+ }
65
+
66
+ // 4. Researcher dispatch covers every non-researcher skill
67
+ const researcher = fs.readFileSync(ABS("skills/researcher/skill.md"), "utf8");
68
+ const missingDispatch = [];
69
+ for (const n of skillNames) {
70
+ if (n === "researcher") continue;
71
+ if (!researcher.includes("`" + n + "`")) missingDispatch.push(n);
72
+ }
73
+ if (missingDispatch.length) {
74
+ note(`RESEARCHER DISPATCH GAPS: ${missingDispatch.join(", ")}`);
75
+ }
76
+
77
+ // 5. AGENTS.md Quick Skill Reference lists every skill
78
+ const agents = fs.readFileSync(ABS("AGENTS.md"), "utf8");
79
+ const missingAgents = [];
80
+ for (const n of skillNames) {
81
+ if (!agents.includes("| " + n + " |") && !agents.includes("|" + n + "|")) {
82
+ missingAgents.push(n);
83
+ }
84
+ }
85
+ if (missingAgents.length) {
86
+ note(`AGENTS.md QUICK REF GAPS: ${missingAgents.join(", ")}`);
87
+ }
88
+
89
+ // 6. Version triple agreement
90
+ const pkg = JSON.parse(fs.readFileSync(ABS("package.json"), "utf8"));
91
+ const cl = fs.readFileSync(ABS("CHANGELOG.md"), "utf8");
92
+ const clTop = cl.match(/^## (\d+\.\d+\.\d+)/m);
93
+ if (clTop && clTop[1] !== pkg.version) {
94
+ note(`VERSION DRIFT: package.json ${pkg.version} vs CHANGELOG top ${clTop[1]}`);
95
+ }
96
+ if (manifest.version !== pkg.version) {
97
+ note(`VERSION DRIFT: manifest.json ${manifest.version} vs package.json ${pkg.version}`);
98
+ }
99
+
100
+ // 7. Manifest snapshot matches manifest
101
+ const snap = JSON.parse(fs.readFileSync(ABS("manifest-snapshot.json"), "utf8"));
102
+ if (snap.skill_count !== skills.length) {
103
+ note(`SNAPSHOT DRIFT: snapshot ${snap.skill_count} vs manifest ${skills.length}`);
104
+ }
105
+ const snapNames = new Set(snap.skills.map((s) => s.name));
106
+ for (const n of skillNames) if (!snapNames.has(n)) note(`SNAPSHOT MISSING: ${n}`);
107
+ for (const n of snapNames) if (!skillNames.has(n)) note(`SNAPSHOT STALE: snapshot has "${n}" not in manifest`);
108
+
109
+ // 8. SBOM counts
110
+ const sbom = JSON.parse(fs.readFileSync(ABS("sbom.cdx.json"), "utf8"));
111
+ const sbomProps = Object.fromEntries(
112
+ (sbom.metadata.properties || []).map((p) => [p.name, p.value])
113
+ );
114
+ const sbomSkills = Number(sbomProps["exceptd:skill:count"]);
115
+ if (sbomSkills !== skills.length) {
116
+ note(`SBOM SKILL COUNT DRIFT: sbom ${sbomSkills} vs live ${skills.length}`);
117
+ }
118
+ const liveCatalogs = fs.readdirSync(ABS("data")).filter((f) => f.endsWith(".json")).length;
119
+ const sbomCatalogs = Number(sbomProps["exceptd:catalog:count"]);
120
+ if (sbomCatalogs !== liveCatalogs) {
121
+ note(`SBOM CATALOG COUNT DRIFT: sbom ${sbomCatalogs} vs live ${liveCatalogs}`);
122
+ }
123
+
124
+ // 9. Catalog ref resolution
125
+ function catKeys(p) {
126
+ return new Set(
127
+ Object.keys(JSON.parse(fs.readFileSync(ABS(p), "utf8"))).filter(
128
+ (k) => !k.startsWith("_")
129
+ )
130
+ );
131
+ }
132
+ const cweK = catKeys("data/cwe-catalog.json");
133
+ const d3K = catKeys("data/d3fend-catalog.json");
134
+ const fwK = catKeys("data/framework-control-gaps.json");
135
+ const atlasK = catKeys("data/atlas-ttps.json");
136
+ const rfcK = catKeys("data/rfc-references.json");
137
+ const dlpK = catKeys("data/dlp-controls.json");
138
+ for (const s of skills) {
139
+ for (const r of s.cwe_refs || []) if (!cweK.has(r)) note(`BAD CWE_REF: ${s.name} cites "${r}" not in cwe-catalog`);
140
+ for (const r of s.d3fend_refs || []) if (!d3K.has(r)) note(`BAD D3FEND_REF: ${s.name} cites "${r}" not in d3fend-catalog`);
141
+ for (const r of s.framework_gaps || []) if (!fwK.has(r)) note(`BAD FRAMEWORK_GAP: ${s.name} cites "${r}" not in framework-control-gaps`);
142
+ for (const r of s.atlas_refs || []) if (!atlasK.has(r)) note(`BAD ATLAS_REF: ${s.name} cites "${r}" not in atlas-ttps`);
143
+ for (const r of s.rfc_refs || []) if (!rfcK.has(r)) note(`BAD RFC_REF: ${s.name} cites "${r}" not in rfc-references`);
144
+ for (const r of s.dlp_refs || []) if (!dlpK.has(r)) note(`BAD DLP_REF: ${s.name} cites "${r}" not in dlp-controls`);
145
+ }
146
+
147
+ // 10. RFC catalog reverse-refs symmetric with manifest forward-refs
148
+ const rfcs = JSON.parse(fs.readFileSync(ABS("data/rfc-references.json"), "utf8"));
149
+ const forwardRfc = {};
150
+ for (const s of skills) {
151
+ for (const r of s.rfc_refs || []) (forwardRfc[r] = forwardRfc[r] || []).push(s.name);
152
+ }
153
+ for (const [rfcId, names] of Object.entries(forwardRfc)) {
154
+ const rev = new Set(rfcs[rfcId]?.skills_referencing || []);
155
+ for (const n of names) {
156
+ if (!rev.has(n)) note(`RFC REVERSE-REF MISSING: ${rfcId}.skills_referencing should include "${n}"`);
157
+ }
158
+ }
159
+ for (const rfcId of Object.keys(rfcs).filter((k) => !k.startsWith("_"))) {
160
+ for (const n of rfcs[rfcId].skills_referencing || []) {
161
+ if (!skillNames.has(n)) {
162
+ note(`RFC STALE REVERSE-REF: ${rfcId} lists "${n}" not in manifest`);
163
+ } else {
164
+ const s = skills.find((x) => x.name === n);
165
+ if (!(s.rfc_refs || []).includes(rfcId)) {
166
+ note(`RFC ASYMMETRIC: ${rfcId}.skills_referencing has "${n}" but skill rfc_refs doesn't include ${rfcId}`);
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ // 11. skill-update-loop Affected-skills blocks reference real skill names
173
+ const sul = fs.readFileSync(ABS("skills/skill-update-loop/skill.md"), "utf8");
174
+ const sulBlocks = [...sul.matchAll(/\*\*Affected skills.*?\*\*\s*([^\n]+)/g)];
175
+ for (const m of sulBlocks) {
176
+ const tokens = m[1].split(/[,;]/).map((s) => s.trim().replace(/[.()`*]/g, ""));
177
+ for (const tok of tokens) {
178
+ const first = tok.split(/\s/)[0];
179
+ if (!first) continue;
180
+ if (!/^[a-z][a-z0-9-]+$/.test(first)) continue;
181
+ if (skillNames.has(first)) continue;
182
+ if (["other", "any", "none", "skill", "existing", "and", "or", "the", "a", "an"].includes(first)) continue;
183
+ note(`SKILL-UPDATE-LOOP unknown affected-skill ref: "${first}"`);
184
+ }
185
+ }
186
+
187
+ // 12. Stale renamed-skill references
188
+ const staleTokens = ["age-gates-minor-safeguarding", "minor-safeguarding"];
189
+ const trackedDocs = [
190
+ "AGENTS.md", "CHANGELOG.md", "README.md",
191
+ "CONTEXT.md", "ARCHITECTURE.md", "MAINTAINERS.md",
192
+ "manifest.json", "manifest-snapshot.json",
193
+ ];
194
+ for (const f of trackedDocs) {
195
+ if (!fs.existsSync(ABS(f))) continue;
196
+ const body = fs.readFileSync(ABS(f), "utf8");
197
+ for (const tok of staleTokens) {
198
+ if (body.includes(tok)) {
199
+ // CHANGELOG legitimately records the rename in the 0.5.4 entry.
200
+ if (f === "CHANGELOG.md") continue;
201
+ note(`STALE RENAME REF in ${f}: contains "${tok}"`);
202
+ }
203
+ }
204
+ }
205
+ for (const s of skills) {
206
+ const body = fs.readFileSync(ABS(s.path), "utf8");
207
+ for (const tok of staleTokens) {
208
+ if (body.includes(tok)) note(`STALE RENAME REF in ${s.path}: contains "${tok}"`);
209
+ }
210
+ }
211
+
212
+ // 13. Trigger collisions (informational; not necessarily bugs)
213
+ const triggerOwners = {};
214
+ for (const s of skills) {
215
+ for (const t of s.triggers || []) {
216
+ const k = String(t).toLowerCase().trim();
217
+ (triggerOwners[k] = triggerOwners[k] || []).push(s.name);
218
+ }
219
+ }
220
+ const collisions = Object.entries(triggerOwners).filter(([, v]) => v.length > 1);
221
+
222
+ // 14. README badges
223
+ const readme = fs.readFileSync(ABS("README.md"), "utf8");
224
+ const badgeMatch = readme.match(/skills-(\d+)-/);
225
+ if (badgeMatch && Number(badgeMatch[1]) !== skills.length) {
226
+ note(`README BADGE DRIFT: shows skills-${badgeMatch[1]}- but manifest has ${skills.length}`);
227
+ }
228
+ const jurBadge = readme.match(/jurisdictions-(\d+)-/);
229
+ const liveJurs = (() => {
230
+ const g = JSON.parse(fs.readFileSync(ABS("data/global-frameworks.json"), "utf8"));
231
+ return Object.keys(g).filter((k) => !k.startsWith("_") && k !== "GLOBAL").length;
232
+ })();
233
+ if (jurBadge && Number(jurBadge[1]) !== liveJurs) {
234
+ note(`README BADGE DRIFT: shows jurisdictions-${jurBadge[1]}- but live count is ${liveJurs}`);
235
+ }
236
+
237
+ // 15. Researcher count claim
238
+ const cntClaim = researcher.match(/(\d+)\s+specialized skills downstream/);
239
+ if (cntClaim) {
240
+ const claimed = Number(cntClaim[1]);
241
+ const actual = skills.length - 1; // minus researcher itself
242
+ if (claimed !== actual) {
243
+ note(`RESEARCHER COUNT CLAIM: says ${claimed} downstream but actual is ${actual}`);
244
+ }
245
+ }
246
+
247
+ // Output
248
+ console.log("\n=== CROSS-SKILL AUDIT ===");
249
+ console.log(`Skills: ${skills.length}`);
250
+ console.log(`Catalogs: ${liveCatalogs}`);
251
+ console.log(`Trigger collisions: ${collisions.length} (informational)`);
252
+ for (const [t, owners] of collisions) {
253
+ console.log(` "${t}" → ${owners.join(", ")}`);
254
+ }
255
+ console.log(`\n=== ISSUES (${issues.length}) ===`);
256
+ if (issues.length === 0) {
257
+ console.log("zero issues");
258
+ } else {
259
+ for (const i of issues) console.log(" • " + i);
260
+ }
261
+ process.exit(issues.length === 0 ? 0 : 1);