@blamejs/exceptd-skills 0.15.45 → 0.15.47

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.
@@ -91,14 +91,13 @@ const REQUIRED_SECTIONS = [
91
91
 
92
92
  // L3 — Defensive Countermeasure Mapping became a required section for skills
93
93
  // reviewed on or after this cutoff (documented in AGENTS.md). Pre-cutoff
94
- // skills remain exempt to preserve patch-class compatibility; v0.13.0 may
95
- // broaden the cutoff.
94
+ // skills remain exempt to preserve patch-class compatibility.
96
95
  const COUNTERMEASURE_SECTION = 'Defensive Countermeasure Mapping';
97
96
  const COUNTERMEASURE_CUTOFF = '2026-05-11';
98
97
 
99
98
  // L1 — Minimum number of words of body text between a section heading and the
100
99
  // next heading (or EOF) for the section to count as populated. Header-only
101
- // sections surface as WARNINGS in v0.12.12; v0.13.0 will tighten to failure.
100
+ // sections surface as warnings; promoted to failures under --strict.
102
101
  const MIN_SECTION_BODY_WORDS = 20;
103
102
 
104
103
  const PLACEHOLDER_PATTERNS = [
@@ -396,8 +395,8 @@ function validateFrontmatter(fm, skillName) {
396
395
  * in the body (case-insensitive). Hard failure.
397
396
  * - headerOnly[] — sections whose heading exists but whose body between
398
397
  * that heading and the next heading is shorter than
399
- * MIN_SECTION_BODY_WORDS words. Warning in v0.12.12;
400
- * v0.13.0 will tighten. */
398
+ * MIN_SECTION_BODY_WORDS words. A warning by default;
399
+ * promoted to an error under --strict. */
401
400
  function findMissingSections(body, requiredSections) {
402
401
  const sections = requiredSections || REQUIRED_SECTIONS;
403
402
  const lines = body.split(/\r?\n/);
@@ -563,16 +562,16 @@ function lintSkill(entry, ctx) {
563
562
  }
564
563
  }
565
564
 
566
- // L2 — attack_refs cross-catalog resolution. Surface as WARNINGS in
567
- // v0.12.12 to preserve patch-class compatibility; v0.13.0 will flip to
568
- // hard failures. If data/attack-techniques.json is missing entirely the
569
- // ctx.attackKeys set is null skip the check (the gate degrades to its
570
- // pre-v0.12.12 behavior).
565
+ // L2 — attack_refs cross-catalog resolution. Surface as warnings by
566
+ // default (preserving patch-class compatibility); promoted to hard
567
+ // failures under --strict (the predeploy gate). If
568
+ // data/attack-techniques.json is missing entirely the ctx.attackKeys set
569
+ // is null — skip the check (the gate degrades gracefully).
571
570
  if (Array.isArray(fm.attack_refs) && ctx.attackKeys) {
572
571
  for (const ref of fm.attack_refs) {
573
572
  if (!ctx.attackKeys.has(ref)) {
574
573
  skillWarnings.push(
575
- `attack_refs: "${ref}" not present in data/attack-techniques.json (will hard-fail in v0.13.0)`,
574
+ `attack_refs: "${ref}" not present in data/attack-techniques.json (an error under --strict)`,
576
575
  );
577
576
  }
578
577
  }
@@ -617,18 +616,18 @@ function lintSkill(entry, ctx) {
617
616
 
618
617
  // L3 — Defensive Countermeasure Mapping is required for skills reviewed
619
618
  // on or after COUNTERMEASURE_CUTOFF. Pre-cutoff skills are exempt. The
620
- // section's absence on a post-cutoff skill is a WARNING in v0.12.12 so
621
- // existing skills can add the section gradually; v0.13.0 will flip to
622
- // a hard failure.
619
+ // section's absence on a post-cutoff skill is a warning by default so
620
+ // existing skills can add the section gradually; promoted to a hard
621
+ // failure under --strict.
623
622
  const { missing, headerOnly } = findMissingSections(body, REQUIRED_SECTIONS);
624
623
  for (const s of missing) {
625
624
  skillErrors.push(`body: missing required section "${s}"`);
626
625
  }
627
626
  for (const ho of headerOnly) {
628
- // L1 — Header-only sections are WARNINGS in v0.12.12; v0.13.0 will
629
- // tighten to failure.
627
+ // L1 — Header-only sections are warnings by default; promoted to a
628
+ // failure under --strict.
630
629
  skillWarnings.push(
631
- `body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); will hard-fail in v0.13.0`,
630
+ `body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
632
631
  );
633
632
  }
634
633
  if (
@@ -639,12 +638,12 @@ function lintSkill(entry, ctx) {
639
638
  const cmResult = findMissingSections(body, [COUNTERMEASURE_SECTION]);
640
639
  if (cmResult.missing.length > 0) {
641
640
  skillWarnings.push(
642
- `body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF}; will hard-fail in v0.13.0)`,
641
+ `body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF}; an error under --strict)`,
643
642
  );
644
643
  } else {
645
644
  for (const ho of cmResult.headerOnly) {
646
645
  skillWarnings.push(
647
- `body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); will hard-fail in v0.13.0`,
646
+ `body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
648
647
  );
649
648
  }
