@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0

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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -1,26 +0,0 @@
1
- export function boundedVisualizationText(value, maxLength) {
2
- return String(value ?? '')
3
- .replace(/[\u0000-\u001f\u007f]/gu, ' ')
4
- .replace(/\s+/gu, ' ')
5
- .trim()
6
- .slice(0, maxLength);
7
- }
8
- export function boundedVisualizationId(value) {
9
- return boundedVisualizationText(value, 64)
10
- .toLowerCase()
11
- .replace(/[^a-z0-9_-]+/gu, '-')
12
- .replace(/^-+|-+$/gu, '');
13
- }
14
- export function boundedVisualizationNumber(value, minimum, maximum, fallback = 0) {
15
- const number = Number(value);
16
- return Number.isFinite(number)
17
- ? Math.max(minimum, Math.min(maximum, number))
18
- : fallback;
19
- }
20
- export function validVisualizationDate(value) {
21
- const text = boundedVisualizationText(value, 64);
22
- const milliseconds = Date.parse(text);
23
- return Number.isFinite(milliseconds)
24
- ? { text: new Date(milliseconds).toISOString(), milliseconds }
25
- : null;
26
- }
@@ -1,352 +0,0 @@
1
- import { forceCenter, forceCollide, forceLink, forceManyBody, forceSimulation, forceX, forceY, } from 'd3-force';
2
- import { boundedVisualizationId as boundedId, boundedVisualizationText as boundedText, } from "./visualization-bounds.js";
3
- export const MAX_FORCE_GRAPH_NODES = 48;
4
- export const MAX_FORCE_GRAPH_LINKS = 80;
5
- export const FORCE_GRAPH_LINK_KINDS = [
6
- 'observed',
7
- 'partial',
8
- 'unknown',
9
- 'derived',
10
- 'summary',
11
- ];
12
- function forceGraphLinkKind(value) {
13
- const normalized = boundedId(value);
14
- if (!normalized)
15
- return 'observed';
16
- return FORCE_GRAPH_LINK_KINDS.includes(normalized)
17
- ? normalized
18
- : 'unknown';
19
- }
20
- const FORCE_GRAPH_GROUPS = {
21
- address: { id: 'network', label: 'Network' },
22
- certificate: { id: 'certificate', label: 'Certificates' },
23
- domain: { id: 'hosts', label: 'Domains and hosts' },
24
- hostname: { id: 'hosts', label: 'Domains and hosts' },
25
- identity: { id: 'identity', label: 'Identity' },
26
- issuer: { id: 'certificate', label: 'Certificates' },
27
- key: { id: 'certificate', label: 'Certificates' },
28
- network: { id: 'network', label: 'Network' },
29
- observation: { id: 'observations', label: 'Source observations' },
30
- origin: { id: 'identity', label: 'Identity' },
31
- prefix: { id: 'network', label: 'Network' },
32
- registrar: { id: 'registration', label: 'Registration' },
33
- relationship: { id: 'relationships', label: 'Shared evidence' },
34
- service: { id: 'services', label: 'Services' },
35
- summary: { id: 'summary', label: 'Grouped evidence' },
36
- target: { id: 'focus', label: 'Lookup target' },
37
- technology: { id: 'technology', label: 'Technology' },
38
- tracker: { id: 'identity', label: 'Identity' },
39
- };
40
- const FORCE_GRAPH_CLUSTER_INDEX = {
41
- certificate: 0,
42
- hosts: 1,
43
- identity: 2,
44
- network: 3,
45
- observations: 4,
46
- registration: 5,
47
- relationships: 6,
48
- services: 7,
49
- summary: 6,
50
- technology: 7,
51
- };
52
- function forceGraphClusterIndex(group) {
53
- const assigned = FORCE_GRAPH_CLUSTER_INDEX[group];
54
- if (assigned !== undefined)
55
- return assigned;
56
- let hash = 0;
57
- for (const [index, character] of [...group].entries()) {
58
- hash = (hash + (character.codePointAt(0) ?? 0) * (index + 1)) % 8;
59
- }
60
- return hash;
61
- }
62
- function forceGraphGroup(kind, rawGroup, rawGroupLabel) {
63
- const requested = boundedId(rawGroup);
64
- const requestedLabel = boundedText(rawGroupLabel, 40);
65
- if (requested) {
66
- return {
67
- id: requested,
68
- label: requestedLabel || requested.replaceAll('-', ' ').replace(/\b\w/gu, (character) => character.toUpperCase()),
69
- };
70
- }
71
- return FORCE_GRAPH_GROUPS[kind] ?? {
72
- id: kind || 'evidence',
73
- label: (kind || 'evidence').replaceAll('-', ' ').replace(/\b\w/gu, (character) => character.toUpperCase()),
74
- };
75
- }
76
- function wrapForceGraphLabel(value, maximumLineLength = 20) {
77
- const lines = [];
78
- for (const word of value.split(/\s+/u).filter(Boolean)) {
79
- const pieces = [];
80
- let remaining = word;
81
- while (remaining.length > maximumLineLength) {
82
- let splitAt = maximumLineLength;
83
- for (let index = maximumLineLength; index >= Math.floor(maximumLineLength * 0.55); index -= 1) {
84
- if (/[./:_-]/u.test(remaining[index - 1] ?? '')) {
85
- splitAt = index;
86
- break;
87
- }
88
- }
89
- pieces.push(remaining.slice(0, splitAt));
90
- remaining = remaining.slice(splitAt);
91
- }
92
- if (remaining)
93
- pieces.push(remaining);
94
- for (const [pieceIndex, piece] of pieces.entries()) {
95
- const current = lines.at(-1);
96
- if (pieceIndex === 0 && current && current.length + piece.length + 1 <= maximumLineLength) {
97
- lines[lines.length - 1] = `${current} ${piece}`;
98
- }
99
- else {
100
- lines.push(piece);
101
- }
102
- }
103
- }
104
- return lines.length ? lines : [value];
105
- }
106
- function forceGraphClusterCenters(groups, width, height, hasFocus) {
107
- const centerX = width / 2;
108
- const centerY = height / 2;
109
- const centers = new Map();
110
- if (!groups.length)
111
- return centers;
112
- if (!hasFocus && groups.length === 1) {
113
- centers.set(groups[0] ?? '', { x: centerX, y: centerY });
114
- return centers;
115
- }
116
- if (!hasFocus && groups.length === 2) {
117
- centers.set(groups[0] ?? '', { x: width * 0.29, y: centerY });
118
- centers.set(groups[1] ?? '', { x: width * 0.71, y: centerY });
119
- return centers;
120
- }
121
- if (hasFocus && groups.length === 1) {
122
- centers.set(groups[0] ?? '', { x: width * 0.72, y: centerY });
123
- return centers;
124
- }
125
- if (hasFocus && groups.length === 2) {
126
- centers.set(groups[0] ?? '', { x: width * 0.25, y: centerY });
127
- centers.set(groups[1] ?? '', { x: width * 0.75, y: centerY });
128
- return centers;
129
- }
130
- const radiusX = hasFocus ? width * 0.33 : width * 0.27;
131
- const radiusY = hasFocus ? Math.max(150, height * 0.36) : Math.max(125, height * 0.3);
132
- for (const [index, group] of groups.entries()) {
133
- const angle = -Math.PI / 2 + index * Math.PI * 2 / groups.length;
134
- centers.set(group, {
135
- x: centerX + Math.cos(angle) * radiusX,
136
- y: centerY + Math.sin(angle) * radiusY,
137
- });
138
- }
139
- return centers;
140
- }
141
- function forceGraphNodeBounds(node) {
142
- if (node.kind === 'target') {
143
- const halfWidth = (node.labelWidth + 20) / 2;
144
- const halfHeight = (node.labelLines.length * 13 + 17) / 2;
145
- return { halfWidth, above: halfHeight, below: halfHeight };
146
- }
147
- return {
148
- halfWidth: Math.max(20, node.labelWidth / 2),
149
- above: 22,
150
- below: 33 + node.labelLines.length * 13,
151
- };
152
- }
153
- function resolveForceGraphLabelCollisions(nodes, width, height, focusNodeId) {
154
- const margin = 7;
155
- for (let iteration = 0; iteration < 120; iteration += 1) {
156
- let moved = false;
157
- for (let leftIndex = 0; leftIndex < nodes.length; leftIndex += 1) {
158
- const left = nodes[leftIndex];
159
- if (!left)
160
- continue;
161
- const leftBounds = forceGraphNodeBounds(left);
162
- for (let rightIndex = leftIndex + 1; rightIndex < nodes.length; rightIndex += 1) {
163
- const right = nodes[rightIndex];
164
- if (!right)
165
- continue;
166
- const rightBounds = forceGraphNodeBounds(right);
167
- const overlapX = leftBounds.halfWidth + rightBounds.halfWidth + margin - Math.abs(left.x - right.x);
168
- const overlapY = Math.min(left.y + leftBounds.below, right.y + rightBounds.below) - Math.max(left.y - leftBounds.above, right.y - rightBounds.above) + margin;
169
- if (overlapX <= 0 || overlapY <= 0)
170
- continue;
171
- moved = true;
172
- const leftFixed = left.id === focusNodeId;
173
- const rightFixed = right.id === focusNodeId;
174
- if (overlapX / (leftBounds.halfWidth + rightBounds.halfWidth) < overlapY / Math.max(leftBounds.above + leftBounds.below, rightBounds.above + rightBounds.below)) {
175
- const direction = left.x <= right.x ? -1 : 1;
176
- const shift = overlapX / (leftFixed || rightFixed ? 1 : 2) + 0.5;
177
- if (!leftFixed)
178
- left.x += direction * shift;
179
- if (!rightFixed)
180
- right.x -= direction * shift;
181
- }
182
- else {
183
- const direction = left.y <= right.y ? -1 : 1;
184
- const shift = overlapY / (leftFixed || rightFixed ? 1 : 2) + 0.5;
185
- if (!leftFixed)
186
- left.y += direction * shift;
187
- if (!rightFixed)
188
- right.y -= direction * shift;
189
- }
190
- }
191
- }
192
- for (const node of nodes) {
193
- const bounds = forceGraphNodeBounds(node);
194
- node.x = Math.max(bounds.halfWidth + margin, Math.min(width - bounds.halfWidth - margin, node.x));
195
- node.y = Math.max(bounds.above + margin, Math.min(height - bounds.below - margin, node.y));
196
- }
197
- if (!moved)
198
- break;
199
- }
200
- }
201
- export function projectBoundedForceGraph(rawNodes, rawLinks, options = {}) {
202
- const rawNodeCount = Array.isArray(rawNodes) ? rawNodes.length : 0;
203
- const rawLinkCount = Array.isArray(rawLinks) ? rawLinks.length : 0;
204
- const seen = new Set();
205
- const candidates = (Array.isArray(rawNodes) ? rawNodes : [])
206
- .slice(0, MAX_FORCE_GRAPH_NODES * 2)
207
- .map((node) => {
208
- const id = boundedText(node?.id, 80);
209
- const label = boundedText(node?.label, 64);
210
- if (!id || !label || seen.has(id))
211
- return null;
212
- seen.add(id);
213
- const kind = boundedId(node?.kind) || 'evidence';
214
- const group = forceGraphGroup(kind, node?.group, node?.groupLabel);
215
- const labelLines = wrapForceGraphLabel(label);
216
- const labelWidth = Math.max(54, Math.min(142, Math.max(...labelLines.map((line) => line.length)) * 6.2 + 16));
217
- return {
218
- id,
219
- label,
220
- labelLines,
221
- labelWidth,
222
- kind,
223
- detail: boundedText(node?.detail, 100),
224
- group: group.id,
225
- groupLabel: group.label,
226
- clusterIndex: 0,
227
- collisionRadius: Math.max(38, labelWidth / 2 + 8, 30 + labelLines.length * 6),
228
- x: 0,
229
- y: 0,
230
- };
231
- })
232
- .filter((node) => Boolean(node))
233
- .sort((a, b) => a.id.localeCompare(b.id));
234
- const nodes = candidates.slice(0, MAX_FORCE_GRAPH_NODES);
235
- const nodeIds = new Set(nodes.map((node) => node.id));
236
- const seenLinks = new Set();
237
- const links = (Array.isArray(rawLinks) ? rawLinks : [])
238
- .slice(0, MAX_FORCE_GRAPH_LINKS * 2)
239
- .map((link, index) => ({
240
- id: boundedText(link?.id, 80) || `link-${index}`,
241
- source: boundedText(link?.source, 80),
242
- target: boundedText(link?.target, 80),
243
- kind: forceGraphLinkKind(link?.kind),
244
- detail: boundedText(link?.detail, 100),
245
- }))
246
- .filter((link) => {
247
- if (link.source === link.target
248
- || !nodeIds.has(link.source)
249
- || !nodeIds.has(link.target)
250
- || seenLinks.has(link.id)) {
251
- return false;
252
- }
253
- seenLinks.add(link.id);
254
- return true;
255
- })
256
- .sort((a, b) => a.id.localeCompare(b.id))
257
- .slice(0, MAX_FORCE_GRAPH_LINKS);
258
- const requestedFocusId = boundedText(options.focusNodeId, 80);
259
- const focusNode = nodes.find((node) => node.id === requestedFocusId)
260
- ?? nodes.find((node) => node.kind === 'target');
261
- const groupLabels = new Map();
262
- for (const node of nodes) {
263
- if (node.id !== focusNode?.id && !groupLabels.has(node.group))
264
- groupLabels.set(node.group, node.groupLabel);
265
- }
266
- const groupIds = [...groupLabels.keys()].sort((left, right) => {
267
- const leftLabel = groupLabels.get(left) ?? left;
268
- const rightLabel = groupLabels.get(right) ?? right;
269
- return leftLabel.localeCompare(rightLabel) || left.localeCompare(right);
270
- });
271
- const height = Math.max(500, Math.min(700, 450 + Math.max(0, nodes.length - 14) * 16));
272
- const width = 900;
273
- const centerX = width / 2;
274
- const centerY = height / 2;
275
- const clusterCenters = forceGraphClusterCenters(groupIds, width, height, Boolean(focusNode));
276
- const clusterIndex = new Map(groupIds.map((group) => [group, forceGraphClusterIndex(group)]));
277
- for (const node of nodes)
278
- node.clusterIndex = clusterIndex.get(node.group) ?? 0;
279
- if (nodes.length) {
280
- if (focusNode) {
281
- focusNode.fx = centerX;
282
- focusNode.fy = centerY;
283
- }
284
- const simulation = forceSimulation(nodes)
285
- .force('link', forceLink(links)
286
- .id((node) => node.id)
287
- .distance((link) => link.kind === 'derived' ? 145 : 125)
288
- .strength(0.22))
289
- .force('charge', forceManyBody().strength(-330))
290
- .force('center', forceCenter(centerX, centerY))
291
- .force('cluster-x', forceX((node) => node.id === focusNode?.id ? centerX : clusterCenters.get(node.group)?.x ?? centerX).strength(0.3))
292
- .force('cluster-y', forceY((node) => node.id === focusNode?.id ? centerY : clusterCenters.get(node.group)?.y ?? centerY).strength(0.3))
293
- .force('collide', forceCollide()
294
- .radius((node) => node.kind === 'target' ? Math.max(66, node.collisionRadius) : node.collisionRadius)
295
- .strength(1)
296
- .iterations(5))
297
- .stop();
298
- simulation.tick(360);
299
- for (const node of nodes) {
300
- const horizontalInset = Math.max(38, Math.min(82, node.collisionRadius));
301
- const topInset = node.kind === 'target' ? 28 : 34;
302
- const bottomInset = Math.max(52, 34 + node.labelLines.length * 13);
303
- node.x = Math.max(horizontalInset, Math.min(width - horizontalInset, Number(node.x) || centerX));
304
- node.y = Math.max(topInset, Math.min(height - bottomInset, Number(node.y) || centerY));
305
- }
306
- resolveForceGraphLabelCollisions(nodes, width, height, focusNode?.id);
307
- }
308
- const projectedLinks = links.flatMap((link) => {
309
- const source = typeof link.source === 'string' ? nodes.find((node) => node.id === link.source) : link.source;
310
- const target = typeof link.target === 'string' ? nodes.find((node) => node.id === link.target) : link.target;
311
- return source && target ? [{
312
- id: link.id,
313
- sourceId: source.id,
314
- targetId: target.id,
315
- sourceX: source.x,
316
- sourceY: source.y,
317
- targetX: target.x,
318
- targetY: target.y,
319
- kind: link.kind,
320
- detail: link.detail,
321
- }] : [];
322
- });
323
- const omittedNodeInputs = Math.max(0, rawNodeCount - nodes.length);
324
- const omittedLinkInputs = Math.max(0, rawLinkCount - projectedLinks.length);
325
- return {
326
- width,
327
- height,
328
- nodes: nodes.map(({ id, label, labelLines, labelWidth, kind, detail, group, groupLabel, clusterIndex, x, y }) => ({
329
- id,
330
- label,
331
- labelLines,
332
- labelWidth,
333
- kind,
334
- detail,
335
- group,
336
- groupLabel,
337
- clusterIndex,
338
- x,
339
- y,
340
- })),
341
- links: projectedLinks,
342
- clusters: groupIds.map((group) => ({
343
- id: group,
344
- label: groupLabels.get(group) ?? group,
345
- count: nodes.filter((node) => node.id !== focusNode?.id && node.group === group).length,
346
- index: clusterIndex.get(group) ?? 0,
347
- })),
348
- omittedNodeInputs,
349
- omittedLinkInputs,
350
- truncated: omittedNodeInputs > 0 || omittedLinkInputs > 0,
351
- };
352
- }
@@ -1,88 +0,0 @@
1
- import { scaleBand } from 'd3-scale';
2
- import { boundedVisualizationId as boundedId, boundedVisualizationText as boundedText, } from "./visualization-bounds.js";
3
- export const MAX_VISUAL_MATRIX_ROWS = 24;
4
- export const MAX_VISUAL_MATRIX_COLUMNS = 6;
5
- function normalizedMatrixState(value) {
6
- const state = boundedText(value, 30).toLowerCase();
7
- if (state === 'equal' || state === 'equivalent' || state === 'same')
8
- return 'equal';
9
- if (state === 'different')
10
- return 'different';
11
- if (state === 'conflict' || state === 'conflicting')
12
- return 'conflict';
13
- if (state === 'observed' || state === 'complete' || state === 'success')
14
- return 'observed';
15
- if (state === 'partial' || state === 'inconclusive' || state === 'rate_limited' || state === 'missing')
16
- return 'partial';
17
- if (state === 'unavailable' || state === 'error' || state === 'failed')
18
- return 'unavailable';
19
- if (state === 'not_collected' || state === 'skipped' || state === 'disabled' || state === 'not_recorded') {
20
- return 'not_collected';
21
- }
22
- return 'unknown';
23
- }
24
- export function projectEvidenceMatrix(rawColumns, rawRows) {
25
- const columns = [...new Set((Array.isArray(rawColumns) ? rawColumns : [])
26
- .map((column) => boundedText(column, 40))
27
- .filter(Boolean))]
28
- .slice(0, MAX_VISUAL_MATRIX_COLUMNS);
29
- const columnSet = new Set(columns);
30
- const seenRows = new Set();
31
- const candidates = (Array.isArray(rawRows) ? rawRows : [])
32
- .map((row, index) => {
33
- const id = boundedId(row?.id) || `row-${index}`;
34
- const label = boundedText(row?.label, 56);
35
- if (!label || seenRows.has(id))
36
- return null;
37
- seenRows.add(id);
38
- const rowCells = (Array.isArray(row.cells) ? row.cells : []);
39
- const byColumn = new Map(rowCells
40
- .map((cell) => {
41
- const column = boundedText(cell?.column, 40);
42
- return [column, {
43
- state: normalizedMatrixState(cell?.state),
44
- detail: boundedText(cell?.detail, 120),
45
- }];
46
- })
47
- .filter(([column]) => columnSet.has(column)));
48
- return {
49
- id,
50
- label,
51
- cells: columns.map((column) => ({
52
- column,
53
- state: byColumn.get(column)?.state ?? 'not_collected',
54
- detail: byColumn.get(column)?.detail ?? '',
55
- })),
56
- };
57
- })
58
- .filter((row) => Boolean(row));
59
- const rows = candidates.slice(0, MAX_VISUAL_MATRIX_ROWS);
60
- const width = 900;
61
- const top = 54;
62
- const left = 210;
63
- const rowHeight = 30;
64
- const x = scaleBand().domain(columns).range([left, 870]).padding(0.12);
65
- const y = scaleBand().domain(rows.map((row) => row.id))
66
- .range([top, top + Math.max(1, rows.length) * rowHeight])
67
- .padding(0.12);
68
- return {
69
- width,
70
- height: Math.max(104, top + rows.length * rowHeight + 22),
71
- columns: columns.map((column) => ({
72
- label: column,
73
- x: x(column) ?? left,
74
- width: x.bandwidth(),
75
- })),
76
- rows: rows.map((row) => ({
77
- ...row,
78
- y: y(row.id) ?? top,
79
- height: y.bandwidth(),
80
- cells: row.cells.map((cell) => ({
81
- ...cell,
82
- x: x(cell.column) ?? left,
83
- width: x.bandwidth(),
84
- })),
85
- })),
86
- truncated: candidates.length > rows.length || rawColumns.length > columns.length,
87
- };
88
- }