@blamejs/exceptd-skills 0.19.32 → 0.19.34

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 (127) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +8 -8
  4. package/data/_indexes/activity-feed.json +2 -2
  5. package/data/_indexes/catalog-summaries.json +7 -7
  6. package/data/_indexes/chains.json +60118 -0
  7. package/data/attack-techniques.json +267 -7
  8. package/data/cve-catalog.json +9991 -3
  9. package/data/cwe-catalog.json +109 -2
  10. package/data/framework-control-gaps.json +578 -3
  11. package/data/zeroday-lessons.json +8330 -1
  12. package/lib/auto-discovery.js +56 -286
  13. package/lib/canonical-eq.js +7 -40
  14. package/lib/citation-resolve.js +22 -70
  15. package/lib/collectors/ai-api.js +20 -54
  16. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  17. package/lib/collectors/citation-hygiene.js +72 -210
  18. package/lib/collectors/containers.js +41 -130
  19. package/lib/collectors/cred-stores.js +31 -115
  20. package/lib/collectors/crypto-codebase.js +55 -138
  21. package/lib/collectors/crypto.js +24 -54
  22. package/lib/collectors/hardening.js +20 -78
  23. package/lib/collectors/kernel.js +16 -46
  24. package/lib/collectors/library-author.js +57 -206
  25. package/lib/collectors/mcp.js +24 -70
  26. package/lib/collectors/runtime.js +24 -86
  27. package/lib/collectors/sbom.js +34 -106
  28. package/lib/collectors/scan-excludes.js +31 -138
  29. package/lib/collectors/secrets.js +62 -178
  30. package/lib/cross-ref-api.js +39 -123
  31. package/lib/currency-severity.js +8 -27
  32. package/lib/cve-batch.js +13 -21
  33. package/lib/cve-cli.js +13 -20
  34. package/lib/cve-curation.js +72 -239
  35. package/lib/cve-regression-watcher.js +29 -152
  36. package/lib/cvss.js +13 -54
  37. package/lib/doctor-bucketing.js +3 -19
  38. package/lib/exit-codes.js +10 -42
  39. package/lib/flag-suggest.js +7 -25
  40. package/lib/framework-gap.js +35 -114
  41. package/lib/gap-detectors.js +37 -159
  42. package/lib/id-validation.js +9 -30
  43. package/lib/job-queue.js +13 -36
  44. package/lib/lint-skills.js +64 -232
  45. package/lib/playbook-runner.js +693 -2095
  46. package/lib/prefetch.js +100 -376
  47. package/lib/refresh-external.js +199 -627
  48. package/lib/refresh-network.js +75 -307
  49. package/lib/rfc-cli.js +23 -68
  50. package/lib/scoring.js +77 -145
  51. package/lib/sign.js +43 -229
  52. package/lib/source-advisories.js +43 -194
  53. package/lib/source-ghsa.js +37 -120
  54. package/lib/source-osv.js +94 -266
  55. package/lib/ttp-mapper.js +14 -24
  56. package/lib/upstream-check-cli.js +10 -28
  57. package/lib/upstream-check.js +19 -44
  58. package/lib/validate-catalog-meta.js +17 -61
  59. package/lib/validate-cve-catalog.js +43 -119
  60. package/lib/validate-indexes.js +25 -76
  61. package/lib/validate-package.js +16 -62
  62. package/lib/validate-playbooks.js +69 -275
  63. package/lib/validate-vendor.js +16 -49
  64. package/lib/verify.js +56 -286
  65. package/lib/version-pins.js +5 -34
  66. package/lib/worker-pool.js +11 -30
  67. package/lib/xml-tokenizer.js +47 -152
  68. package/manifest.json +53 -53
  69. package/orchestrator/dispatcher.js +17 -68
  70. package/orchestrator/event-bus.js +11 -74
  71. package/orchestrator/index.js +138 -412
  72. package/orchestrator/pipeline.js +28 -85
  73. package/orchestrator/scanner.js +34 -138
  74. package/orchestrator/scheduler.js +20 -84
  75. package/package.json +2 -2
  76. package/sbom.cdx.json +253 -253
  77. package/scripts/audit-catalog-gaps.js +9 -62
  78. package/scripts/audit-cross-skill.js +5 -31
  79. package/scripts/audit-perf.js +6 -16
  80. package/scripts/backfill-theater-test.js +7 -64
  81. package/scripts/bootstrap.js +12 -44
  82. package/scripts/build-indexes.js +40 -154
  83. package/scripts/builders/activity-feed.js +4 -14
  84. package/scripts/builders/catalog-summaries.js +3 -10
  85. package/scripts/builders/currency.js +7 -20
  86. package/scripts/builders/cwe-chains.js +7 -30
  87. package/scripts/builders/did-ladders.js +6 -13
  88. package/scripts/builders/frequency.js +5 -19
  89. package/scripts/builders/jurisdiction-clocks.js +6 -25
  90. package/scripts/builders/recipes.js +6 -14
  91. package/scripts/builders/section-offsets.js +13 -51
  92. package/scripts/builders/stale-content.js +7 -28
  93. package/scripts/builders/summary-cards.js +8 -29
  94. package/scripts/builders/theater-fingerprints.js +12 -27
  95. package/scripts/builders/token-budget.js +4 -31
  96. package/scripts/check-agents-md-collectors.js +11 -54
  97. package/scripts/check-catalog-gap-budget.js +15 -32
  98. package/scripts/check-changelog-extract.js +18 -48
  99. package/scripts/check-codebase-patterns-currency.js +6 -22
  100. package/scripts/check-codebase-patterns.js +50 -143
  101. package/scripts/check-epss-consistency.js +9 -64
  102. package/scripts/check-framework-gap-coverage.js +13 -31
  103. package/scripts/check-manifest-snapshot.js +13 -73
  104. package/scripts/check-sbom-currency.js +44 -142
  105. package/scripts/check-test-count.js +15 -52
  106. package/scripts/check-test-coverage.js +66 -197
  107. package/scripts/check-test-subjects.js +21 -62
  108. package/scripts/check-ttp-references.js +14 -38
  109. package/scripts/check-ttp-upstream.js +8 -40
  110. package/scripts/check-version-bump.js +9 -61
  111. package/scripts/check-version-tags.js +20 -121
  112. package/scripts/predeploy.js +38 -184
  113. package/scripts/refresh-manifest-snapshot.js +16 -38
  114. package/scripts/refresh-mitre-atlas.js +3 -8
  115. package/scripts/refresh-mitre-attack.js +1 -8
  116. package/scripts/refresh-mitre-d3fend.js +3 -9
  117. package/scripts/refresh-mitre-ics-attack.js +3 -8
  118. package/scripts/refresh-reverse-refs.js +27 -94
  119. package/scripts/refresh-rfc-index.js +2 -10
  120. package/scripts/refresh-sbom.js +31 -161
  121. package/scripts/refresh-upstream-catalogs.js +40 -137
  122. package/scripts/release.js +69 -232
  123. package/scripts/run-e2e-scenarios.js +24 -71
  124. package/scripts/sync-manifest-metadata.js +10 -34
  125. package/scripts/sync-package-description.js +8 -17
  126. package/scripts/validate-vendor-online.js +13 -44
  127. package/scripts/verify-shipped-tarball.js +35 -140
