@slicedearth/whoisleuth-cli 1.45.0 → 1.47.4
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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +59 -6
- package/bin/whoisleuth.mjs +5 -3
- package/cli/arguments.mjs +256 -9
- package/cli/artifact-structure.mjs +1833 -0
- package/cli/artifact-verify.mjs +242 -49
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +2 -3
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +16 -8
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-scan-command-runner.mjs +4 -5
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +21 -15
- package/cli/domain-control-observations.mjs +25 -2
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +73 -45
- package/cli/export-evidence.mjs +11 -4
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/terminal.mjs +3 -0
- package/cli/input.mjs +57 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-run.mjs +71 -23
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookup-command-runner.mjs +2 -2
- package/cli/lookup-diff.mjs +9 -0
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +28 -8
- package/cli/page-compare.mjs +8 -5
- package/cli/registry-cohort.mjs +385 -61
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +147 -185
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +444 -70
- package/docs/cli.md +24 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +29 -5
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
- package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
- package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
- package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
- package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
- package/frontend/src/lib/analysis/bulk-sort.js +4 -0
- package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +8 -0
- package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
- package/frontend/src/lib/analysis/case-migration-model.js +83 -31
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +42 -12
- package/frontend/src/lib/analysis/case-report.js +39 -6
- package/frontend/src/lib/analysis/case-response-model.js +66 -7
- package/frontend/src/lib/analysis/case-response-packet.js +15 -5
- package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
- package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
- package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
- package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
- package/frontend/src/lib/analysis/domain-control-records.js +88 -0
- package/frontend/src/lib/analysis/external-findings-import.js +70 -6
- package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
- package/frontend/src/lib/analysis/investigation-guide.js +52 -10
- package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
- package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
- package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +61 -6
- package/frontend/src/lib/analysis/lookup-presentation.js +62 -0
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +26 -3
- package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
- package/frontend/src/lib/analysis/visualization-models.js +63 -94
- package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
- package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
- package/frontend/src/lib/analysis/workspace-archive.js +104 -14
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +13 -0
- package/lib/bounded-json-response.mjs +17 -4
- package/lib/bounded-json.mjs +249 -0
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/cryptographic-assurance.mjs +155 -0
- package/lib/ct-search.mjs +87 -47
- package/lib/dns-intelligence.mjs +3 -2
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1053 -0
- package/lib/dnssec-evidence-validation.mjs +10 -3
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-manifest.mjs +17 -129
- package/lib/domain-posture.mjs +7 -4
- package/lib/evidence-export.mjs +751 -78
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/favicon.mjs +8 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +2 -5
- package/lib/interchange-fidelity-registry.mjs +122 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup-response-contract.mjs +691 -3
- package/lib/lookup.mjs +2 -2
- package/lib/observation.mjs +7 -6
- package/lib/outbound-identity.mjs +2 -3
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +21 -0
- package/lib/provider-policy-admission.mjs +9 -4
- package/lib/rdap-normalization.mjs +18 -5
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/risk-calibration-summary.mjs +338 -0
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +59 -10
- package/lib/security-txt.mjs +26 -3
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +784 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/threat-intelligence-runtime.mjs +3 -11
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
package/cli/registry-cohort.mjs
CHANGED
|
@@ -1,119 +1,443 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { canonicalArtifactJsonV2 } from "../frontend/src/lib/analysis/artifact-integrity.js";
|
|
3
|
+
import { scanBoundedJson } from "./bounded-json.mjs";
|
|
2
4
|
import { CliUsageError } from "./errors.mjs";
|
|
3
5
|
import { buildRegistryDoctorReport } from "./registry-doctor.mjs";
|
|
6
|
+
import { SAVED_LOOKUP_SCHEMA, parseSavedLookupDocument, } from "./saved-lookup.mjs";
|
|
4
7
|
export const REGISTRY_COHORT_SCHEMA = 'whoisleuth.cli.registry-cohort';
|
|
5
|
-
export const
|
|
8
|
+
export const LEGACY_REGISTRY_COHORT_VERSION = 1;
|
|
9
|
+
export const REGISTRY_COHORT_VERSION = 2;
|
|
6
10
|
export const MAX_REGISTRY_COHORT_INPUT_BYTES = 16 * 1024 * 1024;
|
|
7
11
|
export const MAX_REGISTRY_COHORT_SAMPLES = 500;
|
|
12
|
+
export const MAX_REGISTRY_COHORT_GROUPS = 500;
|
|
13
|
+
export const MAX_REGISTRY_COHORT_INPUT_POINTS = 2_000;
|
|
14
|
+
export const MAX_REGISTRY_COHORT_TIMELINE_POINTS = 50;
|
|
15
|
+
export const MAX_REGISTRY_COHORT_OMISSIONS = 1_000_000;
|
|
8
16
|
export const MIN_REGISTRY_COHORT_SAMPLE = 5;
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
const LEGACY_ROOT_KEYS = new Set(['schema', 'version', 'generatedAt', 'sampleCount', 'minimumCohortSample', 'cohorts', 'limitations']);
|
|
18
|
+
const LEGACY_COHORT_KEYS = new Set(['suffix', 'profileId', 'sampleCount', 'state', 'sourceAlignment', 'publication']);
|
|
19
|
+
const ROOT_KEYS = new Set(['schema', 'version', 'generatedAt', 'inputFamily', 'sampleCount', 'reportsMerged', 'minimumCohortSample', 'sampleWindow', 'cohorts', 'omissions', 'truncated', 'limitations']);
|
|
20
|
+
const COHORT_KEYS = new Set(['suffix', 'profileId', 'sampleCount', 'state', 'latestState', 'sampleWindow', 'sourceAlignment', 'publication', 'timeline', 'timelineOmitted']);
|
|
21
|
+
const POINT_KEYS = new Set(['reportGeneratedAt', 'sampleWindow', 'sampleCount', 'state', 'sourceAlignment', 'publication']);
|
|
22
|
+
const SOURCE_KEYS = new Set(['investigate', 'expectedConstraints', 'observed']);
|
|
23
|
+
const PUBLICATION_KEYS = new Set(['reviewItems', 'objectIdentifiersObserved', 'baseConformanceObserved', 'selfLinksObserved', 'redactionMetadataObserved']);
|
|
24
|
+
const WINDOW_KEYS = new Set(['from', 'to']);
|
|
25
|
+
const OMISSION_KEYS = new Set(['duplicateTimelinePoints', 'timelinePoints']);
|
|
26
|
+
const LIMITATIONS = Object.freeze([
|
|
27
|
+
'The report intentionally omits domains, queries and raw evidence. It groups retained observations only by suffix and capability profile.',
|
|
28
|
+
`Every timeline point retains its original state. Cohorts with fewer than ${MIN_REGISTRY_COHORT_SAMPLE} observations remain insufficient sample and are never promoted by summing overlapping reports.`,
|
|
29
|
+
'A merged cohort keeps the most conservative retained state and separately reports the latest point. Duplicate points are suppressed and inherited omission counts are not summed across overlapping retained reports.',
|
|
30
|
+
'Review fixture provenance and source health before changing a parser or registry access profile.',
|
|
31
|
+
]);
|
|
32
|
+
const LEGACY_LIMITATIONS = Object.freeze([
|
|
33
|
+
'The report intentionally omits domains, queries and raw evidence. It groups saved observations only by suffix and capability profile.',
|
|
34
|
+
`Cohorts with fewer than ${MIN_REGISTRY_COHORT_SAMPLE} observations remain insufficient sample and must not drive catalogue changes.`,
|
|
35
|
+
'Repeated observations from one environment are not representative by themselves. Review fixture provenance and source health before changing a parser or access profile.',
|
|
36
|
+
]);
|
|
37
|
+
function record(value, label) {
|
|
38
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
39
|
+
throw new CliUsageError(`${label} must be an object.`);
|
|
40
|
+
return value;
|
|
11
41
|
}
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
42
|
+
function exact(value, keys, label) {
|
|
43
|
+
const item = record(value, label);
|
|
44
|
+
if (Object.keys(item).some((key) => !keys.has(key)) || [...keys].some((key) => !Object.hasOwn(item, key))) {
|
|
45
|
+
throw new CliUsageError(`${label} does not match its exact retained contract.`);
|
|
46
|
+
}
|
|
47
|
+
return item;
|
|
48
|
+
}
|
|
49
|
+
function integer(value, minimum, maximum, label) {
|
|
50
|
+
if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum) {
|
|
51
|
+
throw new CliUsageError(`${label} is outside its supported bound.`);
|
|
52
|
+
}
|
|
53
|
+
return Number(value);
|
|
54
|
+
}
|
|
55
|
+
function timestamp(value, label) {
|
|
56
|
+
if (typeof value !== 'string' || value.length > 64 || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
57
|
+
throw new CliUsageError(`${label} is invalid.`);
|
|
58
|
+
}
|
|
59
|
+
const parsed = Date.parse(value);
|
|
60
|
+
if (!Number.isFinite(parsed))
|
|
61
|
+
throw new CliUsageError(`${label} is invalid.`);
|
|
62
|
+
return new Date(parsed).toISOString();
|
|
63
|
+
}
|
|
64
|
+
function identifier(value, label) {
|
|
65
|
+
if (typeof value !== 'string' || !/^[a-z0-9][a-z0-9_-]{0,119}$/u.test(value))
|
|
66
|
+
throw new CliUsageError(`${label} is invalid.`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function suffix(value) {
|
|
70
|
+
if (typeof value !== 'string' || !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(value))
|
|
71
|
+
throw new CliUsageError('Registry cohort suffix is invalid.');
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function state(value, label) {
|
|
75
|
+
if (value !== 'consistent' && value !== 'insufficient_sample' && value !== 'review')
|
|
76
|
+
throw new CliUsageError(`${label} is invalid.`);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
function window(value, label) {
|
|
80
|
+
const item = exact(value, WINDOW_KEYS, label);
|
|
81
|
+
const from = timestamp(item.from, `${label} from`);
|
|
82
|
+
const to = timestamp(item.to, `${label} to`);
|
|
83
|
+
if (from > to)
|
|
84
|
+
throw new CliUsageError(`${label} is reversed.`);
|
|
85
|
+
return Object.freeze({ from, to });
|
|
86
|
+
}
|
|
87
|
+
function sourceAlignment(value, sampleCount, label) {
|
|
88
|
+
const item = exact(value, SOURCE_KEYS, label);
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
investigate: integer(item.investigate, 0, sampleCount * 2, `${label} investigate`),
|
|
91
|
+
expectedConstraints: integer(item.expectedConstraints, 0, sampleCount * 2, `${label} expected constraints`),
|
|
92
|
+
observed: integer(item.observed, 0, sampleCount * 2, `${label} observed`),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function publication(value, sampleCount, label) {
|
|
96
|
+
const item = exact(value, PUBLICATION_KEYS, label);
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
reviewItems: integer(item.reviewItems, 0, sampleCount * 4, `${label} review items`),
|
|
99
|
+
objectIdentifiersObserved: integer(item.objectIdentifiersObserved, 0, sampleCount, `${label} object identifiers`),
|
|
100
|
+
baseConformanceObserved: integer(item.baseConformanceObserved, 0, sampleCount, `${label} base conformance`),
|
|
101
|
+
selfLinksObserved: integer(item.selfLinksObserved, 0, sampleCount, `${label} self links`),
|
|
102
|
+
redactionMetadataObserved: integer(item.redactionMetadataObserved, 0, sampleCount, `${label} redaction metadata`),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function derivedState(sampleCount, sources, published) {
|
|
106
|
+
return sampleCount < MIN_REGISTRY_COHORT_SAMPLE
|
|
107
|
+
? 'insufficient_sample'
|
|
108
|
+
: sources.investigate || published.reviewItems
|
|
109
|
+
? 'review'
|
|
110
|
+
: 'consistent';
|
|
111
|
+
}
|
|
112
|
+
function conservativeState(states) {
|
|
113
|
+
return states.includes('review') ? 'review' : states.includes('insufficient_sample') ? 'insufficient_sample' : 'consistent';
|
|
114
|
+
}
|
|
115
|
+
function timelinePoint(value, label) {
|
|
116
|
+
const item = exact(value, POINT_KEYS, label);
|
|
117
|
+
const sampleCount = integer(item.sampleCount, 1, MAX_REGISTRY_COHORT_SAMPLES, `${label} sample count`);
|
|
118
|
+
const sources = sourceAlignment(item.sourceAlignment, sampleCount, `${label} source alignment`);
|
|
119
|
+
const published = publication(item.publication, sampleCount, `${label} publication`);
|
|
120
|
+
const pointState = state(item.state, `${label} state`);
|
|
121
|
+
if (pointState !== derivedState(sampleCount, sources, published))
|
|
122
|
+
throw new CliUsageError(`${label} state does not match its retained counts.`);
|
|
123
|
+
return Object.freeze({
|
|
124
|
+
reportGeneratedAt: timestamp(item.reportGeneratedAt, `${label} report generatedAt`),
|
|
125
|
+
sampleWindow: window(item.sampleWindow, `${label} sample window`),
|
|
126
|
+
sampleCount,
|
|
127
|
+
state: pointState,
|
|
128
|
+
sourceAlignment: sources,
|
|
129
|
+
publication: published,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function boundedLimitations(value, expected, label) {
|
|
133
|
+
if (!Array.isArray(value) || value.length !== expected.length || value.some((item, index) => item !== expected[index])) {
|
|
134
|
+
throw new CliUsageError(`${label} limitations do not match the supported target-free contract.`);
|
|
15
135
|
}
|
|
136
|
+
}
|
|
137
|
+
function parseInputDocuments(text) {
|
|
138
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_REGISTRY_COHORT_INPUT_BYTES)
|
|
139
|
+
throw new CliUsageError(`Registry cohort input is limited to ${MAX_REGISTRY_COHORT_INPUT_BYTES} bytes.`);
|
|
16
140
|
const trimmed = text.replace(/^\uFEFF/u, '').trim();
|
|
17
141
|
if (!trimmed)
|
|
18
142
|
throw new CliUsageError('Registry cohort input is empty.');
|
|
19
143
|
let values;
|
|
20
144
|
if (trimmed.startsWith('[')) {
|
|
21
|
-
let parsed;
|
|
22
145
|
try {
|
|
23
|
-
|
|
146
|
+
scanBoundedJson(trimmed);
|
|
147
|
+
const parsed = JSON.parse(trimmed);
|
|
148
|
+
if (!Array.isArray(parsed))
|
|
149
|
+
throw new Error('not array');
|
|
150
|
+
values = parsed;
|
|
24
151
|
}
|
|
25
|
-
catch {
|
|
26
|
-
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error instanceof TypeError && /duplicate|nesting|value limit|key limit/iu.test(error.message))
|
|
154
|
+
throw new CliUsageError(error.message);
|
|
155
|
+
throw new CliUsageError('Registry cohort JSON input must be a valid array.');
|
|
27
156
|
}
|
|
28
|
-
if (!Array.isArray(parsed))
|
|
29
|
-
throw new CliUsageError('Registry cohort JSON input must be an array.');
|
|
30
|
-
values = parsed;
|
|
31
157
|
}
|
|
32
158
|
else {
|
|
33
159
|
const lines = trimmed.split(/\r?\n/u).filter((line) => line.trim());
|
|
160
|
+
if (!lines.length || lines.length > MAX_REGISTRY_COHORT_SAMPLES) {
|
|
161
|
+
throw new CliUsageError(`Registry cohort input must contain from 1 to ${MAX_REGISTRY_COHORT_SAMPLES} saved Lookups or retained cohort reports.`);
|
|
162
|
+
}
|
|
34
163
|
values = lines.map((line, index) => {
|
|
35
164
|
try {
|
|
165
|
+
scanBoundedJson(line);
|
|
36
166
|
return JSON.parse(line);
|
|
37
167
|
}
|
|
38
|
-
catch {
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error instanceof TypeError && /duplicate|nesting|value limit|key limit/iu.test(error.message))
|
|
170
|
+
throw new CliUsageError(`Registry cohort JSONL line ${index + 1}: ${error.message}`);
|
|
39
171
|
throw new CliUsageError(`Registry cohort JSONL line ${index + 1} is invalid.`);
|
|
40
172
|
}
|
|
41
173
|
});
|
|
42
174
|
}
|
|
43
|
-
if (!values.length || values.length > MAX_REGISTRY_COHORT_SAMPLES)
|
|
44
|
-
throw new CliUsageError(`Registry cohort input must contain from 1 to ${MAX_REGISTRY_COHORT_SAMPLES} saved Lookups.`);
|
|
45
|
-
}
|
|
46
|
-
const output = values.map(record);
|
|
47
|
-
if (output.some((item) => !Object.keys(item).length))
|
|
48
|
-
throw new CliUsageError('Every registry cohort item must be one saved Lookup object.');
|
|
49
|
-
return output;
|
|
175
|
+
if (!values.length || values.length > MAX_REGISTRY_COHORT_SAMPLES)
|
|
176
|
+
throw new CliUsageError(`Registry cohort input must contain from 1 to ${MAX_REGISTRY_COHORT_SAMPLES} saved Lookups or retained cohort reports.`);
|
|
177
|
+
return values.map((value, index) => record(value, `Registry cohort item ${index + 1}`));
|
|
50
178
|
}
|
|
51
|
-
|
|
52
|
-
const
|
|
179
|
+
function groupPoint(reports, reportGeneratedAt) {
|
|
180
|
+
const sampleCount = reports.length;
|
|
181
|
+
const sources = Object.freeze({
|
|
182
|
+
investigate: reports.reduce((total, item) => total + item.summary.investigate, 0),
|
|
183
|
+
expectedConstraints: reports.reduce((total, item) => total + item.summary.expectedConstraints, 0),
|
|
184
|
+
observed: reports.reduce((total, item) => total + item.summary.observed, 0),
|
|
185
|
+
});
|
|
186
|
+
const published = Object.freeze({
|
|
187
|
+
reviewItems: reports.reduce((total, item) => total + item.publication.reviewItems, 0),
|
|
188
|
+
objectIdentifiersObserved: reports.filter((item) => item.publication.objectIdentifier.state === 'observed').length,
|
|
189
|
+
baseConformanceObserved: reports.filter((item) => item.publication.baseConformance.state === 'observed').length,
|
|
190
|
+
selfLinksObserved: reports.filter((item) => item.publication.selfLink.state === 'observed').length,
|
|
191
|
+
redactionMetadataObserved: reports.filter((item) => item.publication.redactionMetadata.state === 'observed').length,
|
|
192
|
+
});
|
|
193
|
+
const observed = reports.map((report) => report.generatedAt).sort();
|
|
194
|
+
return Object.freeze({
|
|
195
|
+
reportGeneratedAt,
|
|
196
|
+
sampleWindow: Object.freeze({ from: observed[0], to: observed.at(-1) }),
|
|
197
|
+
sampleCount,
|
|
198
|
+
state: derivedState(sampleCount, sources, published),
|
|
199
|
+
sourceAlignment: sources,
|
|
200
|
+
publication: published,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
function sourceReportsFromLookups(documents, generatedAt) {
|
|
204
|
+
const reports = documents.map((item) => {
|
|
205
|
+
const raw = JSON.stringify(item);
|
|
206
|
+
const lookup = parseSavedLookupDocument(raw, { label: 'Registry cohort saved Lookup' });
|
|
207
|
+
return buildRegistryDoctorReport(raw, timestamp(lookup.generatedAt, 'Saved Lookup generatedAt'));
|
|
208
|
+
});
|
|
53
209
|
const groups = new Map();
|
|
54
210
|
for (const report of reports) {
|
|
55
211
|
const key = `${report.suffix}\u0000${report.profile.id}`;
|
|
56
|
-
|
|
57
|
-
group.push(report);
|
|
58
|
-
groups.set(key, group);
|
|
212
|
+
groups.set(key, [...(groups.get(key) ?? []), report]);
|
|
59
213
|
}
|
|
60
214
|
const cohorts = [...groups.values()].map((group) => {
|
|
61
215
|
const sample = group[0];
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
216
|
+
const point = groupPoint(group, generatedAt);
|
|
217
|
+
return Object.freeze({ suffix: sample.suffix, profileId: sample.profile.id, sampleWindow: point.sampleWindow, points: Object.freeze([point]), inheritedState: point.state, upstreamTimelineOmitted: 0 });
|
|
218
|
+
});
|
|
219
|
+
const observed = reports.map((report) => report.generatedAt).sort();
|
|
220
|
+
return [Object.freeze({ generatedAt, sampleWindow: Object.freeze({ from: observed[0], to: observed.at(-1) }), cohorts: Object.freeze(cohorts), duplicateTimelinePoints: 0 })];
|
|
221
|
+
}
|
|
222
|
+
function parseLegacyReport(value) {
|
|
223
|
+
const root = exact(value, LEGACY_ROOT_KEYS, 'Registry cohort v1 report');
|
|
224
|
+
if (root.schema !== REGISTRY_COHORT_SCHEMA || root.version !== LEGACY_REGISTRY_COHORT_VERSION)
|
|
225
|
+
throw new CliUsageError('Registry cohort report version is unsupported.');
|
|
226
|
+
const generatedAt = timestamp(root.generatedAt, 'Registry cohort v1 generatedAt');
|
|
227
|
+
const sampleCount = integer(root.sampleCount, 1, MAX_REGISTRY_COHORT_SAMPLES, 'Registry cohort v1 sample count');
|
|
228
|
+
if (root.minimumCohortSample !== MIN_REGISTRY_COHORT_SAMPLE)
|
|
229
|
+
throw new CliUsageError('Registry cohort v1 minimum sample is unsupported.');
|
|
230
|
+
boundedLimitations(root.limitations, LEGACY_LIMITATIONS, 'Registry cohort v1');
|
|
231
|
+
if (!Array.isArray(root.cohorts) || !root.cohorts.length || root.cohorts.length > MAX_REGISTRY_COHORT_GROUPS)
|
|
232
|
+
throw new CliUsageError('Registry cohort v1 groups are invalid.');
|
|
233
|
+
const seen = new Set();
|
|
234
|
+
let represented = 0;
|
|
235
|
+
const cohorts = root.cohorts.map((candidate, index) => {
|
|
236
|
+
const item = exact(candidate, LEGACY_COHORT_KEYS, `Registry cohort v1 group ${index + 1}`);
|
|
237
|
+
const count = integer(item.sampleCount, 1, sampleCount, 'Registry cohort v1 group sample count');
|
|
238
|
+
const sources = sourceAlignment(item.sourceAlignment, count, 'Registry cohort v1 source alignment');
|
|
239
|
+
const published = publication(item.publication, count, 'Registry cohort v1 publication');
|
|
240
|
+
const pointState = state(item.state, 'Registry cohort v1 state');
|
|
241
|
+
if (pointState !== derivedState(count, sources, published))
|
|
242
|
+
throw new CliUsageError('Registry cohort v1 state does not match its retained counts.');
|
|
243
|
+
const key = `${suffix(item.suffix)}\u0000${identifier(item.profileId, 'Registry cohort v1 profile ID')}`;
|
|
244
|
+
if (seen.has(key))
|
|
245
|
+
throw new CliUsageError('Registry cohort v1 groups must be unique.');
|
|
246
|
+
seen.add(key);
|
|
247
|
+
represented += count;
|
|
248
|
+
const point = Object.freeze({ reportGeneratedAt: generatedAt, sampleWindow: Object.freeze({ from: generatedAt, to: generatedAt }), sampleCount: count, state: pointState, sourceAlignment: sources, publication: published });
|
|
249
|
+
return Object.freeze({ suffix: key.split('\u0000')[0], profileId: key.split('\u0000')[1], sampleWindow: point.sampleWindow, points: Object.freeze([point]), inheritedState: pointState, upstreamTimelineOmitted: 0 });
|
|
250
|
+
});
|
|
251
|
+
if (represented !== sampleCount)
|
|
252
|
+
throw new CliUsageError('Registry cohort v1 sample count does not match its groups.');
|
|
253
|
+
return Object.freeze({ generatedAt, sampleWindow: Object.freeze({ from: generatedAt, to: generatedAt }), cohorts: Object.freeze(cohorts), duplicateTimelinePoints: 0 });
|
|
254
|
+
}
|
|
255
|
+
function parseCurrentReport(value) {
|
|
256
|
+
const root = exact(value, ROOT_KEYS, 'Registry cohort v2 report');
|
|
257
|
+
if (root.schema !== REGISTRY_COHORT_SCHEMA || root.version !== REGISTRY_COHORT_VERSION)
|
|
258
|
+
throw new CliUsageError('Registry cohort report version is unsupported.');
|
|
259
|
+
const generatedAt = timestamp(root.generatedAt, 'Registry cohort v2 generatedAt');
|
|
260
|
+
if (root.inputFamily !== 'saved_lookups' && root.inputFamily !== 'retained_reports')
|
|
261
|
+
throw new CliUsageError('Registry cohort v2 input family is invalid.');
|
|
262
|
+
const reportsMerged = integer(root.reportsMerged, 0, MAX_REGISTRY_COHORT_SAMPLES, 'Registry cohort v2 reports merged');
|
|
263
|
+
if ((root.inputFamily === 'saved_lookups') !== (reportsMerged === 0))
|
|
264
|
+
throw new CliUsageError('Registry cohort v2 report count does not match its input family.');
|
|
265
|
+
if (root.minimumCohortSample !== MIN_REGISTRY_COHORT_SAMPLE)
|
|
266
|
+
throw new CliUsageError('Registry cohort v2 minimum sample is unsupported.');
|
|
267
|
+
const reportWindow = window(root.sampleWindow, 'Registry cohort v2 sample window');
|
|
268
|
+
boundedLimitations(root.limitations, LIMITATIONS, 'Registry cohort v2');
|
|
269
|
+
const omissions = exact(root.omissions, OMISSION_KEYS, 'Registry cohort v2 omissions');
|
|
270
|
+
const duplicateTimelinePoints = integer(omissions.duplicateTimelinePoints, 0, MAX_REGISTRY_COHORT_OMISSIONS, 'Registry cohort v2 duplicate points');
|
|
271
|
+
const omittedTimelinePoints = integer(omissions.timelinePoints, 0, MAX_REGISTRY_COHORT_OMISSIONS, 'Registry cohort v2 omitted points');
|
|
272
|
+
if (root.truncated !== (omittedTimelinePoints > 0))
|
|
273
|
+
throw new CliUsageError('Registry cohort v2 truncation state is inconsistent.');
|
|
274
|
+
if (!Array.isArray(root.cohorts) || !root.cohorts.length || root.cohorts.length > MAX_REGISTRY_COHORT_GROUPS)
|
|
275
|
+
throw new CliUsageError('Registry cohort v2 groups are invalid.');
|
|
276
|
+
const seen = new Set();
|
|
277
|
+
let represented = 0;
|
|
278
|
+
let cohortOmitted = 0;
|
|
279
|
+
const cohorts = root.cohorts.map((candidate, index) => {
|
|
280
|
+
const item = exact(candidate, COHORT_KEYS, `Registry cohort v2 group ${index + 1}`);
|
|
281
|
+
const groupSuffix = suffix(item.suffix);
|
|
282
|
+
const profileId = identifier(item.profileId, 'Registry cohort v2 profile ID');
|
|
283
|
+
const key = `${groupSuffix}\u0000${profileId}`;
|
|
284
|
+
if (seen.has(key))
|
|
285
|
+
throw new CliUsageError('Registry cohort v2 groups must be unique.');
|
|
286
|
+
seen.add(key);
|
|
287
|
+
const count = integer(item.sampleCount, 1, MAX_REGISTRY_COHORT_SAMPLES, 'Registry cohort v2 group sample count');
|
|
288
|
+
const groupWindow = window(item.sampleWindow, 'Registry cohort v2 group sample window');
|
|
289
|
+
const timelineOmitted = integer(item.timelineOmitted, 0, MAX_REGISTRY_COHORT_OMISSIONS, 'Registry cohort v2 group omitted points');
|
|
290
|
+
cohortOmitted += timelineOmitted;
|
|
291
|
+
if (!Array.isArray(item.timeline) || !item.timeline.length || item.timeline.length > MAX_REGISTRY_COHORT_TIMELINE_POINTS)
|
|
292
|
+
throw new CliUsageError('Registry cohort v2 timeline is invalid.');
|
|
293
|
+
const points = item.timeline.map((point, pointIndex) => timelinePoint(point, `Registry cohort v2 point ${pointIndex + 1}`));
|
|
294
|
+
const identities = points.map(canonicalArtifactJsonV2);
|
|
295
|
+
if (new Set(identities).size !== points.length || identities.some((identity, pointIndex) => pointIndex > 0 && pointOrder(points[pointIndex - 1], points[pointIndex]) > 0))
|
|
296
|
+
throw new CliUsageError('Registry cohort v2 timeline must be unique and ordered.');
|
|
297
|
+
const latest = points.at(-1);
|
|
298
|
+
const groupState = state(item.state, 'Registry cohort v2 group state');
|
|
299
|
+
const latestState = state(item.latestState, 'Registry cohort v2 latest state');
|
|
300
|
+
const sources = sourceAlignment(item.sourceAlignment, count, 'Registry cohort v2 group source alignment');
|
|
301
|
+
const published = publication(item.publication, count, 'Registry cohort v2 group publication');
|
|
302
|
+
const visibleState = conservativeState(points.map((point) => point.state));
|
|
303
|
+
const pointFrom = points.map((point) => point.sampleWindow.from).sort()[0];
|
|
304
|
+
const pointTo = points.map((point) => point.sampleWindow.to).sort().at(-1);
|
|
305
|
+
if (latestState !== latest.state || count !== latest.sampleCount
|
|
306
|
+
|| canonicalArtifactJsonV2(sources) !== canonicalArtifactJsonV2(latest.sourceAlignment)
|
|
307
|
+
|| canonicalArtifactJsonV2(published) !== canonicalArtifactJsonV2(latest.publication)
|
|
308
|
+
|| (timelineOmitted === 0 && groupState !== visibleState)
|
|
309
|
+
|| (timelineOmitted > 0 && conservativeState([groupState, visibleState]) !== groupState)
|
|
310
|
+
|| groupWindow.from > pointFrom || groupWindow.to < pointTo) {
|
|
311
|
+
throw new CliUsageError('Registry cohort v2 current group projection is inconsistent with its timeline.');
|
|
312
|
+
}
|
|
313
|
+
represented += count;
|
|
314
|
+
return Object.freeze({ suffix: groupSuffix, profileId, sampleWindow: groupWindow, points: Object.freeze(points), inheritedState: groupState, upstreamTimelineOmitted: timelineOmitted });
|
|
315
|
+
});
|
|
316
|
+
if (integer(root.sampleCount, 1, MAX_REGISTRY_COHORT_GROUPS * MAX_REGISTRY_COHORT_SAMPLES, 'Registry cohort v2 sample count') !== represented
|
|
317
|
+
|| cohortOmitted !== omittedTimelinePoints
|
|
318
|
+
|| reportWindow.from !== [...cohorts].sort((a, b) => a.sampleWindow.from.localeCompare(b.sampleWindow.from))[0]?.sampleWindow.from
|
|
319
|
+
|| reportWindow.to !== [...cohorts].sort((a, b) => b.sampleWindow.to.localeCompare(a.sampleWindow.to))[0]?.sampleWindow.to) {
|
|
320
|
+
throw new CliUsageError('Registry cohort v2 report summary is inconsistent with its groups.');
|
|
321
|
+
}
|
|
322
|
+
return Object.freeze({ generatedAt, sampleWindow: reportWindow, cohorts: Object.freeze(cohorts), duplicateTimelinePoints });
|
|
323
|
+
}
|
|
324
|
+
function pointOrder(left, right) {
|
|
325
|
+
return left.sampleWindow.to.localeCompare(right.sampleWindow.to)
|
|
326
|
+
|| left.reportGeneratedAt.localeCompare(right.reportGeneratedAt)
|
|
327
|
+
|| canonicalArtifactJsonV2(left).localeCompare(canonicalArtifactJsonV2(right));
|
|
328
|
+
}
|
|
329
|
+
function buildCurrentReport(sourceReports, generatedAt, inputFamily, directSampleCount) {
|
|
330
|
+
const totalPoints = sourceReports.reduce((sum, report) => sum + report.cohorts.reduce((groupSum, cohort) => groupSum + cohort.points.length, 0), 0);
|
|
331
|
+
if (totalPoints > MAX_REGISTRY_COHORT_INPUT_POINTS)
|
|
332
|
+
throw new CliUsageError(`Retained registry cohort input contains more than ${MAX_REGISTRY_COHORT_INPUT_POINTS} timeline points.`);
|
|
333
|
+
const groups = new Map();
|
|
334
|
+
for (const report of sourceReports)
|
|
335
|
+
for (const cohort of report.cohorts) {
|
|
336
|
+
const key = `${cohort.suffix}\u0000${cohort.profileId}`;
|
|
337
|
+
groups.set(key, [...(groups.get(key) ?? []), cohort]);
|
|
338
|
+
}
|
|
339
|
+
if (!groups.size || groups.size > MAX_REGISTRY_COHORT_GROUPS)
|
|
340
|
+
throw new CliUsageError(`Registry cohort output is limited to ${MAX_REGISTRY_COHORT_GROUPS} suffix/profile groups.`);
|
|
341
|
+
let duplicateTimelinePoints = Math.max(0, ...sourceReports.map((report) => report.duplicateTimelinePoints));
|
|
342
|
+
const cohorts = [...groups.entries()].map(([key, inputs]) => {
|
|
343
|
+
const unique = new Map();
|
|
344
|
+
for (const input of inputs)
|
|
345
|
+
for (const point of input.points) {
|
|
346
|
+
const identity = canonicalArtifactJsonV2(point);
|
|
347
|
+
if (unique.has(identity))
|
|
348
|
+
duplicateTimelinePoints += 1;
|
|
349
|
+
else
|
|
350
|
+
unique.set(identity, point);
|
|
351
|
+
}
|
|
352
|
+
const allPoints = [...unique.values()].sort(pointOrder);
|
|
353
|
+
const locallyOmitted = Math.max(0, allPoints.length - MAX_REGISTRY_COHORT_TIMELINE_POINTS);
|
|
354
|
+
const timeline = allPoints.slice(-MAX_REGISTRY_COHORT_TIMELINE_POINTS);
|
|
355
|
+
const latest = timeline.at(-1);
|
|
356
|
+
const upstreamTimelineOmitted = Math.max(0, ...inputs.map((input) => input.upstreamTimelineOmitted));
|
|
357
|
+
const groupWindow = Object.freeze({
|
|
358
|
+
from: inputs.map((input) => input.sampleWindow.from).sort()[0],
|
|
359
|
+
to: inputs.map((input) => input.sampleWindow.to).sort().at(-1),
|
|
360
|
+
});
|
|
67
361
|
return Object.freeze({
|
|
68
|
-
suffix:
|
|
69
|
-
profileId:
|
|
70
|
-
sampleCount,
|
|
71
|
-
state:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
expectedConstraints: group.reduce((total, item) => total + item.summary.expectedConstraints, 0),
|
|
79
|
-
observed: group.reduce((total, item) => total + item.summary.observed, 0),
|
|
80
|
-
}),
|
|
81
|
-
publication: Object.freeze({
|
|
82
|
-
reviewItems: publicationReviewItems,
|
|
83
|
-
objectIdentifiersObserved: group.filter((item) => item.publication.objectIdentifier.state === 'observed').length,
|
|
84
|
-
baseConformanceObserved: group.filter((item) => item.publication.baseConformance.state === 'observed').length,
|
|
85
|
-
selfLinksObserved: group.filter((item) => item.publication.selfLink.state === 'observed').length,
|
|
86
|
-
redactionMetadataObserved: group.filter((item) => item.publication.redactionMetadata.state === 'observed').length,
|
|
87
|
-
}),
|
|
362
|
+
suffix: key.split('\u0000')[0],
|
|
363
|
+
profileId: key.split('\u0000')[1],
|
|
364
|
+
sampleCount: latest.sampleCount,
|
|
365
|
+
state: conservativeState([...inputs.map((input) => input.inheritedState), ...allPoints.map((point) => point.state)]),
|
|
366
|
+
latestState: latest.state,
|
|
367
|
+
sampleWindow: groupWindow,
|
|
368
|
+
sourceAlignment: latest.sourceAlignment,
|
|
369
|
+
publication: latest.publication,
|
|
370
|
+
timeline: Object.freeze(timeline),
|
|
371
|
+
timelineOmitted: upstreamTimelineOmitted + locallyOmitted,
|
|
88
372
|
});
|
|
89
373
|
}).sort((left, right) => left.suffix.localeCompare(right.suffix) || left.profileId.localeCompare(right.profileId));
|
|
374
|
+
const timelinePoints = cohorts.reduce((sum, cohort) => sum + cohort.timelineOmitted, 0);
|
|
375
|
+
if (duplicateTimelinePoints > MAX_REGISTRY_COHORT_OMISSIONS || timelinePoints > MAX_REGISTRY_COHORT_OMISSIONS) {
|
|
376
|
+
throw new CliUsageError(`Registry cohort omission accounting exceeds the ${MAX_REGISTRY_COHORT_OMISSIONS}-item bound.`);
|
|
377
|
+
}
|
|
378
|
+
const allWindows = sourceReports.map((report) => report.sampleWindow);
|
|
90
379
|
return Object.freeze({
|
|
91
380
|
schema: REGISTRY_COHORT_SCHEMA,
|
|
92
381
|
version: REGISTRY_COHORT_VERSION,
|
|
93
|
-
generatedAt,
|
|
94
|
-
|
|
382
|
+
generatedAt: timestamp(generatedAt, 'Registry cohort generatedAt'),
|
|
383
|
+
inputFamily,
|
|
384
|
+
sampleCount: inputFamily === 'saved_lookups' ? directSampleCount : cohorts.reduce((sum, cohort) => sum + cohort.sampleCount, 0),
|
|
385
|
+
reportsMerged: inputFamily === 'retained_reports' ? sourceReports.length : 0,
|
|
95
386
|
minimumCohortSample: MIN_REGISTRY_COHORT_SAMPLE,
|
|
387
|
+
sampleWindow: Object.freeze({
|
|
388
|
+
from: allWindows.map((item) => item.from).sort()[0],
|
|
389
|
+
to: allWindows.map((item) => item.to).sort().at(-1),
|
|
390
|
+
}),
|
|
96
391
|
cohorts: Object.freeze(cohorts),
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
'Repeated observations from one environment are not representative by themselves. Review fixture provenance and source health before changing a parser or access profile.',
|
|
101
|
-
]),
|
|
392
|
+
omissions: Object.freeze({ duplicateTimelinePoints, timelinePoints }),
|
|
393
|
+
truncated: timelinePoints > 0,
|
|
394
|
+
limitations: LIMITATIONS,
|
|
102
395
|
});
|
|
103
396
|
}
|
|
397
|
+
export function buildRegistryCohortReport(text, generatedAt = new Date().toISOString()) {
|
|
398
|
+
const documents = parseInputDocuments(text);
|
|
399
|
+
const families = new Set(documents.map((document) => document.schema === SAVED_LOOKUP_SCHEMA ? 'saved_lookups' : document.schema === REGISTRY_COHORT_SCHEMA ? 'retained_reports' : 'unsupported'));
|
|
400
|
+
if (families.size !== 1 || families.has('unsupported'))
|
|
401
|
+
throw new CliUsageError('Registry cohort input must contain only saved Lookups or only retained registry cohort reports; mixed families are rejected.');
|
|
402
|
+
const inputFamily = [...families][0];
|
|
403
|
+
if (inputFamily === 'saved_lookups') {
|
|
404
|
+
return buildCurrentReport(sourceReportsFromLookups(documents, timestamp(generatedAt, 'Registry cohort generatedAt')), generatedAt, inputFamily, documents.length);
|
|
405
|
+
}
|
|
406
|
+
const reports = [];
|
|
407
|
+
let retainedPoints = 0;
|
|
408
|
+
for (const document of documents) {
|
|
409
|
+
const report = document.version === LEGACY_REGISTRY_COHORT_VERSION
|
|
410
|
+
? parseLegacyReport(document)
|
|
411
|
+
: document.version === REGISTRY_COHORT_VERSION
|
|
412
|
+
? parseCurrentReport(document)
|
|
413
|
+
: (() => { throw new CliUsageError('Registry cohort report version is unsupported.'); })();
|
|
414
|
+
retainedPoints += report.cohorts.reduce((total, cohort) => total + cohort.points.length, 0);
|
|
415
|
+
if (retainedPoints > MAX_REGISTRY_COHORT_INPUT_POINTS) {
|
|
416
|
+
throw new CliUsageError(`Retained registry cohort input contains more than ${MAX_REGISTRY_COHORT_INPUT_POINTS} timeline points.`);
|
|
417
|
+
}
|
|
418
|
+
reports.push(report);
|
|
419
|
+
}
|
|
420
|
+
return buildCurrentReport(reports, generatedAt, inputFamily, 0);
|
|
421
|
+
}
|
|
104
422
|
export function formatRegistryCohortReport(report) {
|
|
105
423
|
const output = [
|
|
106
|
-
'Registry quality cohort',
|
|
107
|
-
`
|
|
424
|
+
'Registry quality cohort timeline',
|
|
425
|
+
`Input family ${report.inputFamily.replaceAll('_', ' ')}`,
|
|
426
|
+
`Current samples ${report.sampleCount}`,
|
|
427
|
+
`Reports merged ${report.reportsMerged}`,
|
|
428
|
+
`Sample window ${report.sampleWindow.from} → ${report.sampleWindow.to}`,
|
|
108
429
|
`Minimum sample ${report.minimumCohortSample}`,
|
|
109
430
|
'',
|
|
110
431
|
];
|
|
111
432
|
for (const cohort of report.cohorts) {
|
|
112
433
|
output.push(`.${cohort.suffix} · ${cohort.profileId} [${cohort.state.replaceAll('_', ' ')}]`);
|
|
113
|
-
output.push(`
|
|
434
|
+
output.push(` Latest: ${cohort.latestState.replaceAll('_', ' ')} · ${cohort.sampleCount} samples`);
|
|
435
|
+
output.push(` Timeline points: ${cohort.timeline.length}${cohort.timelineOmitted ? ` · ${cohort.timelineOmitted} omitted` : ''}`);
|
|
114
436
|
output.push(` Source review items: ${cohort.sourceAlignment.investigate}`);
|
|
115
437
|
output.push(` Publication review items: ${cohort.publication.reviewItems}`);
|
|
116
438
|
}
|
|
439
|
+
if (report.omissions.duplicateTimelinePoints)
|
|
440
|
+
output.push('', `Duplicate timeline points suppressed: ${report.omissions.duplicateTimelinePoints}`);
|
|
117
441
|
output.push('', 'Limitations:');
|
|
118
442
|
for (const limitation of report.limitations)
|
|
119
443
|
output.push(` - ${limitation}`);
|
package/cli/registry-support.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registryStandardsCoverageSnapshot, } from "../lib/registry-capabilities.mjs";
|
|
2
2
|
const REGISTRY_SUPPORT_SCHEMA = 'whoisleuth.cli.registry-support';
|
|
3
|
-
const REGISTRY_SUPPORT_SCHEMA_VERSION =
|
|
3
|
+
const REGISTRY_SUPPORT_SCHEMA_VERSION = 4;
|
|
4
4
|
const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
|
|
5
5
|
const MAX_REGISTRY_SUPPORT_TEXT_LENGTH = 2048;
|
|
6
6
|
const MAX_REGISTRY_SUPPORT_REFERENCES = 20;
|
|
@@ -88,6 +88,7 @@ function buildRegistrySupportDocument(requestedInput, capability, catalogueVersi
|
|
|
88
88
|
explicitSuffixProfile: capability.explicitSuffixProfile === true,
|
|
89
89
|
registryClass: boundedText(capability.registryClass, 'unknown', 64),
|
|
90
90
|
coverageState: boundedText(capability.coverageState, 'unknown', 64),
|
|
91
|
+
officialLookupUrl: boundedList([capability.officialLookupUrl], 'url')[0] ?? null,
|
|
91
92
|
rdap: {
|
|
92
93
|
discovery: boundedText(capability.rdapDiscovery, 'unknown', 64),
|
|
93
94
|
accessProfile: boundedText(capability.rdapAccessProfile, 'unknown', 128),
|