@blamejs/exceptd-skills 0.15.44 → 0.15.46

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.
@@ -1865,7 +1865,7 @@
1865
1865
  },
1866
1866
  "attack-surface-pentest": {
1867
1867
  "path": "skills/attack-surface-pentest/skill.md",
1868
- "total_bytes": 33291,
1868
+ "total_bytes": 33158,
1869
1869
  "total_lines": 388,
1870
1870
  "frontmatter": {
1871
1871
  "line_start": 1,
@@ -1888,25 +1888,25 @@
1888
1888
  "normalized_name": "framework-lag-declaration",
1889
1889
  "line": 106,
1890
1890
  "byte_start": 6410,
1891
- "byte_end": 10973,
1892
- "bytes": 4563,
1891
+ "byte_end": 10953,
1892
+ "bytes": 4543,
1893
1893
  "h3_count": 0
1894
1894
  },
1895
1895
  {
1896
1896
  "name": "TTP Mapping (MITRE ATLAS v5.6.0 + MITRE ATT&CK v19.0)",
1897
1897
  "normalized_name": "ttp-mapping",
1898
1898
  "line": 124,
1899
- "byte_start": 10973,
1900
- "byte_end": 13208,
1901
- "bytes": 2235,
1899
+ "byte_start": 10953,
1900
+ "byte_end": 13162,
1901
+ "bytes": 2209,
1902
1902
  "h3_count": 0
1903
1903
  },
1904
1904
  {
1905
1905
  "name": "Exploit Availability Matrix",
1906
1906
  "normalized_name": "exploit-availability-matrix",
1907
1907
  "line": 143,
1908
- "byte_start": 13208,
1909
- "byte_end": 15663,
1908
+ "byte_start": 13162,
1909
+ "byte_end": 15617,
1910
1910
  "bytes": 2455,
1911
1911
  "h3_count": 0
1912
1912
  },
@@ -1914,17 +1914,17 @@
1914
1914
  "name": "Analysis Procedure",
1915
1915
  "normalized_name": "analysis-procedure",
1916
1916
  "line": 160,
1917
- "byte_start": 15663,
1918
- "byte_end": 25366,
1919
- "bytes": 9703,
1917
+ "byte_start": 15617,
1918
+ "byte_end": 25273,
1919
+ "bytes": 9656,
1920
1920
  "h3_count": 4
1921
1921
  },
1922
1922
  {
1923
1923
  "name": "Output Format",
1924
1924
  "normalized_name": "output-format",
1925
1925
  "line": 281,
1926
- "byte_start": 25366,
1927
- "byte_end": 29363,
1926
+ "byte_start": 25273,
1927
+ "byte_end": 29270,
1928
1928
  "bytes": 3997,
1929
1929
  "h3_count": 9
1930
1930
  },
@@ -1932,8 +1932,8 @@
1932
1932
  "name": "Compliance Theater Check",
1933
1933
  "normalized_name": "compliance-theater-check",
1934
1934
  "line": 363,
1935
- "byte_start": 29363,
1936
- "byte_end": 31679,
1935
+ "byte_start": 29270,
1936
+ "byte_end": 31586,
1937
1937
  "bytes": 2316,
1938
1938
  "h3_count": 0
1939
1939
  },
@@ -1941,9 +1941,9 @@
1941
1941
  "name": "Defensive Countermeasure Mapping (D3FEND)",
1942
1942
  "normalized_name": "defensive-countermeasure-mapping",
1943
1943
  "line": 377,
1944
- "byte_start": 31679,
1945
- "byte_end": 33291,
1946
- "bytes": 1612,
1944
+ "byte_start": 31586,
1945
+ "byte_end": 33158,
1946
+ "bytes": 1572,
1947
1947
  "h3_count": 0
1948
1948
  }
1949
1949
  ]
@@ -2984,7 +2984,7 @@
2984
2984
  },