@@ -2,40 +2,10 @@
2
2
  "use strict";
3
3
 
4
4
  /**
5
- * EPSS score/percentile consistency gate.
5
+ * EPSS score/percentile consistency gate. Exit 0 consistent, 1 inconsistent.
6
6
  *
7
- * EPSS publishes a score and a percentile for every CVE, refreshed daily. The
8
- * percentile is the score's RANK within that day's publication: if A scores
9
- * higher than B on a given day, A's percentile is at least B's. That makes the
10
- * pair self-checking with no network access — sort a day's entries by score and
11
- * the percentiles must come out sorted too.
12
- *
13
- * The failure this catches is updating one field without the other. A refresh
14
- * that writes a new score but keeps yesterday's percentile leaves a plausible
15
- * entry: both numbers are in range, both look like EPSS values, and nothing
16
- * downstream can tell they describe different days. The result is a CVE ranked
17
- * as top-decile urgency on a score that no longer supports it, which is exactly
18
- * the input the prioritisation model trusts most.
19
- *
20
- * What the ordering check does and does not establish. Monotonicity is a
21
- * necessary condition, not a sufficient one: it catches a stale percentile that
22
- * contradicts the new score's rank, which is what a partial refresh usually
23
- * produces, but a stale value that happens to preserve the ordering passes.
24
- * Proving two fields share a publication would require the fetched row, and
25
- * that is not recoverable from the catalog offline. So this is a corruption
26
- * detector, not a provenance proof — the provenance guarantee has to come from
27
- * the write side, by fetching and writing score, percentile and date together.
28
- *
29
- * Three checks run:
30
- * - within each epss_date, sorting by score must sort by percentile;
31
- * - an entry carries both numeric fields or neither;
32
- * - epss_note, which is derived text, must restate the current fields.
33
- *
34
- * Entries are grouped by epss_date so each publication is checked against
35
- * itself; comparing across days is meaningless because the whole distribution
36
- * shifts.
37
- *
38
- * Exit codes: 0 consistent, 1 inconsistent.
7
+ * The percentile is the score's rank within one publication, so a cohort must
8
+ * sort the same way by both. Monotonicity is necessary, not sufficient.
39
9
  */
