@blamejs/exceptd-skills 0.19.33 → 0.19.35

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 (119) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bin/exceptd.js +895 -2828
  3. package/data/_indexes/_meta.json +2 -2
  4. package/lib/auto-discovery.js +56 -286
  5. package/lib/canonical-eq.js +7 -40
  6. package/lib/citation-resolve.js +22 -70
  7. package/lib/collectors/ai-api.js +170 -76
  8. package/lib/collectors/cicd-pipeline-compromise.js +113 -136
  9. package/lib/collectors/citation-hygiene.js +72 -210
  10. package/lib/collectors/containers.js +41 -130
  11. package/lib/collectors/cred-stores.js +31 -115
  12. package/lib/collectors/crypto-codebase.js +55 -138
  13. package/lib/collectors/crypto.js +24 -54
  14. package/lib/collectors/hardening.js +20 -78
  15. package/lib/collectors/kernel.js +16 -46
  16. package/lib/collectors/library-author.js +198 -211
  17. package/lib/collectors/mcp.js +24 -70
  18. package/lib/collectors/runtime.js +24 -86
  19. package/lib/collectors/sbom.js +130 -118
  20. package/lib/collectors/scan-excludes.js +33 -139
  21. package/lib/collectors/secrets.js +62 -178
  22. package/lib/cross-ref-api.js +39 -123
  23. package/lib/currency-severity.js +8 -27
  24. package/lib/cve-batch.js +13 -21
  25. package/lib/cve-cli.js +13 -20
  26. package/lib/cve-curation.js +72 -239
  27. package/lib/cve-regression-watcher.js +29 -155
  28. package/lib/cvss.js +13 -54
  29. package/lib/doctor-bucketing.js +3 -19
  30. package/lib/exit-codes.js +10 -42
  31. package/lib/flag-suggest.js +7 -25
  32. package/lib/framework-gap.js +39 -113
  33. package/lib/gap-detectors.js +37 -159
  34. package/lib/id-validation.js +9 -30
  35. package/lib/job-queue.js +13 -36
  36. package/lib/lint-skills.js +88 -236
  37. package/lib/playbook-runner.js +759 -2107
  38. package/lib/prefetch.js +101 -376
  39. package/lib/refresh-external.js +199 -633
  40. package/lib/refresh-network.js +78 -311
  41. package/lib/rfc-cli.js +23 -68
  42. package/lib/scoring.js +85 -146
  43. package/lib/sign.js +43 -229
  44. package/lib/source-advisories.js +43 -194
  45. package/lib/source-ghsa.js +37 -120
  46. package/lib/source-osv.js +94 -266
  47. package/lib/ttp-mapper.js +28 -27
  48. package/lib/upstream-check-cli.js +36 -29
  49. package/lib/upstream-check.js +19 -44
  50. package/lib/validate-catalog-meta.js +17 -61
  51. package/lib/validate-cve-catalog.js +52 -121
  52. package/lib/validate-indexes.js +25 -76
  53. package/lib/validate-package.js +16 -62
  54. package/lib/validate-playbooks.js +78 -286
  55. package/lib/validate-vendor.js +16 -49
  56. package/lib/verify.js +56 -286
  57. package/lib/version-pins.js +5 -34
  58. package/lib/worker-pool.js +11 -30
  59. package/lib/xml-tokenizer.js +47 -152
  60. package/manifest.json +53 -53
  61. package/orchestrator/dispatcher.js +17 -68
  62. package/orchestrator/event-bus.js +11 -74
  63. package/orchestrator/index.js +138 -413
  64. package/orchestrator/pipeline.js +28 -85
  65. package/orchestrator/scanner.js +34 -138
  66. package/orchestrator/scheduler.js +20 -84
  67. package/package.json +1 -1
  68. package/sbom.cdx.json +242 -242
  69. package/scripts/audit-catalog-gaps.js +9 -62
  70. package/scripts/audit-cross-skill.js +5 -31
  71. package/scripts/audit-perf.js +29 -28
  72. package/scripts/backfill-theater-test.js +7 -64
  73. package/scripts/bootstrap.js +12 -44
  74. package/scripts/build-indexes.js +40 -154
  75. package/scripts/builders/activity-feed.js +4 -14
  76. package/scripts/builders/catalog-summaries.js +3 -10
  77. package/scripts/builders/currency.js +7 -20
  78. package/scripts/builders/cwe-chains.js +7 -30
  79. package/scripts/builders/did-ladders.js +6 -13
  80. package/scripts/builders/frequency.js +5 -19
  81. package/scripts/builders/jurisdiction-clocks.js +6 -25
  82. package/scripts/builders/recipes.js +6 -14
  83. package/scripts/builders/section-offsets.js +13 -51
  84. package/scripts/builders/stale-content.js +7 -28
  85. package/scripts/builders/summary-cards.js +8 -29
  86. package/scripts/builders/theater-fingerprints.js +21 -31
  87. package/scripts/builders/token-budget.js +4 -31
  88. package/scripts/check-agents-md-collectors.js +26 -57
  89. package/scripts/check-catalog-gap-budget.js +15 -32
  90. package/scripts/check-changelog-extract.js +18 -48
  91. package/scripts/check-codebase-patterns-currency.js +6 -22
  92. package/scripts/check-codebase-patterns.js +63 -143
  93. package/scripts/check-epss-consistency.js +9 -64
  94. package/scripts/check-framework-gap-coverage.js +13 -31
  95. package/scripts/check-manifest-snapshot.js +62 -81
  96. package/scripts/check-sbom-currency.js +44 -142
  97. package/scripts/check-test-count.js +15 -52
  98. package/scripts/check-test-coverage.js +83 -198
  99. package/scripts/check-test-subjects.js +21 -62
  100. package/scripts/check-ttp-references.js +14 -38
  101. package/scripts/check-ttp-upstream.js +8 -40
  102. package/scripts/check-version-bump.js +9 -61
  103. package/scripts/check-version-tags.js +20 -121
  104. package/scripts/predeploy.js +38 -184
  105. package/scripts/refresh-manifest-snapshot.js +16 -38
  106. package/scripts/refresh-mitre-atlas.js +7 -8
  107. package/scripts/refresh-mitre-attack.js +1 -8
  108. package/scripts/refresh-mitre-d3fend.js +3 -9
  109. package/scripts/refresh-mitre-ics-attack.js +7 -8
  110. package/scripts/refresh-reverse-refs.js +27 -94
  111. package/scripts/refresh-rfc-index.js +7 -10
  112. package/scripts/refresh-sbom.js +31 -161
  113. package/scripts/refresh-upstream-catalogs.js +63 -148
  114. package/scripts/release.js +69 -234
  115. package/scripts/run-e2e-scenarios.js +26 -73
  116. package/scripts/sync-manifest-metadata.js +10 -34
  117. package/scripts/sync-package-description.js +8 -17
  118. package/scripts/validate-vendor-online.js +13 -44
  119. package/scripts/verify-shipped-tarball.js +35 -141