650
649
  }
@@ -74,7 +74,7 @@ try {
74
74
 
75
75
  // Probe the catalog (parse it, surface any load error) LAZILY on first need
76
76
  // rather than at module load. The probe parses the ~2.6MB CVE catalog (~8.5ms);
77
- // doing it eagerly charged that to every cheap verb (brief/plan/look/ask/lint/
77
+ // doing it eagerly charged that to every cheap verb (brief/ask/lint/
78
78
  // discover) that never analyzes. run() calls this before the analyze path, so
79
79
  // a corrupt catalog still surfaces as blocked_by:'catalog_corrupt' before
80
80
  // analyze — just not on verbs that don't touch the catalog. Memoized: probes
@@ -1829,7 +1829,11 @@ function close(playbookId, directiveId, analyzeResult, validateResult, agentSign
1829
1829
  // Severity ladder for active_exploitation. The worst-of reduction lets
1830
1830
  // analyzeFindingShape report the most-exploited CVE in the matched set, not
1831
1831
  // the first-encountered one. Higher index = worse.
1832
- const ACTIVE_EXPLOITATION_RANK = { none: 0, unknown: 1, suspected: 2, confirmed: 3 };
1832
+ // `theoretical` (PoC exists, no in-the-wild use) must rank between `none` and
1833
+ // `unknown`; omitting it made `?? -1` lose to the -1 start, so an all-theoretical
1834
+ // matched set wrongly reduced to 'unknown' and a theoretical+none set dropped
1835
+ // the theoretical entry entirely. This vocabulary is first-class in scoring.js.
1836
+ const ACTIVE_EXPLOITATION_RANK = { none: 0, theoretical: 1, unknown: 2, suspected: 3, confirmed: 4 };
1833
1837
 
1834
1838
  function worstActiveExploitation(matchedCves) {
1835
1839
  let worst = null;
@@ -1840,7 +1844,9 @@ function worstActiveExploitation(matchedCves) {
1840
1844
  const rank = ACTIVE_EXPLOITATION_RANK[v] ?? -1;
1841
1845
  if (rank > worstRank) { worst = v; worstRank = rank; }
1842
1846
  }
1843
- return worst || 'unknown';
1847
+ // Empty / all-unrecognized matched set → 'none' (a draft must not assert
1848
+ // 'unknown' exploitation it never observed).
1849
+ return worst || 'none';
1844
1850
  }
1845
1851
 
1846
1852
  // Severity ladder derived from rwep_adjusted. Playbooks reference
@@ -3838,4 +3844,6 @@ module.exports = {
3838
3844
  _acquireLockDiagnostic: acquireLockDiagnostic,
3839
3845
  _releaseLock: releaseLock,
3840
3846
  _lockFilePath: lockFilePath,
3847
+ _vulnIdToUrn: vulnIdToUrn,
3848
+ _worstActiveExploitation: worstActiveExploitation,
3841
3849
  };
@@ -62,7 +62,7 @@ function parseArgs(argv) {
62
62
  'Usage: node lib/validate-catalog-meta.js [--quiet] [--strict]\n' +
63
63
  '\n' +
64
64
  ' --quiet Suppress per-catalog PASS output; show failures only.\n' +
65
- ' --strict Promote v0.13.0-preview warnings (freshness) to errors.\n',
65
+ ' --strict Promote freshness warnings to errors (used by the predeploy gate).\n',
66
66
  );
67
67
  process.exit(0);
68
68
  } else {
@@ -165,11 +165,12 @@ function validateMeta(catalogPath, opts) {
165
165
 
166
166
  /* freshness enforcement. When both meta.last_updated and
167
167
  * freshness_policy.stale_after_days are present, surface a warning if
168
- * (now - last_updated) > stale_after_days. Patch-class release emits at
169
- * WARN level (does not fail validation); v0.13.0 will flip to an error.
168
+ * (now - last_updated) > stale_after_days. Emitted at WARN level by
169
+ * default (does not fail validation).
170
170
  *
171
171
  * Optional `opts.strict` (or `opts.errorOnStale`) promotes the warning
172
- * to an error today; predeploy keeps the warning posture.
172
+ * to an error; the predeploy gate runs --strict, plain validation keeps
173
+ * the warning posture.
173
174
  */
174
175
  if (
175
176
  typeof meta.last_updated === 'string' &&
@@ -185,7 +186,7 @@ function validateMeta(catalogPath, opts) {
185
186
  const msg =
186
187
  `_meta freshness: last_updated ${meta.last_updated} is ${ageDays} days old ` +
187
188
  `(stale_after_days = ${fp.stale_after_days}); refresh the catalog or bump _meta.last_updated. ` +
188
- `Will hard-fail in v0.13.0.`;
189
+ `Promoted to an error under --strict.`;
189
190
  if (opts && (opts.strict || opts.errorOnStale)) {
190
191
  errors.push(msg);
191
192
  } else {
@@ -40,8 +40,8 @@ const FRAMEWORK_GAPS_PATH = path.join(REPO_ROOT, 'data', 'framework-control-gaps
40
40
  // v0.12.12 — patterns that mark a verification_sources URL as a public exploit
41
41
  // or PoC location. When poc_available: true AND a verification source matches
42
42
  // one of these, the entry must carry an `iocs` block per AGENTS.md Hard Rule
43
- // #14. Surfaced as WARNING-only for v0.12.12 so drafts and pre-IoC entries
44
- // don't break patch-class compatibility; v0.13.0 will tighten to error.
43
+ // #14. Surfaced as a warning by default so drafts and pre-IoC entries don't
44
+ // break patch-class compatibility; promoted to an error under --strict.
45
45
  const PUBLIC_EXPLOIT_URL_PATTERNS = [
46
46
  /github\.com\/.+\/(exploits?|poc|pocs)\b/i,
47
47
  /\bexploit-?db\.com\b/i,
@@ -53,8 +53,8 @@ const PUBLIC_EXPLOIT_URL_PATTERNS = [
53
53
 
54
54
  // v0.12.12 — Tightened CVSS-vector prefix. Schema's existing pattern accepts
55
55
  // any "CVSS:<digits>/"; the strict pattern below admits only known CVSS
56
- // versions (2.0 / 3.0 / 3.1 / 4.0). Emitted as WARNING for v0.12.12; v0.13.0
57
- // will tighten the schema itself.
56
+ // versions (2.0 / 3.0 / 3.1 / 4.0). Emitted as a warning by default;
57
+ // promoted to an error under --strict.
58
58
  const STRICT_CVSS_PATTERN = /^CVSS:(2\.0|3\.[01]|4\.0)\//;
59
59
 
60
60
  // v0.12.12 — Impossible-date guard. Reject obviously bogus year ranges
@@ -80,7 +80,7 @@ function parseArgs(argv) {
80
80
  'Usage: node lib/validate-cve-catalog.js [--quiet] [--strict]\n' +
81
81
  '\n' +
82
82
  ' --quiet Suppress per-CVE PASS output; show failures only.\n' +
83
- ' --strict Promote v0.13.0-preview warnings to errors. Off by default.\n',
83
+ ' --strict Promote advisory warnings to errors (used by the predeploy gate). Off by default.\n',
84
84
  );
85
85
  process.exit(0);
86
86
  } else {
@@ -244,7 +244,7 @@ function additionalChecks(key, entry, ctx) {
244
244
  }
245
245
 
246
246
  // V2 — Cross-catalog reference resolution. Unresolved refs are warnings
247
- // for v0.12.x; v0.13.0 will flip to hard failures. V2 expansion
247
+ // by default; promoted to hard failures under --strict. V2 expansion
248
248
  // extends the walk from cwe_refs only to attack_refs, atlas_refs,
249
249
  // d3fend_refs, AND framework_control_gaps.
250
250
  const REF_FIELDS = [
@@ -266,7 +266,7 @@ function additionalChecks(key, entry, ctx) {
266
266
  if (typeof ref !== 'string') continue;
267
267
  if (!set.has(ref)) {
268
268
  warnings.push(
269
- `${key}: ${field} entry "${ref}" not in ${file} (will hard-fail in v0.13.0)`,
269
+ `${key}: ${field} entry "${ref}" not in ${file} (an error under --strict)`,
270
270
  );
271
271
  }
272
272
  }
@@ -302,7 +302,7 @@ function additionalChecks(key, entry, ctx) {
302
302
  if (typeof entry.cvss_vector === 'string' && entry.cvss_vector.length > 0) {
303
303
  if (!STRICT_CVSS_PATTERN.test(entry.cvss_vector)) {
304
304
  warnings.push(
305
- `${key}: cvss_vector ${JSON.stringify(entry.cvss_vector)} does not match the strict prefix /^CVSS:(2.0|3.0|3.1|4.0)\\//. Schema tolerates this in v0.12.12; v0.13.0 will tighten the schema.`,
305
+ `${key}: cvss_vector ${JSON.stringify(entry.cvss_vector)} does not match the strict prefix /^CVSS:(2.0|3.0|3.1|4.0)\\//. A warning by default; promoted to an error under --strict.`,
306
306
  );
307
307
  }
308
308
  }
@@ -42,8 +42,8 @@
42
42
  * jurisdiction_obligations an explicit `id` field; the shipped playbooks
43
43
  * reference them by this composite string).
44
44
  * - _meta.mutex is symmetric across the whole playbook set: if A lists B,
45
- * B must list A. Asymmetry surfaces as a warning in v0.12.16 (and will
46
- * flip to error in v0.13.0) — see checkMutexReciprocity().
45
+ * B must list A. Asymmetry surfaces as a warning by default (promoted to
46
+ * an error under --strict) — see checkMutexReciprocity().
47
47
  *
48
48
  * Finding severity:
49
49
  * - error — structural problems that block the runner (missing required
@@ -51,9 +51,9 @@
51
51
  * duplicate indicator id).
52
52
  * - warning — schema-shape drift the runner can still tolerate (enum
53
53
  * vocabulary lag, cross-catalog refs introduced after the
54
- * playbook last shipped). v0.12.12 surfaces these to the
55
- * operator without failing the gate; v0.13.0 will flip them
56
- * to hard errors via predeploy `informational: false`.
54
+ * playbook last shipped). Surfaced to the operator without
55
+ * failing the gate by default; promoted to hard errors under
56
+ * --strict (predeploy `informational: false`).
57
57
  *
58
58
  * Exit code: 0 if no errors (warnings allowed), 1 if any errors, 2 on
59
59
  * argv error.
@@ -61,8 +61,8 @@
61
61
  * Usage:
62
62
  * node lib/validate-playbooks.js validate every playbook
63
63
  * node lib/validate-playbooks.js --quiet only print FAIL playbooks + summary
64
- * node lib/validate-playbooks.js --strict treat warnings as errors (v0.13.0
65
- * preview).
64
+ * node lib/validate-playbooks.js --strict treat warnings as errors (used by
65
+ * the predeploy gate).
66
66
  */
67
67
 
68
68
  'use strict';
@@ -92,7 +92,7 @@ function parseArgs(argv) {
92
92
  'Usage: node lib/validate-playbooks.js [--quiet] [--strict]\n' +
93
93
  '\n' +
94
94
  ' --quiet Suppress per-playbook PASS output; show failures only.\n' +
95
- ' --strict Treat warnings as errors (v0.13.0 preview).\n',
95
+ ' --strict Treat warnings as errors (used by the predeploy gate).\n',
96
96
  );
97
97
  process.exit(0);
98
98
  } else {
@@ -129,8 +129,8 @@ function typeMatches(value, expected) {
129
129
  * shaped as { severity, message }. Severity defaults to 'error'; enum
130
130
  * mismatches and unknown additional properties under
131
131
  * additionalProperties:false are downgraded to 'warning' so vocabulary drift
132
- * between the schema and shipped playbooks does not hard-fail v0.12.12.
133
- * v0.13.0 will flip via --strict / predeploy informational:false. */
132
+ * between the schema and shipped playbooks does not hard-fail by default.
133
+ * Promoted to errors under --strict / predeploy informational:false. */
134
134
  function validate(value, schema, schemaName, pathStr) {
135
135
  const findings = [];
136
136
  const here = pathStr || schemaName;
@@ -222,7 +222,7 @@ function validate(value, schema, schemaName, pathStr) {
222
222
  findings.push(...validate(v, addlSchema, schemaName, `${here}.${k}`));
223
223
  } else if (!allowAdditional) {
224
224
  // Drift between schema and shipped data: surface as warning, not
225
- // an error. v0.13.0 will flip these.
225
+ // an error. Promoted to an error under --strict.
226
226
  err(`${here}: unexpected property "${k}"`, 'warning');
227
227
  }
228
228
  }
@@ -528,8 +528,8 @@ function checkCrossRefs(playbook, ctx, playbookIds) {
528
528
  * undeclared side is started first.
529
529
  *
530
530
  * Emits one warning per asymmetric pair (keyed off the side that declares
531
- * the edge). v0.12.16 keeps this at warning severity per the patch-class
532
- * cadence; v0.13.0 will flip it to error via --strict / predeploy
531
+ * the edge). Kept at warning severity by default per the patch-class
532
+ * cadence; promoted to an error under --strict / predeploy
533
533
  * `informational: false`.
534
534
  */
535
535
  function checkMutexReciprocity(playbooks) {
@@ -547,7 +547,7 @@ function checkMutexReciprocity(playbooks) {
547
547
  const otherSet = mutexMap.get(other);
548
548
  if (!otherSet) continue; // unresolved-id warning is already emitted by checkCrossRefs
549
549
  if (!otherSet.has(id)) {
550
- const msg = `_meta.mutex: asymmetric mutex with "${other}" — "${other}" does not list "${id}" in its _meta.mutex. v0.13.0 will flip this to a hard error.`;
550
+ const msg = `_meta.mutex: asymmetric mutex with "${other}" — "${other}" does not list "${id}" in its _meta.mutex. Promoted to a hard error under --strict.`;
551
551
  if (!byPlaybook.has(id)) byPlaybook.set(id, []);
552
552
  byPlaybook.get(id).push(msg);
553
553
  }