40
10
 
41
11
  const fs = require("node:fs");
@@ -44,13 +14,8 @@ const path = require("node:path");
44
14
  const ROOT = path.resolve(__dirname, "..");
45
15
 
46
16
  /**
47
- * Percentiles are published rounded to five decimals, so two scores that are
48
- * adjacent in the ranking can round to percentiles that invert by a hair. That
49
- * is arithmetic, not drift. Real mismatches are pairs pulled from different
50
- * days, which land orders of magnitude above this: the widest rounding artifact
51
- * observed in the catalog is 8e-5, while a stale-field mismatch runs 0.05-0.9.
52
- * The threshold sits between the two, far enough from each that neither
53
- * classification is a close call.
17
+ * Percentiles publish rounded to five decimals, so adjacent scores can invert by
18
+ * a hair. Rounding artifacts reach 8e-5; a stale-field mismatch runs 0.05-0.9.
54
19
  */
55
20
  const ROUNDING_TOLERANCE = 1e-3;
56
21
 
@@ -58,24 +23,10 @@ function loadCatalog(file) {
58
23
  return JSON.parse(fs.readFileSync(file, "utf8"));
59
24
  }
60
25
 
61
- /**
62
- * The operator-readable restatement of an entry's EPSS fields. Entries carry it
63
- * as `epss_note`, and it is derived, not authored — so it must be rebuilt
64
- * whenever the numbers move.
65
- *
66
- * The renderer lives in lib/cve-enrich.js because the refresh writer rebuilds
67
- * the note from the same definition. A second copy here would let the gate and
68
- * the writer disagree about the exact wording, which shows up as this gate
69
- * failing on data that was refreshed correctly.
70
- */
26
+ /** Shared with the refresh writer so gate and writer cannot disagree on wording. */
71
27
  const { renderEpssNote: renderNote } = require("../lib/cve-enrich.js");
72
28
 
