volaro 0.1.0-alpha.12 → 0.1.0-alpha.13

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Volaro
2
2
 
3
- **Limited alpha** (this is `0.1.0-alpha.12`), published under both the
3
+ **Limited alpha** (this is `0.1.0-alpha.13`), published under both the
4
4
  `latest` and `alpha` dist-tags: `npm install volaro`.
5
5
 
6
6
  Volaro is an experimental application language intended for AI authoring and
@@ -1,6 +1,6 @@
1
1
  {
2
- "revision": "92972bae88b0cd71b621a8c21dbeae052dc8a3c9",
2
+ "revision": "d6e19b4e656bee01bf90048117fb24b598abdf3b",
3
3
  "source": "git",
4
4
  "dirty": false,
5
- "content_sha256": "09ae1f5782b061471ba112a8820cd9a08f4519ecff84eb418a5062f8ed23c467"
5
+ "content_sha256": "64c762006f1fceda3601d6c6b3a6de4ba99590d93aa14d04a749f318a840aa17"
6
6
  }
@@ -1 +1 @@
1
- 92972bae88b0cd71b621a8c21dbeae052dc8a3c9
1
+ d6e19b4e656bee01bf90048117fb24b598abdf3b
@@ -24,7 +24,7 @@ from .checks import check
24
24
  from .diagnostics import LineIndex, Severity
25
25
  from .lexer import Lexer
26
26
  from .parser import Parser
27
- from .resolve import resolve
27
+ from .resolve import resolve, resolve_page_server
28
28
  from .project_config import load_config, ConfigError
29
29
  from .diagnostics import Diagnostic
30
30
  from .layoutcompose import check_all as check_project_composition
@@ -59,21 +59,25 @@ def check_source(src: str, filename: str, strict_names: bool = False,
59
59
  # (spec §10.2): handlers and an `errors` block, no module items. Every
60
60
  # other basename parses as an ordinary module.
61
61
  base = os.path.basename(filename)
62
+ resolver = resolve
62
63
  if base in ("page.server.vl", "_server.vl"):
63
64
  if base == "page.server.vl":
64
65
  mod = parser.parse_page_server()
66
+ # Handler bodies resolve like service route bodies, against the
67
+ # project's app.vl, the handler's params and the `_server.vl`
68
+ # context providers above the file (resolve.resolve_page_server).
69
+ resolver = resolve_page_server
65
70
  else:
66
71
  mod = parser.parse_server_ctx()
67
- # The resolver walks module items (services, views, use graphs); a
68
- # page-server file has none of those, so skip it here. Folder-param
69
- # matching against the route manifest is a later stage.
70
- do_resolve = False
72
+ # A `_server.vl` holds only `context` providers; the resolver
73
+ # has no pass for them yet.
74
+ do_resolve = False
71
75
  else:
72
76
  mod = parser.parse_module()
73
77
  diags = (list(lx.diags) + list(parser.diags)
74
78
  + check(mod, filename, li, strict_names=strict_names, release=release))
75
79
  if do_resolve:
76
- diags += resolve(mod, filename, li)
80
+ diags += resolver(mod, filename, li)
77
81
  diags.sort(key=lambda d: (d.start, 0 if d.severity == Severity.ERROR else 1, d.code))
78
82
  return li, diags
79
83
 
@@ -105,12 +105,13 @@ _METHOD_FN_FORM = {
105
105
  "matches": "matches", "match": "matches", "test": "matches",
106
106
  "first_char": "first_char", "split": "split", "join": "join",
107
107
  "filter": "filter", "map": "map", "take": "take", "count": "count",
108
+ "some": "any", "every": "all",
108
109
  "sum": "sum", "sort_by": "sort_by", "group_by": "group_by",
109
110
  "chunk": "chunk", "concat": "concat", "append": "append", "push": "append",
110
111
  "keys": "keys", "values": "values", "get_or": "get_or",
111
112
  }
112
113
  _LIST_FNS = {"filter", "map", "take", "count", "sum", "sort_by", "group_by",
113
- "chunk", "concat", "append", "join"}
114
+ "chunk", "concat", "append", "join", "any", "all"}
114
115
  # Other common carry-overs with a Volaro spelling that is not a function call.
