@valerypopoff/rivet2-evaluations 2.4.0 → 2.6.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/esm/index.js CHANGED
@@ -9,3 +9,4 @@ export * from './suiteTransfer.js';
9
9
  export * from './dataTypes.js';
10
10
  export * from './normalization.js';
11
11
  export * from './library.js';
12
+ export * from './librarySync.js';
@@ -0,0 +1,118 @@
1
+ import { canonicalStringify } from './canonical.js';
2
+ import { normalizeEvaluationLibrary } from './library.js';
3
+ /**
4
+ * Matches the server's canonical resource versioning while retaining the
5
+ * meaningful distinction between an absent resource and a present JSON value.
6
+ */
7
+ export function evaluationLibraryValueEquals(left, right) {
8
+ if (left === undefined || right === undefined)
9
+ return left === right;
10
+ return canonicalStringify(left) === canonicalStringify(right);
11
+ }
12
+ export function getEvaluationSuiteBundle(library, id) {
13
+ const suite = library.data.suites.find((candidate) => candidate.id === id);
14
+ if (!suite)
15
+ return undefined;
16
+ return {
17
+ suite,
18
+ baselines: library.data.baselines.filter((baseline) => baseline.suiteId === id),
19
+ };
20
+ }
21
+ export function getEvaluationLibraryResource(library, kind, id) {
22
+ if (kind === 'suite')
23
+ return { kind, id, value: getEvaluationSuiteBundle(library, id) };
24
+ return { kind, id, value: library.datasets.find((dataset) => dataset.id === id) };
25
+ }
26
+ function resourceIds(library, kind) {
27
+ return new Set(kind === 'suite' ? library.data.suites.map((suite) => suite.id) : library.datasets.map((dataset) => dataset.id));
28
+ }
29
+ /**
30
+ * Builds the smallest resource batch between two already normalized library
31
+ * snapshots. Legacy migration metadata deliberately has its own explicit
32
+ * server operation and is not silently folded into an editor mutation.
33
+ */
34
+ export function diffEvaluationLibraryMutation(previous, next, versions) {
35
+ const changes = [];
36
+ for (const kind of ['dataset', 'suite']) {
37
+ const ids = new Set([...resourceIds(previous, kind), ...resourceIds(next, kind)]);
38
+ for (const id of [...ids].sort()) {
39
+ if (kind === 'suite') {
40
+ const before = getEvaluationSuiteBundle(previous, id);
41
+ const after = getEvaluationSuiteBundle(next, id);
42
+ if (evaluationLibraryValueEquals(before, after))
43
+ continue;
44
+ const currentVersion = versions.suites[id];
45
+ if (before !== undefined && currentVersion === undefined) {
46
+ throw new Error(`The suite "${id}" has no synchronization version.`);
47
+ }
48
+ const expectedVersion = before === undefined ? null : currentVersion;
49
+ if (after === undefined) {
50
+ changes.push({ kind: 'delete-suite', id, expectedVersion: expectedVersion });
51
+ }
52
+ else {
53
+ changes.push({
54
+ kind: 'put-suite',
55
+ id,
56
+ expectedVersion,
57
+ suite: after.suite,
58
+ baselines: after.baselines,
59
+ });
60
+ }
61
+ }
62
+ else {
63
+ const before = previous.datasets.find((dataset) => dataset.id === id);
64
+ const after = next.datasets.find((dataset) => dataset.id === id);
65
+ if (evaluationLibraryValueEquals(before, after))
66
+ continue;
67
+ const currentVersion = versions.datasets[id];
68
+ if (before !== undefined && currentVersion === undefined) {
69
+ throw new Error(`The dataset "${id}" has no synchronization version.`);
70
+ }
71
+ const expectedVersion = before === undefined ? null : currentVersion;
72
+ if (after === undefined) {
73
+ changes.push({ kind: 'delete-dataset', id, expectedVersion: expectedVersion });
74
+ }
75
+ else {
76
+ changes.push({ kind: 'put-dataset', id, expectedVersion, dataset: after });
77
+ }
78
+ }
79
+ }
80
+ }
81
+ return changes.length === 0 ? undefined : { changes };
82
+ }
83
+ function replaceById(items, value) {
84
+ const index = items.findIndex((item) => item.id === value.id);
85
+ if (index === -1)
86
+ return [...items, value];
87
+ return items.map((item) => (item.id === value.id ? value : item));
88
+ }
89
+ /** Applies a server-validated resource batch while preserving unrelated resources. */
90
+ export function applyEvaluationLibraryMutation(library, mutation) {
91
+ let suites = library.data.suites;
92
+ let baselines = library.data.baselines;
93
+ let datasets = library.datasets;
94
+ for (const change of mutation.changes) {
95
+ switch (change.kind) {
96
+ case 'put-suite':
97
+ suites = replaceById(suites, change.suite);
98
+ baselines = [...baselines.filter((baseline) => baseline.suiteId !== change.id), ...change.baselines];
99
+ break;
100
+ case 'delete-suite':
101
+ suites = suites.filter((suite) => suite.id !== change.id);
102
+ baselines = baselines.filter((baseline) => baseline.suiteId !== change.id);
103
+ break;
104
+ case 'put-dataset':
105
+ datasets = replaceById(datasets, change.dataset);
106
+ break;
107
+ case 'delete-dataset':
108
+ datasets = datasets.filter((dataset) => dataset.id !== change.id);
109
+ break;
110
+ }
111
+ }
112
+ return normalizeEvaluationLibrary({
113
+ version: 1,
114
+ data: { version: 1, suites, baselines },
115
+ datasets,
116
+ migratedLegacyProjectIds: library.migratedLegacyProjectIds,
117
+ });
118
+ }
@@ -9,3 +9,4 @@ export * from './suiteTransfer.js';
9
9
  export * from './dataTypes.js';
