evals-lab 0.1.2

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.
Files changed (110) hide show
  1. package/LICENSE.md +57 -0
  2. package/README.md +98 -0
  3. package/bin/evals-lab.js +196 -0
  4. package/lab/demo/CREDITS.md +92 -0
  5. package/lab/demo/datasets/demo-1.json +1839 -0
  6. package/lab/demo/datasets/demo-2.json +1683 -0
  7. package/lab/demo/manifest.json +12 -0
  8. package/lab/demo/pipelines/demo-1.json +104 -0
  9. package/lab/demo/pipelines/demo-2.json +104 -0
  10. package/lab/demo/sources/Demo 1/basketball-hoop.jpg +0 -0
  11. package/lab/demo/sources/Demo 1/blue-boardwalk.jpg +0 -0
  12. package/lab/demo/sources/Demo 1/busy-beach.jpg +0 -0
  13. package/lab/demo/sources/Demo 1/butcher-sign.jpg +0 -0
  14. package/lab/demo/sources/Demo 1/cactus-flower.jpg +0 -0
  15. package/lab/demo/sources/Demo 1/corner-shop.jpg +0 -0
  16. package/lab/demo/sources/Demo 1/crosswalk-cyclist-panorama.jpg +0 -0
  17. package/lab/demo/sources/Demo 1/cyanotype-room.jpg +0 -0
  18. package/lab/demo/sources/Demo 1/dead-end-sign.jpg +0 -0
  19. package/lab/demo/sources/Demo 1/dog-in-snow.jpg +0 -0
  20. package/lab/demo/sources/Demo 1/empty-bedroom.jpg +0 -0
  21. package/lab/demo/sources/Demo 1/farmers-market-stall.jpg +0 -0
  22. package/lab/demo/sources/Demo 1/four-jets.jpg +0 -0
  23. package/lab/demo/sources/Demo 1/german-shepherd.jpg +0 -0
  24. package/lab/demo/sources/Demo 1/harbour-bridge-dusk.jpg +0 -0
  25. package/lab/demo/sources/Demo 1/harrow-on-the-hill-sign.jpg +0 -0
  26. package/lab/demo/sources/Demo 1/hong-kong-street.jpg +0 -0
  27. package/lab/demo/sources/Demo 1/house-salisbury-street.jpg +0 -0
  28. package/lab/demo/sources/Demo 1/infrared-orchard.jpg +0 -0
  29. package/lab/demo/sources/Demo 1/keyboard-desk.jpg +0 -0
  30. package/lab/demo/sources/Demo 1/lighthouse-dunes.jpg +0 -0
  31. package/lab/demo/sources/Demo 1/mountain-lake.jpg +0 -0
  32. package/lab/demo/sources/Demo 1/museum-skeleton.jpg +0 -0
  33. package/lab/demo/sources/Demo 1/music-room.jpg +0 -0
  34. package/lab/demo/sources/Demo 1/old-red-car.jpg +0 -0
  35. package/lab/demo/sources/Demo 1/parked-car-plate.jpg +0 -0
  36. package/lab/demo/sources/Demo 1/phone-and-wallet.png +0 -0
  37. package/lab/demo/sources/Demo 1/phone-box.jpg +0 -0
  38. package/lab/demo/sources/Demo 1/pink-flamingo.jpg +0 -0
  39. package/lab/demo/sources/Demo 1/postcards.jpg +0 -0
  40. package/lab/demo/sources/Demo 1/red-roof-church.jpg +0 -0
  41. package/lab/demo/sources/Demo 1/roadside-mailboxes.jpg +0 -0
  42. package/lab/demo/sources/Demo 1/rodeo.jpg +0 -0
  43. package/lab/demo/sources/Demo 1/running-tap.tif +0 -0
  44. package/lab/demo/sources/Demo 1/snail-on-stem.jpg +0 -0
  45. package/lab/demo/sources/Demo 1/two-horses-field.jpg +0 -0
  46. package/lab/demo/sources/Demo 1/university-sign.jpg +0 -0
  47. package/lab/demo/sources/Demo 1/vegetable-crates.jpg +0 -0
  48. package/lab/demo/sources/Demo 1/watermarked-car.jpg +0 -0
  49. package/lab/demo/sources/Demo 1/watermarked-pills.jpg +0 -0
  50. package/lab/demo/sources/Demo 1/whiteboard-delegate.jpg +0 -0
  51. package/lab/demo/sources/Demo 1/whiteboard-germ-layers.jpg +0 -0
  52. package/lab/demo/sources/Demo 2/aerial-city.jpg +0 -0
  53. package/lab/demo/sources/Demo 2/alligator-pen.jpg +0 -0
  54. package/lab/demo/sources/Demo 2/arm-tattoo.jpg +0 -0
  55. package/lab/demo/sources/Demo 2/bed-and-plant.jpg +0 -0
  56. package/lab/demo/sources/Demo 2/bright-bedroom.jpg +0 -0
  57. package/lab/demo/sources/Demo 2/car-headlight.jpg +0 -0
  58. package/lab/demo/sources/Demo 2/child-in-surf.jpg +0 -0
  59. package/lab/demo/sources/Demo 2/city-highway.jpg +0 -0
  60. package/lab/demo/sources/Demo 2/corner-bakery.jpg +0 -0
  61. package/lab/demo/sources/Demo 2/cyclist-yellow-jacket.jpg +0 -0
  62. package/lab/demo/sources/Demo 2/excavator-street-signs.jpg +0 -0
  63. package/lab/demo/sources/Demo 2/globe-closeup.jpg +0 -0
  64. package/lab/demo/sources/Demo 2/harbour-village.jpg +0 -0
  65. package/lab/demo/sources/Demo 2/high-street-walkers.jpg +0 -0
  66. package/lab/demo/sources/Demo 2/hillside-rooftops.jpg +0 -0
  67. package/lab/demo/sources/Demo 2/house-at-night.jpg +0 -0
  68. package/lab/demo/sources/Demo 2/ivy-leaves.jpg +0 -0
  69. package/lab/demo/sources/Demo 2/man-with-alligator.jpg +0 -0
  70. package/lab/demo/sources/Demo 2/orange-mailbox-hedge.jpg +0 -0
  71. package/lab/demo/sources/Demo 2/pedal-boats.jpg +0 -0
  72. package/lab/demo/sources/Demo 2/pink-trees-vignette.jpg +0 -0
  73. package/lab/demo/sources/Demo 2/please-leave-quietly-sign.jpg +0 -0
  74. package/lab/demo/sources/Demo 2/railroad-crossing-sign.jpg +0 -0
  75. package/lab/demo/sources/Demo 2/roadworks-sign.jpg +0 -0
  76. package/lab/demo/sources/Demo 2/roundabout-sign.jpg +0 -0
  77. package/lab/demo/sources/Demo 2/sea-cave.jpg +0 -0
  78. package/lab/demo/sources/Demo 2/shadow-on-sand.jpg +0 -0
  79. package/lab/demo/sources/Demo 2/signpost-a404.jpg +0 -0
  80. package/lab/demo/sources/Demo 2/street-fruit-cart.jpg +0 -0
  81. package/lab/demo/sources/Demo 2/street-sign-parnassusweg.jpg +0 -0
  82. package/lab/demo/sources/Demo 2/two-horses-close.jpg +0 -0
  83. package/lab/demo/sources/Demo 2/victorian-house.jpg +0 -0
  84. package/lab/demo/sources/Demo 2/vintage-dashboard.jpg +0 -0
  85. package/lab/demo/sources/Demo 2/volcano-at-dusk.jpg +0 -0
  86. package/lab/demo/sources/Demo 2/wall-camera.jpg +0 -0
  87. package/lab/demo/sources/Demo 2/watch-for-rocks-sign.jpg +0 -0
  88. package/lab/demo/sources/Demo 2/waterfall.jpg +0 -0
  89. package/lab/demo/sources/Demo 2/white-domes.jpg +0 -0
  90. package/lab/demo/sources/Demo 2/whiteboard-meeting-notes.jpg +0 -0
  91. package/lab/demo/sources/Demo 2/whiteboard-messages.jpg +0 -0
  92. package/lab/demo/sources/Demo 2/wooden-house-fence.jpg +0 -0
  93. package/lab/evals-core.mjs +4526 -0
  94. package/lab/flows/wdl.mjs +373 -0
  95. package/lab/js-yaml.mjs +3851 -0
  96. package/lab/kinds/list.mjs +411 -0
  97. package/lab/metrics/builtin.mjs +353 -0
  98. package/lab/presets.json +62 -0
  99. package/lab/run-evals.js +1262 -0
  100. package/lab/server.py +5044 -0
  101. package/lab/web/dist/assets/dist-DI3ewZYj.js +1 -0
  102. package/lab/web/dist/assets/gallery-DipkRvqJ.js +3 -0
  103. package/lab/web/dist/assets/gallery-o7c4lfpn.css +1 -0
  104. package/lab/web/dist/assets/main-CpVssvrb.js +18 -0
  105. package/lab/web/dist/assets/main-DjQQums6.css +1 -0
  106. package/lab/web/dist/assets/tokens-B9intIuT.js +51 -0
  107. package/lab/web/dist/assets/tokens-s6I-RMVq.css +1 -0
  108. package/lab/web/dist/gallery.html +18 -0
  109. package/lab/web/dist/index.html +23 -0
  110. package/package.json +20 -0
