ssrwire 0.2.0 → 0.4.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.
- package/CHANGELOG.md +47 -1
- package/CONTRIBUTING.md +3 -1
- package/PUBLISHING.md +15 -15
- package/README.md +179 -29
- package/dist/analyze.d.ts.map +1 -1
- package/dist/analyze.js +162 -6
- package/dist/analyze.js.map +1 -1
- package/dist/audit-report.d.ts +8 -0
- package/dist/audit-report.d.ts.map +1 -0
- package/dist/audit-report.js +243 -0
- package/dist/audit-report.js.map +1 -0
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +2 -0
- package/dist/audit.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +77 -10
- package/dist/cli.js.map +1 -1
- package/dist/compare.d.ts +6 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/compare.js +720 -0
- package/dist/compare.js.map +1 -0
- package/dist/comparison-reporters.d.ts +9 -0
- package/dist/comparison-reporters.d.ts.map +1 -0
- package/dist/comparison-reporters.js +223 -0
- package/dist/comparison-reporters.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -1
- package/dist/config.js.map +1 -1
- package/dist/http-probe.d.ts.map +1 -1
- package/dist/http-probe.js +4 -0
- package/dist/http-probe.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +6 -0
- package/dist/redact.js.map +1 -1
- package/dist/reporters.d.ts.map +1 -1
- package/dist/reporters.js +27 -0
- package/dist/reporters.js.map +1 -1
- package/dist/social.d.ts +14 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +88 -0
- package/dist/social.js.map +1 -0
- package/dist/stability.d.ts.map +1 -1
- package/dist/stability.js +84 -4
- package/dist/stability.js.map +1 -1
- package/dist/stream-parser.d.ts.map +1 -1
- package/dist/stream-parser.js +21 -1
- package/dist/stream-parser.js.map +1 -1
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -1
- package/examples/github-actions.yml +2 -2
- package/examples/ssrwire.config.yml +8 -2
- package/package.json +5 -2
- package/src/analyze.ts +208 -7
- package/src/audit-report.ts +269 -0
- package/src/audit.ts +2 -0
- package/src/cli.ts +106 -12
- package/src/compare.ts +949 -0
- package/src/comparison-reporters.ts +276 -0
- package/src/config.ts +19 -1
- package/src/http-probe.ts +4 -0
- package/src/index.ts +29 -0
- package/src/redact.ts +6 -0
- package/src/reporters.ts +46 -0
- package/src/social.ts +116 -0
- package/src/stability.ts +123 -4
- package/src/stream-parser.ts +31 -1
- package/src/types.ts +117 -0
package/src/stability.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
effectiveTwitterCardSignal,
|
|
3
|
+
firstSocialSignal,
|
|
4
|
+
normalizeSocialValue,
|
|
5
|
+
OPEN_GRAPH_PROPERTIES,
|
|
6
|
+
OPEN_GRAPH_REQUIRED_PROPERTIES,
|
|
7
|
+
socialSignals,
|
|
8
|
+
TWITTER_CARD_REQUIRED_FIELDS,
|
|
9
|
+
} from "./social.js";
|
|
1
10
|
import type {
|
|
2
11
|
AgentProfile,
|
|
3
12
|
AgentStability,
|
|
@@ -7,6 +16,7 @@ import type {
|
|
|
7
16
|
ProbeResult,
|
|
8
17
|
RobotsAudience,
|
|
9
18
|
RobotsSignal,
|
|
19
|
+
SocialMetadataProperty,
|
|
10
20
|
TimingStats,
|
|
11
21
|
} from "./types.js";
|
|
12
22
|
|
|
@@ -25,6 +35,11 @@ export interface StabilityAnalysis {
|
|
|
25
35
|
readonly findings: readonly Finding[];
|
|
26
36
|
}
|
|
27
37
|
|
|
38
|
+
interface SocialSignature {
|
|
39
|
+
openGraph?: unknown;
|
|
40
|
+
twitterCard?: unknown;
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
function normalizeText(value: string): string {
|
|
29
44
|
return value.trim().replace(/\s+/g, " ");
|
|
30
45
|
}
|
|
@@ -117,8 +132,97 @@ function robotsValueLocations(probe: ProbeResult): readonly (readonly [string, s
|
|
|
117
132
|
);
|
|
118
133
|
}
|
|
119
134
|
|
|
120
|
-
function
|
|
135
|
+
function socialValuesForSignature(
|
|
136
|
+
probe: ProbeResult,
|
|
137
|
+
property: SocialMetadataProperty,
|
|
138
|
+
): readonly string[] {
|
|
139
|
+
const values = [
|
|
140
|
+
...new Set(
|
|
141
|
+
socialSignals(probe.signals, property)
|
|
142
|
+
.map((signal) => normalizeSocialValue(property, signal.value, probe.finalUrl))
|
|
143
|
+
.filter((value) => value.length > 0),
|
|
144
|
+
),
|
|
145
|
+
];
|
|
146
|
+
return property === "og:image" ? values : values.sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function socialLocationsForSignature(
|
|
150
|
+
probe: ProbeResult,
|
|
151
|
+
property: SocialMetadataProperty,
|
|
152
|
+
): readonly (readonly [string, string])[] {
|
|
153
|
+
if (property !== "og:image") {
|
|
154
|
+
return signalValueLocations(socialSignals(probe.signals, property), (value) =>
|
|
155
|
+
normalizeSocialValue(property, value, probe.finalUrl),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return socialSignals(probe.signals, property)
|
|
159
|
+
.map(
|
|
160
|
+
(signal) =>
|
|
161
|
+
[normalizeSocialValue(property, signal.value, probe.finalUrl), signal.location] as const,
|
|
162
|
+
)
|
|
163
|
+
.filter(([value]) => value.length > 0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function socialValueSignature(target: AuditTarget, probe: ProbeResult): unknown {
|
|
167
|
+
const signature: SocialSignature = {};
|
|
168
|
+
if (target.expectations.requireOpenGraph === true) {
|
|
169
|
+
signature.openGraph = Object.fromEntries(
|
|
170
|
+
OPEN_GRAPH_PROPERTIES.map((property) => [
|
|
171
|
+
property,
|
|
172
|
+
socialValuesForSignature(probe, property),
|
|
173
|
+
]),
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
if (target.expectations.requireTwitterCard === true) {
|
|
177
|
+
signature.twitterCard = Object.fromEntries(
|
|
178
|
+
TWITTER_CARD_REQUIRED_FIELDS.map((field) => {
|
|
179
|
+
const signal = effectiveTwitterCardSignal(probe.signals, field);
|
|
180
|
+
return [
|
|
181
|
+
field,
|
|
182
|
+
signal === undefined
|
|
183
|
+
? "<missing>"
|
|
184
|
+
: normalizeSocialValue(signal.property, signal.value, probe.finalUrl) || "<missing>",
|
|
185
|
+
];
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return Object.keys(signature).length === 0 ? undefined : signature;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function socialLocationSignature(target: AuditTarget, probe: ProbeResult): unknown {
|
|
193
|
+
const signature: SocialSignature = {};
|
|
194
|
+
if (target.expectations.requireOpenGraph === true) {
|
|
195
|
+
signature.openGraph = Object.fromEntries(
|
|
196
|
+
OPEN_GRAPH_PROPERTIES.map((property) => [
|
|
197
|
+
property,
|
|
198
|
+
socialLocationsForSignature(probe, property),
|
|
199
|
+
]),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (target.expectations.requireTwitterCard === true) {
|
|
203
|
+
signature.twitterCard = Object.fromEntries(
|
|
204
|
+
TWITTER_CARD_REQUIRED_FIELDS.map((field) => {
|
|
205
|
+
const signal = effectiveTwitterCardSignal(probe.signals, field);
|
|
206
|
+
return [
|
|
207
|
+
field,
|
|
208
|
+
signal === undefined
|
|
209
|
+
? []
|
|
210
|
+
: [
|
|
211
|
+
[
|
|
212
|
+
normalizeSocialValue(signal.property, signal.value, probe.finalUrl),
|
|
213
|
+
signal.location,
|
|
214
|
+
],
|
|
215
|
+
],
|
|
216
|
+
];
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return Object.keys(signature).length === 0 ? undefined : signature;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function metadataValueSignature(target: AuditTarget, probe: ProbeResult): string {
|
|
121
224
|
const titles = probe.signals.titles ?? (probe.signals.title ? [probe.signals.title] : []);
|
|
225
|
+
const social = socialValueSignature(target, probe);
|
|
122
226
|
return JSON.stringify({
|
|
123
227
|
title: normalizedSignalValues(titles),
|
|
124
228
|
description: normalizedSignalValues(probe.signals.descriptions),
|
|
@@ -126,11 +230,13 @@ function metadataValueSignature(probe: ProbeResult): string {
|
|
|
126
230
|
normalizeCanonical(value, probe.finalUrl),
|
|
127
231
|
),
|
|
128
232
|
robots: effectiveRobotsValue(probe),
|
|
233
|
+
...(social === undefined ? {} : { social }),
|
|
129
234
|
});
|
|
130
235
|
}
|
|
131
236
|
|
|
132
|
-
function metadataLocationSignature(probe: ProbeResult): string {
|
|
237
|
+
function metadataLocationSignature(target: AuditTarget, probe: ProbeResult): string {
|
|
133
238
|
const titles = probe.signals.titles ?? (probe.signals.title ? [probe.signals.title] : []);
|
|
239
|
+
const social = socialLocationSignature(target, probe);
|
|
134
240
|
return JSON.stringify({
|
|
135
241
|
title: signalValueLocations(titles),
|
|
136
242
|
description: signalValueLocations(probe.signals.descriptions),
|
|
@@ -138,6 +244,7 @@ function metadataLocationSignature(probe: ProbeResult): string {
|
|
|
138
244
|
normalizeCanonical(value, probe.finalUrl),
|
|
139
245
|
),
|
|
140
246
|
robots: robotsValueLocations(probe),
|
|
247
|
+
...(social === undefined ? {} : { social }),
|
|
141
248
|
});
|
|
142
249
|
}
|
|
143
250
|
|
|
@@ -213,6 +320,16 @@ export function criticalSignalsArrivalMs(
|
|
|
213
320
|
}
|
|
214
321
|
if (expectations.requireH1 && !addRequired(firstNonEmpty(probe.signals.h1s))) return undefined;
|
|
215
322
|
if (expectations.requireMainText && !addRequired(probe.signals.firstMainText)) return undefined;
|
|
323
|
+
if (expectations.requireOpenGraph === true) {
|
|
324
|
+
for (const property of OPEN_GRAPH_REQUIRED_PROPERTIES) {
|
|
325
|
+
if (!addRequired(firstSocialSignal(probe.signals, property))) return undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (expectations.requireTwitterCard === true) {
|
|
329
|
+
for (const field of TWITTER_CARD_REQUIRED_FIELDS) {
|
|
330
|
+
if (!addRequired(effectiveTwitterCardSignal(probe.signals, field))) return undefined;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
216
333
|
|
|
217
334
|
return required === 0 ? undefined : Math.max(...marks);
|
|
218
335
|
}
|
|
@@ -275,8 +392,10 @@ function summarizeAgent(target: AuditTarget, probes: readonly ProbeResult[]): Ag
|
|
|
275
392
|
finalUrl: uniqueCount(finalResponses.map((probe) => normalizeUrl(probe.finalUrl))),
|
|
276
393
|
redirectChain: uniqueCount(probes.map(redirectChainSignature)),
|
|
277
394
|
bodySha256: uniqueCount(complete.map((probe) => probe.bodySha256 ?? "<missing>")),
|
|
278
|
-
metadataValues: uniqueCount(complete.map(metadataValueSignature)),
|
|
279
|
-
metadataLocations: uniqueCount(
|
|
395
|
+
metadataValues: uniqueCount(complete.map((probe) => metadataValueSignature(target, probe))),
|
|
396
|
+
metadataLocations: uniqueCount(
|
|
397
|
+
complete.map((probe) => metadataLocationSignature(target, probe)),
|
|
398
|
+
),
|
|
280
399
|
},
|
|
281
400
|
};
|
|
282
401
|
}
|
package/src/stream-parser.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Parser } from "htmlparser2";
|
|
2
2
|
|
|
3
|
+
import { isSocialMetadataProperty } from "./social.js";
|
|
4
|
+
|
|
3
5
|
import type {
|
|
4
6
|
DocumentSignals,
|
|
5
7
|
ElementLocation,
|
|
@@ -7,6 +9,8 @@ import type {
|
|
|
7
9
|
JsonLdSignal,
|
|
8
10
|
RobotsAudience,
|
|
9
11
|
RobotsSignal,
|
|
12
|
+
SocialMetadataProperty,
|
|
13
|
+
SocialMetadataSignal,
|
|
10
14
|
TimingMark,
|
|
11
15
|
} from "./types.js";
|
|
12
16
|
|
|
@@ -93,6 +97,8 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
93
97
|
readonly #descriptions: ElementSignal[] = [];
|
|
94
98
|
readonly #canonicals: ElementSignal[] = [];
|
|
95
99
|
readonly #robots: RobotsSignal[] = [];
|
|
100
|
+
readonly #socialMetadata: SocialMetadataSignal[] = [];
|
|
101
|
+
readonly #socialMetadataCounts = new Map<SocialMetadataProperty, number>();
|
|
96
102
|
readonly #h1s: ElementSignal[] = [];
|
|
97
103
|
readonly #jsonLd: JsonLdSignal[] = [];
|
|
98
104
|
readonly #titles: ElementSignal[] = [];
|
|
@@ -170,6 +176,7 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
170
176
|
descriptions: [...this.#descriptions],
|
|
171
177
|
canonicals: [...this.#canonicals],
|
|
172
178
|
robots: [...this.#robots],
|
|
179
|
+
socialMetadata: [...this.#socialMetadata],
|
|
173
180
|
h1s: [...this.#h1s],
|
|
174
181
|
...(firstMainText === undefined || firstMainText.value.length === 0 ? {} : { firstMainText }),
|
|
175
182
|
jsonLd: [...this.#jsonLd],
|
|
@@ -213,7 +220,14 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
213
220
|
return;
|
|
214
221
|
}
|
|
215
222
|
|
|
216
|
-
const {
|
|
223
|
+
const {
|
|
224
|
+
name: metaNameAttribute,
|
|
225
|
+
property: metaPropertyAttribute,
|
|
226
|
+
content,
|
|
227
|
+
rel,
|
|
228
|
+
href,
|
|
229
|
+
type: scriptType,
|
|
230
|
+
} = attributes;
|
|
217
231
|
if (name === "head") this.#inHead = true;
|
|
218
232
|
if (name === "body") {
|
|
219
233
|
if (this.#bodyStarted === undefined) this.#bodyStarted = this.#mark();
|
|
@@ -232,6 +246,7 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
232
246
|
|
|
233
247
|
if (name === "meta") {
|
|
234
248
|
const metaName = metaNameAttribute?.trim().toLowerCase();
|
|
249
|
+
const metaProperty = metaPropertyAttribute?.trim().toLowerCase();
|
|
235
250
|
if (
|
|
236
251
|
content !== undefined &&
|
|
237
252
|
metaName === "description" &&
|
|
@@ -249,6 +264,21 @@ class HtmlStreamInspector implements StreamInspector {
|
|
|
249
264
|
audience: metaName,
|
|
250
265
|
});
|
|
251
266
|
}
|
|
267
|
+
const socialProperty = isSocialMetadataProperty(metaProperty)
|
|
268
|
+
? metaProperty
|
|
269
|
+
: isSocialMetadataProperty(metaName)
|
|
270
|
+
? metaName
|
|
271
|
+
: undefined;
|
|
272
|
+
if (content !== undefined && socialProperty !== undefined) {
|
|
273
|
+
const count = this.#socialMetadataCounts.get(socialProperty) ?? 0;
|
|
274
|
+
if (count < SIGNAL_LIMIT) {
|
|
275
|
+
this.#socialMetadata.push({
|
|
276
|
+
...this.#elementSignal(content, location),
|
|
277
|
+
property: socialProperty,
|
|
278
|
+
});
|
|
279
|
+
this.#socialMetadataCounts.set(socialProperty, count + 1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
252
282
|
}
|
|
253
283
|
|
|
254
284
|
if (name === "link") {
|
package/src/types.ts
CHANGED
|
@@ -18,6 +18,21 @@ export interface RobotsSignal extends ElementSignal {
|
|
|
18
18
|
readonly audience: RobotsAudience;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export type SocialMetadataProperty =
|
|
22
|
+
| "og:title"
|
|
23
|
+
| "og:type"
|
|
24
|
+
| "og:url"
|
|
25
|
+
| "og:image"
|
|
26
|
+
| "og:description"
|
|
27
|
+
| "twitter:card"
|
|
28
|
+
| "twitter:title"
|
|
29
|
+
| "twitter:description"
|
|
30
|
+
| "twitter:image";
|
|
31
|
+
|
|
32
|
+
export interface SocialMetadataSignal extends ElementSignal {
|
|
33
|
+
readonly property: SocialMetadataProperty;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
export interface JsonLdSignal extends TimingMark {
|
|
22
37
|
readonly location: ElementLocation;
|
|
23
38
|
readonly valid?: boolean;
|
|
@@ -33,6 +48,8 @@ export interface DocumentSignals {
|
|
|
33
48
|
readonly descriptions: readonly ElementSignal[];
|
|
34
49
|
readonly canonicals: readonly ElementSignal[];
|
|
35
50
|
readonly robots: readonly RobotsSignal[];
|
|
51
|
+
/** Present on current probes; optional for low-level callers and older in-memory results. */
|
|
52
|
+
readonly socialMetadata?: readonly SocialMetadataSignal[];
|
|
36
53
|
readonly h1s: readonly ElementSignal[];
|
|
37
54
|
readonly firstMainText?: ElementSignal;
|
|
38
55
|
readonly jsonLd: readonly JsonLdSignal[];
|
|
@@ -109,11 +126,17 @@ export interface TargetExpectations {
|
|
|
109
126
|
readonly requireCanonical: boolean;
|
|
110
127
|
readonly requireH1: boolean;
|
|
111
128
|
readonly requireMainText: boolean;
|
|
129
|
+
/** Require the four Open Graph protocol basic metadata properties. Defaults to false. */
|
|
130
|
+
readonly requireOpenGraph?: boolean;
|
|
131
|
+
/** Require SSRWire's Twitter Card readiness contract. Defaults to false. */
|
|
132
|
+
readonly requireTwitterCard?: boolean;
|
|
112
133
|
readonly maxFirstByteMs?: number;
|
|
113
134
|
readonly maxCriticalMs?: number;
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
export interface AuditTarget {
|
|
138
|
+
/** Stable identity used to match the same target across reports with different origins. */
|
|
139
|
+
readonly id?: string;
|
|
117
140
|
readonly url: string;
|
|
118
141
|
readonly expectations: TargetExpectations;
|
|
119
142
|
}
|
|
@@ -190,6 +213,8 @@ export interface AuditSummary {
|
|
|
190
213
|
}
|
|
191
214
|
|
|
192
215
|
export interface AuditResult {
|
|
216
|
+
/** Version of the persisted audit-report contract, independent of the package version. */
|
|
217
|
+
readonly schemaVersion: 1;
|
|
193
218
|
readonly version: string;
|
|
194
219
|
readonly generatedAt: string;
|
|
195
220
|
readonly durationMs: number;
|
|
@@ -199,3 +224,95 @@ export interface AuditResult {
|
|
|
199
224
|
}
|
|
200
225
|
|
|
201
226
|
export type ReportFormat = "terminal" | "json" | "sarif";
|
|
227
|
+
|
|
228
|
+
export type ComparisonKind = "regression" | "fixed" | "changed";
|
|
229
|
+
|
|
230
|
+
export type ComparisonScope = "target" | "agent" | "finding" | "response" | "metadata" | "timing";
|
|
231
|
+
|
|
232
|
+
export interface ComparisonChange {
|
|
233
|
+
readonly kind: ComparisonKind;
|
|
234
|
+
readonly scope: ComparisonScope;
|
|
235
|
+
readonly code: string;
|
|
236
|
+
readonly message: string;
|
|
237
|
+
readonly agent?: string;
|
|
238
|
+
readonly field?: string;
|
|
239
|
+
readonly baseline?: string | number | boolean;
|
|
240
|
+
readonly candidate?: string | number | boolean;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export interface ComparisonTimelineEvent {
|
|
244
|
+
readonly key: string;
|
|
245
|
+
readonly label: string;
|
|
246
|
+
readonly medianMs: number;
|
|
247
|
+
readonly location?: ElementLocation | "mixed";
|
|
248
|
+
readonly observedByByte?: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface ComparisonTimelineSnapshot {
|
|
252
|
+
readonly samples: number;
|
|
253
|
+
readonly events: readonly ComparisonTimelineEvent[];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface ComparisonTimelineLane {
|
|
257
|
+
readonly agent: string;
|
|
258
|
+
readonly label: string;
|
|
259
|
+
readonly baseline?: ComparisonTimelineSnapshot;
|
|
260
|
+
readonly candidate?: ComparisonTimelineSnapshot;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type TargetComparisonStatus = "matched" | "added" | "removed";
|
|
264
|
+
|
|
265
|
+
export interface TargetComparison {
|
|
266
|
+
readonly key: string;
|
|
267
|
+
readonly id?: string;
|
|
268
|
+
readonly status: TargetComparisonStatus;
|
|
269
|
+
readonly baselineUrl?: string;
|
|
270
|
+
readonly candidateUrl?: string;
|
|
271
|
+
readonly changes: readonly ComparisonChange[];
|
|
272
|
+
readonly timelines: readonly ComparisonTimelineLane[];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export interface AuditReportDescriptor {
|
|
276
|
+
readonly label: string;
|
|
277
|
+
readonly version: string;
|
|
278
|
+
readonly schemaVersion: 1;
|
|
279
|
+
readonly generatedAt: string;
|
|
280
|
+
readonly repeat: number;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface ComparisonThresholds {
|
|
284
|
+
readonly timingRegressionMs: number;
|
|
285
|
+
readonly timingRegressionPercent: number;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export interface ComparisonSummary {
|
|
289
|
+
readonly targets: number;
|
|
290
|
+
readonly matchedTargets: number;
|
|
291
|
+
readonly addedTargets: number;
|
|
292
|
+
readonly removedTargets: number;
|
|
293
|
+
readonly unchangedTargets: number;
|
|
294
|
+
readonly regressions: number;
|
|
295
|
+
readonly fixed: number;
|
|
296
|
+
readonly changed: number;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export interface AuditComparison {
|
|
300
|
+
readonly schemaVersion: 1;
|
|
301
|
+
readonly kind: "comparison";
|
|
302
|
+
readonly version: string;
|
|
303
|
+
readonly generatedAt: string;
|
|
304
|
+
readonly baseline: AuditReportDescriptor;
|
|
305
|
+
readonly candidate: AuditReportDescriptor;
|
|
306
|
+
readonly thresholds: ComparisonThresholds;
|
|
307
|
+
readonly results: readonly TargetComparison[];
|
|
308
|
+
readonly summary: ComparisonSummary;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface CompareAuditOptions {
|
|
312
|
+
readonly baselineLabel?: string;
|
|
313
|
+
readonly candidateLabel?: string;
|
|
314
|
+
readonly timingRegressionMs?: number;
|
|
315
|
+
readonly timingRegressionPercent?: number;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export type ComparisonReportFormat = "terminal" | "json" | "html";
|