@blamejs/exceptd-skills 0.13.20 → 0.13.21

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.
@@ -278,13 +278,42 @@ function emitPretty(report) {
278
278
  const pct = r.entries === 0 ? 0 : ((r.auto_imported / r.entries) * 100).toFixed(1);
279
279
  lines.push(` ${r.catalog.padEnd(28)} ${r.auto_imported} / ${r.entries} (${pct}%)`);
280
280
  }
281
+ // v0.13.21 extended findings sections.
282
+ const ext = report.extended_findings || {};
283
+ const extClasses = Object.keys(ext).sort();
284
+ if (extClasses.length > 0) {
285
+ lines.push("\nExtended findings (v0.13.21):");
286
+ for (const cls of extClasses) {
287
+ const arr = ext[cls];
288
+ lines.push(`\n [${cls}] ${arr.length} finding(s)`);
289
+ for (const f of arr.slice(0, 5)) {
290
+ const id = f.id || f.target_id || "(no id)";
291
+ const ctx = f.catalog ? `${f.catalog} ${id}` : id;
292
+ lines.push(` ${ctx} — ${f.reason || f.rule || "(no reason)"}`);
293
+ }
294
+ if (arr.length > 5) lines.push(` ... +${arr.length - 5} more`);
295
+ }
296
+ }
281
297
  return lines.join("\n");
282
298
  }
283
299
 
284
- // Valid finding-class names for the `--class` filter. The pretty + JSON
285
- // emitters always include every section, but counts and strict-exit
286
- // gating respect the active filter.
287
- const VALID_CLASSES = new Set(["missing-context", "dangling-ref", "draft-debt"]);
300
+ // Valid finding-class names for the `--class` filter. v0.13.21 added 7
301
+ // extended detection classes for gaps the v0.13.19 detector did not
302
+ // surface (content-quality / temporal-staleness / logical-consistency /
303
+ // cross-ref-completeness / schema-evolution / operator-action-sla /
304
+ // unused-orphan). Each is implemented in lib/gap-detectors.js.
305
+ const VALID_CLASSES = new Set([
306
+ "missing-context", "dangling-ref", "draft-debt",
307
+ "content-quality", "temporal-staleness", "logical-consistency",
308
+ "cross-ref-completeness", "schema-evolution", "operator-action-sla",
309
+ "unused-orphan"
310
+ ]);
311
+ const EXTENDED_CLASS_NAMES = new Set([
312
+ "content-quality", "temporal-staleness", "logical-consistency",
313
+ "cross-ref-completeness", "schema-evolution", "operator-action-sla",
314
+ "unused-orphan"
315
+ ]);
316
+ const EXTENDED_DETECTORS = require("../lib/gap-detectors.js");
288
317
 