@@ -0,0 +1,373 @@
1
+ // Power Automate's expressions -- the Workflow Definition Language -- as a
2
+ // Power Automate step's request template holds them (docs/power-automate.md
3
+ // § The HTTP Request step). The lab evaluates a template against a record's
4
+ // scope to rebuild the request a flow would send with new words, so the
5
+ // evaluator has to write a value the way Power Automate does: the fidelity
6
+ // check (a record's request, rebuilt and compared byte for byte) is what
7
+ // holds it to that, on real runs.
8
+ //
9
+ // A string that begins `@` is one expression and its value is whatever the
10
+ // expression gives; `@{…}` inside a string writes each value into the text;
11
+ // `@@` at the start is a literal @. Function names are case-insensitive.
12
+ // A function the lab does not evaluate is refused by name rather than
13
+ // guessed at.
14
+
15
+ /** What an expression reads: a record's scope (flows/record.ts). */
16
+
17
+
18
+
19
+
20
+
21
+
22
+
23
+
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+
34
+ export class WdlError extends Error {}
35
+
36
+ const isObj = (v ) => v != null && typeof v === "object" && !Array.isArray(v);
37
+
38
+ // ---- reading an expression ------------------------------------------------
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+ function tokens(src ) {
48
+ const out = [];
49
+ let i = 0;
50
+ while (i < src.length) {
51
+ const c = src[i] ;
52
+ if (/\s/.test(c)) { i++; continue; }
53
+ if (c === "'") {
54
+ let s = "";
55
+ i++;
56
+ for (;;) {
57
+ if (i >= src.length) throw new WdlError("a quoted string is never closed");
58
+ if (src[i] === "'") {
59
+ if (src[i + 1] === "'") { s += "'"; i += 2; continue; }
60
+ i++;
61
+ break;
62
+ }
63
+ s += src[i++];
64
+ }
65
+ out.push({ t: "str", v: s });
66
+ continue;
67
+ }
68
+ const num = /^-?\d+(\.\d+)?([eE][-+]?\d+)?/.exec(src.slice(i));
69
+ if (num && (c === "-" ? /\d/.test(src[i + 1] ?? "") : true) && /[-\d]/.test(c)) {
70
+ out.push({ t: "num", v: num[0] });
71
+ i += num[0].length;
72
+ continue;
73
+ }
74
+ const id = /^[A-Za-z_$][\w$]*/.exec(src.slice(i));
75
+ if (id) { out.push({ t: "id", v: id[0] }); i += id[0].length; continue; }
76
+ if ("()[],.?".includes(c)) { out.push({ t: "p", v: c }); i++; continue; }
77
+ throw new WdlError(`"${c}" cannot start anything in an expression`);
78
+ }
79
+ return out;
80
+ }
81
+
82
+ function parse(src ) {
83
+ const ts = tokens(src);
84
+ let at = 0;
85
+ const peek = (v ) => (v == null ? ts[at] : ts[at]?.t === "p" && ts[at]?.v === v ? ts[at] : undefined);
86
+ const want = (v ) => {
87
+ if (!peek(v)) throw new WdlError(`expected "${v}" in ${src}`);
88
+ at++;
89
+ };
90
+ const expr = () => {
91
+ const tok = ts[at++];
92
+ if (!tok) throw new WdlError(`${src} ends too soon`);
93
+ let node ;
94
+ if (tok.t === "str") node = { k: "lit", v: tok.v };
95
+ else if (tok.t === "num") node = { k: "lit", v: Number(tok.v) };
96
+ else if (tok.t === "id") {
97
+ const low = tok.v.toLowerCase();
98
+ if (!peek("(") && (low === "true" || low === "false" || low === "null")) {
99
+ node = { k: "lit", v: low === "null" ? null : low === "true" };
100
+ } else {
101
+ want("(");
102
+ const args = [];
103
+ if (!peek(")")) {
104
+ args.push(expr());
105
+ while (peek(",")) { at++; args.push(expr()); }
106
+ }
107
+ want(")");
108
+ node = { k: "call", name: low, args };
109
+ }
110
+ } else throw new WdlError(`"${tok.v}" cannot start an expression`);
111
+ // Property access: ['x'], ?['x'], .x, ?.x
112
+ for (;;) {
113
+ const safe = !!peek("?");
114
+ if (safe) at++;
115
+ if (peek("[")) {
116
+ at++;
117
+ const key = expr();
118
+ want("]");
119
+ node = { k: "get", of: node, key, safe };
120
+ } else if (peek(".")) {
121
+ at++;
122
+ const name = ts[at++];
123
+ if (name?.t !== "id") throw new WdlError(`a property name is wanted after "." in ${src}`);
124
+ node = { k: "get", of: node, key: { k: "lit", v: name.v }, safe };
125
+ } else {
126
+ if (safe) throw new WdlError(`"?" has to be followed by [ or . in ${src}`);
127
+ return node;
128
+ }
129
+ }
130
+ };
131
+ const node = expr();
132
+ if (at < ts.length) throw new WdlError(`${src} goes on after its expression ends`);
133
+ return node;
134
+ }
135
+
136
+ // ---- writing a value -------------------------------------------------------
137
+
138
+ /** A value as Power Automate writes it into text: an array or object as
139
+ compact JSON -- measured on the spike's run -- a boolean as True or
140
+ False, null as nothing. */
141
+ export function asText(v ) {
142
+ if (v == null) return "";
143
+ if (typeof v === "string") return v;
144
+ if (typeof v === "boolean") return v ? "True" : "False";
145
+ if (typeof v === "number") return String(v);
146
+ return JSON.stringify(v);
147
+ }
148
+
149
+ // ---- functions ---------------------------------------------------------------
150
+
151
+
152
+
153
+ const str = (v ) => asText(v);
154
+ const num = (v , fn ) => {
155
+ const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v) : NaN;
156
+ if (Number.isNaN(n)) throw new WdlError(`${fn}() wants a number, and was given ${JSON.stringify(v)}`);
157
+ return n;
158
+ };
159
+ const list = (v , fn ) => {
160
+ if (Array.isArray(v)) return v;
161
+ throw new WdlError(`${fn}() wants an array, and was given ${JSON.stringify(v)}`);
162
+ };
163
+ const equal = (a , b ) => JSON.stringify(a) === JSON.stringify(b);
164
+ const got = (v , what ) => {
165
+ if (v === undefined) throw new WdlError(`${what} is not in the record`);
166
+ return v;
167
+ };
168
+ const name = (args , fn ) => {
169
+ if (typeof args[0] !== "string") throw new WdlError(`${fn}() names what it reads`);
170
+ return args[0];
171
+ };
172
+
173
+ /** A date as ISO 8601 with milliseconds and a Z, as utcNow() writes one. */
174
+ const iso = (d ) => d.toISOString().replace(/\.(\d{3})Z$/, ".$10000Z");
175
+ const date = (v , fn ) => {
176
+ const d = new Date(str(v));
177
+ if (Number.isNaN(d.getTime())) throw new WdlError(`${fn}() wants a time, and was given ${JSON.stringify(v)}`);
178
+ return d;
179
+ };
180
+ /** .NET's custom date format, the tokens flows use. */
181
+ function formatDate(d , fmt ) {
182
+ if (!fmt || fmt === "o") return iso(d);
183
+ const p = (n , w = 2) => String(n).padStart(w, "0");
184
+ const parts = {
185
+ yyyy: String(d.getUTCFullYear()), yy: p(d.getUTCFullYear() % 100), MM: p(d.getUTCMonth() + 1),
186
+ M: String(d.getUTCMonth() + 1), dd: p(d.getUTCDate()), d: String(d.getUTCDate()), HH: p(d.getUTCHours()),
187
+ H: String(d.getUTCHours()), mm: p(d.getUTCMinutes()), ss: p(d.getUTCSeconds()), fff: p(d.getUTCMilliseconds(), 3),
188
+ };
189
+ return fmt.replace(/yyyy|yy|MM|M|dd|d|HH|H|mm|ss|fff/g, (t) => parts[t] );
190
+ }
191
+ const shift = (unit ) => ([ts, n, fmt]) =>
192
+ formatDate(new Date(date(ts, "add").getTime() + num(n, "add") * unit), fmt );
193
+
194
+ const FUNCTIONS = {
195
+ // What the flow read.
196
+ triggerbody: (_, c) => c.scope.trigger?.body ?? null,
197
+ triggeroutputs: (_, c) => ({ body: c.scope.trigger?.body ?? null, headers: c.scope.trigger?.headers ?? {} }),
198
+ trigger: (_, c) => ({ outputs: { body: c.scope.trigger?.body ?? null, headers: c.scope.trigger?.headers ?? {} } }),
199
+ body: (a, c) => got(c.scope.actions?.[name(a, "body")], `body('${a[0]}')`).body ?? null,
200
+ actionbody: (a, c) => got(c.scope.actions?.[name(a, "actionBody")], `body('${a[0]}')`).body ?? null,
201
+ outputs: (a, c) => got(c.scope.actions?.[name(a, "outputs")], `outputs('${a[0]}')`),
202
+ actionoutputs: (a, c) => got(c.scope.actions?.[name(a, "actionOutputs")], `outputs('${a[0]}')`),
203
+ actions: (a, c) => ({ outputs: got(c.scope.actions?.[name(a, "actions")], `actions('${a[0]}')`) }),
204
+ variables: (a, c) => got(c.scope.variables?.[name(a, "variables")], `variables('${a[0]}')`),
205
+ items: (a, c) => got(c.scope.items?.[name(a, "items")], `items('${a[0]}')`),
206
+ item: (_, c) => {
207
+ if (!c.loop) throw new WdlError("item() is only inside a loop");
208
+ return got(c.scope.items?.[c.loop], "item()");
209
+ },
210
+ parameters: (a, c) => got(c.parameters?.[name(a, "parameters")], `parameters('${a[0]}')`),
211
+ utcnow: ([fmt], c) => {
212
+ if (!c.now) throw new WdlError("utcNow() needs the time of the call, and this record has none");
213
+ return formatDate(new Date(c.now), fmt );
214
+ },
215
+ // Text.
216
+ concat: (a) => a.map(str).join(""),
217
+ substring: ([s, start, len]) => {
218
+ const t = str(s), b = num(start, "substring");
219
+ return len == null ? t.slice(b) : t.slice(b, b + num(len, "substring"));
220
+ },
221
+ slice: ([s, start, end]) => str(s).slice(num(start, "slice"), end == null ? undefined : num(end, "slice")),
222
+ replace: ([s, from, to]) => str(s).split(str(from)).join(str(to)),
223
+ tolower: ([s]) => str(s).toLowerCase(),
224
+ toupper: ([s]) => str(s).toUpperCase(),
225
+ trim: ([s]) => str(s).trim(),
226
+ indexof: ([s, t]) => str(s).toLowerCase().indexOf(str(t).toLowerCase()),
227
+ lastindexof: ([s, t]) => str(s).toLowerCase().lastIndexOf(str(t).toLowerCase()),
228
+ startswith: ([s, t]) => str(s).toLowerCase().startsWith(str(t).toLowerCase()),
229
+ endswith: ([s, t]) => str(s).toLowerCase().endsWith(str(t).toLowerCase()),
230
+ split: ([s, sep]) => str(s).split(str(sep)),
231
+ // Collections, and text as one.
232
+ contains: ([c, v]) => typeof c === "string" ? c.includes(str(v))
233
+ : Array.isArray(c) ? c.some((x) => equal(x, v)) : isObj(c) ? str(v) in c : false,
234
+ empty: ([v]) => v == null || (typeof v === "string" || Array.isArray(v) ? v.length === 0 : isObj(v) && !Object.keys(v).length),
235
+ first: ([v]) => (typeof v === "string" ? v[0] ?? "" : list(v, "first")[0] ?? null),
236
+ last: ([v]) => (typeof v === "string" ? v.at(-1) ?? "" : list(v, "last").at(-1) ?? null),
237
+ length: ([v]) => (typeof v === "string" ? v.length : list(v, "length").length),
238
+ join: ([v, sep]) => list(v, "join").map(str).join(str(sep)),
239
+ take: ([v, n]) => (typeof v === "string" ? v.slice(0, num(n, "take")) : list(v, "take").slice(0, num(n, "take"))),
240
+ skip: ([v, n]) => (typeof v === "string" ? v.slice(num(n, "skip")) : list(v, "skip").slice(num(n, "skip"))),
241
+ union: (a) => Array.isArray(a[0]) ? a.flatMap((x) => list(x, "union")).filter((x, i, all) => all.findIndex((y) => equal(x, y)) === i)
242
+ : Object.assign({}, ...a),
243
+ intersection: (a) => list(a[0], "intersection").filter((x) => a.slice(1).every((o) => list(o, "intersection").some((y) => equal(x, y)))),
244
+ createarray: (a) => a,
245
+ reverse: ([v]) => [...list(v, "reverse")].reverse(),
246
+ range: ([s, n]) => Array.from({ length: num(n, "range") }, (_, i) => num(s, "range") + i),
247
+ // Logic.
248
+ equals: ([a, b]) => equal(a, b),
249
+ and: (a) => a.every(Boolean),
250
+ or: (a) => a.some(Boolean),
251
+ not: ([v]) => !v,
252
+ if: ([c, a, b]) => (c ? a : b),
253
+ greater: ([a, b]) => (a ) > (b ),
254
+ greaterorequals: ([a, b]) => (a ) >= (b ),
255
+ less: ([a, b]) => (a ) < (b ),
256
+ lessorequals: ([a, b]) => (a ) <= (b ),
257
+ coalesce: (a) => a.find((v) => v != null) ?? null,
258
+ // Conversion.
259
+ string: ([v]) => str(v),
260
+ int: ([v]) => Math.trunc(num(v, "int")),
261
+ float: ([v]) => num(v, "float"),
262
+ bool: ([v]) => (typeof v === "string" ? v.toLowerCase() === "true" : !!v),
263
+ json: ([v]) => {
264
+ if (typeof v !== "string") return v;
265
+ try { return JSON.parse(v); } catch { throw new WdlError(`json() could not read ${JSON.stringify(v.slice(0, 40))}`); }
266
+ },
267
+ array: ([v]) => [v],
268
+ base64: ([v]) => Bytes.from(str(v), "utf8").toString("base64"),
269
+ base64tostring: ([v]) => Bytes.from(str(v), "base64").toString("utf8"),
270
+ decodebase64: ([v]) => Bytes.from(str(v), "base64").toString("utf8"),
271
+ encodeuricomponent: ([v]) => encodeURIComponent(str(v)),
272
+ uricomponent: ([v]) => encodeURIComponent(str(v)),
273
+ uricomponenttostring: ([v]) => decodeURIComponent(str(v)),
274
+ decodeuricomponent: ([v]) => decodeURIComponent(str(v)),
275
+ // Numbers.
276
+ add: ([a, b]) => num(a, "add") + num(b, "add"),
277
+ sub: ([a, b]) => num(a, "sub") - num(b, "sub"),
278
+ mul: ([a, b]) => num(a, "mul") * num(b, "mul"),
279
+ div: ([a, b]) => {
280
+ const x = num(a, "div"), y = num(b, "div");
281
+ return Number.isInteger(x) && Number.isInteger(y) ? Math.trunc(x / y) : x / y;
282
+ },
283
+ mod: ([a, b]) => num(a, "mod") % num(b, "mod"),
284
+ min: (a) => Math.min(...(Array.isArray(a[0]) ? a[0] : a).map((v) => num(v, "min"))),
285
+ max: (a) => Math.max(...(Array.isArray(a[0]) ? a[0] : a).map((v) => num(v, "max"))),
286
+ // Times.
287
+ formatdatetime: ([ts, fmt]) => formatDate(date(ts, "formatDateTime"), fmt ),
288
+ adddays: shift(86_400_000),
289
+ addhours: shift(3_600_000),
290
+ addminutes: shift(60_000),
291
+ addseconds: shift(1_000),
292
+ // Objects.
293
+ addproperty: ([o, k, v]) => ({ ...(o ), [str(k)]: v }),
294
+ setproperty: ([o, k, v]) => ({ ...(o ), [str(k)]: v }),
295
+ removeproperty: ([o, k]) => {
296
+ const { [str(k)]: _gone, ...rest } = o ;
297
+ return rest;
298
+ },
299
+ };
300
+
301
+ // Bytes is Node's; the page has btoa. One path each, so the core's
302
+ // evaluator runs in both.
303
+ const Bytes = (globalThis ).Bytes ?? {
304
+ from: (s , enc ) => ({
305
+ toString: (to ) => enc === "utf8" && to === "base64"
306
+ ? btoa(String.fromCharCode(...new TextEncoder().encode(s)))
307
+ : new TextDecoder().decode(Uint8Array.from(atob(s), (ch) => ch.charCodeAt(0))),
308
+ }),
309
+ };
310
+
311
+ function run(node , ctx ) {
312
+ if (node.k === "lit") return node.v;
313
+ if (node.k === "get") {
314
+ const of = run(node.of, ctx);
315
+ const key = run(node.key, ctx);
316
+ if (of == null) {
317
+ if (node.safe) return null;
318
+ throw new WdlError(`property ${JSON.stringify(key)} cannot be read from null`);
319
+ }
320
+ const v = Array.isArray(of) && typeof key === "number" ? of[key]
321
+ : isObj(of) ? Object.entries(of).find(([k]) => k.toLowerCase() === str(key).toLowerCase())?.[1]
322
+ : undefined;
323
+ if (v === undefined && !node.safe) throw new WdlError(`property ${JSON.stringify(key)} is not there`);
324
+ return v ?? null;
325
+ }
326
+ const fn = FUNCTIONS[node.name];
327
+ if (!fn) throw new WdlError(`${node.name}() is not an expression function this lab evaluates`);
328
+ return fn(node.args.map((a) => run(a, ctx)), ctx);
329
+ }
330
+
331
+ /** One expression's value: `triggerBody()?['text']`, without its `@`. */
332
+ export function evaluateExpression(src , ctx ) {
333
+ return run(parse(src), ctx);
334
+ }
335
+
336
+ /** A string's value: an expression's own value for a string that is one,
337
+ text with each `@{…}` written in otherwise. */
338
+ function evaluateString(s , ctx ) {
339
+ if (s.startsWith("@@")) return s.slice(1);
340
+ if (s.startsWith("@") && !s.startsWith("@{")) return evaluateExpression(s.slice(1), ctx);
341
+ if (!s.includes("@{")) return s;
342
+ let out = "";
343
+ let i = 0;
344
+ while (i < s.length) {
345
+ const open = s.indexOf("@{", i);
346
+ if (open < 0) { out += s.slice(i); break; }
347
+ out += s.slice(i, open);
348
+ const close = endOf(s, open + 2);
349
+ out += asText(evaluateExpression(s.slice(open + 2, close), ctx));
350
+ i = close + 1;
351
+ }
352
+ return out;
353
+ }
354
+
355
+ /** Where the `}` that closes an `@{` is: braces inside quoted strings do not count. */
356
+ function endOf(s , from ) {
357
+ let quoted = false;
358
+ for (let i = from; i < s.length; i++) {
359
+ const c = s[i];
360
+ if (c === "'") quoted = !quoted;
361
+ else if (c === "}" && !quoted) return i;
362
+ }
363
+ throw new WdlError("an @{ is never closed");
364
+ }
365
+
366
+ /** [template] with every expression in it evaluated: each string that is
367
+ one, or holds one, wherever it sits in objects and arrays. */
368
+ export function evaluate(template , ctx ) {
369
+ if (typeof template === "string") return evaluateString(template, ctx);
370
+ if (Array.isArray(template)) return template.map((v) => evaluate(v, ctx));
371
+ if (isObj(template)) return Object.fromEntries(Object.entries(template).map(([k, v]) => [k, evaluate(v, ctx)]));
372
+ return template;
373
+ }