2985
2985
  "sector-financial": {
2986
2986
  "path": "skills/sector-financial/skill.md",
2987
- "total_bytes": 50325,
2987
+ "total_bytes": 50285,
2988
2988
  "total_lines": 401,
2989
2989
  "frontmatter": {
2990
2990
  "line_start": 1,
@@ -3034,16 +3034,16 @@
3034
3034
  "normalized_name": "analysis-procedure",
3035
3035
  "line": 182,
3036
3036
  "byte_start": 26352,
3037
- "byte_end": 35108,
3038
- "bytes": 8756,
3037
+ "byte_end": 35068,
3038
+ "bytes": 8716,
3039
3039
  "h3_count": 10
3040
3040
  },
3041
3041
  {
3042
3042
  "name": "Output Format",
3043
3043
  "normalized_name": "output-format",
3044
3044
  "line": 271,
3045
- "byte_start": 35108,
3046
- "byte_end": 38515,
3045
+ "byte_start": 35068,
3046
+ "byte_end": 38475,
3047
3047
  "bytes": 3407,
3048
3048
  "h3_count": 15
3049
3049
  },
@@ -3051,8 +3051,8 @@
3051
3051
  "name": "Compliance Theater Check",
3052
3052
  "normalized_name": "compliance-theater-check",
3053
3053
  "line": 335,
3054
- "byte_start": 38515,
3055
- "byte_end": 42853,
3054
+ "byte_start": 38475,
3055
+ "byte_end": 42813,
3056
3056
  "bytes": 4338,
3057
3057
  "h3_count": 0
3058
3058
  },
@@ -3060,8 +3060,8 @@
3060
3060
  "name": "Defensive Countermeasure Mapping",
3061
3061
  "normalized_name": "defensive-countermeasure-mapping",
3062
3062
  "line": 370,
3063
- "byte_start": 42853,
3064
- "byte_end": 46572,
3063
+ "byte_start": 42813,
3064
+ "byte_end": 46532,
3065
3065
  "bytes": 3719,
3066
3066
  "h3_count": 0
3067
3067
  },
@@ -3069,8 +3069,8 @@
3069
3069
  "name": "Hand-Off / Related Skills",
3070
3070
  "normalized_name": "hand-off",
3071
3071
  "line": 385,
3072
- "byte_start": 46572,
3073
- "byte_end": 50325,
3072
+ "byte_start": 46532,
3073
+ "byte_end": 50285,
3074
3074
  "bytes": 3753,
3075
3075
  "h3_count": 0
3076
3076
  }
@@ -4112,7 +4112,7 @@
4112
4112
  },
4113
4113
  "cloud-iam-incident": {
4114
4114
  "path": "skills/cloud-iam-incident/skill.md",
4115
- "total_bytes": 44474,
4115
+ "total_bytes": 44433,
4116
4116
  "total_lines": 416,
4117
4117
  "frontmatter": {
4118
4118
  "line_start": 1,
@@ -4162,16 +4162,16 @@
4162
4162
  "normalized_name": "analysis-procedure",
4163
4163
  "line": 185,
4164
4164
  "byte_start": 22803,
4165
- "byte_end": 30428,
4166
- "bytes": 7625,
4165
+ "byte_end": 30423,
4166
+ "bytes": 7620,
4167
4167
  "h3_count": 12
4168
4168
  },
4169
4169
  {
4170
4170
  "name": "Output Format",
4171
4171
  "normalized_name": "output-format",
4172
4172
  "line": 275,
4173
- "byte_start": 30428,
4174
- "byte_end": 32626,
4173
+ "byte_start": 30423,
4174
+ "byte_end": 32621,
4175
4175
  "bytes": 2198,
4176
4176
  "h3_count": 15
4177
4177
  },
@@ -4179,8 +4179,8 @@
4179
4179
  "name": "Compliance Theater Check",
4180
4180
  "normalized_name": "compliance-theater-check",
4181
4181
  "line": 338,
4182
- "byte_start": 32626,
4183
- "byte_end": 37225,
4182
+ "byte_start": 32621,
4183
+ "byte_end": 37220,
4184
4184
  "bytes": 4599,
4185
4185
  "h3_count": 0
4186
4186
  },
@@ -4188,17 +4188,17 @@
4188
4188
  "name": "Defensive Countermeasure Mapping",
4189
4189
  "normalized_name": "defensive-countermeasure-mapping",
4190
4190
  "line": 374,
4191
- "byte_start": 37225,
4192
- "byte_end": 41301,
4193
- "bytes": 4076,
4191
+ "byte_start": 37220,
4192
+ "byte_end": 41260,
4193
+ "bytes": 4040,
4194
4194
  "h3_count": 0
4195
4195
  },
