@telorun/templating 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cel/analyze.d.ts.map +1 -1
- package/dist/cel/analyze.js +40 -0
- package/dist/cel/catalog.d.ts +19 -1
- package/dist/cel/catalog.d.ts.map +1 -1
- package/dist/cel/catalog.js +476 -10
- package/dist/cel/diagnose.d.ts +5 -0
- package/dist/cel/diagnose.d.ts.map +1 -1
- package/dist/cel/diagnose.js +37 -1
- package/dist/engines/cel.d.ts.map +1 -1
- package/dist/engines/cel.js +5 -0
- package/package.json +4 -2
- package/src/cel/analyze.ts +37 -0
- package/src/cel/catalog.ts +521 -10
- package/src/cel/diagnose.ts +44 -2
- package/src/engines/cel.ts +6 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/cel/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAGpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/cel/analyze.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAGpD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,EAAE,EAAE,CAI7D;AAsFD;;+DAE+D;AAC/D,eAAO,MAAM,aAAa,QAAQ,CAAC;AAqBnC,UAAU,aAAa;IACrB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;CAChB;AAoFD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,OAAO,EACb,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC,aAAa,EAAE,CAIjB;AAoGD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EAAE,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAC1B,MAAM,GAAG,IAAI,CAsBf"}
|
package/dist/cel/analyze.js
CHANGED
|
@@ -11,12 +11,44 @@ export function extractAccessChains(node) {
|
|
|
11
11
|
return chains;
|
|
12
12
|
}
|
|
13
13
|
const COMPREHENSION_METHODS = new Set(["filter", "map", "exists", "all", "exists_one"]);
|
|
14
|
+
/** `cel.bind(name, init, body)` — CEL's only binding form, and the one the
|
|
15
|
+
* parser expands rather than dispatching, so it appears as a receiver call on a
|
|
16
|
+
* bare `cel` identifier that is in no scope and never will be. Both walks below
|
|
17
|
+
* need the same three facts out of it, so the shape is read once here.
|
|
18
|
+
*
|
|
19
|
+
* The receiver is deliberately not returned: it contributes no chain, and
|
|
20
|
+
* descending into it is what produced a `CEL_UNKNOWN_FIELD` for `cel` itself. */
|
|
21
|
+
function bindCall(node) {
|
|
22
|
+
if (node.op !== "rcall" || !Array.isArray(node.args))
|
|
23
|
+
return null;
|
|
24
|
+
const [method, receiver, callArgs] = node.args;
|
|
25
|
+
if (method !== "bind")
|
|
26
|
+
return null;
|
|
27
|
+
if (!isASTNode(receiver) || receiver.op !== "id" || receiver.args !== "cel")
|
|
28
|
+
return null;
|
|
29
|
+
if (!Array.isArray(callArgs) || callArgs.length !== 3)
|
|
30
|
+
return null;
|
|
31
|
+
const [nameNode, init, body] = callArgs;
|
|
32
|
+
if (!isASTNode(nameNode) || nameNode.op !== "id")
|
|
33
|
+
return null;
|
|
34
|
+
if (!isASTNode(init) || !isASTNode(body))
|
|
35
|
+
return null;
|
|
36
|
+
return { name: nameNode.args, init, body };
|
|
37
|
+
}
|
|
14
38
|
function visitNode(node, chains, boundVars) {
|
|
15
39
|
const chain = extractChain(node, boundVars);
|
|
16
40
|
if (chain !== null) {
|
|
17
41
|
chains.push(chain);
|
|
18
42
|
return;
|
|
19
43
|
}
|
|
44
|
+
// The bound name is in scope for the body ONLY; `init` is evaluated in the
|
|
45
|
+
// enclosing scope, so a name used there still has to resolve there.
|
|
46
|
+
const bind = bindCall(node);
|
|
47
|
+
if (bind) {
|
|
48
|
+
visitNode(bind.init, chains, boundVars);
|
|
49
|
+
visitNode(bind.body, chains, new Set(boundVars).add(bind.name));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
20
52
|
if (node.op === "rcall" &&
|
|
21
53
|
Array.isArray(node.args) &&
|
|
22
54
|
typeof node.args[0] === "string" &&
|
|
@@ -210,6 +242,14 @@ function walkNullable(node, nonNull, boundVars, issues, schema) {
|
|
|
210
242
|
walkNullable(b, union(nonNull, carried), boundVars, issues, schema);
|
|
211
243
|
return;
|
|
212
244
|
}
|
|
245
|
+
// `cel.bind` binds a name for its body; mirror extractAccessChains so a bound
|
|
246
|
+
// name is never read as a nullable context field.
|
|
247
|
+
const bind = bindCall(node);
|
|
248
|
+
if (bind) {
|
|
249
|
+
walkNullable(bind.init, nonNull, boundVars, issues, schema);
|
|
250
|
+
walkNullable(bind.body, nonNull, new Set(boundVars).add(bind.name), issues, schema);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
213
253
|
// Comprehension macros bind a loop variable; mirror extractAccessChains so a
|
|
214
254
|
// bound var is never mistaken for a nullable context field.
|
|
215
255
|
if (node.op === "rcall" &&
|
package/dist/cel/catalog.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface CelHandlers {
|
|
|
12
12
|
base64Decode: (s: string) => string;
|
|
13
13
|
json: (value: unknown) => string;
|
|
14
14
|
}
|
|
15
|
-
export type CelFunctionCategory = "conversion" | "time" | "uuid" | "string" | "math" | "collection" | "json" | "encoding" | "hashing" | "null";
|
|
15
|
+
export type CelFunctionCategory = "conversion" | "time" | "uuid" | "string" | "math" | "collection" | "json" | "encoding" | "hashing" | "formatting" | "null";
|
|
16
16
|
/** One entry in the CEL standard library — the single source of truth that both
|
|
17
17
|
* registers the function (`build`) and documents it (everything else). `telo
|
|
18
18
|
* cel functions` and `celFunctionCatalog()` read the metadata; `buildCelEnvironment`
|
|
@@ -38,6 +38,24 @@ export interface CelFunctionDoc {
|
|
|
38
38
|
* analyzer's stub throws if such a function is actually evaluated. */
|
|
39
39
|
readonly hostBacked: boolean;
|
|
40
40
|
readonly build: (h: CelHandlers) => (...args: any[]) => unknown;
|
|
41
|
+
/**
|
|
42
|
+
* Check the arguments that were written as LITERALS, at analysis time.
|
|
43
|
+
*
|
|
44
|
+
* A type is all a signature can constrain, so a guard over a value —
|
|
45
|
+
* an unparseable format specifier, a decimal count out of range, a day length
|
|
46
|
+
* of zero, an unknown IANA zone — fires only when the expression is evaluated.
|
|
47
|
+
* That puts a defect the manifest states in plain sight behind a run, which is
|
|
48
|
+
* the opposite of what static analysis is for.
|
|
49
|
+
*
|
|
50
|
+
* `literals[i]` is the value of argument `i` when it was written as a literal,
|
|
51
|
+
* and `undefined` when it is an expression whose value is not statically
|
|
52
|
+
* known — so a checker MUST skip an `undefined` rather than judge it.
|
|
53
|
+
* Returns a message, or `undefined` when there is nothing to report.
|
|
54
|
+
*
|
|
55
|
+
* Implementations call the SAME guard the runtime calls, so the static and
|
|
56
|
+
* dynamic answers cannot drift into disagreement.
|
|
57
|
+
*/
|
|
58
|
+
readonly checkArgs?: (literals: readonly unknown[]) => string | undefined;
|
|
41
59
|
}
|
|
42
60
|
/** Public, build-free view of a catalog entry (for `--json` / docs). */
|
|
43
61
|
export type CelFunctionInfo = Omit<CelFunctionDoc, "build">;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/cel/catalog.ts"],"names":[],"mappings":"AAIA;;;0DAG0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5B,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,YAAY,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC;CAClC;AA2CD,MAAM,MAAM,mBAAmB,GAC3B,YAAY,GACZ,MAAM,GACN,MAAM,GACN,QAAQ,GACR,MAAM,GACN,YAAY,GACZ,MAAM,GACN,UAAU,GACV,SAAS,GACT,YAAY,GACZ,MAAM,CAAC;AAEX;;;qBAGqB;AACrB,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;0EACsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;yDAIqD;IACrD,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;wBACoB;IACpB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;2EACuE;IACvE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC;IAChE;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;CAC3E;AAYD,wEAAwE;AACxE,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AA8Z5D,eAAO,MAAM,aAAa,EAAE,SAAS,cAAc,EAyxBlD,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,kBAAkB,IAAI,eAAe,EAAE,CAEtD"}
|
package/dist/cel/catalog.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { formatLocale } from "d3-format";
|
|
1
2
|
import { RE2JS } from "re2js";
|
|
2
3
|
import { v1, v3, v4, v5, v6, v7, validate as uuidValidate, version as uuidVersion } from "uuid";
|
|
3
4
|
/** RE2 regex engine for the CEL `regex*` functions — `re2js`, a pure-JS port of
|
|
@@ -40,6 +41,16 @@ const compileRe2 = (fn, pattern, flags) => {
|
|
|
40
41
|
throw new Error(`${fn}: invalid RE2 pattern ${JSON.stringify(pattern)}: ${e instanceof Error ? e.message : String(e)}`);
|
|
41
42
|
}
|
|
42
43
|
};
|
|
44
|
+
/** Run a runtime guard for its refusal, so a `checkArgs` never restates one. */
|
|
45
|
+
const literalGuard = (run) => {
|
|
46
|
+
try {
|
|
47
|
+
run();
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
return e instanceof Error ? e.message : String(e);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
43
54
|
const num = (x) => Number(x);
|
|
44
55
|
const minMax = (list, isMin) => {
|
|
45
56
|
if (!Array.isArray(list) || list.length === 0)
|
|
@@ -62,6 +73,147 @@ const sortList = (list) => [...list].sort((a, b) => {
|
|
|
62
73
|
}
|
|
63
74
|
return String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0;
|
|
64
75
|
});
|
|
76
|
+
/** The number-formatting locale, pinned rather than defaulted.
|
|
77
|
+
*
|
|
78
|
+
* d3-format's default locale renders a negative with U+2212 MINUS SIGN, so
|
|
79
|
+
* `format(-1.5, '.2f')` is `"−1.50"` and not `"-1.50"` — a string that no
|
|
80
|
+
* downstream parser, comparison or diff treats as the number it looks like.
|
|
81
|
+
* Every field here is fixed to its ASCII form for the same reason the layer is
|
|
82
|
+
* locale-free at all: the same manifest must render the same bytes on every
|
|
83
|
+
* runtime, and a second engine implementing the specifier grammar has to be
|
|
84
|
+
* able to reproduce these exactly. */
|
|
85
|
+
const FORMAT_LOCALE = formatLocale({
|
|
86
|
+
decimal: ".",
|
|
87
|
+
thousands: ",",
|
|
88
|
+
grouping: [3],
|
|
89
|
+
currency: ["$", ""],
|
|
90
|
+
minus: "-",
|
|
91
|
+
percent: "%",
|
|
92
|
+
nan: "NaN",
|
|
93
|
+
});
|
|
94
|
+
/** Largest integer a double represents exactly. */
|
|
95
|
+
const MAX_EXACT_INT = 9007199254740991n;
|
|
96
|
+
/** A CEL `int` is a BigInt in this runtime and `d3-format` throws on one
|
|
97
|
+
* outright, so a formattable argument is converted here. Past 2^53 a double
|
|
98
|
+
* stops representing every integer, and silently emitting a number that is not
|
|
99
|
+
* the one the author computed is the defect class this family exists to close —
|
|
100
|
+
* so that case raises instead. */
|
|
101
|
+
const formattable = (fn, x) => {
|
|
102
|
+
if (typeof x === "bigint") {
|
|
103
|
+
if (x > MAX_EXACT_INT || x < -MAX_EXACT_INT) {
|
|
104
|
+
throw new Error(`${fn}: integer ${x} exceeds 2^53-1 and cannot be formatted exactly as a double`);
|
|
105
|
+
}
|
|
106
|
+
return Number(x);
|
|
107
|
+
}
|
|
108
|
+
const n = Number(x);
|
|
109
|
+
// The runtime backstop behind the typed registrations. A value that is not a
|
|
110
|
+
// number formats as the string "NaN", which is the failure this family exists
|
|
111
|
+
// to remove: it looks like an answer and prints into a document. Named here
|
|
112
|
+
// rather than coerced, the way an instant argument is.
|
|
113
|
+
if (!Number.isFinite(n)) {
|
|
114
|
+
throw new Error(`${fn}: expected a finite number, got ${JSON.stringify(x)}`);
|
|
115
|
+
}
|
|
116
|
+
return n;
|
|
117
|
+
};
|
|
118
|
+
/** Specifier type characters d3 implements. An unknown one PARSES — `.2q`
|
|
119
|
+
* yields `"1"` rather than throwing — so a typo would silently format against
|
|
120
|
+
* the default type. The set is checked here so a bad specifier is refused
|
|
121
|
+
* rather than quietly answered. */
|
|
122
|
+
const FORMAT_TYPES = new Set([..."efgrs%pbodxXcn"]);
|
|
123
|
+
/** A specifier is a CEL value, so it can be request-derived — an `Http.Server`
|
|
124
|
+
* evaluating `format(x, request.query.spec)` would otherwise grow this map for
|
|
125
|
+
* the life of the process, and it is module-global, so every in-process kernel
|
|
126
|
+
* shares it. Cleared wholesale at the cap rather than evicted one at a time: a
|
|
127
|
+
* manifest's real specifier set is a handful of constants that repopulate
|
|
128
|
+
* immediately, and an LRU is machinery for a hit rate nothing here needs. */
|
|
129
|
+
const FORMATTER_CACHE_MAX = 256;
|
|
130
|
+
const formatterCache = new Map();
|
|
131
|
+
const formatter = (fn, spec) => {
|
|
132
|
+
const text = String(spec);
|
|
133
|
+
const cached = formatterCache.get(text);
|
|
134
|
+
if (cached)
|
|
135
|
+
return cached;
|
|
136
|
+
const type = text.slice(-1);
|
|
137
|
+
if (text !== "" && /[a-zA-Z%]/.test(type) && !FORMAT_TYPES.has(type)) {
|
|
138
|
+
throw new Error(`${fn}: unknown format type '${type}' (one of ${[...FORMAT_TYPES].join("")})`);
|
|
139
|
+
}
|
|
140
|
+
// The `.precision` group — width is the digits BEFORE the dot, so this is the
|
|
141
|
+
// only `.`-digits sequence the grammar admits.
|
|
142
|
+
const precision = /\.(\d+)/.exec(text);
|
|
143
|
+
if (precision)
|
|
144
|
+
digitCount(fn, precision[1]);
|
|
145
|
+
let built;
|
|
146
|
+
try {
|
|
147
|
+
built = FORMAT_LOCALE.format(text);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
throw new Error(`${fn}: invalid format specifier ${JSON.stringify(text)}`);
|
|
151
|
+
}
|
|
152
|
+
if (formatterCache.size >= FORMATTER_CACHE_MAX)
|
|
153
|
+
formatterCache.clear();
|
|
154
|
+
formatterCache.set(text, built);
|
|
155
|
+
return built;
|
|
156
|
+
};
|
|
157
|
+
/** Decimal places, bounded. The ceiling is well below what `toFixed` accepts
|
|
158
|
+
* because past it the digits are an artefact of the binary representation
|
|
159
|
+
* rather than of the value.
|
|
160
|
+
*
|
|
161
|
+
* ONE rule, enforced wherever a precision is written: `formatter` applies it to
|
|
162
|
+
* a specifier's `.precision` group too. Bounding only this spelling let an
|
|
163
|
+
* author route around the guard by writing `format(x, '.11f')` instead of
|
|
164
|
+
* `fixed(x, 11)` — the family giving two answers to one question. It is not the
|
|
165
|
+
* grammar subsetting the "full d3 surface" decision refuses: every specifier
|
|
166
|
+
* type and flag stays available, and only the digit count is capped. */
|
|
167
|
+
const MAX_DECIMALS = 10;
|
|
168
|
+
const digitCount = (fn, digits) => {
|
|
169
|
+
const n = Number(digits);
|
|
170
|
+
if (!Number.isInteger(n) || n < 0 || n > MAX_DECIMALS) {
|
|
171
|
+
throw new Error(`${fn}: decimal places must be an integer 0-${MAX_DECIMALS}, got ${String(digits)}`);
|
|
172
|
+
}
|
|
173
|
+
return n;
|
|
174
|
+
};
|
|
175
|
+
/** Render a minute count against a declared day length. The day is a policy
|
|
176
|
+
* argument, never an assumption — see the catalog entry's summary. */
|
|
177
|
+
const durationText = (minutes, minutesPerDay) => {
|
|
178
|
+
const perDay = Math.round(formattable("formatDuration", minutesPerDay));
|
|
179
|
+
if (!Number.isFinite(perDay) || perDay <= 0) {
|
|
180
|
+
throw new Error(`formatDuration: minutesPerDay must be a positive number, got ${perDay}`);
|
|
181
|
+
}
|
|
182
|
+
const total = Math.round(formattable("formatDuration", minutes));
|
|
183
|
+
if (!Number.isFinite(total)) {
|
|
184
|
+
throw new Error(`formatDuration: minutes must be a finite number`);
|
|
185
|
+
}
|
|
186
|
+
const magnitude = Math.abs(total);
|
|
187
|
+
const days = Math.floor(magnitude / perDay);
|
|
188
|
+
const withinDay = magnitude % perDay;
|
|
189
|
+
const hours = Math.floor(withinDay / 60);
|
|
190
|
+
const mins = withinDay % 60;
|
|
191
|
+
const parts = [];
|
|
192
|
+
if (days)
|
|
193
|
+
parts.push(`${days}d`);
|
|
194
|
+
if (hours)
|
|
195
|
+
parts.push(`${hours}h`);
|
|
196
|
+
if (mins)
|
|
197
|
+
parts.push(`${mins}m`);
|
|
198
|
+
if (parts.length === 0)
|
|
199
|
+
parts.push("0m");
|
|
200
|
+
return `${total < 0 ? "-" : ""}${parts.join(" ")}`;
|
|
201
|
+
};
|
|
202
|
+
/** Refuse an unknown zone in this family's own voice. Left to `Intl`, the
|
|
203
|
+
* failure is a raw `RangeError` naming neither the function nor what was
|
|
204
|
+
* wrong with the argument — the only refusal here that did not read
|
|
205
|
+
* `<fn>: <what is wrong>`, and whose wording belongs to the JS engine rather
|
|
206
|
+
* than to Telo. Also the guard `checkArgs` runs at analysis time, so a literal
|
|
207
|
+
* zone is checked once and answered identically in both places. */
|
|
208
|
+
const assertZone = (fn, tz) => {
|
|
209
|
+
try {
|
|
210
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
throw new Error(`${fn}: unknown IANA time zone ${JSON.stringify(tz)}`);
|
|
214
|
+
}
|
|
215
|
+
return tz;
|
|
216
|
+
};
|
|
65
217
|
/** `Intl.DateTimeFormat` is an ECMA-402 global in Node (full ICU) and browsers,
|
|
66
218
|
* so timezone handling needs no Node-only API and stays browser-safe. */
|
|
67
219
|
const zoneParts = (date, tz, opts) => {
|
|
@@ -75,8 +227,7 @@ const zoneParts = (date, tz, opts) => {
|
|
|
75
227
|
* offset (e.g. `2026-06-06T18:30:00.000-05:00`). Uses only standard Intl
|
|
76
228
|
* fields and derives the offset arithmetically, so it needs no newer Intl
|
|
77
229
|
* type-lib features and stays portable. */
|
|
78
|
-
const isoInZone = (tz) => {
|
|
79
|
-
const now = new Date();
|
|
230
|
+
const isoInZone = (now, tz) => {
|
|
80
231
|
if (tz === "UTC" || tz === "Z")
|
|
81
232
|
return now.toISOString();
|
|
82
233
|
const p = zoneParts(now, tz, {
|
|
@@ -98,14 +249,148 @@ const isoInZone = (tz) => {
|
|
|
98
249
|
: `${offsetMin > 0 ? "+" : "-"}${String(Math.floor(Math.abs(offsetMin) / 60)).padStart(2, "0")}:${String(Math.abs(offsetMin) % 60).padStart(2, "0")}`;
|
|
99
250
|
return `${p.year}-${p.month}-${p.day}T${p.hour}:${p.minute}:${p.second}.${ms}${offset}`;
|
|
100
251
|
};
|
|
101
|
-
/**
|
|
102
|
-
const dateInZone = (tz) => {
|
|
103
|
-
const now = new Date();
|
|
252
|
+
/** Calendar date (`YYYY-MM-DD`) of an instant in `tz`. */
|
|
253
|
+
const dateInZone = (now, tz) => {
|
|
104
254
|
if (tz === "UTC" || tz === "Z")
|
|
105
255
|
return now.toISOString().slice(0, 10);
|
|
106
256
|
const p = zoneParts(now, tz, { year: "numeric", month: "2-digit", day: "2-digit" });
|
|
107
257
|
return `${p.year}-${p.month}-${p.day}`;
|
|
108
258
|
};
|
|
259
|
+
const zonedFields = (date, tz) => {
|
|
260
|
+
const p = zoneParts(date, tz, {
|
|
261
|
+
hourCycle: "h23",
|
|
262
|
+
year: "numeric",
|
|
263
|
+
month: "2-digit",
|
|
264
|
+
day: "2-digit",
|
|
265
|
+
hour: "2-digit",
|
|
266
|
+
minute: "2-digit",
|
|
267
|
+
second: "2-digit",
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
year: +p.year,
|
|
271
|
+
month: +p.month,
|
|
272
|
+
day: +p.day,
|
|
273
|
+
hour: +p.hour,
|
|
274
|
+
minute: +p.minute,
|
|
275
|
+
second: +p.second,
|
|
276
|
+
};
|
|
277
|
+
};
|
|
278
|
+
/** The zone's offset at `date`, in milliseconds — its wall clock read as UTC,
|
|
279
|
+
* minus the real instant. The same arithmetic `isoInZone` does. */
|
|
280
|
+
const zoneOffsetMs = (date, tz) => {
|
|
281
|
+
const f = zonedFields(date, tz);
|
|
282
|
+
return Date.UTC(f.year, f.month - 1, f.day, f.hour, f.minute, f.second) - date.getTime();
|
|
283
|
+
};
|
|
284
|
+
const sameWallClock = (a, b) => a.year === b.year &&
|
|
285
|
+
a.month === b.month &&
|
|
286
|
+
a.day === b.day &&
|
|
287
|
+
a.hour === b.hour &&
|
|
288
|
+
a.minute === b.minute &&
|
|
289
|
+
a.second === b.second;
|
|
290
|
+
/** The instant whose wall clock in `tz` is the given fields.
|
|
291
|
+
*
|
|
292
|
+
* The offset depends on the instant being solved for, so this takes the offset
|
|
293
|
+
* at the UTC reading, corrects, and then CHECKS by reading the result back.
|
|
294
|
+
* That check is the whole point: a plain fixpoint settles on an instant whose
|
|
295
|
+
* wall clock is not the one asked for whenever the requested time does not
|
|
296
|
+
* exist, and it settles BACKWARDS — which silently moves the calendar day, the
|
|
297
|
+
* one thing `addMonths` and `startOfMonth` exist to control. Chile jumps
|
|
298
|
+
* 00:00 → 01:00 on 2026-09-06 and Cuba on 2026-03-08, so "the 6th at midnight"
|
|
299
|
+
* there is not a time; a fixpoint answered "the 5th at 23:00".
|
|
300
|
+
*
|
|
301
|
+
* Resolution follows Java's `ZonedDateTime` and Temporal's `compatible`:
|
|
302
|
+
* a wall clock that exists twice (a fall-back) takes the EARLIER instant, and
|
|
303
|
+
* one that does not exist (a spring-forward gap) shifts FORWARD out of the gap,
|
|
304
|
+
* which keeps the requested day. */
|
|
305
|
+
const instantOfZoned = (f, tz) => {
|
|
306
|
+
const asUtc = Date.UTC(f.year, f.month - 1, f.day, f.hour, f.minute, f.second);
|
|
307
|
+
const offsetA = zoneOffsetMs(new Date(asUtc), tz);
|
|
308
|
+
const candidateA = asUtc - offsetA;
|
|
309
|
+
const offsetB = zoneOffsetMs(new Date(candidateA), tz);
|
|
310
|
+
if (offsetA === offsetB)
|
|
311
|
+
return new Date(candidateA);
|
|
312
|
+
const candidateB = asUtc - offsetB;
|
|
313
|
+
const aHolds = sameWallClock(zonedFields(new Date(candidateA), tz), f);
|
|
314
|
+
const bHolds = sameWallClock(zonedFields(new Date(candidateB), tz), f);
|
|
315
|
+
if (aHolds && bHolds)
|
|
316
|
+
return new Date(Math.min(candidateA, candidateB));
|
|
317
|
+
if (aHolds)
|
|
318
|
+
return new Date(candidateA);
|
|
319
|
+
if (bHolds)
|
|
320
|
+
return new Date(candidateB);
|
|
321
|
+
return new Date(Math.max(candidateA, candidateB));
|
|
322
|
+
};
|
|
323
|
+
/** `Date.UTC` maps years 0-99 to 1900-1999, so the year is set explicitly. */
|
|
324
|
+
const daysInMonth = (year, month) => {
|
|
325
|
+
const d = new Date(Date.UTC(2000, month, 0));
|
|
326
|
+
d.setUTCFullYear(year, month, 0);
|
|
327
|
+
return d.getUTCDate();
|
|
328
|
+
};
|
|
329
|
+
/** An instant argument arrives as a `Date`; anything else is a caller error the
|
|
330
|
+
* type-checker did not catch (a `dyn` slot), so it is named rather than
|
|
331
|
+
* coerced into an Invalid Date that formats as `NaN`. */
|
|
332
|
+
const instantArg = (fn, v) => {
|
|
333
|
+
if (v instanceof Date && Number.isFinite(v.getTime()))
|
|
334
|
+
return v;
|
|
335
|
+
throw new Error(`${fn}: expected a timestamp`);
|
|
336
|
+
};
|
|
337
|
+
/** Drop entries whose value is null or the empty string. Nothing else: an empty
|
|
338
|
+
* list or map is a value someone deliberately built. CEL hands a map over as a
|
|
339
|
+
* plain object or a `Map` depending on how it was produced, so both are read. */
|
|
340
|
+
const compactValue = (v) => {
|
|
341
|
+
const keep = (x) => x !== null && x !== undefined && x !== "";
|
|
342
|
+
if (Array.isArray(v))
|
|
343
|
+
return v.filter(keep);
|
|
344
|
+
if (v instanceof Map) {
|
|
345
|
+
return new Map([...v.entries()].filter(([, value]) => keep(value)));
|
|
346
|
+
}
|
|
347
|
+
// A PLAIN object only. Rebuilding an arbitrary object from its entries is how
|
|
348
|
+
// a byte buffer becomes `{"0":137,…}` and an instant becomes `{}` — silently,
|
|
349
|
+
// and looking like a value. The same rule the compile walker follows, and the
|
|
350
|
+
// same "name it rather than coerce it" the instant argument follows.
|
|
351
|
+
if (v !== null && typeof v === "object") {
|
|
352
|
+
const proto = Object.getPrototypeOf(v);
|
|
353
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
354
|
+
throw new Error(`compact: expected a map or a list, got ${v.constructor?.name ?? "an object"}`);
|
|
355
|
+
}
|
|
356
|
+
return Object.fromEntries(Object.entries(v).filter(([, value]) => keep(value)));
|
|
357
|
+
}
|
|
358
|
+
throw new Error(`compact: expected a map or a list, got ${JSON.stringify(v)}`);
|
|
359
|
+
};
|
|
360
|
+
/** A CEL map as entries. Read the way `compact` reads one — a map arrives as a
|
|
361
|
+
* plain object or a `Map` depending on how it was produced — and refusing
|
|
362
|
+
* anything that is not one, since rebuilding an arbitrary object from its
|
|
363
|
+
* entries is how a byte buffer becomes `{"0":137,…}` silently. A LIST is named
|
|
364
|
+
* rather than coerced: CEL's `+` already concatenates lists, so a list here is
|
|
365
|
+
* a mistake with a spelling that works, not a case to support. */
|
|
366
|
+
const mapEntries = (fn, v) => {
|
|
367
|
+
if (v instanceof Map)
|
|
368
|
+
return [...v.entries()];
|
|
369
|
+
if (Array.isArray(v))
|
|
370
|
+
throw new Error(`${fn}: expected a map, got a list — use '+' to join lists`);
|
|
371
|
+
if (v !== null && typeof v === "object") {
|
|
372
|
+
const proto = Object.getPrototypeOf(v);
|
|
373
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
374
|
+
throw new Error(`${fn}: expected a map, got ${v.constructor?.name ?? "an object"}`);
|
|
375
|
+
}
|
|
376
|
+
return Object.entries(v);
|
|
377
|
+
}
|
|
378
|
+
throw new Error(`${fn}: expected a map, got ${JSON.stringify(v)}`);
|
|
379
|
+
};
|
|
380
|
+
/** Right-hand precedence, so `merge(defaults, overrides)` reads as it looks.
|
|
381
|
+
*
|
|
382
|
+
* The map case is the one with no spelling at all — `+` joins lists and
|
|
383
|
+
* strings and refuses maps — so a child kind inheriting a map-valued field
|
|
384
|
+
* could only REPLACE it. That turns a default the parent set for a reason into
|
|
385
|
+
* something every consumer must restate, and a consumer who restates it
|
|
386
|
+
* incompletely gets a system that works until the omitted entry matters.
|
|
387
|
+
*
|
|
388
|
+
* Follows the LEFT argument's shape: this extends that map, so what comes back
|
|
389
|
+
* is what was extended. */
|
|
390
|
+
const mergeMaps = (a, b) => {
|
|
391
|
+
const entries = [...mapEntries("merge", a), ...mapEntries("merge", b)];
|
|
392
|
+
return a instanceof Map ? new Map(entries) : Object.fromEntries(entries);
|
|
393
|
+
};
|
|
109
394
|
const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
110
395
|
/** Base64 → bytes, written out rather than delegated: `Buffer` is not browser-
|
|
111
396
|
* safe and `atob` round-trips through a string, which is the corruption this
|
|
@@ -417,12 +702,30 @@ export const CEL_FUNCTIONS = [
|
|
|
417
702
|
},
|
|
418
703
|
{
|
|
419
704
|
name: "round",
|
|
420
|
-
signature: "round(dyn): double",
|
|
705
|
+
signature: "round(dyn, int?): double",
|
|
706
|
+
register: [
|
|
707
|
+
"round(double): double",
|
|
708
|
+
"round(int): double",
|
|
709
|
+
"round(double, int): double",
|
|
710
|
+
"round(int, int): double",
|
|
711
|
+
],
|
|
421
712
|
category: "math",
|
|
422
|
-
summary: "Round to the nearest integer.",
|
|
713
|
+
summary: "Round to the nearest integer, or to the given number of decimal places (0–10). The two-argument form shares the formatter's rounding rule, so a rounded value and the cell rendered beside it agree at the boundary. An integer past 2^53 is refused rather than rounded to a neighbour.",
|
|
423
714
|
deterministic: true,
|
|
424
715
|
hostBacked: false,
|
|
425
|
-
|
|
716
|
+
// Both arities go through `formattable`, so the 2^53 refusal does not depend
|
|
717
|
+
// on which one the author wrote. Guarding only the two-argument form left
|
|
718
|
+
// `round(x)` silently answering with a neighbouring integer — the defect
|
|
719
|
+
// this family exists to close, reachable by writing one fewer argument.
|
|
720
|
+
build: () => (x, digits) => digits === undefined
|
|
721
|
+
? Math.round(formattable("round", x))
|
|
722
|
+
: Number(formattable("round", x).toFixed(digitCount("round", digits))),
|
|
723
|
+
checkArgs: (lit) => literalGuard(() => {
|
|
724
|
+
if (lit[1] !== undefined)
|
|
725
|
+
digitCount("round", lit[1]);
|
|
726
|
+
if (typeof lit[0] === "bigint")
|
|
727
|
+
formattable("round", lit[0]);
|
|
728
|
+
}),
|
|
426
729
|
},
|
|
427
730
|
{
|
|
428
731
|
name: "min",
|
|
@@ -619,7 +922,7 @@ export const CEL_FUNCTIONS = [
|
|
|
619
922
|
summary: "Current time as ISO-8601; UTC by default, or in the given IANA timezone.",
|
|
620
923
|
deterministic: false,
|
|
621
924
|
hostBacked: false,
|
|
622
|
-
build: () => (tz) => isoInZone(tz ?? "UTC"),
|
|
925
|
+
build: () => (tz) => isoInZone(new Date(), tz ?? "UTC"),
|
|
623
926
|
},
|
|
624
927
|
{
|
|
625
928
|
name: "today",
|
|
@@ -628,7 +931,7 @@ export const CEL_FUNCTIONS = [
|
|
|
628
931
|
summary: "Current calendar date (YYYY-MM-DD); UTC by default, or in the given IANA timezone.",
|
|
629
932
|
deterministic: false,
|
|
630
933
|
hostBacked: false,
|
|
631
|
-
build: () => (tz) => dateInZone(tz ?? "UTC"),
|
|
934
|
+
build: () => (tz) => dateInZone(new Date(), tz ?? "UTC"),
|
|
632
935
|
},
|
|
633
936
|
{
|
|
634
937
|
name: "nowMillis",
|
|
@@ -675,6 +978,169 @@ export const CEL_FUNCTIONS = [
|
|
|
675
978
|
hostBacked: false,
|
|
676
979
|
build: () => (t) => BigInt(Math.floor(t.getTime() / 1000)),
|
|
677
980
|
},
|
|
981
|
+
// Formatting. The number surface is the d3-format specifier grammar in full,
|
|
982
|
+
// `[[fill]align][sign][symbol][0][width][,][.precision][~][type]`, so a chart
|
|
983
|
+
// axis label and the table cell beside it cannot round the same value two
|
|
984
|
+
// ways. Rounding is therefore d3's: `f` rounds the double at the decimal
|
|
985
|
+
// place, so `.2f` of 1.005 is "1.00" — 1.005 is not representable and the
|
|
986
|
+
// nearest double sits below the half.
|
|
987
|
+
{
|
|
988
|
+
name: "format",
|
|
989
|
+
signature: "format(dyn, string): string",
|
|
990
|
+
// Registered per numeric type rather than as `dyn`. A `dyn` first parameter
|
|
991
|
+
// accepted a string and answered "NaN" — a value that looks like an answer
|
|
992
|
+
// and prints into a document. A genuinely dynamic expression still passes,
|
|
993
|
+
// because cel-js matches `dyn` against any declared parameter type; what
|
|
994
|
+
// this rejects is a STATICALLY known wrong type, at `telo check`.
|
|
995
|
+
register: ["format(double, string): string", "format(int, string): string"],
|
|
996
|
+
category: "formatting",
|
|
997
|
+
summary: "Format a number with a d3-format specifier (`.2f`, `,.2f`, `.1%`, `.2s`).",
|
|
998
|
+
deterministic: true,
|
|
999
|
+
hostBacked: false,
|
|
1000
|
+
build: () => (x, spec) => formatter("format", spec)(formattable("format", x)),
|
|
1001
|
+
checkArgs: (lit) => literalGuard(() => {
|
|
1002
|
+
if (lit[1] !== undefined)
|
|
1003
|
+
formatter("format", lit[1]);
|
|
1004
|
+
if (typeof lit[0] === "bigint")
|
|
1005
|
+
formattable("format", lit[0]);
|
|
1006
|
+
}),
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "fixed",
|
|
1010
|
+
signature: "fixed(dyn, int): string",
|
|
1011
|
+
register: ["fixed(double, int): string", "fixed(int, int): string"],
|
|
1012
|
+
category: "formatting",
|
|
1013
|
+
summary: "Fixed-decimal string with the given number of places (0–10).",
|
|
1014
|
+
deterministic: true,
|
|
1015
|
+
hostBacked: false,
|
|
1016
|
+
build: () => (x, digits) => formatter("fixed", `.${digitCount("fixed", digits)}f`)(formattable("fixed", x)),
|
|
1017
|
+
checkArgs: (lit) => literalGuard(() => {
|
|
1018
|
+
if (lit[1] !== undefined)
|
|
1019
|
+
digitCount("fixed", lit[1]);
|
|
1020
|
+
if (typeof lit[0] === "bigint")
|
|
1021
|
+
formattable("fixed", lit[0]);
|
|
1022
|
+
}),
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "formatDuration",
|
|
1026
|
+
signature: "formatDuration(dyn, int): string",
|
|
1027
|
+
register: [
|
|
1028
|
+
"formatDuration(double, int): string",
|
|
1029
|
+
"formatDuration(int, int): string",
|
|
1030
|
+
],
|
|
1031
|
+
category: "formatting",
|
|
1032
|
+
summary: "Render a minute count against a declared day length: `formatDuration(510, 480)` is `1d 30m`. The day length is an argument because it is a policy, not arithmetic. The result is a rendering for a reader, not a duration literal — its `d` is the declared day, so it must not be fed back into a field that parses a duration.",
|
|
1033
|
+
deterministic: true,
|
|
1034
|
+
hostBacked: false,
|
|
1035
|
+
build: () => (minutes, minutesPerDay) => durationText(minutes, minutesPerDay),
|
|
1036
|
+
checkArgs: (lit) => literalGuard(() => {
|
|
1037
|
+
if (lit[1] !== undefined)
|
|
1038
|
+
durationText(lit[0] ?? 0, lit[1]);
|
|
1039
|
+
}),
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
name: "dateIn",
|
|
1043
|
+
signature: "dateIn(timestamp, string?): string",
|
|
1044
|
+
register: [
|
|
1045
|
+
"dateIn(google.protobuf.Timestamp): string",
|
|
1046
|
+
"dateIn(google.protobuf.Timestamp, string): string",
|
|
1047
|
+
],
|
|
1048
|
+
category: "time",
|
|
1049
|
+
summary: "Calendar date (`YYYY-MM-DD`) of an instant, in an IANA zone (UTC by default).",
|
|
1050
|
+
deterministic: true,
|
|
1051
|
+
hostBacked: false,
|
|
1052
|
+
build: () => (t, tz) => dateInZone(instantArg("dateIn", t), assertZone("dateIn", tz ?? "UTC")),
|
|
1053
|
+
checkArgs: (lit) => literalGuard(() => {
|
|
1054
|
+
if (typeof lit[1] === "string")
|
|
1055
|
+
assertZone("dateIn", lit[1]);
|
|
1056
|
+
}),
|
|
1057
|
+
},
|
|
1058
|
+
{
|
|
1059
|
+
name: "isoIn",
|
|
1060
|
+
signature: "isoIn(timestamp, string?): string",
|
|
1061
|
+
register: [
|
|
1062
|
+
"isoIn(google.protobuf.Timestamp): string",
|
|
1063
|
+
"isoIn(google.protobuf.Timestamp, string): string",
|
|
1064
|
+
],
|
|
1065
|
+
category: "time",
|
|
1066
|
+
summary: "ISO-8601 rendering of an instant, in an IANA zone (UTC by default).",
|
|
1067
|
+
deterministic: true,
|
|
1068
|
+
hostBacked: false,
|
|
1069
|
+
build: () => (t, tz) => isoInZone(instantArg("isoIn", t), assertZone("isoIn", tz ?? "UTC")),
|
|
1070
|
+
checkArgs: (lit) => literalGuard(() => {
|
|
1071
|
+
if (typeof lit[1] === "string")
|
|
1072
|
+
assertZone("isoIn", lit[1]);
|
|
1073
|
+
}),
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
name: "startOfMonth",
|
|
1077
|
+
signature: "startOfMonth(timestamp, string?): timestamp",
|
|
1078
|
+
register: [
|
|
1079
|
+
"startOfMonth(google.protobuf.Timestamp): google.protobuf.Timestamp",
|
|
1080
|
+
"startOfMonth(google.protobuf.Timestamp, string): google.protobuf.Timestamp",
|
|
1081
|
+
],
|
|
1082
|
+
category: "time",
|
|
1083
|
+
summary: "Midnight on the 1st of the instant's month, in an IANA zone (UTC by default).",
|
|
1084
|
+
deterministic: true,
|
|
1085
|
+
hostBacked: false,
|
|
1086
|
+
checkArgs: (lit) => literalGuard(() => {
|
|
1087
|
+
if (typeof lit[1] === "string")
|
|
1088
|
+
assertZone("startOfMonth", lit[1]);
|
|
1089
|
+
}),
|
|
1090
|
+
build: () => (t, tz) => {
|
|
1091
|
+
const zone = assertZone("startOfMonth", tz ?? "UTC");
|
|
1092
|
+
const f = zonedFields(instantArg("startOfMonth", t), zone);
|
|
1093
|
+
return instantOfZoned({ year: f.year, month: f.month, day: 1, hour: 0, minute: 0, second: 0 }, zone);
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
name: "addMonths",
|
|
1098
|
+
signature: "addMonths(timestamp, int, string?): timestamp",
|
|
1099
|
+
register: [
|
|
1100
|
+
"addMonths(google.protobuf.Timestamp, int): google.protobuf.Timestamp",
|
|
1101
|
+
"addMonths(google.protobuf.Timestamp, int, string): google.protobuf.Timestamp",
|
|
1102
|
+
],
|
|
1103
|
+
category: "time",
|
|
1104
|
+
summary: "Shift an instant by whole months in an IANA zone, clamping the day of month (Jan 31 + 1 month is Feb 28).",
|
|
1105
|
+
deterministic: true,
|
|
1106
|
+
hostBacked: false,
|
|
1107
|
+
checkArgs: (lit) => literalGuard(() => {
|
|
1108
|
+
if (typeof lit[2] === "string")
|
|
1109
|
+
assertZone("addMonths", lit[2]);
|
|
1110
|
+
}),
|
|
1111
|
+
build: () => (t, months, tz) => {
|
|
1112
|
+
const zone = assertZone("addMonths", tz ?? "UTC");
|
|
1113
|
+
const f = zonedFields(instantArg("addMonths", t), zone);
|
|
1114
|
+
const shifted = f.year * 12 + (f.month - 1) + Number(months);
|
|
1115
|
+
// `%` takes the dividend's sign in JS, so a negative total would yield
|
|
1116
|
+
// month 0. Unreachable for realistic dates and wrong for free otherwise.
|
|
1117
|
+
const year = Math.floor(shifted / 12);
|
|
1118
|
+
const month = (((shifted % 12) + 12) % 12) + 1;
|
|
1119
|
+
return instantOfZoned({ ...f, year, month, day: Math.min(f.day, daysInMonth(year, month)) }, zone);
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
name: "compact",
|
|
1124
|
+
signature: "compact(dyn): dyn",
|
|
1125
|
+
// A `dyn` parameter accepted an instant (yielding `{}`) and a byte buffer
|
|
1126
|
+
// (yielding `{"0":137,…}`), both silently and both passing `telo check`.
|
|
1127
|
+
register: ["compact(list): list", "compact(map): map"],
|
|
1128
|
+
category: "collection",
|
|
1129
|
+
summary: "Drop entries whose value is null or the empty string, from a map or a list.",
|
|
1130
|
+
deterministic: true,
|
|
1131
|
+
hostBacked: false,
|
|
1132
|
+
build: () => (v) => compactValue(v),
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
name: "merge",
|
|
1136
|
+
signature: "merge(map, map): map",
|
|
1137
|
+
register: ["merge(map, map): map"],
|
|
1138
|
+
category: "collection",
|
|
1139
|
+
summary: "Combine two maps, with the right-hand map winning on a shared key. Use it to add to a map rather than replace it — `merge(defaults, overrides)`.",
|
|
1140
|
+
deterministic: true,
|
|
1141
|
+
hostBacked: false,
|
|
1142
|
+
build: () => (a, b) => mergeMaps(a, b),
|
|
1143
|
+
},
|
|
678
1144
|
// UUID
|
|
679
1145
|
{
|
|
680
1146
|
name: "uuidv1",
|
package/dist/cel/diagnose.d.ts
CHANGED
|
@@ -32,6 +32,11 @@ export declare function functionIndex(env: Environment): FunctionIndex;
|
|
|
32
32
|
export interface CallAudit {
|
|
33
33
|
readonly diagnostics: readonly EngineDiagnostic[];
|
|
34
34
|
readonly calls: readonly CallSite[];
|
|
35
|
+
/** Refusals decided from arguments written as literals. Reported whatever the
|
|
36
|
+
* type-checker said, unlike {@link CallAudit.diagnostics}: a call whose types
|
|
37
|
+
* are all correct and whose specifier is `.2q` type-checks perfectly, and is
|
|
38
|
+
* exactly the defect this catches. */
|
|
39
|
+
readonly argumentIssues: readonly EngineDiagnostic[];
|
|
35
40
|
/** Names that resolve, but that no registered signature accepts as written.
|
|
36
41
|
* The caller appends their signatures to a type-check failure it could not
|
|
37
42
|
* otherwise explain. */
|