10
10
  export * from './normalization.js';
11
11
  export * from './library.js';
12
+ export * from './librarySync.js';
@@ -0,0 +1,17 @@
1
+ import type { EvaluationLibrary, EvaluationLibraryMutation, EvaluationLibraryResourceValue, EvaluationLibraryResourceVersions, EvaluationLibrarySuiteBundle } from './types.js';
2
+ export type { EvaluationLibraryResourceValue, EvaluationLibrarySuiteBundle } from './types.js';
3
+ /**
4
+ * Matches the server's canonical resource versioning while retaining the
5
+ * meaningful distinction between an absent resource and a present JSON value.
6
+ */
7
+ export declare function evaluationLibraryValueEquals(left: unknown, right: unknown): boolean;
8
+ export declare function getEvaluationSuiteBundle(library: EvaluationLibrary, id: string): EvaluationLibrarySuiteBundle | undefined;
9
+ export declare function getEvaluationLibraryResource(library: EvaluationLibrary, kind: 'suite' | 'dataset', id: string): EvaluationLibraryResourceValue;
10
+ /**
11
+ * Builds the smallest resource batch between two already normalized library
12
+ * snapshots. Legacy migration metadata deliberately has its own explicit
13
+ * server operation and is not silently folded into an editor mutation.
14
+ */
15
+ export declare function diffEvaluationLibraryMutation(previous: EvaluationLibrary, next: EvaluationLibrary, versions: EvaluationLibraryResourceVersions): EvaluationLibraryMutation | undefined;
16
+ /** Applies a server-validated resource batch while preserving unrelated resources. */
17
+ export declare function applyEvaluationLibraryMutation(library: EvaluationLibrary, mutation: EvaluationLibraryMutation): EvaluationLibrary;
@@ -170,6 +170,113 @@ export type EvaluationLibrary = {
170
170
  /** Legacy project IDs whose embedded evaluation resources were imported. */
171
171
  migratedLegacyProjectIds: ProjectId[];
172
172
  };