4196
4196
  {
4197
4197
  "name": "Hand-Off / Related Skills",
4198
4198
  "normalized_name": "hand-off",
4199
4199
  "line": 396,
4200
- "byte_start": 41301,
4201
- "byte_end": 44474,
4200
+ "byte_start": 41260,
4201
+ "byte_end": 44433,
4202
4202
  "bytes": 3173,
4203
4203
  "h3_count": 0
4204
4204
  }
@@ -3,8 +3,8 @@
3
3
  "schema_version": "1.0.0",
4
4
  "tokenizer_note": "Character-density approximation: 1 token ≈ 4 chars. This is the canonical rule-of-thumb for OpenAI tokenizers on English+technical text. Claude's tokenizer is typically more efficient on prose; treat this as an upper-bound budget for both. Consumers with stricter precision needs should re-tokenize with their own tokenizer.",
5
5
  "approx_chars_per_token": 4,
6
- "total_chars": 1673904,
7
- "total_approx_tokens": 418479,
6
+ "total_chars": 1673692,
7
+ "total_approx_tokens": 418426,
8
8
  "skill_count": 42
9
9
  },
10
10
  "skills": {
@@ -1080,10 +1080,10 @@
1080
1080
  },
1081
1081
  "attack-surface-pentest": {
1082
1082
  "path": "skills/attack-surface-pentest/skill.md",
1083
- "bytes": 33291,
1084
- "chars": 33148,
1083
+ "bytes": 33158,
1084
+ "chars": 33017,
1085
1085
  "lines": 388,
1086
- "approx_tokens": 8287,
1086
+ "approx_tokens": 8254,
1087
1087
  "approx_chars_per_token": 4,
1088
1088
  "sections": {
1089
1089
  "threat-context": {
@@ -1092,14 +1092,14 @@
1092
1092
  "approx_tokens": 1173
1093
1093
  },
1094
1094
  "framework-lag-declaration": {
1095
- "bytes": 4563,
1096
- "chars": 4553,
1097
- "approx_tokens": 1138
1095
+ "bytes": 4543,
1096
+ "chars": 4533,
1097
+ "approx_tokens": 1133
1098
1098
  },
1099
1099
  "ttp-mapping": {
1100
- "bytes": 2235,
1101
- "chars": 2214,
1102
- "approx_tokens": 554
1100
+ "bytes": 2209,
1101
+ "chars": 2188,
1102
+ "approx_tokens": 547
1103
1103
  },
1104
1104
  "exploit-availability-matrix": {
1105
1105
  "bytes": 2455,
@@ -1107,9 +1107,9 @@
1107
1107
  "approx_tokens": 610
1108
1108
  },
1109
1109
  "analysis-procedure": {
1110
- "bytes": 9703,
1111
- "chars": 9651,
1112
- "approx_tokens": 2413
1110
+ "bytes": 9656,
1111
+ "chars": 9606,
1112
+ "approx_tokens": 2402
1113
1113
  },
1114
1114
  "output-format": {
1115
1115
  "bytes": 3997,
@@ -1122,9 +1122,9 @@
1122
1122
  "approx_tokens": 576
1123
1123
  },
1124
1124
  "defensive-countermeasure-mapping": {
1125
- "bytes": 1612,
1126
- "chars": 1610,
1127
- "approx_tokens": 403
1125
+ "bytes": 1572,
1126
+ "chars": 1570,
1127
+ "approx_tokens": 393
1128
1128
  }
1129
1129
  }
1130
1130
  },
@@ -1735,10 +1735,10 @@
1735
1735
  },
