@smthrs/evals 0.0.0-stage → 1.0.0-rc.3
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 +142 -0
- package/LICENSE +21 -0
- package/README.md +62 -2
- package/dist/cjs/Baseline.d.ts +118 -0
- package/dist/cjs/Baseline.d.ts.map +1 -0
- package/dist/cjs/Baseline.js +158 -0
- package/dist/cjs/Baseline.js.map +7 -0
- package/dist/cjs/CaseExecutor.d.ts +85 -0
- package/dist/cjs/CaseExecutor.d.ts.map +1 -0
- package/dist/cjs/CaseExecutor.js +59 -0
- package/dist/cjs/CaseExecutor.js.map +7 -0
- package/dist/cjs/EvalError.d.ts +64 -0
- package/dist/cjs/EvalError.d.ts.map +1 -0
- package/dist/cjs/EvalError.js +54 -0
- package/dist/cjs/EvalError.js.map +7 -0
- package/dist/cjs/Gate.d.ts +55 -0
- package/dist/cjs/Gate.d.ts.map +1 -0
- package/dist/cjs/Gate.js +88 -0
- package/dist/cjs/Gate.js.map +7 -0
- package/dist/cjs/Regression.d.ts +116 -0
- package/dist/cjs/Regression.d.ts.map +1 -0
- package/dist/cjs/Regression.js +191 -0
- package/dist/cjs/Regression.js.map +7 -0
- package/dist/cjs/Report.d.ts +114 -0
- package/dist/cjs/Report.d.ts.map +1 -0
- package/dist/cjs/Report.js +161 -0
- package/dist/cjs/Report.js.map +7 -0
- package/dist/cjs/Runner.d.ts +229 -0
- package/dist/cjs/Runner.d.ts.map +1 -0
- package/dist/cjs/Runner.js +430 -0
- package/dist/cjs/Runner.js.map +7 -0
- package/dist/cjs/Suite.d.ts +139 -0
- package/dist/cjs/Suite.d.ts.map +1 -0
- package/dist/cjs/Suite.js +221 -0
- package/dist/cjs/Suite.js.map +7 -0
- package/dist/cjs/Trials.d.ts +93 -0
- package/dist/cjs/Trials.d.ts.map +1 -0
- package/dist/cjs/Trials.js +82 -0
- package/dist/cjs/Trials.js.map +7 -0
- package/dist/cjs/index.d.ts +76 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +51 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/CanonicalJson.d.ts +44 -0
- package/dist/cjs/internal/CanonicalJson.d.ts.map +1 -0
- package/dist/cjs/internal/CanonicalJson.js +114 -0
- package/dist/cjs/internal/CanonicalJson.js.map +7 -0
- package/dist/cjs/internal/controlCharacters.d.ts +44 -0
- package/dist/cjs/internal/controlCharacters.d.ts.map +1 -0
- package/dist/cjs/internal/controlCharacters.js +42 -0
- package/dist/cjs/internal/controlCharacters.js.map +7 -0
- package/dist/cjs/internal/scorerLabel.d.ts +33 -0
- package/dist/cjs/internal/scorerLabel.d.ts.map +1 -0
- package/dist/cjs/internal/scorerLabel.js +28 -0
- package/dist/cjs/internal/scorerLabel.js.map +7 -0
- package/dist/cjs/internal/tupleKey.d.ts +20 -0
- package/dist/cjs/internal/tupleKey.d.ts.map +1 -0
- package/dist/cjs/internal/tupleKey.js +25 -0
- package/dist/cjs/internal/tupleKey.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Baseline.d.ts +118 -0
- package/dist/esm/Baseline.d.ts.map +1 -0
- package/dist/esm/Baseline.js +203 -0
- package/dist/esm/Baseline.js.map +1 -0
- package/dist/esm/CaseExecutor.d.ts +85 -0
- package/dist/esm/CaseExecutor.d.ts.map +1 -0
- package/dist/esm/CaseExecutor.js +60 -0
- package/dist/esm/CaseExecutor.js.map +1 -0
- package/dist/esm/EvalError.d.ts +64 -0
- package/dist/esm/EvalError.d.ts.map +1 -0
- package/dist/esm/EvalError.js +65 -0
- package/dist/esm/EvalError.js.map +1 -0
- package/dist/esm/Gate.d.ts +55 -0
- package/dist/esm/Gate.d.ts.map +1 -0
- package/dist/esm/Gate.js +90 -0
- package/dist/esm/Gate.js.map +1 -0
- package/dist/esm/Regression.d.ts +116 -0
- package/dist/esm/Regression.d.ts.map +1 -0
- package/dist/esm/Regression.js +195 -0
- package/dist/esm/Regression.js.map +1 -0
- package/dist/esm/Report.d.ts +114 -0
- package/dist/esm/Report.d.ts.map +1 -0
- package/dist/esm/Report.js +188 -0
- package/dist/esm/Report.js.map +1 -0
- package/dist/esm/Runner.d.ts +229 -0
- package/dist/esm/Runner.d.ts.map +1 -0
- package/dist/esm/Runner.js +430 -0
- package/dist/esm/Runner.js.map +1 -0
- package/dist/esm/Suite.d.ts +139 -0
- package/dist/esm/Suite.d.ts.map +1 -0
- package/dist/esm/Suite.js +255 -0
- package/dist/esm/Suite.js.map +1 -0
- package/dist/esm/Trials.d.ts +93 -0
- package/dist/esm/Trials.d.ts.map +1 -0
- package/dist/esm/Trials.js +116 -0
- package/dist/esm/Trials.js.map +1 -0
- package/dist/esm/index.d.ts +76 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +76 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/CanonicalJson.d.ts +44 -0
- package/dist/esm/internal/CanonicalJson.d.ts.map +1 -0
- package/dist/esm/internal/CanonicalJson.js +202 -0
- package/dist/esm/internal/CanonicalJson.js.map +1 -0
- package/dist/esm/internal/controlCharacters.d.ts +44 -0
- package/dist/esm/internal/controlCharacters.d.ts.map +1 -0
- package/dist/esm/internal/controlCharacters.js +65 -0
- package/dist/esm/internal/controlCharacters.js.map +1 -0
- package/dist/esm/internal/scorerLabel.d.ts +33 -0
- package/dist/esm/internal/scorerLabel.d.ts.map +1 -0
- package/dist/esm/internal/scorerLabel.js +33 -0
- package/dist/esm/internal/scorerLabel.js.map +1 -0
- package/dist/esm/internal/tupleKey.d.ts +20 -0
- package/dist/esm/internal/tupleKey.d.ts.map +1 -0
- package/dist/esm/internal/tupleKey.js +20 -0
- package/dist/esm/internal/tupleKey.js.map +1 -0
- package/docs/README.md +130 -0
- package/docs/api.md +626 -0
- package/docs/concepts/determinism.md +64 -0
- package/docs/concepts/fixed-suites.md +71 -0
- package/docs/concepts/step-keys.md +60 -0
- package/docs/guides/author-a-suite.md +115 -0
- package/docs/guides/gate-a-run-in-ci.md +129 -0
- package/docs/guides/maintain-a-baseline.md +91 -0
- package/docs/guides/run-a-suite.md +150 -0
- package/docs/installation.md +46 -0
- package/docs/quickstart.md +238 -0
- package/docs/troubleshooting.md +148 -0
- package/package.json +198 -3
- package/src/Baseline.ts +259 -0
- package/src/CaseExecutor.ts +104 -0
- package/src/EvalError.ts +74 -0
- package/src/Gate.ts +128 -0
- package/src/Regression.ts +314 -0
- package/src/Report.ts +268 -0
- package/src/Runner.ts +759 -0
- package/src/Suite.ts +358 -0
- package/src/Trials.ts +154 -0
- package/src/index.ts +76 -0
- package/src/internal/CanonicalJson.ts +234 -0
- package/src/internal/controlCharacters.ts +66 -0
- package/src/internal/scorerLabel.ts +35 -0
- package/src/internal/tupleKey.ts +20 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one canonical JSON encoder both serializers use.
|
|
3
|
+
*
|
|
4
|
+
* A report embeds the raw output of an arbitrary target flow, so the value
|
|
5
|
+
* being serialized is not JSON and cannot be assumed to be finite, acyclic, or
|
|
6
|
+
* even readable. A recursive walker over `JSON.stringify` answered that with a
|
|
7
|
+
* `RangeError` thrown out of a function typed `string`. This encoder is total
|
|
8
|
+
* instead: every shape JSON cannot express is replaced by a marker that names
|
|
9
|
+
* what was there, so a report of a broken run is still a report.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The nesting depth beyond which a value is replaced by a marker.
|
|
16
|
+
*
|
|
17
|
+
* @since 0.1.0
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
const maxDepth = 64
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Default cap on an embedded string, in UTF-16 code units.
|
|
24
|
+
*
|
|
25
|
+
* @since 0.1.0
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
const maxStringLength = 8192
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Total number of values one encode may visit.
|
|
32
|
+
*
|
|
33
|
+
* The cycle detector forgets a value once it leaves it, which is what keeps a
|
|
34
|
+
* value referenced twice from being reported as a cycle. The cost is that a
|
|
35
|
+
* shared acyclic graph is re-expanded at every reference, so `n` nested
|
|
36
|
+
* two-child wrappers over one shared child expand to `2^n` values while
|
|
37
|
+
* staying far below {@link maxDepth}. The ceiling is roughly three times what
|
|
38
|
+
* a suite at its declared case limit produces.
|
|
39
|
+
*
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
const maxNodes = 2_000_000
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Approximate cap on the encoded output, in UTF-16 code units.
|
|
47
|
+
*
|
|
48
|
+
* @since 0.1.0
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
const maxBytes = 33_554_432
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Marker left where a total budget ran out.
|
|
55
|
+
*
|
|
56
|
+
* @since 0.1.0
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
const budgetExceeded = "[budget exceeded]"
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Options accepted by {@link encode}.
|
|
63
|
+
*
|
|
64
|
+
* @since 0.1.0
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
interface Options {
|
|
68
|
+
/** Cap on embedded strings; `undefined` keeps them whole. */
|
|
69
|
+
readonly maxStringLength?: number | undefined
|
|
70
|
+
/** Cap on values visited; defaults to {@link maxNodes}. */
|
|
71
|
+
readonly maxNodes?: number | undefined
|
|
72
|
+
/** Cap on encoded output length; defaults to {@link maxBytes}. */
|
|
73
|
+
readonly maxBytes?: number | undefined
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Remaining total work, decremented in traversal order so truncation is stable. */
|
|
77
|
+
interface Budget {
|
|
78
|
+
nodes: number
|
|
79
|
+
bytes: number
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Orders keys by UTF-16 code unit, never by locale.
|
|
84
|
+
*
|
|
85
|
+
* @since 0.1.0
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
const compareText = (left: string, right: string): number => left < right ? -1 : left > right ? 1 : 0
|
|
89
|
+
|
|
90
|
+
const truncate = (value: string, limit: number | undefined): string =>
|
|
91
|
+
limit === undefined || value.length <= limit
|
|
92
|
+
? value
|
|
93
|
+
: `${value.slice(0, limit)}[truncated ${value.length - limit} chars]`
|
|
94
|
+
|
|
95
|
+
const primitive = (value: unknown, options: Options): unknown => {
|
|
96
|
+
if (typeof value === "string") return truncate(value, options.maxStringLength)
|
|
97
|
+
if (typeof value === "number") {
|
|
98
|
+
if (Number.isNaN(value)) return "[NaN]"
|
|
99
|
+
if (value === Number.POSITIVE_INFINITY) return "[Infinity]"
|
|
100
|
+
if (value === Number.NEGATIVE_INFINITY) return "[-Infinity]"
|
|
101
|
+
return Object.is(value, -0) ? 0 : value
|
|
102
|
+
}
|
|
103
|
+
if (typeof value === "bigint") return `[bigint ${value}]`
|
|
104
|
+
if (typeof value === "function") return "[function]"
|
|
105
|
+
if (typeof value === "symbol") return "[symbol]"
|
|
106
|
+
return value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const unreadable = (cause: unknown): string => {
|
|
110
|
+
try {
|
|
111
|
+
return `[unreadable: ${String(cause)}]`
|
|
112
|
+
} catch {
|
|
113
|
+
return "[unreadable]"
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const entriesOf = (value: object): ReadonlyArray<readonly [string, unknown]> =>
|
|
118
|
+
Object.keys(value).map((key) => {
|
|
119
|
+
try {
|
|
120
|
+
return [key, (value as { readonly [key: string]: unknown })[key]] as const
|
|
121
|
+
} catch (cause) {
|
|
122
|
+
return [key, unreadable(cause)] as const
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const objectOf = (
|
|
127
|
+
entries: ReadonlyArray<readonly [string, unknown]>,
|
|
128
|
+
depth: number,
|
|
129
|
+
seen: Set<object>,
|
|
130
|
+
options: Options,
|
|
131
|
+
budget: Budget
|
|
132
|
+
): object =>
|
|
133
|
+
Object.fromEntries(
|
|
134
|
+
entries
|
|
135
|
+
.filter(([, entry]) => entry !== undefined)
|
|
136
|
+
.sort(([left], [right]) => compareText(left, right))
|
|
137
|
+
.map(([key, entry]) => {
|
|
138
|
+
// The key is rendered too, so a wide object of long keys spends the
|
|
139
|
+
// byte budget even when every value is a marker.
|
|
140
|
+
budget.bytes -= key.length + 3
|
|
141
|
+
return [key, walk(entry, depth + 1, seen, options, budget)]
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
const walk = (value: unknown, depth: number, seen: Set<object>, options: Options, budget: Budget): unknown => {
|
|
146
|
+
if (budget.nodes <= 0 || budget.bytes <= 0) return budgetExceeded
|
|
147
|
+
budget.nodes -= 1
|
|
148
|
+
if (value === null || typeof value !== "object") {
|
|
149
|
+
const encoded = primitive(value, options)
|
|
150
|
+
budget.bytes -= typeof encoded === "string" ? encoded.length + 2 : 8
|
|
151
|
+
return encoded
|
|
152
|
+
}
|
|
153
|
+
if (seen.has(value)) return "[circular]"
|
|
154
|
+
if (depth > maxDepth) return "[depth exceeded]"
|
|
155
|
+
budget.bytes -= 2
|
|
156
|
+
try {
|
|
157
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "[invalid Date]" : value.toISOString()
|
|
158
|
+
seen.add(value)
|
|
159
|
+
try {
|
|
160
|
+
if (value instanceof Error) {
|
|
161
|
+
const properties = new Map(entriesOf(value))
|
|
162
|
+
properties.set("name", String(value.name))
|
|
163
|
+
properties.set("message", String(value.message))
|
|
164
|
+
return objectOf([...properties], depth, seen, options, budget)
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(value)) return value.map((entry) => walk(entry, depth + 1, seen, options, budget))
|
|
167
|
+
if (value instanceof Set) return [...value].map((entry) => walk(entry, depth + 1, seen, options, budget))
|
|
168
|
+
if (value instanceof Map) {
|
|
169
|
+
return [...value.entries()]
|
|
170
|
+
.map((
|
|
171
|
+
[key, entry]
|
|
172
|
+
) => [walk(key, depth + 1, seen, options, budget), walk(entry, depth + 1, seen, options, budget)])
|
|
173
|
+
}
|
|
174
|
+
return objectOf(entriesOf(value), depth, seen, options, budget)
|
|
175
|
+
} finally {
|
|
176
|
+
seen.delete(value)
|
|
177
|
+
}
|
|
178
|
+
} catch (cause) {
|
|
179
|
+
return unreadable(cause)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Rewrites a value into a shape `JSON.stringify` renders deterministically.
|
|
185
|
+
*
|
|
186
|
+
* Object keys are sorted by code unit, `-0` becomes `0`, and `undefined`
|
|
187
|
+
* members are dropped. Everything JSON cannot express becomes a bracketed
|
|
188
|
+
* marker rather than a throw or a silent `null`: `[circular]`,
|
|
189
|
+
* `[depth exceeded]`, `[NaN]`, `[Infinity]`, `[-Infinity]`, `[bigint n]`,
|
|
190
|
+
* `[function]`, `[symbol]`, and `[unreadable: …]` when a foreign operation
|
|
191
|
+
* throws. An `Error` becomes an object containing its own enumerable fields
|
|
192
|
+
* plus `name` and `message`, so typed error fields survive serialization. A
|
|
193
|
+
* `Date` becomes its ISO string, a `Set` an array, and a `Map` an array of
|
|
194
|
+
* key/value pairs.
|
|
195
|
+
*
|
|
196
|
+
* Traversal is bounded in total, not only per branch: once {@link maxNodes}
|
|
197
|
+
* values or {@link maxBytes} output code units are spent, every remaining
|
|
198
|
+
* value becomes `[budget exceeded]`. The budget is spent in traversal order,
|
|
199
|
+
* and traversal order is fixed by the key sort, so one value always truncates
|
|
200
|
+
* at the same place.
|
|
201
|
+
*
|
|
202
|
+
* @since 0.1.0
|
|
203
|
+
* @private
|
|
204
|
+
*/
|
|
205
|
+
const encode = (value: unknown, options: Options = {}): unknown =>
|
|
206
|
+
walk(value, 0, new Set<object>(), options, {
|
|
207
|
+
nodes: options.maxNodes ?? maxNodes,
|
|
208
|
+
bytes: options.maxBytes ?? maxBytes
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Serializes a value as canonical JSON with a trailing newline.
|
|
213
|
+
*
|
|
214
|
+
* @since 0.1.0
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
const stringify = (value: unknown, options: Options = {}): string => `${JSON.stringify(encode(value, options))}\n`
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The canonical JSON encoder: its limits, key order, and serializers.
|
|
221
|
+
*
|
|
222
|
+
* @since 0.1.0
|
|
223
|
+
* @private
|
|
224
|
+
*/
|
|
225
|
+
export const CanonicalJson = {
|
|
226
|
+
maxDepth,
|
|
227
|
+
maxStringLength,
|
|
228
|
+
maxNodes,
|
|
229
|
+
maxBytes,
|
|
230
|
+
budgetExceeded,
|
|
231
|
+
compareText,
|
|
232
|
+
encode,
|
|
233
|
+
stringify
|
|
234
|
+
} as const
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one control-character rule every trust boundary in this package shares.
|
|
3
|
+
*
|
|
4
|
+
* A control character in a name corrupts a Markdown report and a CI log line:
|
|
5
|
+
* a newline lets the value print its own line, which on GitHub Actions is a
|
|
6
|
+
* workflow command the runner executes, an ESC or a C1 CSI (U+009B) sequence
|
|
7
|
+
* rewrites the visible log, and a Unicode bidirectional control reorders what
|
|
8
|
+
* a reader sees. Values an author declares (suite and case names, baseline
|
|
9
|
+
* suites and records) are rejected where they enter the system; values a
|
|
10
|
+
* target returns at runtime (step keys, failure messages) are flattened
|
|
11
|
+
* instead, so a broken target stays a readable failure rather than a rejected
|
|
12
|
+
* run.
|
|
13
|
+
*
|
|
14
|
+
* The rule covers C0 controls (U+0000 through U+001F), DEL (U+007F), C1
|
|
15
|
+
* controls (U+0080 through U+009F), and the bidirectional controls U+061C,
|
|
16
|
+
* U+200E, U+200F, U+202A through U+202E, and U+2066 through U+2069.
|
|
17
|
+
*
|
|
18
|
+
* @since 0.1.0
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether one code point falls under the control-character rule.
|
|
23
|
+
*
|
|
24
|
+
* @since 0.1.0
|
|
25
|
+
* @private
|
|
26
|
+
*/
|
|
27
|
+
export const isControlCodePoint = (code: number): boolean =>
|
|
28
|
+
code < 0x20 ||
|
|
29
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
30
|
+
code === 0x061c ||
|
|
31
|
+
code === 0x200e ||
|
|
32
|
+
code === 0x200f ||
|
|
33
|
+
(code >= 0x202a && code <= 0x202e) ||
|
|
34
|
+
(code >= 0x2066 && code <= 0x2069)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Names the first control character in `value`, or `undefined` when there is
|
|
38
|
+
* none.
|
|
39
|
+
*
|
|
40
|
+
* @since 0.1.0
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
export const controlCharacter = (value: string): string | undefined => {
|
|
44
|
+
for (const character of value) {
|
|
45
|
+
const code = character.codePointAt(0)!
|
|
46
|
+
if (isControlCodePoint(code)) return `U+${code.toString(16).toUpperCase().padStart(4, "0")}`
|
|
47
|
+
}
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Replaces every control character with a space, the way a report cell does,
|
|
53
|
+
* so a hostile string cannot emit its own log line, terminal escape, or
|
|
54
|
+
* reordered text.
|
|
55
|
+
*
|
|
56
|
+
* @since 0.1.0
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
export const flattenControlCharacters = (value: string): string => {
|
|
60
|
+
if (controlCharacter(value) === undefined) return value
|
|
61
|
+
let flattened = ""
|
|
62
|
+
for (const character of value) {
|
|
63
|
+
flattened += isControlCodePoint(character.codePointAt(0)!) ? " " : character
|
|
64
|
+
}
|
|
65
|
+
return flattened
|
|
66
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one scorer label rule every rendering in this package shares.
|
|
3
|
+
*
|
|
4
|
+
* A scorer has two identities: `scorer` is the key, a digest of the scorer's
|
|
5
|
+
* own declaration and the only thing a baseline matches on, and `scorerName`
|
|
6
|
+
* is the readable name carried beside it. A digest is unreadable in a report
|
|
7
|
+
* cell, a CI log line, or a diagnostic, so each of them prints the name with
|
|
8
|
+
* the first eight key characters instead. Without a name there is nothing to
|
|
9
|
+
* abbreviate the key down to, so the label is the key itself: a truncated
|
|
10
|
+
* digest and no name would leave a reader nothing to match a baseline record
|
|
11
|
+
* on. One rule is what keeps a report from naming one scorer two ways, an
|
|
12
|
+
* inconclusive reason built by the runner and a Missing row built by the
|
|
13
|
+
* reporter agree.
|
|
14
|
+
*
|
|
15
|
+
* @since 0.1.0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renders `name (first 8 of the key)`, or the bare key when the scorer carries
|
|
20
|
+
* no name.
|
|
21
|
+
*
|
|
22
|
+
* Every flow carries a name, so a scorer a run holds always has one. A stored
|
|
23
|
+
* observation may not: a baseline recorded before a scorer was named, or one
|
|
24
|
+
* written by another tool, carries the key alone, and it labels as the key
|
|
25
|
+
* rather than as a blank followed by a digest.
|
|
26
|
+
*
|
|
27
|
+
* @since 0.1.0
|
|
28
|
+
* @private
|
|
29
|
+
*/
|
|
30
|
+
export const scorerLabel = (
|
|
31
|
+
identity: { readonly scorer: string; readonly scorerName?: string | undefined }
|
|
32
|
+
): string => {
|
|
33
|
+
const name = identity.scorerName
|
|
34
|
+
return name === undefined || name.length === 0 ? identity.scorer : `${name} (${identity.scorer.slice(0, 8)})`
|
|
35
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one injective tuple encoder every key in this package is built with.
|
|
3
|
+
*
|
|
4
|
+
* Joining caller-supplied strings on a delimiter is not injective: under a NUL
|
|
5
|
+
* separator the tuples `["a", "b" + NUL + "c"]` and `["a" + NUL + "b", "c"]`
|
|
6
|
+
* produce one key, so two distinct score jobs could share an identity and two
|
|
7
|
+
* distinct baseline records could sort as one. Encoding the tuple as a JSON
|
|
8
|
+
* array keeps the components separated by the encoding itself, whatever they
|
|
9
|
+
* contain.
|
|
10
|
+
*
|
|
11
|
+
* @since 0.1.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Encodes a tuple of strings as one key.
|
|
16
|
+
*
|
|
17
|
+
* @since 0.1.0
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
export const tupleKey = (...parts: ReadonlyArray<string>): string => JSON.stringify(parts)
|