@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.
- package/AGENTS.md +232 -0
- package/ARCHITECTURE.md +267 -0
- package/CHANGELOG.md +616 -0
- package/CONTEXT.md +203 -0
- package/LICENSE +200 -0
- package/NOTICE +82 -0
- package/README.md +307 -0
- package/SECURITY.md +73 -0
- package/agents/README.md +81 -0
- package/agents/report-generator.md +156 -0
- package/agents/skill-updater.md +102 -0
- package/agents/source-validator.md +119 -0
- package/agents/threat-researcher.md +149 -0
- package/bin/exceptd.js +183 -0
- package/data/_indexes/_meta.json +88 -0
- package/data/_indexes/activity-feed.json +362 -0
- package/data/_indexes/catalog-summaries.json +229 -0
- package/data/_indexes/chains.json +7135 -0
- package/data/_indexes/currency.json +359 -0
- package/data/_indexes/did-ladders.json +451 -0
- package/data/_indexes/frequency.json +2072 -0
- package/data/_indexes/handoff-dag.json +476 -0
- package/data/_indexes/jurisdiction-clocks.json +967 -0
- package/data/_indexes/jurisdiction-map.json +536 -0
- package/data/_indexes/recipes.json +319 -0
- package/data/_indexes/section-offsets.json +3656 -0
- package/data/_indexes/stale-content.json +14 -0
- package/data/_indexes/summary-cards.json +1736 -0
- package/data/_indexes/theater-fingerprints.json +381 -0
- package/data/_indexes/token-budget.json +2137 -0
- package/data/_indexes/trigger-table.json +1374 -0
- package/data/_indexes/xref.json +818 -0
- package/data/atlas-ttps.json +282 -0
- package/data/cve-catalog.json +496 -0
- package/data/cwe-catalog.json +1017 -0
- package/data/d3fend-catalog.json +738 -0
- package/data/dlp-controls.json +1039 -0
- package/data/exploit-availability.json +67 -0
- package/data/framework-control-gaps.json +1255 -0
- package/data/global-frameworks.json +2913 -0
- package/data/rfc-references.json +324 -0
- package/data/zeroday-lessons.json +377 -0
- package/keys/public.pem +3 -0
- package/lib/framework-gap.js +328 -0
- package/lib/job-queue.js +195 -0
- package/lib/lint-skills.js +536 -0
- package/lib/prefetch.js +372 -0
- package/lib/refresh-external.js +713 -0
- package/lib/schemas/cve-catalog.schema.json +151 -0
- package/lib/schemas/manifest.schema.json +106 -0
- package/lib/schemas/skill-frontmatter.schema.json +113 -0
- package/lib/scoring.js +149 -0
- package/lib/sign.js +197 -0
- package/lib/ttp-mapper.js +80 -0
- package/lib/validate-catalog-meta.js +198 -0
- package/lib/validate-cve-catalog.js +213 -0
- package/lib/validate-indexes.js +83 -0
- package/lib/validate-package.js +162 -0
- package/lib/validate-vendor.js +85 -0
- package/lib/verify.js +216 -0
- package/lib/worker-pool.js +84 -0
- package/manifest-snapshot.json +1833 -0
- package/manifest.json +2108 -0
- package/orchestrator/README.md +124 -0
- package/orchestrator/dispatcher.js +140 -0
- package/orchestrator/event-bus.js +146 -0
- package/orchestrator/index.js +874 -0
- package/orchestrator/pipeline.js +201 -0
- package/orchestrator/scanner.js +327 -0
- package/orchestrator/scheduler.js +137 -0
- package/package.json +113 -0
- package/sbom.cdx.json +158 -0
- package/scripts/audit-cross-skill.js +261 -0
- package/scripts/audit-perf.js +160 -0
- package/scripts/bootstrap.js +205 -0
- package/scripts/build-indexes.js +721 -0
- package/scripts/builders/activity-feed.js +79 -0
- package/scripts/builders/catalog-summaries.js +67 -0
- package/scripts/builders/currency.js +109 -0
- package/scripts/builders/cwe-chains.js +105 -0
- package/scripts/builders/did-ladders.js +149 -0
- package/scripts/builders/frequency.js +89 -0
- package/scripts/builders/jurisdiction-clocks.js +126 -0
- package/scripts/builders/recipes.js +159 -0
- package/scripts/builders/section-offsets.js +162 -0
- package/scripts/builders/stale-content.js +171 -0
- package/scripts/builders/summary-cards.js +166 -0
- package/scripts/builders/theater-fingerprints.js +198 -0
- package/scripts/builders/token-budget.js +96 -0
- package/scripts/check-manifest-snapshot.js +217 -0
- package/scripts/predeploy.js +267 -0
- package/scripts/refresh-manifest-snapshot.js +57 -0
- package/scripts/refresh-sbom.js +222 -0
- package/skills/age-gates-child-safety/skill.md +456 -0
- package/skills/ai-attack-surface/skill.md +282 -0
- package/skills/ai-c2-detection/skill.md +440 -0
- package/skills/ai-risk-management/skill.md +311 -0
- package/skills/api-security/skill.md +287 -0
- package/skills/attack-surface-pentest/skill.md +381 -0
- package/skills/cloud-security/skill.md +384 -0
- package/skills/compliance-theater/skill.md +365 -0
- package/skills/container-runtime-security/skill.md +379 -0
- package/skills/coordinated-vuln-disclosure/skill.md +473 -0
- package/skills/defensive-countermeasure-mapping/skill.md +300 -0
- package/skills/dlp-gap-analysis/skill.md +337 -0
- package/skills/email-security-anti-phishing/skill.md +206 -0
- package/skills/exploit-scoring/skill.md +331 -0
- package/skills/framework-gap-analysis/skill.md +374 -0
- package/skills/fuzz-testing-strategy/skill.md +313 -0
- package/skills/global-grc/skill.md +564 -0
- package/skills/identity-assurance/skill.md +272 -0
- package/skills/incident-response-playbook/skill.md +546 -0
- package/skills/kernel-lpe-triage/skill.md +303 -0
- package/skills/mcp-agent-trust/skill.md +326 -0
- package/skills/mlops-security/skill.md +325 -0
- package/skills/ot-ics-security/skill.md +340 -0
- package/skills/policy-exception-gen/skill.md +437 -0
- package/skills/pqc-first/skill.md +546 -0
- package/skills/rag-pipeline-security/skill.md +294 -0
- package/skills/researcher/skill.md +310 -0
- package/skills/sector-energy/skill.md +409 -0
- package/skills/sector-federal-government/skill.md +302 -0
- package/skills/sector-financial/skill.md +398 -0
- package/skills/sector-healthcare/skill.md +373 -0
- package/skills/security-maturity-tiers/skill.md +464 -0
- package/skills/skill-update-loop/skill.md +463 -0
- package/skills/supply-chain-integrity/skill.md +318 -0
- package/skills/threat-model-currency/skill.md +404 -0
- package/skills/threat-modeling-methodology/skill.md +312 -0
- package/skills/webapp-security/skill.md +281 -0
- package/skills/zeroday-gap-learn/skill.md +350 -0
- package/vendor/blamejs/LICENSE +201 -0
- package/vendor/blamejs/README.md +54 -0
- package/vendor/blamejs/_PROVENANCE.json +54 -0
- package/vendor/blamejs/retry.js +335 -0
- 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 };
|