@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1

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 (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. package/lib/hostname.mjs +0 -2
@@ -0,0 +1,352 @@
1
+ import { ANALYST_REVIEW_REASON_VALUES } from "./analyst-taxonomy.mjs";
2
+ import { scanBoundedJson } from "./bounded-json.mjs";
3
+ import { RISK_REVIEW_THRESHOLD } from "./risk-scoring.mjs";
4
+ import { MAX_RISK_CALIBRATION_RECORDS, MAX_RISK_CALIBRATION_JSON_CONTAINER_ITEMS, MAX_RISK_CALIBRATION_JSON_DEPTH, MAX_RISK_CALIBRATION_JSON_KEYS, MAX_RISK_CALIBRATION_JSON_VALUES, MAX_RISK_CALIBRATION_MODEL_VERSION, MAX_RISK_CALIBRATION_REVIEW_REASON_LENGTH, MAX_RISK_CALIBRATION_SUMMARY_BYTES, MAX_RISK_CALIBRATION_SUMMARY_STRATA, MAX_RISK_CALIBRATION_TIMESTAMP_LENGTH, RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_CONFIDENCE_KEYS, RISK_CALIBRATION_REPORT_DATASET_KEYS, RISK_CALIBRATION_REPORT_INTERPRETATION_KEYS, RISK_CALIBRATION_REPORT_INTERVAL_KEYS, RISK_CALIBRATION_REPORT_METRIC_KEYS, RISK_CALIBRATION_REPORT_MODEL_COMPARISON_KEYS, RISK_CALIBRATION_REPORT_PRIVACY_KEYS, RISK_CALIBRATION_REPORT_SCHEMA, RISK_CALIBRATION_REPORT_SCORE_BAND_KEYS, RISK_CALIBRATION_REPORT_STRATUM_KEYS, RISK_CALIBRATION_REPORT_SUMMARY_KEYS, RISK_CALIBRATION_REPORT_VERSION, RISK_CALIBRATION_REPORT_V3_SUMMARY_ROOT_KEYS, SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS, } from "../contracts/risk-calibration.mjs";
5
+ export const RISK_CALIBRATION_SUMMARY_SCHEMA = RISK_CALIBRATION_REPORT_SCHEMA;
6
+ export const RISK_CALIBRATION_SUMMARY_VERSION = RISK_CALIBRATION_REPORT_VERSION;
7
+ export const RISK_CALIBRATION_SUMMARY_MAX_BYTES = MAX_RISK_CALIBRATION_SUMMARY_BYTES;
8
+ export const RISK_CALIBRATION_SUMMARY_MAX_RECORDS = MAX_RISK_CALIBRATION_RECORDS;
9
+ export const RISK_CALIBRATION_SUMMARY_MIN_STRATUM_SAMPLE = 20;
10
+ export const RISK_CALIBRATION_SUMMARY_THRESHOLDS = Object.freeze([...new Set([40, 50, 60, RISK_REVIEW_THRESHOLD, 80, 90])].sort((left, right) => left - right));
11
+ export const RISK_CALIBRATION_SUMMARY_INTERPRETATION = 'This offline replay compares heuristic Risk scores with analyst dispositions. It does not prove maliciousness or safety.';
12
+ const ROOT_KEYS = new Set(RISK_CALIBRATION_REPORT_V3_SUMMARY_ROOT_KEYS);
13
+ const DATASET_KEYS = new Set(RISK_CALIBRATION_REPORT_DATASET_KEYS);
14
+ const SUMMARY_KEYS = new Set(RISK_CALIBRATION_REPORT_SUMMARY_KEYS);
15
+ const SCORE_BAND_KEYS = new Set(RISK_CALIBRATION_REPORT_SCORE_BAND_KEYS);
16
+ const METRIC_KEYS = new Set(RISK_CALIBRATION_REPORT_METRIC_KEYS);
17
+ const CONFIDENCE_KEYS = new Set(RISK_CALIBRATION_REPORT_CONFIDENCE_KEYS);
18
+ const INTERVAL_KEYS = new Set(RISK_CALIBRATION_REPORT_INTERVAL_KEYS);
19
+ const STRATUM_KEYS = new Set(RISK_CALIBRATION_REPORT_STRATUM_KEYS);
20
+ const MODEL_COMPARISON_KEYS = new Set(RISK_CALIBRATION_REPORT_MODEL_COMPARISON_KEYS);
21
+ const PRIVACY_KEYS = new Set(RISK_CALIBRATION_REPORT_PRIVACY_KEYS);
22
+ const INTERPRETATION_KEYS = new Set(RISK_CALIBRATION_REPORT_INTERPRETATION_KEYS);
23
+ const REVIEW_REASON_VALUES = new Set([...ANALYST_REVIEW_REASON_VALUES, 'not_recorded']);
24
+ const SCAN_DEPTH_VALUES = new Set(['deep', 'fast', 'unknown']);
25
+ const CONTROL_RE = /[\x00-\x1f\x7f]/u;
26
+ function object(value, label) {
27
+ if (!value || typeof value !== 'object' || Array.isArray(value))
28
+ throw new Error(`${label} must be an object.`);
29
+ return value;
30
+ }
31
+ function exactKeys(value, expected, label) {
32
+ const keys = Object.keys(value);
33
+ if (keys.length !== expected.size || keys.some((key) => !expected.has(key))) {
34
+ throw new Error(`${label} contains unsupported or missing fields.`);
35
+ }
36
+ }
37
+ function boundedCount(value, label, maximum = RISK_CALIBRATION_SUMMARY_MAX_RECORDS) {
38
+ if (!Number.isSafeInteger(value) || Number(value) < 0 || Number(value) > maximum) {
39
+ throw new Error(`${label} must be a bounded non-negative integer.`);
40
+ }
41
+ return Number(value);
42
+ }
43
+ function modelVersion(value, label) {
44
+ if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > MAX_RISK_CALIBRATION_MODEL_VERSION) {
45
+ throw new Error(`${label} must be a supported positive model version.`);
46
+ }
47
+ return Number(value);
48
+ }
49
+ function canonicalTimestamp(value, label) {
50
+ if (typeof value !== 'string'
51
+ || value.length > MAX_RISK_CALIBRATION_TIMESTAMP_LENGTH
52
+ || !Number.isFinite(Date.parse(value))) {
53
+ throw new Error(`${label} must be a valid date and time.`);
54
+ }
55
+ const canonical = new Date(value).toISOString();
56
+ if (canonical !== value)
57
+ throw new Error(`${label} must use canonical UTC ISO-8601 form.`);
58
+ return canonical;
59
+ }
60
+ function ratio(numerator, denominator) {
61
+ return denominator ? Number((numerator / denominator).toFixed(4)) : null;
62
+ }
63
+ function wilson95(successes, total) {
64
+ if (!total)
65
+ return null;
66
+ const z = 1.96;
67
+ const proportion = successes / total;
68
+ const denominator = 1 + (z * z) / total;
69
+ const center = (proportion + (z * z) / (2 * total)) / denominator;
70
+ const margin = (z * Math.sqrt((proportion * (1 - proportion) + (z * z) / (4 * total)) / total)) / denominator;
71
+ return Object.freeze({
72
+ lower: Number(Math.max(0, center - margin).toFixed(4)),
73
+ upper: Number(Math.min(1, center + margin).toFixed(4)),
74
+ });
75
+ }
76
+ function exactRatio(value, expected, label) {
77
+ if (value !== expected)
78
+ throw new Error(`${label} is inconsistent with the retained confusion counts.`);
79
+ return expected;
80
+ }
81
+ function interval(value, expected, label) {
82
+ if (expected === null) {
83
+ if (value !== null)
84
+ throw new Error(`${label} must be null when its denominator is zero.`);
85
+ return null;
86
+ }
87
+ const candidate = object(value, label);
88
+ exactKeys(candidate, INTERVAL_KEYS, label);
89
+ if (candidate.lower !== expected.lower || candidate.upper !== expected.upper) {
90
+ throw new Error(`${label} is inconsistent with the retained confusion counts.`);
91
+ }
92
+ return Object.freeze({ lower: expected.lower, upper: expected.upper });
93
+ }
94
+ function thresholdMetrics(value, label, expectedThreshold, expectedTotal, expectedPositive = null, expectedNegative = null) {
95
+ const metric = object(value, label);
96
+ exactKeys(metric, METRIC_KEYS, label);
97
+ if (metric.threshold !== expectedThreshold)
98
+ throw new Error(`${label} has an unexpected threshold.`);
99
+ const truePositive = boundedCount(metric.truePositive, `${label} true-positive count`, expectedTotal);
100
+ const falseNegative = boundedCount(metric.falseNegative, `${label} false-negative count`, expectedTotal);
101
+ const trueNegative = boundedCount(metric.trueNegative, `${label} true-negative count`, expectedTotal);
102
+ const falsePositive = boundedCount(metric.falsePositive, `${label} false-positive count`, expectedTotal);
103
+ if (truePositive + falsePositive + trueNegative + falseNegative !== expectedTotal
104
+ || (expectedPositive !== null && truePositive + falseNegative !== expectedPositive)
105
+ || (expectedNegative !== null && trueNegative + falsePositive !== expectedNegative)) {
106
+ throw new Error(`${label} has inconsistent class counts.`);
107
+ }
108
+ const precision = ratio(truePositive, truePositive + falsePositive);
109
+ const recall = ratio(truePositive, truePositive + falseNegative);
110
+ const specificity = ratio(trueNegative, trueNegative + falsePositive);
111
+ const falsePositiveRate = ratio(falsePositive, falsePositive + trueNegative);
112
+ const f1 = ratio(2 * truePositive, 2 * truePositive + falsePositive + falseNegative);
113
+ const balancedAccuracy = recall === null || specificity === null
114
+ ? null
115
+ : Number(((recall + specificity) / 2).toFixed(4));
116
+ const confidence = object(metric.confidence95, `${label} confidence intervals`);
117
+ exactKeys(confidence, CONFIDENCE_KEYS, `${label} confidence intervals`);
118
+ return Object.freeze({
119
+ threshold: expectedThreshold,
120
+ truePositive,
121
+ falsePositive,
122
+ trueNegative,
123
+ falseNegative,
124
+ precision: exactRatio(metric.precision, precision, `${label} precision`),
125
+ recall: exactRatio(metric.recall, recall, `${label} recall`),
126
+ specificity: exactRatio(metric.specificity, specificity, `${label} specificity`),
127
+ falsePositiveRate: exactRatio(metric.falsePositiveRate, falsePositiveRate, `${label} false-positive rate`),
128
+ f1: exactRatio(metric.f1, f1, `${label} F1`),
129
+ balancedAccuracy: exactRatio(metric.balancedAccuracy, balancedAccuracy, `${label} balanced accuracy`),
130
+ confidence95: Object.freeze({
131
+ precision: interval(confidence.precision, wilson95(truePositive, truePositive + falsePositive), `${label} precision interval`),
132
+ recall: interval(confidence.recall, wilson95(truePositive, truePositive + falseNegative), `${label} recall interval`),
133
+ specificity: interval(confidence.specificity, wilson95(trueNegative, trueNegative + falsePositive), `${label} specificity interval`),
134
+ }),
135
+ });
136
+ }
137
+ export function parseRiskCalibrationSummaryReport(raw) {
138
+ if (typeof raw !== 'string')
139
+ throw new Error('Risk calibration summary must be JSON text.');
140
+ const bytes = new TextEncoder().encode(raw).byteLength;
141
+ if (bytes < 1 || bytes > RISK_CALIBRATION_SUMMARY_MAX_BYTES) {
142
+ throw new Error(`Risk calibration summary must be between 1 byte and ${RISK_CALIBRATION_SUMMARY_MAX_BYTES} bytes.`);
143
+ }
144
+ let parsed;
145
+ try {
146
+ const withoutBom = raw.replace(/^\uFEFF/u, '');
147
+ scanBoundedJson(withoutBom, {
148
+ maximumDepth: MAX_RISK_CALIBRATION_JSON_DEPTH,
149
+ maximumKeys: MAX_RISK_CALIBRATION_JSON_KEYS,
150
+ maximumValues: MAX_RISK_CALIBRATION_JSON_VALUES,
151
+ maximumContainerItems: MAX_RISK_CALIBRATION_JSON_CONTAINER_ITEMS,
152
+ });
153
+ parsed = JSON.parse(withoutBom);
154
+ }
155
+ catch (reason) {
156
+ const message = reason instanceof Error
157
+ ? reason.message.replace(/^Artefact (?:input|JSON)/u, 'Risk calibration summary')
158
+ : 'Risk calibration summary is not valid JSON.';
159
+ throw new Error(message);
160
+ }
161
+ const root = object(parsed, 'Risk calibration summary');
162
+ exactKeys(root, ROOT_KEYS, 'Risk calibration summary');
163
+ if (root.schema !== RISK_CALIBRATION_SUMMARY_SCHEMA
164
+ || root.version !== RISK_CALIBRATION_SUMMARY_VERSION
165
+ || root.mode !== 'summary') {
166
+ throw new Error('Risk calibration summary has an unsupported schema, version, or mode.');
167
+ }
168
+ const dataset = object(root.dataset, 'Risk calibration dataset metadata');
169
+ exactKeys(dataset, DATASET_KEYS, 'Risk calibration dataset metadata');
170
+ if (dataset.schema !== RISK_CALIBRATION_DATASET_SCHEMA
171
+ || typeof dataset.version !== 'number'
172
+ || !SUPPORTED_RISK_CALIBRATION_DATASET_VERSIONS.includes(dataset.version)) {
173
+ throw new Error('Risk calibration dataset metadata has an unsupported schema or version.');
174
+ }
175
+ const recordCount = boundedCount(dataset.recordCount, 'Risk calibration dataset record count');
176
+ if (recordCount < 1)
177
+ throw new Error('Risk calibration dataset metadata must describe at least one record.');
178
+ const riskModelVersion = modelVersion(root.riskModelVersion, 'Risk model version');
179
+ if (!RISK_CALIBRATION_SUMMARY_THRESHOLDS.includes(Number(root.currentReviewThreshold))) {
180
+ throw new Error('Current review threshold is not part of the fixed calibration threshold set.');
181
+ }
182
+ const currentReviewThreshold = Number(root.currentReviewThreshold);
183
+ const rawSummary = object(root.summary, 'Risk calibration summary counts');
184
+ exactKeys(rawSummary, SUMMARY_KEYS, 'Risk calibration summary counts');
185
+ const total = boundedCount(rawSummary.total, 'Total record count');
186
+ const positive = boundedCount(rawSummary.positive, 'Positive label count', total);
187
+ const negative = boundedCount(rawSummary.negative, 'Negative label count', total);
188
+ const excluded = boundedCount(rawSummary.excluded, 'Excluded label count', total);
189
+ if (total !== recordCount || positive + negative + excluded !== total) {
190
+ throw new Error('Risk calibration summary label counts are inconsistent.');
191
+ }
192
+ const rawBands = object(rawSummary.scoreBands, 'Risk calibration score bands');
193
+ exactKeys(rawBands, SCORE_BAND_KEYS, 'Risk calibration score bands');
194
+ const scoreBands = Object.freeze({
195
+ not_scored: boundedCount(rawBands.not_scored, 'Not-scored count', total),
196
+ '0_39': boundedCount(rawBands['0_39'], 'Low-band count', total),
197
+ '40_69': boundedCount(rawBands['40_69'], 'Review-band count', total),
198
+ '70_100': boundedCount(rawBands['70_100'], 'High-band count', total),
199
+ });
200
+ if (Object.values(scoreBands).reduce((sum, count) => sum + count, 0) !== total) {
201
+ throw new Error('Risk calibration score-band counts are inconsistent.');
202
+ }
203
+ if (scoreBands.not_scored > excluded) {
204
+ throw new Error('Risk calibration not-scored count cannot exceed excluded labels.');
205
+ }
206
+ if (!Array.isArray(root.thresholds) || root.thresholds.length !== RISK_CALIBRATION_SUMMARY_THRESHOLDS.length) {
207
+ throw new Error('Risk calibration summary must contain the fixed threshold set exactly once.');
208
+ }
209
+ const thresholds = Object.freeze(root.thresholds.map((value, index) => thresholdMetrics(value, `Threshold ${index + 1}`, RISK_CALIBRATION_SUMMARY_THRESHOLDS[index], positive + negative, positive, negative)));
210
+ for (let index = 1; index < thresholds.length; index += 1) {
211
+ const lower = thresholds[index - 1];
212
+ const higher = thresholds[index];
213
+ // The population and class totals are already fixed by thresholdMetrics.
214
+ // Raising the threshold can remove positive classifications, never add them.
215
+ if (higher.truePositive > lower.truePositive || higher.falsePositive > lower.falsePositive) {
216
+ throw new Error('Risk calibration positive classifications increase at a higher threshold.');
217
+ }
218
+ }
219
+ if (!Array.isArray(root.strata) || root.strata.length > MAX_RISK_CALIBRATION_SUMMARY_STRATA) {
220
+ throw new Error('Risk calibration strata must be a bounded array.');
221
+ }
222
+ const seenStrata = new Set();
223
+ const strata = Object.freeze(root.strata.map((value, index) => {
224
+ const label = `Stratum ${index + 1}`;
225
+ const stratum = object(value, label);
226
+ exactKeys(stratum, STRATUM_KEYS, label);
227
+ if (stratum.dimension !== 'review_reason' && stratum.dimension !== 'scan_depth') {
228
+ throw new Error(`${label} has an unsupported dimension.`);
229
+ }
230
+ if (typeof stratum.value !== 'string'
231
+ || !stratum.value
232
+ || stratum.value.length > MAX_RISK_CALIBRATION_REVIEW_REASON_LENGTH
233
+ || CONTROL_RE.test(stratum.value)) {
234
+ throw new Error(`${label} has an invalid value.`);
235
+ }
236
+ const allowedValues = stratum.dimension === 'review_reason' ? REVIEW_REASON_VALUES : SCAN_DEPTH_VALUES;
237
+ if (!allowedValues.has(stratum.value))
238
+ throw new Error(`${label} has an unsupported value.`);
239
+ const key = `${stratum.dimension}\u0000${stratum.value}`;
240
+ if (seenStrata.has(key))
241
+ throw new Error(`${label} is duplicated.`);
242
+ seenStrata.add(key);
243
+ const sampleCount = boundedCount(stratum.sampleCount, `${label} sample count`, positive + negative);
244
+ if (stratum.insufficientSample !== (sampleCount < RISK_CALIBRATION_SUMMARY_MIN_STRATUM_SAMPLE)) {
245
+ throw new Error(`${label} has an inconsistent sample-sufficiency state.`);
246
+ }
247
+ return Object.freeze({
248
+ dimension: stratum.dimension,
249
+ value: stratum.value,
250
+ sampleCount,
251
+ insufficientSample: stratum.insufficientSample,
252
+ metrics: thresholdMetrics(stratum.metrics, `${label} metrics`, currentReviewThreshold, sampleCount),
253
+ });
254
+ }));
255
+ const currentMetrics = thresholds.find((metric) => metric.threshold === currentReviewThreshold);
256
+ for (const dimension of ['review_reason', 'scan_depth']) {
257
+ const members = strata.filter((stratum) => stratum.dimension === dimension);
258
+ const dimensionTotal = members.reduce((sum, stratum) => sum + stratum.sampleCount, 0);
259
+ if (dimensionTotal !== positive + negative) {
260
+ throw new Error(`Risk calibration ${dimension.replace('_', '-')} strata do not cover every included label exactly once.`);
261
+ }
262
+ for (const field of ['truePositive', 'falsePositive', 'trueNegative', 'falseNegative']) {
263
+ if (members.reduce((sum, stratum) => sum + stratum.metrics[field], 0) !== currentMetrics[field]) {
264
+ throw new Error(`Risk calibration ${dimension.replace('_', '-')} strata disagree with the current-threshold confusion counts.`);
265
+ }
266
+ }
267
+ }
268
+ const comparison = object(root.modelComparison, 'Risk model comparison');
269
+ exactKeys(comparison, MODEL_COMPARISON_KEYS, 'Risk model comparison');
270
+ if (typeof comparison.available !== 'boolean')
271
+ throw new Error('Risk model comparison availability must be true or false.');
272
+ const currentModelVersion = modelVersion(comparison.currentModelVersion, 'Compared current model version');
273
+ if (currentModelVersion !== riskModelVersion)
274
+ throw new Error('Risk model comparison does not match the report model version.');
275
+ const previousModelVersion = comparison.previousModelVersion === null
276
+ ? null
277
+ : modelVersion(comparison.previousModelVersion, 'Compared previous model version');
278
+ const scoresChanged = boundedCount(comparison.scoresChanged, 'Changed-score count', total);
279
+ const bandsChanged = boundedCount(comparison.bandsChanged, 'Changed-band count', total);
280
+ const thresholdClassificationsChanged = boundedCount(comparison.thresholdClassificationsChanged, 'Changed-threshold count', total);
281
+ if ((!comparison.available && (previousModelVersion !== null || scoresChanged || bandsChanged || thresholdClassificationsChanged))
282
+ || (comparison.available && previousModelVersion === null)
283
+ || bandsChanged > scoresChanged
284
+ || thresholdClassificationsChanged > scoresChanged) {
285
+ throw new Error('Risk model comparison availability and counts are inconsistent.');
286
+ }
287
+ const privacy = object(root.privacy, 'Risk calibration privacy declaration');
288
+ exactKeys(privacy, PRIVACY_KEYS, 'Risk calibration privacy declaration');
289
+ if (privacy.targetsRetained !== 0 || privacy.identifiersRetained !== 0 || privacy.rawEvidenceRetained !== 0) {
290
+ throw new Error('Risk calibration summary must retain zero targets, identifiers, and raw evidence.');
291
+ }
292
+ const interpretation = object(root.interpretation, 'Risk calibration interpretation');
293
+ exactKeys(interpretation, INTERPRETATION_KEYS, 'Risk calibration interpretation');
294
+ if (interpretation.authority !== 'analyst_context_only'
295
+ || interpretation.statement !== RISK_CALIBRATION_SUMMARY_INTERPRETATION
296
+ || interpretation.automaticTuning !== false
297
+ || interpretation.networkRequests !== false
298
+ || interpretation.persisted !== false) {
299
+ throw new Error('Risk calibration interpretation is inconsistent with the offline diagnostic boundary.');
300
+ }
301
+ return Object.freeze({
302
+ schema: RISK_CALIBRATION_SUMMARY_SCHEMA,
303
+ version: RISK_CALIBRATION_SUMMARY_VERSION,
304
+ mode: 'summary',
305
+ generatedAt: canonicalTimestamp(root.generatedAt, 'Risk calibration generation time'),
306
+ dataset: Object.freeze({
307
+ schema: RISK_CALIBRATION_DATASET_SCHEMA,
308
+ version: dataset.version,
309
+ recordCount,
310
+ }),
311
+ riskModelVersion,
312
+ currentReviewThreshold,
313
+ summary: Object.freeze({ total, positive, negative, excluded, scoreBands }),
314
+ thresholds,
315
+ strata,
316
+ modelComparison: Object.freeze({
317
+ available: comparison.available,
318
+ previousModelVersion,
319
+ currentModelVersion,
320
+ scoresChanged,
321
+ bandsChanged,
322
+ thresholdClassificationsChanged,
323
+ }),
324
+ privacy: Object.freeze({ targetsRetained: 0, identifiersRetained: 0, rawEvidenceRetained: 0 }),
325
+ interpretation: Object.freeze({
326
+ authority: 'analyst_context_only',
327
+ statement: RISK_CALIBRATION_SUMMARY_INTERPRETATION,
328
+ automaticTuning: false,
329
+ networkRequests: false,
330
+ persisted: false,
331
+ }),
332
+ });
333
+ }
334
+ export function buildRiskCalibrationSummaryReport(source) {
335
+ const report = object(source, 'Risk calibration report');
336
+ const projected = {
337
+ schema: RISK_CALIBRATION_SUMMARY_SCHEMA,
338
+ version: RISK_CALIBRATION_SUMMARY_VERSION,
339
+ mode: 'summary',
340
+ generatedAt: report.generatedAt,
341
+ dataset: report.dataset,
342
+ riskModelVersion: report.riskModelVersion,
343
+ currentReviewThreshold: report.currentReviewThreshold,
344
+ summary: report.summary,
345
+ thresholds: report.thresholds,
346
+ strata: report.strata,
347
+ modelComparison: report.modelComparison,
348
+ privacy: { targetsRetained: 0, identifiersRetained: 0, rawEvidenceRetained: 0 },
349
+ interpretation: report.interpretation,
350
+ };
351
+ return parseRiskCalibrationSummaryReport(JSON.stringify(projected));
352
+ }