@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
@@ -0,0 +1,721 @@
1
+ "use strict";
2
+ /**
3
+ * scripts/build-indexes.js
4
+ *
5
+ * Produces pre-computed indexes under `data/_indexes/` so AI consumers
6
+ * and downstream tooling don't have to scan all 38 skills + 10 catalogs
7
+ * to answer routine cross-reference questions.
8
+ *
9
+ * Outputs (17 total):
10
+ * xref.json — inverted index over cwe/d3fend/framework_gap/
11
+ * atlas/attack/rfc/dlp citations
12
+ * trigger-table.json — flat trigger → [skills]
13
+ * chains.json — pre-computed cross-walks per CVE and per CWE
14
+ * jurisdiction-map.json — jurisdiction → skills that reference it
15
+ * handoff-dag.json — cross-skill mention graph
16
+ * summary-cards.json — per-skill 100-word abstract
17
+ * section-offsets.json — per-skill byte/line offsets of every H2
18
+ * token-budget.json — approximate token cost per skill + section
19
+ * recipes.json — curated multi-skill recipes
20
+ * jurisdiction-clocks.json — normalized obligation × hours matrix
21
+ * did-ladders.json — canonical defense-in-depth ladders
22
+ * theater-fingerprints.json — structured compliance-theater pattern records
23
+ * currency.json — pre-computed skill currency snapshot
24
+ * frequency.json — citation-count tables per catalog field
25
+ * activity-feed.json — "what changed when" feed
26
+ * catalog-summaries.json — compact per-catalog summary cards
27
+ * stale-content.json — persisted stale-content findings
28
+ * _meta.json — SHA-256 of every source file for staleness
29
+ *
30
+ * Flags:
31
+ * (default) build all outputs
32
+ * --only <names> build only the comma-separated outputs (and
33
+ * anything they depend on)
34
+ * --changed build only outputs whose declared deps changed
35
+ * since the last _meta.json snapshot. Safe in CI:
36
+ * identical inputs always produce identical outputs.
37
+ * --parallel run independent builders concurrently via
38
+ * Promise.all (I/O concurrency, no worker threads).
39
+ * For CPU-bound fan-out, callers can compose with
40
+ * lib/worker-pool.js directly.
41
+ * --quiet suppress per-output log lines
42
+ *
43
+ * Re-build conditions:
44
+ * _meta.json records sha256 of every source file. validate-indexes
45
+ * (predeploy gate) re-hashes those and fails if any source changed
46
+ * after the last build. --changed reads that same table to decide what
47
+ * to rebuild.
48
+ *
49
+ * Index file naming convention: leading underscore marks them as derived
50
+ * (mirroring `_meta` in catalog files), so anyone scanning `data/` for
51
+ * primary data filters them out.
52
+ *
53
+ * Node 24 stdlib only — zero npm deps.
54
+ */
55
+
56
+ const fs = require("fs");
57
+ const path = require("path");
58
+ const crypto = require("crypto");
59
+
60
+ const ROOT = path.join(__dirname, "..");
61
+ const ABS = (p) => path.join(ROOT, p);
62
+ const IDX = ABS("data/_indexes");
63
+
64
+ if (!fs.existsSync(IDX)) fs.mkdirSync(IDX, { recursive: true });
65
+
66
+ function sha256(buf) {
67
+ return crypto.createHash("sha256").update(buf).digest("hex");
68
+ }
69
+
70
+ function writeJson(name, obj) {
71
+ fs.writeFileSync(path.join(IDX, name), JSON.stringify(obj, null, 2) + "\n", "utf8");
72
+ }
73
+
74
+ function readJson(p) {
75
+ return JSON.parse(fs.readFileSync(p, "utf8"));
76
+ }
77
+
78
+ function parseArgs(argv) {
79
+ const out = { only: null, changed: false, parallel: false, quiet: false, help: false };
80
+ for (let i = 2; i < argv.length; i++) {
81
+ const a = argv[i];
82
+ if (a === "--only") out.only = argv[++i];
83
+ else if (a.startsWith("--only=")) out.only = a.slice("--only=".length);
84
+ else if (a === "--changed") out.changed = true;
85
+ else if (a === "--parallel") out.parallel = true;
86
+ else if (a === "--quiet") out.quiet = true;
87
+ else if (a === "--help" || a === "-h") out.help = true;
88
+ }
89
+ return out;
90
+ }
91
+
92
+ function printHelp() {
93
+ console.log(`build-indexes — produce data/_indexes/*.json from canonical sources.
94
+
95
+ Flags:
96
+ (default) build all 17 outputs
97
+ --only <names> comma-separated subset (xref,chains,recipes,...)
98
+ --changed rebuild only outputs whose declared dependencies
99
+ changed since the last _meta.json (CI-safe).
100
+ --parallel run independent builders concurrently via Promise.all.
101
+ --quiet suppress per-output log lines.
102
+
103
+ Examples:
104
+ npm run build-indexes
105
+ node scripts/build-indexes.js --only summary-cards,section-offsets
106
+ node scripts/build-indexes.js --changed --parallel
107
+ `);
108
+ }
109
+
110
+ // --- Source loading (shared in-memory snapshot) -------------------------
111
+
112
+ function loadSources() {
113
+ const manifest = readJson(ABS("manifest.json"));
114
+ const skills = manifest.skills;
115
+ const skillNames = new Set(skills.map((s) => s.name));
116
+ const catalogFiles = fs.readdirSync(ABS("data")).filter((f) => f.endsWith(".json")).map((f) => "data/" + f);
117
+
118
+ // Per-skill body cache so multiple builders don't re-read the same file.
119
+ const skillBodies = {};
120
+ for (const s of skills) skillBodies[s.name] = fs.readFileSync(ABS(s.path), "utf8");
121
+
122
+ const ctx = {
123
+ root: ROOT,
124
+ manifest,
125
+ skills,
126
+ skillNames,
127
+ skillBodies,
128
+ catalogFiles,
129
+ cveCatalog: readJson(ABS("data/cve-catalog.json")),
130
+ frameworkGaps: readJson(ABS("data/framework-control-gaps.json")),
131
+ atlasTtps: readJson(ABS("data/atlas-ttps.json")),
132
+ cweCatalog: readJson(ABS("data/cwe-catalog.json")),
133
+ d3Catalog: readJson(ABS("data/d3fend-catalog.json")),
134
+ rfcCatalog: readJson(ABS("data/rfc-references.json")),
135
+ dlpCatalog: readJson(ABS("data/dlp-controls.json")),
136
+ globalFrameworks: readJson(ABS("data/global-frameworks.json")),
137
+ };
138
+ return ctx;
139
+ }
140
+
141
+ // --- Outputs registry ---------------------------------------------------
142
+ // Each entry: { name, file, deps, build, dependsOn?: [name, ...] }
143
+ // deps: list of source-file pattern functions. A pattern is a function that
144
+ // returns true if a given relative path counts as a dep for this
145
+ // output. The --changed planner walks every changed source and
146
+ // flags every output whose deps match.
147
+ // dependsOn: names of other outputs that must be built first (used by
148
+ // chains.json which composes CVE + CWE halves, and
149
+ // token-budget.json which consumes section-offsets).
150
+
151
+ function isAnySkillBody(p) { return p.startsWith("skills/") && p.endsWith("/skill.md"); }
152
+ function isManifest(p) { return p === "manifest.json"; }
153
+ function isCatalog(name) { return (p) => p === `data/${name}.json`; }
154
+ function isAnyCatalog(p) { return p.startsWith("data/") && p.endsWith(".json") && !p.includes("/_indexes/"); }
155
+
156
+ const OUTPUTS = [
157
+ {
158
+ name: "xref",
159
+ file: "xref.json",
160
+ deps: [isManifest],
161
+ build: (ctx) => {
162
+ const xref = {
163
+ cwe_refs: {}, d3fend_refs: {}, framework_gaps: {},
164
+ atlas_refs: {}, attack_refs: {}, rfc_refs: {}, dlp_refs: {},
165
+ };
166
+ for (const s of ctx.skills) {
167
+ for (const field of Object.keys(xref)) {
168
+ for (const v of s[field] || []) (xref[field][v] = xref[field][v] || []).push(s.name);
169
+ }
170
+ }
171
+ for (const field of Object.keys(xref)) {
172
+ for (const k of Object.keys(xref[field])) xref[field][k].sort();
173
+ }
174
+ const stats = {};
175
+ for (const field of Object.keys(xref)) stats[field] = Object.keys(xref[field]).length;
176
+ ctx._xrefStats = stats;
177
+ return xref;
178
+ },
179
+ },
180
+
181
+ {
182
+ name: "trigger-table",
183
+ file: "trigger-table.json",
184
+ deps: [isManifest],
185
+ build: (ctx) => {
186
+ const t = {};
187
+ for (const s of ctx.skills) {
188
+ for (const tr of s.triggers || []) {
189
+ const k = String(tr).toLowerCase().trim();
190
+ (t[k] = t[k] || []).push(s.name);
191
+ }
192
+ }
193
+ for (const k of Object.keys(t)) t[k].sort();
194
+ return t;
195
+ },
196
+ },
197
+
198
+ {
199
+ name: "jurisdiction-map",
200
+ file: "jurisdiction-map.json",
201
+ deps: [isManifest, isCatalog("global-frameworks"), isAnySkillBody],
202
+ build: (ctx) => {
203
+ const codes = Object.keys(ctx.globalFrameworks).filter((k) => !k.startsWith("_") && k !== "GLOBAL");
204
+ const NAME_TO_CODE = {
205
+ "GDPR": "EU", "NIS2": "EU", "DORA": "EU", "EU AI Act": "EU", "ENISA": "EU",
206
+ "NCSC": "UK", "Children's Code": "UK", "Online Safety Act": "UK",
207
+ "ISM": "AU", "Essential 8": "AU", "APRA": "AU", "eSafety": "AU",
208
+ "MAS TRM": "SG", "CSA Singapore": "SG",
209
+ "APPI": "JP", "FISC": "JP", "NISC": "JP",
210
+ "DPDPA": "IN", "CERT-In": "IN", "SEBI": "IN",
211
+ "OSFI": "CA", "Quebec Law 25": "CA", "PIPEDA": "CA",
212
+ "LGPD": "BR", "ANPD": "BR",
213
+ "PIPL": "CN", "CAC": "CN", "DSL": "CN", "CSL": "CN",
214
+ "POPIA": "ZA", "UAE PDPL": "AE",
215
+ "KSA PDPL": "SA", "SAMA": "SA",
216
+ "Privacy Act 2020": "NZ", "PIPA": "KR",
217
+ "INCD": "IL", "BoI Directive 361": "IL",
218
+ "FADP": "CH", "FINMA": "CH",
219
+ "PDPO": "HK", "HKMA": "HK", "CSMA": "TW",
220
+ "UU PDP": "ID", "BSSN": "ID",
221
+ "Vietnam Cybersecurity Law": "VN",
222
+ "NYDFS": "US_NYDFS", "23 NYCRR 500": "US_NYDFS",
223
+ "CCPA": "US_CALIFORNIA", "CPRA": "US_CALIFORNIA", "CPPA": "US_CALIFORNIA",
224
+ "BSI": "EU_DE_BSI", "IT-Grundschutz": "EU_DE_BSI",
225
+ "ANSSI": "EU_FR_ANSSI", "AEPD": "EU_ES_AEPD",
226
+ "AgID": "EU_IT_AgID_ACN", "ACN": "EU_IT_AgID_ACN",
227
+ "NSM": "NO",
228
+ "LFPDPPP": "MX", "INAI": "MX", "AAIP": "AR",
229
+ "KVKK": "TR", "PDPA Thailand": "TH", "DPA Philippines": "PH",
230
+ };
231
+ const out = {};
232
+ for (const code of codes) out[code] = { skills: [], example_excerpts: {} };
233
+ for (const s of ctx.skills) {
234
+ const body = ctx.skillBodies[s.name];
235
+ for (const code of codes) {
236
+ const re = new RegExp("\\b" + code + "\\b");
237
+ if (re.test(body) && !out[code].skills.includes(s.name)) out[code].skills.push(s.name);
238
+ }
239
+ for (const [name, code] of Object.entries(NAME_TO_CODE)) {
240
+ if (body.includes(name) && !out[code].skills.includes(s.name)) out[code].skills.push(s.name);
241
+ }
242
+ }
243
+ for (const code of codes) {
244
+ out[code].skills.sort();
245
+ out[code].skill_count = out[code].skills.length;
246
+ }
247
+ ctx._jurisdictionCount = codes.length;
248
+ return out;
249
+ },
250
+ },
251
+
252
+ {
253
+ name: "handoff-dag",
254
+ file: "handoff-dag.json",
255
+ deps: [isManifest, isAnySkillBody],
256
+ build: (ctx) => {
257
+ const edges = {}, nodes = [...ctx.skillNames].sort();
258
+ for (const s of ctx.skills) edges[s.name] = [];
259
+ for (const s of ctx.skills) {
260
+ const body = ctx.skillBodies[s.name];
261
+ for (const other of ctx.skillNames) {
262
+ if (other === s.name) continue;
263
+ if (body.includes("`" + other + "`")) edges[s.name].push(other);
264
+ }
265
+ edges[s.name].sort();
266
+ }
267
+ const inDeg = {}, outDeg = {};
268
+ for (const n of nodes) { inDeg[n] = 0; outDeg[n] = 0; }
269
+ for (const [from, tos] of Object.entries(edges)) {
270
+ outDeg[from] = tos.length;
271
+ for (const to of tos) inDeg[to] = (inDeg[to] || 0) + 1;
272
+ }
273
+ return { nodes, edges, in_degree: inDeg, out_degree: outDeg };
274
+ },
275
+ },
276
+
277
+ {
278
+ name: "chains",
279
+ file: "chains.json",
280
+ deps: [
281
+ isManifest,
282
+ isCatalog("cve-catalog"),
283
+ isCatalog("cwe-catalog"),
284
+ isCatalog("framework-control-gaps"),
285
+ isCatalog("atlas-ttps"),
286
+ isCatalog("d3fend-catalog"),
287
+ isCatalog("rfc-references"),
288
+ ],
289
+ build: (ctx) => {
290
+ const { buildCweChains } = require("./builders/cwe-chains");
291
+ const chains = {
292
+ _meta: {
293
+ schema_version: "1.1.0",
294
+ note: "Pre-computed cross-walks keyed by CVE-id and (v0.7.0+) CWE-id.",
295
+ entry_types: ["CVE", "CWE"],
296
+ },
297
+ };
298
+ // CVE half
299
+ for (const cveId of Object.keys(ctx.cveCatalog).filter((k) => !k.startsWith("_"))) {
300
+ const cve = ctx.cveCatalog[cveId];
301
+ const referencingSkills = ctx.skills
302
+ .filter((s) => {
303
+ for (const fg of s.framework_gaps || []) {
304
+ const evd = (ctx.frameworkGaps[fg] || {}).evidence_cves || [];
305
+ if (evd.includes(cveId)) return true;
306
+ }
307
+ return false;
308
+ })
309
+ .map((s) => s.name);
310
+ const accum = {
311
+ cwe_refs: new Set(), atlas_refs: new Set(), attack_refs: new Set(),
312
+ framework_gaps: new Set(), d3fend_refs: new Set(), rfc_refs: new Set(),
313
+ };
314
+ for (const name of referencingSkills) {
315
+ const s = ctx.skills.find((x) => x.name === name);
316
+ for (const field of Object.keys(accum)) {
317
+ for (const v of s[field] || []) accum[field].add(v);
318
+ }
319
+ }
320
+ const hydrated = {
321
+ cwes: [...accum.cwe_refs].sort().map((c) => ({ id: c, name: ctx.cweCatalog[c]?.name, category: ctx.cweCatalog[c]?.category })),
322
+ atlas: [...accum.atlas_refs].sort().map((a) => ({ id: a, name: ctx.atlasTtps[a]?.name, tactic: ctx.atlasTtps[a]?.tactic })),
323
+ d3fend: [...accum.d3fend_refs].sort().map((d) => ({ id: d, name: ctx.d3Catalog[d]?.name, tactic: ctx.d3Catalog[d]?.tactic })),
324
+ framework_gaps: [...accum.framework_gaps].sort().map((f) => ({ id: f, framework: ctx.frameworkGaps[f]?.framework, control_name: ctx.frameworkGaps[f]?.control_name })),
325
+ attack_refs: [...accum.attack_refs].sort(),
326
+ rfc_refs: [...accum.rfc_refs].sort(),
327
+ };
328
+ chains[cveId] = {
329
+ name: cve.name, rwep: cve.rwep_score, cvss: cve.cvss_score,
330
+ cisa_kev: cve.cisa_kev, epss_score: cve.epss_score, epss_percentile: cve.epss_percentile,
331
+ referencing_skills: referencingSkills, chain: hydrated,
332
+ };
333
+ }
334
+ // CWE half (delegated to builder)
335
+ const cweChains = buildCweChains({
336
+ skills: ctx.skills, cweCatalog: ctx.cweCatalog, atlasTtps: ctx.atlasTtps,
337
+ cveCatalog: ctx.cveCatalog, frameworkGaps: ctx.frameworkGaps,
338
+ d3fendCatalog: ctx.d3Catalog, rfcCatalog: ctx.rfcCatalog,
339
+ });
340
+ for (const [k, v] of Object.entries(cweChains)) chains[k] = v;
341
+ return chains;
342
+ },
343
+ },
344
+
345
+ {
346
+ name: "summary-cards",
347
+ file: "summary-cards.json",
348
+ deps: [isManifest, isAnySkillBody],
349
+ build: (ctx) => {
350
+ const { buildSummaryCards } = require("./builders/summary-cards");
351
+ return buildSummaryCards({ root: ctx.root, manifest: ctx.manifest, skills: ctx.skills });
352
+ },
353
+ },
354
+
355
+ {
356
+ name: "section-offsets",
357
+ file: "section-offsets.json",
358
+ deps: [isManifest, isAnySkillBody],
359
+ build: (ctx) => {
360
+ const { buildSectionOffsets } = require("./builders/section-offsets");
361
+ return buildSectionOffsets({ root: ctx.root, skills: ctx.skills });
362
+ },
363
+ },
364
+
365
+ {
366
+ name: "token-budget",
367
+ file: "token-budget.json",
368
+ deps: [isManifest, isAnySkillBody],
369
+ dependsOn: ["section-offsets"], // needs the produced index
370
+ build: (ctx) => {
371
+ const { buildTokenBudget } = require("./builders/token-budget");
372
+ // section-offsets output is already on disk (built first by the
373
+ // dependency planner). Read it back for the token splitter.
374
+ const sectionOffsets = readJson(path.join(IDX, "section-offsets.json"));
375
+ return buildTokenBudget({ root: ctx.root, skills: ctx.skills, sectionOffsets });
376
+ },
377
+ },
378
+
379
+ {
380
+ name: "recipes",
381
+ file: "recipes.json",
382
+ deps: [isManifest],
383
+ build: (ctx) => {
384
+ const { buildRecipes } = require("./builders/recipes");
385
+ return buildRecipes({ skills: ctx.skills });
386
+ },
387
+ },
388
+
389
+ {
390
+ name: "jurisdiction-clocks",
391
+ file: "jurisdiction-clocks.json",
392
+ deps: [isCatalog("global-frameworks")],
393
+ build: (ctx) => {
394
+ const { buildJurisdictionClocks } = require("./builders/jurisdiction-clocks");
395
+ return buildJurisdictionClocks({ globalFrameworks: ctx.globalFrameworks });
396
+ },
397
+ },
398
+
399
+ {
400
+ name: "did-ladders",
401
+ file: "did-ladders.json",
402
+ deps: [isManifest, isCatalog("d3fend-catalog")],
403
+ build: (ctx) => {
404
+ const { buildDidLadders } = require("./builders/did-ladders");
405
+ return buildDidLadders({ skills: ctx.skills, d3fendCatalog: ctx.d3Catalog });
406
+ },
407
+ },
408
+
409
+ {
410
+ name: "theater-fingerprints",
411
+ file: "theater-fingerprints.json",
412
+ deps: [(p) => p === "skills/compliance-theater/skill.md"],
413
+ build: (ctx) => {
414
+ const { buildTheaterFingerprints } = require("./builders/theater-fingerprints");
415
+ return buildTheaterFingerprints({ root: ctx.root });
416
+ },
417
+ },
418
+
419
+ {
420
+ name: "currency",
421
+ file: "currency.json",
422
+ deps: [isManifest, isAnySkillBody],
423
+ build: (ctx) => {
424
+ const { buildCurrency } = require("./builders/currency");
425
+ return buildCurrency({ root: ctx.root, manifest: ctx.manifest, skills: ctx.skills });
426
+ },
427
+ },
428
+
429
+ {
430
+ name: "frequency",
431
+ file: "frequency.json",
432
+ deps: [isManifest, isAnyCatalog],
433
+ build: (ctx) => {
434
+ const { buildFrequency } = require("./builders/frequency");
435
+ return buildFrequency({
436
+ skills: ctx.skills,
437
+ catalogs: {
438
+ cwe: ctx.cweCatalog, atlas: ctx.atlasTtps, d3fend: ctx.d3Catalog,
439
+ frameworkGaps: ctx.frameworkGaps, rfc: ctx.rfcCatalog, dlp: ctx.dlpCatalog,
440
+ },
441
+ });
442
+ },
443
+ },
444
+
445
+ {
446
+ name: "activity-feed",
447
+ file: "activity-feed.json",
448
+ deps: [isManifest, isAnyCatalog],
449
+ build: (ctx) => {
450
+ const { buildActivityFeed } = require("./builders/activity-feed");
451
+ return buildActivityFeed({ root: ctx.root, manifest: ctx.manifest, skills: ctx.skills, catalogFiles: ctx.catalogFiles });
452
+ },
453
+ },
454
+
455
+ {
456
+ name: "catalog-summaries",
457
+ file: "catalog-summaries.json",
458
+ deps: [isAnyCatalog],
459
+ build: (ctx) => {
460
+ const { buildCatalogSummaries } = require("./builders/catalog-summaries");
461
+ return buildCatalogSummaries({ root: ctx.root, catalogFiles: ctx.catalogFiles });
462
+ },
463
+ },
464
+
465
+ {
466
+ name: "stale-content",
467
+ file: "stale-content.json",
468
+ deps: [isManifest, isAnySkillBody, isAnyCatalog],
469
+ build: (ctx) => {
470
+ const { buildStaleContent } = require("./builders/stale-content");
471
+ return buildStaleContent({ root: ctx.root, manifest: ctx.manifest, skills: ctx.skills, catalogFiles: ctx.catalogFiles });
472
+ },
473
+ },
474
+ ];
475
+
476
+ // --- Plan + run --------------------------------------------------------
477
+
478
+ function loadPriorMeta() {
479
+ const p = path.join(IDX, "_meta.json");
480
+ if (!fs.existsSync(p)) return null;
481
+ try { return readJson(p); } catch { return null; }
482
+ }
483
+
484
+ function liveSourceSet(ctx) {
485
+ const out = new Set();
486
+ out.add("manifest.json");
487
+ for (const c of ctx.catalogFiles) out.add(c);
488
+ for (const s of ctx.skills) out.add(s.path);
489
+ return out;
490
+ }
491
+
492
+ function changedSources(ctx, priorMeta) {
493
+ // Returns the array of source paths whose sha256 differs from prior, OR
494
+ // every source if there's no prior meta. Also accounts for new + removed
495
+ // source files (which always force a rebuild).
496
+ if (!priorMeta || !priorMeta.source_hashes) {
497
+ return [...liveSourceSet(ctx)];
498
+ }
499
+ const live = liveSourceSet(ctx);
500
+ const recorded = new Set(Object.keys(priorMeta.source_hashes));
501
+ const changed = [];
502
+ for (const p of live) {
503
+ const h = sha256(fs.readFileSync(ABS(p)));
504
+ if (priorMeta.source_hashes[p] !== h) changed.push(p);
505
+ }
506
+ // Any source that disappeared since last build counts as a change.
507
+ for (const p of recorded) if (!live.has(p)) changed.push(p);
508
+ return changed;
509
+ }
510
+
511
+ function outputsAffectedBy(changedPaths) {
512
+ const affected = new Set();
513
+ for (const o of OUTPUTS) {
514
+ for (const dep of o.deps) {
515
+ if (changedPaths.some(dep)) { affected.add(o.name); break; }
516
+ }
517
+ }
518
+ return affected;
519
+ }
520
+
521
+ function withDependencyClosure(names) {
522
+ // Pull in any dependsOn entries (e.g. token-budget needs section-offsets).
523
+ const closure = new Set(names);
524
+ let added = true;
525
+ while (added) {
526
+ added = false;
527
+ for (const o of OUTPUTS) {
528
+ if (!closure.has(o.name)) continue;
529
+ for (const dep of o.dependsOn || []) {
530
+ if (!closure.has(dep)) {
531
+ closure.add(dep);
532
+ added = true;
533
+ }
534
+ }
535
+ }
536
+ }
537
+ return closure;
538
+ }
539
+
540
+ function topoOrder(names) {
541
+ const want = new Set(names);
542
+ const order = [];
543
+ const visited = new Set();
544
+ function visit(name) {
545
+ if (visited.has(name)) return;
546
+ visited.add(name);
547
+ const out = OUTPUTS.find((x) => x.name === name);
548
+ for (const dep of (out?.dependsOn || [])) if (want.has(dep)) visit(dep);
549
+ order.push(name);
550
+ }
551
+ for (const o of OUTPUTS) if (want.has(o.name)) visit(o.name);
552
+ return order;
553
+ }
554
+
555
+ async function runBuilders(ctx, names, opts) {
556
+ // Build the dependency-respecting execution plan, then dispatch.
557
+ const order = topoOrder(names);
558
+ const log = (s) => opts.quiet || console.log(s);
559
+
560
+ // Group by levels — outputs with no produced-output deps go first, then
561
+ // outputs depending on those, etc. This is the parallelization unit.
562
+ const remaining = new Set(order);
563
+ const levels = [];
564
+ while (remaining.size > 0) {
565
+ const level = [];
566
+ for (const n of remaining) {
567
+ const o = OUTPUTS.find((x) => x.name === n);
568
+ const blockers = (o.dependsOn || []).filter((d) => remaining.has(d));
569
+ if (blockers.length === 0) level.push(n);
570
+ }
571
+ if (level.length === 0) {
572
+ throw new Error("build-indexes: dependency cycle detected — please check OUTPUTS.dependsOn");
573
+ }
574
+ levels.push(level);
575
+ for (const n of level) remaining.delete(n);
576
+ }
577
+
578
+ const results = {};
579
+ for (const level of levels) {
580
+ const runOne = async (name) => {
581
+ const o = OUTPUTS.find((x) => x.name === name);
582
+ const t0 = Date.now();
583
+ const payload = await o.build(ctx);
584
+ writeJson(o.file, payload);
585
+ results[name] = payload;
586
+ const ms = Date.now() - t0;
587
+ log(` ✓ ${o.file} (${ms}ms)`);
588
+ };
589
+ if (opts.parallel) {
590
+ await Promise.all(level.map(runOne));
591
+ } else {
592
+ for (const n of level) await runOne(n);
593
+ }
594
+ }
595
+
596
+ return results;
597
+ }
598
+
599
+ function writeMeta(ctx, results) {
600
+ const sourceFiles = [...liveSourceSet(ctx)];
601
+ const sourceHashes = {};
602
+ for (const p of sourceFiles) sourceHashes[p] = sha256(fs.readFileSync(ABS(p)));
603
+
604
+ // Stats are computed from in-memory results when available, else from disk
605
+ // (covers --only / --changed runs that didn't rebuild every output).
606
+ function readBack(name) {
607
+ if (results[name]) return results[name];
608
+ const o = OUTPUTS.find((x) => x.name === name);
609
+ if (!o) return null;
610
+ const p = path.join(IDX, o.file);
611
+ if (!fs.existsSync(p)) return null;
612
+ try { return readJson(p); } catch { return null; }
613
+ }
614
+
615
+ const xref = readBack("xref") || {};
616
+ const trigger = readBack("trigger-table") || {};
617
+ const chains = readBack("chains") || {};
618
+ const handoff = readBack("handoff-dag") || { nodes: [], edges: {} };
619
+ const summaryCards = readBack("summary-cards") || { skills: {} };
620
+ const sectionOffsets = readBack("section-offsets") || { skills: {} };
621
+ const tokenBudget = readBack("token-budget") || { _meta: {} };
622
+ const recipes = readBack("recipes") || { recipes: [] };
623
+ const jurisdictionClocks = readBack("jurisdiction-clocks") || { by_jurisdiction: {} };
624
+ const didLadders = readBack("did-ladders") || { ladders: [] };
625
+ const theater = readBack("theater-fingerprints") || { patterns: {} };
626
+ const currency = readBack("currency") || { summary: {} };
627
+ const frequency = readBack("frequency") || { counts: {} };
628
+ const activity = readBack("activity-feed") || { events: [] };
629
+ const catSummaries = readBack("catalog-summaries") || { catalogs: {} };
630
+ const stale = readBack("stale-content") || { _meta: { finding_count: 0, by_severity: {} } };
631
+
632
+ const cveChainCount = Object.keys(chains).filter((k) => k.startsWith("CVE-")).length;
633
+ const cweChainCount = Object.keys(chains).filter((k) => k.startsWith("CWE-")).length;
634
+ const xrefStats = {};
635
+ for (const field of Object.keys(xref)) xrefStats[field] = Object.keys(xref[field]).length;
636
+
637
+ const meta = {
638
+ schema_version: "1.1.0",
639
+ generated_at: new Date().toISOString(),
640
+ generator: "scripts/build-indexes.js",
641
+ source_count: sourceFiles.length,
642
+ source_hashes: sourceHashes,
643
+ skill_count: ctx.skills.length,
644
+ catalog_count: ctx.catalogFiles.length,
645
+ index_stats: {
646
+ xref_entries: xrefStats,
647
+ trigger_table_entries: Object.keys(trigger).length,
648
+ chains_cve_entries: cveChainCount,
649
+ chains_cwe_entries: cweChainCount,
650
+ jurisdictions_indexed: Object.keys(jurisdictionClocks.by_jurisdiction || {}).length || Object.keys(readBack("jurisdiction-map") || {}).length,
651
+ handoff_dag_nodes: handoff.nodes?.length || 0,
652
+ summary_cards: Object.keys(summaryCards.skills || {}).length,
653
+ section_offsets_skills: Object.keys(sectionOffsets.skills || {}).length,
654
+ token_budget_total_approx: tokenBudget._meta?.total_approx_tokens || 0,
655
+ recipes: (recipes.recipes || []).length,
656
+ jurisdiction_clocks: Object.keys(jurisdictionClocks.by_jurisdiction || {}).length,
657
+ did_ladders: (didLadders.ladders || []).length,
658
+ theater_fingerprints: Object.keys(theater.patterns || {}).length,
659
+ currency_action_required: currency.summary?.action_required || 0,
660
+ frequency_fields: Object.keys(frequency.counts || {}).length,
661
+ activity_feed_events: (activity.events || []).length,
662
+ catalog_summaries: Object.keys(catSummaries.catalogs || {}).length,
663
+ stale_content_findings: stale._meta?.finding_count || 0,
664
+ },
665
+ invalidation_note: "If any source file in source_hashes has a different SHA-256 than recorded here, the indexes are stale. Re-run `npm run build-indexes`.",
666
+ };
667
+ writeJson("_meta.json", meta);
668
+ return meta;
669
+ }
670
+
671
+ async function main() {
672
+ const opts = parseArgs(process.argv);
673
+ if (opts.help) { printHelp(); return; }
674
+
675
+ const ctx = loadSources();
676
+ const log = (s) => opts.quiet || console.log(s);
677
+
678
+ // Decide which outputs to build.
679
+ let chosen;
680
+ if (opts.only) {
681
+ const wanted = opts.only.split(",").map((s) => s.trim()).filter(Boolean);
682
+ for (const n of wanted) {
683
+ if (!OUTPUTS.find((o) => o.name === n)) {
684
+ console.error(`build-indexes: unknown output "${n}". Valid: ${OUTPUTS.map((o) => o.name).join(", ")}`);
685
+ process.exit(2);
686
+ }
687
+ }
688
+ chosen = withDependencyClosure(wanted);
689
+ } else if (opts.changed) {
690
+ const prior = loadPriorMeta();
691
+ const changed = changedSources(ctx, prior);
692
+ log(`changed sources: ${changed.length}`);
693
+ const affected = outputsAffectedBy(changed);
694
+ chosen = withDependencyClosure(affected);
695
+ if (chosen.size === 0) {
696
+ log("build-indexes: no outputs need rebuilding (sources unchanged)");
697
+ // Still rewrite _meta.json with the same hashes — preserves freshness
698
+ // semantics for the predeploy gate even when nothing else changed.
699
+ writeMeta(ctx, {});
700
+ return;
701
+ }
702
+ } else {
703
+ chosen = new Set(OUTPUTS.map((o) => o.name));
704
+ }
705
+
706
+ log(`build-indexes — ${chosen.size} output(s) ${opts.parallel ? "in parallel" : "sequential"}${opts.changed ? " (--changed)" : ""}${opts.only ? ` (--only ${opts.only})` : ""}`);
707
+
708
+ const results = await runBuilders(ctx, chosen, opts);
709
+ writeMeta(ctx, results);
710
+
711
+ log(`build-indexes — done`);
712
+ }
713
+
714
+ if (require.main === module) {
715
+ main().catch((err) => {
716
+ console.error(`build-indexes: fatal: ${err && err.stack ? err.stack : err}`);
717
+ process.exit(1);
718
+ });
719
+ }
720
+
721
+ module.exports = { OUTPUTS, loadSources, runBuilders, writeMeta };