1736
1736
  "sector-financial": {
1737
1737
  "path": "skills/sector-financial/skill.md",
1738
- "bytes": 50325,
1739
- "chars": 50160,
1738
+ "bytes": 50285,
1739
+ "chars": 50120,
1740
1740
  "lines": 401,
1741
- "approx_tokens": 12540,
1741
+ "approx_tokens": 12530,
1742
1742
  "approx_chars_per_token": 4,
1743
1743
  "sections": {
1744
1744
  "threat-context": {
@@ -1762,9 +1762,9 @@
1762
1762
  "approx_tokens": 724
1763
1763
  },
1764
1764
  "analysis-procedure": {
1765
- "bytes": 8756,
1766
- "chars": 8726,
1767
- "approx_tokens": 2182
1765
+ "bytes": 8716,
1766
+ "chars": 8686,
1767
+ "approx_tokens": 2172
1768
1768
  },
1769
1769
  "output-format": {
1770
1770
  "bytes": 3407,
@@ -2395,10 +2395,10 @@
2395
2395
  },
2396
2396
  "cloud-iam-incident": {
2397
2397
  "path": "skills/cloud-iam-incident/skill.md",
2398
- "bytes": 44474,
2399
- "chars": 44316,
2398
+ "bytes": 44433,
2399
+ "chars": 44275,
2400
2400
  "lines": 416,
2401
- "approx_tokens": 11079,
2401
+ "approx_tokens": 11069,
2402
2402
  "approx_chars_per_token": 4,
2403
2403
  "sections": {
2404
2404
  "threat-context": {
@@ -2422,9 +2422,9 @@
2422
2422
  "approx_tokens": 842
2423
2423
  },
2424
2424
  "analysis-procedure": {
2425
- "bytes": 7625,
2426
- "chars": 7601,
2427
- "approx_tokens": 1900
2425
+ "bytes": 7620,
2426
+ "chars": 7596,
2427
+ "approx_tokens": 1899
2428
2428
  },
2429
2429
  "output-format": {
2430
2430
  "bytes": 2198,
@@ -2437,9 +2437,9 @@
2437
2437
  "approx_tokens": 1146
2438
2438
  },
2439
2439
  "defensive-countermeasure-mapping": {
2440
- "bytes": 4076,
2441
- "chars": 4068,
2442
- "approx_tokens": 1017
2440
+ "bytes": 4040,
2441
+ "chars": 4032,
2442
+ "approx_tokens": 1008
2443
2443
  },
2444
2444
  "hand-off": {
2445
2445
  "bytes": 3173,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_meta": {
3
3
  "schema_version": "1.0.0",
4
- "last_updated": "2026-05-18",
4
+ "last_updated": "2026-05-30",
5
5
  "source": "NVD + CISA KEV + vendor advisories — see sources/index.json",
6
6
  "required_fields": [
7
7
  "type",
@@ -90,7 +90,7 @@
90
90
  ],
91
91
  "note": "Catalog keys are CVE-* by default. For pre-CVE-assignment advisories under active operational impact, the project accepts OSV-native identifier shapes as the canonical key, with cross-references retained in `aliases`: MAL-* (OSSF Malicious Packages dataset — published into OSV.dev; primary key for malicious-package compromises), GHSA-* (GitHub Advisory Database; primary key when the package is on GitHub and no CVE has issued yet), and SNYK-* (Snyk advisory dataset; primary key for advisories Snyk catalogued before OSV/GHSA ingested them). When MITRE issues a CVE, the entry is renamed in lockstep with the matching zeroday-lessons key; the previous identifier is retained in `aliases` so historical references continue to resolve. Precedent: MAL-2026-3083 added 2026-05-13 (the elementary-data PyPI worm, 1.1M monthly downloads, OSV/OSSF-cataloged before any CVE issued). EPSS coverage does not extend to non-CVE identifiers; epss_score is null with a documenting epss_note on such entries. Upstream pull from OSV.dev: `exceptd refresh --source osv` (added v0.12.10)."
92
92
  },
93
- "last_threat_review": "2026-05-15"
93
+ "last_threat_review": "2026-05-30"
94
94
  },
95
95
  "CVE-2025-0282": {
96
96
  "ai_assisted_weaponization": false,
@@ -10,8 +10,10 @@
10
10
  * distance ≤ 2 AND ≤ floor(flag.length / 2).
11
11
  *
12
12
  * Per-verb allowlists are the canonical CLI surface. Adding a new flag to a
13
- * verb means appending to the allowlist here AND updating the printPlaybookVerbHelp
14
- * block; a test asserts the two sets agree.
13
+ * verb means appending to the allowlist here AND updating the
14
+ * printPlaybookVerbHelp block; keep the two in sync. doctor maintains its own
15
+ * KNOWN_DOCTOR_FLAGS set in bin/exceptd.js — keep VERB_FLAG_ALLOWLIST.doctor
16
+ * aligned with it (tests/lib-flag-suggest.test.js pins the shared flags).
15
17
  */
16
18
 
17
19
  function editDistance(a, b) {
@@ -66,7 +68,7 @@ function suggestFlag(flag, allowlist) {
66
68
  * (e.g. `pretty`, `json`, `help`) live under '_global'.
67
69
  */
68
70
  const VERB_FLAG_ALLOWLIST = Object.freeze({
69
- _global: ['help', 'pretty', 'json', 'verbose'],
71
+ _global: ['help', 'pretty', 'json', 'verbose', 'quiet'],
70
72
  run: [
71
73
  'evidence', 'evidence-dir', 'session-id', 'force-overwrite', 'attestation-root',
72
74
  'mode', 'air-gap', 'force-stale', 'operator', 'ack', 'csaf-status',
@@ -96,12 +98,6 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
96
98
  'mode', 'force-stale', 'tlp',
97
99
  'bundle-deterministic', 'bundle-epoch',
98
100
  ],
99
- ingest: [
100
- 'evidence', 'session-id', 'force-overwrite', 'attestation-root', 'operator',
101
- 'ack', 'csaf-status', 'publisher-namespace', 'air-gap', 'force-stale',
102
- 'strict-preconditions',
103
- 'bundle-deterministic', 'bundle-epoch',
104
- ],
105
101
  brief: ['all', 'scope', 'directives', 'flat', 'phase'],
106
102
  discover: ['scan-only', 'scope', 'cwd'],
107
103
  ask: [],
@@ -112,7 +108,7 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
112
108
  reattest: [
113
109
  'playbook', 'since', 'latest', 'force-replay', 'attestation-root',
114
110
  ],
115
- doctor: ['signatures', 'cves', 'rfcs', 'fix', 'registry-check', 'exit-codes', 'shipped-tarball', 'ai-config', 'currency', 'collectors'],
111
+ doctor: ['signatures', 'cves', 'rfcs', 'fix', 'registry-check', 'exit-codes', 'shipped-tarball', 'ai-config', 'currency', 'collectors', 'air-gap'],
116
112
  lint: ['evidence'],
117
113
  collect: ['cwd', 'attest-ownership', 'resolve'],
118
114
  refresh: [
@@ -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
  };
@@ -206,7 +206,8 @@ Sources (default = all):
206
206
  osv (v0.12.10) OSV.dev aggregator — OSSF Malicious Packages (MAL-*) + Snyk
207
207
  + GHSA + RustSec + Mageia + Go Vuln DB + Ubuntu USN. Unauthenticated.
208
208
  Use --advisory MAL-* / RUSTSEC-* / SNYK-* / USN-* to seed a single
209
- draft. Bulk import via package watchlist is a v0.13 follow-up.
209
+ draft. One advisory ID per invocation; there is no bulk or
210
+ package-watchlist import.
210
211
 
211
212
  Air-gap workflow:
212
213
  1. On a connected host: \`exceptd refresh --prefetch\`
@@ -595,7 +596,8 @@ const GHSA_SOURCE = {
595
596
  if (ctx.fixtures?.ghsa) return synthesizeFromFixture(ctx, "ghsa");
596
597
  if (ctx.cacheDir) {
597
598
  // Cache parity: ghsa cache layout is .cache/upstream/ghsa/<published-date>.json
598
- // For v0.12.0 we fall through to live fetch caching is a v0.13 follow-up.
599
+ // GHSA has no cache layer yet — fall through to live fetch. (Unlike NVD
600
+ // and RFC, GHSA does not honor the air-gap --from-cache path.)
599
601
  }
600
602
  const ghsa = require("./source-ghsa");
601
603
  return ghsa.buildDiff(ctx);
package/lib/source-osv.js CHANGED
@@ -372,7 +372,9 @@ async function fetchAdvisoryById(id, opts = {}) {
372
372
 
373
373
  /**
374
374
  * List advisories for a package, optionally filtered to a specific version.
375
- * v0.12.10 ships the network path; bulk-import callers are a v0.13 follow-up.
375
+ * The single-package network path is implemented; there is no bulk /
376
+ * package-watchlist import caller — advisories are seeded one at a time via
377
+ * `refresh --advisory <id>`.
376
378
  */
377
379
  async function fetchAdvisoriesForPackage(name, ecosystem, version, opts = {}) {
378
380
  if (!name || !ecosystem) {
@@ -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 {