@@ -1,32 +1,16 @@
1
1
  "use strict";
2
2
  /**
3
- * scripts/refresh-manifest-snapshot.js
3
+ * Captures the public skill surface from manifest.json into
4
+ * manifest-snapshot.json. Run it AFTER an intentional surface change and commit
5
+ * the new snapshot alongside that change — never to "fix" a failing
6
+ * check-manifest-snapshot.js gate, whose breaking-change list is the thing to
7
+ * read first. A breaking change is a surface narrowing every downstream
8
+ * consumer needs to know about.
4
9
  *
5
- * Captures the current public skill surface from manifest.json and
6
- * writes it to manifest-snapshot.json. Run this AFTER an intentional
7
- * surface change (added skill, renamed trigger, refreshed framework
8
- * refs) and commit the new snapshot alongside the change.
9
- *
10
- * Do NOT run this to "fix" a failing check-manifest-snapshot.js gate
11
- * blindly — read the breaking-change list first. A breaking change is
12
- * a surface narrowing every downstream consumer needs to know about.
13
- *
14
- * commitOnly mode. Pass `--commit-only` (or set the env
15
- * EXCEPTD_SNAPSHOT_AUDIT_ACK=1) to acknowledge that the operator
16
- * deliberately wants to overwrite the committed snapshot. When neither
17
- * flag nor env is set AND the snapshot would actually change, the
18
- * script refuses and emits a structured diff hint. This stops an
19
- * accidental `npm run refresh-snapshot` (run as muscle-memory while
20
- * triaging a failing gate) from masking a real breaking change.
21
- *
22
- * Usage:
23
- * node scripts/refresh-manifest-snapshot.js # dry-shows the diff
24
- * EXCEPTD_SNAPSHOT_AUDIT_ACK=1 \
25
- * node scripts/refresh-manifest-snapshot.js # writes the new snapshot
26
- * node scripts/refresh-manifest-snapshot.js --commit-only # same thing, on argv
27
- *
28
- * The flag is documented in scripts/predeploy.js so contributors see it
29
- * the moment the snapshot gate fails.
10
+ * Overwriting an existing snapshot takes `--commit-only` or
11
+ * EXCEPTD_SNAPSHOT_AUDIT_ACK=1. Without either, a run whose capture differs
12
+ * refuses and prints a diff hint, so muscle memory cannot mask a real breaking
13
+ * change. scripts/predeploy.js names the flag where the gate fails.
30
14
  */