73
- /**
74
- * Group entries by publication date. Entries missing either field are reported
75
- * separately rather than skipped — a half-populated pair is its own defect, and
76
- * silently ignoring it would let the gate pass on the very shape it exists to
77
- * catch.
78
- */
29
+ /** Group entries by publication date; a half-populated pair is reported, not skipped. */
79
30
  function cohorts(catalog) {
80
31
  const groups = new Map();
81
32
  const incomplete = [];
@@ -101,10 +52,7 @@ function cohorts(catalog) {
101
52
  return { groups, incomplete };
102
53
  }
103
54
 
104
- /**
105
- * Sort a cohort by score and report every place the percentile goes backwards
106
- * by more than rounding can explain.
107
- */
55
+ /** Sort by score; report every percentile step backwards that rounding cannot explain. */
108
56
  function inversions(rows) {
109
57
  const sorted = [...rows].sort((a, b) => a.score - b.score);
110
58
  const found = [];
@@ -141,10 +89,7 @@ function check(catalogFile) {
141
89
  failures.push(`${row.id}: has one EPSS field but not ${row.missing} — write both together or neither`);
142
90
  }
143
91
 
144
- // The prose restatement drifts the same way the percentile does: a refresh
145
- // moves the numbers and leaves the sentence describing the previous
146
- // publication, so the entry states two different scores as of two different
147
- // dates. It is derived text, so it can be checked exactly.
92
+ // epss_note is derived text, so it can be checked exactly against the fields.
148
93
  for (const [id, entry] of Object.entries(catalog)) {
149
94
  if (typeof entry.epss_note !== "string") continue;
150
95
  if (typeof entry.epss_score !== "number" || typeof entry.epss_percentile !== "number") continue;
@@ -1,31 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /*
3
- * scripts/check-framework-gap-coverage.js
3
+ * Enforces global-first coverage (AGENTS.md Hard Rule #5): every curated CVE in
4
+ * data/cve-catalog.json must declare a framework_control_gaps statement for all
5
+ * five jurisdiction buckets, so a multi-jurisdiction operator reading the
6
+ * offline catalog gets more than a US-centric subset.
4
7
  *
5
- * AGENTS.md Hard Rule #5 (global-first) enforcement gate. Every curated CVE
6
- * in data/cve-catalog.json must declare a framework_control_gaps statement
7
- * for all five jurisdiction buckets:
8
+ * Draft entries (_auto_imported) are exempt — they carry raw NVD data, and the
9
+ * curation bar requires promotion before shipping, at which point this applies.
8
10
  *
9
- * NIST — NIST-800-53-* / NIST-800-63*
10
- * EU — NIS2-* / DORA-* / EU-AI-* / GDPR-*
11
- * UK — UK-CAF-*
12
- * AU — AU-Essential-8-* / AU-ISM-*
13
- * ISO — ISO-27001-2022-*
14
- *
15
- * A multi-jurisdiction operator reading the offline catalog's framework-gap
16
- * output must get coverage for every required jurisdiction on every CVE, not
17
- * a US-centric subset. Before this gate, two-thirds of the corpus mapped only
18
- * a subset; a codex review surfaced it, the corpus was completed, and this
19
- * gate keeps it from regressing when new CVEs are curated.
20
- *
21
- * Draft entries (_auto_imported === true) are exempt — they carry raw NVD
22
- * data that has not been through curation yet. The curation bar (elsewhere)
23
- * requires drafts to be promoted before shipping, at which point this gate
24
- * applies.
25
- *
26
- * Exit code: 0 when every curated entry is complete, 1 when any entry omits a
27
- * required bucket (the list is printed). Uses process.exitCode (not
28
- * process.exit) so buffered stdout drains before the process ends.
11
+ * Exits 0 when every curated entry is complete, 1 when any omits a bucket.
12
+ * `process.exitCode`, never `process.exit()`: the failure list is a buffered
13
+ * stdout write that the exit can truncate.
29
14
  */
30
15
 
31
16
  'use strict';
@@ -34,19 +19,16 @@ const fs = require('node:fs');
34
19
  const path = require('node:path');
35
20
 
36
21
  const ROOT = path.resolve(__dirname, '..');
37
- // Catalog path is overridable (argv[2]) so the gate's own test can point it at
38
- // a fixture; defaults to the shipped catalog for the real predeploy run.
22
+ // argv[2] overrides the catalog so the gate's own test can point at a fixture.
39
23
  const CATALOG = process.argv[2] || path.join(ROOT, 'data', 'cve-catalog.json');
40
24
 
41
25
  const REQUIRED = ['NIST', 'EU', 'UK', 'AU', 'ISO'];
42
26
 
43
27
  function bucketOf(key) {
44
28
  if (/^NIST/i.test(key)) return 'NIST';
45
- // EU bucket is the security-regulation family AGENTS.md Hard Rule #5 names:
46
- // NIS2, DORA, the EU AI Act, and the EU Cyber Resilience Act. GDPR / EU-GDPR
47
- // (data-protection) is NOT sufficient on its own — an entry mapped only to
48
- // GDPR still owes NIS2/DORA/EU-AI coverage, so GDPR does not satisfy this
49
- // bucket. Note EU-AI / EU-CRA match here; EU-GDPR deliberately does not.
29
+ // The EU bucket is the security-regulation family Hard Rule #5 names. GDPR is
30
+ // data protection and does NOT satisfy it: an entry mapped only to GDPR still
31
+ // owes NIS2/DORA/EU-AI coverage, so EU-GDPR must not match here.
50
32
  if (/^(NIS2|DORA|EU-AI|EU-CRA)/i.test(key)) return 'EU';
51
33
  if (/^UK-CAF/i.test(key)) return 'UK';
52
34
  if (/^(AU-Essential-?8|AU-ISM|Essential-?8)/i.test(key)) return 'AU';
@@ -1,33 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * scripts/check-manifest-snapshot.js
4
- *
5
- * CI gate. Captures the current public skill surface (skill name +
6
- * version + triggers + data_deps + atlas_refs + attack_refs +
7
- * framework_gaps) from manifest.json and compares it to the committed
8
- * manifest-snapshot.json baseline.
9
- *
10
- * The skill surface is the public contract this repo offers downstream
11
- * AI assistants: skill names that downstream prompts may reference,
12
- * trigger keywords that downstream skill-matchers index on, and the
13
- * data files that skills rely on. Removing a skill or trigger keyword
14
- * silently breaks every consumer that pinned that surface.
15
- *
16
- * Exit codes:
17
- * 0 — no breaking changes (additive changes printed but not failing)
18
- * 1 — breaking changes detected
19
- * 2 — script-level error (missing baseline, IO failure, etc.)
20
- *
21
- * Operators see this gate in SECURITY.md / CONTRIBUTING.md as a CI
22
- * promise: "removed skills, removed triggers, or removed data deps fail
23
- * the build before they reach main."
24
- *
25
- * Usage:
26
- * node scripts/check-manifest-snapshot.js
27
- *
28
- * Regenerate the baseline after an intentional removal:
29
- * node scripts/refresh-manifest-snapshot.js
30
- * git add manifest-snapshot.json && git commit
3
+ * CI gate: diffs the public skill surface in manifest.json — name, version,
4
+ * triggers, data_deps, ref arrays — against the committed manifest-snapshot.json.
5
+ * Exit 0 when nothing broke (additive changes print and still pass), 1 on a
6
+ * breaking change, 2 on a script-level error such as a missing baseline.
31
7
  */
32
8
 
33
9
  const fs = require("fs");
@@ -39,9 +15,7 @@ const MANIFEST_PATH = path.join(ROOT, "manifest.json");
39
15
  const SNAPSHOT_PATH = path.join(ROOT, "manifest-snapshot.json");
40
16
 
41
17
  function captureSurface(manifest) {
42
- // Public surface = the set of facts downstream consumers may have
43
- // pinned against. NOT included: sha256 / signature / signed_at —
44
- // those change every commit and are not a public contract.
18
+ // Only what a consumer pins against: sha256, signature and signed_at change every commit.
45
19
  const skills = (manifest.skills || []).map(s => ({
46
20
  name: s.name,
47
21
  version: s.version || null,
@@ -70,30 +44,24 @@ function diff(baseline, current) {
70
44
  const bSkills = new Map(baseline.skills.map(s => [s.name, s]));
71
45
  const cSkills = new Map(current.skills.map(s => [s.name, s]));
72
46
 
73
- // Removed skills are breaking.
74
47
  for (const name of bSkills.keys()) {
75
48
  if (!cSkills.has(name)) {
76
49
  breaking.push(`removed skill: ${name}`);
77
50
  }
78
51
  }
79
52
 
80
- // Added skills are additive.
81
53
  for (const name of cSkills.keys()) {
82
54
  if (!bSkills.has(name)) {
83
55
  additive.push(`added skill: ${name}`);
84
56
  }
85
57
  }
86
58
 
87
- // For each skill present in both, diff the pinned facts.
88
59
  for (const [name, b] of bSkills) {
89
60
  const c = cSkills.get(name);
90
61
  if (!c) continue;
91
62
 
92
- // version downgrades are breaking; bumps are additive.
93
63
  if (b.version && c.version && b.version !== c.version) {
94
- // Use a simple lexicographic compare — semver isn't enforced
95
- // upstream and the manifest version field is informational. The
96
- // operator should bump, not unbump.
64
+ // Lexicographic, not semver: nothing upstream enforces a semver shape.
97
65
  if (c.version < b.version) {
98
66
  breaking.push(`${name}: version downgraded ${b.version} -> ${c.version}`);
99
67
  } else {
@@ -111,7 +79,7 @@ function diff(baseline, current) {
111
79
  additive.push(`${name}: added trigger keywords: ${addedTriggers.join(", ")}`);
112
80
  }
113
81
 
114
- // Removed data deps break the skill at load time. Additions are fine.
82
+ // Removed data deps break the skill at load time.
115
83
  const removedDeps = b.data_deps.filter(d => !c.data_deps.includes(d));
116
84
  if (removedDeps.length > 0) {
117
85
  breaking.push(`${name}: removed data deps: ${removedDeps.join(", ")}`);
@@ -121,11 +89,7 @@ function diff(baseline, current) {
121
89
  additive.push(`${name}: added data deps: ${addedDeps.join(", ")}`);
122
90
  }
123
91
 
124
- // Removed ATLAS/ATT&CK/framework refs are surface narrowing.
125
- // Per AGENTS.md rule #4 (no orphaned controls) and #12 (external
126
- // data version pinning), narrowing the cited surface is a
127
- // deliberate decision worth surfacing in CI. Treat as breaking;
128
- // the operator can refresh the baseline alongside the intent.
92
+ // Narrowing the cited surface is deliberate (AGENTS.md #4, #12), so removal is breaking.
129
93
  for (const field of ["atlas_refs", "attack_refs", "framework_gaps", "rfc_refs", "cwe_refs", "d3fend_refs", "dlp_refs"]) {
130
94
  const removed = b[field].filter(r => !c[field].includes(r));
131
95
  if (removed.length > 0) {
@@ -138,9 +102,6 @@ function diff(baseline, current) {
138
102
  }
139
103
  }
140
104
 
141
- // ATLAS pinned-version change is breaking per AGENTS.md rule #12
142
- // (never silently inherit version changes). The operator must update
143
- // the baseline alongside the audit of TTP ID changes.
144
105
  if (baseline.atlas_version && current.atlas_version &&
145
106
  baseline.atlas_version !== current.atlas_version) {
146
107
  breaking.push(
@@ -171,33 +132,16 @@ function formatDiff(result) {
171
132
  }
172
133
 
173
134
  /**
174
- * Verify the on-disk snapshot still hashes to the value recorded in its
175
- * .sha256 sidecar. The sidecar is the only thing that catches a hand-edit
176
- * of manifest-snapshot.json that bypassed refresh-manifest-snapshot.js
177
- * (the surface diff alone is defeated by editing manifest.json AND the
178
- * baseline in lockstep — e.g. to hide a removed skill/trigger). The
179
- * sidecar pins the baseline's exact bytes so that lockstep edit no longer
180
- * produces a matching pair.
181
- *
182
- * Pairing invariant: refresh-manifest-snapshot.js always writes BOTH the
183
- * snapshot and its sidecar, and package.json `files` ships them together.
184
- * So a snapshot present WITHOUT its sidecar is not a benign legacy state
185
- * for any tree the current gate runs against (CI/predeploy run on the
186
- * committed tree, where both are present; the shipped tarball ships both).
187
- * An absent sidecar next to a present snapshot is the integrity-evasion
188
- * shape — treat it as a failure, symmetric with the present-but-mismatch
189
- * failure. Returns { ok, error } so callers can surface a hard exit.
190
- *
191
- * @param {string} root repo root containing the snapshot + sidecar
192
- * @returns {{ok: boolean, error: (string|null)}}
135
+ * Verify the snapshot still hashes to its .sha256 sidecar. The surface diff
136
+ * alone is defeated by editing manifest.json and the baseline in lockstep; the
137
+ * sidecar pins the baseline's exact bytes so that pair no longer matches.
193
138
  */
194
139
  function checkSnapshotIntegrity(root) {
195
140
  const snapshotPath = path.join(root, "manifest-snapshot.json");
196
141
  const shaPath = path.join(root, "manifest-snapshot.sha256");
197
142
 
198
143
  if (!fs.existsSync(snapshotPath)) {
199
- // No snapshot at all — the caller's baseline-read handles this as a
200
- // distinct error. Nothing for the integrity check to anchor against.
144
+ // Nothing to anchor against; the caller's baseline read reports this.
201
145
  return { ok: true, error: null };
202
146
  }
203
147
 
@@ -253,11 +197,7 @@ if (require.main === module) {
253
197
  process.exit(2);
254
198
  }
255
199
 
256
- // Integrity gate: the snapshot must hash to its recorded sidecar value,
257
- // and the sidecar must be present whenever the snapshot is. A missing
258
- // sidecar next to a present snapshot is itself a failure — it is the
259
- // only thing that would have caught a lockstep hand-edit of manifest.json
260
- // + the baseline, so allowing it to be deleted re-opens that bypass.
200
+ // Before the diff: a tampered baseline must not be diffed as though trustworthy.
261
201
  const integrity = checkSnapshotIntegrity(ROOT);
262
202
  if (!integrity.ok) {
263
203
  console.error("[check-manifest-snapshot] " + integrity.error);