173
+ /**
174
+ * A collaboration boundary for the shared evaluation library. A suite owns
175
+ * its baselines because a baseline is only meaningful with that suite; a
176
+ * dataset is independently editable and reusable by many suites.
177
+ */
178
+ export type EvaluationLibraryResourceKind = 'suite' | 'dataset';
179
+ export type EvaluationLibraryResourceVersions = {
180
+ suites: Record<string, string>;
181
+ datasets: Record<string, string>;
182
+ };
183
+ /** A server-authoritative library snapshot used by collaborative hosts. */
184
+ export type EvaluationLibrarySyncSnapshot = {
185
+ revision: number;
186
+ library: EvaluationLibrary;
187
+ resourceVersions: EvaluationLibraryResourceVersions;
188
+ };
189
+ /**
190
+ * A validated, external change token for the shared evaluation library.
191
+ * `epoch` identifies one SSE generation so reconnects can require a resync
192
+ * even when a server's revision counter has not advanced.
193
+ */
194
+ export type EvaluationLibraryInvalidation = {
195
+ epoch: string;
196
+ revision: number;
197
+ };
198
+ /** The durable value for one independently synchronized library resource. */
199
+ export type EvaluationLibrarySuiteBundle = {
200
+ suite: EvaluationSuite;
201
+ baselines: EvaluationBaselineSnapshot[];
202
+ };
203
+ export type EvaluationLibraryResourceValue = {
204
+ kind: 'suite';
205
+ id: string;
206
+ value: EvaluationLibrarySuiteBundle | undefined;
207
+ } | {
208
+ kind: 'dataset';
209
+ id: string;
210
+ value: EvaluationDataset | undefined;
211
+ };
212
+ /**
213
+ * Every mutation is an all-or-nothing resource batch. Existing resources use
214
+ * their opaque version token; null means that creation requires the resource
215
+ * to remain absent.
216
+ */
217
+ export type EvaluationLibraryMutationChange = {
218
+ kind: 'put-suite';
219
+ id: string;
220
+ expectedVersion: string | null;
221
+ suite: EvaluationSuite;
222
+ baselines: EvaluationBaselineSnapshot[];
223
+ } | {
224
+ kind: 'delete-suite';
225
+ id: string;
226
+ expectedVersion: string;
227
+ } | {
228
+ kind: 'put-dataset';
229
+ id: string;
230
+ expectedVersion: string | null;
231
+ dataset: EvaluationDataset;
232
+ } | {
233
+ kind: 'delete-dataset';
234
+ id: string;
235
+ expectedVersion: string;
236
+ };
237
+ export type EvaluationLibraryMutation = {
238
+ changes: EvaluationLibraryMutationChange[];
239
+ };
240
+ export type EvaluationLibraryConflictResource = {
241
+ kind: EvaluationLibraryResourceKind;
242
+ id: string;
243
+ expectedVersion: string | null;
244
+ currentVersion: string | null;
245
+ };
246
+ /**
247
+ * A conflict is deliberately expressed with both values. Hosts use this to
248
+ * present an explicit choice instead of silently overwriting either editor.
249
+ */
250
+ export type EvaluationLibraryConflictDraft = EvaluationLibraryConflictResource & {
251
+ local: EvaluationLibraryResourceValue;
252
+ server: EvaluationLibraryResourceValue;
253
+ };
254
+ export type EvaluationLibrarySyncIssue = {
255
+ id: string;
256
+ kind: 'conflict';
257
+ message: string;
258
+ conflicts: readonly EvaluationLibraryConflictDraft[];
259
+ } | {
260
+ id: string;
261
+ kind: 'retryable' | 'failed';
262
+ message: string;
263
+ };
264
+ export type EvaluationLibraryConflictResolution = {
265
+ issueId: string;
266
+ kind: EvaluationLibraryResourceKind;
267
+ id: string;
268
+ action: 'use-server' | 'keep-mine-as-copy';
269
+ };
270
+ /** A host can expose this optional capability without changing local stores. */
271
+ export type EvaluationLibrarySyncStore = {
272
+ getLibrarySyncSnapshot(): Promise<EvaluationLibrarySyncSnapshot>;
273
+ mutateLibrary(input: EvaluationLibraryMutation): Promise<EvaluationLibrarySyncSnapshot>;
274
+ subscribeLibraryInvalidation?(listener: (invalidation: EvaluationLibraryInvalidation) => void): () => void;
275
+ /** An unresolved hosted save requiring an explicit user decision. */
276
+ subscribeLibrarySyncIssue?(listener: (issue: EvaluationLibrarySyncIssue | undefined) => void): () => void;
277
+ resolveLibraryConflict?(input: EvaluationLibraryConflictResolution): Promise<EvaluationLibrary>;
278
+ retryLibrarySync?(): Promise<EvaluationLibrary>;
279
+ };
173
280
  export type EvaluationObservationStatus = 'passed' | 'failed' | 'scored' | 'error' | 'skipped';
174
281
  export type EvaluationObservation = {
175
282
  id: string;
@@ -495,6 +602,13 @@ export type EvaluationStore = EvaluationRunStore & {
495
602
  initialize?(): Promise<EvaluationStoreInitialization | void>;
496
603
  getLibrary(): Promise<EvaluationLibrary>;
497
604
  putLibrary(library: EvaluationLibrary): Promise<void>;
605
+ /** Optional hosted capability for resource-scoped collaborative saves. */
606
+ getLibrarySyncSnapshot?: EvaluationLibrarySyncStore['getLibrarySyncSnapshot'];
607
+ mutateLibrary?: EvaluationLibrarySyncStore['mutateLibrary'];
608
+ subscribeLibraryInvalidation?: EvaluationLibrarySyncStore['subscribeLibraryInvalidation'];
609
+ subscribeLibrarySyncIssue?: EvaluationLibrarySyncStore['subscribeLibrarySyncIssue'];
610
+ resolveLibraryConflict?: EvaluationLibrarySyncStore['resolveLibraryConflict'];
611
+ retryLibrarySync?: EvaluationLibrarySyncStore['retryLibrarySync'];
498
612
  };
499
613
  export type EvaluationReporter = {
500
614
  report(run: EvaluationRun): Promise<void> | void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valerypopoff/rivet2-evaluations",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/cjs/bundle.cjs",
6
6
  "module": "dist/esm/index.js",
@@ -36,7 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@valerypopoff/rivet2-core": "^2.4.0",
39
+ "@valerypopoff/rivet2-core": "^2.6.0",
40
40
  "yaml": "^2.9.0"
41
41
  }
42
42
  }