31
15
 
32
16
  const fs = require("fs");
@@ -64,18 +48,14 @@ const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8"));
64
48
  const snapshot = captureSurface(manifest);
65
49
  const newJson = JSON.stringify(snapshot, null, 2) + "\n";
66
50
 
67
- // F5 — refuse to overwrite an existing snapshot unless the operator
68
- // has explicitly acknowledged the rewrite (env or --commit-only flag).
69
51
  const argv = process.argv.slice(2);
70
52
  const commitOnly =
71
53
  argv.includes("--commit-only") ||
72
54
  process.env.EXCEPTD_SNAPSHOT_AUDIT_ACK === "1";
73
55
 
74
- // Read the committed snapshot once and branch on the read RESULT rather than
75
- // an existsSync(SNAPSHOT_PATH)-then-readFileSync probe — the latter is a
76
- // check-then-use window (CodeQL js/file-system-race) where the existence the
77
- // guard decides on may not be the file it then reads. ENOENT IS the "no prior
78
- // snapshot, write a fresh one" signal.
56
+ // Branch on the read RESULT, never on an existsSync-then-read probe: that is a
57
+ // check-then-use window (CodeQL js/file-system-race) where the file the guard
58
+ // decided on need not be the one read. ENOENT IS the "no prior snapshot" signal.
79
59
  let current = null;