289
318
  function main() {
290
319
  const opts = parseArgs(process.argv);
@@ -309,27 +338,47 @@ function main() {
309
338
  for (const k of Object.keys(SPEC)) if (!allLoaded[k]) allLoaded[k] = loadCatalog(SPEC[k].file);
310
339
  const dangling = opts.catalog && opts.catalog !== "cve-catalog" ? [] : inspectRefs(allLoaded);
311
340
 
341
+ // v0.13.21 extended detectors. --catalog scoping mutes them (they're
342
+ // cross-catalog by nature); --class scoping filters down to one.
343
+ const extendedFindings = opts.catalog
344
+ ? []
345
+ : EXTENDED_DETECTORS.runAllDetectors(allLoaded, {});
346
+
312
347
  // Apply the --class filter before counts + strict-exit gating.
313
- // Missing-context findings on per_catalog and dangling_refs are the
314
- // two policed classes; draft-debt is informational-only (the audit
315
- // surfaces draft-debt but it does not fail strict mode by design).
316
- const filteredPerCatalog = opts.klass === "dangling-ref" || opts.klass === "draft-debt"
348
+ const filteredPerCatalog = (opts.klass === "dangling-ref" || opts.klass === "draft-debt" ||
349
+ EXTENDED_CLASS_NAMES.has(opts.klass))
317
350
  ? perCatalog.map((r) => ({ ...r, missing_context: [] }))
318
351
  : perCatalog;
319
- const filteredDangling = opts.klass === "missing-context" || opts.klass === "draft-debt"
352
+ const filteredDangling = (opts.klass === "missing-context" || opts.klass === "draft-debt" ||
353
+ EXTENDED_CLASS_NAMES.has(opts.klass))
320
354
  ? []
321
355
  : dangling;
356
+ const filteredExtended = opts.klass
357
+ ? (EXTENDED_CLASS_NAMES.has(opts.klass)
358
+ ? extendedFindings.filter((f) => f.class === opts.klass)
359
+ : [])
360
+ : extendedFindings;
361
+
362
+ const extendedByClass = {};
363
+ for (const f of filteredExtended) {
364
+ if (!extendedByClass[f.class]) extendedByClass[f.class] = [];
365
+ extendedByClass[f.class].push(f);
366
+ }
322
367
 
323
368
  const report = {
324
369
  generated_at: TODAY,
325
370
  class_filter: opts.klass || null,
326
371
  per_catalog: filteredPerCatalog,
327
372
  dangling_refs: filteredDangling,
373
+ extended_findings: extendedByClass,
328
374
  totals: {
329
375
  catalogs: filteredPerCatalog.length,
330
376
  entries: filteredPerCatalog.reduce((n, r) => n + r.entries, 0),
331
377
  missing_context: filteredPerCatalog.reduce((n, r) => n + r.missing_context.length, 0),
332
- dangling_refs: filteredDangling.length
378
+ dangling_refs: filteredDangling.length,
379
+ extended: Object.fromEntries(
380
+ Object.entries(extendedByClass).map(([cls, arr]) => [cls, arr.length])
381
+ )
333
382
  }
334
383
  };
335
384
  if (opts.pretty) {
@@ -337,7 +386,10 @@ function main() {
337
386
  } else {
338
387
  process.stdout.write(JSON.stringify(report, null, 2) + "\n");
339
388
  }
340
- if (opts.strict && (report.totals.missing_context > 0 || report.totals.dangling_refs > 0)) {
389
+ const extendedTotal = Object.values(report.totals.extended || {}).reduce((n, v) => n + v, 0);
390
+ if (opts.strict && (report.totals.missing_context > 0 ||
391
+ report.totals.dangling_refs > 0 ||
392
+ extendedTotal > 0)) {
341
393
  process.exitCode = 1;
342
394
  }
343
395
  }
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * scripts/check-catalog-gap-budget.js
5
+ *
6
+ * Predeploy / CI gate that runs the v0.13.21 extended gap detectors
7
+ * and asserts no class exceeds its budget. Mirrors the budget in
8
+ * tests/shipped-catalog-integrity.test.js but runs as a standalone
9
+ * predeploy gate so the check is visible in the gate summary even
10
+ * when the broader test suite is skipped (or is the gate that's
11
+ * failing for an unrelated reason).
12
+ *
13
+ * Exit codes:
14
+ * 0 — every extended class within budget
15
+ * 1 — at least one class regressed
16
+ * 2 — internal error
17
+ *
18
+ * The budget is intentionally duplicated (here + integrity test) for
19
+ * fail-loud-at-two-levels. Operators see the regression in BOTH the
20
+ * test-suite output AND the predeploy gate-summary table.
21
+ */
22
+
23
+ const path = require("path");
24
+ const fs = require("fs");
25
+ const ROOT = path.join(__dirname, "..");
26
+
27
+ let D;
28
+ try {
29
+ D = require(path.join(ROOT, "lib", "gap-detectors.js"));
30
+ } catch (e) {
31
+ console.error("[check-catalog-gap-budget] failed to load lib/gap-detectors.js:", e.message);
32
+ process.exit(2);
33
+ }
34
+
35
+ function loadAll() {
36
+ const data = path.join(ROOT, "data");
37
+ const read = (name) => JSON.parse(fs.readFileSync(path.join(data, name), "utf8"));
38
+ return {
39
+ "cve-catalog": read("cve-catalog.json"),
40
+ "cwe-catalog": read("cwe-catalog.json"),
41
+ "attack-techniques": read("attack-techniques.json"),
42
+ "atlas-ttps": read("atlas-ttps.json"),
43
+ "d3fend-catalog": read("d3fend-catalog.json"),
44
+ "rfc-references": read("rfc-references.json"),
45
+ "framework-control-gaps": read("framework-control-gaps.json"),
46
+ "zeroday-lessons": read("zeroday-lessons.json")
47
+ };
48
+ }
49
+
50
+ // Per-class regression budgets. Kept in sync with the canonical version
51
+ // in tests/shipped-catalog-integrity.test.js.
52
+ const BUDGET = {
53
+ "content-quality": 12,
54
+ "temporal-staleness": 260,
55
+ "logical-consistency": 5,
56
+ "cross-ref-completeness": 5,
57
+ "schema-evolution": 0,
58
+ "operator-action-sla": 0,
59
+ "unused-orphan": 1400
60
+ };
61
+
62
+ function main() {
63
+ const all = D.runAllDetectors(loadAll(), {});
64
+ const byClass = {};
65
+ for (const f of all) byClass[f.class] = (byClass[f.class] || 0) + 1;
66
+ const regressions = [];
67
+
68
+ // Fail-closed contract (codex P2 PR #61): every class actually
69
+ // emitted by the detector must have a budget entry. If a future
70
+ // 8th detector lands without a budget update, the gate fires with
71
+ // an unbudgeted-class error instead of silently passing.
72
+ const unbudgeted = [];
73
+ for (const cls of Object.keys(byClass)) {
74
+ if (!(cls in BUDGET)) {
75
+ unbudgeted.push({ class: cls, count: byClass[cls] });
76
+ }
77
+ }
78
+ // Inverse check: every class declared by the detector module's
79
+ // canonical class list must appear in BUDGET (covers the case where
80
+ // a new class produces zero findings on this run but still needs
81
+ // an explicit budget so a future regression caps fail-closed).
82
+ const missingBudget = [];
83
+ if (Array.isArray(D.DETECTOR_CLASSES)) {
84
+ for (const cls of D.DETECTOR_CLASSES) {
85
+ if (!(cls in BUDGET)) missingBudget.push(cls);
86
+ }
87
+ }
88
+
89
+ for (const cls of Object.keys(BUDGET)) {
90
+ const actual = byClass[cls] || 0;
91
+ const allowed = BUDGET[cls];
92
+ if (actual > allowed) {
93
+ regressions.push({ class: cls, allowed, actual, delta: actual - allowed });
94
+ }
95
+ }
96
+ const summary = Object.keys(BUDGET).map((cls) => {
97
+ const actual = byClass[cls] || 0;
98
+ const allowed = BUDGET[cls];
99
+ const mark = actual > allowed ? "✗" : "✓";
100
+ return ` ${mark} ${cls.padEnd(28)} actual=${actual} budget=${allowed}`;
101
+ }).join("\n");
102
+ console.log("[check-catalog-gap-budget] extended detection classes:");
103
+ console.log(summary);
104
+
105
+ if (unbudgeted.length > 0) {
106
+ console.error("\n[check-catalog-gap-budget] UNBUDGETED detector classes — fail-closed:");
107
+ for (const u of unbudgeted) {
108
+ console.error(` ${u.class}: ${u.count} finding(s), no BUDGET entry`);
109
+ }
110
+ console.error("Add an explicit budget entry in both:");
111
+ console.error(" scripts/check-catalog-gap-budget.js");
112
+ console.error(" tests/shipped-catalog-integrity.test.js");
113
+ process.exit(1);
114
+ }
115
+ if (missingBudget.length > 0) {
116
+ console.error("\n[check-catalog-gap-budget] BUDGET missing entries for declared classes:");
117
+ for (const c of missingBudget) console.error(` ${c}: declared by lib/gap-detectors.js DETECTOR_CLASSES, no BUDGET entry`);
118
+ process.exit(1);
119
+ }
120
+ if (regressions.length > 0) {
121
+ console.error("\n[check-catalog-gap-budget] REGRESSION beyond budget:");
122
+ for (const r of regressions) {
123
+ console.error(` ${r.class}: actual=${r.actual} > budget=${r.allowed} (delta +${r.delta})`);
124
+ }
125
+ console.error("\nClose the gap in this PR (preferred) or update BUDGET in both:");
126
+ console.error(" scripts/check-catalog-gap-budget.js");
127
+ console.error(" tests/shipped-catalog-integrity.test.js");
128
+ process.exit(1);
129
+ }
130
+ console.log("[check-catalog-gap-budget] all classes within budget; every class is budgeted.");
131
+ }
132
+
133
+ main();
@@ -193,6 +193,20 @@ const GATES = [
193
193
  // shares the integrity-tier framing with manifest-snapshot etc.
194
194
  ciJobName: "Data integrity (catalog + manifest snapshot)",
195
195
  },
196
+ {
197
+ // v0.13.21: catalog-gap budget gate. Runs the seven extended
198
+ // detection classes added in v0.13.21 (content-quality,
199
+ // temporal-staleness, logical-consistency, cross-ref-completeness,
200
+ // schema-evolution, operator-action-sla, unused-orphan) against
201
+ // the shipped catalog and fails if any class regresses beyond its
202
+ // documented budget. Mirrors the budget enforced by
203
+ // tests/shipped-catalog-integrity.test.js so the regression
204
+ // surfaces in BOTH the gate-summary table AND the test output.
205
+ name: "Catalog-gap budget (v0.13.21 extended detection classes)",
206
+ command: process.execPath,
207
+ args: [path.join(ROOT, "scripts", "check-catalog-gap-budget.js")],
208
+ ciJobName: "Data integrity (catalog + manifest snapshot)",
209
+ },
196
210
  ];
197
211
 
198
212
  function runGate(gate) {