@valerypopoff/rivet2-evaluations 2.3.0 → 2.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/dist/cjs/bundle.cjs +572 -309
- package/dist/cjs/bundle.cjs.map +4 -4
- package/dist/esm/canonical.js +37 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/library.js +84 -0
- package/dist/esm/normalization.js +101 -1
- package/dist/esm/runner.js +201 -129
- package/dist/esm/serialization.js +3 -0
- package/dist/esm/store.js +25 -5
- package/dist/types/canonical.d.ts +3 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/library.d.ts +8 -0
- package/dist/types/normalization.d.ts +15 -0
- package/dist/types/runner.d.ts +58 -2
- package/dist/types/store.d.ts +10 -3
- package/dist/types/types.d.ts +74 -2
- package/package.json +2 -2
package/dist/esm/canonical.js
CHANGED
|
@@ -100,10 +100,46 @@ export function fingerprintEvaluationDataset(value) {
|
|
|
100
100
|
* accepting a snapshot that can no longer prove which cases were run.
|
|
101
101
|
*/
|
|
102
102
|
export function assertEvaluationDatasetSnapshot(snapshot) {
|
|
103
|
-
if (
|
|
103
|
+
if (typeof snapshot.projectId !== 'string' ||
|
|
104
|
+
snapshot.projectId.length === 0 ||
|
|
105
|
+
typeof snapshot.fingerprint !== 'string' ||
|
|
106
|
+
snapshot.fingerprint.length === 0 ||
|
|
107
|
+
typeof snapshot.createdAt !== 'string' ||
|
|
108
|
+
!Number.isFinite(Date.parse(snapshot.createdAt))) {
|
|
109
|
+
throw new Error('An evaluation dataset snapshot has invalid identity or creation time.');
|
|
110
|
+
}
|
|
111
|
+
if (snapshot.dataset.projectId !== snapshot.projectId) {
|
|
104
112
|
throw new Error('An evaluation dataset snapshot must belong to its project.');
|
|
105
113
|
}
|
|
106
114
|
if (fingerprintEvaluationDataset(snapshot.dataset) !== snapshot.fingerprint) {
|
|
107
115
|
throw new Error('An evaluation dataset snapshot fingerprint must match its dataset ID, fields, and cases.');
|
|
108
116
|
}
|
|
109
117
|
}
|
|
118
|
+
/** Rejects malformed replay evidence at both write and durable-read boundaries. */
|
|
119
|
+
export function assertEvaluationRecordingArtifact(value) {
|
|
120
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
121
|
+
throw new Error('An evaluation recording artifact must be an object.');
|
|
122
|
+
}
|
|
123
|
+
const artifact = value;
|
|
124
|
+
if (typeof artifact.projectId !== 'string' ||
|
|
125
|
+
artifact.projectId.length === 0 ||
|
|
126
|
+
typeof artifact.runId !== 'string' ||
|
|
127
|
+
artifact.runId.length === 0 ||
|
|
128
|
+
typeof artifact.trialId !== 'string' ||
|
|
129
|
+
artifact.trialId.length === 0 ||
|
|
130
|
+
typeof artifact.serialized !== 'string' ||
|
|
131
|
+
typeof artifact.createdAt !== 'string' ||
|
|
132
|
+
!Number.isFinite(Date.parse(artifact.createdAt))) {
|
|
133
|
+
throw new Error('An evaluation recording artifact has invalid identity, payload, or creation time.');
|
|
134
|
+
}
|
|
135
|
+
const reference = artifact.reference;
|
|
136
|
+
if (typeof reference !== 'object' ||
|
|
137
|
+
reference === null ||
|
|
138
|
+
typeof reference.id !== 'string' ||
|
|
139
|
+
reference.id.length === 0 ||
|
|
140
|
+
!['temporary', 'failure', 'baseline', 'retained'].includes(reference.retention ?? '') ||
|
|
141
|
+
(reference.expiresAt !== undefined &&
|
|
142
|
+
(typeof reference.expiresAt !== 'string' || !Number.isFinite(Date.parse(reference.expiresAt))))) {
|
|
143
|
+
throw new Error('An evaluation recording artifact has an invalid retention reference.');
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { localizeEvaluationDataset } from './datasetTransfer.js';
|
|
2
|
+
import { createEmptyEvaluationProjectData, deserializeEvaluationProjectData } from './serialization.js';
|
|
3
|
+
export function createEmptyEvaluationLibrary() {
|
|
4
|
+
return {
|
|
5
|
+
version: 1,
|
|
6
|
+
data: createEmptyEvaluationProjectData(),
|
|
7
|
+
datasets: [],
|
|
8
|
+
migratedLegacyProjectIds: [],
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Repairs a library resource-by-resource so one malformed suite, baseline, or
|
|
13
|
+
* dataset cannot hide unrelated valid resources. The top-level persistence
|
|
14
|
+
* envelope is validated by the owning store before this function is called.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeEvaluationLibrary(value) {
|
|
17
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
18
|
+
return createEmptyEvaluationLibrary();
|
|
19
|
+
const candidate = value;
|
|
20
|
+
let data = createEmptyEvaluationProjectData();
|
|
21
|
+
try {
|
|
22
|
+
data = deserializeEvaluationProjectData(candidate.data);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
const dataCandidate = typeof candidate.data === 'object' && candidate.data !== null && !Array.isArray(candidate.data)
|
|
26
|
+
? candidate.data
|
|
27
|
+
: undefined;
|
|
28
|
+
const suites = Array.isArray(dataCandidate?.suites)
|
|
29
|
+
? dataCandidate.suites.flatMap((suite) => {
|
|
30
|
+
try {
|
|
31
|
+
return [deserializeEvaluationProjectData({ version: 1, suites: [suite], baselines: [] }).suites[0]];
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
: [];
|
|
38
|
+
const baselines = Array.isArray(dataCandidate?.baselines)
|
|
39
|
+
? dataCandidate.baselines.flatMap((baseline) => {
|
|
40
|
+
try {
|
|
41
|
+
return [deserializeEvaluationProjectData({ version: 1, suites: [], baselines: [baseline] }).baselines[0]];
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
: [];
|
|
48
|
+
const suiteIds = new Set(suites.map((suite) => suite.id));
|
|
49
|
+
data = { version: 1, suites, baselines: baselines.filter((baseline) => suiteIds.has(baseline.suiteId)) };
|
|
50
|
+
}
|
|
51
|
+
const datasets = Array.isArray(candidate.datasets)
|
|
52
|
+
? candidate.datasets.flatMap((dataset) => {
|
|
53
|
+
try {
|
|
54
|
+
return [localizeEvaluationDataset(dataset)];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
: [];
|
|
61
|
+
const suites = uniqueById(data.suites);
|
|
62
|
+
const suiteIds = new Set(suites.map((suite) => suite.id));
|
|
63
|
+
return {
|
|
64
|
+
version: 1,
|
|
65
|
+
data: {
|
|
66
|
+
...data,
|
|
67
|
+
suites,
|
|
68
|
+
baselines: uniqueById(data.baselines).filter((baseline) => suiteIds.has(baseline.suiteId)),
|
|
69
|
+
},
|
|
70
|
+
datasets: uniqueById(datasets),
|
|
71
|
+
migratedLegacyProjectIds: Array.isArray(candidate.migratedLegacyProjectIds)
|
|
72
|
+
? Array.from(new Set(candidate.migratedLegacyProjectIds.filter((id) => typeof id === 'string')))
|
|
73
|
+
: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function uniqueById(values) {
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
return values.filter((value) => {
|
|
79
|
+
if (seen.has(value.id))
|
|
80
|
+
return false;
|
|
81
|
+
seen.add(value.id);
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -19,6 +19,14 @@ function isRunExecutionStatus(value) {
|
|
|
19
19
|
function isTerminalRunExecutionStatus(status) {
|
|
20
20
|
return status === 'completed' || status === 'canceled' || status === 'error';
|
|
21
21
|
}
|
|
22
|
+
function reconcileRecordingReference(primary, secondary) {
|
|
23
|
+
if (!primary)
|
|
24
|
+
return secondary;
|
|
25
|
+
if (!secondary)
|
|
26
|
+
return primary;
|
|
27
|
+
const rank = (retention) => retention === 'temporary' ? 0 : retention === 'failure' ? 1 : 2;
|
|
28
|
+
return rank(secondary.retention) > rank(primary.retention) ? secondary : primary;
|
|
29
|
+
}
|
|
22
30
|
/**
|
|
23
31
|
* Orders snapshots for one run without allowing an equal-revision progress
|
|
24
32
|
* update to demote a terminal result. Writers and delayed UI reads share this
|
|
@@ -31,7 +39,80 @@ export function shouldReplaceEvaluationRun(existing, incoming) {
|
|
|
31
39
|
const incomingRevision = incoming.revision ?? 0;
|
|
32
40
|
if (incomingRevision !== existingRevision)
|
|
33
41
|
return incomingRevision > existingRevision;
|
|
34
|
-
return !isTerminalRunExecutionStatus(existing.executionStatus) || isTerminalRunExecutionStatus(incoming.executionStatus);
|
|
42
|
+
return (!isTerminalRunExecutionStatus(existing.executionStatus) || isTerminalRunExecutionStatus(incoming.executionStatus));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Execution snapshots never own a user-assigned run name. Preserve it when a
|
|
46
|
+
* runner publishes a newer progress or terminal revision after a rename.
|
|
47
|
+
*/
|
|
48
|
+
export function preserveEvaluationRunName(existing, incoming) {
|
|
49
|
+
return existing?.name !== undefined && incoming.name === undefined ? { ...incoming, name: existing.name } : incoming;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Reconciles two snapshots of the same run without discarding evidence.
|
|
53
|
+
*
|
|
54
|
+
* Revisions remain authoritative. Within one revision, however, persistence
|
|
55
|
+
* and recording-retention updates can race with a terminal UI snapshot. Keep
|
|
56
|
+
* the snapshot with the fuller trial set, while still allowing a terminal
|
|
57
|
+
* snapshot to complete a progress snapshot and allowing recording references
|
|
58
|
+
* and warnings to enrich already-settled evidence.
|
|
59
|
+
*/
|
|
60
|
+
export function reconcileEvaluationRunSnapshots(existing, incoming) {
|
|
61
|
+
if (!existing)
|
|
62
|
+
return incoming;
|
|
63
|
+
const existingRevision = existing.revision ?? 0;
|
|
64
|
+
const incomingRevision = incoming.revision ?? 0;
|
|
65
|
+
if (incomingRevision < existingRevision)
|
|
66
|
+
return existing;
|
|
67
|
+
if (incomingRevision > existingRevision)
|
|
68
|
+
return preserveEvaluationRunName(existing, incoming);
|
|
69
|
+
if (!shouldReplaceEvaluationRun(existing, incoming))
|
|
70
|
+
return existing;
|
|
71
|
+
const existingTerminal = isTerminalRunExecutionStatus(existing.executionStatus);
|
|
72
|
+
const incomingTerminal = isTerminalRunExecutionStatus(incoming.executionStatus);
|
|
73
|
+
const existingTrials = existing.trials ?? [];
|
|
74
|
+
const incomingTrials = incoming.trials ?? [];
|
|
75
|
+
const incomingIsPrimary = incomingTerminal !== existingTerminal ? incomingTerminal : incomingTrials.length >= existingTrials.length;
|
|
76
|
+
const primary = preserveEvaluationRunName(existing, incomingIsPrimary ? incoming : existing);
|
|
77
|
+
const secondary = incomingIsPrimary ? existing : incoming;
|
|
78
|
+
const primaryTrials = primary.trials ?? [];
|
|
79
|
+
const secondaryTrialList = secondary.trials ?? [];
|
|
80
|
+
const name = existing.name ?? incoming.name;
|
|
81
|
+
const primaryTrialIds = new Set(primaryTrials.map((trial) => trial.id));
|
|
82
|
+
const secondaryTrials = new Map(secondaryTrialList.map((trial) => [trial.id, trial]));
|
|
83
|
+
return {
|
|
84
|
+
...primary,
|
|
85
|
+
trials: [
|
|
86
|
+
...primaryTrials.map((trial) => {
|
|
87
|
+
const other = secondaryTrials.get(trial.id);
|
|
88
|
+
if (!other)
|
|
89
|
+
return trial;
|
|
90
|
+
const secondaryObservations = new Map((other.observations ?? []).map((observation) => [observation.id, observation]));
|
|
91
|
+
const primaryObservationIds = new Set((trial.observations ?? []).map((observation) => observation.id));
|
|
92
|
+
const recording = reconcileRecordingReference(trial.recording, other.recording);
|
|
93
|
+
return {
|
|
94
|
+
...trial,
|
|
95
|
+
...(recording === undefined ? {} : { recording }),
|
|
96
|
+
observations: [
|
|
97
|
+
...(trial.observations ?? []).map((observation) => {
|
|
98
|
+
const otherObservation = secondaryObservations.get(observation.id);
|
|
99
|
+
const observationRecording = reconcileRecordingReference(observation.recording, otherObservation?.recording);
|
|
100
|
+
return observationRecording === observation.recording
|
|
101
|
+
? observation
|
|
102
|
+
: {
|
|
103
|
+
...observation,
|
|
104
|
+
...(observationRecording === undefined ? {} : { recording: observationRecording }),
|
|
105
|
+
};
|
|
106
|
+
}),
|
|
107
|
+
...(other.observations ?? []).filter((observation) => !primaryObservationIds.has(observation.id)),
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
}),
|
|
111
|
+
...secondaryTrialList.filter((trial) => !primaryTrialIds.has(trial.id)),
|
|
112
|
+
],
|
|
113
|
+
...(name === undefined ? {} : { name }),
|
|
114
|
+
warnings: [...new Set([...(primary.warnings ?? []), ...(secondary.warnings ?? [])])],
|
|
115
|
+
};
|
|
35
116
|
}
|
|
36
117
|
function isPurpose(value) {
|
|
37
118
|
return value === 'evaluation' || value === 'execution-benchmark';
|
|
@@ -184,6 +265,15 @@ export function normalizeEvaluationAggregate(value, trials) {
|
|
|
184
265
|
// as the persisted normalized score and render as an impossible 9900/100.
|
|
185
266
|
if (!isNormalizedScore(normalized.meanScore))
|
|
186
267
|
delete normalized.meanScore;
|
|
268
|
+
if (!isNormalizedScore(normalized.medianScore))
|
|
269
|
+
delete normalized.medianScore;
|
|
270
|
+
if (!isNormalizedScore(normalized.p95Score))
|
|
271
|
+
delete normalized.p95Score;
|
|
272
|
+
if (typeof normalized.medianLatencyMs !== 'number' ||
|
|
273
|
+
!Number.isFinite(normalized.medianLatencyMs) ||
|
|
274
|
+
normalized.medianLatencyMs < 0) {
|
|
275
|
+
delete normalized.medianLatencyMs;
|
|
276
|
+
}
|
|
187
277
|
if (trials) {
|
|
188
278
|
const completed = trials.filter((trial) => trial.executionStatus === 'completed');
|
|
189
279
|
const evaluated = completed.filter((trial) => trial.qualityStatus === 'passed' || trial.qualityStatus === 'failed');
|
|
@@ -347,6 +437,14 @@ export function normalizeEvaluationRun(value) {
|
|
|
347
437
|
// As with legacy trial.status, the old verdict is input-only compatibility
|
|
348
438
|
// data. New v2 values expose one unambiguous qualityStatus instead.
|
|
349
439
|
delete normalized.verdict;
|
|
440
|
+
const name = typeof value.name === 'string' && value.name.trim().length > 0 ? value.name.trim() : undefined;
|
|
441
|
+
delete normalized.name;
|
|
442
|
+
const requestedTrialCount = typeof value.requestedTrialCount === 'number' &&
|
|
443
|
+
Number.isSafeInteger(value.requestedTrialCount) &&
|
|
444
|
+
value.requestedTrialCount >= 0
|
|
445
|
+
? value.requestedTrialCount
|
|
446
|
+
: undefined;
|
|
447
|
+
delete normalized.requestedTrialCount;
|
|
350
448
|
const aggregate = value.aggregate === undefined ? undefined : normalizeEvaluationAggregate(value.aggregate, trials);
|
|
351
449
|
return {
|
|
352
450
|
...normalized,
|
|
@@ -362,6 +460,8 @@ export function normalizeEvaluationRun(value) {
|
|
|
362
460
|
thresholdResults: Array.isArray(value.thresholdResults)
|
|
363
461
|
? structuredClone(value.thresholdResults)
|
|
364
462
|
: [],
|
|
463
|
+
...(name === undefined ? {} : { name }),
|
|
464
|
+
...(requestedTrialCount === undefined ? {} : { requestedTrialCount }),
|
|
365
465
|
...(aggregate === undefined
|
|
366
466
|
? {}
|
|
367
467
|
: { aggregate: accountingStatus === 'partial' ? withoutAuthoritativeCost(aggregate) : aggregate }),
|