@slicedearth/whoisleuth-cli 1.47.4 → 2.0.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.
- package/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/project-metadata.mjs +1 -1
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +13 -5
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
- package/third-party-notices.txt +14 -14
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -255
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -1,131 +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
|
-
// A row is one source-pair lane, so it can publish at most one cell for each
|
|
6
|
-
// allowed column. This keeps output at 24 × 6 = 144 cells and prevents hostile
|
|
7
|
-
// duplicate inputs from creating sub-pixel marks or unbounded cell arrays.
|
|
8
|
-
export const MAX_VISUAL_MATRIX_CELLS = MAX_VISUAL_MATRIX_ROWS * MAX_VISUAL_MATRIX_COLUMNS;
|
|
9
|
-
const MAX_VISUAL_MATRIX_COLUMN_INPUTS = MAX_VISUAL_MATRIX_COLUMNS * 4;
|
|
10
|
-
const MAX_VISUAL_MATRIX_ROW_INPUTS = MAX_VISUAL_MATRIX_ROWS * 4;
|
|
11
|
-
const MAX_VISUAL_MATRIX_CELL_INPUTS_PER_ROW = MAX_VISUAL_MATRIX_COLUMNS * 4;
|
|
12
|
-
function normalizedMatrixTone(value) {
|
|
13
|
-
const state = boundedText(value, 30).toLowerCase();
|
|
14
|
-
if (state === 'equal' || state === 'equivalent' || state === 'same')
|
|
15
|
-
return 'equal';
|
|
16
|
-
if (state === 'different')
|
|
17
|
-
return 'different';
|
|
18
|
-
if (state === 'conflict' || state === 'conflicting')
|
|
19
|
-
return 'conflict';
|
|
20
|
-
if (state === 'observed' || state === 'complete' || state === 'success')
|
|
21
|
-
return 'observed';
|
|
22
|
-
if (state === 'partial' || state === 'inconclusive' || state === 'rate_limited' || state === 'missing')
|
|
23
|
-
return 'partial';
|
|
24
|
-
if (state === 'unavailable' || state === 'error' || state === 'failed')
|
|
25
|
-
return 'unavailable';
|
|
26
|
-
if (state === 'not_collected' || state === 'skipped' || state === 'disabled' || state === 'not_recorded') {
|
|
27
|
-
return 'not_collected';
|
|
28
|
-
}
|
|
29
|
-
return 'unknown';
|
|
30
|
-
}
|
|
31
|
-
export function projectEvidenceMatrix(rawColumns, rawRows) {
|
|
32
|
-
const columnInputs = Array.isArray(rawColumns) ? rawColumns : [];
|
|
33
|
-
const columns = [];
|
|
34
|
-
const seenColumns = new Set();
|
|
35
|
-
for (const rawColumn of columnInputs.slice(0, MAX_VISUAL_MATRIX_COLUMN_INPUTS)) {
|
|
36
|
-
const column = boundedText(rawColumn, 40);
|
|
37
|
-
if (!column || seenColumns.has(column) || columns.length >= MAX_VISUAL_MATRIX_COLUMNS)
|
|
38
|
-
continue;
|
|
39
|
-
seenColumns.add(column);
|
|
40
|
-
columns.push(column);
|
|
41
|
-
}
|
|
42
|
-
const columnSet = new Set(columns);
|
|
43
|
-
const seenRows = new Set();
|
|
44
|
-
const rowInputs = Array.isArray(rawRows) ? rawRows : [];
|
|
45
|
-
const rows = [];
|
|
46
|
-
let discardedCellInputs = 0;
|
|
47
|
-
const scannedRows = rowInputs.slice(0, MAX_VISUAL_MATRIX_ROW_INPUTS);
|
|
48
|
-
for (const [index, row] of scannedRows.entries()) {
|
|
49
|
-
if (rows.length >= MAX_VISUAL_MATRIX_ROWS)
|
|
50
|
-
break;
|
|
51
|
-
const id = boundedId(row?.id) || `row-${index}`;
|
|
52
|
-
const label = boundedText(row?.label, 56);
|
|
53
|
-
if (!label || seenRows.has(id))
|
|
54
|
-
continue;
|
|
55
|
-
seenRows.add(id);
|
|
56
|
-
const rowCells = (Array.isArray(row.cells) ? row.cells : []);
|
|
57
|
-
const scannedCells = rowCells.slice(0, MAX_VISUAL_MATRIX_CELL_INPUTS_PER_ROW);
|
|
58
|
-
discardedCellInputs += Math.max(0, rowCells.length - scannedCells.length);
|
|
59
|
-
const byColumn = new Map();
|
|
60
|
-
for (const cell of scannedCells) {
|
|
61
|
-
const column = boundedText(cell?.column, 40);
|
|
62
|
-
if (!columnSet.has(column) || byColumn.has(column)) {
|
|
63
|
-
discardedCellInputs += 1;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
const exactState = boundedText(cell?.state, 40).toLowerCase() || 'unknown';
|
|
67
|
-
byColumn.set(column, {
|
|
68
|
-
column,
|
|
69
|
-
state: exactState,
|
|
70
|
-
tone: normalizedMatrixTone(cell?.tone ?? exactState),
|
|
71
|
-
detail: boundedText(cell?.detail, 320),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
const cells = columns.flatMap((column) => {
|
|
75
|
-
const value = byColumn.get(column);
|
|
76
|
-
if (value)
|
|
77
|
-
return [value];
|
|
78
|
-
if (row.sparse === true)
|
|
79
|
-
return [];
|
|
80
|
-
return [{
|
|
81
|
-
column,
|
|
82
|
-
state: 'not_collected',
|
|
83
|
-
tone: 'not_collected',
|
|
84
|
-
detail: '',
|
|
85
|
-
}];
|
|
86
|
-
});
|
|
87
|
-
rows.push({
|
|
88
|
-
id,
|
|
89
|
-
label,
|
|
90
|
-
context: boundedText(row.context, 90),
|
|
91
|
-
status: boundedText(row.status, 40).toLowerCase() || 'unknown',
|
|
92
|
-
assessment: boundedText(row.assessment, 80),
|
|
93
|
-
cells,
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
const discarded = {
|
|
97
|
-
columnInputs: Math.max(0, columnInputs.length - columns.length),
|
|
98
|
-
rowInputs: Math.max(0, rowInputs.length - rows.length),
|
|
99
|
-
cellInputs: discardedCellInputs,
|
|
100
|
-
};
|
|
101
|
-
const width = 900;
|
|
102
|
-
const top = 54;
|
|
103
|
-
const left = 250;
|
|
104
|
-
const rowHeight = 40;
|
|
105
|
-
const x = scaleBand().domain(columns).range([left, 870]).padding(0.12);
|
|
106
|
-
const y = scaleBand().domain(rows.map((row) => row.id))
|
|
107
|
-
.range([top, top + Math.max(1, rows.length) * rowHeight])
|
|
108
|
-
.padding(0.12);
|
|
109
|
-
return {
|
|
110
|
-
width,
|
|
111
|
-
height: Math.max(104, top + rows.length * rowHeight + 22),
|
|
112
|
-
columns: columns.map((column) => ({
|
|
113
|
-
label: column,
|
|
114
|
-
x: x(column) ?? left,
|
|
115
|
-
width: x.bandwidth(),
|
|
116
|
-
})),
|
|
117
|
-
rows: rows.map((row) => ({
|
|
118
|
-
...row,
|
|
119
|
-
y: y(row.id) ?? top,
|
|
120
|
-
height: y.bandwidth(),
|
|
121
|
-
cells: row.cells.map((cell) => ({
|
|
122
|
-
...cell,
|
|
123
|
-
x: x(cell.column) ?? left,
|
|
124
|
-
width: x.bandwidth(),
|
|
125
|
-
})),
|
|
126
|
-
})),
|
|
127
|
-
discarded,
|
|
128
|
-
projectedCellCount: rows.reduce((sum, row) => sum + row.cells.length, 0),
|
|
129
|
-
truncated: discarded.columnInputs > 0 || discarded.rowInputs > 0 || discarded.cellInputs > 0,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
import { scaleBand, scaleLinear, scalePoint } from 'd3-scale';
|
|
2
|
-
import { boundedVisualizationId as boundedId, boundedVisualizationNumber as boundedNumber, boundedVisualizationText as boundedText, validVisualizationDate as validDate, } from "./visualization-bounds.js";
|
|
3
|
-
export { FORCE_GRAPH_LINK_KINDS, MAX_FORCE_GRAPH_LINKS, MAX_FORCE_GRAPH_NODES, projectBoundedForceGraph, } from "./visualization-force-graph.js";
|
|
4
|
-
export { MAX_VISUAL_MATRIX_CELLS, MAX_VISUAL_MATRIX_COLUMNS, MAX_VISUAL_MATRIX_ROWS, projectEvidenceMatrix, } from "./visualization-matrix.js";
|
|
5
|
-
export { MAX_COLLECTION_TIMING_SOURCES, MAX_LIFECYCLE_EVENTS, MAX_TREND_POINTS, projectCollectionTiming, projectLifecycleEvents, projectTrendPoints, } from "./visualization-time-series.js";
|
|
6
|
-
export const MAX_REDIRECT_NODES = 9;
|
|
7
|
-
export const WATCHLIST_ACTIVITY_DAYS = 28;
|
|
8
|
-
export const MAX_SCORE_FACTORS = 16;
|
|
9
|
-
export const MAX_SCORE_FACTOR_LABEL_LENGTH = 160;
|
|
10
|
-
export const MAX_PROFILE_LISTING_BAR_GROUPS = 18;
|
|
11
|
-
export const MAX_MONITOR_TIMELINE_EVENTS = 12;
|
|
12
|
-
export const MAX_MONITOR_TIMELINE_LANES = 6;
|
|
13
|
-
function redirectLabel(value) {
|
|
14
|
-
const text = boundedText(value, 320);
|
|
15
|
-
try {
|
|
16
|
-
const parsed = new URL(text);
|
|
17
|
-
const path = parsed.pathname === '/' ? '' : parsed.pathname;
|
|
18
|
-
return `${parsed.hostname}${path}`.slice(0, 72);
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return text.slice(0, 72);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
export function projectRedirectPath(rawRedirects) {
|
|
25
|
-
const redirects = (Array.isArray(rawRedirects) ? rawRedirects : [])
|
|
26
|
-
.slice(0, MAX_REDIRECT_NODES - 1)
|
|
27
|
-
.map((redirect) => ({
|
|
28
|
-
status: boundedText(redirect?.status, 12) || '—',
|
|
29
|
-
from: redirectLabel(redirect?.from),
|
|
30
|
-
to: redirectLabel(redirect?.to),
|
|
31
|
-
queryOmitted: Boolean(redirect?.queryOmitted),
|
|
32
|
-
}))
|
|
33
|
-
.filter((redirect) => redirect.from && redirect.to);
|
|
34
|
-
const rawCount = Array.isArray(rawRedirects) ? rawRedirects.length : 0;
|
|
35
|
-
if (!redirects.length) {
|
|
36
|
-
return { width: 900, height: 150, nodes: [], edges: [], truncated: rawCount > 0 };
|
|
37
|
-
}
|
|
38
|
-
const firstRedirect = redirects[0];
|
|
39
|
-
if (!firstRedirect) {
|
|
40
|
-
return { width: 900, height: 150, nodes: [], edges: [], truncated: rawCount > 0 };
|
|
41
|
-
}
|
|
42
|
-
const nodes = [
|
|
43
|
-
{ id: 'redirect-start', label: firstRedirect.from, status: 'start', queryOmitted: false },
|
|
44
|
-
...redirects.map((redirect, index) => ({
|
|
45
|
-
id: `redirect-${index + 1}`,
|
|
46
|
-
label: redirect.to,
|
|
47
|
-
status: redirect.status,
|
|
48
|
-
queryOmitted: redirect.queryOmitted,
|
|
49
|
-
})),
|
|
50
|
-
];
|
|
51
|
-
const x = scalePoint().domain(nodes.map((node) => node.id)).range([62, 838]).padding(0.35);
|
|
52
|
-
const projected = nodes.map((node) => ({ ...node, x: x(node.id) ?? 450, y: 70 }));
|
|
53
|
-
const edges = redirects.flatMap((redirect, index) => {
|
|
54
|
-
const from = projected[index];
|
|
55
|
-
const to = projected[index + 1];
|
|
56
|
-
return from && to ? [{
|
|
57
|
-
id: `redirect-edge-${index}`,
|
|
58
|
-
fromX: from.x,
|
|
59
|
-
toX: to.x,
|
|
60
|
-
y: 70,
|
|
61
|
-
status: redirect.status,
|
|
62
|
-
}] : [];
|
|
63
|
-
});
|
|
64
|
-
return {
|
|
65
|
-
width: 900,
|
|
66
|
-
height: 150,
|
|
67
|
-
nodes: projected,
|
|
68
|
-
edges,
|
|
69
|
-
truncated: rawCount > redirects.length,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
function utcDayKey(milliseconds) {
|
|
73
|
-
return new Date(milliseconds).toISOString().slice(0, 10);
|
|
74
|
-
}
|
|
75
|
-
function utcWeekLabel(startMilliseconds, endMilliseconds) {
|
|
76
|
-
const start = new Date(startMilliseconds);
|
|
77
|
-
const end = new Date(endMilliseconds);
|
|
78
|
-
const startDay = start.getUTCDate();
|
|
79
|
-
const endDay = end.getUTCDate();
|
|
80
|
-
const startMonth = start.toLocaleDateString('en-AU', { month: 'short', timeZone: 'UTC' });
|
|
81
|
-
const endMonth = end.toLocaleDateString('en-AU', { month: 'short', timeZone: 'UTC' });
|
|
82
|
-
const endYear = end.getUTCFullYear();
|
|
83
|
-
return startMonth === endMonth
|
|
84
|
-
? `${startDay}–${endDay} ${endMonth} ${endYear}`
|
|
85
|
-
: `${startDay} ${startMonth}–${endDay} ${endMonth} ${endYear}`;
|
|
86
|
-
}
|
|
87
|
-
export function projectWatchlistActivity(rawEvents) {
|
|
88
|
-
const valid = (Array.isArray(rawEvents) ? rawEvents : [])
|
|
89
|
-
.map((event) => {
|
|
90
|
-
const date = validDate(event?.checkedAt);
|
|
91
|
-
if (!date)
|
|
92
|
-
return null;
|
|
93
|
-
const changeCount = Math.max(0, Math.min(10_000, Math.trunc(Number(event.changeCount) || 0)));
|
|
94
|
-
const resultCount = Math.max(0, Math.min(10_000, Math.trunc(Number(event.resultCount) || 0)));
|
|
95
|
-
const conclusiveCount = Math.max(0, Math.min(resultCount, Math.trunc(Number(event.conclusiveCount) || 0)));
|
|
96
|
-
return { ...date, changeCount, resultCount, conclusiveCount };
|
|
97
|
-
})
|
|
98
|
-
.filter((event) => Boolean(event));
|
|
99
|
-
if (!valid.length) {
|
|
100
|
-
return {
|
|
101
|
-
width: 660,
|
|
102
|
-
height: 235,
|
|
103
|
-
days: [],
|
|
104
|
-
weekdayLabels: [],
|
|
105
|
-
weekLabels: [],
|
|
106
|
-
timeBasis: 'UTC calendar days',
|
|
107
|
-
windowStart: null,
|
|
108
|
-
windowEnd: null,
|
|
109
|
-
maxChanges: 0,
|
|
110
|
-
totalChecks: 0,
|
|
111
|
-
totalChanges: 0,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
const latest = Math.max(...valid.map((event) => event.milliseconds));
|
|
115
|
-
const latestDay = Date.UTC(new Date(latest).getUTCFullYear(), new Date(latest).getUTCMonth(), new Date(latest).getUTCDate());
|
|
116
|
-
const start = latestDay - (WATCHLIST_ACTIVITY_DAYS - 1) * 86_400_000;
|
|
117
|
-
const windowEnd = latestDay + 86_400_000;
|
|
118
|
-
const windowEvents = valid.filter((event) => event.milliseconds >= start && event.milliseconds < windowEnd);
|
|
119
|
-
const byDay = new Map();
|
|
120
|
-
for (const event of windowEvents) {
|
|
121
|
-
const key = utcDayKey(event.milliseconds);
|
|
122
|
-
const current = byDay.get(key) ?? { checks: 0, changes: 0, results: 0, conclusive: 0 };
|
|
123
|
-
current.checks += 1;
|
|
124
|
-
current.changes += event.changeCount;
|
|
125
|
-
current.results += event.resultCount;
|
|
126
|
-
current.conclusive += event.conclusiveCount;
|
|
127
|
-
byDay.set(key, current);
|
|
128
|
-
}
|
|
129
|
-
const weekScale = scaleBand().domain([0, 1, 2, 3]).range([92, 638]).padding(0.12);
|
|
130
|
-
const dayScale = scaleBand().domain([0, 1, 2, 3, 4, 5, 6]).range([42, 190]).padding(0.12);
|
|
131
|
-
const dayWidth = weekScale.bandwidth();
|
|
132
|
-
const dayHeight = dayScale.bandwidth();
|
|
133
|
-
const days = Array.from({ length: WATCHLIST_ACTIVITY_DAYS }, (_, index) => {
|
|
134
|
-
const milliseconds = start + index * 86_400_000;
|
|
135
|
-
const key = utcDayKey(milliseconds);
|
|
136
|
-
const activity = byDay.get(key) ?? { checks: 0, changes: 0, results: 0, conclusive: 0 };
|
|
137
|
-
return {
|
|
138
|
-
date: key,
|
|
139
|
-
label: new Date(milliseconds).toLocaleDateString('en-AU', { day: 'numeric', month: 'short', timeZone: 'UTC' }),
|
|
140
|
-
week: Math.floor(index / 7),
|
|
141
|
-
weekday: index % 7,
|
|
142
|
-
x: weekScale(Math.floor(index / 7)) ?? 0,
|
|
143
|
-
y: dayScale(index % 7) ?? 0,
|
|
144
|
-
width: dayWidth,
|
|
145
|
-
height: dayHeight,
|
|
146
|
-
...activity,
|
|
147
|
-
};
|
|
148
|
-
});
|
|
149
|
-
const weekdayLabels = Array.from({ length: 7 }, (_, index) => ({
|
|
150
|
-
index,
|
|
151
|
-
label: new Date(start + index * 86_400_000).toLocaleDateString('en-AU', { weekday: 'short', timeZone: 'UTC' }),
|
|
152
|
-
y: (dayScale(index) ?? 0) + dayHeight / 2 + 3,
|
|
153
|
-
}));
|
|
154
|
-
const weekLabels = Array.from({ length: 4 }, (_, index) => {
|
|
155
|
-
const weekStart = start + index * 7 * 86_400_000;
|
|
156
|
-
const weekEnd = weekStart + 6 * 86_400_000;
|
|
157
|
-
return {
|
|
158
|
-
index,
|
|
159
|
-
label: utcWeekLabel(weekStart, weekEnd),
|
|
160
|
-
startDate: utcDayKey(weekStart),
|
|
161
|
-
endDate: utcDayKey(weekEnd),
|
|
162
|
-
x: (weekScale(index) ?? 0) + dayWidth / 2,
|
|
163
|
-
};
|
|
164
|
-
});
|
|
165
|
-
return {
|
|
166
|
-
width: 660,
|
|
167
|
-
height: 235,
|
|
168
|
-
days,
|
|
169
|
-
weekdayLabels,
|
|
170
|
-
weekLabels,
|
|
171
|
-
timeBasis: 'UTC calendar days',
|
|
172
|
-
windowStart: utcDayKey(start),
|
|
173
|
-
windowEnd: utcDayKey(latestDay),
|
|
174
|
-
maxChanges: Math.max(0, ...days.map((day) => day.changes)),
|
|
175
|
-
totalChecks: windowEvents.length,
|
|
176
|
-
totalChanges: windowEvents.reduce((sum, event) => sum + event.changeCount, 0),
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
export function projectScoreFactors(rawFactors) {
|
|
180
|
-
const candidates = (Array.isArray(rawFactors) ? rawFactors : [])
|
|
181
|
-
.map((factor, index) => ({
|
|
182
|
-
id: `${boundedId(factor?.label) || 'factor'}-${index}`,
|
|
183
|
-
label: boundedText(factor?.label, MAX_SCORE_FACTOR_LABEL_LENGTH) || `Factor ${index + 1}`,
|
|
184
|
-
delta: boundedNumber(factor?.delta, -100, 100),
|
|
185
|
-
}));
|
|
186
|
-
const factors = candidates.slice(0, MAX_SCORE_FACTORS);
|
|
187
|
-
const maximum = Math.max(1, ...factors.map((factor) => Math.abs(factor.delta)));
|
|
188
|
-
const x = scaleLinear().domain([-maximum, maximum]).range([220, 860]).clamp(true);
|
|
189
|
-
const zeroX = x(0);
|
|
190
|
-
const rowHeight = 28;
|
|
191
|
-
return {
|
|
192
|
-
width: 900,
|
|
193
|
-
height: Math.max(74, 42 + factors.length * rowHeight),
|
|
194
|
-
zeroX,
|
|
195
|
-
maximum,
|
|
196
|
-
factors: factors.map((factor, index) => {
|
|
197
|
-
const valueX = x(factor.delta);
|
|
198
|
-
return {
|
|
199
|
-
...factor,
|
|
200
|
-
x: Math.min(zeroX, valueX),
|
|
201
|
-
width: factor.delta === 0 ? 0 : Math.max(2, Math.abs(valueX - zeroX)),
|
|
202
|
-
y: 24 + index * rowHeight,
|
|
203
|
-
};
|
|
204
|
-
}),
|
|
205
|
-
truncated: candidates.length > factors.length,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
export function projectProfileListingBars(rawGroups) {
|
|
209
|
-
const candidates = (Array.isArray(rawGroups) ? rawGroups : [])
|
|
210
|
-
.map((group, index) => {
|
|
211
|
-
const outcomes = {
|
|
212
|
-
registered: Math.trunc(boundedNumber(group?.registered, 0, 100_000)),
|
|
213
|
-
available: Math.trunc(boundedNumber(group?.available, 0, 100_000)),
|
|
214
|
-
unknown: Math.trunc(boundedNumber(group?.unknown, 0, 100_000)),
|
|
215
|
-
};
|
|
216
|
-
const total = Object.values(outcomes).reduce((sum, value) => sum + value, 0);
|
|
217
|
-
return {
|
|
218
|
-
id: boundedId(group?.id) || `group-${index}`,
|
|
219
|
-
label: boundedText(group?.label, 56) || `Group ${index + 1}`,
|
|
220
|
-
...outcomes,
|
|
221
|
-
total,
|
|
222
|
-
profileListed: Math.min(total, Math.trunc(boundedNumber(group?.profileListed, 0, 100_000))),
|
|
223
|
-
};
|
|
224
|
-
})
|
|
225
|
-
.filter((group) => group.total > 0)
|
|
226
|
-
.sort((a, b) => b.total - a.total || a.label.localeCompare(b.label));
|
|
227
|
-
const groups = candidates.slice(0, MAX_PROFILE_LISTING_BAR_GROUPS);
|
|
228
|
-
const maximum = Math.max(1, ...groups.map((group) => group.total));
|
|
229
|
-
const x = scaleLinear().domain([0, maximum]).range([210, 860]).clamp(true);
|
|
230
|
-
const rowHeight = 40;
|
|
231
|
-
return {
|
|
232
|
-
width: 900,
|
|
233
|
-
height: Math.max(76, 40 + groups.length * rowHeight),
|
|
234
|
-
groups: groups.map((group, index) => {
|
|
235
|
-
let offset = 0;
|
|
236
|
-
const segments = ['registered', 'available', 'unknown'].map((state) => {
|
|
237
|
-
const value = group[state];
|
|
238
|
-
const start = offset;
|
|
239
|
-
offset += value;
|
|
240
|
-
return {
|
|
241
|
-
state,
|
|
242
|
-
value,
|
|
243
|
-
x: x(start),
|
|
244
|
-
width: Math.max(value ? 1 : 0, x(offset) - x(start)),
|
|
245
|
-
};
|
|
246
|
-
});
|
|
247
|
-
return { ...group, y: 20 + index * rowHeight, segments };
|
|
248
|
-
}),
|
|
249
|
-
truncated: candidates.length > groups.length,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
export function projectMonitorTimeline(rawEvents) {
|
|
253
|
-
const candidates = (Array.isArray(rawEvents) ? rawEvents : [])
|
|
254
|
-
.map((event, index) => {
|
|
255
|
-
const date = validDate(event?.checkedAt);
|
|
256
|
-
const id = boundedId(event?.id) || `event-${index}`;
|
|
257
|
-
if (!date)
|
|
258
|
-
return null;
|
|
259
|
-
return {
|
|
260
|
-
id,
|
|
261
|
-
checkedAt: date.text,
|
|
262
|
-
milliseconds: date.milliseconds,
|
|
263
|
-
mode: boundedText(event?.mode, 20).toLowerCase() || 'saved',
|
|
264
|
-
groups: (Array.isArray(event.groups) ? event.groups : [])
|
|
265
|
-
.map((group) => ({
|
|
266
|
-
key: boundedId(group?.key),
|
|
267
|
-
label: boundedText(group?.label, 40),
|
|
268
|
-
changeCount: Math.trunc(boundedNumber(group?.changeCount, 0, 10_000)),
|
|
269
|
-
}))
|
|
270
|
-
.filter((group) => group.key && group.label),
|
|
271
|
-
};
|
|
272
|
-
})
|
|
273
|
-
.filter((event) => Boolean(event))
|
|
274
|
-
.sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id));
|
|
275
|
-
const events = candidates.slice(-MAX_MONITOR_TIMELINE_EVENTS);
|
|
276
|
-
const laneMap = new Map();
|
|
277
|
-
for (const event of events) {
|
|
278
|
-
for (const group of event.groups) {
|
|
279
|
-
if (!laneMap.has(group.key) && laneMap.size < MAX_MONITOR_TIMELINE_LANES) {
|
|
280
|
-
laneMap.set(group.key, group.label);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
const lanes = [...laneMap].map(([key, label]) => ({ key, label }));
|
|
285
|
-
const x = scaleBand().domain(events.map((event) => event.id)).range([170, 865]).padding(0.18);
|
|
286
|
-
const y = scaleBand().domain(lanes.map((lane) => lane.key)).range([42, 42 + lanes.length * 40]).padding(0.18);
|
|
287
|
-
const maxChanges = Math.max(1, ...events.flatMap((event) => event.groups.map((group) => group.changeCount)));
|
|
288
|
-
return {
|
|
289
|
-
width: 900,
|
|
290
|
-
height: Math.max(110, 64 + lanes.length * 40),
|
|
291
|
-
maxChanges,
|
|
292
|
-
events: events.map((event) => ({
|
|
293
|
-
...event,
|
|
294
|
-
x: x(event.id) ?? 170,
|
|
295
|
-
width: x.bandwidth(),
|
|
296
|
-
cells: lanes.map((lane) => ({
|
|
297
|
-
key: lane.key,
|
|
298
|
-
label: lane.label,
|
|
299
|
-
count: event.groups.find((group) => group.key === lane.key)?.changeCount ?? 0,
|
|
300
|
-
y: y(lane.key) ?? 42,
|
|
301
|
-
height: y.bandwidth(),
|
|
302
|
-
})),
|
|
303
|
-
})),
|
|
304
|
-
lanes: lanes.map((lane) => ({ ...lane, y: y(lane.key) ?? 42, height: y.bandwidth() })),
|
|
305
|
-
truncated: candidates.length > events.length
|
|
306
|
-
|| events.some((event) => event.groups.some((group) => !laneMap.has(group.key))),
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
export function projectCertificateValidity(input) {
|
|
310
|
-
const validFrom = validDate(input.validFrom);
|
|
311
|
-
const validTo = validDate(input.validTo);
|
|
312
|
-
const observed = validDate(input.observedAt);
|
|
313
|
-
if (!validFrom || !validTo || validTo.milliseconds <= validFrom.milliseconds) {
|
|
314
|
-
return { available: false, width: 900, height: 110 };
|
|
315
|
-
}
|
|
316
|
-
const observedMilliseconds = observed?.milliseconds ?? validFrom.milliseconds;
|
|
317
|
-
const span = validTo.milliseconds - validFrom.milliseconds;
|
|
318
|
-
const padding = Math.max(86_400_000, span * 0.08);
|
|
319
|
-
const x = scaleLinear()
|
|
320
|
-
.domain([validFrom.milliseconds - padding, validTo.milliseconds + padding])
|
|
321
|
-
.range([72, 828])
|
|
322
|
-
.clamp(true);
|
|
323
|
-
return {
|
|
324
|
-
available: true,
|
|
325
|
-
width: 900,
|
|
326
|
-
height: 110,
|
|
327
|
-
validFrom: validFrom.text,
|
|
328
|
-
validTo: validTo.text,
|
|
329
|
-
observedAt: observed?.text ?? '',
|
|
330
|
-
hasObservation: Boolean(observed),
|
|
331
|
-
fromX: x(validFrom.milliseconds),
|
|
332
|
-
toX: x(validTo.milliseconds),
|
|
333
|
-
observedX: x(observedMilliseconds),
|
|
334
|
-
observedWithinValidity: observedMilliseconds >= validFrom.milliseconds
|
|
335
|
-
&& observedMilliseconds <= validTo.milliseconds,
|
|
336
|
-
elapsedPercent: Math.max(0, Math.min(100, ((observedMilliseconds - validFrom.milliseconds) / span) * 100)),
|
|
337
|
-
};
|
|
338
|
-
}
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { scaleLinear, scalePoint } from 'd3-scale';
|
|
2
|
-
import { boundedVisualizationId as boundedId, boundedVisualizationNumber as boundedNumber, boundedVisualizationText as boundedText, validVisualizationDate as validDate, } from "./visualization-bounds.js";
|
|
3
|
-
export const MAX_LIFECYCLE_EVENTS = 8;
|
|
4
|
-
export const MAX_COLLECTION_TIMING_SOURCES = 16;
|
|
5
|
-
export const MAX_TREND_POINTS = 24;
|
|
6
|
-
export function projectLifecycleEvents(rawEvents) {
|
|
7
|
-
const seen = new Set();
|
|
8
|
-
const candidates = (Array.isArray(rawEvents) ? rawEvents : [])
|
|
9
|
-
.map((event) => {
|
|
10
|
-
const id = boundedId(event?.id);
|
|
11
|
-
const label = boundedText(event?.label, 40);
|
|
12
|
-
const date = validDate(event?.date);
|
|
13
|
-
if (!id || !label || !date)
|
|
14
|
-
return null;
|
|
15
|
-
const kind = event.kind === 'certificate' || event.kind === 'observation'
|
|
16
|
-
? event.kind
|
|
17
|
-
: 'registry';
|
|
18
|
-
return {
|
|
19
|
-
id,
|
|
20
|
-
label,
|
|
21
|
-
date: date.text,
|
|
22
|
-
milliseconds: date.milliseconds,
|
|
23
|
-
detail: boundedText(event.detail, 100),
|
|
24
|
-
kind,
|
|
25
|
-
};
|
|
26
|
-
})
|
|
27
|
-
.filter((event) => Boolean(event))
|
|
28
|
-
.sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id))
|
|
29
|
-
.filter((event) => {
|
|
30
|
-
if (seen.has(event.id))
|
|
31
|
-
return false;
|
|
32
|
-
seen.add(event.id);
|
|
33
|
-
return true;
|
|
34
|
-
});
|
|
35
|
-
const accepted = candidates.slice(0, MAX_LIFECYCLE_EVENTS);
|
|
36
|
-
const x = scalePoint()
|
|
37
|
-
.domain(accepted.map((event) => event.id))
|
|
38
|
-
.range([70, 830])
|
|
39
|
-
.padding(accepted.length > 1 ? 0.35 : 0.5);
|
|
40
|
-
const laneY = [48, 158, 27];
|
|
41
|
-
const events = accepted.map((event, index) => ({
|
|
42
|
-
...event,
|
|
43
|
-
x: x(event.id) ?? 450,
|
|
44
|
-
labelY: laneY[index % laneY.length] ?? 102,
|
|
45
|
-
anchor: index === 0 ? 'start' : index === accepted.length - 1 ? 'end' : 'middle',
|
|
46
|
-
}));
|
|
47
|
-
return {
|
|
48
|
-
width: 900,
|
|
49
|
-
height: 205,
|
|
50
|
-
axisY: 102,
|
|
51
|
-
events,
|
|
52
|
-
truncated: candidates.length > accepted.length,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
export function projectCollectionTiming(rawSources, rawTotalMs) {
|
|
56
|
-
const candidates = (Array.isArray(rawSources) ? rawSources : [])
|
|
57
|
-
.map((source, index) => {
|
|
58
|
-
const id = boundedId(source?.source) || `source-${index}`;
|
|
59
|
-
const durationMs = boundedNumber(source?.durationMs, 0, 300_000);
|
|
60
|
-
const completedAfterMs = boundedNumber(source?.completedAfterMs, 0, 300_000);
|
|
61
|
-
return {
|
|
62
|
-
id,
|
|
63
|
-
label: boundedText(source?.source, 48) || `Source ${index + 1}`,
|
|
64
|
-
outcome: boundedText(source?.outcome, 24).toLowerCase() || 'unknown',
|
|
65
|
-
durationMs,
|
|
66
|
-
completedAfterMs,
|
|
67
|
-
startedAfterMs: Math.max(0, completedAfterMs - durationMs),
|
|
68
|
-
};
|
|
69
|
-
})
|
|
70
|
-
.sort((a, b) => a.completedAfterMs - b.completedAfterMs || a.id.localeCompare(b.id));
|
|
71
|
-
const sources = candidates.slice(0, MAX_COLLECTION_TIMING_SOURCES);
|
|
72
|
-
const longestSource = [...sources]
|
|
73
|
-
.sort((a, b) => b.durationMs - a.durationMs
|
|
74
|
-
|| b.completedAfterMs - a.completedAfterMs
|
|
75
|
-
|| a.id.localeCompare(b.id))[0] ?? null;
|
|
76
|
-
const lastSettledSource = sources.at(-1) ?? null;
|
|
77
|
-
const requestErrorCount = sources.filter((source) => source.outcome === 'rejected').length;
|
|
78
|
-
const totalMs = Math.max(1, boundedNumber(rawTotalMs, 0, 300_000), ...sources.map((source) => source.completedAfterMs));
|
|
79
|
-
const x = scaleLinear().domain([0, totalMs]).range([190, 860]).clamp(true);
|
|
80
|
-
const rowHeight = 30;
|
|
81
|
-
return {
|
|
82
|
-
width: 900,
|
|
83
|
-
height: Math.max(92, 50 + sources.length * rowHeight),
|
|
84
|
-
totalMs,
|
|
85
|
-
longestSource,
|
|
86
|
-
lastSettledSource,
|
|
87
|
-
requestErrorCount,
|
|
88
|
-
ticks: x.ticks(5).map((value) => ({ value, x: x(value) })),
|
|
89
|
-
sources: sources.map((source, index) => ({
|
|
90
|
-
...source,
|
|
91
|
-
x: x(source.startedAfterMs),
|
|
92
|
-
width: Math.max(3, x(source.completedAfterMs) - x(source.startedAfterMs)),
|
|
93
|
-
y: 30 + index * rowHeight,
|
|
94
|
-
})),
|
|
95
|
-
truncated: candidates.length > sources.length,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
export function projectTrendPoints(rawPoints) {
|
|
99
|
-
const seen = new Set();
|
|
100
|
-
const candidates = (Array.isArray(rawPoints) ? rawPoints : [])
|
|
101
|
-
.map((point, index) => {
|
|
102
|
-
const date = validDate(point?.date);
|
|
103
|
-
const id = boundedId(point?.id) || `point-${index}`;
|
|
104
|
-
if (!date || seen.has(id))
|
|
105
|
-
return null;
|
|
106
|
-
seen.add(id);
|
|
107
|
-
return {
|
|
108
|
-
id,
|
|
109
|
-
date: date.text,
|
|
110
|
-
milliseconds: date.milliseconds,
|
|
111
|
-
total: Math.trunc(boundedNumber(point?.total, 0, 100_000)),
|
|
112
|
-
added: Math.trunc(boundedNumber(point?.added, 0, 100_000)),
|
|
113
|
-
partial: Boolean(point?.partial),
|
|
114
|
-
};
|
|
115
|
-
})
|
|
116
|
-
.filter((point) => Boolean(point))
|
|
117
|
-
.sort((a, b) => a.milliseconds - b.milliseconds || a.id.localeCompare(b.id));
|
|
118
|
-
const points = candidates.slice(-MAX_TREND_POINTS);
|
|
119
|
-
const firstMilliseconds = points[0]?.milliseconds ?? 0;
|
|
120
|
-
const latestMilliseconds = points.at(-1)?.milliseconds ?? firstMilliseconds;
|
|
121
|
-
const x = scaleLinear()
|
|
122
|
-
.domain(firstMilliseconds === latestMilliseconds
|
|
123
|
-
? [firstMilliseconds - 1, latestMilliseconds + 1]
|
|
124
|
-
: [firstMilliseconds, latestMilliseconds])
|
|
125
|
-
.range([64, 850])
|
|
126
|
-
.clamp(true);
|
|
127
|
-
const maximum = Math.max(1, ...points.map((point) => point.total));
|
|
128
|
-
const y = scaleLinear().domain([0, maximum]).range([190, 24]).nice().clamp(true);
|
|
129
|
-
const first = points[0] ?? null;
|
|
130
|
-
const latest = points.at(-1) ?? null;
|
|
131
|
-
const projectedPoints = points.map((point, sequenceIndex) => ({
|
|
132
|
-
...point,
|
|
133
|
-
sequenceIndex,
|
|
134
|
-
x: points.length > 1 ? x(point.milliseconds) : 450,
|
|
135
|
-
y: y(point.total),
|
|
136
|
-
addedY: y(point.added),
|
|
137
|
-
}));
|
|
138
|
-
// A capped point is a lower bound, so neither adjacent slope is measured.
|
|
139
|
-
// Retain only directly adjacent complete-to-complete segments and leave a
|
|
140
|
-
// visible gap around every capped point.
|
|
141
|
-
const segments = projectedPoints.slice(1).flatMap((to, index) => {
|
|
142
|
-
const from = projectedPoints[index];
|
|
143
|
-
return from && !from.partial && !to.partial ? [{
|
|
144
|
-
fromId: from.id,
|
|
145
|
-
toId: to.id,
|
|
146
|
-
x1: from.x,
|
|
147
|
-
y1: from.y,
|
|
148
|
-
x2: to.x,
|
|
149
|
-
y2: to.y,
|
|
150
|
-
}] : [];
|
|
151
|
-
});
|
|
152
|
-
const partialChecks = points.filter((point) => point.partial).length;
|
|
153
|
-
const hasPartialChecks = partialChecks > 0;
|
|
154
|
-
const summaryMetric = (value, lowerBound) => ({
|
|
155
|
-
value,
|
|
156
|
-
lowerBound,
|
|
157
|
-
label: lowerBound ? `At least ${value}` : String(value),
|
|
158
|
-
});
|
|
159
|
-
const peakTotal = points.reduce((peak, point) => Math.max(peak, point.total), 0);
|
|
160
|
-
const newlyObserved = points.reduce((total, point) => total + point.added, 0);
|
|
161
|
-
return {
|
|
162
|
-
width: 900,
|
|
163
|
-
height: 225,
|
|
164
|
-
maximum,
|
|
165
|
-
spacing: 'elapsed_time',
|
|
166
|
-
elapsed: {
|
|
167
|
-
firstAt: points[0]?.date ?? null,
|
|
168
|
-
latestAt: points.at(-1)?.date ?? null,
|
|
169
|
-
milliseconds: Math.max(0, latestMilliseconds - firstMilliseconds),
|
|
170
|
-
},
|
|
171
|
-
ticks: y.ticks(4).map((value) => ({ value, y: y(value) })),
|
|
172
|
-
points: projectedPoints,
|
|
173
|
-
segments,
|
|
174
|
-
summary: {
|
|
175
|
-
first: summaryMetric(first?.total ?? 0, Boolean(first?.partial)),
|
|
176
|
-
latest: summaryMetric(latest?.total ?? 0, Boolean(latest?.partial)),
|
|
177
|
-
// Any capped total can exceed the displayed observed maximum.
|
|
178
|
-
peak: summaryMetric(peakTotal, hasPartialChecks),
|
|
179
|
-
// Every capped new-count is itself a lower bound, including observed 0.
|
|
180
|
-
newlyObserved: summaryMetric(newlyObserved, hasPartialChecks),
|
|
181
|
-
partialChecks,
|
|
182
|
-
},
|
|
183
|
-
truncated: candidates.length > points.length,
|
|
184
|
-
};
|
|
185
|
-
}
|