115
116
  _METHOD_OTHER_FIX = {
116
117
  "length": "`{r}.len()`", "size": "`{r}.len()`",
@@ -153,6 +154,8 @@ STD_BUILT_FNS = frozenset({
153
154
  "concat", "append",
154
155
  # `unique(xs)`: first occurrence of each distinct value, in order
155
156
  "unique",
157
+ # `any(xs, f)` / `all(xs, f)`: whether some / every item passes `f`
158
+ "any", "all",
156
159
  # std.str (spec section 13: split, join, trim, case, replace, contains,
157
160
  # regex -- `matches`)
158
161
  "join", "split", "first_char", "trim", "upper", "lower", "replace",
@@ -182,9 +185,7 @@ _FN_NOT_BUILT = {
182
185
  "index_of": "a `for i in 0..xs.len()` loop that compares `xs[i]`",
183
186
  "slice": "take(xs, n) for a prefix, or a `for i in a..b` loop",
184
187
  "range": "a `for i in 0..n` loop",
185
- "any": "count(filter(xs, f)) > 0",
186
- "all": "count(filter(xs, f)) == count(xs)",
187
- "none": "count(filter(xs, f)) == 0",
188
+ "none": "not any(xs, f)",
188
189
  "avg": "sum(xs) / count(xs) (check count(xs) > 0 first)",
189
190
  "min": "if (a < b) a else b",
190
191
  "max": "if (a > b) a else b",
@@ -235,7 +236,7 @@ CONTEXTUAL_WORDS = frozenset({"asc", "desc", "google", "github"})
235
236
  _WHERE_PREDICATE_FIX = {
236
237
  "lt": "`a < b`", "lte": "`a <= b`", "gt": "`a > b`", "gte": "`a >= b`",
237
238
  "ne": "`a != b`", "between": "`lo <= x and x <= hi`",
238
- "any_of": "`count(filter(xs, fn(v) v == x)) > 0`",
239
+ "any_of": "`any(xs, fn(v) v == x)`",
239
240
  }
240
241
  # Error codes (spec section 6): a code is constructed only as the argument of
241
242
  # `err(...)` in a service or page-server route body, where the build turns
@@ -246,6 +247,20 @@ _ERROR_CODES = frozenset({
246
247
  "out_of_range", "decode", "invalid_credentials", "unverified_email",
247
248
  "totp_required", "rate_limited",
248
249
  })
250
+ # The codes the compiler-owned auth endpoints fail with (`catch
251
+ # weak_password`); vlbuild's emitter reads this set.
252
+ AUTH_ERROR_CODES = frozenset({
253
+ "email_taken", "weak_password", "breached_password", "rate_limited",
254
+ "invalid_credentials", "unverified_email", "totp_required",
255
+ "provider_error", "invalid_token", "expired_token", "invalid_code",
256
+ "no_totp_enrolled",
257
+ })
258
+ # A route body's `catch <head>` tests the thrown error's code when <head> is
259
+ # one of these (or a declared union variant); any other head binds the
260
+ # caught error itself (`catch e`). resolve.py and vlbuild's `_route_try`
261
+ # both read this set, so `err(e)` means the same thing to check and build.
262
+ ROUTE_CATCH_CODES = (_ERROR_CODES | AUTH_ERROR_CODES
263
+ | frozenset({"bad_request", "unauthorized"}))
249
264
  _ERR_ROUTE_FIX = ("`return err(not_found(\"...\"))` in a `service` or "
250
265
  "page.server route body; a view reports a failure by "
251
266
  "setting its own state")
@@ -50,11 +50,12 @@ import os
50
50
  import re
51
51
 
52
52
  from . import ast_nodes as A
53
- from .checks import (BUILTINS, CONTEXTUAL_WORDS, STD_BUILT_FNS, _FN_NOT_BUILT,
54
- _PRIM_NAMES)
53
+ from .checks import (BUILTINS, CONTEXTUAL_WORDS, ROUTE_CATCH_CODES,
54
+ STD_BUILT_FNS, _ERROR_CODES, _FN_NOT_BUILT, _PRIM_NAMES)
55
55
  from .diagnostics import Diagnostic, LineIndex, Severity
56
56
  from .elements import ELEMENT_NAMES
57
57
  from .lexer import Lexer
58
+ from .pageroutes import _provider_chain
58
59
  from .parser import Parser
59
60
 
60
61
  # std free-function names: the ones this build lowers and the ones it rejects
@@ -190,6 +191,13 @@ _METHODS = {
190
191
  _KNOWN = (set(BUILTINS) | set(_PRIM_NAMES) | _STD_EXTRA | _CONTEXTUAL
191
192
  | {"true", "false", "nil", "self", "_", "?"})
192
193
 
194
+ # The type-info a `catch e` binding carries: the caught error itself, the
195
+ # one bare name `err(...)` accepts in a route body (`err(e)` rethrows it).
196
+ _CAUGHT = ("!caught", False)
197
+ _ERR_ARG_FIX = ('an error code call `err(conflict("..."))`, a variant of an '
198
+ 'error union you declare `err(invalid_field("title"))`, or '
199
+ 'the error a `catch e` arm caught: `err(e)`')
200
+
193
201
 
194
202
  class Sym:
195
203
  __slots__ = ("kind", "params", "fields", "variants", "ret", "routes",
@@ -323,6 +331,9 @@ class Resolver:
323
331
  self._in_view = False
324
332
  self._view_name = ""
325
333
  self._vmf_seen: set[str] = set()
334
+ # True while walking a service route or page.server handler body,
335
+ # the only places the build lowers `err(...)` (E-ERR-ARG).
336
+ self._in_route = False
326
337
  # Spec §7: `_name` is private to its file. A bare `use ./x` does not
327
338
  # bind x's `_` names; remember which module declared each one so a
328
339
  # use of it reports E-IMPORT-PRIVATE instead of a bare R-UNDEF.
@@ -534,7 +545,47 @@ class Resolver:
534
545
  for seg in r.path.split("/"):
535
546
  if seg.startswith(":"):
536
547
  sc[seg[1:]] = None
548
+ self._in_route = True
537
549
  self._scoped(r.body, [sc])
550
+ self._in_route = False
551
+
552
+ def run_page_server(self, mod: A.PageServerFile) -> list[Diagnostic]:
553
+ """Resolve a `page.server.vl` file (spec section 10.2) the way a
554
+ service route body is resolved. Before, `check_source` skipped the
555
+ resolver for these files, so an undefined name, a wrong arity or an
556
+ unknown field in a handler passed `volaro check`.
557
+
558
+ In scope: the names the build gives the handler -- the project's
559
+ `app.vl` declarations when it has one (its models, types, unions and
560
+ `api` clients, which the single-entry project build lowers the
561
+ handlers against), the handler's own parameters, `session`,
562
+ `request`, and every `_server.vl` context provider on the file's
563
+ directory path."""
564
+ root = _project_root(self.filename)
565
+ providers = []
566
+ if root is not None:
567
+ entry = os.path.join(root, "app.vl")
568
+ if os.path.isfile(entry):
569
+ self.table.update(_collect_module(entry, self._loading))
570
+ # its parse diagnostics belong to the _server.vl files, which
571
+ # `volaro check` reports on their own
572
+ providers, _diags = _provider_chain(
573
+ os.path.join(root, "app"),
574
+ os.path.dirname(os.path.abspath(self.filename)))
575
+ for h in mod.handlers:
576
+ sc = {p.name: _tref(p.type)[:2] for p in h.params}
577
+ sc["session"] = None
578
+ sc["request"] = None
579
+ for prov in providers:
580
+ sc.setdefault(prov.name, _tref(prov.type)[:2])
581
+ guard = getattr(h, "guard", None)
582
+ if guard is not None and guard.kind == "owner" and isinstance(
583
+ guard.detail, A.Node):
584
+ self._expr(guard.detail, [sc])
585
+ self._in_route = True
586
+ self._scoped(h.body, [sc])
587
+ self._in_route = False
588
+ return self.diags
538
589
 
539
590
  def _load_type(self, s: A.LoadDecl, scopes):
540
591
  if isinstance(s.expr, A.Call):
@@ -603,8 +654,7 @@ class Resolver:
603
654
  elif isinstance(st, A.Try):
604
655
  self._expr(st.expr, scopes)
605
656
  for pat, body in st.catches:
606
- inner = scopes + [{b: None for b in getattr(pat, "binds", [])}]
607
- self._scoped(body, inner)
657
+ self._scoped(body, scopes + [self._catch_scope(pat)])
608
658
  elif isinstance(st, (A.Break, A.Skip)):
609
659
  pass
610
660
  elif isinstance(st, A.StateDecl):
@@ -729,6 +779,7 @@ class Resolver:
729
779
  self._expr(v, scopes)
730
780
  self._check_call(ex, scopes)
731
781
  self._check_view_module_fn(ex, scopes)
782
+ self._check_err_arg(ex, scopes)
732
783
  elif isinstance(ex, A.Unary):
733
784
  self._expr(ex.operand, scopes)
734
785
  elif isinstance(ex, A.Binary):
@@ -751,8 +802,7 @@ class Resolver:
751
802
  # `vlbuild` refused it (Testing Lab 2026-09-27, volaro-F2).
752
803
  self._expr(ex.expr, scopes)
753
804
  for pat, body in ex.catches:
754
- inner = scopes + [{b: None for b in getattr(pat, "binds", [])}]
755
- self._scoped(body, inner)
805
+ self._scoped(body, scopes + [self._catch_scope(pat)])
756
806
  elif isinstance(ex, A.IfExpr):
757
807
  self._expr(ex.cond, scopes)
758
808
  self._expr(ex.then, scopes)
@@ -999,6 +1049,69 @@ class Resolver:
999
1049
  self._arity(Sym("fn", params=[(n, False, not req) for n, req in params]),
1000
1050
  ex, f"'auth.{m}'")
1001
1051
 
1052
+ def _catch_scope(self, pat) -> dict:
1053
+ """The bindings a `catch` arm adds. An arm headed by an error code or
1054
+ a declared union variant binds the variant's fields; any other head
1055
+ (`catch e`) binds the caught error itself, marked `_CAUGHT` so
1056
+ `err(e)` can rethrow it."""
1057
+ head = getattr(pat, "head", "")
1058
+ binds = list(getattr(pat, "binds", []) or [])
1059
+ sym = self.table.get(head)
1060
+ if (head == "_" or head in ROUTE_CATCH_CODES
1061
+ or (sym is not None and sym.kind == "variant")
1062
+ or binds != [head]):
1063
+ return {b: None for b in binds}
1064
+ return {head: _CAUGHT}
1065
+
1066
+ def _check_err_arg(self, ex: A.Call, scopes) -> None:
1067
+ """E-ERR-ARG: in a route body the build lowers `err(X)` only when X
1068
+ is a call to an error code (`conflict("x")`) or to a declared union
1069
+ variant (`invalid_field("title")`), or is the error a `catch e` arm
1070
+ bound (`err(e)` rethrows it). Anything else checked clean and then
1071
+ failed the build ("`err(...)` argument must be a standard error
1072
+ ..."). A view's `err(...)` is checks.py's E-ERR-CONTEXT, and an
1073
+ undefined name is already R-UNDEF."""
1074
+ if not (self._in_route and isinstance(ex.callee, A.Name)
1075
+ and ex.callee.id == "err"):
1076
+ return
1077
+ if any("err" in sc for sc in scopes) or "err" in self.table:
1078
+ return
1079
+ if len(ex.args) != 1 or ex.named:
1080
+ self._d("E-ERR-ARG",
1081
+ "`err(...)` takes exactly one positional argument, the "
1082
+ "error -- the build cannot lower this call", ex,
1083
+ "use " + _ERR_ARG_FIX)
1084
+ return
1085
+ arg = ex.args[0]
1086
+ if isinstance(arg, A.Call) and isinstance(arg.callee, A.Name):
1087
+ n = arg.callee.id
1088
+ sym = self.table.get(n)
1089
+ if any(n in sc for sc in scopes):
1090
+ what = f"'{n}' is a local binding, not an error constructor"
1091
+ elif sym is not None and sym.kind == "variant":
1092
+ return
1093
+ elif sym is None and n in _ERROR_CODES:
1094
+ return
1095
+ elif sym is None and not self._resolves(n, scopes):
1096
+ return # already R-UNDEF
1097
+ else:
1098
+ what = f"'{n}' is not an error code or a declared union variant"
1099
+ elif isinstance(arg, A.Name):
1100
+ if self._typeof(arg, scopes) == _CAUGHT:
1101
+ return
1102
+ if not self._resolves(arg.id, scopes):
1103
+ return # already R-UNDEF
1104
+ what = (f"'{arg.id}' is not an error a `catch` arm caught"
1105
+ + (" -- call the error code: "
1106
+ f"`err({arg.id}(\"...\"))`"
1107
+ if arg.id in _ERROR_CODES else ""))
1108
+ else:
1109
+ what = "the argument is not an error constructor call"
1110
+ self._d("E-ERR-ARG",
1111
+ f"{what}, so `err(...)` cannot be built -- the build lowers "
1112
+ "`err(...)` only for an error code, a declared union variant "
1113
+ "or a caught error", ex, "use " + _ERR_ARG_FIX)
1114
+
1002
1115
  def _check_view_module_fn(self, ex: A.Call, scopes) -> None:
1003
1116
  """A view -- or a closure / `on` handler / block inside one -- that
1004
1117
  calls a module-level `fn` resolves fine, then `vlbuild` refuses it:
@@ -1187,3 +1300,22 @@ def resolve(mod: A.Module, filename: str, li: LineIndex,
1187
1300
  layout_params: dict | None = None) -> list[Diagnostic]:
1188
1301
  return Resolver(filename, li, page_clients=page_clients,
1189
1302
  layout_params=layout_params).run(mod)
1303
+
1304
+
1305
+ def _project_root(filename: str) -> str | None:
1306
+ """The project a page-server file belongs to: the parent of the nearest
1307
+ enclosing `app/` directory, or None for a file outside one."""
1308
+ d = os.path.dirname(os.path.abspath(filename))
1309
+ while True:
1310
+ if os.path.basename(d) == "app":
1311
+ return os.path.dirname(d)
1312
+ parent = os.path.dirname(d)
1313
+ if parent == d:
1314
+ return None
1315
+ d = parent
1316
+
1317
+
1318
+ def resolve_page_server(mod: A.PageServerFile, filename: str,
1319
+ li: LineIndex) -> list[Diagnostic]:
1320
+ """The resolver pass for a `page.server.vl` file (`Resolver.run_page_server`)."""
1321
+ return Resolver(filename, li).run_page_server(mod)
@@ -86,21 +86,43 @@
86
86
  // of VL.std (test_std_fn_parity in tests/test_build.py checks this file,
87
87
  // the minified copy and server.js).
88
88
  function asArray(x) { return x == null ? [] : x; }
89
- // Volaro equality for `unique`: scalars as `==` compares them (`===`), and
90
- // records and lists structurally, equal when their fields or items are
91
- // (spec section 4.3: no identity or reference semantics).
89
+ // Volaro equality (spec section 4.3), used by `==` / `!=` (VL.eq below)
90
+ // and `unique`: scalars compare as `===` does, `nil` is null or undefined
91
+ // (a missing optional field is nil), and records and lists compare
92
+ // structurally, equal when their fields or items are. No identity or
93
+ // reference semantics. Only plain objects (a record literal, a decoded
94
+ // JSON value, a database row) and arrays are compared structurally; any
95
+ // other object compares by identity.
96
+ function isPlain(x) {
97
+ if (Array.isArray(x)) return true;
98
+ var p = Object.getPrototypeOf(x);
99
+ return p === Object.prototype || p === null;
100
+ }
92
101
  function stdEq(a, b) {
93
102
  if (a === b) return true;
94
- if (a === null || b === null || typeof a !== "object" || typeof b !== "object"
95
- || Array.isArray(a) !== Array.isArray(b)) return false;
96
- var ka = Object.keys(a);
97
- if (ka.length !== Object.keys(b).length) return false;
98
- for (var i = 0; i < ka.length; i++) {
99
- if (!Object.prototype.hasOwnProperty.call(b, ka[i])
100
- || !stdEq(a[ka[i]], b[ka[i]])) return false;
103
+ if (a == null || b == null) return a == null && b == null;
104
+ if (typeof a !== "object" || typeof b !== "object"
105
+ || Array.isArray(a) !== Array.isArray(b) || !isPlain(a) || !isPlain(b)) return false;
106
+ var i;
107
+ if (Array.isArray(a)) {
108
+ if (a.length !== b.length) return false;
109
+ for (i = 0; i < a.length; i++) if (!stdEq(a[i], b[i])) return false;
110
+ return true;
111
+ }
112
+ // A field that is nil on one side and absent on the other is equal.
113
+ var ka = Object.keys(a), kb = Object.keys(b);
114
+ for (i = 0; i < ka.length; i++) if (!stdEq(a[ka[i]], b[ka[i]])) return false;
115
+ for (i = 0; i < kb.length; i++) {
116
+ if (!Object.prototype.hasOwnProperty.call(a, kb[i]) && b[kb[i]] != null) return false;
101
117
  }
102
118
  return true;
103
119
  }
120
+ // `a == b` where the build cannot tell both sides are scalars. A scalar
121
+ // pair costs one `===` and a typeof test; records and lists go to stdEq.
122
+ VL.eq = function (a, b) {
123
+ return a === b || (a == null ? b == null
124
+ : b != null && typeof a === "object" && typeof b === "object" && stdEq(a, b));
125
+ };
104
126
  VL.std = {
105
127
  filter: function (xs, f) { return asArray(xs).filter(function (x) { return f(x); }); },
106
128
  map: function (xs, f) { return asArray(xs).map(function (x) { return f(x); }); },
@@ -152,6 +174,11 @@
152
174
  get_or: function (m, k, dflt) {
153
175
  return m && (k in m) ? m[k] : dflt;
154
176
  },
177
+ // `any(xs, f)` / `all(xs, f)`: whether some / every item passes `f`.
178
+ // `any` of an empty list is false, `all` of an empty list is true. Both
179
+ // stop at the first item that settles the answer.
180
+ any: function (xs, f) { return asArray(xs).some(function (x) { return !!f(x); }); },
181
+ all: function (xs, f) { return asArray(xs).every(function (x) { return !!f(x); }); },
155
182
  int_to_float: function (x) { return x; },
156
183
  float_to_int: function (x) { return Math.trunc(x); },
157
184
  // List growth (spec section 20.9) — the only way to combine or grow a
@@ -1,2 +1,2 @@
1
1
  /* minified vlrt.js; readable source in the volaro package */
2
- "use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}function i(e,t){if(e===t)return!0;if(null===e||null===t||"object"!=typeof e||"object"!=typeof t||Array.isArray(e)!==Array.isArray(t))return!1;var n=Object.keys(e);if(n.length!==Object.keys(t).length)return!1;for(var r=0;r<n.length;r++)if(!Object.prototype.hasOwnProperty.call(t,n[r])||!i(e[n[r]],t[n[r]]))return!1;return!0}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var a=r[i.op];t=t.filter(function(e){return a(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},contains:function(e,t){return-1!==String(e).indexOf(String(t))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},unique:function(e){var t=[],n=new Set,o=[];return r(e).forEach(function(e){if(null!==e&&"object"==typeof e){for(var r=0;r<o.length;r++)if(i(o[r],e))return;o.push(e)}else if(e==e){if(n.has(e))return;n.add(e)}t.push(e)}),t},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var a in i)if(Object.prototype.hasOwnProperty.call(i,a)){if(void 0===n[a])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+a);i[a]&&"object"==typeof i[a]&&e(n[a],i[a],o+"/"+a)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,a){var l,u,s,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(l=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(l))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!a)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!s.ok){var v=null;try{v=await s.json()}catch(e){}if(a){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+s.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+s.status),g=v.error.fields;throw y.fields=g&&"object"==typeof g?g:{},y.kind=a.indexOf(p)>=0?"domain":s.status>=500?"internal":"framework",y}var m=v&&v.error?v.error:404===s.status?"not_found":"http."+s.status,b=t.err(m,n+" "+r+" -> "+s.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===s.status?null:s.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var o=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function a(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function l(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){l(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function u(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function s(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function c(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function d(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function f(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function v(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function h(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),p(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function p(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return y(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=h(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function y(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),a=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):a.push(e)});var l=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(l[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!l[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var u=e.document,s=u&&u.activeElement,c=null;s&&"number"==typeof s.selectionStart&&(c={start:s.selectionStart,end:s.selectionEnd,dir:s.selectionDirection});for(var d=[],v=0;v<t._overlays.length;v++){var p=t._overlays[v];if(p.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!l[g._vlKey]&&g.contains&&g.contains(p.restoreFocus)){d.push({rec:p,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!l[_._vlKey]&&_.contains&&_.contains(s)){m=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?h(r,e):e;i._vlKey=n,w.push(i)}else{var l=a[S++];w.push(l?h(l,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(s&&u&&u.contains&&u.contains(s)&&u.activeElement!==s&&"function"==typeof s.focus){try{s.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof s.setSelectionRange)try{s.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,a=0;a<e;a++){var u=i[a];u&&null!=u._vlKey&&l[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return f(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var C=A(m);if(C&&"function"==typeof C.focus)try{C.focus({preventScroll:!0})}catch(e){}}for(var x=0;x<d.length;x++)d[x].rec.restoreFallback=A(d[x].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,o[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var o=r(e),l=[],u=Object.create(null),s=0;s<o.length;s++){var c=i(o[s],s);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+s+"; a keyed list needs a stable, defined key per row");if(u[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");u[c]=!0;var d=n(o[s],s);a(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=l,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},l(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",a=r+"e",l=e.input||{},u=l.attrs=l.attrs||{};u.id=i;var s=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];s&&f.push(o),d&&f.push(a),f.length?u["aria-describedby"]=f.join(" "):delete u["aria-describedby"],e.hasError&&(u["aria-invalid"]=d?"true":null),e.aria&&(u["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",l,e.children||[])),s&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:a})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var a=t._mainLabel;if(a&&o.getAttribute){var l=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||l)&&(o.getAttribute("aria-label")!==a&&o.setAttribute("aria-label",a),l||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var u=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);u.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(u)}}else{var s=r||i;s&&s.parentNode&&s.parentNode.removeChild(s)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=s(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var a=i.body||u()&&u().parentNode||null;a&&a.appendChild(o);var l=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:l&&l!==i.body?l:null}),1===t._overlays.length&&c(u(),!0),o},close:function(n){var r=s(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(c(u(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&d(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=f(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var a;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return a},o)}),a=i(),null;var l=s(n)<0,u=this.open(n);u.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var c=t._overlays[s(n)],d="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),p=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":d,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:d},text:r},[]),i()]);if(c.panel&&c.panel.parentNode===u&&u.replaceChild){var y=h(c.panel,p);y!==c.panel&&(u.replaceChild(y,c.panel),c.panel=y)}else if(u.replaceChildren)u.replaceChildren(p),c.panel=p;else{for(;u.firstChild;)u.removeChild(u.firstChild);u.appendChild(p),c.panel=p}return l&&(c._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=c.panel,r=f(n);if(r.length){var i=r[0],o=r[r.length-1],a=e.document.activeElement;t.shiftKey&&a===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||a!==o&&n.contains(a)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},u.addEventListener&&u.addEventListener("keydown",c._onTab),c._onClick=function(e){e.target===u&&v(n)},u.addEventListener&&u.addEventListener("click",c._onClick),c._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||v(n)},e.document.addEventListener&&e.document.addEventListener("keydown",c._onKey),this.focusInto(c.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function a(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var l=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),a(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),a(r-1)):"Home"===t?(i(),a(0)):"End"===t&&(i(),a(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),u=n[r],s=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},u?[u.panel()]:[]);return t.el("div",{class:"vl-tabs"},[l,s])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},loading:function(e){return n.loading(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var a="undefined"!=typeof AbortController?new AbortController:null,u={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:a,signal:a?a.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),a=this._loads[e];if(!a||a.key!==o){a=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var l=this;Promise.resolve().then(r).then(function(n){l._loads[e]===a&&(a.pending=!1,a.value=n,l.schedule(),t._onLoadSettled&&t._onLoadSettled(l))},function(n){l._loads[e]===a&&(a.pending=!1,a.error=n,l.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(l,e,n),t._onLoadSettled&&t._onLoadSettled(l))})}return a},reload:function(e){delete this._loads[e],this.schedule()},loading:function(e){var t=this._loads[e];return!t||t.pending},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,a=e.document,u=a&&a.activeElement,c=null;u&&u.getAttribute&&u.getAttribute("data-vl-bind")&&(!a.contains||a.contains(u))&&(c={node:u,key:u.getAttribute("data-vl-bind"),start:u.selectionStart,end:u.selectionEnd,direction:u.selectionDirection,scrollLeft:u.scrollLeft,scrollTop:u.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,s);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();l(f,i),r.replaceChild?p(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?p(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&a){var v=a.contains&&a.contains(c.node)?c.node:null;if(!v&&a.querySelectorAll){var h=Array.prototype.filter.call(a.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===h.length&&(v=h[0])}if(v&&a.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},s=t.subctx({_pfx:"",_host:u},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){u._composing=!0}),c.addEventListener("compositionend",function(){u._composing=!1,u.schedule()})),u.render(),u},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var g=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(g[n])return g[n];var i=e.document,o=new Promise(function(e,o){var a=i.createElement("script");a.src=r,a.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},a.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(a)});return g[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
2
+ "use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}function i(e){if(Array.isArray(e))return!0;var t=Object.getPrototypeOf(e);return t===Object.prototype||null===t}function o(e,t){if(e===t)return!0;if(null==e||null==t)return null==e&&null==t;if("object"!=typeof e||"object"!=typeof t||Array.isArray(e)!==Array.isArray(t)||!i(e)||!i(t))return!1;var n;if(Array.isArray(e)){if(e.length!==t.length)return!1;for(n=0;n<e.length;n++)if(!o(e[n],t[n]))return!1;return!0}var r=Object.keys(e),l=Object.keys(t);for(n=0;n<r.length;n++)if(!o(e[r[n]],t[r[n]]))return!1;for(n=0;n<l.length;n++)if(!Object.prototype.hasOwnProperty.call(e,l[n])&&null!=t[l[n]])return!1;return!0}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var l=r[i.op];t=t.filter(function(e){return l(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.eq=function(e,t){return e===t||(null==e?null==t:null!=t&&"object"==typeof e&&"object"==typeof t&&o(e,t))},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},contains:function(e,t){return-1!==String(e).indexOf(String(t))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},any:function(e,t){return r(e).some(function(e){return!!t(e)})},all:function(e,t){return r(e).every(function(e){return!!t(e)})},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},unique:function(e){var t=[],n=new Set,i=[];return r(e).forEach(function(e){if(null!==e&&"object"==typeof e){for(var r=0;r<i.length;r++)if(o(i[r],e))return;i.push(e)}else if(e==e){if(n.has(e))return;n.add(e)}t.push(e)}),t},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var l in i)if(Object.prototype.hasOwnProperty.call(i,l)){if(void 0===n[l])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+l);i[l]&&"object"==typeof i[l]&&e(n[l],i[l],o+"/"+l)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,l){var a,u,s,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(a=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(a))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!l)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!s.ok){var v=null;try{v=await s.json()}catch(e){}if(l){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+s.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+s.status),g=v.error.fields;throw y.fields=g&&"object"==typeof g?g:{},y.kind=l.indexOf(p)>=0?"domain":s.status>=500?"internal":"framework",y}var m=v&&v.error?v.error:404===s.status?"not_found":"http."+s.status,b=t.err(m,n+" "+r+" -> "+s.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===s.status?null:s.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var l=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function a(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function u(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){u(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function s(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function c(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function d(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function f(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function v(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function h(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function p(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),y(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function y(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return g(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=p(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function g(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),l=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):l.push(e)});var a=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(a[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!a[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var u=e.document,s=u&&u.activeElement,c=null;s&&"number"==typeof s.selectionStart&&(c={start:s.selectionStart,end:s.selectionEnd,dir:s.selectionDirection});for(var d=[],f=0;f<t._overlays.length;f++){var h=t._overlays[f];if(h.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!a[g._vlKey]&&g.contains&&g.contains(h.restoreFocus)){d.push({rec:h,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!a[_._vlKey]&&_.contains&&_.contains(s)){m=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?p(r,e):e;i._vlKey=n,w.push(i)}else{var a=l[S++];w.push(a?p(a,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(s&&u&&u.contains&&u.contains(s)&&u.activeElement!==s&&"function"==typeof s.focus){try{s.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof s.setSelectionRange)try{s.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,l=0;l<e;l++){var u=i[l];u&&null!=u._vlKey&&a[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return v(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var j=A(m);if(j&&"function"==typeof j.focus)try{j.focus({preventScroll:!0})}catch(e){}}for(var C=0;C<d.length;C++)d[C].rec.restoreFallback=A(d[C].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,l[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var o=r(e),l=[],u=Object.create(null),s=0;s<o.length;s++){var c=i(o[s],s);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+s+"; a keyed list needs a stable, defined key per row");if(u[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");u[c]=!0;var d=n(o[s],s);a(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=u,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},u(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",l=r+"e",a=e.input||{},u=a.attrs=a.attrs||{};u.id=i;var s=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];s&&f.push(o),d&&f.push(l),f.length?u["aria-describedby"]=f.join(" "):delete u["aria-describedby"],e.hasError&&(u["aria-invalid"]=d?"true":null),e.aria&&(u["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",a,e.children||[])),s&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:l})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var l=t._mainLabel;if(l&&o.getAttribute){var a=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||a)&&(o.getAttribute("aria-label")!==l&&o.setAttribute("aria-label",l),a||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var u=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);u.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(u)}}else{var s=r||i;s&&s.parentNode&&s.parentNode.removeChild(s)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=c(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var l=i.body||s()&&s().parentNode||null;l&&l.appendChild(o);var a=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:a&&a!==i.body?a:null}),1===t._overlays.length&&d(s(),!0),o},close:function(n){var r=c(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(d(s(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&f(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=v(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var l;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return l},o)}),l=i(),null;var a=c(n)<0,u=this.open(n);u.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var s=t._overlays[c(n)],d="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),f=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":d,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:d},text:r},[]),i()]);if(s.panel&&s.panel.parentNode===u&&u.replaceChild){var y=p(s.panel,f);y!==s.panel&&(u.replaceChild(y,s.panel),s.panel=y)}else if(u.replaceChildren)u.replaceChildren(f),s.panel=f;else{for(;u.firstChild;)u.removeChild(u.firstChild);u.appendChild(f),s.panel=f}return a&&(s._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=s.panel,r=v(n);if(r.length){var i=r[0],o=r[r.length-1],l=e.document.activeElement;t.shiftKey&&l===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||l!==o&&n.contains(l)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},u.addEventListener&&u.addEventListener("keydown",s._onTab),s._onClick=function(e){e.target===u&&h(n)},u.addEventListener&&u.addEventListener("click",s._onClick),s._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||h(n)},e.document.addEventListener&&e.document.addEventListener("keydown",s._onKey),this.focusInto(s.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function l(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var a=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),l(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),l(r-1)):"Home"===t?(i(),l(0)):"End"===t&&(i(),l(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),u=n[r],s=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},u?[u.panel()]:[]);return t.el("div",{class:"vl-tabs"},[a,s])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},loading:function(e){return n.loading(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var l="undefined"!=typeof AbortController?new AbortController:null,a={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:l,signal:l?l.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),l=this._loads[e];if(!l||l.key!==o){l=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var a=this;Promise.resolve().then(r).then(function(n){a._loads[e]===l&&(l.pending=!1,l.value=n,a.schedule(),t._onLoadSettled&&t._onLoadSettled(a))},function(n){a._loads[e]===l&&(l.pending=!1,l.error=n,a.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(a,e,n),t._onLoadSettled&&t._onLoadSettled(a))})}return l},reload:function(e){delete this._loads[e],this.schedule()},loading:function(e){var t=this._loads[e];return!t||t.pending},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,l=e.document,a=l&&l.activeElement,c=null;a&&a.getAttribute&&a.getAttribute("data-vl-bind")&&(!l.contains||l.contains(a))&&(c={node:a,key:a.getAttribute("data-vl-bind"),start:a.selectionStart,end:a.selectionEnd,direction:a.selectionDirection,scrollLeft:a.scrollLeft,scrollTop:a.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,s);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();u(f,i),r.replaceChild?y(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?y(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&l){var v=l.contains&&l.contains(c.node)?c.node:null;if(!v&&l.querySelectorAll){var h=Array.prototype.filter.call(l.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===h.length&&(v=h[0])}if(v&&l.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},s=t.subctx({_pfx:"",_host:a},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){a._composing=!0}),c.addEventListener("compositionend",function(){a._composing=!1,a.schedule()})),a.render(),a},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var m=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(m[n])return m[n];var i=e.document,o=new Promise(function(e,o){var l=i.createElement("script");l.src=r,l.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},l.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(l)});return m[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
@@ -59,7 +59,9 @@ from vlcheck.elements import (
59
59
  SPELLCHECK_HOMES, TABINDEX_ALLOWED, TEXT_STYLE_ATTRS,
60
60
  UNSAFE_ELEMENTS,
61
61
  )
62
- from vlcheck.checks import STD_BUILT_FNS, _lit_int, _lit_text, _rendered_children
62
+ from vlcheck.checks import (AUTH_ERROR_CODES, ROUTE_CATCH_CODES, STD_BUILT_FNS,
63
+ _ERROR_CODES, _lit_int, _lit_text,
64
+ _rendered_children)
63
65
  from vlcheck.lexer import Lexer
64
66
  from vlcheck.parser import Parser
65
67
  from vlcheck.resolve import (_AUTH_API, AUTH_BUILT, _route_name,
@@ -169,12 +171,7 @@ WHERE_PREDICATES = {
169
171
  "contains": 1,
170
172
  }
171
173
 
172
- _AUTH_ERROR_CODES = {
173
- "email_taken", "weak_password", "breached_password", "rate_limited",
174
- "invalid_credentials", "unverified_email", "totp_required",
175
- "provider_error", "invalid_token", "expired_token", "invalid_code",
176
- "no_totp_enrolled",
177
- }
174
+ _AUTH_ERROR_CODES = AUTH_ERROR_CODES
178
175
 
179
176
 
180
177
  def _where_predicate_call(v):
@@ -286,7 +283,13 @@ _VIEW_STMT_OK = (A.StateDecl, A.DeriveDecl, A.LoadDecl,
286
283
  _ARITH_OPS = {"+", "-", "*", "/", "%"}
287
284
  # std functions whose result is a list (resolve.py E-LIST-OPERATOR)
288
285
  _LIST_RESULT_FNS = {"concat", "append", "filter", "map", "take", "sort_by",
289
- "chunk", "keys", "values", "split"}
286
+ "chunk", "keys", "values", "split", "unique"}
287
+ # std functions whose result is a scalar (a number, a string or a bool).
288
+ _SCALAR_RESULT_FNS = {"count", "sum", "contains", "matches", "join",
289
+ "first_char", "trim", "upper", "lower", "replace",
290
+ "int_to_float", "float_to_int", "any", "all"}
291
+ _SCALAR_OPS = {"+", "-", "*", "/", "%", "<", "<=", ">", ">=", "==", "!=",
292
+ "and", "or"}
290
293
 
291
294
 
292
295
  def _known_list(e) -> bool:
@@ -308,6 +311,34 @@ def _known_list(e) -> bool:
308
311
  return False
309
312
 
310
313
 
314
+ def _known_scalar(e, bare_variants=()) -> bool:
315
+ """True for an expression that is a number, string or bool by its shape
316
+ alone: a non-nil literal, `true` / `false`, a fieldless union variant
317
+ (emitted as its name), `not` / unary `-`, arithmetic, a comparison, a
318
+ logical operator, `.len()` / `.format()`, or a std function that returns
319
+ a scalar (directly or through `|>`). `==` with one side known scalar
320
+ stays JavaScript's `===`; anything else goes through `VL.eq`, which
321
+ compares records and lists structurally (spec section 4.3)."""
322
+ if isinstance(e, A.Literal):
323
+ return e.kind in ("int", "float", "bool", "str", "duration")
324
+ if isinstance(e, A.Name):
325
+ return e.id in ("true", "false") or e.id in bare_variants
326
+ if isinstance(e, A.Unary):
327
+ return True
328
+ if isinstance(e, A.Binary):
329
+ if e.op == "|>":
330
+ r = e.right
331
+ callee = r.callee if isinstance(r, A.Call) else r
332
+ return isinstance(callee, A.Name) and callee.id in _SCALAR_RESULT_FNS
333
+ return e.op in _SCALAR_OPS
334
+ if isinstance(e, A.Call):
335
+ c = e.callee
336
+ if isinstance(c, A.Member):
337
+ return c.name in ("len", "format")
338
+ return isinstance(c, A.Name) and c.id in _SCALAR_RESULT_FNS
339
+ return False
340
+
341
+
311
342
  def _nested_view_drop(stmts, where):
312
343
  """The first statement in a view's render tree that the emitter would
313
344
  not lower, as `(code, message)`, or None. vlcheck's `silent_drops` is
@@ -412,6 +443,9 @@ class Emitter:
412
443
  # the name resolves through these frames.
413
444
  self._scopes: list[dict[str, str]] = []
414
445
  self._local_seq = 0
446
+ # JS names bound to a caught error by a route body's `catch e`
447
+ # (`_route_try`): `err(e)` rethrows that error as it is.
448
+ self._caught: set[str] = set()
415
449
  self.views: list[A.ViewDecl] = []
416
450
  self.view_names: set[str] = set()
417
451
  self.routes: dict[str, A.Route] = {}
@@ -3540,6 +3574,10 @@ class Emitter:
3540
3574
  if isinstance(st, A.ExprStmt):
3541
3575
  last = tail and i == len(stmts) - 1
3542
3576
  parts.append("%s%s;" % ("return " if last else "", self.expr(st.expr)))
3577
+ elif isinstance(st, A.Let) and isinstance(st.init, A.Try):
3578
+ parts.append(self._route_try(st.init, decl, in_loop, let=st))
3579
+ elif isinstance(st, A.Try):
3580
+ parts.append(self._route_try(st, decl, in_loop))
3543
3581
  elif isinstance(st, A.Let):
3544
3582
  init = self.expr(st.init) if st.init is not None else "undefined"
3545
3583
  parts.append("%s %s = %s;" % (decl, self._bind_local(st.name), init))
@@ -3576,14 +3614,90 @@ class Emitter:
3576
3614
  "E-LOOP-CONTROL should have caught this" % word)
3577
3615
  parts.append("break;" if word == "break" else "continue;")
3578
3616
  else:
3579
- hint = {"Match": " Use if / elif / else instead.",
3580
- "Try": " Propagate with `?`, or check the value with if."}
3617
+ hint = {"Match": " Use if / elif / else instead."}
3581
3618
  raise BuildError(
3582
3619
  "route body statement `%s` is not lowered by vlbuild -- emitting it "
3583
3620
  "would silently skip it.%s" % (type(st).__name__,
3584
3621
  hint.get(type(st).__name__, "")))
3585
3622
  return parts
3586
3623
 
3624
+ def _route_try(self, t: A.Try, decl: str, in_loop: bool,
3625
+ let: A.Let | None = None) -> str:
3626
+ """`try EXPR` / `catch ...` in a route body (spec section 6.2), both
3627
+ forms. It used to fail the build ("route body statement `Try` is
3628
+ not lowered") although `volaro check` accepted it, so the spec's own
3629
+ `catch e` / `return err(e)` example could not be built.
3630
+
3631
+ - An arm headed by an error code (`catch not_found`), a declared
3632
+ union variant or an auth error code runs when the thrown error has
3633
+ that `code`, binding the variant's fields by name. Any other head
3634
+ (`catch e`, `catch _`) catches every error and binds it to `e`;
3635
+ arms after it are unreachable, as in a view handler. An error no
3636
+ arm matches is rethrown.
3637
+ - Statement form: a handled error does not fall through. The arm
3638
+ ends the route: its last expression statement is the route's
3639
+ value (like a route body's own last line); otherwise the route
3640
+ returns nil.
3641
+ - `let x = try EXPR`: `x` is EXPR's value, or the value of the arm
3642
+ that ran (its last expression statement); the statements after
3643
+ the `let` then run as usual.
3644
+ - `err(e)` inside an arm rethrows the caught error unchanged
3645
+ (`_ex_ServerErr`)."""
3646
+ codes = set(ROUTE_CATCH_CODES) | set(self.variant_params)
3647
+ call_js = self.expr(t.expr)
3648
+ out: list[str] = []
3649
+ target = None
3650
+ if let is not None:
3651
+ target = self._bind_local(let.name)
3652
+ out.append("%s %s;" % (decl, target))
3653
+ out.append("try { %s = await %s; } catch (_vlErr) {" % (target, call_js))
3654
+ else:
3655
+ out.append("try { await %s; } catch (_vlErr) {" % call_js)
3656
+ fallback = False
3657
+ for i, (pat, body) in enumerate(t.catches):
3658
+ head = getattr(pat, "head", "")
3659
+ is_code = head in codes
3660
+ if is_code:
3661
+ out.append("%s (_vlErr && _vlErr.code === %s) {" %
3662
+ ("if" if i == 0 else "else if", json.dumps(head)))
3663
+ else:
3664
+ out.append("{" if i == 0 else "else {")
3665
+ fallback = True
3666
+ self._push_scope()
3667
+ caught = None
3668
+ try:
3669
+ binds = list(getattr(pat, "binds", []) or [])
3670
+ if is_code:
3671
+ for name in binds:
3672
+ if name != head:
3673
+ out.append("%s %s = _vlErr[%s];" % (
3674
+ decl, self._bind_local(name), json.dumps(name)))
3675
+ elif binds and head != "_":
3676
+ caught = self._bind_local(binds[0])
3677
+ out.append("%s %s = _vlErr;" % (decl, caught))
3678
+ self._caught.add(caught)
3679
+ stmts = list(body)
3680
+ last = stmts[-1] if stmts and isinstance(stmts[-1], A.ExprStmt) else None
3681
+ head_stmts = stmts[:-1] if last is not None else stmts
3682
+ out.extend(self._route_block(head_stmts, decl, in_loop, tail=False))
3683
+ if let is not None:
3684
+ if last is not None:
3685
+ out.append("%s = %s;" % (target, self.expr(last.expr)))
3686
+ elif last is not None:
3687
+ out.append("return %s;" % self.expr(last.expr))
3688
+ elif not (stmts and isinstance(stmts[-1], A.Return)):
3689
+ out.append("return null;")
3690
+ finally:
3691
+ self._caught.discard(caught)
3692
+ self._pop_scope()
3693
+ out.append("}")
3694
+ if fallback:
3695
+ break
3696
+ if not fallback:
3697
+ out.append("else { throw _vlErr; }")
3698
+ out.append("}")
3699
+ return " ".join(out)
3700
+
3587
3701
  def expr(self, e) -> str:
3588
3702
  if e is None:
3589
3703
  return "undefined"
@@ -3760,16 +3874,28 @@ class Emitter:
3760
3874
  dispatcher's catch handler forwards every other own field (so
3761
3875
  `err(invalid_field("title"))` reaches the client as
3762
3876
  `{error: "invalid_field", field: "title"}`, not just a bare code)."""
3763
- standard = {"not_found", "bad_request", "unauthorized", "forbidden"}
3877
+ # `err(e)` in a route's `catch e` arm (spec section 6.2's own
3878
+ # example): rethrow the caught error unchanged, so its code and
3879
+ # fields reach the error response as they would have with no
3880
+ # `try` at all.
3881
+ if isinstance(inner, A.Name) and self._js_name(inner.id) in self._caught:
3882
+ return "(() => { throw %s; })()" % self._js_name(inner.id)
3883
+ # Every spec error code (vlcheck's `_ERROR_CODES`: `conflict`,
3884
+ # `rejected`, ... -- the checker accepts each as the argument of
3885
+ # `err(...)`), plus the server's own `bad_request` / `unauthorized`.
3886
+ standard = set(_ERROR_CODES) | {"bad_request", "unauthorized"}
3764
3887
  if not (isinstance(inner, A.Call) and isinstance(inner.callee, A.Name)
3765
3888
  and (inner.callee.id in self.variant_params
3766
3889
  or inner.callee.id in standard)):
3767
3890
  got = (inner.callee.id if isinstance(inner, A.Call)
3768
- and isinstance(inner.callee, A.Name) else type(inner).__name__)
3891
+ and isinstance(inner.callee, A.Name)
3892
+ else inner.id if isinstance(inner, A.Name)
3893
+ else type(inner).__name__)
3769
3894
  raise BuildError(
3770
- "server target: `err(...)` argument must be a standard error "
3771
- "or a declared error-union variant constructor — `%s` is not "
3772
- "lowered" % got)
3895
+ "E-ERR-ARG: `err(...)` takes an error code call "
3896
+ "(`not_found(\"...\")`), a declared error-union variant "
3897
+ "(`invalid_field(\"title\")`), or the error a `catch e` arm "
3898
+ "caught -- `%s` is not lowered" % got)
3773
3899
  variant = inner.callee.id
3774
3900
  param_names = (self.variant_params[variant]
3775
3901
  if variant in self.variant_params else ["message"])
@@ -3902,8 +4028,35 @@ class Emitter:
3902
4028
  raise BuildError(
3903
4029
  "E-LIST-OPERATOR: '%s' with a list operand is not defined; use "
3904
4030
  "concat(a, b) or xs |> append(x)" % e.op)
4031
+ if e.op in ("==", "!="):
4032
+ return self._equality_js(e)
3905
4033
  return "(%s %s %s)" % (self.expr(e.left), js, self.expr(e.right))
3906
4034
 
4035
+ def _equality_js(self, e: A.Binary) -> str:
4036
+ """`==` / `!=` (spec section 4.3: records and lists compare by value,
4037
+ never by identity). JavaScript's `===` compares objects by identity,
4038
+ so `{a: 1} == {a: 1}` was false and a list of records never matched
4039
+ itself, while `unique` compared the same values structurally. The
4040
+ emitter has no types, so it decides by shape: when either side is a
4041
+ known scalar (`_known_scalar`) the comparison stays `===`; a `nil`
4042
+ literal on either side is a null-or-undefined test (`== null`, since
4043
+ a missing optional field is nil too); anything else calls `VL.eq`,
4044
+ which is a single `===` for two scalars and structural for records
4045
+ and lists. Both targets have `VL.eq` (the shared std block)."""
4046
+ neg = e.op == "!="
4047
+ left, right = self.expr(e.left), self.expr(e.right)
4048
+ nil = lambda x: ((isinstance(x, A.Literal) and x.kind == "nil")
4049
+ or (isinstance(x, A.Name) and x.id == "nil"
4050
+ and self._js_name("nil") is None))
4051
+ if nil(e.left) or nil(e.right):
4052
+ other = right if nil(e.left) else left
4053
+ return "(%s %s null)" % (other, "!=" if neg else "==")
4054
+ bare = {v for v, ps in self.variant_params.items()
4055
+ if not ps and self._js_name(v) is None}
4056
+ if _known_scalar(e.left, bare) or _known_scalar(e.right, bare):
4057
+ return "(%s %s %s)" % (left, "!==" if neg else "===", right)
4058
+ return "%sVL.eq(%s, %s)" % ("!" if neg else "", left, right)
4059
+
3907
4060
  def _ex_Unary(self, e: A.Unary) -> str:
3908
4061
  return "(%s%s)" % ("!" if e.op == "not" else e.op, self.expr(e.operand))
3909
4062
 
@@ -116,11 +116,13 @@ def lower_page_server(root: Path, emitter) -> A.ServiceDecl:
116
116
  # `build_manifest` parses each page.server.vl but only structurally; the
117
117
  # page-server CHECKS (E-PS-WRITE-AUTH, dup verbs/actions, errors-block
118
118
  # rules) are what make the synthetic service safe for `precheck`, so run
119
- # the full per-file pipeline too.
119
+ # the full per-file pipeline too, with the resolver `volaro check` runs
120
+ # (names, arity, fields, `err(...)` arguments), so the build reports
121
+ # exactly what the check does.
120
122
  for path in sorted({e.source for e in manifest.entries}
121
123
  | _page_server_files(root)):
122
124
  src = Path(path).read_text(encoding="utf-8")
123
- _, diags = check_source(src, str(path))
125
+ _, diags = check_source(src, str(path), do_resolve=True)
124
126
  for d in diags:
125
127
  if d.severity == Severity.ERROR:
126
128
  problems.append(f"{d.code}: {d.message} ({d.file})")
@@ -60,7 +60,7 @@ _DB_METHODS = {"where", "order", "limit", "all", "first", "get", "insert", "upda
60
60
  # Statements Emitter.route_block lowers (action-34). It raises BuildError
61
61
  # for anything else at any nesting depth; this top-level check just fails
62
62
  # earlier with the route named.
63
- _STMT_OK = (A.ExprStmt, A.Let, A.Assign, A.Return, A.If, A.For, A.While)
63
+ _STMT_OK = (A.ExprStmt, A.Let, A.Assign, A.Return, A.If, A.For, A.While, A.Try)
64
64
 
65
65
  # A route's declared params are lowered if every one is a coercible primitive;
66
66
  # a record/list/map param stays a hard error — see precheck. `get`/`delete`
@@ -1330,14 +1330,13 @@ function err(code, message) { const e = new Error(message || code); e.code = cod
1330
1330
  // `STD_FNS` one function at a time (`matches`, `split`, `filter`, `count`,
1331
1331
  // `replace`, `contains`, `lower`, `trim`, `map`, `concat` were each found
1332
1332
  // missing by a route that called them).
1333
- const VL = {
1334
- len: (x) => (x == null ? 0 : x.length),
1335
- std: (function () {
1336
- const VL = {};
1333
+ // The block also defines `VL.eq`, the structural `==` a route body's
1334
+ // record and list comparisons compile to.
1335
+ const VL = (function () {
1336
+ const VL = { len: (x) => (x == null ? 0 : x.length) };
1337
1337
  %(vl_std)s
1338
- return VL.std;
1339
- })(),
1340
- };
1338
+ return VL;
1339
+ })();
1341
1340
 
1342
1341
  // Coerce one `?name=raw` query-string value or one decoded path segment to
1343
1342
  // its declared Volaro scalar type, or throw a 400 (via
package/language/crib.md CHANGED
@@ -119,6 +119,10 @@ derive shown = items |> filter(fn(a) q == "" or contains(lower(a.title), lower(t
119
119
 
120
120
  # no + on lists, no spread — concat/append are the only way to grow one:
121
121
  concat(xs, ys) · xs |> append(x)
122
+ # does some / every item pass? any(xs, f) / all(xs, f) -> bool
123
+ if not (cs |> any(fn(c) c == a.category))
124
+ cs = cs |> append(a.category)
125
+ # == / != compare records and lists by value (fields, items), like unique(xs)
122
126
 
123
127
  # exact pattern validation (std.str's `regex`) -- no implicit anchoring,
124
128
  # caller supplies ^/$. An invalid pattern never throws -- matches() always
@@ -211,7 +215,11 @@ errors # per-file code -> status; `_` required
211
215
  Handler shape: `method [action] (params) Ret[!Err] [as alias]` + body. One
212
216
  unnamed handler per method per file. `?` propagates only errors in the
213
217
  handler's declared `!E` set (db failures are always fine — they become the
214
- opaque internal error); translate anything else with try/catch.
218
+ opaque internal error); translate anything else with try/catch. `err(...)`
219
+ takes an error code call (`err(conflict("taken"))`), a variant of an error
220
+ union declared in `app.vl`, or a caught error: `catch e` then `return err(e)`
221
+ rethrows it. Anything else is `E-ERR-ARG`. `volaro check` resolves these
222
+ files like any other.
215
223
 
216
224
  Views call the structural client — one options object, dynamic segments are
217
225
  params: `api.posts.list(limit: 20)` · `api.posts.get(id:)` ·
package/language/spec.md CHANGED
@@ -1702,7 +1702,7 @@ The library is small on purpose. A large surface is a large thing to remember an
1702
1702
  | `std.db` | query builder, migrations, transactions |
1703
1703
  | `std.time` | `now`, formatting, parsing, arithmetic on `time` and `dur` |
1704
1704
  | `std.str` | split, join, trim, case, replace, contains (`contains(s, part) bool` — case-sensitive substring test, an empty `part` is `true`; distinct from the §13.1 `where()` predicate of the same name), regex (`matches(s, pattern) bool` — an unanchored whole-`RegExp`-source test, never throws; §6.2's model of "the compiler-owned surface" — see `volaro supported` for exactly what this build ships of this row) |
1705
- | `std.list` | map, filter, fold, sort_by, group_by, take, chunk, concat, append, unique (`unique(xs) [T]` — the first occurrence of each distinct value, in list order; values compare by §4.3 equality, so two records are the same when their fields are equal) |
1705
+ | `std.list` | map, filter, fold, sort_by, group_by, take, chunk, concat, append, unique (`unique(xs) [T]` — the first occurrence of each distinct value, in list order; values compare by §4.3 equality, so two records are the same when their fields are equal), any, all (`any(xs, f) bool` — true when some item passes `f`, false for an empty list; `all(xs, f) bool` — true when every item passes `f`, true for an empty list) |
1706
1706
  | `std.map` | keys, values, merge, get_or |
1707
1707
  | `std.json` | encode, decode, pointer access |
1708
1708
  | `std.crypto` | random bytes, hashing, timing-safe compare |
@@ -423,8 +423,8 @@ audio video source track picture map area
423
423
  `load`, so they are valid only on a name the view's own `load` binds
424
424
  (`E-LOAD-METHOD`); `loading()` is true while its fetch is in flight.
425
425
  The std free functions this build has are the ones listed under
426
- "Data / services" below (`concat`, `append`, `unique`, `matches`,
427
- `contains`, `replace`), plus `filter`, `map`, `take`, `count`, `sum`,
426
+ "Data / services" below (`concat`, `append`, `unique`, `any`, `all`,
427
+ `matches`, `contains`, `replace`), plus `filter`, `map`, `take`, `count`, `sum`,
428
428
  `sort_by`, `group_by`, `chunk`, `join`, `split`, `first_char`, `trim`,
429
429
  `upper`, `lower`, `keys`, `values`, `get_or`, `int_to_float` and
430
430
  `float_to_int`. Any other std name (`sorted`, `first`, `fold`, `merge`,
@@ -444,6 +444,14 @@ audio video source track picture map area
444
444
  only the argument of `err(...)`. `not_found("x")` anywhere else is
445
445
  `E-ERR-CONTEXT`. A service with no `model` runs its routes in the
446
446
  browser, and `err(...)` fails the call there too.
447
+ - The argument of `err(...)` is an error code call (`err(conflict("x"))`,
448
+ any code in the list above), a variant of an error union you declare
449
+ (`err(invalid_field("title"))`; a page.server handler sees the unions
450
+ in the project's `app.vl`), or the error a `catch e` arm caught
451
+ (`err(e)` rethrows it unchanged). Anything else (a string, a local
452
+ value, a record type, a function) is `E-ERR-ARG` in `volaro check` and
453
+ the build. A code with no `errors` entry answers with status 500, like
454
+ any unmapped code.
447
455
  - A namespace or ambient object (`db`, `env`, `log`, `route`, `auth`,
448
456
  `session`, `request`, `api`, and the unbuilt `analytics`, `events`,
449
457
  `ws`, `mailer`, `billing`), a type name (`str(x)`, `int(x)`, …), `ok`,
@@ -472,9 +480,22 @@ audio video source track picture map area
472
480
  writes.
473
481
  - Route bodies (services and `page.server.vl` handlers) run `let`,
474
482
  assignment (`=`, `+=` …), `if`/`elif`/`else`, `for`, `while`, `break`,
475
- `skip` and `return`; the last expression is the route's value. Any other
476
- statement (`match` or bare `try` as a statement, `par`, `spawn`) fails
483
+ `skip`, `return` and `try`/`catch`; the last expression is the route's
484
+ value. Any other statement (`match` as a statement, `par`, `spawn`) fails
477
485
  the build, at any nesting depth. Use `?` to propagate errors.
486
+ - `try`/`catch` in a route body, both forms. `catch not_found` (an error
487
+ code, or a variant of a union you declare) runs for that code only and
488
+ binds the variant's fields; `catch e` catches anything. An error no arm
489
+ matches propagates. The bare statement form does not fall through: the
490
+ arm ends the route, and its last expression is the route's value.
491
+ `let x = try ...` gives `x` the arm's last expression and carries on.
492
+ `catch e` then `return err(e)` rethrows the caught error as it was.
493
+ - `page.server.vl` handlers are resolved by `volaro check` like service
494
+ routes: an undefined name, a wrong argument count, an unknown field of
495
+ a typed parameter or a bad `err(...)` argument is reported there. In
496
+ scope: the project's `app.vl` declarations, the handler's parameters,
497
+ `session`, `request`, and the `_server.vl` context providers above the
498
+ file.
478
499
  - Password auth: `auth.sign_up` / `sign_in` / `sign_out` / `reset_password` /
479
500
  `confirm_reset` / `confirm_verify` against compiler-owned endpoints.
480
501
  Any other `auth.` name is `E-AUTH-METHOD` in `volaro check` and the build,
@@ -507,6 +528,19 @@ audio video source track picture map area
507
528
  - `std.list` `concat` / `append`; `xs |> map|filter(fn(x) …)`. (`fold` is in
508
529
  `volaro spec`'s stdlib table but is **not implemented** in this build —
509
530
  found stale while documenting `matches` below; corrected here, not fixed.)
531
+ - `std.list` `any(xs, f)` / `all(xs, f)` — whether some / every item
532
+ passes `f` (a `bool`). `any` of an empty list is `false`, `all` of an
533
+ empty list is `true`. Both forms work, in views, handlers and route bodies:
534
+ ```vl
535
+ if not (cs |> any(fn(c) c == a.category))
536
+ cs = cs |> append(a.category)
537
+ derive all_done = all(tasks, fn(t) t.done)
538
+ ```
539
+ - `==` and `!=` compare records and lists by value, the way `unique` does
540
+ (spec section 4.3): two records are equal when their fields are, two lists
541
+ when their items are, nested records included, in views and route bodies
542
+ alike. `nil` equals a missing optional field. Numbers, strings and bools
543
+ compare as before.
510
544
  - `std.list` `unique(xs)` — the first occurrence of each distinct value, in
511
545
  list order, as a new list. Scalars compare as `==` does (`1` and `"1"`
512
546
  differ); records and lists compare by their fields and items. Works in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volaro",
3
- "version": "0.1.0-alpha.12",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "Volaro \u2014 an application language written to be authored by an AI agent and read by a person. Ships the language reference and a working compiler for a supported subset (volaro check / build / dev). CLI: `volaro` (alias `vl`).",
5
5
  "repository": {
6
6
  "type": "git",