80
60
  try {
81
61
  current = fs.readFileSync(SNAPSHOT_PATH, "utf8");
@@ -83,8 +63,7 @@ try {
83
63
  if (e.code !== "ENOENT") throw e;
84
64
  }
85
65
  if (current !== null && !commitOnly) {
86
- // Normalise the _generated_at timestamp for comparison — that field
87
- // changes every run and shouldn't trigger the guard.
66
+ // _generated_at changes every run, so it must not trigger the guard.
88
67
  const stripGenerated = (s) => s.replace(
89
68
  /"_generated_at":\s*"[^"]+",?\s*\n?/, ""
90
69
  );
@@ -108,9 +87,8 @@ fs.writeFileSync(SNAPSHOT_PATH, newJson, "utf8");
108
87
  console.log(`[refresh-manifest-snapshot] wrote ${snapshot.skill_count} skills to manifest-snapshot.json`);
109
88
  console.log("[refresh-manifest-snapshot] commit this file alongside the surface change.");
110
89
 
111
- // write a tracked SHA-256 of the snapshot so the
112
- // check-manifest-snapshot.js gate can verify integrity (no hand edits
113
- // after refresh).
90
+ // A tracked SHA-256 lets check-manifest-snapshot.js catch a hand edit made
91
+ // after the refresh.
114
92
  const crypto = require("crypto");
115
93
  const snapshotSha = crypto.createHash("sha256").update(newJson).digest("hex");
116
94
  const snapshotShaPath = path.join(ROOT, "manifest-snapshot.sha256");
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-mitre-atlas.js
4
+ * Thin per-type wrapper for the MITRE ATLAS refresher; the logic lives in
5
+ * scripts/refresh-upstream-catalogs.js#refreshAtlas. Wired as
6
+ * `npm run refresh-mitre-atlas`, and takes --dry-run.
5
7
  *
6
- * Thin per-type wrapper for the MITRE ATLAS refresher. Logic lives in
7
- * scripts/refresh-upstream-catalogs.js#refreshAtlas.
8
- *
9
- * node scripts/refresh-mitre-atlas.js [--dry-run]
10
- *
11
- * Wired as `npm run refresh-mitre-atlas`.
8
+ * `CAP=<n>` bounds how many NEW techniques one run may add, matching every
9
+ * other per-type wrapper. Context backfill onto existing rows is never capped.
12
10
  */
13
11
  const { refreshAtlas } = require("./refresh-upstream-catalogs.js");
14
12
  const dry = process.argv.includes("--dry-run");
15
- refreshAtlas({ dry }).catch((e) => { console.error("[err]", e); process.exit(1); });
13
+ const cap = Number(process.env.CAP || Infinity);
14
+ refreshAtlas({ dry, cap }).catch((e) => { console.error("[err]", e); process.exit(1); });
@@ -1,15 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-mitre-attack.js
5
- *
6
- * Thin per-type wrapper for the MITRE ATT&CK refresher. Logic lives in
4
+ * Refreshes only the MITRE ATT&CK catalog. Logic lives in
7
5
  * scripts/refresh-upstream-catalogs.js#refreshAttack.
8
- *
9
- * node scripts/refresh-mitre-attack.js [--dry-run]
10
- * CAP=200 node scripts/refresh-mitre-attack.js
11
- *
12
- * Wired as `npm run refresh-mitre-attack`.
13
6
  */
14
7
  const { refreshAttack } = require("./refresh-upstream-catalogs.js");
15
8
  const dry = process.argv.includes("--dry-run");
@@ -1,15 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-mitre-d3fend.js
5
- *
6
- * Thin per-type wrapper for the MITRE D3FEND refresher. Logic lives in
7
- * scripts/refresh-upstream-catalogs.js#refreshD3fend.
8
- *
9
- * node scripts/refresh-mitre-d3fend.js [--dry-run]
10
- * CAP=120 node scripts/refresh-mitre-d3fend.js
11
- *
12
- * Wired as `npm run refresh-mitre-d3fend`.
4
+ * Thin wrapper for the MITRE D3FEND refresher; the logic lives in
5
+ * scripts/refresh-upstream-catalogs.js#refreshD3fend. Wired as
6
+ * `npm run refresh-mitre-d3fend`.
13
7
  */
14
8
  const { refreshD3fend } = require("./refresh-upstream-catalogs.js");
15
9
  const dry = process.argv.includes("--dry-run");
@@ -1,15 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-mitre-ics-attack.js
4
+ * `npm run refresh-mitre-ics-attack [-- --dry-run]` — per-type wrapper for the
5
+ * MITRE ICS-attack STIX refresher; the logic lives in
6
+ * scripts/refresh-upstream-catalogs.js#refreshIcsAttack.
5
7
  *
6
- * Thin per-type wrapper for the MITRE ICS-attack STIX refresher. Logic
7
- * lives in scripts/refresh-upstream-catalogs.js#refreshIcsAttack.
8
- *
9
- * node scripts/refresh-mitre-ics-attack.js [--dry-run]
10
- *
11
- * Wired as `npm run refresh-mitre-ics-attack`.
8
+ * `CAP=<n>` bounds how many NEW techniques one run may add, matching every
9
+ * other per-type wrapper. Context backfill onto existing rows is never capped.
12
10
  */
13
11
  const { refreshIcsAttack } = require("./refresh-upstream-catalogs.js");
14
12
  const dry = process.argv.includes("--dry-run");
15
- refreshIcsAttack({ dry }).catch((e) => { console.error("[err]", e); process.exit(1); });
13
+ const cap = Number(process.env.CAP || Infinity);
14
+ refreshIcsAttack({ dry, cap }).catch((e) => { console.error("[err]", e); process.exit(1); });
@@ -1,35 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * scripts/refresh-reverse-refs.js — rebuild reverse references in
4
- * data/{atlas-ttps,cwe-catalog,d3fend-catalog,rfc-references}.json from
5
- * the manifest.json forward direction.
6
- *
7
- * Background. Each skill in manifest.json declares forward references via
8
- * atlas_refs / cwe_refs / d3fend_refs / rfc_refs. The four catalogs above
9
- * carry a denormalised reverse field per entry (`exceptd_skills` for
10
- * atlas-ttps, `skills_referencing` for the other three) listing every
11
- * skill that points at that entry. The reverse field drifts whenever a
12
- * skill adds or removes a forward ref without the catalog being updated
13
- * in lockstep — this script rebuilds the reverse direction from the
14
- * forward source of truth so the two never disagree.
15
- *
16
- * Behaviour. For each catalog file:
17
- * 1. Walk every skill's relevant forward-ref array in manifest.json.
18
- * 2. For every catalog entry, list every skill that references it.
19
- * 3. Sort the resulting skill list and write it back into the per-entry
20
- * reverse field. All other fields are preserved untouched.
21
- *
22
- * The script is idempotent: a second run produces no further changes.
23
- *
24
- * The script does NOT touch playbooks_referencing — that field carries
25
- * playbook ids (data/playbooks/*.json), not skill names; it has its own
26
- * source of truth and is out of scope for this refresh.
27
- *
28
- * Run: node scripts/refresh-reverse-refs.js
29
- * npm run refresh-reverse-refs
30
- *
31
- * Exit code: 0 always (script is unconditionally write-mode). Use
32
- * tests/reverse-ref-drift.test.js as the read-only drift detector.
3
+ * Rebuilds the denormalised reverse-reference fields in the data catalogs from the
4
+ * forward direction, which is the source of truth: a skill's atlas_refs / cwe_refs
5
+ * / d3fend_refs / rfc_refs, and a CVE's forward refs. Every other field is
6
+ * preserved and a second run changes nothing. `playbooks_referencing` is out of
7
+ * scope — it carries playbook ids, not skill names.
8
+ * Write-mode only, exit 0 always; the read-only drift detector is
9
+ * tests/reverse-ref-drift.test.js.
33
10
  */
34
11
 
35
12
  'use strict';
@@ -43,17 +20,12 @@ const CVE_CATALOG_PATH = path.join(REPO_ROOT, 'data', 'cve-catalog.json');
43
20
  const DATA_DIR = path.join(REPO_ROOT, 'data');
44
21
 
45
22
  /* Per-catalog config:
46
- * file relative path under data/
47
- * forwardField source-collection[].* array name
48
- * reverseField per-entry reverse field name in the catalog
49
- * source 'manifest.skills' (default) — walk every skill's forward ref
50
- * 'cve.entries' — walk every CVE's forward ref (added in
51
- * v0.12.32); contributes CVE-IDs (skipping `_draft: true`
52
- * entries so the reverse direction tracks operator-queryable
53
- * truth, not in-progress curation state)
54
- * entryKey field on the source object used as the reverse-list value
55
- * ('name' for skills; '<self-id>' for CVE entries via the
56
- * map key, so the helper substitutes the iterating key)
23
+ * file relative path under data/
24
+ * forwardField array (or dict, with forwardFieldShape) on the source object
25
+ * reverseField per-entry field this script overwrites in the catalog
26
+ * source 'manifest.skills' walks every skill; 'cve.entries' every
27
+ * non-draft CVE
28
+ * entryKey field used as the reverse-list value; null means the key itself
57
29
  */
58
30
  const CATALOGS = [
59
31
  {
@@ -84,12 +56,7 @@ const CATALOGS = [
84
56
  source: 'manifest.skills',
85
57
  entryKey: 'name',
86
58
  },
87
- // v0.12.32: CVE → CWE reverse direction. CWE entries declare
88
- // `evidence_cves` as the operator-facing "which CVEs land here" index;
89
- // previously hand-maintained and drifted whenever a new CVE landed
90
- // without the matching CWE's evidence_cves being updated. Now mirrors
91
- // `cve.cwe_refs` → `cwe.evidence_cves` automatically. Drafts excluded
92
- // (they're invisible to default consumers anyway).
59
+ // `evidence_cves` mirrors cve.cwe_refs: which CVEs land on this CWE.
93
60
  {
94
61
  file: 'cwe-catalog.json',
95
62
  forwardField: 'cwe_refs',
@@ -97,14 +64,8 @@ const CATALOGS = [
97
64
  source: 'cve.entries',
98
65
  entryKey: null, // value is the iterating CVE id
99
66
  },
100
- // v0.12.40: CVE → framework-gap reverse direction. Resolved 137
101
- // directional mismatches between cve.framework_control_gaps (dict-keyed
102
- // by gap-id) and gap.evidence_cves (array of CVE ids). The forward
103
- // shape on the CVE side is an OBJECT not an array — keys are the gap
104
- // ids, values are per-CVE narrative. The reverse direction (which CVEs
105
- // cite this gap) is a simple set of CVE ids on the gap entry. The
106
- // helper handles the dict-keyed forward field via the
107
- // `forwardFieldShape: 'object-keys'` flag.
67
+ // The forward side here is an OBJECT, not an array: keys are gap ids, values
68
+ // the per-CVE narrative, hence forwardFieldShape. The reverse is a set of ids.
108
69
  {
109
70
  file: 'framework-control-gaps.json',
110
71
  forwardField: 'framework_control_gaps',
@@ -113,12 +74,7 @@ const CATALOGS = [
113
74
  source: 'cve.entries',
114
75
  entryKey: null, // value is the iterating CVE id
115
76
  },
116
- // v0.13.0: ATLAS / ATT&CK back-edge — every ATLAS TTP and ATT&CK
117
- // technique gets a `cve_refs` array carrying the CVE ids that cite it.
118
- // Pre-v0.13 these catalogs had only forward refs (CVE → TTP); operators
119
- // reading an ATLAS / ATT&CK entry could not see which CVEs cite it
120
- // without grepping the whole catalog. These back-edges make the
121
- // relationship symmetric: a citation is readable from either end.
77
+ // `cve_refs` back-edges: an ATLAS or ATT&CK entry shows which CVEs cite it.
122
78
  {
123
79
  file: 'atlas-ttps.json',
124
80
  forwardField: 'atlas_refs',
@@ -152,16 +108,9 @@ function buildReverseIndex(skills, forwardField) {
152
108
  return index;
153
109
  }
154
110
 
155
- // v0.12.32: build a reverse index keyed by catalog ID from the CVE
156
- // catalog's forward refs. Each CVE entry has cwe_refs / attack_refs
157
- // arrays; the reverse side is the CVE ID, indexed by the catalog entry.
158
- // Draft entries are skipped — drafts are invisible to default consumers
159
- // via cross-ref-api, so the reverse direction should track operator-
160
- // queryable truth, not in-progress curation state.
161
- //
162
- // v0.12.40: forwardFieldShape parameter handles the
163
- // CVE.framework_control_gaps case where the forward field is a dict
164
- // (gap-id → narrative) rather than an array.
111
+ // catalogEntryId -> Set<cveId>. Drafts are skipped: they are invisible to default
112
+ // consumers through cross-ref-api. Pass forwardFieldShape 'object-keys' when the
113
+ // forward field is a dict.
165
114
  function buildCveReverseIndex(cveCatalog, forwardField, forwardFieldShape) {
166
115
  const index = new Map();
167
116
  for (const [cveId, entry] of Object.entries(cveCatalog)) {
@@ -221,14 +170,9 @@ function rebuildCatalog(cfg, manifest, cveCatalog) {
221
170
  }
222
171
  }
223
172
 
224
- // Surface forward refs that point at catalog entries that don't exist.
225
- // Informational only — orphans never change the exit code (this script is
226
- // unconditionally write-mode, exit 0 always; see the file header). The
227
- // failing gates for the two signals this script can surface live
228
- // elsewhere: orphan forward refs are hard-errored by lib/lint-skills.js
229
- // ref-resolution ("<id> not present in data/<catalog>"), and reverse-field
230
- // drift is hard-failed by tests/reverse-ref-drift.test.js. Do not wire
231
- // this script itself as a "reverse refs clean?" check — it always passes.
173
+ // Orphans — forward refs naming a catalog entry that does not exist — are
174
+ // reported, never fatal. Never wire this script up as a "reverse refs clean?"
175
+ // check: the failing gates are lib/lint-skills.js and reverse-ref-drift.test.js.
232
176
  for (const id of index.keys()) {
233
177
  if (!seenIds.has(id)) orphans.push(id);
234
178
  }
@@ -249,15 +193,8 @@ function rebuildCatalog(cfg, manifest, cveCatalog) {
249
193
  };
250
194
  }
251
195
 
252
- /**
253
- * v0.13.0: playbook `fed_by` reverse direction. Every playbook declares
254
- * `feeds_into[]` listing playbook ids it chains TO; `fed_by[]` is the
255
- * symmetric "which playbooks chain into me." Pre-v0.13 operators reading
256
- * a playbook couldn't see what fed it without grepping every other
257
- * playbook's feeds_into. The reverse field lives on the playbook's
258
- * top-level under `_meta.fed_by` (paired with the existing _meta.feeds_into
259
- * for shape symmetry).
260
- */
196
+ // `_meta.fed_by` is the symmetric counterpart of `_meta.feeds_into`: which
197
+ // playbooks chain INTO this one. Writes each playbook file in place.
261
198
  function rebuildPlaybookReverse() {
262
199
  const playbooksDir = path.join(DATA_DIR, 'playbooks');
263
200
  if (!fs.existsSync(playbooksDir)) return { file: 'playbooks/*.json', source: 'playbook.feeds_into', reverseField: 'fed_by', changed: 0, added: 0, removed: 0, unchanged: 0, orphans: [] };
@@ -271,12 +208,8 @@ function rebuildPlaybookReverse() {
271
208
  if (!data || !data._meta || !data._meta.id) continue;
272
209
  playbookEntries.push({ file: f, filePath, data });
273
210
  }
274
- // Build reverse index: target-id -> Set<source-id>
275
- // feeds_into entries are objects with `playbook_id` + `condition`.
276
- // The fed_by reverse is a simple array of source playbook ids (the
277
- // condition is per-edge; preserved on feeds_into, not duplicated on
278
- // fed_by — operators read fed_by to find candidates, then look at the
279
- // source playbook's feeds_into for the gating condition).
211
+ // targetId -> Set<sourceId>. A feeds_into entry carries `playbook_id` +
212
+ // `condition`; fed_by keeps only the ids, so the condition is not duplicated.
280
213
  const index = new Map();
281
214
  for (const { data } of playbookEntries) {
282
215
  const meta = data._meta;
@@ -1,17 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  /**
4
- * scripts/refresh-rfc-index.js
4
+ * Refreshes only the RFC catalog, leaving ATT&CK / ATLAS / D3FEND untouched.
5
+ * Logic lives in scripts/refresh-upstream-catalogs.js#refreshRfc.
5
6
  *
6
- * Thin per-type wrapper for the RFC refresher. Logic lives in
7
- * scripts/refresh-upstream-catalogs.js#refreshRfc. Use this entry when
8
- * you want to refresh only the RFC catalog without touching ATT&CK /
9
- * ATLAS / D3FEND.
10
- *
11
- * node scripts/refresh-rfc-index.js [--dry-run]
12
- *
13
- * Wired as `npm run refresh-rfc-index`.
7
+ * `CAP=<n>` bounds how many NEW rows one run may add — the upstream RFC index
8
+ * carries ~9000 entries, so an uncapped first run imports all of them at once.
9
+ * Context backfill onto rows already curated is never capped.
14
10
  */
15
11
  const { refreshRfc } = require("./refresh-upstream-catalogs.js");
16
12
  const dry = process.argv.includes("--dry-run");
17
- refreshRfc({ dry }).catch((e) => { console.error("[err]", e); process.exit(1); });
13
+ const cap = Number(process.env.CAP || Infinity);
14
+ refreshRfc({ dry, cap }).catch((e) => { console.error("[err]", e); process.exit(1); });