@loadmap/cli 0.0.0-stage → 0.1.1

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.
@@ -0,0 +1,4257 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { createServer } from "node:http";
3
+ import { extname, join, normalize } from "node:path";
4
+ import { parseArgs } from "node:util";
5
+ import { chromium } from "playwright";
6
+ // ../core/src/capacity.ts
7
+ /** A capacity read off a dashboard or a load test. */
8
+ function measured(value, unit, o) {
9
+ return capacity(value, unit, "measured", o);
10
+ }
11
+ /** A capacity from a vendor's figure, a benchmark elsewhere or a guess: the verdict says so. */
12
+ function estimated(value, unit, o) {
13
+ return capacity(value, unit, "estimated", o);
14
+ }
15
+ function capacity(value, unit, provenance, o) {
16
+ const { source, within } = typeof o === "string" ? {
17
+ source: o,
18
+ within: void 0
19
+ } : o ?? {};
20
+ if (within != null && !(within >= 0 && within < 1)) throw new Error(`loadmap: within is a share from 0 to below 1 (0.3 is ±30 %), not ${within}`);
21
+ const c = {
22
+ value,
23
+ unit,
24
+ provenance,
25
+ source
26
+ };
27
+ if (within) c.within = within;
28
+ return c;
29
+ }
30
+ /** The capacity's value at its low or high bound; the value itself without a ±. */
31
+ function boundOf(c, bound) {
32
+ if (!bound || !c.within) return c.value;
33
+ return c.value * (bound === "low" ? 1 - c.within : 1 + c.within);
34
+ }
35
+ // ../core/src/format.ts
36
+ /** A number for a reader: 1,234 / 12.3 / 0.4. */
37
+ function fmt(n) {
38
+ if (!Number.isFinite(n)) return n > 0 ? "∞" : "—";
39
+ if (Math.abs(n) >= 1e3) return Math.round(n).toLocaleString("en-US");
40
+ return (Math.round(n * 10) / 10).toString();
41
+ }
42
+ /** A compact number: 980 / 1.2k / 34k / 1.5M. */
43
+ function compact(n) {
44
+ if (!Number.isFinite(n)) return n > 0 ? "∞" : "—";
45
+ const a = Math.abs(n);
46
+ if (a >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
47
+ if (a >= 1e4) return `${Math.round(n / 1e3)}k`;
48
+ if (a >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
49
+ return Math.round(n).toString();
50
+ }
51
+ /** A load multiple, rounded down so it never promises more than the model: 2.6× / 53×. */
52
+ function multiple(u, searchMax = 2e3) {
53
+ if (u == null) return "—";
54
+ if (!Number.isFinite(u)) return `beyond ${fmt(searchMax)}×`;
55
+ return u >= 10 ? `${Math.floor(u)}×` : `${Math.floor(u * 10) / 10}×`;
56
+ }
57
+ /**
58
+ * Two load multiples: "7.4×" when both edges read the same, else "between 7.4× and 10×" or, short, "7.4×–10×". An
59
+ * edge that never comes reads "7.4× or more" ("≥ 7.4×").
60
+ */
61
+ function multiples(r, searchMax = 2e3, short = false) {
62
+ const [lo, hi] = [multiple(r.low, searchMax), multiple(r.high, searchMax)];
63
+ if (lo === hi) return lo;
64
+ if (!Number.isFinite(r.high)) return short ? `≥ ${lo}` : `${lo} or more`;
65
+ return short ? `${lo}–${hi}` : `between ${lo} and ${hi}`;
66
+ }
67
+ /** When two edges are reached: "(around March 2029)", "(between March 2029 and May 2030)" or "(… or later)". */
68
+ function months(low, high) {
69
+ if (!low) return "";
70
+ if (!high) return ` (${around(low)} or later)`;
71
+ return monthOf(low) === monthOf(high) ? ` (${around(low)})` : ` (between ${monthOf(low)} and ${monthOf(high)})`;
72
+ }
73
+ /** A percentage of a 0–1 share: 50 %. */
74
+ function pct(share) {
75
+ return `${Math.round(share * 100)} %`;
76
+ }
77
+ /** A duration in seconds for a meter: 0 s / 42 s / 3 min / > 1 h / stalled. */
78
+ function secs(s) {
79
+ if (!Number.isFinite(s)) return "stalled";
80
+ if (s < 1) return "0 s";
81
+ if (s < 60) return `${Math.round(s)} s`;
82
+ if (s < 3600) return `${Math.round(s / 60)} min`;
83
+ return "> 1 h";
84
+ }
85
+ /** "a", "a and b", "a, b and c". */
86
+ function joinAnd(items) {
87
+ if (items.length <= 1) return items[0] ?? "";
88
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
89
+ }
90
+ const MONTHS = [
91
+ "January",
92
+ "February",
93
+ "March",
94
+ "April",
95
+ "May",
96
+ "June",
97
+ "July",
98
+ "August",
99
+ "September",
100
+ "October",
101
+ "November",
102
+ "December"
103
+ ];
104
+ /** A month in UTC, "March 2028" or short "Mar 2028". Fixed names, whatever the locale. */
105
+ function monthOf(d, short = false) {
106
+ const name = MONTHS[d.getUTCMonth()];
107
+ return `${short ? name.slice(0, 3) : name} ${d.getUTCFullYear()}`;
108
+ }
109
+ /** A date as a reader plans with it, by month: "around March 2028". */
110
+ function around(d) {
111
+ return `around ${monthOf(d)}`;
112
+ }
113
+ // ../core/src/growth.ts
114
+ const AS_OF = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/;
115
+ /** The day `asOf` names, at midnight UTC; null when it is not a real date. */
116
+ function parseAsOf(asOf) {
117
+ const m = AS_OF.exec(asOf);
118
+ if (!m) return null;
119
+ const [y, mo, d] = [
120
+ Number(m[1]),
121
+ Number(m[2]),
122
+ Number(m[3] ?? 1)
123
+ ];
124
+ const date = new Date(Date.UTC(y, mo - 1, d));
125
+ return date.getUTCFullYear() === y && date.getUTCMonth() === mo - 1 && date.getUTCDate() === d ? date : null;
126
+ }
127
+ function growth(o) {
128
+ if (!(o.rate > 0) || !Number.isFinite(o.rate)) throw new Error(`loadmap: growth needs a rate above 0, not ${o.rate}`);
129
+ if (o.per !== "month" && o.per !== "year") throw new Error(`loadmap: growth is per month or per year, not "${o.per}"`);
130
+ if (!parseAsOf(o.asOf)) throw new Error(`loadmap: growth asOf "${o.asOf}" is not a date like 2026-11-01`);
131
+ return {
132
+ rate: o.rate,
133
+ per: o.per,
134
+ asOf: o.asOf
135
+ };
136
+ }
137
+ /**
138
+ * When the load reaches `multiple`: `ln(multiple) / ln(1 + rate)` periods after `asOf`. Null without a multiple or
139
+ * when it is never reached; `asOf` itself for 1× and below.
140
+ */
141
+ function dateAt(g, multiple) {
142
+ const start = parseAsOf(g.asOf);
143
+ if (!start || multiple == null || !Number.isFinite(multiple)) return null;
144
+ if (multiple <= 1) return start;
145
+ const months = Math.log(multiple) / Math.log(1 + g.rate) * (g.per === "year" ? 12 : 1);
146
+ const whole = Math.floor(months);
147
+ const y = start.getUTCFullYear();
148
+ const mo = start.getUTCMonth() + whole;
149
+ const days = new Date(Date.UTC(y, mo + 1, 0)).getUTCDate();
150
+ const at = Date.UTC(y, mo, Math.min(start.getUTCDate(), days));
151
+ return new Date(at + (months - whole) * days * 864e5);
152
+ }
153
+ // ../core/src/params.ts
154
+ const p = {
155
+ /** The load, as a multiple of today. One per model; it drives the limits. */
156
+ load(o) {
157
+ const min = o.min ?? 1;
158
+ return {
159
+ kind: "load",
160
+ label: o.label ?? "Load, × today",
161
+ min,
162
+ max: o.max,
163
+ step: o.step ?? 1,
164
+ default: o.default ?? min
165
+ };
166
+ },
167
+ select(o) {
168
+ if (!o.options.includes(o.default)) throw new Error(`loadmap: "${o.label}" defaults to ${o.default}, not an option`);
169
+ return {
170
+ kind: "select",
171
+ ...o
172
+ };
173
+ },
174
+ switch(o) {
175
+ return {
176
+ kind: "switch",
177
+ ...o
178
+ };
179
+ },
180
+ range(o) {
181
+ return {
182
+ kind: "range",
183
+ step: 1,
184
+ ...o
185
+ };
186
+ }
187
+ };
188
+ function isEnabled(def, values) {
189
+ return def.kind === "load" || !def.enabled || Boolean(def.enabled(values));
190
+ }
191
+ // ../core/src/stages.ts
192
+ function pass(component, o = {}) {
193
+ return {
194
+ kind: "pass",
195
+ component,
196
+ ...o
197
+ };
198
+ }
199
+ function queue(o) {
200
+ return {
201
+ kind: "queue",
202
+ ...o
203
+ };
204
+ }
205
+ function workers(component, o) {
206
+ return {
207
+ kind: "workers",
208
+ component,
209
+ ...o,
210
+ fill: o.fill ?? "round-robin"
211
+ };
212
+ }
213
+ function store(component, o = {}) {
214
+ return {
215
+ kind: "store",
216
+ component,
217
+ ...o
218
+ };
219
+ }
220
+ function sink(o = {}) {
221
+ return {
222
+ kind: "sink",
223
+ ...o
224
+ };
225
+ }
226
+ function fanout(o) {
227
+ return {
228
+ kind: "fanout",
229
+ ...o
230
+ };
231
+ }
232
+ function dyn(v, ctx) {
233
+ return typeof v === "function" ? v(ctx) : v;
234
+ }
235
+ function resolveStage(id, s, ctx, flowIds) {
236
+ const label = s.label ?? id;
237
+ switch (s.kind) {
238
+ case "queue": return {
239
+ id,
240
+ label,
241
+ kind: "queue",
242
+ component: s.component,
243
+ partitions: dyn(s.partitions, ctx)
244
+ };
245
+ case "workers": return {
246
+ id,
247
+ label,
248
+ kind: "workers",
249
+ component: s.component,
250
+ pods: dyn(s.pods, ctx),
251
+ perPod: dyn(s.perPod, ctx),
252
+ fill: s.fill
253
+ };
254
+ case "fanout": {
255
+ const to = dyn(s.to, ctx);
256
+ const perFlow = {};
257
+ for (const f of flowIds) perFlow[f] = Array.isArray(to) ? to : to[f] ?? [];
258
+ return {
259
+ id,
260
+ label,
261
+ kind: "fanout",
262
+ component: s.component,
263
+ to: perFlow
264
+ };
265
+ }
266
+ case "sink": return {
267
+ id,
268
+ label,
269
+ kind: "sink"
270
+ };
271
+ default: return {
272
+ id,
273
+ label,
274
+ kind: s.kind,
275
+ component: s.component
276
+ };
277
+ }
278
+ }
279
+ // ../core/src/model.ts
280
+ const RESERVED$1 = [
281
+ "load",
282
+ "flows",
283
+ "demand",
284
+ "util",
285
+ "derived"
286
+ ];
287
+ function defineModel(def) {
288
+ const params = def.params ?? {};
289
+ for (const id of Object.keys(params)) if (RESERVED$1.includes(id)) throw new Error(`loadmap: "${id}" is reserved and cannot name a parameter`);
290
+ const flowIds = Object.keys(def.flows);
291
+ const componentIds = Object.keys(def.components);
292
+ const stageIds = Object.keys(def.stages);
293
+ for (const f of flowIds) {
294
+ const route = def.routes[f];
295
+ if (!route?.length) throw new Error(`loadmap: flow "${f}" has no route`);
296
+ route.forEach((s, i) => {
297
+ const st = def.stages[s];
298
+ if (!st) throw new Error(`loadmap: flow "${f}" routes through unknown stage "${s}"`);
299
+ if (st.kind === "fanout" && i < route.length - 1) throw new Error(`loadmap: fan-out "${s}" must be the last stage of the route of "${f}"`);
300
+ });
301
+ }
302
+ const thresholds = {
303
+ target: def.thresholds?.target ?? .5,
304
+ full: def.thresholds?.full ?? 1
305
+ };
306
+ const searchMax = def.searchMax ?? 2e3;
307
+ const growth$1 = def.growth && growth(def.growth);
308
+ const when = (x) => growth$1 && x != null && x > 1 ? dateAt(growth$1, x) : null;
309
+ const paramList = [{
310
+ id: "load",
311
+ def: def.load
312
+ }, ...Object.entries(params).map(([id, d]) => ({
313
+ id,
314
+ def: d
315
+ }))];
316
+ const defaults = () => {
317
+ const v = { load: def.load.default };
318
+ for (const [id, d] of Object.entries(params)) v[id] = d.default;
319
+ return v;
320
+ };
321
+ /** Throws unless every value is one its parameter (or the load, where allowed) can take. */
322
+ const takes = (owner, set, load) => {
323
+ for (const k of Object.keys(set)) {
324
+ const v = set[k];
325
+ if (k === "load") {
326
+ if (!load) throw new Error(`loadmap: ${owner} sets the load; a scenario leaves it to the slider`);
327
+ if (!(typeof v === "number" && v >= def.load.min && v <= def.load.max)) throw new Error(`loadmap: ${owner} sets the load to ${String(v)}, outside ${def.load.min} to ${def.load.max}`);
328
+ continue;
329
+ }
330
+ const p = Object.hasOwn(params, k) ? params[k] : void 0;
331
+ if (!p) throw new Error(`loadmap: ${owner} sets unknown parameter "${k}"`);
332
+ if (!(p.kind === "select" ? p.options.includes(v) : p.kind === "switch" ? typeof v === "boolean" : typeof v === "number" && v >= p.min && v <= p.max)) throw new Error(`loadmap: ${owner} sets "${k}" to ${String(v)}, which it cannot take`);
333
+ }
334
+ };
335
+ const scenarios = Object.entries(def.scenarios ?? {}).map(([label, set]) => {
336
+ takes(`scenario "${label}"`, set, false);
337
+ const { load: _, ...values } = {
338
+ ...defaults(),
339
+ ...set
340
+ };
341
+ return {
342
+ label,
343
+ values
344
+ };
345
+ });
346
+ const scenarioOf = (values) => scenarios.find((s) => Object.entries(s.values).every(([k, v]) => values[k] === v)) ?? null;
347
+ const expectations = (def.expect ?? []).map((e) => {
348
+ const name = e.label ? `expectation "${e.label}"` : "an expectation";
349
+ if (e.scenario != null && !scenarios.some((s) => s.label === e.scenario)) throw new Error(`loadmap: ${name} names unknown scenario "${e.scenario}"`);
350
+ if (e.component != null && !componentIds.includes(e.component)) throw new Error(`loadmap: ${name} names unknown component "${e.component}"`);
351
+ if (e.target == null && e.full == null) throw new Error(`loadmap: ${name} needs a target or a full multiple`);
352
+ for (const k of ["target", "full"]) {
353
+ const x = e[k];
354
+ if (x != null && !(x > 0 && Number.isFinite(x))) throw new Error(`loadmap: ${name} needs a ${k} multiple above 0, not ${x}`);
355
+ }
356
+ return { ...e };
357
+ });
358
+ const story = (def.story ?? []).map((st, i) => {
359
+ const name = `story step ${i + 1}`;
360
+ const from = st.scenario == null ? void 0 : scenarios.find((s) => s.label === st.scenario);
361
+ if (st.scenario != null && !from) throw new Error(`loadmap: ${name} names unknown scenario "${st.scenario}"`);
362
+ if (st.focus != null && !componentIds.includes(st.focus)) throw new Error(`loadmap: ${name} points at unknown component "${st.focus}"`);
363
+ if (st.seconds != null && !(st.seconds > 0 && Number.isFinite(st.seconds))) throw new Error(`loadmap: ${name} needs seconds above 0, not ${st.seconds}`);
364
+ takes(name, st.set ?? {}, true);
365
+ return {
366
+ text: st.text,
367
+ values: {
368
+ ...defaults(),
369
+ ...from?.values,
370
+ ...st.set
371
+ },
372
+ ...st.scenario != null ? { scenario: st.scenario } : {},
373
+ ...st.focus != null ? { focus: st.focus } : {},
374
+ seconds: st.seconds ?? 4
375
+ };
376
+ });
377
+ const utilAt = (values, bound, pinned) => (load) => context(def, {
378
+ ...values,
379
+ load
380
+ }, bound, pinned).ctx.util;
381
+ /** The highest utilization of any live component: it crosses a threshold where the first of them does. */
382
+ const envelope = (values, bound, pinned) => (load) => {
383
+ const util = utilAt(values, bound, pinned)(load);
384
+ let top = null;
385
+ for (const c of componentIds) {
386
+ const u = util[c];
387
+ if (u != null && (top == null || u > top)) top = u;
388
+ }
389
+ return top;
390
+ };
391
+ const crossingCache = /* @__PURE__ */ new Map();
392
+ const crossingsFor = (values) => {
393
+ const { load: _, ...rest } = values;
394
+ const key = JSON.stringify(rest);
395
+ const hit = crossingCache.get(key);
396
+ if (hit) return hit;
397
+ const stated = context(def, values);
398
+ const uncertain = componentIds.filter((c) => (stated.capacity(c).within ?? 0) > 0);
399
+ const components = {};
400
+ for (const c of componentIds) {
401
+ const at = (bound) => (load) => utilAt(values, bound)(load)[c];
402
+ const both = (th) => {
403
+ const x = crossing(at(), th, searchMax);
404
+ if (x == null) return {
405
+ x,
406
+ range: null
407
+ };
408
+ if (!uncertain.length) return {
409
+ x,
410
+ range: {
411
+ low: x,
412
+ high: x
413
+ }
414
+ };
415
+ return {
416
+ x,
417
+ range: {
418
+ low: crossing(at("low"), th, searchMax) ?? x,
419
+ high: crossing(at("high"), th, searchMax) ?? x
420
+ }
421
+ };
422
+ };
423
+ const t = both(thresholds.target);
424
+ const f = both(thresholds.full);
425
+ components[c] = {
426
+ target: t.x,
427
+ full: f.x,
428
+ targetRange: t.range,
429
+ fullRange: f.range
430
+ };
431
+ }
432
+ let hint;
433
+ const out = {
434
+ components,
435
+ measure: () => hint ??= measure(values, uncertain, components)
436
+ };
437
+ if (crossingCache.size > 64) crossingCache.delete(crossingCache.keys().next().value);
438
+ crossingCache.set(key, out);
439
+ return out;
440
+ };
441
+ /**
442
+ * The capacity whose ± matters most: with it at its stated value (the others still ranged), the target range
443
+ * narrows the most. Only when the range is wider than 10 %.
444
+ */
445
+ const measure = (values, uncertain, components) => {
446
+ const ranges = Object.values(components).flatMap((r) => r.targetRange ? [r.targetRange] : []);
447
+ if (!uncertain.length || !ranges.length) return null;
448
+ const now = {
449
+ low: Math.min(...ranges.map((r) => r.low)),
450
+ high: Math.min(...ranges.map((r) => r.high))
451
+ };
452
+ const width = (r) => Math.log(Math.min(r.high, searchMax * 2) / r.low);
453
+ if (!(now.low > 0 && Number.isFinite(now.low)) || width(now) <= Math.log(1.1)) return null;
454
+ let best = null;
455
+ for (const k of uncertain) {
456
+ const low = crossing(envelope(values, "low", k), thresholds.target, searchMax);
457
+ const high = crossing(envelope(values, "high", k), thresholds.target, searchMax);
458
+ if (low == null || high == null || !(low > 0)) continue;
459
+ const target = {
460
+ low,
461
+ high
462
+ };
463
+ const gain = width(now) - width(target);
464
+ if (gain > .01 && (!best || gain > best.gain)) best = {
465
+ component: k,
466
+ target,
467
+ gain
468
+ };
469
+ }
470
+ if (!best) return null;
471
+ const util = utilAt(values, "low", best.component)(best.target.low);
472
+ let first = best.component;
473
+ for (const c of componentIds) {
474
+ const u = util[c];
475
+ if (u != null && u > (util[first] ?? Number.NEGATIVE_INFINITY)) first = c;
476
+ }
477
+ return {
478
+ component: best.component,
479
+ target: best.target,
480
+ firstLimit: first
481
+ };
482
+ };
483
+ const evaluate = (partial) => {
484
+ const values = {
485
+ ...defaults(),
486
+ ...partial
487
+ };
488
+ const { ctx, capacity, over, describe } = context(def, values);
489
+ const crossings = crossingsFor(values);
490
+ const components = {};
491
+ for (const c of componentIds) {
492
+ const d = def.components[c];
493
+ const util = ctx.util[c];
494
+ const off = util == null;
495
+ components[c] = {
496
+ id: c,
497
+ name: d.name,
498
+ short: d.short ?? d.name,
499
+ plural: d.plural ?? false,
500
+ util,
501
+ demand: off ? null : ctx.demand[c],
502
+ capacity: off ? null : capacity(c),
503
+ state: off ? "off" : util > thresholds.full ? "over" : util > thresholds.target ? "warn" : "ok",
504
+ ...crossings.components[c],
505
+ over: over(c),
506
+ describe: describe(c)
507
+ };
508
+ }
509
+ const all = componentIds.map((c) => components[c]);
510
+ const live = all.filter((r) => r.util != null);
511
+ const order = (x) => x == null ? Number.POSITIVE_INFINITY : x;
512
+ const cmp = (x, y) => order(x) === order(y) ? 0 : order(x) < order(y) ? -1 : 1;
513
+ const ranked = [...all].sort((a, b) => Number(a.util == null) - Number(b.util == null) || cmp(a.targetRange?.low, b.targetRange?.low) || cmp(a.target, b.target) || cmp(a.fullRange?.low, b.fullRange?.low));
514
+ const firstLimit = live.length ? ranked[0] : null;
515
+ const firstFull = live.length ? live.reduce((a, b) => cmp(b.fullRange?.low, a.fullRange?.low) < 0 ? b : a) : null;
516
+ const edges = (k) => {
517
+ const rs = live.flatMap((r) => r[k] ? [r[k]] : []);
518
+ return rs.length ? {
519
+ low: Math.min(...rs.map((r) => r.low)),
520
+ high: Math.min(...rs.map((r) => r.high))
521
+ } : null;
522
+ };
523
+ const target = edges("targetRange");
524
+ const full = edges("fullRange");
525
+ const flows = {};
526
+ for (const f of flowIds) flows[f] = ctx.flows[f];
527
+ const stages = {};
528
+ for (const s of stageIds) stages[s] = resolveStage(s, def.stages[s], ctx, flowIds);
529
+ for (const st of Object.values(stages)) if (st.kind === "fanout") for (const b of Object.values(st.to).flat()) {
530
+ const to = stages[b.stage];
531
+ if (!to || to.kind === "queue" || to.kind === "workers" || to.kind === "fanout") throw new Error(`loadmap: fan-out "${st.id}" branches to "${b.stage}", which is not a sink, store or pass stage`);
532
+ }
533
+ const overload = {};
534
+ for (const f of flowIds) {
535
+ const o = def.flows[f].overload;
536
+ overload[f] = (typeof o === "function" ? o(ctx) : o) ?? {};
537
+ }
538
+ const assumptions = [];
539
+ for (const c of componentIds) {
540
+ const cap = components[c].capacity ?? capacity(c);
541
+ assumptions.push({
542
+ kind: "capacity",
543
+ component: c,
544
+ name: def.components[c].name,
545
+ capacity: cap
546
+ });
547
+ }
548
+ for (const [name, fit] of Object.entries(def.calibration ?? {})) assumptions.push({
549
+ kind: "fit",
550
+ name,
551
+ fit
552
+ });
553
+ return {
554
+ values,
555
+ flows,
556
+ components,
557
+ ranked,
558
+ firstLimit,
559
+ firstFull,
560
+ target,
561
+ full,
562
+ verdict: verdict(values.load, live, firstLimit, target, thresholds, searchMax, when),
563
+ get measure() {
564
+ return measureText(crossings.measure(), components, firstLimit, thresholds, searchMax);
565
+ },
566
+ presets: presets(def, target, full, thresholds, searchMax),
567
+ assumptions,
568
+ stages,
569
+ routes: def.routes,
570
+ overload,
571
+ ctx
572
+ };
573
+ };
574
+ return {
575
+ def,
576
+ title: def.title,
577
+ load: def.load,
578
+ params: paramList,
579
+ flowIds,
580
+ componentIds,
581
+ stageIds,
582
+ thresholds,
583
+ searchMax,
584
+ scenarios,
585
+ expectations,
586
+ story,
587
+ growth: growth$1,
588
+ defaults,
589
+ isEnabled: (id, values) => {
590
+ const d = paramList.find((x) => x.id === id)?.def;
591
+ return d ? isEnabled(d, values) : false;
592
+ },
593
+ evaluate,
594
+ utilization: (partial, bound) => {
595
+ const util = context(def, {
596
+ ...defaults(),
597
+ ...partial
598
+ }, bound).ctx.util;
599
+ return Object.fromEntries(componentIds.map((c) => [c, util[c]]));
600
+ },
601
+ scenarioOf,
602
+ dateAt: when
603
+ };
604
+ }
605
+ /**
606
+ * The model's numbers for one set of values. Every number is computed on first use and kept, so a model function
607
+ * may read any other; a value that needs itself throws, naming the loop.
608
+ */
609
+ function context(def, values, bound, pinned) {
610
+ const cache = /* @__PURE__ */ new Map();
611
+ const stack = [];
612
+ const memo = (key, f) => {
613
+ if (cache.has(key)) return cache.get(key);
614
+ if (stack.includes(key)) throw new Error(`loadmap: ${[...stack.slice(stack.indexOf(key)), key].join(" → ")} is a loop`);
615
+ stack.push(key);
616
+ try {
617
+ const v = f();
618
+ cache.set(key, v);
619
+ return v;
620
+ } finally {
621
+ stack.pop();
622
+ }
623
+ };
624
+ const table = (ids, get) => {
625
+ const t = {};
626
+ for (const id of ids) Object.defineProperty(t, id, {
627
+ enumerable: true,
628
+ get: () => get(id)
629
+ });
630
+ return Object.freeze(t);
631
+ };
632
+ const ctx = { ...values };
633
+ const flowIds = Object.keys(def.flows);
634
+ const componentIds = Object.keys(def.components);
635
+ const capacity = (c) => memo(`capacity of ${c}`, () => {
636
+ const cap = def.components[c].capacity;
637
+ return typeof cap === "function" ? cap(ctx) : cap;
638
+ });
639
+ const enabled = (c) => memo(`${c} enabled`, () => def.components[c].when?.(ctx) ?? true);
640
+ ctx.flows = table(flowIds, (f) => memo(`flow ${f}`, () => def.flows[f].rate(ctx)));
641
+ ctx.demand = table(componentIds, (c) => memo(`demand of ${c}`, () => def.components[c].demand(ctx)));
642
+ ctx.util = table(componentIds, (c) => memo(`utilization of ${c}`, () => {
643
+ if (!enabled(c)) return null;
644
+ const cap = boundOf(capacity(c), c === pinned ? void 0 : bound);
645
+ const d = ctx.demand[c];
646
+ return cap > 0 ? d / cap : d > 0 ? Number.POSITIVE_INFINITY : 0;
647
+ }));
648
+ ctx.derived = table(Object.keys(def.derived ?? {}), (k) => memo(`derived ${k}`, () => def.derived[k](ctx)));
649
+ const text = (v) => typeof v === "function" ? v(ctx) : v;
650
+ return {
651
+ ctx: Object.freeze(ctx),
652
+ capacity,
653
+ over: (c) => text(def.components[c].over) ?? "",
654
+ describe: (c) => text(def.components[c].describe)
655
+ };
656
+ }
657
+ /**
658
+ * The lowest load multiple at which `util(load)` reaches `th`. A coarse geometric scan finds the first bracket, so a
659
+ * utilization that does not rise steadily with the load is not mis-bisected; bisection then narrows it.
660
+ */
661
+ function crossing(util, th, max) {
662
+ const u0 = util(0);
663
+ if (u0 == null) return null;
664
+ if (u0 >= th) return 0;
665
+ const steps = 96;
666
+ const lo0 = .01;
667
+ let prev = 0;
668
+ for (let i = 0; i <= steps; i++) {
669
+ const at = lo0 * (max / lo0) ** (i / steps);
670
+ const u = util(at);
671
+ if (u != null && u >= th) {
672
+ let lo = prev;
673
+ let hi = at;
674
+ for (let k = 0; k < 60; k++) {
675
+ const mid = (lo + hi) / 2;
676
+ const um = util(mid);
677
+ if (um != null && um >= th) hi = mid;
678
+ else lo = mid;
679
+ }
680
+ return hi;
681
+ }
682
+ prev = at;
683
+ }
684
+ return Number.POSITIVE_INFINITY;
685
+ }
686
+ /** Where a range of multiples is reached, with its dates: "between 7.4× and 10× (between May 2029 and …)". */
687
+ function reach(r, searchMax, when = () => null) {
688
+ if (!r) return multiple(null);
689
+ return `${multiples(r, searchMax)}${months(when(r.low), when(r.high))}`;
690
+ }
691
+ /** " (capacity estimated ±30 %)"; nothing for a measured capacity without a ±. */
692
+ function sureness(c) {
693
+ if (!c || c.provenance === "measured" && !c.within) return "";
694
+ return ` (capacity ${c.provenance}${c.within ? ` ±${pct(c.within)}` : ""})`;
695
+ }
696
+ function verdict(load, live, first, range, th, searchMax, when) {
697
+ const at = `At ${fmt(load)}×`;
698
+ const target = `${pct(th.target)} target`;
699
+ if (!first) return {
700
+ tier: "ok",
701
+ text: `${at}, every component is switched off.`
702
+ };
703
+ const est = (c) => sureness(c.capacity);
704
+ const upTo = reach(range, searchMax, when);
705
+ const holds = `The ${target} holds up to ${upTo}, limited by ${first.short}${est(first)}.`;
706
+ const verb = (list, one, many) => list.length > 1 || list[0]?.plural ? many : one;
707
+ const overs = live.filter((c) => (c.util ?? 0) > th.full).sort((a, b) => (b.util ?? 0) - (a.util ?? 0));
708
+ if (overs.length) return {
709
+ tier: "over",
710
+ text: `${at}, ${joinAnd(overs.map((c) => c.short))} ${verb(overs, "is", "are")} past capacity: ${overs.map((c) => c.over).join("; ")}. ${holds}`
711
+ };
712
+ const may = (k) => live.filter((c) => c[k] != null && load > c[k].low);
713
+ const mayOver = may("fullRange");
714
+ if (mayOver.length) return {
715
+ tier: "warn",
716
+ text: `${at}, everything fits at the stated capacities, but ${joinAnd(mayOver.map((c) => c.short))} may already be past capacity: ${mayOver.map((c) => c.over).join("; ")}. ${holds}`
717
+ };
718
+ const warns = live.filter((c) => (c.util ?? 0) > th.target);
719
+ if (warns.length) return {
720
+ tier: "warn",
721
+ text: `${at}, everything still fits, but ${joinAnd(warns.map((c) => c.short))} ${verb(warns, "runs", "run")} above the ${target}. ${holds}`
722
+ };
723
+ const mayWarn = may("targetRange");
724
+ if (mayWarn.length) return {
725
+ tier: "warn",
726
+ text: `${at}, every component is under the ${target} at its stated capacity, but ${joinAnd(mayWarn.map((c) => c.short))} may already be above it. ${holds}`
727
+ };
728
+ return {
729
+ tier: "ok",
730
+ text: `${at}, every component is under the ${target}. It holds up to ${upTo}; ${first.short} ${verb([first], "is", "are")} the first limit${est(first)}.`
731
+ };
732
+ }
733
+ function measureText(m, components, first, th, searchMax) {
734
+ if (!m || !first) return null;
735
+ const k = components[m.component];
736
+ const then = components[m.firstLimit];
737
+ const holds = `the ${pct(th.target)} target holds up to ${multiples(m.target, searchMax)}`;
738
+ const text = then.id !== first.id ? `Measuring ${k.short} would change the answer: at its stated capacity, ${then.short} ${then.plural ? "are" : "is"} the first limit and ${holds}.` : `Measuring ${k.short} would narrow this most: at its stated capacity, ${holds}.`;
739
+ return {
740
+ component: k.id,
741
+ target: m.target,
742
+ firstLimit: then.id,
743
+ text
744
+ };
745
+ }
746
+ function presets(def, target, full, th, searchMax) {
747
+ const { min, max, step } = def.load;
748
+ const out = [];
749
+ if (min <= 1 && max >= 1) out.push({
750
+ label: "Today · 1×",
751
+ values: { load: 1 },
752
+ derived: false
753
+ });
754
+ for (const [label, values] of Object.entries(def.presets ?? {})) out.push({
755
+ label,
756
+ values,
757
+ derived: false
758
+ });
759
+ const at = (label, x) => {
760
+ if (x == null || !Number.isFinite(x) || x < min) return;
761
+ const load = Math.min(max, Math.floor(x / step) * step);
762
+ out.push({
763
+ label: `${label} · ${multiple(x, searchMax)}`,
764
+ values: { load },
765
+ derived: true
766
+ });
767
+ };
768
+ at(`${pct(th.target)} target`, target?.low);
769
+ at("Runs out", full?.low);
770
+ return out;
771
+ }
772
+ // ../core/src/check.ts
773
+ /** Tests a model's expectations (or these) on the pessimistic edge. */
774
+ function check$1(model, expectations = model.expectations) {
775
+ return expectations.map((e) => {
776
+ const scenario = e.scenario == null ? null : model.scenarios.find((s) => s.label === e.scenario);
777
+ if (e.scenario != null && !scenario) throw new Error(`loadmap: no scenario "${e.scenario}"`);
778
+ const ev = model.evaluate(scenario?.values);
779
+ const c = e.component == null ? null : ev.components[e.component];
780
+ if (e.component != null && !c) throw new Error(`loadmap: no component "${e.component}"`);
781
+ const where = scenario ? ` in ${scenario.label}` : "";
782
+ const range = (k) => c ? c[`${k}Range`] : ev[k];
783
+ const found = {
784
+ target: range("target"),
785
+ full: range("full")
786
+ };
787
+ if (c && c.util == null) return {
788
+ expectation: e,
789
+ ok: true,
790
+ ...found,
791
+ message: `${c.short} is switched off${where}`
792
+ };
793
+ const claims = [];
794
+ const r = (x) => x ? multiples(x, model.searchMax) : "never";
795
+ const target = `the ${pct(model.thresholds.target)} target`;
796
+ if (e.target != null) {
797
+ const by = c ?? ev.firstLimit;
798
+ claims.push({
799
+ ok: (found.target?.low ?? Number.POSITIVE_INFINITY) >= e.target,
800
+ min: e.target,
801
+ text: c ? `${c.short} reaches ${target} at ${r(found.target)}${where}${sureness(c.capacity)}` : `${target} holds up to ${r(found.target)}${where}${by ? `, limited by ${by.short}${sureness(by.capacity)}` : ""}`
802
+ });
803
+ }
804
+ if (e.full != null) {
805
+ const by = c ?? ev.firstFull;
806
+ claims.push({
807
+ ok: (found.full?.low ?? Number.POSITIVE_INFINITY) >= e.full,
808
+ min: e.full,
809
+ text: by ? `${by.short} ${by.plural ? "run" : "runs"} out at ${r(found.full)}${where}${sureness(by.capacity)}` : `nothing runs out${where}`
810
+ });
811
+ }
812
+ const failed = claims.filter((x) => !x.ok);
813
+ const message = failed.length ? failed.map((x) => `expected at least ${fmt(x.min)}×, but ${x.text}`).join("; ") : claims.map((x) => x.text).join("; ");
814
+ return {
815
+ expectation: e,
816
+ ok: !failed.length,
817
+ ...found,
818
+ message
819
+ };
820
+ });
821
+ }
822
+ // ../core/src/calibrate.ts
823
+ /**
824
+ * Ordinary least squares over `points[x]` → `points[y]`. Needs at least two distinct x values.
825
+ * `through: 'origin'` fixes a at 0 (a cost with no idle part).
826
+ */
827
+ function fitLinear(points, o) {
828
+ const xs = points.map((pt) => Number(pt[o.x]));
829
+ const ys = points.map((pt) => Number(pt[o.y]));
830
+ const n = xs.length;
831
+ if (xs.some((v) => !Number.isFinite(v)) || ys.some((v) => !Number.isFinite(v))) throw new Error(`loadmap: fitLinear(${o.x} → ${o.y}) got a non-numeric point`);
832
+ if (n < 2 || new Set(xs).size < 2) throw new Error(`loadmap: fitLinear(${o.x} → ${o.y}) needs two distinct x values`);
833
+ let a;
834
+ let b;
835
+ if (o.through === "origin") {
836
+ a = 0;
837
+ b = sum(xs.map((x, i) => x * ys[i])) / sum(xs.map((x) => x * x));
838
+ } else {
839
+ const mx = sum(xs) / n;
840
+ const my = sum(ys) / n;
841
+ b = sum(xs.map((x, i) => (x - mx) * (ys[i] - my))) / sum(xs.map((x) => (x - mx) ** 2));
842
+ a = my - b * mx;
843
+ }
844
+ const my = sum(ys) / n;
845
+ const ssTot = sum(ys.map((y) => (y - my) ** 2));
846
+ const ssRes = sum(xs.map((x, i) => (ys[i] - (a + b * x)) ** 2));
847
+ const r2 = ssTot === 0 ? 1 : Math.max(0, 1 - ssRes / ssTot);
848
+ const fit = ((x) => a + b * x);
849
+ return Object.assign(fit, {
850
+ a,
851
+ b,
852
+ r2,
853
+ n,
854
+ x: o.x,
855
+ y: o.y,
856
+ source: o.source,
857
+ provenance: "measured"
858
+ });
859
+ }
860
+ const sum = (v) => v.reduce((s, x) => s + x, 0);
861
+ // ../layout/src/layout.ts
862
+ /** The current text of every label. Cheap: call it on every change of values; the layout stays put. */
863
+ function labelsAt(view, ev) {
864
+ const ctx = ev.ctx;
865
+ const nodes = {};
866
+ for (const [id, n] of Object.entries(view.def.nodes)) nodes[id] = {
867
+ title: text(n.label, ctx),
868
+ sub: n.sub == null ? void 0 : text(n.sub, ctx)
869
+ };
870
+ const edges = {};
871
+ visibleEdges(view, ctx).forEach(({ e, id }) => {
872
+ if (e.label != null) edges[id] = text(e.label, ctx);
873
+ });
874
+ return {
875
+ nodes,
876
+ edges
877
+ };
878
+ }
879
+ function visibleEdges(view, ctx) {
880
+ return view.def.edges.map((e, i) => ({
881
+ e,
882
+ id: `e${i}`
883
+ })).filter(({ e }) => e.when ? Boolean(e.when(ctx)) : true);
884
+ }
885
+ function text(l, ctx) {
886
+ return typeof l === "function" ? l(ctx) : l;
887
+ }
888
+ // ../layout/src/view.ts
889
+ function defineView(model, def) {
890
+ const nodes = def.nodes;
891
+ for (const e of def.edges) for (const end of [e.from, e.to]) if (!(end in nodes)) throw new Error(`loadmap: edge to unknown node "${end}"`);
892
+ for (const [f, fv] of Object.entries(def.flows)) if (!(fv.from in nodes)) throw new Error(`loadmap: flow "${f}" starts at unknown node "${fv.from}"`);
893
+ const shows = (n, s) => Array.isArray(n.stage) ? n.stage.includes(s) : n.stage === s;
894
+ for (const s of model.stageIds) if (!Object.values(nodes).some((n) => shows(n, s))) throw new Error(`loadmap: no node shows stage "${s}"`);
895
+ return {
896
+ model,
897
+ def
898
+ };
899
+ }
900
+ /** Collects diagnostics and turns offsets into lines and columns. */
901
+ var Reporter = class {
902
+ source;
903
+ list = [];
904
+ seen = /* @__PURE__ */ new Set();
905
+ starts;
906
+ constructor(source) {
907
+ this.source = source;
908
+ this.starts = [0];
909
+ for (let i = 0; i < source.length; i++) if (source[i] === "\n") this.starts.push(i + 1);
910
+ }
911
+ error(at, message, hint) {
912
+ this.add("error", at, message, hint);
913
+ }
914
+ warning(at, message, hint) {
915
+ this.add("warning", at, message, hint);
916
+ }
917
+ /** The 1-based line and column of an offset. */
918
+ position(offset) {
919
+ return position(this.starts, offset);
920
+ }
921
+ get failed() {
922
+ return this.list.some((d) => d.severity === "error");
923
+ }
924
+ add(severity, at, message, hint) {
925
+ const key = `${at.from}:${message}`;
926
+ if (this.seen.has(key)) return;
927
+ this.seen.add(key);
928
+ if (this.list.length >= 200) {
929
+ if (this.list.length === 200) this.list.push({
930
+ ...this.list.at(-1),
931
+ severity: "error",
932
+ message: `more than 200 problems; fix these first`,
933
+ hint: void 0
934
+ });
935
+ return;
936
+ }
937
+ const { line, column } = position(this.starts, at.from);
938
+ this.list.push({
939
+ severity,
940
+ message,
941
+ from: at.from,
942
+ to: Math.max(at.to, at.from),
943
+ line,
944
+ column,
945
+ hint
946
+ });
947
+ }
948
+ };
949
+ function position(starts, offset) {
950
+ let lo = 0;
951
+ let hi = starts.length - 1;
952
+ while (lo < hi) {
953
+ const mid = lo + hi + 1 >> 1;
954
+ if (starts[mid] <= offset) lo = mid;
955
+ else hi = mid - 1;
956
+ }
957
+ return {
958
+ line: lo + 1,
959
+ column: offset - starts[lo] + 1
960
+ };
961
+ }
962
+ /**
963
+ * One diagnostic as a terminal shows it:
964
+ *
965
+ * checkout.loadmap:24:17 error: unknown flow "order"
966
+ * 24 | demand: flows.order + flows.events
967
+ * | ^^^^^ did you mean "orders"?
968
+ */
969
+ function formatDiagnostic(d, source, file = "input.loadmap") {
970
+ const text = source.split("\n")[d.line - 1] ?? "";
971
+ const n = String(d.line);
972
+ const pad = " ".repeat(n.length);
973
+ const width = Math.max(1, Math.min(d.to - d.from, text.length - d.column + 1));
974
+ const mark = `${" ".repeat(d.column - 1)}${"^".repeat(width)}${d.hint ? ` ${d.hint}` : ""}`;
975
+ return `${file}:${d.line}:${d.column} ${d.severity}: ${d.message}\n ${n} | ${text}\n ${pad} | ${mark}`;
976
+ }
977
+ /** The closest of `options` to `name`, when it is close enough to be a typo. */
978
+ function closest(name, options) {
979
+ let best;
980
+ let bestD = Number.POSITIVE_INFINITY;
981
+ const lower = name.toLowerCase();
982
+ for (const o of options) {
983
+ const d = o.toLowerCase() === lower ? 0 : distance(lower, o.toLowerCase());
984
+ if (d < bestD) {
985
+ best = o;
986
+ bestD = d;
987
+ }
988
+ }
989
+ return best != null && bestD <= Math.max(1, Math.floor(name.length / 3)) ? best : void 0;
990
+ }
991
+ function didYouMean(name, options) {
992
+ const c = closest(name, options);
993
+ return c ? `did you mean "${c}"?` : void 0;
994
+ }
995
+ function distance(a, b) {
996
+ const row = Array.from({ length: b.length + 1 }, (_, j) => j);
997
+ for (let i = 1; i <= a.length; i++) {
998
+ let prev = row[0];
999
+ row[0] = i;
1000
+ for (let j = 1; j <= b.length; j++) {
1001
+ const cur = row[j];
1002
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
1003
+ prev = cur;
1004
+ }
1005
+ }
1006
+ return row[b.length];
1007
+ }
1008
+ // ../dsl/src/schema.ts
1009
+ /**
1010
+ * Every statement of the language: what follows its keyword, its properties and what it may hold. The checker,
1011
+ * autocomplete and the language reference (docs/dsl.md) all read this table, so it is the one place to add a feature.
1012
+ */
1013
+ /** `o[k]` for an own entry only: a name like "constructor" must not find what every object inherits. */
1014
+ function own(o, k) {
1015
+ return Object.hasOwn(o, k) ? o[k] : void 0;
1016
+ }
1017
+ const STAGE_LABEL = [{
1018
+ name: "id",
1019
+ kind: "id",
1020
+ optional: true
1021
+ }, {
1022
+ name: "label",
1023
+ kind: "text",
1024
+ optional: true
1025
+ }];
1026
+ const STAGE_COMPONENT = {
1027
+ value: "component",
1028
+ scope: "const",
1029
+ doc: "The component whose capacity this stage uses. Defaults to its box's component."
1030
+ };
1031
+ /** A component's properties: on a box, or in a `component` block. */
1032
+ const COMPONENT_PROPS = {
1033
+ name: {
1034
+ value: "string",
1035
+ scope: "const",
1036
+ doc: "The component's name in the limits list. Defaults to the label."
1037
+ },
1038
+ short: {
1039
+ value: "string",
1040
+ scope: "const",
1041
+ doc: "The name inside a sentence, with its article: \"the database\"."
1042
+ },
1043
+ plural: {
1044
+ value: "boolean",
1045
+ scope: "const",
1046
+ doc: "\"are past capacity\" instead of \"is\"."
1047
+ },
1048
+ demand: {
1049
+ value: "number",
1050
+ scope: "ctx",
1051
+ doc: "What the component must handle, in its capacity's unit."
1052
+ },
1053
+ capacity: {
1054
+ value: "capacity",
1055
+ scope: "ctx",
1056
+ doc: "What it can handle: measured(value, unit, source) or estimated(value, unit, source); `value ± 30%` says how sure it is."
1057
+ },
1058
+ when: {
1059
+ value: "boolean",
1060
+ scope: "ctx",
1061
+ doc: "The component is switched off (no utilization) while this is false."
1062
+ },
1063
+ over: {
1064
+ value: "string",
1065
+ scope: "ctx",
1066
+ doc: "What happens past 100 %, as a clause: \"clients get 429s\"."
1067
+ },
1068
+ describe: {
1069
+ value: "string",
1070
+ scope: "ctx",
1071
+ doc: "One line under the name in the limits list."
1072
+ }
1073
+ };
1074
+ const BOX_ARGS = [
1075
+ {
1076
+ name: "id",
1077
+ kind: "id"
1078
+ },
1079
+ {
1080
+ name: "label",
1081
+ kind: "text",
1082
+ optional: true,
1083
+ dynamic: true
1084
+ },
1085
+ {
1086
+ name: "sub",
1087
+ kind: "text",
1088
+ optional: true,
1089
+ dynamic: true
1090
+ }
1091
+ ];
1092
+ const SUB = {
1093
+ value: "string",
1094
+ scope: "ctx",
1095
+ doc: "The second line of the box."
1096
+ };
1097
+ const OVERLOAD_WHEN = {
1098
+ value: "boolean",
1099
+ scope: "ctx",
1100
+ doc: "The rule applies only while this is true."
1101
+ };
1102
+ const COLORS = [
1103
+ "accent",
1104
+ "alt",
1105
+ "ok",
1106
+ "warn",
1107
+ "drop",
1108
+ "muted"
1109
+ ];
1110
+ const SCHEMA = {
1111
+ "": {
1112
+ doc: "A loadmap definition.",
1113
+ example: "",
1114
+ args: [],
1115
+ props: {
1116
+ title: {
1117
+ value: "string",
1118
+ scope: "const",
1119
+ doc: "The heading above the diagram."
1120
+ },
1121
+ layout: {
1122
+ value: "enum",
1123
+ scope: "const",
1124
+ values: ["right", "down"],
1125
+ doc: "Which way the diagram runs. Narrow screens always run down."
1126
+ },
1127
+ speed: {
1128
+ value: "number",
1129
+ scope: "const",
1130
+ doc: "Simulated seconds per real second. Default 10."
1131
+ },
1132
+ assume: {
1133
+ value: "string",
1134
+ scope: "const",
1135
+ repeat: true,
1136
+ doc: "A line added before the generated assumptions."
1137
+ },
1138
+ searchMax: {
1139
+ value: "number",
1140
+ scope: "const",
1141
+ doc: "The highest load multiple the limits search. Default 2000."
1142
+ }
1143
+ },
1144
+ children: [
1145
+ "loadmap",
1146
+ "load",
1147
+ "param",
1148
+ "preset",
1149
+ "scenario",
1150
+ "growth",
1151
+ "expect",
1152
+ "step",
1153
+ "let",
1154
+ "fit",
1155
+ "flow",
1156
+ "component",
1157
+ "node",
1158
+ "cluster",
1159
+ "pool",
1160
+ "thresholds"
1161
+ ],
1162
+ edges: "edges"
1163
+ },
1164
+ loadmap: {
1165
+ doc: "The first line: the version of the language this definition is written in.",
1166
+ example: "loadmap 1",
1167
+ args: [{
1168
+ name: "version",
1169
+ kind: "number"
1170
+ }],
1171
+ props: {},
1172
+ children: []
1173
+ },
1174
+ load: {
1175
+ doc: "The load, as a multiple of today. It drives the limits.",
1176
+ example: "load \"Users, × today\" { max: 100; default: 10 }",
1177
+ args: [{
1178
+ name: "label",
1179
+ kind: "text",
1180
+ optional: true
1181
+ }],
1182
+ props: {
1183
+ min: {
1184
+ value: "number",
1185
+ scope: "const",
1186
+ doc: "The lowest multiple. Default 1."
1187
+ },
1188
+ max: {
1189
+ value: "number",
1190
+ scope: "const",
1191
+ doc: "The highest multiple on the slider.",
1192
+ required: true
1193
+ },
1194
+ step: {
1195
+ value: "number",
1196
+ scope: "const",
1197
+ doc: "The slider step. Default 1."
1198
+ },
1199
+ default: {
1200
+ value: "number",
1201
+ scope: "const",
1202
+ doc: "Where the slider starts. Default the minimum."
1203
+ }
1204
+ },
1205
+ children: []
1206
+ },
1207
+ param: {
1208
+ doc: "A control: options make a select, min and max a slider, a true or false default a switch.",
1209
+ example: "param pods \"Worker pods\" { options: [2, 3, 4, 6]; default: 3 }",
1210
+ args: [{
1211
+ name: "id",
1212
+ kind: "id"
1213
+ }, {
1214
+ name: "label",
1215
+ kind: "text",
1216
+ optional: true
1217
+ }],
1218
+ props: {
1219
+ options: {
1220
+ value: "options",
1221
+ scope: "const",
1222
+ doc: "The choices of a select: [2, 3, 4] or [\"a\", \"b\"]."
1223
+ },
1224
+ min: {
1225
+ value: "number",
1226
+ scope: "const",
1227
+ doc: "The lowest value of a slider."
1228
+ },
1229
+ max: {
1230
+ value: "number",
1231
+ scope: "const",
1232
+ doc: "The highest value of a slider."
1233
+ },
1234
+ step: {
1235
+ value: "number",
1236
+ scope: "const",
1237
+ doc: "The step of a slider. Default 1."
1238
+ },
1239
+ default: {
1240
+ value: "value",
1241
+ scope: "const",
1242
+ doc: "The starting value.",
1243
+ required: true
1244
+ },
1245
+ hint: {
1246
+ value: "string",
1247
+ scope: "const",
1248
+ doc: "One line under the control."
1249
+ },
1250
+ tag: {
1251
+ value: "string",
1252
+ scope: "const",
1253
+ doc: "A short tag next to the label, e.g. \"estimate\"."
1254
+ },
1255
+ enabled: {
1256
+ value: "boolean",
1257
+ scope: "values",
1258
+ doc: "Greys the control out while this is false; it reads only parameters."
1259
+ },
1260
+ format: {
1261
+ value: "string",
1262
+ scope: "value",
1263
+ doc: "How a value is shown, from `value`: `${value} pods`."
1264
+ }
1265
+ },
1266
+ children: []
1267
+ },
1268
+ preset: {
1269
+ doc: "A named set of values, offered as a button.",
1270
+ example: "preset \"Black Friday\" { load: 40; pods: 6 }",
1271
+ args: [{
1272
+ name: "label",
1273
+ kind: "text"
1274
+ }],
1275
+ props: {},
1276
+ children: [],
1277
+ open: {
1278
+ names: "values",
1279
+ spec: {
1280
+ value: "value",
1281
+ scope: "const",
1282
+ doc: "A value of the load or a parameter."
1283
+ }
1284
+ }
1285
+ },
1286
+ scenario: {
1287
+ doc: "A named design to compare: the defaults plus these values, never the load. The first is the baseline.",
1288
+ example: "scenario \"Proposed\" { pods: 6; gateways: 2 }",
1289
+ args: [{
1290
+ name: "label",
1291
+ kind: "text"
1292
+ }],
1293
+ props: {},
1294
+ children: [],
1295
+ open: {
1296
+ names: "params",
1297
+ spec: {
1298
+ value: "value",
1299
+ scope: "const",
1300
+ doc: "A value of a parameter."
1301
+ }
1302
+ }
1303
+ },
1304
+ growth: {
1305
+ doc: "How fast the load grows, compounded from asOf, where it is 1×: load multiples become dates.",
1306
+ example: "growth { rate: 0.06; per: month; asOf: \"2026-11-01\" }",
1307
+ args: [],
1308
+ props: {
1309
+ rate: {
1310
+ value: "number",
1311
+ scope: "const",
1312
+ required: true,
1313
+ doc: "Growth per period, as a share: 0.06 is 6 %."
1314
+ },
1315
+ per: {
1316
+ value: "enum",
1317
+ scope: "const",
1318
+ values: ["month", "year"],
1319
+ required: true,
1320
+ doc: "month or year."
1321
+ },
1322
+ asOf: {
1323
+ value: "string",
1324
+ scope: "const",
1325
+ required: true,
1326
+ doc: "The day the load is 1×: \"2026-11-01\"."
1327
+ }
1328
+ },
1329
+ children: []
1330
+ },
1331
+ expect: {
1332
+ doc: "What must keep holding, on the pessimistic edge (every capacity at its low bound): `loadmap check` fails when it does not.",
1333
+ example: "expect \"Today keeps 7×\" { target: 7 }",
1334
+ args: [{
1335
+ name: "label",
1336
+ kind: "text",
1337
+ optional: true
1338
+ }],
1339
+ props: {
1340
+ scenario: {
1341
+ value: "scenario",
1342
+ scope: "const",
1343
+ doc: "The scenario whose values it checks; the defaults without one."
1344
+ },
1345
+ component: {
1346
+ value: "component",
1347
+ scope: "const",
1348
+ doc: "One component; the first limit without one."
1349
+ },
1350
+ target: {
1351
+ value: "number",
1352
+ scope: "const",
1353
+ doc: "The target holds up to at least this load multiple."
1354
+ },
1355
+ full: {
1356
+ value: "number",
1357
+ scope: "const",
1358
+ doc: "Nothing (or the component) runs out before this load multiple."
1359
+ }
1360
+ },
1361
+ children: []
1362
+ },
1363
+ step: {
1364
+ doc: "A step of the story the page walks through, in order: its text, and the values to show it at. `loadmap record --story` plays it.",
1365
+ example: "step \"At 30×, the workers run out first.\" { load: 30; focus: workers }",
1366
+ args: [{
1367
+ name: "label",
1368
+ kind: "text"
1369
+ }],
1370
+ props: {
1371
+ scenario: {
1372
+ value: "scenario",
1373
+ scope: "const",
1374
+ doc: "Start from this scenario's values; without one, from the defaults."
1375
+ },
1376
+ focus: {
1377
+ value: "component",
1378
+ scope: "const",
1379
+ doc: "The component to point at: its box is outlined."
1380
+ },
1381
+ seconds: {
1382
+ value: "number",
1383
+ scope: "const",
1384
+ doc: "How long a recording holds the step. Default 4."
1385
+ }
1386
+ },
1387
+ children: [],
1388
+ open: {
1389
+ names: "values",
1390
+ spec: {
1391
+ value: "value",
1392
+ scope: "const",
1393
+ doc: "A value of the load or a parameter."
1394
+ }
1395
+ }
1396
+ },
1397
+ let: {
1398
+ doc: "A named formula other formulas can use by its name.",
1399
+ example: "let perPosition = watchOnly ? 0.3 : 1.2",
1400
+ args: [{
1401
+ name: "id",
1402
+ kind: "id"
1403
+ }],
1404
+ init: true,
1405
+ props: {},
1406
+ children: []
1407
+ },
1408
+ fit: {
1409
+ doc: "cost = a + b × x, fitted from measured points. Call it like a function: workerCores(rate).",
1410
+ example: "fit workerCores \"Worker CPU per request\" { source: \"load test\"; points rate cores { 200 0.26 } }",
1411
+ args: [{
1412
+ name: "id",
1413
+ kind: "id"
1414
+ }, {
1415
+ name: "label",
1416
+ kind: "text",
1417
+ optional: true
1418
+ }],
1419
+ props: {
1420
+ source: {
1421
+ value: "string",
1422
+ scope: "const",
1423
+ doc: "Where the points came from."
1424
+ },
1425
+ through: {
1426
+ value: "enum",
1427
+ scope: "const",
1428
+ values: ["origin"],
1429
+ doc: "origin: a cost with no idle part (a = 0)."
1430
+ }
1431
+ },
1432
+ children: ["points"]
1433
+ },
1434
+ points: {
1435
+ doc: "The measured points of a fit: the names of x and y, then one \"x y\" row per line.",
1436
+ example: "points rate cores {\n 200 0.2599\n 350 0.3061\n}",
1437
+ args: [{
1438
+ name: "x",
1439
+ kind: "id"
1440
+ }, {
1441
+ name: "y",
1442
+ kind: "id"
1443
+ }],
1444
+ props: {},
1445
+ children: [],
1446
+ rows: true
1447
+ },
1448
+ flow: {
1449
+ doc: "Something that moves through the system, with its rate and its route.",
1450
+ example: "flow orders \"orders\" { unit: \"order\"; rate: 40 * load; route: gateway -> topic -> workers }",
1451
+ args: [{
1452
+ name: "id",
1453
+ kind: "id"
1454
+ }, {
1455
+ name: "label",
1456
+ kind: "text",
1457
+ optional: true
1458
+ }],
1459
+ props: {
1460
+ unit: {
1461
+ value: "string",
1462
+ scope: "const",
1463
+ doc: "One unit of the flow, singular: \"order\"."
1464
+ },
1465
+ rate: {
1466
+ value: "number",
1467
+ scope: "ctx",
1468
+ doc: "Units per second.",
1469
+ required: true
1470
+ },
1471
+ priority: {
1472
+ value: "number",
1473
+ scope: "const",
1474
+ doc: "1 is served first. Flows of the same priority share."
1475
+ },
1476
+ route: {
1477
+ value: "route",
1478
+ scope: "const",
1479
+ doc: "The stages it passes, in order: a -> b -> c.",
1480
+ required: true
1481
+ },
1482
+ from: {
1483
+ value: "node",
1484
+ scope: "const",
1485
+ doc: "The node its dots start from.",
1486
+ required: true
1487
+ },
1488
+ color: {
1489
+ value: "enum",
1490
+ scope: "const",
1491
+ values: COLORS,
1492
+ doc: "A theme colour (accent, alt, ok, warn, drop, muted) or a CSS colour in quotes."
1493
+ }
1494
+ },
1495
+ children: ["drop", "merge"]
1496
+ },
1497
+ drop: {
1498
+ doc: "Units older than olderThan simulated seconds are dropped before they are handled.",
1499
+ example: "drop { olderThan: 60; reason: \"too old\" }",
1500
+ args: [],
1501
+ props: {
1502
+ olderThan: {
1503
+ value: "number",
1504
+ scope: "ctx",
1505
+ doc: "Seconds.",
1506
+ required: true
1507
+ },
1508
+ reason: {
1509
+ value: "string",
1510
+ scope: "ctx",
1511
+ doc: "Shown with the drop meter."
1512
+ },
1513
+ when: OVERLOAD_WHEN
1514
+ },
1515
+ children: []
1516
+ },
1517
+ merge: {
1518
+ doc: "Under backlog, this share of what is handled is superseded by a newer unit: keep the newest per key.",
1519
+ example: "merge { share: 0.37; reason: \"newest kept\" }",
1520
+ args: [],
1521
+ props: {
1522
+ share: {
1523
+ value: "number",
1524
+ scope: "ctx",
1525
+ doc: "The share superseded, 0 to 1.",
1526
+ required: true
1527
+ },
1528
+ reason: {
1529
+ value: "string",
1530
+ scope: "ctx",
1531
+ doc: "Shown with the meter."
1532
+ },
1533
+ when: OVERLOAD_WHEN
1534
+ },
1535
+ children: []
1536
+ },
1537
+ component: {
1538
+ doc: "Something that can run out: its demand against its capacity. On a box, write these lines on the box.",
1539
+ example: "component cache \"Cache\" { demand: flows.reads; capacity: estimated(9000, \"req/s\") }",
1540
+ args: [{
1541
+ name: "id",
1542
+ kind: "id",
1543
+ optional: true
1544
+ }, {
1545
+ name: "label",
1546
+ kind: "text",
1547
+ optional: true
1548
+ }],
1549
+ props: COMPONENT_PROPS,
1550
+ children: []
1551
+ },
1552
+ node: {
1553
+ doc: "A box. It may run a stage (pass, store, sink, fanout) and own a component (demand and capacity lines).",
1554
+ example: "node gateway \"API gateway\" { pass; demand: flows.orders; capacity: estimated(20000, \"req/s\") }",
1555
+ args: BOX_ARGS,
1556
+ props: {
1557
+ sub: SUB,
1558
+ shape: {
1559
+ value: "enum",
1560
+ scope: "const",
1561
+ values: ["box", "cylinder"],
1562
+ doc: "box (the default) or cylinder."
1563
+ },
1564
+ ...COMPONENT_PROPS
1565
+ },
1566
+ children: [
1567
+ "pass",
1568
+ "store",
1569
+ "sink",
1570
+ "fanout",
1571
+ "component"
1572
+ ]
1573
+ },
1574
+ cluster: {
1575
+ doc: "Members (brokers) holding the partitions of one or more queues (topics).",
1576
+ example: "cluster kafka \"Kafka\" { members: 3; queue topic { partitions { orders: 12 } } }",
1577
+ args: BOX_ARGS,
1578
+ props: {
1579
+ sub: SUB,
1580
+ members: {
1581
+ value: "number",
1582
+ scope: "ctx",
1583
+ doc: "How many members.",
1584
+ required: true
1585
+ },
1586
+ memberLabel: {
1587
+ value: "string",
1588
+ scope: "i",
1589
+ doc: "Each member's label, from its index i: `broker ${i}`."
1590
+ },
1591
+ ...COMPONENT_PROPS
1592
+ },
1593
+ children: ["queue", "component"]
1594
+ },
1595
+ pool: {
1596
+ doc: "A workers stage drawn as pods holding a grid of workers.",
1597
+ example: "pool workers \"Workers\" { workers { pods: pods; perPod: 8 } }",
1598
+ args: BOX_ARGS,
1599
+ props: {
1600
+ sub: SUB,
1601
+ podLabel: {
1602
+ value: "string",
1603
+ scope: "i",
1604
+ doc: "Each pod's label, from its index i: `pod ${i}`."
1605
+ },
1606
+ ...COMPONENT_PROPS
1607
+ },
1608
+ children: ["workers", "component"]
1609
+ },
1610
+ pass: {
1611
+ doc: "Flows pass through; past capacity the share above 100 % is rejected.",
1612
+ example: "pass \"the gateway\"",
1613
+ args: STAGE_LABEL,
1614
+ props: { component: STAGE_COMPONENT },
1615
+ children: []
1616
+ },
1617
+ store: {
1618
+ doc: "Where a flow ends: written to its component.",
1619
+ example: "store",
1620
+ args: STAGE_LABEL,
1621
+ props: { component: STAGE_COMPONENT },
1622
+ children: []
1623
+ },
1624
+ sink: {
1625
+ doc: "Where a flow ends without a component: a receiver, a viewer.",
1626
+ example: "sink \"contacts\"",
1627
+ args: STAGE_LABEL,
1628
+ props: {},
1629
+ children: []
1630
+ },
1631
+ queue: {
1632
+ doc: "Flows wait here; the next stage of each route drains it.",
1633
+ example: "queue topic \"orders topic\" { partitions { orders: 12 } }",
1634
+ args: STAGE_LABEL,
1635
+ props: { component: STAGE_COMPONENT },
1636
+ children: ["partitions"]
1637
+ },
1638
+ partitions: {
1639
+ doc: "Partitions per flow, drawn as cells across the members.",
1640
+ example: "partitions { orders: 12; events: 24 }",
1641
+ args: [],
1642
+ props: {},
1643
+ children: [],
1644
+ open: {
1645
+ names: "flows",
1646
+ spec: {
1647
+ value: "number",
1648
+ scope: "ctx",
1649
+ doc: "Partitions of this flow."
1650
+ }
1651
+ }
1652
+ },
1653
+ workers: {
1654
+ doc: "A pool of workers: pods × workers per pod.",
1655
+ example: "workers { pods: pods; perPod: 8 }",
1656
+ args: STAGE_LABEL,
1657
+ props: {
1658
+ pods: {
1659
+ value: "number",
1660
+ scope: "ctx",
1661
+ doc: "How many pods.",
1662
+ required: true
1663
+ },
1664
+ perPod: {
1665
+ value: "number",
1666
+ scope: "ctx",
1667
+ doc: "Workers per pod.",
1668
+ required: true
1669
+ },
1670
+ fill: {
1671
+ value: "enum",
1672
+ scope: "const",
1673
+ values: ["round-robin", "in-order"],
1674
+ doc: "round-robin spreads work across pods (the default); in-order fills pod 0 first."
1675
+ },
1676
+ component: STAGE_COMPONENT
1677
+ },
1678
+ children: []
1679
+ },
1680
+ fanout: {
1681
+ doc: "Each unit is copied to several stages; one \"flow -> stage: copies\" line per branch. Last on a route.",
1682
+ example: "fanout \"push broker\" {\n alerts -> contacts: 1\n}",
1683
+ args: STAGE_LABEL,
1684
+ props: { component: STAGE_COMPONENT },
1685
+ children: [],
1686
+ edges: "branches"
1687
+ },
1688
+ thresholds: {
1689
+ doc: "The utilization of the target and of \"runs out\".",
1690
+ example: "thresholds { target: 0.5; full: 1 }",
1691
+ args: [],
1692
+ props: {
1693
+ target: {
1694
+ value: "number",
1695
+ scope: "const",
1696
+ doc: "Default 0.5."
1697
+ },
1698
+ full: {
1699
+ value: "number",
1700
+ scope: "const",
1701
+ doc: "Default 1."
1702
+ }
1703
+ },
1704
+ children: []
1705
+ }
1706
+ };
1707
+ /** The properties of an edge's block: `a -> b { label: …; when: … }`. */
1708
+ const EDGE_PROPS = {
1709
+ label: {
1710
+ value: "string",
1711
+ scope: "ctx",
1712
+ doc: "The text on the edge."
1713
+ },
1714
+ when: {
1715
+ value: "boolean",
1716
+ scope: "ctx",
1717
+ doc: "The edge is left out while this is false."
1718
+ }
1719
+ };
1720
+ const BOXES = [
1721
+ "node",
1722
+ "cluster",
1723
+ "pool"
1724
+ ];
1725
+ const STAGES = [
1726
+ "pass",
1727
+ "store",
1728
+ "sink",
1729
+ "queue",
1730
+ "workers",
1731
+ "fanout"
1732
+ ];
1733
+ // ../dsl/src/expr.ts
1734
+ /** A formula failed while the model ran: where, and why. */
1735
+ var FormulaError = class extends Error {
1736
+ span;
1737
+ constructor(span, message) {
1738
+ super(message);
1739
+ this.span = span;
1740
+ }
1741
+ };
1742
+ const num = (f) => f;
1743
+ /** Functions renamed before 0.1: a definition from before says what to write instead. */
1744
+ const RENAMED = { kfmt: "compact" };
1745
+ const BUILTINS = {
1746
+ min: {
1747
+ args: ["number"],
1748
+ variadic: true,
1749
+ min: 1,
1750
+ result: "number",
1751
+ fn: num(Math.min)
1752
+ },
1753
+ max: {
1754
+ args: ["number"],
1755
+ variadic: true,
1756
+ min: 1,
1757
+ result: "number",
1758
+ fn: num(Math.max)
1759
+ },
1760
+ round: {
1761
+ args: ["number"],
1762
+ result: "number",
1763
+ fn: num(Math.round)
1764
+ },
1765
+ floor: {
1766
+ args: ["number"],
1767
+ result: "number",
1768
+ fn: num(Math.floor)
1769
+ },
1770
+ ceil: {
1771
+ args: ["number"],
1772
+ result: "number",
1773
+ fn: num(Math.ceil)
1774
+ },
1775
+ abs: {
1776
+ args: ["number"],
1777
+ result: "number",
1778
+ fn: num(Math.abs)
1779
+ },
1780
+ sqrt: {
1781
+ args: ["number"],
1782
+ result: "number",
1783
+ fn: num(Math.sqrt)
1784
+ },
1785
+ log: {
1786
+ args: ["number"],
1787
+ result: "number",
1788
+ fn: num(Math.log)
1789
+ },
1790
+ exp: {
1791
+ args: ["number"],
1792
+ result: "number",
1793
+ fn: num(Math.exp)
1794
+ },
1795
+ pow: {
1796
+ args: ["number", "number"],
1797
+ result: "number",
1798
+ fn: num(Math.pow)
1799
+ },
1800
+ fmt: {
1801
+ args: ["number"],
1802
+ result: "string",
1803
+ fn: fmt
1804
+ },
1805
+ compact: {
1806
+ args: ["number"],
1807
+ result: "string",
1808
+ fn: compact
1809
+ },
1810
+ pct: {
1811
+ args: ["number"],
1812
+ result: "string",
1813
+ fn: pct
1814
+ },
1815
+ secs: {
1816
+ args: ["number"],
1817
+ result: "string",
1818
+ fn: secs
1819
+ },
1820
+ measured: {
1821
+ args: [
1822
+ "number",
1823
+ "string",
1824
+ "string"
1825
+ ],
1826
+ min: 2,
1827
+ result: "capacity",
1828
+ fn: measured
1829
+ },
1830
+ estimated: {
1831
+ args: [
1832
+ "number",
1833
+ "string",
1834
+ "string"
1835
+ ],
1836
+ min: 2,
1837
+ result: "capacity",
1838
+ fn: estimated
1839
+ }
1840
+ };
1841
+ /** The tables a formula may read with a ".": flows.orders, demand.db, util.db, derived.x. */
1842
+ const TABLES = [
1843
+ "flows",
1844
+ "demand",
1845
+ "util",
1846
+ "derived"
1847
+ ];
1848
+ /** Names a declaration cannot take: the tables, the locals, the literals and what plain objects already have. */
1849
+ const RESERVED = /* @__PURE__ */ new Set([
1850
+ "load",
1851
+ ...TABLES,
1852
+ "i",
1853
+ "value",
1854
+ "true",
1855
+ "false",
1856
+ "Math",
1857
+ ...Object.keys(BUILTINS),
1858
+ ...Object.getOwnPropertyNames(Object.prototype),
1859
+ "prototype"
1860
+ ]);
1861
+ const WORD = {
1862
+ number: "a number",
1863
+ boolean: "true or false",
1864
+ string: "a text",
1865
+ capacity: "a capacity",
1866
+ any: "a value"
1867
+ };
1868
+ const typeWord = (t) => WORD[t];
1869
+ /**
1870
+ * Checks a formula against what it may read (`scope`) and compiles it. Errors are reported; the result then has type
1871
+ * `any` and returns undefined, so checking goes on.
1872
+ */
1873
+ function compileFormula(e, names, scope, report) {
1874
+ return new Compiler$1(names, scope, report).compile(e);
1875
+ }
1876
+ const bad = {
1877
+ type: "any",
1878
+ constant: true,
1879
+ run: () => void 0
1880
+ };
1881
+ var Compiler$1 = class {
1882
+ names;
1883
+ scope;
1884
+ report;
1885
+ constructor(names, scope, report) {
1886
+ this.names = names;
1887
+ this.scope = scope;
1888
+ this.report = report;
1889
+ }
1890
+ canRead(e, what) {
1891
+ if (this.scope === "ctx" || this.scope === "values" && what === "a parameter") return true;
1892
+ const here = this.scope === "const" ? "this value must be a constant" : this.scope === "values" ? "this formula reads only parameters" : `this formula reads only "${this.scope}"`;
1893
+ this.report.error(e, `${here}; it cannot read ${what}`);
1894
+ return false;
1895
+ }
1896
+ compile(e) {
1897
+ switch (e.t) {
1898
+ case "error": return bad;
1899
+ case "num":
1900
+ case "str":
1901
+ case "bool": {
1902
+ const v = e.value;
1903
+ return {
1904
+ type: e.t === "num" ? "number" : e.t === "str" ? "string" : "boolean",
1905
+ constant: true,
1906
+ run: () => v
1907
+ };
1908
+ }
1909
+ case "tpl": {
1910
+ const parts = e.parts.map((p) => this.compile(p));
1911
+ const { quasis } = e;
1912
+ const runs = parts.map((p) => p.run);
1913
+ return {
1914
+ type: "string",
1915
+ constant: parts.every((p) => p.constant),
1916
+ run: (c, l) => {
1917
+ let s = quasis[0];
1918
+ for (let i = 0; i < runs.length; i++) s += String(runs[i](c, l)) + quasis[i + 1];
1919
+ return s;
1920
+ }
1921
+ };
1922
+ }
1923
+ case "id": return this.name(e, e.name);
1924
+ case "member": return this.member(e);
1925
+ case "call": return this.call(e);
1926
+ case "unary": {
1927
+ const a = this.compile(e.arg);
1928
+ const run = a.run;
1929
+ if (e.op === "!") return {
1930
+ type: "boolean",
1931
+ constant: a.constant,
1932
+ run: (c, l) => !run(c, l)
1933
+ };
1934
+ this.want(e.arg, a, "number");
1935
+ return {
1936
+ type: "number",
1937
+ constant: a.constant,
1938
+ run: e.op === "-" ? (c, l) => -run(c, l) : run
1939
+ };
1940
+ }
1941
+ case "bin": return this.binary(e);
1942
+ case "cond": {
1943
+ const test = this.compile(e.test);
1944
+ const then = this.compile(e.then);
1945
+ const other = this.compile(e.else);
1946
+ const [t, a, b] = [
1947
+ test.run,
1948
+ then.run,
1949
+ other.run
1950
+ ];
1951
+ return {
1952
+ type: then.type === other.type ? then.type : "any",
1953
+ constant: test.constant && then.constant && other.constant,
1954
+ run: (c, l) => t(c, l) ? a(c, l) : b(c, l)
1955
+ };
1956
+ }
1957
+ case "array":
1958
+ this.report.error(e, "a list is only allowed as the options of a parameter");
1959
+ return bad;
1960
+ case "pm":
1961
+ this.report.error(e, "\"±\" only goes in the first value of measured(…) or estimated(…)");
1962
+ return bad;
1963
+ }
1964
+ }
1965
+ name(e, name) {
1966
+ if (name === "i" || name === "value") {
1967
+ if (this.scope !== name) {
1968
+ this.report.error(e, `"${name}" is only known in ${name === "i" ? "memberLabel and podLabel" : "format"}`);
1969
+ return bad;
1970
+ }
1971
+ return {
1972
+ type: name === "i" ? "number" : "any",
1973
+ constant: false,
1974
+ run: (_, l) => l
1975
+ };
1976
+ }
1977
+ const param = this.names.params.get(name);
1978
+ if (param) {
1979
+ if (!this.canRead(e, "a parameter")) return bad;
1980
+ return {
1981
+ type: param,
1982
+ constant: false,
1983
+ run: (c) => c[name]
1984
+ };
1985
+ }
1986
+ const let_ = this.names.lets.get(name);
1987
+ if (let_) {
1988
+ if (!this.canRead(e, `"${name}"`)) return bad;
1989
+ return {
1990
+ type: let_,
1991
+ constant: false,
1992
+ run: (c) => c.derived[name]
1993
+ };
1994
+ }
1995
+ if (TABLES.includes(name)) {
1996
+ this.report.error(e, `"${name}" is a table: name one of its entries, like ${name}.${this.example(name)}`);
1997
+ return bad;
1998
+ }
1999
+ if (this.names.fits.has(name) || own(BUILTINS, name) || own(this.names.functions, name)) {
2000
+ this.report.error(e, `"${name}" is a function: call it, ${name}(…)`);
2001
+ return bad;
2002
+ }
2003
+ const known = [...this.names.params.keys(), ...this.names.lets.keys()];
2004
+ this.report.error(e, `unknown name "${name}"`, didYouMean(name, known));
2005
+ return bad;
2006
+ }
2007
+ example(table) {
2008
+ return [...table === "flows" ? this.names.flows : table === "derived" ? this.names.lets.keys() : this.names.components][0] ?? "x";
2009
+ }
2010
+ member(e) {
2011
+ const table = e.object.name;
2012
+ const key = e.prop.name;
2013
+ const pool = table === "flows" ? this.names.flows : table === "demand" || table === "util" ? this.names.components : table === "derived" ? this.names.lets.keys() : void 0;
2014
+ if (!pool) {
2015
+ this.report.error(e.object, `"${table}" has no entries: only flows., demand., util. and derived. do`);
2016
+ return bad;
2017
+ }
2018
+ if (!(table === "derived" ? this.names.lets.has(key) : pool.has(key))) {
2019
+ const what = table === "flows" ? "flow" : table === "derived" ? "let" : "component";
2020
+ this.report.error(e.prop, `unknown ${what} "${key}"`, didYouMean(key, pool));
2021
+ return bad;
2022
+ }
2023
+ if (!this.canRead(e, `${table}.${key}`)) return bad;
2024
+ return {
2025
+ type: table === "derived" ? this.names.lets.get(key) : "number",
2026
+ constant: false,
2027
+ run: (c) => c[table][key]
2028
+ };
2029
+ }
2030
+ call(e) {
2031
+ const name = e.callee.name;
2032
+ const pm = e.args[0]?.t === "pm" ? e.args[0] : null;
2033
+ if (pm && (name === "measured" || name === "estimated")) return this.capacity(e, pm);
2034
+ const args = e.args.map((a) => this.compile(a));
2035
+ const runs = args.map((a) => a.run);
2036
+ const fit = this.names.fits.get(name);
2037
+ if (this.names.fits.has(name)) {
2038
+ if (args.length !== 1) this.report.error(e, `the fit "${name}" takes one number`);
2039
+ else this.want(e.args[0], args[0], "number");
2040
+ const a = runs[0] ?? (() => void 0);
2041
+ return {
2042
+ type: "number",
2043
+ constant: args.every((x) => x.constant),
2044
+ run: (c, l) => fit?.(a(c, l))
2045
+ };
2046
+ }
2047
+ const b = own(BUILTINS, name);
2048
+ if (b) {
2049
+ const max = b.variadic ? Number.POSITIVE_INFINITY : b.args.length;
2050
+ const min = b.min ?? b.args.length;
2051
+ if (args.length < min || args.length > max) this.report.error(e, `${name}() takes ${min === max ? min : `${min} to ${max}`} values, not ${args.length}`);
2052
+ for (const [i, a] of args.entries()) this.want(e.args[i], a, b.args[Math.min(i, b.args.length - 1)]);
2053
+ const fn = b.fn;
2054
+ return {
2055
+ type: b.result,
2056
+ constant: args.every((a) => a.constant),
2057
+ run: (c, l) => fn(...runs.map((r) => r(c, l)))
2058
+ };
2059
+ }
2060
+ const host = own(this.names.functions, name);
2061
+ if (host) return {
2062
+ type: "any",
2063
+ constant: false,
2064
+ run: (c, l) => host(...runs.map((r) => r(c, l)))
2065
+ };
2066
+ const known = [
2067
+ ...Object.keys(BUILTINS),
2068
+ ...this.names.fits.keys(),
2069
+ ...Object.keys(this.names.functions)
2070
+ ];
2071
+ this.report.error(e.callee, `unknown function "${name}"`, (own(RENAMED, name) ? `renamed to ${own(RENAMED, name)}()` : void 0) ?? didYouMean(name, known) ?? "a host page can provide it in compile(…, { functions })");
2072
+ return bad;
2073
+ }
2074
+ /** measured(value ± within[%], unit, source): the ± becomes the capacity's `within`, a share of the value. */
2075
+ capacity(e, pm) {
2076
+ const fn = e.callee.name === "measured" ? measured : estimated;
2077
+ const [value, within] = [this.compile(pm.value), this.compile(pm.within)];
2078
+ const rest = e.args.slice(1).map((a) => this.compile(a));
2079
+ this.want(pm.value, value, "number");
2080
+ this.want(pm.within, within, "number");
2081
+ if (rest.length < 1 || rest.length > 2) this.report.error(e, `${e.callee.name}() takes 2 to 3 values, not ${rest.length + 1}`);
2082
+ for (const [i, a] of rest.entries()) this.want(e.args[i + 1], a, "string");
2083
+ const [v, w] = [value.run, within.run];
2084
+ const [unit, source] = rest.map((a) => a.run);
2085
+ const { percent } = pm;
2086
+ return {
2087
+ type: "capacity",
2088
+ constant: [
2089
+ value,
2090
+ within,
2091
+ ...rest
2092
+ ].every((a) => a.constant),
2093
+ run: (c, l) => {
2094
+ const x = v(c, l);
2095
+ const pmv = w(c, l);
2096
+ const share = percent ? pmv / 100 : x ? pmv / x : 0;
2097
+ if (!(share >= 0 && share < 1)) {
2098
+ const said = percent ? `${pmv} %` : `${pmv} of ${x}`;
2099
+ throw new Error(`± ${said} is not a share from 0 to below 100 %`);
2100
+ }
2101
+ return fn(x, unit?.(c, l), {
2102
+ source: source?.(c, l),
2103
+ within: share
2104
+ });
2105
+ }
2106
+ };
2107
+ }
2108
+ binary(e) {
2109
+ const l = this.compile(e.left);
2110
+ const r = this.compile(e.right);
2111
+ const [a, b] = [l.run, r.run];
2112
+ const constant = l.constant && r.constant;
2113
+ const f = (type, run) => ({
2114
+ type,
2115
+ constant,
2116
+ run
2117
+ });
2118
+ switch (e.op) {
2119
+ case "||": return f(l.type === r.type ? l.type : "any", (c, x) => a(c, x) || b(c, x));
2120
+ case "&&": return f(l.type === r.type ? l.type : "any", (c, x) => a(c, x) && b(c, x));
2121
+ case "==": return f("boolean", (c, x) => a(c, x) === b(c, x));
2122
+ case "!=": return f("boolean", (c, x) => a(c, x) !== b(c, x));
2123
+ case "<": return f("boolean", (c, x) => a(c, x) < b(c, x));
2124
+ case "<=": return f("boolean", (c, x) => a(c, x) <= b(c, x));
2125
+ case ">": return f("boolean", (c, x) => a(c, x) > b(c, x));
2126
+ case ">=": return f("boolean", (c, x) => a(c, x) >= b(c, x));
2127
+ case "+": {
2128
+ const type = l.type === "string" || r.type === "string" ? "string" : l.type === "number" && r.type === "number" ? "number" : "any";
2129
+ if (type !== "string") {
2130
+ this.want(e.left, l, "number");
2131
+ this.want(e.right, r, "number");
2132
+ }
2133
+ return f(type, (c, x) => a(c, x) + b(c, x));
2134
+ }
2135
+ default: {
2136
+ this.want(e.left, l, "number");
2137
+ this.want(e.right, r, "number");
2138
+ const op = e.op;
2139
+ return f("number", (c, x) => {
2140
+ const u = a(c, x);
2141
+ const v = b(c, x);
2142
+ return op === "-" ? u - v : op === "*" ? u * v : op === "/" ? u / v : u % v;
2143
+ });
2144
+ }
2145
+ }
2146
+ }
2147
+ want(at, f, type) {
2148
+ if (f.type !== type && f.type !== "any" && type !== "any") this.report.error(at, `expected ${typeWord(type)}, this is ${typeWord(f.type)}`);
2149
+ }
2150
+ };
2151
+ /**
2152
+ * Wraps a formula for the model: it takes the context, and any error it throws carries the formula's place. An error
2153
+ * already placed (from a formula this one read) keeps its place: the innermost formula is the one to fix.
2154
+ */
2155
+ function placed(span, run) {
2156
+ return (ctx, local) => {
2157
+ try {
2158
+ return run(ctx, local);
2159
+ } catch (err) {
2160
+ if (err instanceof FormulaError) throw err;
2161
+ throw new FormulaError(span, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
2162
+ }
2163
+ };
2164
+ }
2165
+ const isCapacity = (v) => typeof v === "object" && v != null && "provenance" in v && "value" in v;
2166
+ // ../dsl/src/lexer.ts
2167
+ const PUNCT = [
2168
+ "===",
2169
+ "!==",
2170
+ "+/-",
2171
+ "->",
2172
+ "==",
2173
+ "!=",
2174
+ "<=",
2175
+ ">=",
2176
+ "&&",
2177
+ "||",
2178
+ ..."{}()[],;:.?+-*/%<>!=±"
2179
+ ];
2180
+ /**
2181
+ * Splits `source[from, to)` into tokens. Newlines end statements, except inside parentheses and brackets, so a long
2182
+ * formula can wrap there. `#` starts a comment to the end of the line.
2183
+ */
2184
+ function lex(source, report, from = 0, to = source.length) {
2185
+ const out = [];
2186
+ let depth = 0;
2187
+ let i = from;
2188
+ const push = (t) => out.push(t);
2189
+ while (i < to) {
2190
+ const c = source[i];
2191
+ if (c === "\n") {
2192
+ if (depth === 0 && out.at(-1)?.kind !== "newline") push({
2193
+ kind: "newline",
2194
+ text: "\n",
2195
+ from: i,
2196
+ to: i + 1
2197
+ });
2198
+ i++;
2199
+ } else if (c === " " || c === " " || c === "\r") i++;
2200
+ else if (c === "#") while (i < to && source[i] !== "\n") i++;
2201
+ else if (isIdStart(c)) {
2202
+ const s = i;
2203
+ while (i < to && isIdPart(source[i])) i++;
2204
+ push({
2205
+ kind: "ident",
2206
+ text: source.slice(s, i),
2207
+ from: s,
2208
+ to: i
2209
+ });
2210
+ } else if (isDigit(c) || c === "." && isDigit(source[i + 1] ?? "")) {
2211
+ const s = i;
2212
+ while (i < to && (isDigit(source[i]) || source[i] === "_")) i++;
2213
+ if (source[i] === "." && isDigit(source[i + 1] ?? "")) {
2214
+ i++;
2215
+ while (i < to && (isDigit(source[i]) || source[i] === "_")) i++;
2216
+ }
2217
+ const sign = source[i + 1] === "+" || source[i + 1] === "-" ? 1 : 0;
2218
+ if ((source[i] === "e" || source[i] === "E") && isDigit(source[i + 1 + sign] ?? "")) {
2219
+ i += 1 + sign;
2220
+ while (i < to && isDigit(source[i])) i++;
2221
+ }
2222
+ const text = source.slice(s, i);
2223
+ if (text.endsWith("_") || text.includes("__")) report.error({
2224
+ from: s,
2225
+ to: i
2226
+ }, `"${text}" is not a number`);
2227
+ push({
2228
+ kind: "number",
2229
+ text,
2230
+ value: Number(text.replaceAll("_", "")),
2231
+ from: s,
2232
+ to: i
2233
+ });
2234
+ } else if (c === "\"" || c === "'") {
2235
+ const s = i;
2236
+ const r = readString(source, i + 1, to, c);
2237
+ if (!r.closed) report.error({
2238
+ from: s,
2239
+ to: r.end
2240
+ }, "this text has no closing quote", `end it with ${c}`);
2241
+ i = r.end;
2242
+ push({
2243
+ kind: "string",
2244
+ text: source.slice(s, i),
2245
+ value: r.value,
2246
+ from: s,
2247
+ to: i
2248
+ });
2249
+ } else if (c === "`") {
2250
+ const s = i;
2251
+ const t = readTemplate(source, i + 1, to, report);
2252
+ if (!t.closed) report.error({
2253
+ from: s,
2254
+ to: t.end
2255
+ }, "this template has no closing backtick", "end it with `");
2256
+ i = t.end;
2257
+ push({
2258
+ kind: "template",
2259
+ text: source.slice(s, i),
2260
+ quasis: t.quasis,
2261
+ parts: t.parts,
2262
+ from: s,
2263
+ to: i
2264
+ });
2265
+ } else {
2266
+ const p = PUNCT.find((m) => source.startsWith(m, i));
2267
+ if (!p) {
2268
+ report.error({
2269
+ from: i,
2270
+ to: i + 1
2271
+ }, `unexpected character "${c}"`);
2272
+ i++;
2273
+ continue;
2274
+ }
2275
+ if (p === "(" || p === "[") depth++;
2276
+ else if ((p === ")" || p === "]") && depth > 0) depth--;
2277
+ else if (p === "{" || p === "}") depth = 0;
2278
+ push({
2279
+ kind: "punct",
2280
+ text: p,
2281
+ from: i,
2282
+ to: i + p.length
2283
+ });
2284
+ i += p.length;
2285
+ }
2286
+ }
2287
+ push({
2288
+ kind: "eof",
2289
+ text: "",
2290
+ from: to,
2291
+ to
2292
+ });
2293
+ return out;
2294
+ }
2295
+ const isIdStart = (c) => /[A-Za-z_]/.test(c);
2296
+ const isIdPart = (c) => /[A-Za-z0-9_]/.test(c);
2297
+ const isDigit = (c) => c >= "0" && c <= "9";
2298
+ const ESCAPES = {
2299
+ n: "\n",
2300
+ t: " ",
2301
+ "\\": "\\",
2302
+ "\"": "\"",
2303
+ "'": "'",
2304
+ "`": "`",
2305
+ $: "$"
2306
+ };
2307
+ function readString(src, i, to, q) {
2308
+ let value = "";
2309
+ while (i < to && src[i] !== q && src[i] !== "\n") if (src[i] === "\\" && i + 1 < to) {
2310
+ value += ESCAPES[src[i + 1]] ?? src[i + 1];
2311
+ i += 2;
2312
+ } else value += src[i++];
2313
+ const closed = src[i] === q;
2314
+ return {
2315
+ value,
2316
+ end: closed ? i + 1 : i,
2317
+ closed
2318
+ };
2319
+ }
2320
+ /** Templates nested deeper than this are refused: the lexer reads them recursively. */
2321
+ const MAX_TEMPLATE_DEPTH = 16;
2322
+ function readTemplate(src, i, to, report, depth = 0) {
2323
+ const quasis = [];
2324
+ const parts = [];
2325
+ let cur = "";
2326
+ while (i < to && src[i] !== "`") if (src[i] === "\\" && i + 1 < to) {
2327
+ cur += ESCAPES[src[i + 1]] ?? src[i + 1];
2328
+ i += 2;
2329
+ } else if (src[i] === "$" && src[i + 1] === "{") {
2330
+ quasis.push(cur);
2331
+ cur = "";
2332
+ const start = i + 2;
2333
+ const end = skipExpression(src, start, to, report, depth);
2334
+ parts.push({
2335
+ from: start,
2336
+ to: end
2337
+ });
2338
+ if (src[end] === "}") i = end + 1;
2339
+ else {
2340
+ report.error({
2341
+ from: i,
2342
+ to: end
2343
+ }, "this \"${\" has no closing \"}\"");
2344
+ i = end;
2345
+ }
2346
+ } else cur += src[i++];
2347
+ quasis.push(cur);
2348
+ const closed = src[i] === "`";
2349
+ return {
2350
+ quasis,
2351
+ parts,
2352
+ end: closed ? i + 1 : i,
2353
+ closed
2354
+ };
2355
+ }
2356
+ /** The offset of the `}` that closes a template's `${`, skipping nested braces, strings and templates. */
2357
+ function skipExpression(src, i, to, report, depth) {
2358
+ let braces = 0;
2359
+ while (i < to) {
2360
+ const c = src[i];
2361
+ if (c === "}" && braces === 0) return i;
2362
+ if (c === "{") braces++;
2363
+ else if (c === "}") braces--;
2364
+ else if (c === "\"" || c === "'") {
2365
+ i = readString(src, i + 1, to, c).end;
2366
+ continue;
2367
+ } else if (c === "`") {
2368
+ if (depth + 1 >= MAX_TEMPLATE_DEPTH) {
2369
+ report.error({
2370
+ from: i,
2371
+ to: i + 1
2372
+ }, `templates are nested more than ${MAX_TEMPLATE_DEPTH} deep`);
2373
+ return to;
2374
+ }
2375
+ i = readTemplate(src, i + 1, to, report, depth + 1).end;
2376
+ continue;
2377
+ } else if (c === "\n") return i;
2378
+ i++;
2379
+ }
2380
+ return i;
2381
+ }
2382
+ /** Longer sources are refused before parsing. */
2383
+ const MAX_SOURCE = 2e5;
2384
+ const PREC = {
2385
+ "||": 1,
2386
+ "&&": 2,
2387
+ "==": 3,
2388
+ "!=": 3,
2389
+ "===": 3,
2390
+ "!==": 3,
2391
+ "<": 4,
2392
+ "<=": 4,
2393
+ ">": 4,
2394
+ ">=": 4,
2395
+ "+": 5,
2396
+ "-": 5,
2397
+ "*": 6,
2398
+ "/": 6,
2399
+ "%": 6
2400
+ };
2401
+ /** Thrown after a syntax error has been reported, to skip to the next statement. */
2402
+ var Bail$1 = class {};
2403
+ /** Parses a definition into statements. Never throws: errors go to the reporter, and parsing goes on at the next line. */
2404
+ function parse$1(source, report = new Reporter(source)) {
2405
+ if (source.length > 2e5) {
2406
+ report.error({
2407
+ from: 0,
2408
+ to: 0
2409
+ }, `the definition is ${source.length} characters long; at most ${MAX_SOURCE}`);
2410
+ return {
2411
+ doc: { body: [] },
2412
+ report
2413
+ };
2414
+ }
2415
+ return {
2416
+ doc: { body: new Parser(lex(source, report), source, report).block(false) },
2417
+ report
2418
+ };
2419
+ }
2420
+ /** Parses one formula on its own, e.g. a template's `${…}` part. */
2421
+ function parseExpression(source, from, to, report, depth = 0) {
2422
+ const p = new Parser(lex(source, report, from, to), source, report);
2423
+ p.depth = depth;
2424
+ try {
2425
+ const e = p.expr();
2426
+ if (p.peek().kind !== "eof") p.fail(p.peek(), `unexpected ${describe(p.peek())} in the formula`);
2427
+ return e;
2428
+ } catch (e) {
2429
+ if (e instanceof Bail$1) return {
2430
+ t: "error",
2431
+ from,
2432
+ to
2433
+ };
2434
+ throw e;
2435
+ }
2436
+ }
2437
+ var Parser = class {
2438
+ toks;
2439
+ source;
2440
+ report;
2441
+ i = 0;
2442
+ depth = 0;
2443
+ /** Blocks open around the statement being read. */
2444
+ blocks = 0;
2445
+ constructor(toks, source, report) {
2446
+ this.toks = toks;
2447
+ this.source = source;
2448
+ this.report = report;
2449
+ }
2450
+ peek(k = 0) {
2451
+ return this.toks[Math.min(this.i + k, this.toks.length - 1)];
2452
+ }
2453
+ next() {
2454
+ const t = this.peek();
2455
+ if (t.kind !== "eof") this.i++;
2456
+ return t;
2457
+ }
2458
+ is(text, k = 0) {
2459
+ const t = this.peek(k);
2460
+ return t.kind === "punct" && t.text === text;
2461
+ }
2462
+ eat(text) {
2463
+ if (!this.is(text)) return false;
2464
+ this.i++;
2465
+ return true;
2466
+ }
2467
+ expect(text, what) {
2468
+ if (!this.is(text)) this.fail(this.peek(), `expected ${what}, found ${describe(this.peek())}`);
2469
+ return this.next();
2470
+ }
2471
+ fail(at, message, hint) {
2472
+ this.report.error(at, message, hint);
2473
+ throw new Bail$1();
2474
+ }
2475
+ /** Statements up to the end of the file, or up to the `}` that closes this block (left for the caller). */
2476
+ block(inBraces) {
2477
+ const out = [];
2478
+ for (;;) {
2479
+ while (this.peek().kind === "newline" || this.is(";")) this.next();
2480
+ const t = this.peek();
2481
+ if (t.kind === "eof") break;
2482
+ if (this.is("}")) {
2483
+ if (inBraces) break;
2484
+ this.report.error(t, "this \"}\" closes no block");
2485
+ this.next();
2486
+ continue;
2487
+ }
2488
+ try {
2489
+ out.push(this.statement());
2490
+ const end = this.peek();
2491
+ if (!(end.kind === "newline" || end.kind === "eof" || this.is(";") || this.is("}"))) this.fail(end, `expected the end of the line, found ${describe(end)}`);
2492
+ } catch (e) {
2493
+ if (!(e instanceof Bail$1)) throw e;
2494
+ this.recover();
2495
+ }
2496
+ }
2497
+ return out;
2498
+ }
2499
+ /** Skips to the end of the broken statement: the next line, `;` or the `}` of the enclosing block. */
2500
+ recover() {
2501
+ let depth = 0;
2502
+ for (;;) {
2503
+ const t = this.peek();
2504
+ if (t.kind === "eof") return;
2505
+ if (depth === 0 && (t.kind === "newline" || this.is(";") || this.is("}"))) return;
2506
+ if (this.is("{")) depth++;
2507
+ else if (this.is("}")) depth--;
2508
+ this.next();
2509
+ }
2510
+ }
2511
+ statement() {
2512
+ const t = this.peek();
2513
+ if (t.kind === "number" || this.is("-") && this.peek(1).kind === "number") return this.row();
2514
+ if (t.kind !== "ident") this.fail(t, `expected a statement, found ${describe(t)}`);
2515
+ if (this.is(":", 1)) return this.property();
2516
+ if (this.is("->", 1)) return this.edge();
2517
+ return this.decl();
2518
+ }
2519
+ ident(what = "a name") {
2520
+ const t = this.peek();
2521
+ if (t.kind !== "ident") this.fail(t, `expected ${what}, found ${describe(t)}`);
2522
+ this.next();
2523
+ return {
2524
+ name: t.text,
2525
+ from: t.from,
2526
+ to: t.to
2527
+ };
2528
+ }
2529
+ property() {
2530
+ const key = this.ident();
2531
+ this.expect(":", "\":\"");
2532
+ if (this.endsStatement()) this.fail(this.peek(), `"${key.name}" has no value`);
2533
+ const value = this.expr();
2534
+ if (this.is("->")) {
2535
+ if (value.t !== "id") this.fail(value, "a route is a list of names joined by \"->\"");
2536
+ const chain = [{
2537
+ name: value.name,
2538
+ from: value.from,
2539
+ to: value.to
2540
+ }];
2541
+ while (this.eat("->")) chain.push(this.ident("a stage"));
2542
+ return {
2543
+ kind: "prop",
2544
+ key,
2545
+ value,
2546
+ chain,
2547
+ from: key.from,
2548
+ to: chain.at(-1).to
2549
+ };
2550
+ }
2551
+ return {
2552
+ kind: "prop",
2553
+ key,
2554
+ value,
2555
+ from: key.from,
2556
+ to: value.to
2557
+ };
2558
+ }
2559
+ edge() {
2560
+ const chain = [this.ident()];
2561
+ while (this.eat("->")) chain.push(this.ident("a name after \"->\""));
2562
+ const e = {
2563
+ kind: "edge",
2564
+ chain,
2565
+ from: chain[0].from,
2566
+ to: chain.at(-1).to
2567
+ };
2568
+ if (this.eat(":")) {
2569
+ if (this.endsStatement()) this.fail(this.peek(), "expected a value after \":\"");
2570
+ e.value = this.expr();
2571
+ e.to = e.value.to;
2572
+ }
2573
+ if (this.is("{")) this.body(e);
2574
+ return e;
2575
+ }
2576
+ decl() {
2577
+ const k = this.next();
2578
+ const d = {
2579
+ kind: "decl",
2580
+ keyword: {
2581
+ name: k.text,
2582
+ from: k.from,
2583
+ to: k.to
2584
+ },
2585
+ args: [],
2586
+ from: k.from,
2587
+ to: k.to
2588
+ };
2589
+ for (;;) {
2590
+ const t = this.peek();
2591
+ if (t.kind === "ident" || t.kind === "string" || t.kind === "template" || t.kind === "number") d.args.push(this.primary());
2592
+ else if (this.is("-") && this.peek(1).kind === "number") d.args.push(this.unary());
2593
+ else break;
2594
+ d.to = d.args.at(-1).to;
2595
+ }
2596
+ if (this.eat("=")) {
2597
+ if (this.endsStatement()) this.fail(this.peek(), "expected a formula after \"=\"");
2598
+ d.init = this.expr();
2599
+ d.to = d.init.to;
2600
+ }
2601
+ if (this.is("{")) this.body(d);
2602
+ else if (!this.endsStatement()) this.fail(this.peek(), `expected "{" or the end of the line, found ${describe(this.peek())}`);
2603
+ return d;
2604
+ }
2605
+ body(d) {
2606
+ if (this.blocks >= 64) {
2607
+ const open = this.next();
2608
+ this.report.error(open, `blocks are nested more than 64 deep`);
2609
+ for (let depth = 1; depth > 0 && this.peek().kind !== "eof";) {
2610
+ const t = this.next();
2611
+ if (t.kind === "punct" && t.text === "{") depth++;
2612
+ else if (t.kind === "punct" && t.text === "}") depth--;
2613
+ }
2614
+ d.to = this.peek().from;
2615
+ return;
2616
+ }
2617
+ this.blocks++;
2618
+ try {
2619
+ this.bodyInside(d);
2620
+ } finally {
2621
+ this.blocks--;
2622
+ }
2623
+ }
2624
+ bodyInside(d) {
2625
+ const open = this.next();
2626
+ d.body = this.block(true);
2627
+ const close = this.peek();
2628
+ if (!this.is("}")) {
2629
+ this.report.error(open, `this "{" of "${"keyword" in d ? d.keyword.name : "edge"}" is never closed`);
2630
+ d.bodySpan = {
2631
+ from: open.from,
2632
+ to: close.from + 1
2633
+ };
2634
+ d.to = close.from;
2635
+ return;
2636
+ }
2637
+ this.next();
2638
+ d.bodySpan = {
2639
+ from: open.from,
2640
+ to: close.to
2641
+ };
2642
+ d.to = close.to;
2643
+ }
2644
+ row() {
2645
+ const values = [];
2646
+ while (this.peek().kind === "number" || this.is("-") && this.peek(1).kind === "number") {
2647
+ values.push(this.unary());
2648
+ this.eat(",");
2649
+ }
2650
+ return {
2651
+ kind: "row",
2652
+ values,
2653
+ from: values[0].from,
2654
+ to: values.at(-1).to
2655
+ };
2656
+ }
2657
+ endsStatement() {
2658
+ const t = this.peek();
2659
+ return t.kind === "newline" || t.kind === "eof" || this.is(";") || this.is("}");
2660
+ }
2661
+ expr() {
2662
+ if (++this.depth > 64) this.fail(this.peek(), `the formula is nested more than 64 deep`);
2663
+ try {
2664
+ const test = this.binary(1);
2665
+ if (this.is("±") || this.is("+/-")) return this.pm(test);
2666
+ if (!this.eat("?")) return test;
2667
+ const then = this.expr();
2668
+ this.expect(":", "\":\" of the \"?\"");
2669
+ const otherwise = this.expr();
2670
+ return {
2671
+ t: "cond",
2672
+ test,
2673
+ then,
2674
+ else: otherwise,
2675
+ from: test.from,
2676
+ to: otherwise.to
2677
+ };
2678
+ } finally {
2679
+ this.depth--;
2680
+ }
2681
+ }
2682
+ /** `value ± within` or `value ± within%`: parsed anywhere, so the checker can say where it belongs. */
2683
+ pm(value) {
2684
+ this.next();
2685
+ const within = this.unary();
2686
+ const percent = this.is("%") ? this.next() : null;
2687
+ return {
2688
+ t: "pm",
2689
+ value,
2690
+ within,
2691
+ percent: !!percent,
2692
+ from: value.from,
2693
+ to: percent?.to ?? within.to
2694
+ };
2695
+ }
2696
+ binary(min) {
2697
+ let left = this.unary();
2698
+ for (;;) {
2699
+ const t = this.peek();
2700
+ const prec = t.kind === "punct" ? PREC[t.text] : void 0;
2701
+ if (prec == null || prec < min) return left;
2702
+ this.next();
2703
+ const right = this.binary(prec + 1);
2704
+ left = {
2705
+ t: "bin",
2706
+ op: t.text === "===" ? "==" : t.text === "!==" ? "!=" : t.text,
2707
+ left,
2708
+ right,
2709
+ from: left.from,
2710
+ to: right.to
2711
+ };
2712
+ }
2713
+ }
2714
+ unary() {
2715
+ const t = this.peek();
2716
+ if (this.is("-") || this.is("+") || this.is("!")) {
2717
+ this.next();
2718
+ if (++this.depth > 64) this.fail(t, `the formula is nested more than 64 deep`);
2719
+ try {
2720
+ const arg = this.unary();
2721
+ return {
2722
+ t: "unary",
2723
+ op: t.text,
2724
+ arg,
2725
+ from: t.from,
2726
+ to: arg.to
2727
+ };
2728
+ } finally {
2729
+ this.depth--;
2730
+ }
2731
+ }
2732
+ return this.primary();
2733
+ }
2734
+ primary() {
2735
+ const t = this.peek();
2736
+ if (!(t.kind !== "punct" ? t.kind !== "newline" && t.kind !== "eof" : t.text === "(" || t.text === "[")) this.fail(t, `expected a value, found ${describe(t)}`);
2737
+ this.next();
2738
+ switch (t.kind) {
2739
+ case "number": return {
2740
+ t: "num",
2741
+ value: t.value,
2742
+ from: t.from,
2743
+ to: t.to
2744
+ };
2745
+ case "string": return {
2746
+ t: "str",
2747
+ value: t.value,
2748
+ from: t.from,
2749
+ to: t.to
2750
+ };
2751
+ case "template": return {
2752
+ t: "tpl",
2753
+ quasis: t.quasis,
2754
+ parts: t.parts.map((s) => this.part(s)),
2755
+ from: t.from,
2756
+ to: t.to
2757
+ };
2758
+ case "ident": {
2759
+ if (t.text === "true" || t.text === "false") return {
2760
+ t: "bool",
2761
+ value: t.text === "true",
2762
+ from: t.from,
2763
+ to: t.to
2764
+ };
2765
+ let callee = {
2766
+ name: t.text,
2767
+ from: t.from,
2768
+ to: t.to
2769
+ };
2770
+ if (this.is(".")) {
2771
+ this.next();
2772
+ const prop = this.ident("a name after \".\"");
2773
+ if (!this.is("(")) {
2774
+ if (this.is(".")) this.fail(this.peek(), "only one \".\" is allowed: flows.orders, util.db");
2775
+ return {
2776
+ t: "member",
2777
+ object: callee,
2778
+ prop,
2779
+ from: t.from,
2780
+ to: prop.to
2781
+ };
2782
+ }
2783
+ if (callee.name !== "Math") this.fail(callee, `"${callee.name}.${prop.name}(…)" is not a function`);
2784
+ callee = prop;
2785
+ }
2786
+ if (this.is("(")) {
2787
+ this.next();
2788
+ const args = [];
2789
+ while (!this.is(")")) {
2790
+ args.push(this.expr());
2791
+ if (!this.eat(",")) break;
2792
+ }
2793
+ const close = this.expect(")", "\")\" or \",\"");
2794
+ return {
2795
+ t: "call",
2796
+ callee,
2797
+ args,
2798
+ from: t.from,
2799
+ to: close.to
2800
+ };
2801
+ }
2802
+ return {
2803
+ t: "id",
2804
+ name: t.text,
2805
+ from: t.from,
2806
+ to: t.to
2807
+ };
2808
+ }
2809
+ case "punct":
2810
+ if (t.text === "(") {
2811
+ const e = this.expr();
2812
+ const close = this.expect(")", "\")\"");
2813
+ return {
2814
+ ...e,
2815
+ from: t.from,
2816
+ to: close.to
2817
+ };
2818
+ }
2819
+ if (t.text === "[") {
2820
+ const items = [];
2821
+ while (!this.is("]")) {
2822
+ items.push(this.expr());
2823
+ if (!this.eat(",")) break;
2824
+ }
2825
+ const close = this.expect("]", "\"]\" or \",\"");
2826
+ return {
2827
+ t: "array",
2828
+ items,
2829
+ from: t.from,
2830
+ to: close.to
2831
+ };
2832
+ }
2833
+ }
2834
+ this.fail(t, `expected a value, found ${describe(t)}`);
2835
+ }
2836
+ /** One `${…}` of a template, parsed from its place in the source so errors point into it. */
2837
+ part(s) {
2838
+ if (s.to > s.from) return parseExpression(this.source, s.from, s.to, this.report, this.depth);
2839
+ this.report.error({
2840
+ from: s.from - 2,
2841
+ to: s.to + 1
2842
+ }, "an empty part in a template");
2843
+ return {
2844
+ t: "error",
2845
+ from: s.from,
2846
+ to: s.to
2847
+ };
2848
+ }
2849
+ };
2850
+ function describe(t) {
2851
+ switch (t.kind) {
2852
+ case "eof": return "the end of the file";
2853
+ case "newline": return "the end of the line";
2854
+ case "string":
2855
+ case "template": return "a text";
2856
+ case "number": return `the number ${t.text}`;
2857
+ default: return `"${t.text}"`;
2858
+ }
2859
+ }
2860
+ /** Counts that size the drawing; larger ones would hang the layout, so a shared link cannot ask for them. */
2861
+ const LIMITS = {
2862
+ members: 64,
2863
+ pods: 256,
2864
+ perPod: 256,
2865
+ partitions: 1024
2866
+ };
2867
+ /** Reads a definition: parses, checks, and builds the model and view `<Loadmap>` takes. Never throws. */
2868
+ function compile(source, o = {}) {
2869
+ const report = new Reporter(source);
2870
+ const c = new Compiler(source, report, o.functions ?? {});
2871
+ let doc = { body: [] };
2872
+ let definition;
2873
+ let expectations = [];
2874
+ try {
2875
+ doc = parse$1(source, report).doc;
2876
+ definition = c.run(doc);
2877
+ if (definition) expectations = check$1(definition.model).map((result, i) => {
2878
+ const at = c.expectSpans[i];
2879
+ return {
2880
+ at,
2881
+ ...report.position(at.from),
2882
+ result
2883
+ };
2884
+ });
2885
+ } catch (err) {
2886
+ if (err instanceof FormulaError) report.error(err.span, err.message);
2887
+ else if (!(err instanceof Bail)) report.error({
2888
+ from: 0,
2889
+ to: 0
2890
+ }, `internal error: ${err?.message ?? err}`);
2891
+ }
2892
+ const diagnostics = [...report.list].sort((a, b) => a.from - b.from);
2893
+ return {
2894
+ ok: !report.failed && !!definition,
2895
+ diagnostics,
2896
+ definition,
2897
+ expectations,
2898
+ symbols: c.symbols,
2899
+ doc
2900
+ };
2901
+ }
2902
+ /**
2903
+ * The diagnostics and every failing expectation, in source order: an editor marks a failure as a warning (the
2904
+ * diagram still shows), `loadmap check` as an error.
2905
+ */
2906
+ function problems(r, failed = "warning") {
2907
+ const out = [...r.diagnostics];
2908
+ for (const e of r.expectations) if (!e.result.ok) {
2909
+ const name = e.result.expectation.label ? `"${e.result.expectation.label}" fails: ` : "";
2910
+ out.push({
2911
+ severity: failed,
2912
+ message: `${name}${e.result.message}`,
2913
+ ...e.at,
2914
+ line: e.line,
2915
+ column: e.column
2916
+ });
2917
+ }
2918
+ return out.sort((a, b) => a.from - b.from);
2919
+ }
2920
+ /** Stops compiling after an error that leaves nothing sensible to check. */
2921
+ var Bail = class {};
2922
+ var Compiler = class {
2923
+ source;
2924
+ report;
2925
+ functions;
2926
+ symbols = [];
2927
+ /** Where each expectation of the model is written, in order. */
2928
+ expectSpans = [];
2929
+ names;
2930
+ fits = /* @__PURE__ */ new Map();
2931
+ lets = /* @__PURE__ */ new Map();
2932
+ constructor(source, report, functions) {
2933
+ this.source = source;
2934
+ this.report = report;
2935
+ this.functions = functions;
2936
+ }
2937
+ run(doc) {
2938
+ const root = this.shape("", SCHEMA[""], doc.body, {
2939
+ from: 0,
2940
+ to: 0
2941
+ });
2942
+ this.header(doc.body);
2943
+ const byKind = (k) => root.children.filter((b) => b.keyword === k);
2944
+ const loads = byKind("load");
2945
+ if (!loads.length) this.report.error({
2946
+ from: 0,
2947
+ to: 0
2948
+ }, "a definition needs a load line", "load { max: 100 }");
2949
+ for (const extra of loads.slice(1)) this.report.error(extra.at, "only one load line is allowed");
2950
+ const params = byKind("param");
2951
+ const lets = byKind("let");
2952
+ const fits = byKind("fit");
2953
+ const flows = byKind("flow");
2954
+ const boxes = root.children.filter((b) => BOXES.includes(b.keyword) && b.args.id).map((b) => this.box(b));
2955
+ const loose = byKind("component");
2956
+ const formulaNames = /* @__PURE__ */ new Map();
2957
+ const own = (kind, b, taken) => {
2958
+ const id = b.args.id;
2959
+ if (!id) return;
2960
+ this.declare(kind, id.name, id, b, taken);
2961
+ };
2962
+ for (const b of [
2963
+ ...params,
2964
+ ...lets,
2965
+ ...fits
2966
+ ]) {
2967
+ own(b.keyword === "param" ? "param" : b.keyword === "let" ? "let" : "fit", b, formulaNames);
2968
+ if (b.args.id && RESERVED.has(b.args.id.name)) this.report.error(b.args.id, `"${b.args.id.name}" is a reserved name; pick another`);
2969
+ }
2970
+ const flowNames = /* @__PURE__ */ new Map();
2971
+ for (const b of flows) own("flow", b, flowNames);
2972
+ const nodeNames = /* @__PURE__ */ new Map();
2973
+ const stageNames = /* @__PURE__ */ new Map();
2974
+ const componentNames = /* @__PURE__ */ new Map();
2975
+ for (const box of boxes) {
2976
+ this.declare("node", box.id, box.block.args.id, box.block, nodeNames, box.block.keyword);
2977
+ for (const s of box.stages) this.declare("stage", s.id, s.block.args.id ?? s.block.at, s.block, stageNames, box.id);
2978
+ if (box.component) {
2979
+ const c = box.component;
2980
+ this.declare("component", c.id, c.own ? box.block.args.id : c.block.args.id ?? c.block.at, c.block, componentNames);
2981
+ }
2982
+ }
2983
+ for (const b of loose) if (!b.args.id) this.report.error(b.at, "a component outside a box needs an id: component cache \"Cache\"");
2984
+ else own("component", b, componentNames);
2985
+ const paramTypes = /* @__PURE__ */ new Map([["load", "number"]]);
2986
+ for (const b of params) if (b.args.id) paramTypes.set(b.args.id.name, this.paramType(b));
2987
+ for (const f of fits) this.fit(f);
2988
+ this.names = {
2989
+ params: paramTypes,
2990
+ lets: this.lets,
2991
+ flows: new Set(flowNames.keys()),
2992
+ components: new Set(componentNames.keys()),
2993
+ fits: this.fits,
2994
+ functions: this.functions
2995
+ };
2996
+ for (const b of lets) if (b.args.id) this.lets.set(b.args.id.name, "any");
2997
+ for (let changed = true, pass = 0; changed && pass <= lets.length; pass++) {
2998
+ changed = false;
2999
+ for (const b of lets) {
3000
+ if (!b.args.id || !b.decl?.init) continue;
3001
+ const type = compileFormula(b.decl.init, this.names, "ctx", new Reporter(this.source)).type;
3002
+ if (type !== this.lets.get(b.args.id.name)) changed = true;
3003
+ this.lets.set(b.args.id.name, type);
3004
+ }
3005
+ }
3006
+ const stageOf = /* @__PURE__ */ new Map();
3007
+ for (const box of boxes) for (const s of box.stages) stageOf.set(s.id, {
3008
+ block: s.block,
3009
+ box
3010
+ });
3011
+ const boxById = new Map(boxes.map((b) => [b.id, b]));
3012
+ const load = loads[0] && this.load(loads[0]);
3013
+ const paramDefs = {};
3014
+ for (const b of params) if (b.args.id) paramDefs[b.args.id.name] = this.param(b);
3015
+ const presets = {};
3016
+ for (const b of byKind("preset")) {
3017
+ const label = this.text(b.args.label);
3018
+ if (label != null) presets[label] = this.preset(b, params);
3019
+ }
3020
+ const scenarios = {};
3021
+ for (const b of byKind("scenario")) {
3022
+ const label = this.text(b.args.label);
3023
+ if (label == null) continue;
3024
+ if (Object.hasOwn(scenarios, label)) this.report.error(b.args.label, `scenario "${label}" is declared twice`);
3025
+ else scenarios[label] = this.preset(b, params, "scenario");
3026
+ this.symbols.push({
3027
+ kind: "scenario",
3028
+ id: label,
3029
+ span: b.args.label,
3030
+ decl: b.decl ?? b.at
3031
+ });
3032
+ }
3033
+ const expects = [];
3034
+ for (const b of byKind("expect")) {
3035
+ const e = this.expectation(b, scenarios, componentNames);
3036
+ if (!e) continue;
3037
+ expects.push(e);
3038
+ this.expectSpans.push({
3039
+ from: b.at.from,
3040
+ to: b.args.label?.to ?? b.at.to
3041
+ });
3042
+ }
3043
+ const story = [];
3044
+ for (const b of byKind("step")) {
3045
+ const st = this.step(b, scenarios, componentNames, params, load);
3046
+ if (st) story.push(st);
3047
+ }
3048
+ const growths = byKind("growth");
3049
+ for (const extra of growths.slice(1)) this.report.error(extra.at, "only one growth line is allowed");
3050
+ const growth = growths[0] && this.growth(growths[0]);
3051
+ const derived = {};
3052
+ for (const b of lets) if (b.args.id && b.decl?.init) derived[b.args.id.name] = placed(b.decl.init, this.formula(b.decl.init, "ctx").run);
3053
+ const resolveStage = (n, what) => {
3054
+ if (stageOf.has(n.name)) return n.name;
3055
+ const box = boxById.get(n.name);
3056
+ if (box?.stages.length === 1) return box.stages[0].id;
3057
+ if (box) this.report.error(n, box.stages.length ? `"${n.name}" holds ${box.stages.length} stages; name one: ${box.stages.map((s) => s.id).join(", ")}` : `the box "${n.name}" runs no stage`);
3058
+ else this.report.error(n, `unknown ${what} "${n.name}"`, didYouMean(n.name, [...stageOf.keys(), ...boxById.keys()]));
3059
+ };
3060
+ const flowDefs = {};
3061
+ const flowViews = {};
3062
+ const routes = {};
3063
+ const reached = /* @__PURE__ */ new Set();
3064
+ for (const b of flows) {
3065
+ const id = b.args.id;
3066
+ if (!id) continue;
3067
+ const label = this.text(b.args.label);
3068
+ const def = {};
3069
+ if (label != null) def.label = label;
3070
+ const unit = this.constProp(b, "unit");
3071
+ if (unit != null) def.unit = unit;
3072
+ def.rate = this.dynProp(b, "rate", "number");
3073
+ const priority = this.constProp(b, "priority");
3074
+ if (priority != null) def.priority = priority;
3075
+ const overload = this.overload(b);
3076
+ if (overload) def.overload = overload;
3077
+ flowDefs[id.name] = def;
3078
+ const route = this.prop(b, "route");
3079
+ const stages = [];
3080
+ for (const [i, n] of (route?.chain ?? (route?.value.t === "id" ? [route.value] : [])).entries()) {
3081
+ const s = resolveStage(n, "stage");
3082
+ if (!s) continue;
3083
+ if (stageOf.get(s).block.keyword === "fanout" && i < (route?.chain?.length ?? 1) - 1) this.report.error(n, `the fan-out "${s}" must be the last stage of the route`);
3084
+ stages.push(s);
3085
+ reached.add(s);
3086
+ }
3087
+ if (route && route.value.t !== "id") this.report.error(route.value, "a route is a list of stages: a -> b -> c");
3088
+ routes[id.name] = stages;
3089
+ const from = this.prop(b, "from");
3090
+ const view = {
3091
+ from: "",
3092
+ color: "accent"
3093
+ };
3094
+ if (from) {
3095
+ if (from.value.t === "id" && boxById.has(from.value.name)) view.from = from.value.name;
3096
+ else this.report.error(from.value, `unknown node "${this.slice(from.value)}"`, didYouMean(this.slice(from.value), boxById.keys()));
3097
+ }
3098
+ const color = this.prop(b, "color");
3099
+ if (color) view.color = this.color(color.value);
3100
+ if (label != null) view.label = label;
3101
+ flowViews[id.name] = view;
3102
+ }
3103
+ const componentDefs = {};
3104
+ for (const box of boxes) if (box.component) componentDefs[box.component.id] = this.component(box.component.block, box.block);
3105
+ for (const b of loose) if (b.args.id) componentDefs[b.args.id.name] = this.component(b);
3106
+ const stageDefs = {};
3107
+ for (const box of boxes) for (const s of box.stages) {
3108
+ const def = this.stage(s.block, box, componentNames, resolveStage, flowNames, reached);
3109
+ if (def) stageDefs[s.id] = def;
3110
+ }
3111
+ for (const [id, s] of stageOf) if (!reached.has(id) && s.block.keyword !== "fanout") this.report.warning(s.block.args.id ?? s.block.at, `no flow passes the stage "${id}"`);
3112
+ const calibration = {};
3113
+ for (const f of fits) {
3114
+ const fit = f.args.id && this.fits.get(f.args.id.name);
3115
+ if (fit) calibration[this.text(f.args.label) ?? f.args.id.name] = fit;
3116
+ }
3117
+ const model = {
3118
+ load,
3119
+ params: paramDefs,
3120
+ flows: flowDefs,
3121
+ components: componentDefs,
3122
+ stages: stageDefs,
3123
+ routes
3124
+ };
3125
+ const title = this.constProp(root, "title");
3126
+ if (title != null) model.title = title;
3127
+ if (Object.keys(presets).length) model.presets = presets;
3128
+ if (Object.keys(scenarios).length) model.scenarios = scenarios;
3129
+ if (growth) model.growth = growth;
3130
+ if (expects.length) model.expect = expects;
3131
+ if (story.length) model.story = story;
3132
+ if (Object.keys(derived).length) model.derived = derived;
3133
+ if (Object.keys(calibration).length) model.calibration = calibration;
3134
+ const th = byKind("thresholds")[0];
3135
+ if (th) {
3136
+ const t = {};
3137
+ for (const k of ["target", "full"]) {
3138
+ const v = this.constProp(th, k);
3139
+ if (v != null) t[k] = v;
3140
+ }
3141
+ model.thresholds = t;
3142
+ }
3143
+ const searchMax = this.constProp(root, "searchMax");
3144
+ if (searchMax != null) model.searchMax = searchMax;
3145
+ const nodes = {};
3146
+ for (const box of boxes) nodes[box.id] = this.node(box);
3147
+ const edges = [];
3148
+ for (const e of root.edges) edges.push(...this.edges(e, boxById));
3149
+ if (!flows.length) this.report.error({
3150
+ from: 0,
3151
+ to: 0
3152
+ }, "a definition needs at least one flow");
3153
+ const options = { assumptions: [] };
3154
+ const speed = this.constProp(root, "speed");
3155
+ if (speed != null) options.speed = speed;
3156
+ const layout = this.prop(root, "layout");
3157
+ if (layout) options.direction = this.enumValue(layout, SCHEMA[""].props.layout) === "down" ? "DOWN" : "RIGHT";
3158
+ for (const a of root.props.get("assume") ?? []) {
3159
+ const v = this.constant(a.value, "string");
3160
+ if (typeof v === "string") options.assumptions.push(v);
3161
+ }
3162
+ if (this.report.failed) return void 0;
3163
+ return this.build(model, {
3164
+ nodes,
3165
+ edges,
3166
+ flows: flowViews
3167
+ }, options, byKind("preset"), byKind("scenario"));
3168
+ }
3169
+ /** Hands the parts to core and layout, then runs the model once per preset and scenario so formula errors show now. */
3170
+ build(modelDef, viewDef, options, presets, scenarios) {
3171
+ const top = {
3172
+ from: 0,
3173
+ to: 0
3174
+ };
3175
+ let model;
3176
+ let view;
3177
+ try {
3178
+ model = defineModel(modelDef);
3179
+ view = defineView(model, viewDef);
3180
+ } catch (err) {
3181
+ this.report.error(top, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
3182
+ return;
3183
+ }
3184
+ const runs = [{
3185
+ values: {},
3186
+ at: top
3187
+ }];
3188
+ for (const b of presets) {
3189
+ const label = this.text(b.args.label);
3190
+ const v = label != null && modelDef.presets?.[label];
3191
+ if (v) runs.push({
3192
+ values: v,
3193
+ at: b.at
3194
+ });
3195
+ }
3196
+ for (const b of scenarios) {
3197
+ const label = this.text(b.args.label);
3198
+ const v = label != null && model.scenarios.find((s) => s.label === label)?.values;
3199
+ if (v) runs.push({
3200
+ values: v,
3201
+ at: b.at
3202
+ });
3203
+ }
3204
+ for (const { values, at } of runs) try {
3205
+ const ev = model.evaluate(values);
3206
+ labelsAt(view, ev);
3207
+ for (const k of Object.keys(modelDef.derived ?? {})) ev.ctx.derived[k];
3208
+ for (const n of Object.values(viewDef.nodes)) {
3209
+ if (n.kind === "cluster") {
3210
+ const members = typeof n.members === "function" ? n.members(ev.ctx) : n.members;
3211
+ n.memberLabel?.(Math.max(0, members - 1));
3212
+ }
3213
+ if (n.kind === "pool") n.podLabel?.(0);
3214
+ }
3215
+ for (const e of viewDef.edges) e.when?.(ev.ctx);
3216
+ for (const f of model.flowIds) if (!Number.isFinite(ev.flows[f])) this.report.warning(this.flowRate.get(f) ?? at, `the rate of "${f}" is ${ev.flows[f]} here`);
3217
+ } catch (err) {
3218
+ if (err instanceof FormulaError) this.report.error(err.span, err.message);
3219
+ else this.report.error(at, err instanceof Error ? err.message.replace(/^loadmap: /, "") : String(err));
3220
+ }
3221
+ if (this.report.failed) return void 0;
3222
+ return {
3223
+ model,
3224
+ view,
3225
+ options
3226
+ };
3227
+ }
3228
+ flowRate = /* @__PURE__ */ new Map();
3229
+ shape(keyword, spec, stmts, at, decl) {
3230
+ const b = {
3231
+ keyword,
3232
+ spec,
3233
+ at,
3234
+ decl,
3235
+ args: {},
3236
+ props: /* @__PURE__ */ new Map(),
3237
+ children: [],
3238
+ edges: [],
3239
+ rows: []
3240
+ };
3241
+ if (decl) this.args(b, decl);
3242
+ const here = keyword ? `in ${keyword}` : "at the top";
3243
+ for (const s of stmts) if (s.kind === "prop") {
3244
+ const k = s.key.name;
3245
+ if (!spec.open && !own(spec.props, k)) {
3246
+ const hint = spec.children.includes(k) ? `write it as a block: ${k} { … }` : didYouMean(k, [...Object.keys(spec.props), ...spec.children]);
3247
+ this.report.error(s.key, `unknown property "${k}" ${here}`, hint);
3248
+ continue;
3249
+ }
3250
+ const list = b.props.get(k) ?? [];
3251
+ if (list.length && !(own(spec.props, k)?.repeat ?? false)) this.report.error(s.key, `"${k}" is set twice ${here}`, "remove one of them");
3252
+ list.push(s);
3253
+ b.props.set(k, list);
3254
+ } else if (s.kind === "decl") {
3255
+ const k = s.keyword.name;
3256
+ const childSpec = own(SCHEMA, k);
3257
+ if (!spec.children.includes(k) || !childSpec || k === "") {
3258
+ if (own(spec.props, k)) this.report.error(s.keyword, `"${k}" is a property: write "${k}: …"`);
3259
+ else if (childSpec) this.report.error(s.keyword, `"${k}" cannot go ${here}`, where(k));
3260
+ else this.report.error(s.keyword, `unknown statement "${k}" ${here}`, didYouMean(k, [...spec.children, ...Object.keys(spec.props)]));
3261
+ continue;
3262
+ }
3263
+ b.children.push(this.shape(k, childSpec, s.body ?? [], s.keyword, s));
3264
+ } else if (s.kind === "edge") {
3265
+ if (!spec.edges) this.report.error(s.chain[0], `an "a -> b" line cannot go ${here}`);
3266
+ else b.edges.push(s);
3267
+ } else if (!spec.rows) this.report.error(s, `a row of numbers can only go in "points"`);
3268
+ else b.rows.push(s);
3269
+ if (decl) {
3270
+ for (const [k, p] of Object.entries(spec.props)) if (p.required && !b.props.has(k)) this.report.error(at, `"${keyword}" needs "${k}: …"`, p.doc);
3271
+ }
3272
+ return b;
3273
+ }
3274
+ args(b, d) {
3275
+ const slots = [...b.spec.args];
3276
+ for (const a of d.args) {
3277
+ const kind = a.t === "id" ? "id" : a.t === "str" || a.t === "tpl" ? "text" : a.t === "num" ? "number" : null;
3278
+ const i = slots.findIndex((s) => s.kind === kind);
3279
+ if (kind == null || i < 0 || slots.slice(0, i).some((s) => !s.optional)) {
3280
+ this.report.error(a, `unexpected ${this.slice(a)} after "${b.keyword}"`, usage(b.keyword));
3281
+ continue;
3282
+ }
3283
+ const slot = slots[i];
3284
+ slots.splice(0, i + 1);
3285
+ if (a.t === "tpl" && !slot.dynamic && !a.parts.length) {
3286
+ b.args[slot.name] = {
3287
+ t: "str",
3288
+ value: a.quasis[0],
3289
+ from: a.from,
3290
+ to: a.to
3291
+ };
3292
+ continue;
3293
+ }
3294
+ if (a.t === "tpl" && !slot.dynamic) this.report.error(a, `the ${slot.name} of "${b.keyword}" must be a plain text`);
3295
+ if (a.t === "id") b.args[slot.name] = {
3296
+ name: a.name,
3297
+ from: a.from,
3298
+ to: a.to
3299
+ };
3300
+ else b.args[slot.name] = a;
3301
+ }
3302
+ const missing = slots.find((s) => !s.optional);
3303
+ if (missing) this.report.error(b.at, `"${b.keyword}" needs its ${missing.name}`, usage(b.keyword));
3304
+ if (b.spec.init && !d.init) this.report.error(b.at, `"${b.keyword}" needs "= formula"`, usage(b.keyword));
3305
+ if (!b.spec.init && d.init) this.report.error(d.init, `"${b.keyword}" takes no "="`);
3306
+ }
3307
+ header(body) {
3308
+ const first = body[0];
3309
+ const isHeader = (s) => s?.kind === "decl" && s.keyword.name === "loadmap";
3310
+ if (!isHeader(first)) {
3311
+ this.report.error(first ?? {
3312
+ from: 0,
3313
+ to: 0
3314
+ }, `a definition starts with "loadmap 1"`, `add "loadmap 1" as the first line`);
3315
+ throw new Bail();
3316
+ }
3317
+ for (const s of body.slice(1)) if (isHeader(s)) this.report.error(s.keyword, "the \"loadmap\" line must be the first and only one");
3318
+ const v = first.args[0];
3319
+ if (v?.t === "num" && v.value !== 1) {
3320
+ this.report.error(v, v.value > 1 ? `this definition is written in language version ${v.value}; this loadmap reads version 1` : `there is no language version ${v.value}; the first is 1`, v.value > 1 ? "update @loadmap/dsl" : void 0);
3321
+ throw new Bail();
3322
+ }
3323
+ }
3324
+ declare(kind, id, span, b, taken, detail) {
3325
+ if (FORBIDDEN.has(id)) this.report.error(span, `"${id}" cannot be used as a name`);
3326
+ if (taken.has(id)) this.report.error(span, `"${id}" is declared twice`, "rename one of them");
3327
+ else taken.set(id, span);
3328
+ this.symbols.push({
3329
+ kind,
3330
+ id,
3331
+ span,
3332
+ decl: b.decl ?? b.at,
3333
+ detail
3334
+ });
3335
+ }
3336
+ box(b) {
3337
+ const id = b.args.id.name;
3338
+ const stageBlocks = b.children.filter((c) => STAGES.includes(c.keyword));
3339
+ const stages = stageBlocks.map((s) => ({
3340
+ block: s,
3341
+ id: s.args.id?.name ?? id
3342
+ }));
3343
+ const ownLines = Object.keys(COMPONENT_PROPS).some((k) => b.props.has(k));
3344
+ const nested = b.children.filter((c) => c.keyword === "component");
3345
+ let component;
3346
+ if (ownLines) component = {
3347
+ block: b,
3348
+ id,
3349
+ own: true
3350
+ };
3351
+ for (const n of nested) if (component) this.report.error(n.at, `"${id}" already has a component`, "one component per box; put others at the top");
3352
+ else component = {
3353
+ block: n,
3354
+ id: n.args.id?.name ?? id,
3355
+ own: false
3356
+ };
3357
+ if (b.keyword === "node" && stages.length > 1) this.report.error(stageBlocks[1].at, "a node runs at most one stage; a cluster can hold several queues");
3358
+ if (b.keyword === "cluster" && !stages.length) this.report.error(b.at, "a cluster needs a queue", "queue topic { partitions { orders: 12 } }");
3359
+ if (b.keyword === "pool" && stages.length !== 1) this.report.error(b.at, "a pool needs exactly one workers stage", "workers { pods: 3; perPod: 8 }");
3360
+ const ids = /* @__PURE__ */ new Set();
3361
+ for (const s of stages) {
3362
+ if (ids.has(s.id)) this.report.error(s.block.at, `"${id}" has two stages called "${s.id}"`, "give each an id");
3363
+ ids.add(s.id);
3364
+ }
3365
+ return {
3366
+ block: b,
3367
+ id,
3368
+ stages,
3369
+ component
3370
+ };
3371
+ }
3372
+ slice(s) {
3373
+ return this.source.slice(s.from, s.to);
3374
+ }
3375
+ prop(b, key) {
3376
+ return b.props.get(key)?.[0];
3377
+ }
3378
+ formula(e, scope, want) {
3379
+ const f = compileFormula(e, this.names, scope, this.report);
3380
+ if (want && want !== "any" && f.type !== want && f.type !== "any") {
3381
+ if (want === "string" && f.type === "number") {
3382
+ const run = f.run;
3383
+ return {
3384
+ ...f,
3385
+ type: "string",
3386
+ run: (c, l) => String(run(c, l))
3387
+ };
3388
+ }
3389
+ if (want === "capacity" && f.type === "number") this.report.error(e, "a capacity says where it came from", `write measured(${this.slice(e)}, "unit", "source") or estimated(…)`);
3390
+ else this.report.error(e, `expected ${typeWord(want)}, this is ${typeWord(f.type)}`);
3391
+ }
3392
+ return f;
3393
+ }
3394
+ /** A value that must not depend on the model. */
3395
+ constant(e, want) {
3396
+ const f = this.formula(e, "const", want);
3397
+ try {
3398
+ return f.run(void 0);
3399
+ } catch (err) {
3400
+ this.report.error(e, err instanceof Error ? err.message : String(err));
3401
+ return;
3402
+ }
3403
+ }
3404
+ constProp(b, key) {
3405
+ const p = this.prop(b, key);
3406
+ if (!p) return void 0;
3407
+ const spec = b.spec.props[key];
3408
+ if (spec.value === "enum") return this.enumValue(p, spec);
3409
+ return this.constant(p.value, spec.value);
3410
+ }
3411
+ /**
3412
+ * A plain or computed value: a constant is computed now, so the model sees it as it would from TypeScript. `check`
3413
+ * throws on a value the drawing cannot take (a count too large); at run time its error carries the formula's place.
3414
+ */
3415
+ dyn(e, want, scope = "ctx", check) {
3416
+ const f = this.formula(e, scope, want);
3417
+ if (f.constant) {
3418
+ const v = this.constant(e, want);
3419
+ try {
3420
+ if (v !== void 0) check?.(v);
3421
+ } catch (err) {
3422
+ this.report.error(e, err.message);
3423
+ }
3424
+ return v;
3425
+ }
3426
+ const run = f.run;
3427
+ if (!check) return placed(e, run);
3428
+ return placed(e, (c, l) => {
3429
+ const v = run(c, l);
3430
+ check(v);
3431
+ return v;
3432
+ });
3433
+ }
3434
+ dynProp(b, key, want, check) {
3435
+ const p = this.prop(b, key);
3436
+ if (!p) return void 0;
3437
+ if (key === "rate" && b.args.id) this.flowRate.set(b.args.id.name, p.value);
3438
+ return this.dyn(p.value, want, own(b.spec.props, key)?.scope ?? "ctx", check);
3439
+ }
3440
+ /** A label or name argument: a plain text, or a template when the argument may be computed. */
3441
+ text(e) {
3442
+ if (!e) return void 0;
3443
+ const v = this.constant(e, "string");
3444
+ return typeof v === "string" ? v : void 0;
3445
+ }
3446
+ label(e) {
3447
+ if (!e) return void 0;
3448
+ return this.dyn(e, "string");
3449
+ }
3450
+ enumValue(p, spec) {
3451
+ const raw = p.value.t === "str" ? p.value.value : this.slice(p.value).replace(/\s+/g, "");
3452
+ if (spec.values?.includes(raw)) return raw;
3453
+ this.report.error(p.value, `"${raw}" is not one of ${spec.values.join(", ")}`, didYouMean(raw, spec.values ?? []));
3454
+ }
3455
+ color(e) {
3456
+ if (e.t === "id") {
3457
+ if (COLORS.includes(e.name)) return e.name;
3458
+ this.report.error(e, `"${e.name}" is not a theme colour`, didYouMean(e.name, COLORS) ?? "or quote a CSS colour");
3459
+ } else if (e.t === "str") {
3460
+ if (CSS_COLOR.test(e.value.trim())) return e.value.trim();
3461
+ this.report.error(e, `"${e.value}" is not a colour`, "use #rrggbb, rgb(…), hsl(…) or a theme colour");
3462
+ } else this.report.error(e, "a colour is a theme colour or a CSS colour in quotes");
3463
+ return "accent";
3464
+ }
3465
+ count(what) {
3466
+ const max = LIMITS[what];
3467
+ return (v) => {
3468
+ if (typeof v !== "number" || !Number.isInteger(v) || v < 0) throw new Error(`${what} must be a whole number of at least 0, not ${String(v)}`);
3469
+ if (v > max) throw new Error(`${what} is ${v}; at most ${max}`);
3470
+ };
3471
+ }
3472
+ load(b) {
3473
+ const o = {};
3474
+ const label = this.text(b.args.label);
3475
+ if (label != null) o.label = label;
3476
+ for (const k of [
3477
+ "min",
3478
+ "max",
3479
+ "step",
3480
+ "default"
3481
+ ]) {
3482
+ const v = this.constProp(b, k);
3483
+ if (v != null) o[k] = v;
3484
+ }
3485
+ if (typeof o.max !== "number") o.max = 1;
3486
+ return p.load(o);
3487
+ }
3488
+ paramType(b) {
3489
+ const options = this.prop(b, "options");
3490
+ if (options?.value.t === "array") return options.value.items[0]?.t === "str" ? "string" : "number";
3491
+ if (b.props.has("min") || b.props.has("max")) return "number";
3492
+ const d = this.prop(b, "default")?.value;
3493
+ return d?.t === "bool" ? "boolean" : d?.t === "str" ? "string" : d?.t === "num" ? "number" : "any";
3494
+ }
3495
+ param(b) {
3496
+ const o = { label: this.text(b.args.label) ?? b.args.id.name };
3497
+ for (const k of ["hint", "tag"]) {
3498
+ const v = this.constProp(b, k);
3499
+ if (v != null) o[k] = v;
3500
+ }
3501
+ const enabled = this.prop(b, "enabled");
3502
+ if (enabled) o.enabled = placed(enabled.value, this.formula(enabled.value, "values").run);
3503
+ const format = this.prop(b, "format");
3504
+ if (format) {
3505
+ const run = placed(format.value, this.formula(format.value, "value", "string").run);
3506
+ o.format = (v) => run(void 0, v);
3507
+ }
3508
+ const dflt = this.prop(b, "default");
3509
+ if (!dflt) return void 0;
3510
+ const value = this.constant(dflt.value, "any");
3511
+ const options = this.prop(b, "options");
3512
+ if (options) {
3513
+ for (const k of [
3514
+ "min",
3515
+ "max",
3516
+ "step"
3517
+ ]) if (b.props.has(k)) this.report.error(this.prop(b, k).key, `"${k}" is for a slider; this param has options`);
3518
+ const list = this.options(options);
3519
+ if (!list) return void 0;
3520
+ if (!list.includes(value)) {
3521
+ this.report.error(dflt.value, `the default ${this.slice(dflt.value)} is not one of the options`, list.join(", "));
3522
+ return;
3523
+ }
3524
+ return p.select({
3525
+ ...o,
3526
+ options: list,
3527
+ default: value
3528
+ });
3529
+ }
3530
+ if (b.props.has("min") || b.props.has("max")) {
3531
+ for (const k of ["min", "max"]) if (!b.props.has(k)) this.report.error(b.at, `a slider needs "${k}: …"`);
3532
+ const r = { ...o };
3533
+ for (const k of [
3534
+ "min",
3535
+ "max",
3536
+ "step"
3537
+ ]) {
3538
+ const v = this.constProp(b, k);
3539
+ if (v != null) r[k] = v;
3540
+ }
3541
+ if (typeof value !== "number") this.report.error(dflt.value, "the default of a slider is a number");
3542
+ return p.range({
3543
+ ...r,
3544
+ default: value
3545
+ });
3546
+ }
3547
+ if (typeof value === "boolean") return p.switch({
3548
+ ...o,
3549
+ default: value
3550
+ });
3551
+ this.report.error(b.at, `"${b.args.id.name}" is neither a select, a slider nor a switch`, "give it options: [...], min and max, or a default of true or false");
3552
+ }
3553
+ options(prop) {
3554
+ if (prop.value.t !== "array" || !prop.value.items.length) {
3555
+ this.report.error(prop.value, "options are a list: [2, 3, 4] or [\"a\", \"b\"]");
3556
+ return;
3557
+ }
3558
+ const list = prop.value.items.map((e) => this.constant(e, "any"));
3559
+ const kind = typeof list[0];
3560
+ if (!list.every((v) => (typeof v === "number" || typeof v === "string") && typeof v === kind)) {
3561
+ this.report.error(prop.value, "options are all numbers or all texts");
3562
+ return;
3563
+ }
3564
+ return list;
3565
+ }
3566
+ /** A preset's values, or a scenario's: the same, but a scenario leaves the load to the slider. */
3567
+ preset(b, params, kind = "preset") {
3568
+ const out = {};
3569
+ const known = new Map(params.filter((x) => x.args.id).map((x) => [x.args.id.name, x]));
3570
+ for (const [k, props] of b.props) {
3571
+ if (own(b.spec.props, k)) continue;
3572
+ const prop = props[0];
3573
+ const v = this.constant(prop.value, "any");
3574
+ if (k === "load" && kind === "scenario") {
3575
+ this.report.error(prop.key, "a scenario leaves the load to the slider", "use a preset for a load");
3576
+ continue;
3577
+ }
3578
+ if (k !== "load" && !known.has(k)) {
3579
+ const names = [...kind === "preset" ? ["load"] : [], ...known.keys()];
3580
+ this.report.error(prop.key, `unknown parameter "${k}"`, didYouMean(k, names));
3581
+ continue;
3582
+ }
3583
+ const type = this.names.params.get(k);
3584
+ if (type && type !== "any" && typeof v !== type) {
3585
+ this.report.error(prop.value, `"${k}" takes ${typeWord(type)}`);
3586
+ continue;
3587
+ }
3588
+ const opts = known.get(k) && this.prop(known.get(k), "options");
3589
+ if (opts?.value.t === "array") {
3590
+ const list = opts.value.items.map((e) => e.t === "num" || e.t === "str" ? e.value : void 0);
3591
+ if (!list.includes(v)) this.report.error(prop.value, `${String(v)} is not an option of "${k}"`, list.join(", "));
3592
+ }
3593
+ out[k] = v;
3594
+ }
3595
+ return out;
3596
+ }
3597
+ /** An `expect` line as the model takes it: names checked here, so a mistake points at its word. */
3598
+ expectation(b, scenarios, components) {
3599
+ const e = {};
3600
+ const label = this.text(b.args.label);
3601
+ if (label != null) e.label = label;
3602
+ const scenario = this.named(b, "scenario", Object.keys(scenarios), "scenario");
3603
+ const component = this.named(b, "component", [...components.keys()], "component");
3604
+ if (scenario === null || component === null) return void 0;
3605
+ if (scenario != null) e.scenario = scenario;
3606
+ if (component != null) e.component = component;
3607
+ for (const k of ["target", "full"]) {
3608
+ const v = this.constProp(b, k);
3609
+ if (v == null) continue;
3610
+ if (!(v > 0 && Number.isFinite(v))) this.report.error(this.prop(b, k).value, `${k} is a load multiple above 0`);
3611
+ else e[k] = v;
3612
+ }
3613
+ if (!b.props.has("target") && !b.props.has("full")) {
3614
+ this.report.error(b.at, "an expect needs a target or a full multiple", "expect { target: 8 }");
3615
+ return;
3616
+ }
3617
+ return e.target != null || e.full != null ? e : void 0;
3618
+ }
3619
+ /** A property naming a scenario or a component: undefined when absent, null when wrong (reported). */
3620
+ named(b, key, known, what) {
3621
+ const p = this.prop(b, key);
3622
+ if (!p) return void 0;
3623
+ const v = p.value.t === "id" ? p.value.name : p.value.t === "str" ? p.value.value : void 0;
3624
+ if (v == null) this.report.error(p.value, `${key} is the name of a ${what}`);
3625
+ else if (!known.includes(v)) this.report.error(p.value, `unknown ${what} "${v}"`, didYouMean(v, known));
3626
+ else return v;
3627
+ return null;
3628
+ }
3629
+ /** A story step as the model takes it. */
3630
+ step(b, scenarios, components, params, load) {
3631
+ const text = this.text(b.args.label);
3632
+ for (const k of Object.keys(b.spec.props)) {
3633
+ const at = b.props.get(k)?.[0];
3634
+ if (at && params.some((x) => x.args.id?.name === k)) this.report.error(at.key, `"${k}" in a step is the step's own ${k}, not the parameter "${k}"`, "rename the parameter to set it in a step");
3635
+ }
3636
+ const scenario = this.named(b, "scenario", Object.keys(scenarios), "scenario");
3637
+ const focus = this.named(b, "focus", [...components.keys()], "component");
3638
+ const seconds = this.constProp(b, "seconds");
3639
+ if (seconds != null && !(seconds > 0 && Number.isFinite(seconds))) this.report.error(this.prop(b, "seconds").value, "seconds is a duration above 0");
3640
+ const set = this.preset(b, params);
3641
+ const l = this.prop(b, "load");
3642
+ if (l && load && typeof set.load === "number" && !(set.load >= load.min && set.load <= load.max)) this.report.error(l.value, `the load goes from ${load.min} to ${load.max}`);
3643
+ if (text == null || scenario === null || focus === null) return void 0;
3644
+ return {
3645
+ text,
3646
+ ...scenario != null ? { scenario } : {},
3647
+ ...focus != null ? { focus } : {},
3648
+ ...seconds != null && seconds > 0 ? { seconds } : {},
3649
+ ...Object.keys(set).length ? { set } : {}
3650
+ };
3651
+ }
3652
+ growth(b) {
3653
+ const rate = this.constProp(b, "rate");
3654
+ const per = this.constProp(b, "per");
3655
+ const asOf = this.constProp(b, "asOf");
3656
+ if (rate != null && !(rate > 0)) this.report.error(this.prop(b, "rate").value, "growth needs a rate above 0");
3657
+ if (asOf != null && !parseAsOf(asOf)) this.report.error(this.prop(b, "asOf").value, `"${asOf}" is not a date`, "a day like \"2026-11-01\"");
3658
+ if (rate == null || per == null || asOf == null) return void 0;
3659
+ return {
3660
+ rate,
3661
+ per,
3662
+ asOf
3663
+ };
3664
+ }
3665
+ fit(b) {
3666
+ const id = b.args.id;
3667
+ if (!id) return;
3668
+ this.fits.set(id.name, null);
3669
+ const pts = b.children.filter((c) => c.keyword === "points");
3670
+ if (pts.length !== 1) {
3671
+ this.report.error(b.at, pts.length ? "a fit has one points block" : "a fit needs its points", usage("points"));
3672
+ return;
3673
+ }
3674
+ const block = pts[0];
3675
+ const { x, y } = block.args;
3676
+ if (!x || !y) return;
3677
+ if (x.name === y.name) this.report.error(y, "x and y need different names");
3678
+ const points = [];
3679
+ for (const r of block.rows) {
3680
+ if (r.values.length !== 2) {
3681
+ this.report.error(r, `a row is two numbers: ${x.name} and ${y.name}`);
3682
+ continue;
3683
+ }
3684
+ const [a, c] = r.values.map((v) => this.constant(v, "number"));
3685
+ points.push({
3686
+ [x.name]: a,
3687
+ [y.name]: c
3688
+ });
3689
+ }
3690
+ const source = this.constProp(b, "source");
3691
+ const through = this.constProp(b, "through");
3692
+ try {
3693
+ const o = {
3694
+ x: x.name,
3695
+ y: y.name
3696
+ };
3697
+ if (source != null) o.source = source;
3698
+ if (through) o.through = through;
3699
+ this.fits.set(id.name, fitLinear(points, o));
3700
+ } catch (err) {
3701
+ this.report.error(block.at, err.message.replace(/^loadmap: /, ""));
3702
+ }
3703
+ }
3704
+ overload(b) {
3705
+ const rules = b.children.filter((c) => c.keyword === "drop" || c.keyword === "merge");
3706
+ if (!rules.length) return void 0;
3707
+ const seen = /* @__PURE__ */ new Set();
3708
+ const parts = [];
3709
+ for (const r of rules) {
3710
+ const key = r.keyword;
3711
+ if (seen.has(key)) this.report.error(r.at, `"${key}" is set twice in this flow`);
3712
+ seen.add(key);
3713
+ const fields = [];
3714
+ const first = key === "drop" ? "olderThan" : "share";
3715
+ for (const k of [first, "reason"]) {
3716
+ const v = this.dynProp(r, k, k === "reason" ? "string" : "number");
3717
+ if (v !== void 0) fields.push([k, v]);
3718
+ }
3719
+ const when = this.prop(r, "when");
3720
+ parts.push({
3721
+ key,
3722
+ fields,
3723
+ when: when && placed(when.value, this.formula(when.value, "ctx").run)
3724
+ });
3725
+ }
3726
+ const fixed = parts.every((x) => !x.when && x.fields.every(([, v]) => typeof v !== "function"));
3727
+ const at = (ctx) => {
3728
+ const o = {};
3729
+ for (const x of parts) {
3730
+ if (x.when && !x.when(ctx)) continue;
3731
+ const rule = {};
3732
+ for (const [k, v] of x.fields) rule[k] = typeof v === "function" ? v(ctx) : v;
3733
+ o[x.key] = rule;
3734
+ }
3735
+ return o;
3736
+ };
3737
+ return fixed ? at(void 0) : at;
3738
+ }
3739
+ component(b, box) {
3740
+ const o = {};
3741
+ const name = this.constProp(b, "name") ?? this.text(b.args.label) ?? this.text(box?.args.label);
3742
+ if (name == null) {
3743
+ const label = box?.args.label ?? b.args.label;
3744
+ if (label) this.report.error(label, "a computed label cannot name the component", "add name: \"…\"");
3745
+ }
3746
+ o.name = name ?? (b.args.id ?? box?.args.id)?.name;
3747
+ for (const k of ["short", "plural"]) {
3748
+ const v = this.constProp(b, k);
3749
+ if (v != null) o[k] = v;
3750
+ }
3751
+ for (const [k, want] of [
3752
+ ["demand", "number"],
3753
+ ["capacity", "capacity"],
3754
+ ["when", "any"],
3755
+ ["over", "string"],
3756
+ ["describe", "string"]
3757
+ ]) {
3758
+ const prop = this.prop(b, k);
3759
+ if (!prop) continue;
3760
+ const check = k === "capacity" ? (v) => {
3761
+ if (!isCapacity(v)) throw new Error("a capacity comes from measured(…) or estimated(…)");
3762
+ } : void 0;
3763
+ o[k] = this.dyn(prop.value, want, "ctx", check);
3764
+ }
3765
+ const at = box ? box.args.id ?? box.at : b.at;
3766
+ for (const k of [
3767
+ "demand",
3768
+ "capacity",
3769
+ "over"
3770
+ ]) if (!(k in o)) this.report.error(at, `the component needs "${k}: …"`, COMPONENT_PROPS[k].doc);
3771
+ return o;
3772
+ }
3773
+ stage(b, box, components, resolve, flows, reached) {
3774
+ const label = this.text(b.args.label);
3775
+ const base = label != null ? { label } : {};
3776
+ let component = box.component?.id;
3777
+ const named = this.prop(b, "component");
3778
+ if (named) {
3779
+ const n = named.value;
3780
+ if (n.t === "id" && components.has(n.name)) component = n.name;
3781
+ else this.report.error(n, `unknown component "${this.slice(n)}"`, didYouMean(this.slice(n), components.keys()));
3782
+ }
3783
+ const needs = () => {
3784
+ this.report.error(b.at, `a ${b.keyword} stage needs a component`, "give the box demand and capacity lines, or name one: component: db");
3785
+ };
3786
+ switch (b.keyword) {
3787
+ case "pass": return component ? pass(component, base) : needs();
3788
+ case "store": return component ? store(component, base) : needs();
3789
+ case "sink": return sink(base);
3790
+ case "workers": {
3791
+ if (!component) return needs();
3792
+ const pods = this.dynProp(b, "pods", "number", this.count("pods"));
3793
+ const perPod = this.dynProp(b, "perPod", "number", this.count("perPod"));
3794
+ const fill = this.constProp(b, "fill");
3795
+ return workers(component, {
3796
+ ...base,
3797
+ pods: pods ?? 0,
3798
+ perPod: perPod ?? 0,
3799
+ ...fill ? { fill } : {}
3800
+ });
3801
+ }
3802
+ case "queue": {
3803
+ const block = b.children.find((c) => c.keyword === "partitions");
3804
+ if (!block) {
3805
+ this.report.error(b.at, "a queue needs its partitions", "partitions { orders: 12 }");
3806
+ return;
3807
+ }
3808
+ const entries = [];
3809
+ for (const [f, props] of block.props) {
3810
+ const prop = props[0];
3811
+ if (!flows.has(f)) {
3812
+ this.report.error(prop.key, `unknown flow "${f}"`, didYouMean(f, flows.keys()));
3813
+ continue;
3814
+ }
3815
+ entries.push([f, this.dyn(prop.value, "number", "ctx", this.count("partitions"))]);
3816
+ }
3817
+ const fixed = entries.every(([, v]) => typeof v === "number");
3818
+ const at = (ctx) => Object.fromEntries(entries.map(([k, v]) => [k, typeof v === "function" ? v(ctx) : v]));
3819
+ const partitions = fixed ? at(void 0) : at;
3820
+ return queue({
3821
+ ...base,
3822
+ partitions,
3823
+ ...component ? { component } : {}
3824
+ });
3825
+ }
3826
+ case "fanout": {
3827
+ const branches = [];
3828
+ for (const e of b.edges) {
3829
+ if (e.chain.length !== 2) {
3830
+ this.report.error(e, "a branch is one \"flow -> stage: copies\" line");
3831
+ continue;
3832
+ }
3833
+ const [f, s] = e.chain;
3834
+ if (!flows.has(f.name)) this.report.error(f, `unknown flow "${f.name}"`, didYouMean(f.name, flows.keys()));
3835
+ const stage = resolve(s, "stage");
3836
+ if (stage) reached.add(stage);
3837
+ if (!e.value) this.report.error(e, "how many copies per unit?", `${f.name} -> ${s.name}: 1`);
3838
+ if (e.body) this.report.error(e.bodySpan ?? e, "a branch has no block");
3839
+ if (stage && e.value && flows.has(f.name)) branches.push([
3840
+ f.name,
3841
+ stage,
3842
+ this.dyn(e.value, "number")
3843
+ ]);
3844
+ }
3845
+ const build = (ctx) => {
3846
+ const to = {};
3847
+ for (const [f, stage, share] of branches) {
3848
+ const list = to[f] ?? [];
3849
+ list.push({
3850
+ stage,
3851
+ share: typeof share === "function" ? share(ctx) : share
3852
+ });
3853
+ to[f] = list;
3854
+ }
3855
+ return to;
3856
+ };
3857
+ const fixed = branches.every(([, , v]) => typeof v === "number");
3858
+ return fanout({
3859
+ ...base,
3860
+ to: fixed ? build(void 0) : build,
3861
+ ...component ? { component } : {}
3862
+ });
3863
+ }
3864
+ }
3865
+ }
3866
+ node(box) {
3867
+ const b = box.block;
3868
+ const label = this.label(b.args.label) ?? box.id;
3869
+ const subProp = this.prop(b, "sub");
3870
+ if (subProp && b.args.sub) this.report.error(subProp.key, "the sub-line is set twice: on the first line and here");
3871
+ const sub = this.label(b.args.sub ?? subProp?.value);
3872
+ const o = { label };
3873
+ if (sub != null) o.sub = sub;
3874
+ const indexLabel = (key) => {
3875
+ const prop = this.prop(b, key);
3876
+ if (!prop) return void 0;
3877
+ const run = placed(prop.value, this.formula(prop.value, "i", "string").run);
3878
+ return (i) => run(void 0, i);
3879
+ };
3880
+ const stageIds = box.stages.map((s) => s.id);
3881
+ if (b.keyword === "cluster") {
3882
+ o.stage = stageIds.length === 1 ? stageIds[0] : stageIds;
3883
+ const members = this.prop(b, "members");
3884
+ o.members = members ? this.dyn(members.value, "number", "ctx", this.count("members")) : 1;
3885
+ const memberLabel = indexLabel("memberLabel");
3886
+ if (memberLabel) o.memberLabel = memberLabel;
3887
+ return {
3888
+ kind: "cluster",
3889
+ ...o
3890
+ };
3891
+ }
3892
+ if (b.keyword === "pool") {
3893
+ o.stage = stageIds[0];
3894
+ const podLabel = indexLabel("podLabel");
3895
+ if (podLabel) o.podLabel = podLabel;
3896
+ return {
3897
+ kind: "pool",
3898
+ ...o
3899
+ };
3900
+ }
3901
+ if (stageIds.length) o.stage = stageIds[0];
3902
+ else if (box.component) o.component = box.component.id;
3903
+ return {
3904
+ kind: "node",
3905
+ shape: this.constProp(b, "shape") ?? "box",
3906
+ ...o
3907
+ };
3908
+ }
3909
+ edges(e, boxes) {
3910
+ for (const n of e.chain) if (!boxes.has(n.name)) this.report.error(n, `unknown node "${n.name}"`, didYouMean(n.name, boxes.keys()));
3911
+ const props = /* @__PURE__ */ new Map();
3912
+ for (const s of e.body ?? []) {
3913
+ if (s.kind !== "prop" || !own(EDGE_PROPS, s.key.name)) {
3914
+ const at = s.kind === "prop" ? s.key : s.kind === "decl" ? s.keyword : s;
3915
+ this.report.error(at, "an edge holds only label and when", didYouMean(this.slice(at), Object.keys(EDGE_PROPS)));
3916
+ continue;
3917
+ }
3918
+ if (props.has(s.key.name)) this.report.error(s.key, `"${s.key.name}" is set twice on this edge`);
3919
+ props.set(s.key.name, s);
3920
+ }
3921
+ const labelExpr = e.value ?? props.get("label")?.value;
3922
+ if (e.value && props.has("label")) this.report.error(props.get("label").key, "the label is set twice");
3923
+ if (labelExpr && e.chain.length > 2) this.report.error(labelExpr, "a label goes on a single edge: a -> b: \"…\"");
3924
+ const label = this.label(labelExpr);
3925
+ const when = props.get("when");
3926
+ const out = [];
3927
+ for (let i = 0; i + 1 < e.chain.length; i++) {
3928
+ const d = {
3929
+ from: e.chain[i].name,
3930
+ to: e.chain[i + 1].name
3931
+ };
3932
+ if (label != null) d.label = label;
3933
+ if (when) d.when = placed(when.value, this.formula(when.value, "ctx").run);
3934
+ out.push(d);
3935
+ }
3936
+ return out;
3937
+ }
3938
+ };
3939
+ const FORBIDDEN = /* @__PURE__ */ new Set([...Object.getOwnPropertyNames(Object.prototype), "prototype"]);
3940
+ const CSS_COLOR = /^(#[0-9a-f]{3,8}|(rgb|hsl)a?\([\d\s.,%/+-]+\))$/i;
3941
+ function usage(keyword) {
3942
+ const ex = own(SCHEMA, keyword)?.example;
3943
+ return ex ? `e.g. ${ex.split("\n")[0]}` : void 0;
3944
+ }
3945
+ function where(keyword) {
3946
+ const parents = Object.entries(SCHEMA).filter(([, s]) => s.children.includes(keyword)).map(([k]) => k === "" ? "the top level" : k);
3947
+ return parents.length ? `it goes in ${parents.join(" or ")}` : void 0;
3948
+ }
3949
+ // src/check.ts
3950
+ /**
3951
+ * `loadmap check`: compiles each definition and tests its expect lines. Resolves to the exit code: 1 when a
3952
+ * definition has an error or an expectation fails, 2 for a usage mistake, 0 when everything holds.
3953
+ */
3954
+ async function check(argv, o = {}) {
3955
+ const log = o.log ?? console.log;
3956
+ let parsed;
3957
+ try {
3958
+ parsed = parse(argv, o.env ?? process.env);
3959
+ } catch (err) {
3960
+ log(err.message);
3961
+ return 2;
3962
+ }
3963
+ let failed = false;
3964
+ for (const file of parsed.files) {
3965
+ let source;
3966
+ try {
3967
+ source = await readFile(file, "utf8");
3968
+ } catch (err) {
3969
+ log(`${file}: cannot read it (${err.code ?? err.message})`);
3970
+ failed = true;
3971
+ continue;
3972
+ }
3973
+ const r = compile(source);
3974
+ const list = problems(r, "error");
3975
+ if (parsed.format === "github") for (const d of list) log(github(d, file, source));
3976
+ for (const d of list) log(formatDiagnostic(d, source, file));
3977
+ if (list.some((d) => d.severity === "error")) failed = true;
3978
+ const errors = r.diagnostics.filter((d) => d.severity === "error").length;
3979
+ const n = r.expectations.length;
3980
+ const bad = r.expectations.filter((e) => !e.result.ok).length;
3981
+ const many = (k, one, more) => `${k} ${k === 1 ? one : more}`;
3982
+ log(errors ? `${file}: does not compile (${many(errors, "error", "errors")})` : !n ? `${file}: compiles; it has no expect lines` : bad ? `${file}: ${bad} of ${many(n, "expectation", "expectations")} failed` : `${file}: ${many(n, "expectation holds", "expectations hold")}`);
3983
+ for (const e of r.expectations) if (e.result.ok) log(` ✓ ${e.result.expectation.label ?? `line ${e.line}`}: ${e.result.message}`);
3984
+ }
3985
+ return failed ? 1 : 0;
3986
+ }
3987
+ function parse(argv, env) {
3988
+ const { values, positionals } = parseArgs({
3989
+ args: argv,
3990
+ allowPositionals: true,
3991
+ options: { format: { type: "string" } }
3992
+ });
3993
+ const format = values.format ?? (env.GITHUB_ACTIONS === "true" ? "github" : "text");
3994
+ if (format !== "text" && format !== "github") throw new Error(`--format is text or github, not "${format}"`);
3995
+ if (!positionals.length) throw new Error("loadmap check needs one or more .loadmap files");
3996
+ return {
3997
+ files: positionals,
3998
+ format
3999
+ };
4000
+ }
4001
+ /**
4002
+ * A diagnostic as a GitHub Actions workflow command: an annotation on its line of the file in the pull request.
4003
+ * https://docs.github.com/actions/reference/workflow-commands-for-github-actions
4004
+ */
4005
+ function github(d, file, source) {
4006
+ const breaks = source.slice(d.from, Math.max(d.from, d.to)).split("\n");
4007
+ const endLine = d.line + breaks.length - 1;
4008
+ const endColumn = (breaks.length > 1 ? 1 : d.column) + breaks.at(-1).length;
4009
+ const props = [
4010
+ `file=${prop(file)}`,
4011
+ `line=${d.line}`,
4012
+ `col=${d.column}`,
4013
+ `endLine=${endLine}`,
4014
+ `endColumn=${endColumn}`,
4015
+ `title=${prop("loadmap check")}`
4016
+ ];
4017
+ const message = d.hint ? `${d.message} (${d.hint})` : d.message;
4018
+ return `::${d.severity === "error" ? "error" : "warning"} ${props.join(",")}::${data(message)}`;
4019
+ }
4020
+ const data = (s) => s.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
4021
+ const prop = (s) => data(s).replaceAll(":", "%3A").replaceAll(",", "%2C");
4022
+ // src/index.ts
4023
+ const USAGE = `loadmap check <file.loadmap>… [--format text|github]
4024
+ loadmap record <url | file.loadmap> --out <file.gif|.webm|.mp4|.png|.svg> [options]
4025
+
4026
+ check compiles each definition and tests its expect lines on the pessimistic edge (every capacity at its low
4027
+ bound). It exits 1 when a definition has an error or an expectation fails, 0 when everything holds.
4028
+
4029
+ --format <f> text, or github to add annotations on the lines (the default under GitHub Actions)
4030
+
4031
+ record opens the page in headless Chromium, finds the loadmap it exposes (expose() from @loadmap/export) and
4032
+ records it on a fixed clock, so the same options give the same file. A .loadmap file is drawn by the viewer that
4033
+ comes with the command.
4034
+
4035
+ --out <file> the extension picks the format: gif, webm, mp4, png or svg
4036
+ --script <file.json> [{ "at": 0, "set": { "load": 5 } }, { "at": 3, "scenario": "Proposed" }, …]
4037
+ or { "seconds", "fps", "script": […] }
4038
+ --scenario <label> start from a scenario's values (--set goes on top)
4039
+ --set <key=value> a value before recording starts; repeatable (--set load=30 --set pods=6)
4040
+ --caption the title, scenario and load above the board, the verdict below it
4041
+ --story play the definition's story, a step at a time (with --caption, its text below the board)
4042
+ --poster with .png: the board, the verdict, the limits and the scenarios compared
4043
+ --seconds <n> length of a video or GIF (default 8, or the script's)
4044
+ --fps <n> frames per second (default 30; GIF 15, at most 50)
4045
+ --scale <n> pixels per diagram unit (default 1, PNG 2)
4046
+ --seed <n> the sim's seed (default 1)
4047
+ --name <name> which exposed loadmap, when the page has several (default "default")
4048
+ --width <px> the browser window (default 1280; below 640 the diagram runs top to bottom)
4049
+ --color-scheme <s> light or dark, for a page that follows the system theme (default light)`;
4050
+ /** Runs the command line; resolves to the exit code. */
4051
+ async function main(argv, log = console.log) {
4052
+ const [command, ...rest] = argv;
4053
+ if (command === "check") return check(rest, { log });
4054
+ if (command !== "record") {
4055
+ log(USAGE);
4056
+ return command === "--help" || command === "-h" ? 0 : 2;
4057
+ }
4058
+ const args = await parseRecord(rest);
4059
+ const bytes = await record(args);
4060
+ await writeFile(args.out, bytes);
4061
+ log(`${args.out}: ${(bytes.length / 1024).toFixed(0)} KB`);
4062
+ return 0;
4063
+ }
4064
+ async function parseRecord(argv) {
4065
+ const { values: v, positionals } = parseArgs({
4066
+ args: argv,
4067
+ allowPositionals: true,
4068
+ options: {
4069
+ out: { type: "string" },
4070
+ script: { type: "string" },
4071
+ set: {
4072
+ type: "string",
4073
+ multiple: true
4074
+ },
4075
+ scenario: { type: "string" },
4076
+ caption: { type: "boolean" },
4077
+ poster: { type: "boolean" },
4078
+ story: { type: "boolean" },
4079
+ seconds: { type: "string" },
4080
+ fps: { type: "string" },
4081
+ scale: { type: "string" },
4082
+ seed: { type: "string" },
4083
+ name: { type: "string" },
4084
+ width: { type: "string" },
4085
+ "color-scheme": { type: "string" }
4086
+ }
4087
+ });
4088
+ const target = positionals[0];
4089
+ if (!target || !v.out) throw new Error(`loadmap record needs a URL or a .loadmap file and --out\n\n${USAGE}`);
4090
+ const file = !/^[a-z][a-z\d+.-]*:\/\//i.test(target) && extname(target).toLowerCase() === ".loadmap" ? target : void 0;
4091
+ const num = (x, name) => {
4092
+ if (x == null) return void 0;
4093
+ const n = Number(x);
4094
+ if (!x.trim() || !Number.isFinite(n) || n <= 0) throw new Error(`--${name} takes a positive number, not "${x}"`);
4095
+ return n;
4096
+ };
4097
+ let script;
4098
+ let fromFile = {};
4099
+ if (v.script) {
4100
+ const parsed = JSON.parse(await readFile(v.script, "utf8"));
4101
+ script = Array.isArray(parsed) ? parsed : parsed.script;
4102
+ fromFile = Array.isArray(parsed) ? {} : parsed;
4103
+ }
4104
+ const set = {};
4105
+ for (const kv of v.set ?? []) {
4106
+ const i = kv.indexOf("=");
4107
+ if (i < 1) throw new Error(`--set takes key=value, not "${kv}"`);
4108
+ const raw = kv.slice(i + 1).trim();
4109
+ if (!raw) throw new Error(`--set ${kv.slice(0, i)}= has no value`);
4110
+ const n = Number(raw);
4111
+ set[kv.slice(0, i)] = raw === "true" ? true : raw === "false" ? false : Number.isFinite(n) ? n : raw;
4112
+ }
4113
+ const scheme = v["color-scheme"] ?? "light";
4114
+ if (scheme !== "light" && scheme !== "dark") throw new Error("--color-scheme is light or dark");
4115
+ if (v.poster && extname(v.out).toLowerCase() !== ".png") throw new Error("--poster writes a PNG: --out must end in .png");
4116
+ if (v.story && ![
4117
+ ".gif",
4118
+ ".webm",
4119
+ ".mp4"
4120
+ ].includes(extname(v.out).toLowerCase())) throw new Error("--story plays a GIF or a video: --out must end in .gif, .webm or .mp4");
4121
+ if (v.story && (v.script || v.seconds || v.scenario || v.set)) throw new Error("--story sets the values and the length itself: leave out --script, --seconds, --scenario and --set");
4122
+ return {
4123
+ url: file ? "" : target,
4124
+ ...file ? { file } : {},
4125
+ out: v.out,
4126
+ script,
4127
+ scenario: v.scenario,
4128
+ set,
4129
+ caption: v.caption ?? false,
4130
+ poster: v.poster ?? false,
4131
+ story: v.story ?? false,
4132
+ seconds: num(v.seconds, "seconds") ?? fromFile.seconds,
4133
+ fps: num(v.fps, "fps") ?? fromFile.fps,
4134
+ scale: num(v.scale, "scale"),
4135
+ seed: num(v.seed, "seed"),
4136
+ name: v.name ?? "default",
4137
+ width: num(v.width, "width") ?? 1280,
4138
+ colorScheme: scheme
4139
+ };
4140
+ }
4141
+ /**
4142
+ * Records in a fresh headless Chromium and returns the file's bytes. A .loadmap file is compiled here first (its
4143
+ * errors are thrown, formatted), then drawn by the viewer in `o.viewer`: by default the one the build puts in
4144
+ * dist/viewer.
4145
+ */
4146
+ async function record(a, o = {}) {
4147
+ const format = extname(a.out).slice(1).toLowerCase();
4148
+ if (![
4149
+ "gif",
4150
+ "webm",
4151
+ "mp4",
4152
+ "png",
4153
+ "svg"
4154
+ ].includes(format)) throw new Error(`--out must end in .gif, .webm, .mp4, .png or .svg`);
4155
+ if (!a.file) return recordPage(a, format);
4156
+ const source = await readFile(a.file, "utf8");
4157
+ const r = compile(source);
4158
+ if (!r.ok) throw new Error(r.diagnostics.map((d) => formatDiagnostic(d, source, a.file)).join("\n"));
4159
+ const server = await serve(source, o.viewer ?? join(import.meta.dirname, "../dist/viewer"));
4160
+ try {
4161
+ return await recordPage({
4162
+ ...a,
4163
+ url: server.url
4164
+ }, format);
4165
+ } finally {
4166
+ await server.close();
4167
+ }
4168
+ }
4169
+ const TYPES = {
4170
+ ".html": "text/html; charset=utf-8",
4171
+ ".js": "text/javascript; charset=utf-8",
4172
+ ".css": "text/css; charset=utf-8",
4173
+ ".loadmap": "text/plain; charset=utf-8"
4174
+ };
4175
+ /** The viewer and the definition, on a port of 127.0.0.1 for as long as the recording takes. */
4176
+ async function serve(source, dir) {
4177
+ try {
4178
+ await readFile(join(dir, "index.html"));
4179
+ } catch {
4180
+ throw new Error(`no viewer in ${dir}: build the CLI first (pnpm --filter @loadmap/cli build)`);
4181
+ }
4182
+ const server = createServer(async (req, res) => {
4183
+ try {
4184
+ const path = normalize(decodeURIComponent(new URL(req.url ?? "/", "http://x").pathname)).replace(/^[/\\]+/, "");
4185
+ if (path.split(/[/\\]/).includes("..")) throw new Error("outside the viewer");
4186
+ const file = path || "index.html";
4187
+ const body = file === "definition.loadmap" ? Buffer.from(source) : await readFile(join(dir, file));
4188
+ res.writeHead(200, { "content-type": TYPES[extname(file)] ?? "application/octet-stream" });
4189
+ res.end(body);
4190
+ } catch {
4191
+ res.writeHead(404).end();
4192
+ }
4193
+ });
4194
+ await new Promise((done) => server.listen(0, "127.0.0.1", done));
4195
+ const { port } = server.address();
4196
+ return {
4197
+ url: `http://127.0.0.1:${port}/`,
4198
+ close: () => new Promise((done) => server.close(() => done()))
4199
+ };
4200
+ }
4201
+ async function recordPage(a, format) {
4202
+ const browser = await chromium.launch();
4203
+ try {
4204
+ const page = await browser.newPage({
4205
+ viewport: {
4206
+ width: a.width,
4207
+ height: 900
4208
+ },
4209
+ colorScheme: a.colorScheme
4210
+ });
4211
+ await page.goto(a.url);
4212
+ await page.waitForFunction((name) => Boolean(globalThis.__loadmap?.[name]), a.name, { timeout: 3e4 }).catch(() => {
4213
+ throw new Error(`no loadmap "${a.name}" on ${a.url}: the page must call expose(handle${a.name === "default" ? "" : `, '${a.name}'`})`);
4214
+ });
4215
+ const out = await page.evaluate(async ({ a, format }) => {
4216
+ const r = globalThis.__loadmap[a.name];
4217
+ const still = {
4218
+ scenario: a.scenario,
4219
+ set: a.set,
4220
+ scale: a.scale,
4221
+ seed: a.seed,
4222
+ caption: a.caption
4223
+ };
4224
+ if (format === "svg") return r.svg(still);
4225
+ if (format === "png") return a.poster ? r.poster(still) : r.still(still);
4226
+ if (a.story) return r.record({
4227
+ format,
4228
+ story: true,
4229
+ fps: a.fps,
4230
+ scale: a.scale,
4231
+ seed: a.seed,
4232
+ caption: a.caption
4233
+ });
4234
+ const script = [{
4235
+ at: 0,
4236
+ scenario: a.scenario,
4237
+ set: a.set
4238
+ }, ...a.script ?? []];
4239
+ return r.record({
4240
+ format,
4241
+ seconds: a.seconds ?? 8,
4242
+ fps: a.fps,
4243
+ scale: a.scale,
4244
+ seed: a.seed,
4245
+ caption: a.caption,
4246
+ script
4247
+ });
4248
+ }, {
4249
+ a,
4250
+ format
4251
+ });
4252
+ return format === "svg" ? Buffer.from(out, "utf8") : Buffer.from(out, "base64");
4253
+ } finally {
4254
+ await browser.close();
4255
+ }
4256
+ }
4257
+ export { check as a, record as i, main as n, github as o, parseRecord as r, USAGE as t };