@klarkxy/dsh-deep-view 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js ADDED
@@ -0,0 +1,1436 @@
1
+ import { RPC_CHANNEL, VIEW_OWNER, isRecord as isRecord$1 } from "./contracts.js";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ //#region ../dsh-plugin-kit/src/host-rpc.ts
5
+ /** The shared authenticated HTTP boundary. Each plugin still owns its RPC envelope semantics. */
6
+ function registerHostRpcTransport(ctx, channel, dispatch, options = {}) {
7
+ const bodyLimit = options.maxBodyBytes ?? 65536;
8
+ if (!Number.isSafeInteger(bodyLimit) || bodyLimit <= 0) throw new Error("Invalid RPC body limit");
9
+ const isEndpoint = options.isEndpoint ?? ((endpoint) => /^[a-z.]+$/.test(endpoint));
10
+ return ctx.webServer.register({
11
+ kind: "prefix",
12
+ path: channel,
13
+ handler: async (req, res) => {
14
+ const reject = (status, error, override) => {
15
+ if (res.destroyed) return;
16
+ if (status === 413) res.writeHead(status, { connection: "close" });
17
+ else res.writeHead(status);
18
+ res.end(override ?? options.errorBody?.(status, error) ?? (status === 500 ? "rpc request failed" : ""));
19
+ };
20
+ try {
21
+ if (typeof ctx.connection.requestRejection !== "function") {
22
+ reject(503);
23
+ return;
24
+ }
25
+ const rejection = ctx.connection.requestRejection(req);
26
+ if (rejection !== void 0) {
27
+ reject(rejection);
28
+ return;
29
+ }
30
+ const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
31
+ const endpoint = pathname.startsWith(`${channel}/`) ? pathname.slice(channel.length + 1) : "";
32
+ if (req.method !== "POST" || !isEndpoint(endpoint)) {
33
+ reject(404);
34
+ return;
35
+ }
36
+ const contentType = String(req.headers["content-type"] ?? "").split(";", 1)[0]?.trim();
37
+ if ((options.caseInsensitiveContentType ? contentType.toLowerCase() : contentType) !== "application/json") {
38
+ reject(415);
39
+ return;
40
+ }
41
+ const guarded = options.beforeBody?.(req, endpoint);
42
+ if (guarded) {
43
+ reject(guarded.status, void 0, guarded.body);
44
+ return;
45
+ }
46
+ const controller = new AbortController();
47
+ res.on("close", () => {
48
+ if (!res.writableEnded) controller.abort();
49
+ });
50
+ const chunks = [];
51
+ let size = 0;
52
+ for await (const chunk of req) {
53
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
54
+ size += buffer.length;
55
+ if (size > bodyLimit) {
56
+ reject(413);
57
+ req.destroy();
58
+ return;
59
+ }
60
+ chunks.push(buffer);
61
+ }
62
+ let body;
63
+ try {
64
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8") || (options.emptyBodyAsNull ? "null" : ""));
65
+ } catch {
66
+ reject(400);
67
+ return;
68
+ }
69
+ await dispatch(endpoint, body, controller.signal, res);
70
+ } catch (error) {
71
+ reject(500, error);
72
+ }
73
+ }
74
+ });
75
+ }
76
+ /** Write the native Connection response envelope after a plugin has handled a request. */
77
+ function sendHostRpcResponse(response, rpcId, result, noStore = true) {
78
+ if (response.destroyed) return;
79
+ response.writeHead(200, {
80
+ "content-type": "application/json",
81
+ ...noStore ? { "cache-control": "no-store" } : {}
82
+ });
83
+ response.end(JSON.stringify({
84
+ type: "server-response",
85
+ rpcId,
86
+ result
87
+ }));
88
+ }
89
+ /** Safe host RPC registration. Missing requestRejection fails closed. */
90
+ function registerHostRpc(ctx, channel, call, options = {}) {
91
+ return registerHostRpcTransport(ctx, channel, async (endpoint, body, signal, res) => {
92
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
93
+ res.writeHead(400);
94
+ res.end();
95
+ return;
96
+ }
97
+ const request = body;
98
+ if (request.type !== "client-request" || request.method !== endpoint || typeof request.rpcId !== "string" || options.requirePayload && !Object.hasOwn(request, "payload")) {
99
+ res.writeHead(400);
100
+ res.end();
101
+ return;
102
+ }
103
+ const raw = await call(endpoint, request.payload, signal);
104
+ const result = raw.ok ? raw : {
105
+ ...raw,
106
+ error: {
107
+ details: {},
108
+ ...raw.error
109
+ }
110
+ };
111
+ sendHostRpcResponse(res, request.rpcId, result);
112
+ }, options);
113
+ }
114
+ //#endregion
115
+ //#region src/core/formula.js
116
+ const FUNCTIONS = {
117
+ min: {
118
+ args: [1, Infinity],
119
+ run: (...values) => Math.min(...values.map(num))
120
+ },
121
+ max: {
122
+ args: [1, Infinity],
123
+ run: (...values) => Math.max(...values.map(num))
124
+ },
125
+ abs: {
126
+ args: [1, 1],
127
+ run: (value) => Math.abs(num(value))
128
+ },
129
+ floor: {
130
+ args: [1, 1],
131
+ run: (value) => Math.floor(num(value))
132
+ },
133
+ ceil: {
134
+ args: [1, 1],
135
+ run: (value) => Math.ceil(num(value))
136
+ },
137
+ sqrt: {
138
+ args: [1, 1],
139
+ run: (value) => Math.sqrt(num(value))
140
+ },
141
+ pow: {
142
+ args: [2, 2],
143
+ run: (base, power) => num(base) ** num(power)
144
+ },
145
+ round: {
146
+ args: [1, 2],
147
+ run: (value, digits = 0) => roundTo(num(value), digitsOf(digits))
148
+ },
149
+ clamp: {
150
+ args: [3, 3],
151
+ run: (value, low, high) => Math.min(Math.max(num(value), num(low)), num(high))
152
+ },
153
+ fixed: {
154
+ args: [1, 2],
155
+ run: (value, digits = 0) => roundTo(num(value), digitsOf(digits)).toFixed(digitsOf(digits))
156
+ },
157
+ format: {
158
+ args: [1, 2],
159
+ run: (value, digits = 0) => grouped(num(value), digitsOf(digits))
160
+ },
161
+ percent: {
162
+ args: [1, 2],
163
+ run: (value, digits = 0) => `${grouped(num(value) * 100, digitsOf(digits))}%`
164
+ }
165
+ };
166
+ const FORMULA_FUNCTIONS = Object.keys(FUNCTIONS);
167
+ const CONSTANTS = /* @__PURE__ */ new Map([["true", true], ["false", false]]);
168
+ const MAX_NESTING = 64;
169
+ var FormulaError = class extends Error {};
170
+ const fail$1 = (message) => {
171
+ throw new FormulaError(message);
172
+ };
173
+ function num(value) {
174
+ if (typeof value === "number") return value;
175
+ if (typeof value === "boolean") return value ? 1 : 0;
176
+ const parsed = Number(value);
177
+ if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(parsed)) return parsed;
178
+ return fail$1(`${JSON.stringify(value)} is not a number`);
179
+ }
180
+ const digitsOf = (digits) => {
181
+ const value = num(digits);
182
+ if (!Number.isInteger(value) || value < 0 || value > 10) fail$1("digits must be a whole number from 0 to 10");
183
+ return value;
184
+ };
185
+ const roundTo = (value, digits) => {
186
+ const scale = 10 ** digits;
187
+ return Math.round(value * scale) / scale;
188
+ };
189
+ function grouped(value, digits) {
190
+ if (!Number.isFinite(value)) return String(value);
191
+ const [whole, part] = Math.abs(roundTo(value, digits)).toFixed(digits).split(".");
192
+ return `${value < 0 && Number(whole + (part ?? "")) !== 0 ? "-" : ""}${whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}${part ? `.${part}` : ""}`;
193
+ }
194
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
195
+ const isName = (value) => typeof value === "string" && IDENT.test(value);
196
+ const OPERATORS = [
197
+ "==",
198
+ "!=",
199
+ "<=",
200
+ ">=",
201
+ "&&",
202
+ "||",
203
+ "+",
204
+ "-",
205
+ "*",
206
+ "/",
207
+ "%",
208
+ "<",
209
+ ">",
210
+ "!",
211
+ "?",
212
+ ":",
213
+ "(",
214
+ ")",
215
+ ","
216
+ ];
217
+ function tokenize(source) {
218
+ const tokens = [];
219
+ let at = 0;
220
+ while (at < source.length) {
221
+ const char = source[at];
222
+ if (/\s/.test(char)) {
223
+ at += 1;
224
+ continue;
225
+ }
226
+ const number = /^(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(source.slice(at));
227
+ if (number) {
228
+ tokens.push({
229
+ kind: "number",
230
+ value: Number(number[0]),
231
+ at
232
+ });
233
+ at += number[0].length;
234
+ continue;
235
+ }
236
+ const name = /^[A-Za-z_][A-Za-z0-9_]*/.exec(source.slice(at));
237
+ if (name) {
238
+ tokens.push({
239
+ kind: "name",
240
+ value: name[0],
241
+ at
242
+ });
243
+ at += name[0].length;
244
+ continue;
245
+ }
246
+ if (char === "\"" || char === "'") {
247
+ let end = at + 1;
248
+ let text = "";
249
+ while (end < source.length && source[end] !== char) {
250
+ if (source[end] === "\\" && end + 1 < source.length) {
251
+ text += source[end + 1];
252
+ end += 2;
253
+ continue;
254
+ }
255
+ text += source[end];
256
+ end += 1;
257
+ }
258
+ if (end >= source.length) fail$1(`unclosed string at ${at + 1}`);
259
+ tokens.push({
260
+ kind: "string",
261
+ value: text,
262
+ at
263
+ });
264
+ at = end + 1;
265
+ continue;
266
+ }
267
+ const operator = OPERATORS.find((op) => source.startsWith(op, at));
268
+ if (operator === void 0) fail$1(`unexpected ${JSON.stringify(char)} at ${at + 1}`);
269
+ tokens.push({
270
+ kind: "op",
271
+ value: operator,
272
+ at
273
+ });
274
+ at += operator.length;
275
+ }
276
+ return tokens;
277
+ }
278
+ const BINARY = [
279
+ ["||"],
280
+ ["&&"],
281
+ ["==", "!="],
282
+ [
283
+ "<",
284
+ "<=",
285
+ ">",
286
+ ">="
287
+ ],
288
+ ["+", "-"],
289
+ [
290
+ "*",
291
+ "/",
292
+ "%"
293
+ ]
294
+ ];
295
+ function parse(source) {
296
+ const tokens = tokenize(source);
297
+ let index = 0;
298
+ let depth = 0;
299
+ const peek = () => tokens[index];
300
+ const isOp = (value) => peek()?.kind === "op" && peek().value === value;
301
+ const expect = (value) => {
302
+ if (!isOp(value)) fail$1(peek() ? `expected "${value}" at ${peek().at + 1}` : `expected "${value}" at the end`);
303
+ index += 1;
304
+ };
305
+ const nest = (read) => {
306
+ depth += 1;
307
+ if (depth > MAX_NESTING) fail$1(`nested more than ${MAX_NESTING} levels`);
308
+ const node = read();
309
+ depth -= 1;
310
+ return node;
311
+ };
312
+ const ternary = () => nest(() => {
313
+ const test = binary(0);
314
+ if (!isOp("?")) return test;
315
+ index += 1;
316
+ const yes = ternary();
317
+ expect(":");
318
+ return {
319
+ type: "if",
320
+ test,
321
+ yes,
322
+ no: ternary()
323
+ };
324
+ });
325
+ const binary = (level) => {
326
+ if (level === BINARY.length) return unary();
327
+ let left = binary(level + 1);
328
+ while (peek()?.kind === "op" && BINARY[level].includes(peek().value)) {
329
+ const op = peek().value;
330
+ index += 1;
331
+ left = {
332
+ type: "binary",
333
+ op,
334
+ left,
335
+ right: binary(level + 1)
336
+ };
337
+ }
338
+ return left;
339
+ };
340
+ const unary = () => {
341
+ if (isOp("-") || isOp("!") || isOp("+")) {
342
+ const op = peek().value;
343
+ index += 1;
344
+ return nest(() => ({
345
+ type: "unary",
346
+ op,
347
+ value: unary()
348
+ }));
349
+ }
350
+ return primary();
351
+ };
352
+ const primary = () => {
353
+ const token = peek();
354
+ if (token === void 0) fail$1("the formula ends too early");
355
+ index += 1;
356
+ if (token.kind === "number" || token.kind === "string") return {
357
+ type: "value",
358
+ value: token.value
359
+ };
360
+ if (token.kind === "op" && token.value === "(") {
361
+ const inner = ternary();
362
+ expect(")");
363
+ return inner;
364
+ }
365
+ if (token.kind !== "name") fail$1(`unexpected "${token.value}" at ${token.at + 1}`);
366
+ if (!isOp("(")) return CONSTANTS.has(token.value) ? {
367
+ type: "value",
368
+ value: CONSTANTS.get(token.value)
369
+ } : {
370
+ type: "name",
371
+ name: token.value
372
+ };
373
+ const fn = Object.hasOwn(FUNCTIONS, token.value) ? FUNCTIONS[token.value] : void 0;
374
+ if (fn === void 0) fail$1(`unknown function "${token.value}"; allowed: ${FORMULA_FUNCTIONS.join(", ")}`);
375
+ index += 1;
376
+ const args = [];
377
+ if (!isOp(")")) {
378
+ args.push(ternary());
379
+ while (isOp(",")) {
380
+ index += 1;
381
+ args.push(ternary());
382
+ }
383
+ }
384
+ expect(")");
385
+ if (args.length < fn.args[0] || args.length > fn.args[1]) fail$1(`${token.value}() takes ${fn.args[0] === fn.args[1] ? fn.args[0] : fn.args[1] === Infinity ? `at least ${fn.args[0]}` : `${fn.args[0]} or ${fn.args[1]}`} argument${fn.args[1] === 1 ? "" : "s"}; got ${args.length}`);
386
+ return {
387
+ type: "call",
388
+ name: token.value,
389
+ args
390
+ };
391
+ };
392
+ if (tokens.length === 0) fail$1("the formula is empty");
393
+ const tree = ternary();
394
+ if (index < tokens.length) fail$1(`unexpected "${peek().value}" at ${peek().at + 1}`);
395
+ return tree;
396
+ }
397
+ function namesIn(node, found = /* @__PURE__ */ new Set()) {
398
+ if (node.type === "name") found.add(node.name);
399
+ else if (node.type === "unary") namesIn(node.value, found);
400
+ else if (node.type === "binary") {
401
+ namesIn(node.left, found);
402
+ namesIn(node.right, found);
403
+ } else if (node.type === "if") {
404
+ namesIn(node.test, found);
405
+ namesIn(node.yes, found);
406
+ namesIn(node.no, found);
407
+ } else if (node.type === "call") for (const arg of node.args) namesIn(arg, found);
408
+ return found;
409
+ }
410
+ function compileFormula(source) {
411
+ if (typeof source !== "string") return { error: "expected a formula string" };
412
+ try {
413
+ const tree = parse(source);
414
+ return {
415
+ tree,
416
+ names: [...namesIn(tree)]
417
+ };
418
+ } catch (error) {
419
+ if (error instanceof FormulaError) return { error: error.message };
420
+ throw error;
421
+ }
422
+ }
423
+ function run(node, lookup) {
424
+ switch (node.type) {
425
+ case "value": return node.value;
426
+ case "name": return lookup(node.name);
427
+ case "unary": {
428
+ const value = run(node.value, lookup);
429
+ if (node.op === "!") return !truthy(value);
430
+ return node.op === "-" ? -num(value) : num(value);
431
+ }
432
+ case "if": return truthy(run(node.test, lookup)) ? run(node.yes, lookup) : run(node.no, lookup);
433
+ case "call": return FUNCTIONS[node.name].run(...node.args.map((arg) => run(arg, lookup)));
434
+ default: return binary(node, lookup);
435
+ }
436
+ }
437
+ const truthy = (value) => typeof value === "string" ? value !== "" : Boolean(value);
438
+ function binary(node, lookup) {
439
+ if (node.op === "&&") return truthy(run(node.left, lookup)) ? run(node.right, lookup) : false;
440
+ if (node.op === "||") {
441
+ const left = run(node.left, lookup);
442
+ return truthy(left) ? left : run(node.right, lookup);
443
+ }
444
+ const left = run(node.left, lookup);
445
+ const right = run(node.right, lookup);
446
+ switch (node.op) {
447
+ case "+": return typeof left === "string" || typeof right === "string" ? `${show(left)}${show(right)}` : num(left) + num(right);
448
+ case "-": return num(left) - num(right);
449
+ case "*": return num(left) * num(right);
450
+ case "/": return num(left) / num(right);
451
+ case "%": return num(left) % num(right);
452
+ case "==": return left === right;
453
+ case "!=": return left !== right;
454
+ case "<": return num(left) < num(right);
455
+ case "<=": return num(left) <= num(right);
456
+ case ">": return num(left) > num(right);
457
+ default: return num(left) >= num(right);
458
+ }
459
+ }
460
+ function show(value) {
461
+ if (typeof value === "number") return Number.isFinite(value) ? String(Number(value.toPrecision(12))) : String(value);
462
+ return String(value);
463
+ }
464
+ function createScope(inputs, lets) {
465
+ const done = /* @__PURE__ */ new Map();
466
+ const busy = /* @__PURE__ */ new Set();
467
+ const lookup = (name) => {
468
+ if (inputs.has(name)) return inputs.get(name);
469
+ if (done.has(name)) return done.get(name);
470
+ const tree = lets.get(name);
471
+ if (tree === void 0) fail$1(`unknown name "${name}"`);
472
+ if (busy.has(name)) fail$1(`"${name}" refers to itself`);
473
+ busy.add(name);
474
+ try {
475
+ const value = run(tree, lookup);
476
+ done.set(name, value);
477
+ return value;
478
+ } finally {
479
+ busy.delete(name);
480
+ }
481
+ };
482
+ return lookup;
483
+ }
484
+ function evaluate(tree, lookup) {
485
+ try {
486
+ return { value: run(tree, lookup) };
487
+ } catch (error) {
488
+ if (error instanceof FormulaError) return { error: error.message };
489
+ if (error instanceof RangeError) return { error: "the formula is too deep to work out" };
490
+ throw error;
491
+ }
492
+ }
493
+ //#endregion
494
+ //#region src/core/views.js
495
+ const LAYOUT = ["stack", "row"];
496
+ const DISPLAY = [
497
+ "text",
498
+ "metric",
499
+ "table",
500
+ "chart",
501
+ "progress",
502
+ "callout",
503
+ "sources"
504
+ ];
505
+ const INPUT_TYPES = [
506
+ "slider",
507
+ "choice",
508
+ "toggle",
509
+ "number"
510
+ ];
511
+ const ANSWERS = [
512
+ "options",
513
+ "confirm",
514
+ "form",
515
+ "submit"
516
+ ];
517
+ const COMPONENTS = [
518
+ ...LAYOUT,
519
+ ...DISPLAY,
520
+ ...INPUT_TYPES,
521
+ ...ANSWERS
522
+ ];
523
+ const TONES = [
524
+ "good",
525
+ "warn",
526
+ "bad"
527
+ ];
528
+ const TEXT_TONES = ["muted"];
529
+ const CALLOUT_TONES = [
530
+ "info",
531
+ "good",
532
+ "warn",
533
+ "bad"
534
+ ];
535
+ const CHART_KINDS = [
536
+ "bar",
537
+ "hbar",
538
+ "line",
539
+ "area",
540
+ "donut"
541
+ ];
542
+ const INTERACTIVE = new Set(ANSWERS);
543
+ const INPUTS = new Set(INPUT_TYPES);
544
+ const PROPS = {
545
+ stack: ["children", "let"],
546
+ row: ["children", "let"],
547
+ text: ["text", "tone"],
548
+ metric: [
549
+ "label",
550
+ "value",
551
+ "tone",
552
+ "caption"
553
+ ],
554
+ table: ["columns", "rows"],
555
+ chart: [
556
+ "kind",
557
+ "categories",
558
+ "series",
559
+ "unit",
560
+ "caption"
561
+ ],
562
+ progress: [
563
+ "label",
564
+ "percent",
565
+ "value",
566
+ "max",
567
+ "valueLabel",
568
+ "tone"
569
+ ],
570
+ callout: ["text", "tone"],
571
+ sources: ["items"],
572
+ slider: [
573
+ "id",
574
+ "label",
575
+ "min",
576
+ "max",
577
+ "step",
578
+ "value",
579
+ "unit"
580
+ ],
581
+ choice: [
582
+ "id",
583
+ "label",
584
+ "options",
585
+ "value"
586
+ ],
587
+ toggle: [
588
+ "id",
589
+ "label",
590
+ "value"
591
+ ],
592
+ number: [
593
+ "id",
594
+ "label",
595
+ "min",
596
+ "max",
597
+ "step",
598
+ "value",
599
+ "unit"
600
+ ],
601
+ options: [
602
+ "id",
603
+ "options",
604
+ "multi"
605
+ ],
606
+ confirm: [
607
+ "id",
608
+ "confirmLabel",
609
+ "cancelLabel"
610
+ ],
611
+ form: [
612
+ "id",
613
+ "fields",
614
+ "submitLabel"
615
+ ],
616
+ submit: ["id", "label"]
617
+ };
618
+ const MAX_DEPTH = 8;
619
+ const MAX_NODES = 64;
620
+ const MAX_CHILDREN = 16;
621
+ const MAX_TEXT = 2e3;
622
+ const MAX_CELL = 200;
623
+ const MAX_COLUMNS = 8;
624
+ const MAX_ROWS = 50;
625
+ const MAX_OPTIONS = 8;
626
+ const MAX_FIELDS = 6;
627
+ const SUMMARY_PART = 40;
628
+ const SUMMARY_COUNT = 8;
629
+ const RESERVED = /* @__PURE__ */ new Set(["true", "false"]);
630
+ const VIEW_BYTE_LIMIT = 8192;
631
+ const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
632
+ const quoted = (value) => JSON.stringify(value);
633
+ const allowed = (list) => list.join(", ");
634
+ const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
635
+ const isScalar = (value) => typeof value === "string" || typeof value === "boolean" || isNumber(value);
636
+ const isFormula = (value) => isRecord(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "=");
637
+ function viewBytes(title, view) {
638
+ return new TextEncoder().encode(`${JSON.stringify(view) ?? ""}${title ?? ""}`).length;
639
+ }
640
+ const NUMERIC = [
641
+ "value",
642
+ "percent",
643
+ "min",
644
+ "max",
645
+ "step",
646
+ "height"
647
+ ];
648
+ const numeric = (value) => typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value)) ? Number(value) : value;
649
+ function normalizeView(value) {
650
+ if (Array.isArray(value)) return value.map(normalizeView);
651
+ if (!isRecord(value)) return value;
652
+ const keys = Object.keys(value);
653
+ if (keys.length === 1 && keys[0] === "item") {
654
+ const inner = normalizeView(value.item);
655
+ if (Array.isArray(inner)) return inner;
656
+ }
657
+ const out = {};
658
+ for (const key of keys) Object.defineProperty(out, key, {
659
+ value: normalizeView(value[key]),
660
+ writable: true,
661
+ enumerable: true,
662
+ configurable: true
663
+ });
664
+ for (const field of NUMERIC) if (Object.hasOwn(out, field)) out[field] = numeric(out[field]);
665
+ if (Array.isArray(out.values)) out.values = out.values.map(numeric);
666
+ return out;
667
+ }
668
+ function validateView(view) {
669
+ const ctx = {
670
+ ids: /* @__PURE__ */ new Set(),
671
+ nodes: 0,
672
+ interactive: false,
673
+ inputs: /* @__PURE__ */ new Map(),
674
+ lets: /* @__PURE__ */ new Map(),
675
+ formulas: []
676
+ };
677
+ const error = checkNode(view, "view", 1, ctx) ?? checkFormulas(ctx);
678
+ return error === void 0 ? {
679
+ ok: true,
680
+ interactive: ctx.interactive
681
+ } : {
682
+ ok: false,
683
+ error
684
+ };
685
+ }
686
+ function checkShownView(title, sent) {
687
+ let bytes;
688
+ try {
689
+ bytes = viewBytes(title, sent);
690
+ } catch {
691
+ return { error: "The view is not JSON data." };
692
+ }
693
+ if (bytes > 8192) return { error: `The view is ${bytes} bytes; the limit is ${VIEW_BYTE_LIMIT}.` };
694
+ if (title !== void 0 && title !== null && typeof title !== "string") return { error: "title: expected a string" };
695
+ const named = String(title ?? "").trim();
696
+ const view = normalizeView(sent);
697
+ const checked = validateView(view);
698
+ if (!checked.ok) return { error: checked.error };
699
+ return {
700
+ title: named,
701
+ view,
702
+ interactive: checked.interactive
703
+ };
704
+ }
705
+ function checkNode(node, path, depth, ctx) {
706
+ if (!isRecord(node)) return `${path}: expected a component object`;
707
+ if (depth > MAX_DEPTH) return `${path}: ${depth} levels exceeds the limit of ${MAX_DEPTH}`;
708
+ ctx.nodes += 1;
709
+ if (ctx.nodes > MAX_NODES) return `${path}: ${ctx.nodes} components exceeds the limit of ${MAX_NODES}`;
710
+ const type = node.type;
711
+ if (!COMPONENTS.includes(type)) return `${path}.type: unknown component ${quoted(type)}; allowed: ${allowed(COMPONENTS)}`;
712
+ const props = PROPS[type];
713
+ for (const key of Object.keys(node)) {
714
+ if (key === "type" || props.includes(key)) continue;
715
+ return `${path}.${key}: unknown property ${quoted(key)}; allowed: ${allowed(props)}`;
716
+ }
717
+ if (INTERACTIVE.has(type)) ctx.interactive = true;
718
+ switch (type) {
719
+ case "stack":
720
+ case "row": return checkLayout(node, path, depth, ctx);
721
+ case "text": return checkText(node, path, ctx);
722
+ case "metric": return checkMetric(node, path, ctx);
723
+ case "table": return checkTable(node, path, ctx);
724
+ case "chart": return checkChart(node, path, ctx);
725
+ case "progress": return checkProgress(node, path, ctx);
726
+ case "callout": return checkCallout(node, path, ctx);
727
+ case "sources": return checkSources(node, path);
728
+ case "slider":
729
+ case "number": return checkNumberInput(node, path, ctx);
730
+ case "choice": return checkChoice(node, path, ctx);
731
+ case "toggle": return checkToggle(node, path, ctx);
732
+ case "options": return checkOptions(node, path, ctx);
733
+ case "confirm": return checkConfirm(node, path, ctx);
734
+ case "form": return checkForm(node, path, ctx);
735
+ default: return checkSubmit(node, path, ctx);
736
+ }
737
+ }
738
+ function checkLayout(node, path, depth, ctx) {
739
+ if (node.let !== void 0) {
740
+ if (depth !== 1) return `${path}.let: only the outermost component can define names`;
741
+ if (!isRecord(node.let)) return `${path}.let: expected an object of names to formulas`;
742
+ for (const [name, source] of Object.entries(node.let)) {
743
+ if (!isName(name) || RESERVED.has(name)) return `${path}.let.${name}: a name is letters, digits and _, and starts with a letter or _`;
744
+ if (typeof source !== "string") return `${path}.let.${name}: expected a formula string`;
745
+ ctx.lets.set(name, {
746
+ source,
747
+ path: `${path}.let.${name}`
748
+ });
749
+ }
750
+ }
751
+ if (!Array.isArray(node.children)) return `${path}.children: expected an array of 1 to ${MAX_CHILDREN} components`;
752
+ if (node.children.length < 1 || node.children.length > MAX_CHILDREN) return `${path}.children: expected 1 to ${MAX_CHILDREN} children; got ${node.children.length}`;
753
+ for (let index = 0; index < node.children.length; index += 1) {
754
+ const error = checkNode(node.children[index], `${path}.children[${index}]`, depth + 1, ctx);
755
+ if (error !== void 0) return error;
756
+ }
757
+ }
758
+ function checkValue(node, key, path, ctx, kind, { required = false, max = MAX_TEXT } = {}) {
759
+ const value = node[key];
760
+ const at = `${path}.${key}`;
761
+ if (value === void 0) return required ? `${at}: ${expected(kind)}` : void 0;
762
+ if (isFormula(value)) {
763
+ ctx.formulas.push({
764
+ source: value["="],
765
+ path: at,
766
+ kind
767
+ });
768
+ return;
769
+ }
770
+ if (kind === "number") return isNumber(value) ? void 0 : `${at}: ${expected(kind)}`;
771
+ if (Array.isArray(kind)) return kind.includes(value) ? void 0 : `${at}: unknown tone ${quoted(value)}; allowed: ${allowed(kind)}`;
772
+ if (kind === "string") {
773
+ if (typeof value !== "string") return `${at}: ${expected(kind)}`;
774
+ return value.length > max ? `${at}: ${value.length} characters exceeds the limit of ${max}` : void 0;
775
+ }
776
+ if (!isScalar(value)) return `${at}: ${expected(kind)}`;
777
+ }
778
+ const expected = (kind) => kind === "number" ? "expected a number" : kind === "string" ? "expected a string" : Array.isArray(kind) ? `expected one of ${allowed(kind)}` : "expected a string or a number";
779
+ function checkText(node, path, ctx) {
780
+ return checkValue(node, "text", path, ctx, "string", { required: true }) ?? checkValue(node, "tone", path, ctx, TEXT_TONES);
781
+ }
782
+ function checkMetric(node, path, ctx) {
783
+ if (typeof node.label !== "string") return `${path}.label: expected a string`;
784
+ if (node.value === void 0) return `${path}.value: expected a string or a number`;
785
+ if (typeof node.value === "number" && !Number.isFinite(node.value)) return `${path}.value: expected a finite number`;
786
+ return checkValue(node, "value", path, ctx, "scalar") ?? checkValue(node, "tone", path, ctx, TONES) ?? checkValue(node, "caption", path, ctx, "string");
787
+ }
788
+ function checkTable(node, path, ctx) {
789
+ if (!Array.isArray(node.columns)) return `${path}.columns: expected an array of at most ${MAX_COLUMNS} columns`;
790
+ if (node.columns.length > MAX_COLUMNS) return `${path}.columns: ${node.columns.length} columns exceeds the limit of ${MAX_COLUMNS}`;
791
+ for (let index = 0; index < node.columns.length; index += 1) {
792
+ const column = node.columns[index];
793
+ const at = `${path}.columns[${index}]`;
794
+ if (!isRecord(column)) return `${at}: expected a column object`;
795
+ for (const key of Object.keys(column)) if (key !== "key" && key !== "label") return `${at}.${key}: unknown property ${quoted(key)}; allowed: key, label`;
796
+ if (typeof column.key !== "string" || column.key === "") return `${at}.key: expected a non-empty string`;
797
+ if (typeof column.label !== "string") return `${at}.label: expected a string`;
798
+ }
799
+ if (!Array.isArray(node.rows)) return `${path}.rows: expected an array of at most ${MAX_ROWS} rows`;
800
+ if (node.rows.length > MAX_ROWS) return `${path}.rows: ${node.rows.length} rows exceeds the limit of ${MAX_ROWS}`;
801
+ for (let row = 0; row < node.rows.length; row += 1) {
802
+ const cells = node.rows[row];
803
+ if (!Array.isArray(cells)) return `${path}.rows[${row}]: expected an array of cells`;
804
+ for (let column = 0; column < cells.length; column += 1) {
805
+ const at = `${path}.rows[${row}][${column}]`;
806
+ if (isFormula(cells[column])) {
807
+ ctx.formulas.push({
808
+ source: cells[column]["="],
809
+ path: at,
810
+ kind: "scalar"
811
+ });
812
+ continue;
813
+ }
814
+ const text = cellText(cells[column]);
815
+ if (text.length > MAX_CELL) return `${at}: ${text.length} characters exceeds the limit of ${MAX_CELL}`;
816
+ }
817
+ }
818
+ }
819
+ function checkChart(node, path, ctx) {
820
+ if (node.kind !== void 0 && !CHART_KINDS.includes(node.kind)) return `${path}.kind: unknown chart ${quoted(node.kind)}; allowed: ${allowed(CHART_KINDS)}`;
821
+ if (!Array.isArray(node.categories) || node.categories.length === 0) return `${path}.categories: expected a non-empty array of labels`;
822
+ for (let index = 0; index < node.categories.length; index += 1) {
823
+ const label = node.categories[index];
824
+ if (typeof label !== "string" && !isNumber(label)) return `${path}.categories[${index}]: expected a string`;
825
+ }
826
+ if (!Array.isArray(node.series) || node.series.length === 0) return `${path}.series: expected a non-empty array of { name?, values }`;
827
+ if (node.kind === "donut" && node.series.length > 1) return `${path}.series: a donut takes one series; got ${node.series.length}`;
828
+ for (let index = 0; index < node.series.length; index += 1) {
829
+ const series = node.series[index];
830
+ const at = `${path}.series[${index}]`;
831
+ if (!isRecord(series)) return `${at}: expected a series object`;
832
+ for (const key of Object.keys(series)) if (key !== "name" && key !== "values") return `${at}.${key}: unknown property ${quoted(key)}; allowed: name, values`;
833
+ if (series.name !== void 0 && typeof series.name !== "string") return `${at}.name: expected a string`;
834
+ if (!Array.isArray(series.values)) return `${at}.values: expected an array of numbers`;
835
+ if (series.values.length !== node.categories.length) return `${at}.values: ${series.values.length} values for ${node.categories.length} categories`;
836
+ for (let item = 0; item < series.values.length; item += 1) {
837
+ const value = series.values[item];
838
+ if (isFormula(value)) ctx.formulas.push({
839
+ source: value["="],
840
+ path: `${at}.values[${item}]`,
841
+ kind: "number"
842
+ });
843
+ else if (!isNumber(value)) return `${at}.values[${item}]: expected a number`;
844
+ }
845
+ }
846
+ if (node.unit !== void 0 && typeof node.unit !== "string") return `${path}.unit: expected a string`;
847
+ if (node.caption !== void 0 && typeof node.caption !== "string") return `${path}.caption: expected a string`;
848
+ }
849
+ function checkProgress(node, path, ctx) {
850
+ if (node.label !== void 0 && typeof node.label !== "string") return `${path}.label: expected a string`;
851
+ if (node.percent === void 0 === (node.value === void 0)) return `${path}: give percent (0 to 100), or value with max`;
852
+ if (node.percent !== void 0 && !isFormula(node.percent)) {
853
+ if (typeof node.percent !== "number" || !Number.isFinite(node.percent) || node.percent < 0 || node.percent > 100) return `${path}.percent: expected a number from 0 to 100`;
854
+ }
855
+ if (node.value !== void 0 && node.max === void 0) return `${path}.max: expected a number with value`;
856
+ if (node.value === void 0 && node.max !== void 0) return `${path}.value: expected a number with max`;
857
+ return checkValue(node, "percent", path, ctx, "number") ?? checkValue(node, "value", path, ctx, "number") ?? checkValue(node, "max", path, ctx, "number") ?? checkValue(node, "valueLabel", path, ctx, "scalar") ?? checkValue(node, "tone", path, ctx, TONES);
858
+ }
859
+ function checkCallout(node, path, ctx) {
860
+ return checkValue(node, "text", path, ctx, "string", { required: true }) ?? checkValue(node, "tone", path, ctx, CALLOUT_TONES);
861
+ }
862
+ function checkSources(node, path) {
863
+ if (!Array.isArray(node.items) || node.items.length === 0) return `${path}.items: expected a non-empty array of { label, url }`;
864
+ for (let index = 0; index < node.items.length; index += 1) {
865
+ const item = node.items[index];
866
+ const at = `${path}.items[${index}]`;
867
+ if (!isRecord(item)) return `${at}: expected a { label, url } object`;
868
+ for (const key of Object.keys(item)) if (key !== "label" && key !== "url") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, url`;
869
+ if (typeof item.label !== "string" || item.label.trim() === "") return `${at}.label: expected a non-empty string`;
870
+ if (typeof item.url !== "string" || !/^https?:\/\/[^\s]+$/i.test(item.url)) return `${at}.url: expected an http(s) address`;
871
+ }
872
+ }
873
+ function checkInputId(node, path, ctx) {
874
+ if (!isName(node.id) || RESERVED.has(node.id)) return `${path}.id: an input id is letters, digits and _, and starts with a letter or _, so formulas can use it`;
875
+ if (ctx.ids.has(node.id)) return `${path}.id: duplicate id ${quoted(node.id)}`;
876
+ ctx.ids.add(node.id);
877
+ if (typeof node.label !== "string") return `${path}.label: expected a string`;
878
+ }
879
+ function checkNumberInput(node, path, ctx) {
880
+ const idError = checkInputId(node, path, ctx);
881
+ if (idError !== void 0) return idError;
882
+ for (const key of [
883
+ "min",
884
+ "max",
885
+ "step"
886
+ ]) if (node[key] !== void 0 && !isNumber(node[key])) return `${path}.${key}: expected a number`;
887
+ if (node.type === "slider" && (node.min === void 0 || node.max === void 0)) return `${path}: a slider needs min and max`;
888
+ if (node.min !== void 0 && node.max !== void 0 && node.max <= node.min) return `${path}.max: expected more than min (${node.min})`;
889
+ if (node.step !== void 0 && node.step <= 0) return `${path}.step: expected more than 0`;
890
+ if (!isNumber(node.value)) return `${path}.value: expected a number`;
891
+ if (node.min !== void 0 && node.value < node.min || node.max !== void 0 && node.value > node.max) return `${path}.value: expected a number from ${node.min ?? "-∞"} to ${node.max ?? "∞"}`;
892
+ if (node.unit !== void 0 && typeof node.unit !== "string") return `${path}.unit: expected a string`;
893
+ ctx.inputs.set(node.id, node.value);
894
+ }
895
+ function checkChoice(node, path, ctx) {
896
+ const idError = checkInputId(node, path, ctx);
897
+ if (idError !== void 0) return idError;
898
+ if (!Array.isArray(node.options) || node.options.length === 0) return `${path}.options: expected a non-empty array of { label, value }`;
899
+ const labels = /* @__PURE__ */ new Set();
900
+ const values = /* @__PURE__ */ new Set();
901
+ for (let index = 0; index < node.options.length; index += 1) {
902
+ const option = node.options[index];
903
+ const at = `${path}.options[${index}]`;
904
+ if (!isRecord(option)) return `${at}: expected a { label, value } object`;
905
+ for (const key of Object.keys(option)) if (key !== "label" && key !== "value") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, value`;
906
+ if (typeof option.label !== "string" || option.label.trim() === "") return `${at}.label: expected a non-empty string`;
907
+ if (labels.has(option.label)) return `${at}.label: duplicate label ${quoted(option.label)}`;
908
+ labels.add(option.label);
909
+ const value = option.value ?? option.label;
910
+ if (!isScalar(value)) return `${at}.value: expected a string, a number or true/false`;
911
+ if (values.has(value)) return `${at}.value: duplicate value ${quoted(value)}`;
912
+ values.add(value);
913
+ }
914
+ const chosen = node.value ?? choiceValue(node.options[0]);
915
+ if (!values.has(chosen)) return `${path}.value: expected one of the options' values`;
916
+ ctx.inputs.set(node.id, chosen);
917
+ }
918
+ function checkToggle(node, path, ctx) {
919
+ const idError = checkInputId(node, path, ctx);
920
+ if (idError !== void 0) return idError;
921
+ if (node.value !== void 0 && typeof node.value !== "boolean") return `${path}.value: expected true or false`;
922
+ ctx.inputs.set(node.id, node.value === true);
923
+ }
924
+ function checkOptions(node, path, ctx) {
925
+ const idError = checkId(node, path, ctx);
926
+ if (idError !== void 0) return idError;
927
+ if (node.multi !== void 0 && typeof node.multi !== "boolean") return `${path}.multi: expected a boolean`;
928
+ if (!Array.isArray(node.options)) return `${path}.options: expected an array of 1 to ${MAX_OPTIONS} options`;
929
+ if (node.options.length < 1 || node.options.length > MAX_OPTIONS) return `${path}.options: expected 1 to ${MAX_OPTIONS} options; got ${node.options.length}`;
930
+ const labels = /* @__PURE__ */ new Set();
931
+ for (let index = 0; index < node.options.length; index += 1) {
932
+ const option = node.options[index];
933
+ const at = `${path}.options[${index}]`;
934
+ if (!isRecord(option)) return `${at}: expected an option object`;
935
+ for (const key of Object.keys(option)) if (key !== "label" && key !== "description") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, description`;
936
+ if (typeof option.label !== "string" || option.label.trim() === "") return `${at}.label: expected a non-empty string`;
937
+ if (labels.has(option.label)) return `${at}.label: duplicate label ${quoted(option.label)}`;
938
+ labels.add(option.label);
939
+ if (option.description !== void 0 && typeof option.description !== "string") return `${at}.description: expected a string`;
940
+ }
941
+ }
942
+ function checkConfirm(node, path, ctx) {
943
+ const idError = checkId(node, path, ctx);
944
+ if (idError !== void 0) return idError;
945
+ if (node.confirmLabel !== void 0 && typeof node.confirmLabel !== "string") return `${path}.confirmLabel: expected a string`;
946
+ if (node.cancelLabel !== void 0 && typeof node.cancelLabel !== "string") return `${path}.cancelLabel: expected a string`;
947
+ }
948
+ function checkForm(node, path, ctx) {
949
+ const idError = checkId(node, path, ctx);
950
+ if (idError !== void 0) return idError;
951
+ if (node.submitLabel !== void 0 && typeof node.submitLabel !== "string") return `${path}.submitLabel: expected a string`;
952
+ if (!Array.isArray(node.fields)) return `${path}.fields: expected an array of at most ${MAX_FIELDS} fields`;
953
+ if (node.fields.length > MAX_FIELDS) return `${path}.fields: ${node.fields.length} fields exceeds the limit of ${MAX_FIELDS}`;
954
+ const seen = /* @__PURE__ */ new Set();
955
+ for (let index = 0; index < node.fields.length; index += 1) {
956
+ const field = node.fields[index];
957
+ const at = `${path}.fields[${index}]`;
958
+ if (!isRecord(field)) return `${at}: expected a field object`;
959
+ for (const key of Object.keys(field)) if (key !== "id" && key !== "label" && key !== "placeholder") return `${at}.${key}: unknown property ${quoted(key)}; allowed: id, label, placeholder`;
960
+ if (typeof field.id !== "string" || field.id === "") return `${at}.id: expected a non-empty string`;
961
+ if (field.id === "__proto__") return `${at}.id: "__proto__" cannot be an id`;
962
+ if (seen.has(field.id)) return `${at}.id: duplicate field id ${quoted(field.id)}`;
963
+ seen.add(field.id);
964
+ if (typeof field.label !== "string") return `${at}.label: expected a string`;
965
+ if (field.placeholder !== void 0 && typeof field.placeholder !== "string") return `${at}.placeholder: expected a string`;
966
+ }
967
+ }
968
+ function checkSubmit(node, path, ctx) {
969
+ const idError = checkId(node, path, ctx);
970
+ if (idError !== void 0) return idError;
971
+ if (node.label !== void 0 && typeof node.label !== "string") return `${path}.label: expected a string`;
972
+ }
973
+ function checkId(node, path, ctx) {
974
+ if (typeof node.id !== "string" || node.id === "") return `${path}.id: expected a non-empty string`;
975
+ if (node.id === "__proto__") return `${path}.id: "__proto__" cannot be an id`;
976
+ if (ctx.ids.has(node.id)) return `${path}.id: duplicate id ${quoted(node.id)}`;
977
+ ctx.ids.add(node.id);
978
+ }
979
+ function checkFormulas(ctx) {
980
+ const known = /* @__PURE__ */ new Set([...ctx.inputs.keys(), ...ctx.lets.keys()]);
981
+ const lets = /* @__PURE__ */ new Map();
982
+ for (const [name, entry] of ctx.lets) {
983
+ if (ctx.inputs.has(name)) return `${entry.path}: ${quoted(name)} is already an input id`;
984
+ const compiled = compileFormula(entry.source);
985
+ if (compiled.error !== void 0) return `${entry.path}: ${compiled.error}`;
986
+ const unknown = compiled.names.find((item) => !known.has(item));
987
+ if (unknown !== void 0) return `${entry.path}: unknown name ${quoted(unknown)}; known: ${allowed([...known]) || "(none)"}`;
988
+ lets.set(name, compiled.tree);
989
+ }
990
+ const lookup = createScope(ctx.inputs, lets);
991
+ for (const [name, entry] of ctx.lets) {
992
+ const result = evaluate(lets.get(name), lookup);
993
+ if (result.error !== void 0) return `${entry.path}: ${result.error}`;
994
+ }
995
+ for (const formula of ctx.formulas) {
996
+ const compiled = compileFormula(formula.source);
997
+ if (compiled.error !== void 0) return `${formula.path}: ${compiled.error}`;
998
+ const unknown = compiled.names.find((item) => !known.has(item));
999
+ if (unknown !== void 0) return `${formula.path}: unknown name ${quoted(unknown)}; known: ${allowed([...known]) || "(none)"}`;
1000
+ const result = evaluate(compiled.tree, lookup);
1001
+ if (result.error !== void 0) return `${formula.path}: ${result.error}`;
1002
+ const problem = kindProblem(result.value, formula.kind);
1003
+ if (problem !== void 0) return `${formula.path}: the formula gives ${quoted(result.value)}; ${problem}`;
1004
+ }
1005
+ }
1006
+ function kindProblem(value, kind) {
1007
+ if (kind === "number") return isNumber(value) ? void 0 : "expected a number";
1008
+ if (Array.isArray(kind)) return kind.includes(value) ? void 0 : `expected one of ${allowed(kind)}`;
1009
+ if (kind === "string") return isScalar(value) ? void 0 : "expected text";
1010
+ return isScalar(value) ? void 0 : "expected a string or a number";
1011
+ }
1012
+ function cellText(value) {
1013
+ if (typeof value === "string") return value;
1014
+ const text = JSON.stringify(value);
1015
+ return typeof text === "string" ? text : "";
1016
+ }
1017
+ const compiled = /* @__PURE__ */ new WeakMap();
1018
+ const treeOf = (formula) => {
1019
+ let entry = compiled.get(formula);
1020
+ if (entry === void 0) {
1021
+ entry = compileFormula(formula["="]);
1022
+ compiled.set(formula, entry);
1023
+ }
1024
+ return entry;
1025
+ };
1026
+ const choiceValue = (option) => option?.value ?? option?.label;
1027
+ function walk(node, visit) {
1028
+ if (!isRecord(node)) return;
1029
+ visit(node);
1030
+ if (LAYOUT.includes(node.type) && Array.isArray(node.children)) for (const child of node.children) walk(child, visit);
1031
+ }
1032
+ function viewInputs(view) {
1033
+ const found = [];
1034
+ walk(view, (node) => {
1035
+ if (INPUTS.has(node.type)) found.push(node);
1036
+ });
1037
+ return found;
1038
+ }
1039
+ function inputDefault(node) {
1040
+ if (node.type === "toggle") return node.value === true;
1041
+ if (node.type === "choice") return node.value ?? choiceValue(Array.isArray(node.options) ? node.options[0] : void 0);
1042
+ return node.value;
1043
+ }
1044
+ function viewScope(view, values = {}) {
1045
+ const inputs = /* @__PURE__ */ new Map();
1046
+ for (const node of viewInputs(view)) inputs.set(node.id, Object.hasOwn(values, node.id) ? values[node.id] : inputDefault(node));
1047
+ const lets = /* @__PURE__ */ new Map();
1048
+ for (const [name, source] of Object.entries(isRecord(view?.let) ? view.let : {})) {
1049
+ const entry = compileFormula(source);
1050
+ if (entry.tree !== void 0) lets.set(name, entry.tree);
1051
+ }
1052
+ return createScope(inputs, lets);
1053
+ }
1054
+ function resolve(value, lookup, fallback = void 0) {
1055
+ if (!isFormula(value)) return value;
1056
+ const entry = treeOf(value);
1057
+ if (entry.tree === void 0) return fallback;
1058
+ const result = evaluate(entry.tree, lookup);
1059
+ return result.error === void 0 ? result.value : fallback;
1060
+ }
1061
+ const asText = (value) => value === void 0 || value === null ? "" : show(value);
1062
+ function clip(text) {
1063
+ const flat = String(text).replace(/\s+/g, " ").trim();
1064
+ return flat.length <= SUMMARY_PART ? flat : flat.slice(0, SUMMARY_PART);
1065
+ }
1066
+ const choiceLabel = (node, value) => (Array.isArray(node.options) ? node.options : []).find((option) => choiceValue(option) === value)?.label ?? asText(value);
1067
+ function inputText(node, value) {
1068
+ if (node.type === "toggle") return value === true ? "on" : "off";
1069
+ if (node.type === "choice") return choiceLabel(node, value);
1070
+ return `${asText(value)}${node.unit ? ` ${node.unit}` : ""}`;
1071
+ }
1072
+ function progressPercent(node, lookup) {
1073
+ if (node.percent !== void 0) return Number(resolve(node.percent, lookup, 0));
1074
+ const max = Number(resolve(node.max, lookup, 0));
1075
+ return max > 0 ? Number(resolve(node.value, lookup, 0)) / max * 100 : 0;
1076
+ }
1077
+ function componentSummary(node, lookup) {
1078
+ const value = (item) => asText(resolve(item, lookup, ""));
1079
+ switch (node.type) {
1080
+ case "text": return `text: ${value(node.text)}`;
1081
+ case "metric": return `metric: ${node.label}=${value(node.value)}`;
1082
+ case "table": return `table: ${(Array.isArray(node.columns) ? node.columns : []).map((column) => column?.label ?? column?.key ?? "").filter(Boolean).join("/")}`;
1083
+ case "chart": {
1084
+ const series = Array.isArray(node.series) ? node.series[0] : void 0;
1085
+ const values = Array.isArray(series?.values) ? series.values : [];
1086
+ const unit = node.unit ? ` ${node.unit}` : "";
1087
+ return `chart: ${(Array.isArray(node.categories) ? node.categories : []).map((label, index) => `${label}=${value(values[index])}${unit}`).join(", ")}`;
1088
+ }
1089
+ case "progress": return `progress: ${node.label ? `${node.label} ` : ""}${Math.round(progressPercent(node, lookup))}%`;
1090
+ case "callout": return `callout: ${value(node.text)}`;
1091
+ case "sources": return `sources: ${(Array.isArray(node.items) ? node.items : []).map((item) => item?.label).filter(Boolean).join("/")}`;
1092
+ case "slider":
1093
+ case "number":
1094
+ case "choice":
1095
+ case "toggle": return `${node.type}: ${node.label}=${inputText(node, inputDefault(node))}`;
1096
+ case "options": return `options: ${(Array.isArray(node.options) ? node.options : []).map((option) => option?.label).filter(Boolean).join("/")}`;
1097
+ case "confirm": return "confirm";
1098
+ case "form": return `form: ${(Array.isArray(node.fields) ? node.fields : []).map((field) => field?.label ?? field?.id).filter(Boolean).join("/")}`;
1099
+ case "submit": return `submit: ${node.label ?? "Send"}`;
1100
+ default: return;
1101
+ }
1102
+ }
1103
+ function viewSummary(view) {
1104
+ const parts = [];
1105
+ const lookup = viewScope(view);
1106
+ const visit = (node) => {
1107
+ if (parts.length >= SUMMARY_COUNT || !isRecord(node)) return;
1108
+ const line = componentSummary(node, lookup);
1109
+ if (line !== void 0) parts.push(clip(line));
1110
+ if (parts.length >= SUMMARY_COUNT || !LAYOUT.includes(node.type) || !Array.isArray(node.children)) return;
1111
+ for (const child of node.children) visit(child);
1112
+ };
1113
+ visit(view);
1114
+ return parts.join("; ");
1115
+ }
1116
+ function inputsSummary(view, inputs) {
1117
+ if (!isRecord(inputs)) return "";
1118
+ return viewInputs(view).filter((node) => Object.hasOwn(inputs, node.id)).map((node) => `${node.label || node.id}=${inputText(node, inputs[node.id])}`).join("; ");
1119
+ }
1120
+ function interactiveNodes(node, found = []) {
1121
+ walk(node, (item) => {
1122
+ if (INTERACTIVE.has(item.type)) found.push(item);
1123
+ });
1124
+ return found;
1125
+ }
1126
+ function validateViewAnswer(view, action, values) {
1127
+ const nodes = interactiveNodes(view);
1128
+ if (typeof action !== "string" || action === "") return { error: `action: expected an interactive component id; allowed: ${allowed(nodes.map((node) => node.id))}` };
1129
+ const node = nodes.find((item) => item.id === action);
1130
+ if (node === void 0) return { error: `action: no interactive component with id ${quoted(action)}; allowed: ${allowed(nodes.map((item) => item.id))}` };
1131
+ if (!isRecord(values)) return { error: "values: expected an object" };
1132
+ const inputs = validateInputs(view, values.inputs);
1133
+ if (inputs.error !== void 0) return inputs;
1134
+ const extra = inputs.inputs === void 0 ? {} : { inputs: inputs.inputs };
1135
+ if (node.type === "submit") return { ...extra };
1136
+ const own = node.type === "options" ? validateOptions(node, values) : node.type === "confirm" ? validateConfirm(values) : validateForm(node, values);
1137
+ return own.error === void 0 ? {
1138
+ ...own,
1139
+ ...extra
1140
+ } : own;
1141
+ }
1142
+ function validateInputs(view, sent) {
1143
+ const nodes = viewInputs(view);
1144
+ if (nodes.length === 0) return sent === void 0 ? {} : { error: "values.inputs: this view has no inputs" };
1145
+ if (sent !== void 0 && !isRecord(sent)) return { error: "values.inputs: expected an object of input ids to values" };
1146
+ const given = sent ?? {};
1147
+ const ids = nodes.map((node) => node.id);
1148
+ const unknown = Object.keys(given).find((key) => !ids.includes(key));
1149
+ if (unknown !== void 0) return { error: `values.inputs.${unknown}: unknown input; allowed: ${allowed(ids)}` };
1150
+ const inputs = {};
1151
+ for (const node of nodes) {
1152
+ const value = Object.hasOwn(given, node.id) ? given[node.id] : inputDefault(node);
1153
+ const at = `values.inputs.${node.id}`;
1154
+ if (node.type === "toggle" && typeof value !== "boolean") return { error: `${at}: expected true or false` };
1155
+ if (node.type === "choice" && !(Array.isArray(node.options) ? node.options : []).some((option) => choiceValue(option) === value)) return { error: `${at}: expected one of the options' values` };
1156
+ if (node.type === "slider" || node.type === "number") {
1157
+ if (!isNumber(value)) return { error: `${at}: expected a number` };
1158
+ if (node.min !== void 0 && value < node.min || node.max !== void 0 && value > node.max) return { error: `${at}: expected a number from ${node.min ?? "-∞"} to ${node.max ?? "∞"}` };
1159
+ }
1160
+ inputs[node.id] = value;
1161
+ }
1162
+ return { inputs };
1163
+ }
1164
+ function validateOptions(node, values) {
1165
+ if (!Array.isArray(values.selected)) return { error: "values.selected: expected an array of option labels" };
1166
+ const labels = new Set((node.options ?? []).map((option) => option?.label));
1167
+ const picked = values.selected.filter((label) => typeof label === "string" && labels.has(label));
1168
+ const selected = node.multi === true ? [...new Set(picked)] : picked.slice(0, 1);
1169
+ if (selected.length === 0) return { error: "values.selected: select at least one option" };
1170
+ return { selected };
1171
+ }
1172
+ function validateConfirm(values) {
1173
+ if (typeof values.confirmed !== "boolean") return { error: "values.confirmed: expected a boolean" };
1174
+ return { confirmed: values.confirmed };
1175
+ }
1176
+ function validateForm(node, values) {
1177
+ if (!isRecord(values.fields)) return { error: "values.fields: expected an object of field ids to strings" };
1178
+ const declared = (node.fields ?? []).map((field) => field?.id).filter((id) => typeof id === "string");
1179
+ const allowedIds = new Set(declared);
1180
+ const fields = {};
1181
+ for (const [key, value] of Object.entries(values.fields)) {
1182
+ if (!allowedIds.has(key)) return { error: `values.fields.${key}: unknown field; allowed: ${allowed(declared)}` };
1183
+ if (typeof value !== "string") return { error: `values.fields.${key}: expected a string` };
1184
+ if (value.length > MAX_TEXT) return { error: `values.fields.${key}: ${value.length} characters exceeds the limit of ${MAX_TEXT}` };
1185
+ fields[key] = value;
1186
+ }
1187
+ return { fields };
1188
+ }
1189
+ //#endregion
1190
+ //#region src/answer-text.ts
1191
+ function controls(view) {
1192
+ return [
1193
+ "options",
1194
+ "confirm",
1195
+ "form",
1196
+ "submit"
1197
+ ].includes(view.type) ? [view] : (view.children ?? []).flatMap(controls);
1198
+ }
1199
+ function answerText(card, action, values) {
1200
+ const node = controls(card.view).find((node) => node.id === action);
1201
+ const lines = [card.title || "Deep View"];
1202
+ if (Array.isArray(values.selected)) lines.push(values.selected.map(String).join(", "));
1203
+ if (typeof values.confirmed === "boolean") lines.push(values.confirmed ? node?.confirmLabel ?? "Confirm" : node?.cancelLabel ?? "Cancel");
1204
+ if (isRecord$1(values.fields)) for (const field of node?.fields ?? []) lines.push(`${field.label}: ${String(values.fields[field.id] ?? "")}`);
1205
+ const inputs = inputsSummary(card.view, values.inputs);
1206
+ if (inputs) lines.push(inputs);
1207
+ if (node?.type === "submit") lines.push(node.label ?? "Submit");
1208
+ return lines.join("\n");
1209
+ }
1210
+ //#endregion
1211
+ //#region src/index.ts
1212
+ const name = "deep-view";
1213
+ const inject = [
1214
+ "tools",
1215
+ "sessionController",
1216
+ "sessions",
1217
+ "agents",
1218
+ "connection",
1219
+ "webServer"
1220
+ ];
1221
+ const isLiveRoot = (host, agent) => agent !== void 0 && host.agents.roots().some((root) => root.id === agent.id);
1222
+ const ok = (value) => ({
1223
+ ok: true,
1224
+ value
1225
+ });
1226
+ const fail = (code, message) => ({
1227
+ ok: false,
1228
+ error: {
1229
+ code,
1230
+ message
1231
+ }
1232
+ });
1233
+ const short = (value, max = 160) => typeof value === "string" && value.length > 0 && value.length <= max;
1234
+ const checkedShown = (title, view) => checkShownView(title, view);
1235
+ const isShown = (value) => !("error" in value);
1236
+ function presentation(args, value) {
1237
+ if (!isRecord$1(value) || value.owner !== "dsh-deep-view" || !["shown", "pending"].includes(String(value.status))) return void 0;
1238
+ const shown = checkedShown(isRecord$1(args) ? args.title : void 0, isRecord$1(args) ? args.view : void 0);
1239
+ if (!isShown(shown)) return void 0;
1240
+ return {
1241
+ owner: VIEW_OWNER,
1242
+ ...short(value.viewId, 80) ? { viewId: value.viewId } : {},
1243
+ ...shown,
1244
+ awaitReply: value.awaitReply === true
1245
+ };
1246
+ }
1247
+ function createShowViewTool(host, pending, makeId = randomUUID) {
1248
+ return {
1249
+ name: "show_view",
1250
+ description: [
1251
+ "Attach one card below your final reply when a visual or interactive form beats prose: compared numbers, a trend, a share, progress; an estimate with user-adjustable inputs; or a choice the user answers afterwards (use ask_user_question when you cannot continue without it).",
1252
+ "Prefer plain Markdown otherwise. Call once, then finish the reply; the card is not yet visible when this tool returns.",
1253
+ "Never end on the call, wait or poll, repeat the card JSON, or claim the user saw it. Answers accept one reply, sent as the next user message. Never collect keys, tokens or passwords."
1254
+ ].join(" "),
1255
+ parameters: {
1256
+ type: "object",
1257
+ properties: {
1258
+ title: {
1259
+ type: "string",
1260
+ description: "Short heading above the card."
1261
+ },
1262
+ view: {
1263
+ type: "object",
1264
+ additionalProperties: true,
1265
+ description: [
1266
+ "One ViewNode, nodes nest under stack|row(children,let?).",
1267
+ "Display: text,metric{label,value},table{columns:[{key,label}],rows:[[cell]]},chart{kind:bar|hbar|line|area|donut,categories,series:[{name?,values}]},progress{percent|value+max},callout{text},sources{items:[{label,url}]}.",
1268
+ "Inputs: slider,number,choice,toggle{id,label,value}. A {\"=\":\"expr\"} formula replaces a value or cell, reading input ids and let names.",
1269
+ "Answers(need await_reply true): options{id,options,multi?},confirm{id},form{id,fields},submit{id,label}. No html."
1270
+ ].join(" ")
1271
+ },
1272
+ await_reply: {
1273
+ type: "boolean",
1274
+ description: "Whether this card asks the user for a response."
1275
+ }
1276
+ },
1277
+ required: ["view"]
1278
+ },
1279
+ output: {
1280
+ schema: {
1281
+ type: "object",
1282
+ properties: {
1283
+ owner: {
1284
+ type: "string",
1285
+ const: VIEW_OWNER
1286
+ },
1287
+ status: { type: "string" },
1288
+ viewId: { type: "string" },
1289
+ awaitReply: { type: "boolean" },
1290
+ summary: { type: "string" }
1291
+ },
1292
+ required: [
1293
+ "owner",
1294
+ "status",
1295
+ "awaitReply",
1296
+ "summary"
1297
+ ],
1298
+ additionalProperties: false
1299
+ },
1300
+ render: (_args, value) => [{
1301
+ type: "text",
1302
+ text: String(value.summary ?? "")
1303
+ }],
1304
+ presentationMeta: presentation
1305
+ },
1306
+ async execute(args, exec) {
1307
+ const input = isRecord$1(args) ? args : {};
1308
+ const shown = checkedShown(input.title, input.view);
1309
+ if (!isShown(shown)) return {
1310
+ owner: VIEW_OWNER,
1311
+ status: "invalid",
1312
+ awaitReply: false,
1313
+ summary: shown.error
1314
+ };
1315
+ if (typeof input.await_reply !== "undefined" && typeof input.await_reply !== "boolean") return {
1316
+ owner: VIEW_OWNER,
1317
+ status: "invalid",
1318
+ awaitReply: false,
1319
+ summary: "await_reply: expected true or false"
1320
+ };
1321
+ const awaitReply = input.await_reply ?? shown.interactive;
1322
+ if (awaitReply !== shown.interactive) return {
1323
+ owner: VIEW_OWNER,
1324
+ status: "invalid",
1325
+ awaitReply: false,
1326
+ summary: shown.interactive ? "This card has answer controls; omit await_reply or set it true." : "A display-only card cannot wait for an answer; omit await_reply or set it false."
1327
+ };
1328
+ const id = exec.agent?.id;
1329
+ const agent = id === void 0 ? void 0 : host.agents.get(id);
1330
+ if (awaitReply && !isLiveRoot(host, agent)) return {
1331
+ owner: VIEW_OWNER,
1332
+ status: "invalid",
1333
+ awaitReply: false,
1334
+ summary: "An interactive view only works in a live root session. Put the question or decision in your response instead."
1335
+ };
1336
+ let viewId;
1337
+ if (awaitReply && agent) {
1338
+ viewId = makeId();
1339
+ pending.set(agent.id, {
1340
+ id: viewId,
1341
+ sessionId: agent.id,
1342
+ ...exec.callId ? { callId: exec.callId } : {},
1343
+ ...shown
1344
+ });
1345
+ }
1346
+ return {
1347
+ owner: VIEW_OWNER,
1348
+ status: awaitReply ? "pending" : "shown",
1349
+ ...viewId ? { viewId } : {},
1350
+ awaitReply,
1351
+ summary: `Card prepared for the final reply. Finish with a concise final answer; do not call this tool again for the same card.${awaitReply ? " The user can answer the card after your reply finishes; do not wait or poll." : ""} ${viewSummary(shown.view)}`
1352
+ };
1353
+ },
1354
+ isConcurrencySafe: () => false
1355
+ };
1356
+ }
1357
+ function createRpc(host, pending) {
1358
+ const claims = /* @__PURE__ */ new Set();
1359
+ return async (endpoint, payload, signal) => {
1360
+ if (!isRecord$1(payload) || !short(payload.sessionId, 200)) return fail("bad-request", "A bounded sessionId is required");
1361
+ const sessionId = payload.sessionId;
1362
+ const record = pending.get(sessionId);
1363
+ if (endpoint === "pending") return ok(record?.callId === payload.callId || payload.callId === void 0 ? record ?? null : null);
1364
+ if (endpoint === "dismiss") {
1365
+ if (record?.id === payload.viewId) pending.delete(sessionId);
1366
+ return ok(true);
1367
+ }
1368
+ if (endpoint !== "reply") return fail("not-found", "Unknown Deep View endpoint");
1369
+ if (!record || !short(payload.viewId, 80) || payload.viewId !== record.id || !short(payload.action, 160) || !isRecord$1(payload.values)) return fail("stale", "This view is no longer open");
1370
+ if (claims.has(record.id)) return fail("busy", "This view is already being answered");
1371
+ const checked = validateViewAnswer(record.view, payload.action, payload.values);
1372
+ if (checked.error !== void 0) return fail("invalid", checked.error);
1373
+ claims.add(record.id);
1374
+ pending.delete(sessionId);
1375
+ let appended = false;
1376
+ try {
1377
+ if (signal.aborted) return fail("cancelled", "The request was cancelled");
1378
+ const resolved = await host.sessionController.resolveAgent(sessionId);
1379
+ const agent = resolved.agent;
1380
+ if (!agent || host.agents.get(agent.id) !== agent || !host.agents.roots().some((root) => root.id === agent.id)) throw new Error(resolved.error?.message ?? "The session is not a live root session");
1381
+ if (signal.aborted) return fail("cancelled", "The request was cancelled");
1382
+ const deepView = {
1383
+ owner: VIEW_OWNER,
1384
+ version: 1,
1385
+ answer: {
1386
+ id: record.id,
1387
+ action: payload.action,
1388
+ values: checked
1389
+ }
1390
+ };
1391
+ agent.followup(createUserMessage({
1392
+ content: [{
1393
+ type: "text",
1394
+ text: answerText(record, payload.action, checked)
1395
+ }],
1396
+ source: {
1397
+ kind: "user",
1398
+ deepView
1399
+ }
1400
+ }));
1401
+ appended = true;
1402
+ await host.sessions.flush(agent.session);
1403
+ return ok({ answered: true });
1404
+ } catch (error) {
1405
+ return fail("internal", error instanceof Error ? error.message : String(error));
1406
+ } finally {
1407
+ if (!appended && !pending.has(sessionId)) pending.set(sessionId, record);
1408
+ claims.delete(record.id);
1409
+ }
1410
+ };
1411
+ }
1412
+ async function apply(ctx) {
1413
+ const host = ctx;
1414
+ const pending = /* @__PURE__ */ new Map();
1415
+ ctx.effect(() => {
1416
+ if (host.tools.get("show_view")) {
1417
+ host.logger?.warn("deep-view: show_view is already registered by another plugin; this bundle stays disabled for its own tool");
1418
+ return () => {};
1419
+ }
1420
+ try {
1421
+ return host.tools.register(createShowViewTool(host, pending));
1422
+ } catch (error) {
1423
+ host.logger?.warn("deep-view: show_view registration skipped: %s", error instanceof Error ? error.message : String(error));
1424
+ return () => {};
1425
+ }
1426
+ }, "deep-view.tool");
1427
+ ctx.effect(() => ctx.on("session/event", (session, event) => {
1428
+ if (event.type !== "user/message" || !isRecord$1(event.data) || !isRecord$1(event.data.source) || event.data.source.kind !== "user") return;
1429
+ pending.delete(session.id);
1430
+ }), "deep-view.close-pending");
1431
+ ctx.effect(() => registerHostRpc(host, RPC_CHANNEL, createRpc(host, pending), { maxBodyBytes: 24576 }), "deep-view.rpc");
1432
+ }
1433
+ //#endregion
1434
+ export { apply, createRpc, createShowViewTool, inject, name };
1435
+
1436
+ //# sourceMappingURL=index.js.map