@objectstack/formula 4.0.4 → 4.0.5

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/formula@4.0.4 build /home/runner/work/framework/framework/packages/formula
2
+ > @objectstack/formula@4.0.5 build /home/runner/work/framework/framework/packages/formula
3
3
  > tsup --config ../../tsup.config.ts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- ESM dist/index.mjs 8.51 KB
14
- ESM dist/index.mjs.map 22.93 KB
15
- ESM ⚡️ Build success in 83ms
16
- CJS dist/index.js 9.93 KB
17
- CJS dist/index.js.map 23.80 KB
18
- CJS ⚡️ Build success in 85ms
13
+ ESM dist/index.mjs 12.57 KB
14
+ ESM dist/index.mjs.map 33.26 KB
15
+ ESM ⚡️ Build success in 75ms
16
+ CJS dist/index.js 14.06 KB
17
+ CJS dist/index.js.map 34.24 KB
18
+ CJS ⚡️ Build success in 75ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 2326ms
21
- DTS dist/index.d.mts 9.36 KB
22
- DTS dist/index.d.ts 9.36 KB
20
+ DTS ⚡️ Build success in 2249ms
21
+ DTS dist/index.d.mts 10.73 KB
22
+ DTS dist/index.d.ts 10.73 KB
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @objectstack/formula
2
+
3
+ ## 4.0.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 15e0df6: chore: unify all package versions to a single patch release
8
+ - Updated dependencies [15e0df6]
9
+ - @objectstack/spec@4.0.5
package/dist/index.d.mts CHANGED
@@ -162,6 +162,40 @@ declare const DEFAULT_LIMITS: {
162
162
  };
163
163
  declare const celEngine: DialectEngine;
164
164
 
165
+ /**
166
+ * Cron dialect engine.
167
+ *
168
+ * Validates cron expressions at compile time without depending on a parser.
169
+ * Actual schedule firing lives in the scheduler service — this engine just
170
+ * round-trips the expression through `Expression.evaluate`, returning the
171
+ * source so callers can hand it to a scheduler library.
172
+ *
173
+ * Accepted forms:
174
+ * - 5-field standard cron: `m h dom mon dow`
175
+ * - 6-field extended cron: `s m h dom mon dow`
176
+ * - Aliases: @yearly, @annually, @monthly, @weekly, @daily, @hourly, @reboot
177
+ */
178
+
179
+ declare const cronEngine: DialectEngine;
180
+
181
+ /**
182
+ * Template dialect engine — strict Mustache subset.
183
+ *
184
+ * Supports `{{path.to.value}}` interpolation only. No conditionals, no loops,
185
+ * no helpers. The variable scope is the same as CEL (`record`, `previous`,
186
+ * `input`, `os.user`, `os.org`, `os.env`, plus `extra`), so authors can move
187
+ * fluidly between a CEL formula and a template body without re-learning a
188
+ * second variable namespace.
189
+ *
190
+ * Why a separate dialect from CEL: templates produce strings (notification
191
+ * subjects, prompt bodies, titleFormat). CEL is a value-typed expression
192
+ * language. Routing them through the same envelope (`{ dialect: 'template' }`)
193
+ * keeps the AI author rule simple — "anything templated or computed is an
194
+ * Expression" — without conflating the two semantics.
195
+ */
196
+
197
+ declare const templateEngine: DialectEngine;
198
+
165
199
  /**
166
200
  * ObjectStack standard CEL function library.
167
201
  *
@@ -237,4 +271,4 @@ declare function normalizeExpressionTree(root: unknown, path?: string[]): {
237
271
  error: EvalError;
238
272
  } | null;
239
273
 
240
- export { DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, ExpressionEngine, type SeedPrimitive, type SeedValue, buildScope, celEngine, getEngine, hasDialect, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord };
274
+ export { DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, ExpressionEngine, type SeedPrimitive, type SeedValue, buildScope, celEngine, cronEngine, getEngine, hasDialect, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine };
package/dist/index.d.ts CHANGED
@@ -162,6 +162,40 @@ declare const DEFAULT_LIMITS: {
162
162
  };
163
163
  declare const celEngine: DialectEngine;
164
164
 
165
+ /**
166
+ * Cron dialect engine.
167
+ *
168
+ * Validates cron expressions at compile time without depending on a parser.
169
+ * Actual schedule firing lives in the scheduler service — this engine just
170
+ * round-trips the expression through `Expression.evaluate`, returning the
171
+ * source so callers can hand it to a scheduler library.
172
+ *
173
+ * Accepted forms:
174
+ * - 5-field standard cron: `m h dom mon dow`
175
+ * - 6-field extended cron: `s m h dom mon dow`
176
+ * - Aliases: @yearly, @annually, @monthly, @weekly, @daily, @hourly, @reboot
177
+ */
178
+
179
+ declare const cronEngine: DialectEngine;
180
+
181
+ /**
182
+ * Template dialect engine — strict Mustache subset.
183
+ *
184
+ * Supports `{{path.to.value}}` interpolation only. No conditionals, no loops,
185
+ * no helpers. The variable scope is the same as CEL (`record`, `previous`,
186
+ * `input`, `os.user`, `os.org`, `os.env`, plus `extra`), so authors can move
187
+ * fluidly between a CEL formula and a template body without re-learning a
188
+ * second variable namespace.
189
+ *
190
+ * Why a separate dialect from CEL: templates produce strings (notification
191
+ * subjects, prompt bodies, titleFormat). CEL is a value-typed expression
192
+ * language. Routing them through the same envelope (`{ dialect: 'template' }`)
193
+ * keeps the AI author rule simple — "anything templated or computed is an
194
+ * Expression" — without conflating the two semantics.
195
+ */
196
+
197
+ declare const templateEngine: DialectEngine;
198
+
165
199
  /**
166
200
  * ObjectStack standard CEL function library.
167
201
  *
@@ -237,4 +271,4 @@ declare function normalizeExpressionTree(root: unknown, path?: string[]): {
237
271
  error: EvalError;
238
272
  } | null;
239
273
 
240
- export { DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, ExpressionEngine, type SeedPrimitive, type SeedValue, buildScope, celEngine, getEngine, hasDialect, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord };
274
+ export { DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, ExpressionEngine, type SeedPrimitive, type SeedValue, buildScope, celEngine, cronEngine, getEngine, hasDialect, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine };
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  ExpressionEngine: () => ExpressionEngine,
25
25
  buildScope: () => buildScope,
26
26
  celEngine: () => celEngine,
27
+ cronEngine: () => cronEngine,
27
28
  getEngine: () => getEngine,
28
29
  hasDialect: () => hasDialect,
29
30
  normalizeExpression: () => normalizeExpression,
@@ -31,7 +32,8 @@ __export(index_exports, {
31
32
  register: () => register,
32
33
  registerStdLib: () => registerStdLib,
33
34
  resolveSeed: () => resolveSeed,
34
- resolveSeedRecord: () => resolveSeedRecord
35
+ resolveSeedRecord: () => resolveSeedRecord,
36
+ templateEngine: () => templateEngine
35
37
  });
36
38
  module.exports = __toCommonJS(index_exports);
37
39
 
@@ -59,6 +61,17 @@ function registerStdLib(env, now) {
59
61
  ).registerFunction(
60
62
  "daysAgo(int): google.protobuf.Timestamp",
61
63
  (n) => addDaysUtc(now(), -Number(n))
64
+ ).registerFunction(
65
+ "isBlank(dyn): bool",
66
+ (value) => {
67
+ if (value === null || value === void 0) return true;
68
+ if (typeof value === "string") return value.length === 0;
69
+ if (Array.isArray(value)) return value.length === 0;
70
+ return false;
71
+ }
72
+ ).registerFunction(
73
+ "coalesce(dyn, dyn): dyn",
74
+ (value, fallback) => value === null || value === void 0 ? fallback : value
62
75
  );
63
76
  }
64
77
  function buildScope(ctx) {
@@ -158,6 +171,143 @@ var celEngine = {
158
171
  }
159
172
  };
160
173
 
174
+ // src/cron-engine.ts
175
+ var ALIASES = /* @__PURE__ */ new Set([
176
+ "@yearly",
177
+ "@annually",
178
+ "@monthly",
179
+ "@weekly",
180
+ "@daily",
181
+ "@hourly",
182
+ "@reboot"
183
+ ]);
184
+ function validate(source) {
185
+ const trimmed = source.trim();
186
+ if (trimmed.length === 0) {
187
+ return { ok: false, error: { kind: "parse", message: "cron source is empty" } };
188
+ }
189
+ if (trimmed.startsWith("@")) {
190
+ if (!ALIASES.has(trimmed)) {
191
+ return {
192
+ ok: false,
193
+ error: { kind: "parse", message: `unknown cron alias '${trimmed}'` }
194
+ };
195
+ }
196
+ return { ok: true, value: trimmed };
197
+ }
198
+ const fields = trimmed.split(/\s+/);
199
+ if (fields.length !== 5 && fields.length !== 6) {
200
+ return {
201
+ ok: false,
202
+ error: {
203
+ kind: "parse",
204
+ message: `cron requires 5 or 6 space-separated fields, got ${fields.length}`
205
+ }
206
+ };
207
+ }
208
+ const allowed = /^[\d*/,\-?LWA-Z#]+$/i;
209
+ for (let i = 0; i < fields.length; i++) {
210
+ if (!allowed.test(fields[i])) {
211
+ return {
212
+ ok: false,
213
+ error: {
214
+ kind: "parse",
215
+ message: `cron field ${i + 1} contains invalid characters: '${fields[i]}'`
216
+ }
217
+ };
218
+ }
219
+ }
220
+ return { ok: true, value: trimmed };
221
+ }
222
+ var cronEngine = {
223
+ dialect: "cron",
224
+ compile(source) {
225
+ return validate(source);
226
+ },
227
+ evaluate(expr, _ctx) {
228
+ if (expr.dialect !== "cron") {
229
+ return {
230
+ ok: false,
231
+ error: { kind: "dialect", message: `cronEngine cannot evaluate dialect '${expr.dialect}'` }
232
+ };
233
+ }
234
+ if (typeof expr.source !== "string") {
235
+ return { ok: false, error: { kind: "parse", message: "cron Expression.source required" } };
236
+ }
237
+ const result = validate(expr.source);
238
+ if (!result.ok) return result;
239
+ return { ok: true, value: result.value };
240
+ }
241
+ };
242
+
243
+ // src/template-engine.ts
244
+ var PATH_RE = /\{\{\s*([\w.[\]]+?)\s*\}\}/g;
245
+ function resolvePath(scope, path) {
246
+ const normalized = path.replace(/\[(\w+)\]/g, ".$1");
247
+ const segments = normalized.split(".").filter(Boolean);
248
+ let cursor = scope;
249
+ for (const seg of segments) {
250
+ if (cursor == null || typeof cursor !== "object") return void 0;
251
+ cursor = cursor[seg];
252
+ }
253
+ return cursor;
254
+ }
255
+ function stringify(value) {
256
+ if (value === null || value === void 0) return "";
257
+ if (value instanceof Date) return value.toISOString();
258
+ if (typeof value === "string") return value;
259
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
260
+ if (typeof value === "bigint") return value.toString();
261
+ try {
262
+ return JSON.stringify(value);
263
+ } catch {
264
+ return String(value);
265
+ }
266
+ }
267
+ function compileTemplate(source) {
268
+ const matches = source.match(/\{\{|\}\}/g) ?? [];
269
+ if (matches.length % 2 !== 0) {
270
+ return {
271
+ ok: false,
272
+ error: { kind: "parse", message: "template has unbalanced {{ }} delimiters" }
273
+ };
274
+ }
275
+ const refs = [];
276
+ let m;
277
+ PATH_RE.lastIndex = 0;
278
+ while ((m = PATH_RE.exec(source)) !== null) {
279
+ refs.push(m[1]);
280
+ }
281
+ return { ok: true, value: refs };
282
+ }
283
+ var templateEngine = {
284
+ dialect: "template",
285
+ compile(source) {
286
+ return compileTemplate(source);
287
+ },
288
+ evaluate(expr, ctx) {
289
+ if (expr.dialect !== "template") {
290
+ return {
291
+ ok: false,
292
+ error: { kind: "dialect", message: `templateEngine cannot evaluate dialect '${expr.dialect}'` }
293
+ };
294
+ }
295
+ if (typeof expr.source !== "string") {
296
+ return {
297
+ ok: false,
298
+ error: { kind: "parse", message: "template Expression.source required" }
299
+ };
300
+ }
301
+ const check = compileTemplate(expr.source);
302
+ if (!check.ok) return check;
303
+ const scope = buildScope(ctx);
304
+ const out = expr.source.replace(PATH_RE, (_match, path) => {
305
+ return stringify(resolvePath(scope, path));
306
+ });
307
+ return { ok: true, value: out };
308
+ }
309
+ };
310
+
161
311
  // src/registry.ts
162
312
  var registry = /* @__PURE__ */ new Map();
163
313
  function register(engine) {
@@ -177,8 +327,9 @@ function makeStub(dialect, reason) {
177
327
  };
178
328
  }
179
329
  register(celEngine);
330
+ register(cronEngine);
331
+ register(templateEngine);
180
332
  register(makeStub("js", "dialect 'js' not registered. Install @objectstack/plugin-js-vm"));
181
- register(makeStub("cron", "dialect 'cron' not registered. Install @objectstack/plugin-cron"));
182
333
  var ExpressionEngine = {
183
334
  register,
184
335
  getEngine,
@@ -323,6 +474,7 @@ function looksLikeExpression(value) {
323
474
  ExpressionEngine,
324
475
  buildScope,
325
476
  celEngine,
477
+ cronEngine,
326
478
  getEngine,
327
479
  hasDialect,
328
480
  normalizeExpression,
@@ -330,6 +482,7 @@ function looksLikeExpression(value) {
330
482
  register,
331
483
  registerStdLib,
332
484
  resolveSeed,
333
- resolveSeedRecord
485
+ resolveSeedRecord,
486
+ templateEngine
334
487
  });
335
488
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cel-engine.ts","../src/stdlib.ts","../src/registry.ts","../src/seed-eval.ts","../src/normalize.ts"],"sourcesContent":["/**\n * @objectstack/formula\n *\n * Canonical expression engine for ObjectStack. CEL (Common Expression\n * Language) is the default dialect; `js` and `cron` are dispatched to\n * dedicated plugin engines.\n *\n * @see content/docs/concepts/north-star.mdx §8 \"No private expression DSL\"\n * @see ROADMAP.md M9 \"Expression Unification\"\n */\n\nexport { ExpressionEngine, getEngine, hasDialect, register } from './registry';\nexport { celEngine, DEFAULT_LIMITS } from './cel-engine';\nexport { registerStdLib, buildScope } from './stdlib';\nexport { resolveSeed, resolveSeedRecord } from './seed-eval';\nexport { normalizeExpression, normalizeExpressionTree } from './normalize';\nexport type { SeedValue, SeedPrimitive } from './seed-eval';\nexport type { DialectEngine, EvalContext, EvalResult, EvalError } from './types';\n","/**\n * CEL dialect engine — wraps `@marcbachmann/cel-js` with the ObjectStack\n * stdlib, bounded execution limits, and result coercion.\n *\n * Why a thin wrapper:\n *\n * - cel-js returns `BigInt` for ints. The kernel and CRM expect plain\n * numbers, so we coerce at the boundary.\n * - cel-js parses dotted names as receiver-typed methods; we register\n * `now()`, `today()`, `daysFromNow()` as bare functions and let `os.*`\n * refer to context data only (see {@link buildScope}).\n * - Bounds (`maxAstNodes`, `maxDepth`, …) are enforced spec-wide so\n * third-party plugins can't ship runaway predicates.\n */\n\nimport { Environment } from '@marcbachmann/cel-js';\nimport type { Expression } from '@objectstack/spec';\n\nimport { buildScope, registerStdLib } from './stdlib';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\n/**\n * Default execution bounds. Picked conservatively — every metadata-authored\n * expression we've seen is well under these. If you hit them, the expression\n * is too complex for ObjectStack and should be moved to a hook (`dialect: js`).\n */\nexport const DEFAULT_LIMITS = {\n maxAstNodes: 256,\n maxDepth: 32,\n maxListElements: 64,\n maxMapEntries: 64,\n maxCallArguments: 16,\n} as const;\n\nfunction buildEnv(now: () => Date): Environment {\n const env = new Environment({\n unlistedVariablesAreDyn: true,\n enableOptionalTypes: true,\n limits: DEFAULT_LIMITS,\n });\n return registerStdLib(env, now);\n}\n\n/** Coerce cel-js's BigInt-flavored return into spec-friendly JS values. */\nfunction coerce(value: unknown): unknown {\n if (typeof value === 'bigint') {\n // BigInt → number when safe, else string to avoid silent truncation.\n if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {\n return Number(value);\n }\n return value.toString();\n }\n if (Array.isArray(value)) return value.map(coerce);\n if (value && typeof value === 'object' && !(value instanceof Date)) {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = coerce(v);\n return out;\n }\n return value;\n}\n\nfunction classifyError(err: unknown): EvalResult<never> {\n const message = err instanceof Error ? err.message : String(err);\n let kind: 'parse' | 'type' | 'runtime' | 'bounds' = 'runtime';\n if (/Exceeded max/i.test(message)) kind = 'bounds';\n else if (/parse|unexpected|syntax/i.test(message)) kind = 'parse';\n else if (/type|unknown variable|undeclared/i.test(message)) kind = 'type';\n return { ok: false, error: { kind, message } };\n}\n\nexport const celEngine: DialectEngine = {\n dialect: 'cel',\n\n compile(source: string): EvalResult<unknown> {\n try {\n // We use a wall-clock now() here purely for parse-time stdlib\n // type-checking; the function is never actually called.\n const env = buildEnv(() => new Date(0));\n const compiled = env.parse(source);\n // Surface check errors eagerly.\n const checkErrors = compiled.check?.();\n if (checkErrors && Array.isArray(checkErrors) && checkErrors.length > 0) {\n return {\n ok: false,\n error: { kind: 'type', message: checkErrors.join('; ') },\n };\n }\n return { ok: true, value: compiled.ast };\n } catch (err) {\n return classifyError(err);\n }\n },\n\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'cel') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `celEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n const source = expr.source;\n if (typeof source !== 'string' || source.length === 0) {\n // AST-only inputs: cel-js does not currently expose a public API to\n // re-execute a parsed AST without re-serializing. We persist `source`\n // as the canonical form during M9.1 and revisit AST-only execution in\n // M9.7 when we cut the spec persistence over.\n return {\n ok: false,\n error: { kind: 'parse', message: 'AST-only evaluation not yet supported; persist `source`' },\n };\n }\n\n const now = () => ctx.now ?? new Date();\n try {\n const env = buildEnv(now);\n const scope = buildScope(ctx);\n const raw = env.evaluate(source, scope);\n return { ok: true, value: coerce(raw) as T };\n } catch (err) {\n return classifyError(err);\n }\n },\n};\n","/**\n * ObjectStack standard CEL function library.\n *\n * Registered into the per-evaluation `Environment` by the CEL engine. All\n * functions are pure given a pinned `now` — that determinism is what makes\n * `objectstack build` artifacts byte-stable across runs.\n *\n * Function naming intentionally avoids the `os.` prefix because cel-js binds\n * dotted names to receiver types. Instead, the `os` namespace in CEL holds\n * *data* (`os.user`, `os.org`, `os.env`) supplied by the caller's\n * {@link EvalContext}.\n */\n\nimport type { Environment } from '@marcbachmann/cel-js';\n\nimport type { EvalContext } from './types';\n\n/** Truncate a Date to start-of-day in UTC. */\nfunction startOfDayUtc(d: Date): Date {\n const out = new Date(d.getTime());\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\n/** Add `n` days to a Date in UTC; returns a new Date. */\nfunction addDaysUtc(d: Date, n: number): Date {\n const out = new Date(d.getTime());\n out.setUTCDate(out.getUTCDate() + n);\n return out;\n}\n\n/**\n * Register the ObjectStack standard library into a CEL environment.\n *\n * The `now` resolver is closed over so each call uses the pinned\n * `EvalContext.now` (or wall-clock fallback). Implementations are kept tiny\n * and dependency-free — they're the contract surface for AI authors and must\n * stay legible.\n */\nexport function registerStdLib(\n env: Environment,\n now: () => Date,\n): Environment {\n return env\n .registerFunction('now(): google.protobuf.Timestamp', () => now())\n .registerFunction(\n 'today(): google.protobuf.Timestamp',\n () => startOfDayUtc(now()),\n )\n .registerFunction(\n 'daysFromNow(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), Number(n)),\n )\n .registerFunction(\n 'daysAgo(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), -Number(n)),\n );\n}\n\n/**\n * Build the variable scope for a single evaluation. Absent fields are simply\n * not bound — CEL macros (`has(record.foo)`) handle missing-key safely.\n */\nexport function buildScope(ctx: EvalContext): Record<string, unknown> {\n const scope: Record<string, unknown> = {};\n\n if (ctx.record !== undefined) scope.record = ctx.record;\n if (ctx.previous !== undefined) scope.previous = ctx.previous;\n if (ctx.input !== undefined) scope.input = ctx.input;\n\n // Namespaced data — written as `os.user.id`, `os.env`, etc. in CEL.\n const os: Record<string, unknown> = {};\n if (ctx.user !== undefined) os.user = ctx.user;\n if (ctx.org !== undefined) os.org = ctx.org;\n if (ctx.env !== undefined) os.env = ctx.env;\n if (Object.keys(os).length > 0) scope.os = os;\n\n if (ctx.extra !== undefined) Object.assign(scope, ctx.extra);\n\n return scope;\n}\n","/**\n * Dialect-pluggable Expression engine registry.\n *\n * Replaces the per-call-site `compileFormula` / `evaluateFormula` direct\n * imports of the deleted custom engine. Call sites now ask the registry to\n * dispatch by `expression.dialect`.\n *\n * Stub dialects (`js`, `cron`) are registered at module load with explicit\n * `dialect`-error responses so call sites get a clear message instead of\n * silent `undefined` (the old engine's anti-pattern).\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport { celEngine } from './cel-engine';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst registry = new Map<string, DialectEngine>();\n\n/** Register or replace a dialect engine. */\nexport function register(engine: DialectEngine): void {\n registry.set(engine.dialect, engine);\n}\n\n/** Look up a dialect engine without dispatching. */\nexport function getEngine(dialect: string): DialectEngine | undefined {\n return registry.get(dialect);\n}\n\n/** Whether a dialect has a real (non-stub) implementation registered. */\nexport function hasDialect(dialect: string): boolean {\n return registry.has(dialect) && !registry.get(dialect)!.dialect.startsWith('stub:');\n}\n\nfunction makeStub(dialect: string, reason: string): DialectEngine {\n return {\n dialect,\n compile: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n evaluate: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n };\n}\n\n// Real engines.\nregister(celEngine);\n\n// Stubs — phased in by later milestones (M9.5+ for `js`, M9.6 for `cron`).\nregister(makeStub('js', \"dialect 'js' not registered. Install @objectstack/plugin-js-vm\"));\nregister(makeStub('cron', \"dialect 'cron' not registered. Install @objectstack/plugin-cron\"));\n\n/**\n * The unified evaluation entry point. Replaces the old direct calls to\n * `evaluateFormula` from the deleted custom engine.\n */\nexport const ExpressionEngine = {\n register,\n getEngine,\n hasDialect,\n\n /**\n * Compile-only — parse + type-check, returning the engine-native AST. Used\n * by `objectstack compile` to normalize source into AST in artifacts.\n */\n compile(expr: Expression): EvalResult<unknown> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return {\n ok: false,\n error: { kind: 'parse', message: 'Expression.source required for compile()' },\n };\n }\n return engine.compile(expr.source);\n },\n\n /**\n * Evaluate an expression in the given context. Never throws — branch on\n * `result.ok`. Errors carry a `kind` for caller-side classification.\n */\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n return engine.evaluate<T>(expr, ctx);\n },\n};\n","/**\n * Seed-value resolver.\n *\n * `Dataset.records` accepts {@link SeedValue} = primitive | Expression | array\n * | object — install-time resolution walks the tree and replaces any\n * Expression node with its evaluated result. This is what makes\n * `close_date: cel\\`now() + duration(\"P30D\")\\`` resolve to *the customer's*\n * \"today + 30 days\" instead of the developer's compile-time clock.\n */\n\nimport { ExpressionSchema, type Expression } from '@objectstack/spec';\n\nimport type { EvalContext, EvalResult } from './types';\nimport { ExpressionEngine } from './registry';\n\nexport type SeedPrimitive = string | number | boolean | null | Date;\nexport type SeedValue = SeedPrimitive | Expression | SeedValue[] | { [key: string]: SeedValue };\n\n/** Detect an Expression-shaped object without throwing on unrelated shapes. */\nfunction isExpressionLike(value: unknown): value is Expression {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n\n/**\n * Recursively resolve a SeedValue. Records that contain Expression leaves are\n * evaluated with `ctx`; other values are passed through unchanged.\n *\n * Returns the first failure encountered. Callers (seed loader) typically\n * abort the whole record on failure rather than silently writing partial data.\n */\nexport function resolveSeed(\n value: SeedValue,\n ctx: EvalContext,\n): EvalResult<unknown> {\n if (value === null || value === undefined) {\n return { ok: true, value };\n }\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return { ok: true, value };\n }\n if (value instanceof Date) {\n return { ok: true, value };\n }\n if (Array.isArray(value)) {\n const out: unknown[] = [];\n for (const item of value) {\n const r = resolveSeed(item, ctx);\n if (!r.ok) return r;\n out.push(r.value);\n }\n return { ok: true, value: out };\n }\n if (isExpressionLike(value)) {\n return ExpressionEngine.evaluate(value, ctx);\n }\n // Plain object — recurse field-by-field.\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, SeedValue>)) {\n const r = resolveSeed(v, ctx);\n if (!r.ok) return r;\n out[k] = r.value;\n }\n return { ok: true, value: out };\n}\n\n/**\n * Resolve a single record (object of fields), pinning `ctx.now` so all\n * expressions within see one logical clock.\n */\nexport function resolveSeedRecord(\n record: Record<string, SeedValue>,\n ctx: EvalContext,\n): EvalResult<Record<string, unknown>> {\n const pinnedCtx: EvalContext = { ...ctx, now: ctx.now ?? new Date() };\n const result = resolveSeed(record, pinnedCtx) as EvalResult<Record<string, unknown>>;\n return result;\n}\n","/**\n * Build-time normalization helpers.\n *\n * The CLI `objectstack compile` step walks the assembled `objectstack.json`\n * artifact and rewrites every Expression so that:\n *\n * 1. String shorthand input is replaced by `{ dialect: 'cel', source }`.\n * 2. The persisted envelope carries an `ast` field produced by the dialect\n * engine (M9.2 deliverable). Source is retained for round-trip / debug.\n *\n * Spec layer cannot do step 2 because it must remain dependency-free; this\n * package owns the engine import and therefore the AST step.\n */\n\nimport {\n ExpressionInputSchema,\n ExpressionSchema,\n type Expression,\n type ExpressionInput,\n} from '@objectstack/spec';\n\nimport { ExpressionEngine } from './registry';\nimport type { EvalResult } from './types';\n\n/**\n * Normalize an {@link ExpressionInput} (string shorthand OR full envelope) into\n * a fully-resolved {@link Expression} carrying both `source` and `ast`.\n *\n * Returns an EvalResult so the caller can render a structured compile error\n * pointing at the offending metadata path.\n */\nexport function normalizeExpression(input: ExpressionInput): EvalResult<Expression> {\n const parsed = ExpressionInputSchema.safeParse(input);\n if (!parsed.success) {\n return {\n ok: false,\n error: { kind: 'parse', message: parsed.error.message },\n };\n }\n\n const expr = parsed.data as Expression;\n\n // Already AST-only — accept as-is.\n if (expr.ast !== undefined && expr.source === undefined) {\n return { ok: true, value: expr };\n }\n\n // Source-bearing: ask the dialect engine to compile. Failures surface here\n // as part of the build (no silent skip).\n const compiled = ExpressionEngine.compile(expr);\n if (!compiled.ok) {\n return compiled;\n }\n\n return {\n ok: true,\n value: {\n ...expr,\n ast: compiled.value,\n },\n };\n}\n\n/**\n * Walk an arbitrary JSON tree and normalize every embedded Expression in\n * place. Used by the build pipeline to traverse the assembled metadata\n * artifact. Returns the first error encountered (paired with the dotted path\n * for diagnostics) or `null` when fully clean.\n */\nexport function normalizeExpressionTree(\n root: unknown,\n path: string[] = [],\n): { path: string; error: import('./types').EvalError } | null {\n if (root === null || typeof root !== 'object') return null;\n\n if (looksLikeExpression(root)) {\n const r = normalizeExpression(root as ExpressionInput);\n if (!r.ok) return { path: path.join('.'), error: r.error };\n Object.assign(root as Record<string, unknown>, r.value);\n return null;\n }\n\n if (Array.isArray(root)) {\n for (let i = 0; i < root.length; i++) {\n const r = normalizeExpressionTree(root[i], [...path, String(i)]);\n if (r) return r;\n }\n return null;\n }\n\n for (const [k, v] of Object.entries(root as Record<string, unknown>)) {\n const r = normalizeExpressionTree(v, [...path, k]);\n if (r) return r;\n }\n return null;\n}\n\nfunction looksLikeExpression(value: unknown): boolean {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n"],"mappings":"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","names":["out","import_spec"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cel-engine.ts","../src/stdlib.ts","../src/cron-engine.ts","../src/template-engine.ts","../src/registry.ts","../src/seed-eval.ts","../src/normalize.ts"],"sourcesContent":["/**\n * @objectstack/formula\n *\n * Canonical expression engine for ObjectStack. CEL (Common Expression\n * Language) is the default dialect; `js` and `cron` are dispatched to\n * dedicated plugin engines.\n *\n * @see content/docs/concepts/north-star.mdx §8 \"No private expression DSL\"\n * @see ROADMAP.md M9 \"Expression Unification\"\n */\n\nexport { ExpressionEngine, getEngine, hasDialect, register } from './registry';\nexport { celEngine, DEFAULT_LIMITS } from './cel-engine';\nexport { cronEngine } from './cron-engine';\nexport { templateEngine } from './template-engine';\nexport { registerStdLib, buildScope } from './stdlib';\nexport { resolveSeed, resolveSeedRecord } from './seed-eval';\nexport { normalizeExpression, normalizeExpressionTree } from './normalize';\nexport type { SeedValue, SeedPrimitive } from './seed-eval';\nexport type { DialectEngine, EvalContext, EvalResult, EvalError } from './types';\n","/**\n * CEL dialect engine — wraps `@marcbachmann/cel-js` with the ObjectStack\n * stdlib, bounded execution limits, and result coercion.\n *\n * Why a thin wrapper:\n *\n * - cel-js returns `BigInt` for ints. The kernel and CRM expect plain\n * numbers, so we coerce at the boundary.\n * - cel-js parses dotted names as receiver-typed methods; we register\n * `now()`, `today()`, `daysFromNow()` as bare functions and let `os.*`\n * refer to context data only (see {@link buildScope}).\n * - Bounds (`maxAstNodes`, `maxDepth`, …) are enforced spec-wide so\n * third-party plugins can't ship runaway predicates.\n */\n\nimport { Environment } from '@marcbachmann/cel-js';\nimport type { Expression } from '@objectstack/spec';\n\nimport { buildScope, registerStdLib } from './stdlib';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\n/**\n * Default execution bounds. Picked conservatively — every metadata-authored\n * expression we've seen is well under these. If you hit them, the expression\n * is too complex for ObjectStack and should be moved to a hook (`dialect: js`).\n */\nexport const DEFAULT_LIMITS = {\n maxAstNodes: 256,\n maxDepth: 32,\n maxListElements: 64,\n maxMapEntries: 64,\n maxCallArguments: 16,\n} as const;\n\nfunction buildEnv(now: () => Date): Environment {\n const env = new Environment({\n unlistedVariablesAreDyn: true,\n enableOptionalTypes: true,\n limits: DEFAULT_LIMITS,\n });\n return registerStdLib(env, now);\n}\n\n/** Coerce cel-js's BigInt-flavored return into spec-friendly JS values. */\nfunction coerce(value: unknown): unknown {\n if (typeof value === 'bigint') {\n // BigInt → number when safe, else string to avoid silent truncation.\n if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {\n return Number(value);\n }\n return value.toString();\n }\n if (Array.isArray(value)) return value.map(coerce);\n if (value && typeof value === 'object' && !(value instanceof Date)) {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = coerce(v);\n return out;\n }\n return value;\n}\n\nfunction classifyError(err: unknown): EvalResult<never> {\n const message = err instanceof Error ? err.message : String(err);\n let kind: 'parse' | 'type' | 'runtime' | 'bounds' = 'runtime';\n if (/Exceeded max/i.test(message)) kind = 'bounds';\n else if (/parse|unexpected|syntax/i.test(message)) kind = 'parse';\n else if (/type|unknown variable|undeclared/i.test(message)) kind = 'type';\n return { ok: false, error: { kind, message } };\n}\n\nexport const celEngine: DialectEngine = {\n dialect: 'cel',\n\n compile(source: string): EvalResult<unknown> {\n try {\n // We use a wall-clock now() here purely for parse-time stdlib\n // type-checking; the function is never actually called.\n const env = buildEnv(() => new Date(0));\n const compiled = env.parse(source);\n // Surface check errors eagerly.\n const checkErrors = compiled.check?.();\n if (checkErrors && Array.isArray(checkErrors) && checkErrors.length > 0) {\n return {\n ok: false,\n error: { kind: 'type', message: checkErrors.join('; ') },\n };\n }\n return { ok: true, value: compiled.ast };\n } catch (err) {\n return classifyError(err);\n }\n },\n\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'cel') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `celEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n const source = expr.source;\n if (typeof source !== 'string' || source.length === 0) {\n // AST-only inputs: cel-js does not currently expose a public API to\n // re-execute a parsed AST without re-serializing. We persist `source`\n // as the canonical form during M9.1 and revisit AST-only execution in\n // M9.7 when we cut the spec persistence over.\n return {\n ok: false,\n error: { kind: 'parse', message: 'AST-only evaluation not yet supported; persist `source`' },\n };\n }\n\n const now = () => ctx.now ?? new Date();\n try {\n const env = buildEnv(now);\n const scope = buildScope(ctx);\n const raw = env.evaluate(source, scope);\n return { ok: true, value: coerce(raw) as T };\n } catch (err) {\n return classifyError(err);\n }\n },\n};\n","/**\n * ObjectStack standard CEL function library.\n *\n * Registered into the per-evaluation `Environment` by the CEL engine. All\n * functions are pure given a pinned `now` — that determinism is what makes\n * `objectstack build` artifacts byte-stable across runs.\n *\n * Function naming intentionally avoids the `os.` prefix because cel-js binds\n * dotted names to receiver types. Instead, the `os` namespace in CEL holds\n * *data* (`os.user`, `os.org`, `os.env`) supplied by the caller's\n * {@link EvalContext}.\n */\n\nimport type { Environment } from '@marcbachmann/cel-js';\n\nimport type { EvalContext } from './types';\n\n/** Truncate a Date to start-of-day in UTC. */\nfunction startOfDayUtc(d: Date): Date {\n const out = new Date(d.getTime());\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\n/** Add `n` days to a Date in UTC; returns a new Date. */\nfunction addDaysUtc(d: Date, n: number): Date {\n const out = new Date(d.getTime());\n out.setUTCDate(out.getUTCDate() + n);\n return out;\n}\n\n/**\n * Register the ObjectStack standard library into a CEL environment.\n *\n * The `now` resolver is closed over so each call uses the pinned\n * `EvalContext.now` (or wall-clock fallback). Implementations are kept tiny\n * and dependency-free — they're the contract surface for AI authors and must\n * stay legible.\n */\nexport function registerStdLib(\n env: Environment,\n now: () => Date,\n): Environment {\n return env\n .registerFunction('now(): google.protobuf.Timestamp', () => now())\n .registerFunction(\n 'today(): google.protobuf.Timestamp',\n () => startOfDayUtc(now()),\n )\n .registerFunction(\n 'daysFromNow(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), Number(n)),\n )\n .registerFunction(\n 'daysAgo(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), -Number(n)),\n )\n // Returns true when `value` is null, undefined, empty string, or empty list.\n // Matches the intent of legacy `ISBLANK()` while staying CEL-idiomatic.\n .registerFunction(\n 'isBlank(dyn): bool',\n (value: unknown) => {\n if (value === null || value === undefined) return true;\n if (typeof value === 'string') return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n return false;\n },\n )\n // Returns `value` when not null/undefined, otherwise the `fallback`.\n // Use this to safely concatenate optional string fields:\n // coalesce(record.salutation, '') + ' ' + coalesce(record.first_name, '')\n .registerFunction(\n 'coalesce(dyn, dyn): dyn',\n (value: unknown, fallback: unknown) =>\n (value === null || value === undefined) ? fallback : value,\n );\n}\n\n/**\n * Build the variable scope for a single evaluation. Absent fields are simply\n * not bound — CEL macros (`has(record.foo)`) handle missing-key safely.\n */\nexport function buildScope(ctx: EvalContext): Record<string, unknown> {\n const scope: Record<string, unknown> = {};\n\n if (ctx.record !== undefined) scope.record = ctx.record;\n if (ctx.previous !== undefined) scope.previous = ctx.previous;\n if (ctx.input !== undefined) scope.input = ctx.input;\n\n // Namespaced data — written as `os.user.id`, `os.env`, etc. in CEL.\n const os: Record<string, unknown> = {};\n if (ctx.user !== undefined) os.user = ctx.user;\n if (ctx.org !== undefined) os.org = ctx.org;\n if (ctx.env !== undefined) os.env = ctx.env;\n if (Object.keys(os).length > 0) scope.os = os;\n\n if (ctx.extra !== undefined) Object.assign(scope, ctx.extra);\n\n return scope;\n}\n","/**\n * Cron dialect engine.\n *\n * Validates cron expressions at compile time without depending on a parser.\n * Actual schedule firing lives in the scheduler service — this engine just\n * round-trips the expression through `Expression.evaluate`, returning the\n * source so callers can hand it to a scheduler library.\n *\n * Accepted forms:\n * - 5-field standard cron: `m h dom mon dow`\n * - 6-field extended cron: `s m h dom mon dow`\n * - Aliases: @yearly, @annually, @monthly, @weekly, @daily, @hourly, @reboot\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst ALIASES = new Set([\n '@yearly', '@annually', '@monthly', '@weekly', '@daily', '@hourly', '@reboot',\n]);\n\nfunction validate(source: string): EvalResult<string> {\n const trimmed = source.trim();\n if (trimmed.length === 0) {\n return { ok: false, error: { kind: 'parse', message: 'cron source is empty' } };\n }\n if (trimmed.startsWith('@')) {\n if (!ALIASES.has(trimmed)) {\n return {\n ok: false,\n error: { kind: 'parse', message: `unknown cron alias '${trimmed}'` },\n };\n }\n return { ok: true, value: trimmed };\n }\n const fields = trimmed.split(/\\s+/);\n if (fields.length !== 5 && fields.length !== 6) {\n return {\n ok: false,\n error: {\n kind: 'parse',\n message: `cron requires 5 or 6 space-separated fields, got ${fields.length}`,\n },\n };\n }\n // Each field must use only allowed cron characters.\n const allowed = /^[\\d*/,\\-?LWA-Z#]+$/i;\n for (let i = 0; i < fields.length; i++) {\n if (!allowed.test(fields[i])) {\n return {\n ok: false,\n error: {\n kind: 'parse',\n message: `cron field ${i + 1} contains invalid characters: '${fields[i]}'`,\n },\n };\n }\n }\n return { ok: true, value: trimmed };\n}\n\nexport const cronEngine: DialectEngine = {\n dialect: 'cron',\n\n compile(source: string): EvalResult<unknown> {\n return validate(source);\n },\n\n evaluate<T = unknown>(expr: Expression, _ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'cron') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `cronEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return { ok: false, error: { kind: 'parse', message: 'cron Expression.source required' } };\n }\n const result = validate(expr.source);\n if (!result.ok) return result as EvalResult<T>;\n // Cron expressions don't \"evaluate\" to a value at predicate time — they\n // describe a schedule. Returning the source lets schedulers consume it.\n return { ok: true, value: result.value as unknown as T };\n },\n};\n","/**\n * Template dialect engine — strict Mustache subset.\n *\n * Supports `{{path.to.value}}` interpolation only. No conditionals, no loops,\n * no helpers. The variable scope is the same as CEL (`record`, `previous`,\n * `input`, `os.user`, `os.org`, `os.env`, plus `extra`), so authors can move\n * fluidly between a CEL formula and a template body without re-learning a\n * second variable namespace.\n *\n * Why a separate dialect from CEL: templates produce strings (notification\n * subjects, prompt bodies, titleFormat). CEL is a value-typed expression\n * language. Routing them through the same envelope (`{ dialect: 'template' }`)\n * keeps the AI author rule simple — \"anything templated or computed is an\n * Expression\" — without conflating the two semantics.\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport { buildScope } from './stdlib';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst PATH_RE = /\\{\\{\\s*([\\w.[\\]]+?)\\s*\\}\\}/g;\n\nfunction resolvePath(scope: Record<string, unknown>, path: string): unknown {\n // Support `a.b.c` and `a[0].b` style. Bracket notation collapses to dotted.\n const normalized = path.replace(/\\[(\\w+)\\]/g, '.$1');\n const segments = normalized.split('.').filter(Boolean);\n let cursor: unknown = scope;\n for (const seg of segments) {\n if (cursor == null || typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\nfunction stringify(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n if (typeof value === 'bigint') return value.toString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nfunction compileTemplate(source: string): EvalResult<string[]> {\n // Compile is only a structural validity check — no helpers, no balanced\n // open/close beyond what the regex enforces.\n const matches = source.match(/\\{\\{|\\}\\}/g) ?? [];\n if (matches.length % 2 !== 0) {\n return {\n ok: false,\n error: { kind: 'parse', message: 'template has unbalanced {{ }} delimiters' },\n };\n }\n const refs: string[] = [];\n let m: RegExpExecArray | null;\n PATH_RE.lastIndex = 0;\n while ((m = PATH_RE.exec(source)) !== null) {\n refs.push(m[1]);\n }\n return { ok: true, value: refs };\n}\n\nexport const templateEngine: DialectEngine = {\n dialect: 'template',\n\n compile(source: string): EvalResult<unknown> {\n return compileTemplate(source);\n },\n\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'template') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `templateEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return {\n ok: false,\n error: { kind: 'parse', message: 'template Expression.source required' },\n };\n }\n const check = compileTemplate(expr.source);\n if (!check.ok) return check as EvalResult<T>;\n\n const scope = buildScope(ctx);\n const out = expr.source.replace(PATH_RE, (_match, path) => {\n return stringify(resolvePath(scope, path));\n });\n return { ok: true, value: out as unknown as T };\n },\n};\n","/**\n * Dialect-pluggable Expression engine registry.\n *\n * Replaces the per-call-site `compileFormula` / `evaluateFormula` direct\n * imports of the deleted custom engine. Call sites now ask the registry to\n * dispatch by `expression.dialect`.\n *\n * Stub dialects (`js`, `cron`) are registered at module load with explicit\n * `dialect`-error responses so call sites get a clear message instead of\n * silent `undefined` (the old engine's anti-pattern).\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport { celEngine } from './cel-engine';\nimport { cronEngine } from './cron-engine';\nimport { templateEngine } from './template-engine';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst registry = new Map<string, DialectEngine>();\n\n/** Register or replace a dialect engine. */\nexport function register(engine: DialectEngine): void {\n registry.set(engine.dialect, engine);\n}\n\n/** Look up a dialect engine without dispatching. */\nexport function getEngine(dialect: string): DialectEngine | undefined {\n return registry.get(dialect);\n}\n\n/** Whether a dialect has a real (non-stub) implementation registered. */\nexport function hasDialect(dialect: string): boolean {\n return registry.has(dialect) && !registry.get(dialect)!.dialect.startsWith('stub:');\n}\n\nfunction makeStub(dialect: string, reason: string): DialectEngine {\n return {\n dialect,\n compile: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n evaluate: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n };\n}\n\n// Real engines.\nregister(celEngine);\nregister(cronEngine);\nregister(templateEngine);\n\n// Stubs — `js` lives in @objectstack/plugin-js-vm (not yet shipped).\nregister(makeStub('js', \"dialect 'js' not registered. Install @objectstack/plugin-js-vm\"));\n\n/**\n * The unified evaluation entry point. Replaces the old direct calls to\n * `evaluateFormula` from the deleted custom engine.\n */\nexport const ExpressionEngine = {\n register,\n getEngine,\n hasDialect,\n\n /**\n * Compile-only — parse + type-check, returning the engine-native AST. Used\n * by `objectstack compile` to normalize source into AST in artifacts.\n */\n compile(expr: Expression): EvalResult<unknown> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return {\n ok: false,\n error: { kind: 'parse', message: 'Expression.source required for compile()' },\n };\n }\n return engine.compile(expr.source);\n },\n\n /**\n * Evaluate an expression in the given context. Never throws — branch on\n * `result.ok`. Errors carry a `kind` for caller-side classification.\n */\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n return engine.evaluate<T>(expr, ctx);\n },\n};\n","/**\n * Seed-value resolver.\n *\n * `Dataset.records` accepts {@link SeedValue} = primitive | Expression | array\n * | object — install-time resolution walks the tree and replaces any\n * Expression node with its evaluated result. This is what makes\n * `close_date: cel\\`now() + duration(\"P30D\")\\`` resolve to *the customer's*\n * \"today + 30 days\" instead of the developer's compile-time clock.\n */\n\nimport { ExpressionSchema, type Expression } from '@objectstack/spec';\n\nimport type { EvalContext, EvalResult } from './types';\nimport { ExpressionEngine } from './registry';\n\nexport type SeedPrimitive = string | number | boolean | null | Date;\nexport type SeedValue = SeedPrimitive | Expression | SeedValue[] | { [key: string]: SeedValue };\n\n/** Detect an Expression-shaped object without throwing on unrelated shapes. */\nfunction isExpressionLike(value: unknown): value is Expression {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n\n/**\n * Recursively resolve a SeedValue. Records that contain Expression leaves are\n * evaluated with `ctx`; other values are passed through unchanged.\n *\n * Returns the first failure encountered. Callers (seed loader) typically\n * abort the whole record on failure rather than silently writing partial data.\n */\nexport function resolveSeed(\n value: SeedValue,\n ctx: EvalContext,\n): EvalResult<unknown> {\n if (value === null || value === undefined) {\n return { ok: true, value };\n }\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return { ok: true, value };\n }\n if (value instanceof Date) {\n return { ok: true, value };\n }\n if (Array.isArray(value)) {\n const out: unknown[] = [];\n for (const item of value) {\n const r = resolveSeed(item, ctx);\n if (!r.ok) return r;\n out.push(r.value);\n }\n return { ok: true, value: out };\n }\n if (isExpressionLike(value)) {\n return ExpressionEngine.evaluate(value, ctx);\n }\n // Plain object — recurse field-by-field.\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, SeedValue>)) {\n const r = resolveSeed(v, ctx);\n if (!r.ok) return r;\n out[k] = r.value;\n }\n return { ok: true, value: out };\n}\n\n/**\n * Resolve a single record (object of fields), pinning `ctx.now` so all\n * expressions within see one logical clock.\n */\nexport function resolveSeedRecord(\n record: Record<string, SeedValue>,\n ctx: EvalContext,\n): EvalResult<Record<string, unknown>> {\n const pinnedCtx: EvalContext = { ...ctx, now: ctx.now ?? new Date() };\n const result = resolveSeed(record, pinnedCtx) as EvalResult<Record<string, unknown>>;\n return result;\n}\n","/**\n * Build-time normalization helpers.\n *\n * The CLI `objectstack compile` step walks the assembled `objectstack.json`\n * artifact and rewrites every Expression so that:\n *\n * 1. String shorthand input is replaced by `{ dialect: 'cel', source }`.\n * 2. The persisted envelope carries an `ast` field produced by the dialect\n * engine (M9.2 deliverable). Source is retained for round-trip / debug.\n *\n * Spec layer cannot do step 2 because it must remain dependency-free; this\n * package owns the engine import and therefore the AST step.\n */\n\nimport {\n ExpressionInputSchema,\n ExpressionSchema,\n type Expression,\n type ExpressionInput,\n} from '@objectstack/spec';\n\nimport { ExpressionEngine } from './registry';\nimport type { EvalResult } from './types';\n\n/**\n * Normalize an {@link ExpressionInput} (string shorthand OR full envelope) into\n * a fully-resolved {@link Expression} carrying both `source` and `ast`.\n *\n * Returns an EvalResult so the caller can render a structured compile error\n * pointing at the offending metadata path.\n */\nexport function normalizeExpression(input: ExpressionInput): EvalResult<Expression> {\n const parsed = ExpressionInputSchema.safeParse(input);\n if (!parsed.success) {\n return {\n ok: false,\n error: { kind: 'parse', message: parsed.error.message },\n };\n }\n\n const expr = parsed.data as Expression;\n\n // Already AST-only — accept as-is.\n if (expr.ast !== undefined && expr.source === undefined) {\n return { ok: true, value: expr };\n }\n\n // Source-bearing: ask the dialect engine to compile. Failures surface here\n // as part of the build (no silent skip).\n const compiled = ExpressionEngine.compile(expr);\n if (!compiled.ok) {\n return compiled;\n }\n\n return {\n ok: true,\n value: {\n ...expr,\n ast: compiled.value,\n },\n };\n}\n\n/**\n * Walk an arbitrary JSON tree and normalize every embedded Expression in\n * place. Used by the build pipeline to traverse the assembled metadata\n * artifact. Returns the first error encountered (paired with the dotted path\n * for diagnostics) or `null` when fully clean.\n */\nexport function normalizeExpressionTree(\n root: unknown,\n path: string[] = [],\n): { path: string; error: import('./types').EvalError } | null {\n if (root === null || typeof root !== 'object') return null;\n\n if (looksLikeExpression(root)) {\n const r = normalizeExpression(root as ExpressionInput);\n if (!r.ok) return { path: path.join('.'), error: r.error };\n Object.assign(root as Record<string, unknown>, r.value);\n return null;\n }\n\n if (Array.isArray(root)) {\n for (let i = 0; i < root.length; i++) {\n const r = normalizeExpressionTree(root[i], [...path, String(i)]);\n if (r) return r;\n }\n return null;\n }\n\n for (const [k, v] of Object.entries(root as Record<string, unknown>)) {\n const r = normalizeExpressionTree(v, [...path, k]);\n if (r) return r;\n }\n return null;\n}\n\nfunction looksLikeExpression(value: unknown): boolean {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n"],"mappings":"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","names":["out","import_spec"]}
package/dist/index.mjs CHANGED
@@ -22,6 +22,17 @@ function registerStdLib(env, now) {
22
22
  ).registerFunction(
23
23
  "daysAgo(int): google.protobuf.Timestamp",
24
24
  (n) => addDaysUtc(now(), -Number(n))
25
+ ).registerFunction(
26
+ "isBlank(dyn): bool",
27
+ (value) => {
28
+ if (value === null || value === void 0) return true;
29
+ if (typeof value === "string") return value.length === 0;
30
+ if (Array.isArray(value)) return value.length === 0;
31
+ return false;
32
+ }
33
+ ).registerFunction(
34
+ "coalesce(dyn, dyn): dyn",
35
+ (value, fallback) => value === null || value === void 0 ? fallback : value
25
36
  );
26
37
  }
27
38
  function buildScope(ctx) {
@@ -121,6 +132,143 @@ var celEngine = {
121
132
  }
122
133
  };
123
134
 
135
+ // src/cron-engine.ts
136
+ var ALIASES = /* @__PURE__ */ new Set([
137
+ "@yearly",
138
+ "@annually",
139
+ "@monthly",
140
+ "@weekly",
141
+ "@daily",
142
+ "@hourly",
143
+ "@reboot"
144
+ ]);
145
+ function validate(source) {
146
+ const trimmed = source.trim();
147
+ if (trimmed.length === 0) {
148
+ return { ok: false, error: { kind: "parse", message: "cron source is empty" } };
149
+ }
150
+ if (trimmed.startsWith("@")) {
151
+ if (!ALIASES.has(trimmed)) {
152
+ return {
153
+ ok: false,
154
+ error: { kind: "parse", message: `unknown cron alias '${trimmed}'` }
155
+ };
156
+ }
157
+ return { ok: true, value: trimmed };
158
+ }
159
+ const fields = trimmed.split(/\s+/);
160
+ if (fields.length !== 5 && fields.length !== 6) {
161
+ return {
162
+ ok: false,
163
+ error: {
164
+ kind: "parse",
165
+ message: `cron requires 5 or 6 space-separated fields, got ${fields.length}`
166
+ }
167
+ };
168
+ }
169
+ const allowed = /^[\d*/,\-?LWA-Z#]+$/i;
170
+ for (let i = 0; i < fields.length; i++) {
171
+ if (!allowed.test(fields[i])) {
172
+ return {
173
+ ok: false,
174
+ error: {
175
+ kind: "parse",
176
+ message: `cron field ${i + 1} contains invalid characters: '${fields[i]}'`
177
+ }
178
+ };
179
+ }
180
+ }
181
+ return { ok: true, value: trimmed };
182
+ }
183
+ var cronEngine = {
184
+ dialect: "cron",
185
+ compile(source) {
186
+ return validate(source);
187
+ },
188
+ evaluate(expr, _ctx) {
189
+ if (expr.dialect !== "cron") {
190
+ return {
191
+ ok: false,
192
+ error: { kind: "dialect", message: `cronEngine cannot evaluate dialect '${expr.dialect}'` }
193
+ };
194
+ }
195
+ if (typeof expr.source !== "string") {
196
+ return { ok: false, error: { kind: "parse", message: "cron Expression.source required" } };
197
+ }
198
+ const result = validate(expr.source);
199
+ if (!result.ok) return result;
200
+ return { ok: true, value: result.value };
201
+ }
202
+ };
203
+
204
+ // src/template-engine.ts
205
+ var PATH_RE = /\{\{\s*([\w.[\]]+?)\s*\}\}/g;
206
+ function resolvePath(scope, path) {
207
+ const normalized = path.replace(/\[(\w+)\]/g, ".$1");
208
+ const segments = normalized.split(".").filter(Boolean);
209
+ let cursor = scope;
210
+ for (const seg of segments) {
211
+ if (cursor == null || typeof cursor !== "object") return void 0;
212
+ cursor = cursor[seg];
213
+ }
214
+ return cursor;
215
+ }
216
+ function stringify(value) {
217
+ if (value === null || value === void 0) return "";
218
+ if (value instanceof Date) return value.toISOString();
219
+ if (typeof value === "string") return value;
220
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
221
+ if (typeof value === "bigint") return value.toString();
222
+ try {
223
+ return JSON.stringify(value);
224
+ } catch {
225
+ return String(value);
226
+ }
227
+ }
228
+ function compileTemplate(source) {
229
+ const matches = source.match(/\{\{|\}\}/g) ?? [];
230
+ if (matches.length % 2 !== 0) {
231
+ return {
232
+ ok: false,
233
+ error: { kind: "parse", message: "template has unbalanced {{ }} delimiters" }
234
+ };
235
+ }
236
+ const refs = [];
237
+ let m;
238
+ PATH_RE.lastIndex = 0;
239
+ while ((m = PATH_RE.exec(source)) !== null) {
240
+ refs.push(m[1]);
241
+ }
242
+ return { ok: true, value: refs };
243
+ }
244
+ var templateEngine = {
245
+ dialect: "template",
246
+ compile(source) {
247
+ return compileTemplate(source);
248
+ },
249
+ evaluate(expr, ctx) {
250
+ if (expr.dialect !== "template") {
251
+ return {
252
+ ok: false,
253
+ error: { kind: "dialect", message: `templateEngine cannot evaluate dialect '${expr.dialect}'` }
254
+ };
255
+ }
256
+ if (typeof expr.source !== "string") {
257
+ return {
258
+ ok: false,
259
+ error: { kind: "parse", message: "template Expression.source required" }
260
+ };
261
+ }
262
+ const check = compileTemplate(expr.source);
263
+ if (!check.ok) return check;
264
+ const scope = buildScope(ctx);
265
+ const out = expr.source.replace(PATH_RE, (_match, path) => {
266
+ return stringify(resolvePath(scope, path));
267
+ });
268
+ return { ok: true, value: out };
269
+ }
270
+ };
271
+
124
272
  // src/registry.ts
125
273
  var registry = /* @__PURE__ */ new Map();
126
274
  function register(engine) {
@@ -140,8 +288,9 @@ function makeStub(dialect, reason) {
140
288
  };
141
289
  }
142
290
  register(celEngine);
291
+ register(cronEngine);
292
+ register(templateEngine);
143
293
  register(makeStub("js", "dialect 'js' not registered. Install @objectstack/plugin-js-vm"));
144
- register(makeStub("cron", "dialect 'cron' not registered. Install @objectstack/plugin-cron"));
145
294
  var ExpressionEngine = {
146
295
  register,
147
296
  getEngine,
@@ -288,6 +437,7 @@ export {
288
437
  ExpressionEngine,
289
438
  buildScope,
290
439
  celEngine,
440
+ cronEngine,
291
441
  getEngine,
292
442
  hasDialect,
293
443
  normalizeExpression,
@@ -295,6 +445,7 @@ export {
295
445
  register,
296
446
  registerStdLib,
297
447
  resolveSeed,
298
- resolveSeedRecord
448
+ resolveSeedRecord,
449
+ templateEngine
299
450
  };
300
451
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cel-engine.ts","../src/stdlib.ts","../src/registry.ts","../src/seed-eval.ts","../src/normalize.ts"],"sourcesContent":["/**\n * CEL dialect engine — wraps `@marcbachmann/cel-js` with the ObjectStack\n * stdlib, bounded execution limits, and result coercion.\n *\n * Why a thin wrapper:\n *\n * - cel-js returns `BigInt` for ints. The kernel and CRM expect plain\n * numbers, so we coerce at the boundary.\n * - cel-js parses dotted names as receiver-typed methods; we register\n * `now()`, `today()`, `daysFromNow()` as bare functions and let `os.*`\n * refer to context data only (see {@link buildScope}).\n * - Bounds (`maxAstNodes`, `maxDepth`, …) are enforced spec-wide so\n * third-party plugins can't ship runaway predicates.\n */\n\nimport { Environment } from '@marcbachmann/cel-js';\nimport type { Expression } from '@objectstack/spec';\n\nimport { buildScope, registerStdLib } from './stdlib';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\n/**\n * Default execution bounds. Picked conservatively — every metadata-authored\n * expression we've seen is well under these. If you hit them, the expression\n * is too complex for ObjectStack and should be moved to a hook (`dialect: js`).\n */\nexport const DEFAULT_LIMITS = {\n maxAstNodes: 256,\n maxDepth: 32,\n maxListElements: 64,\n maxMapEntries: 64,\n maxCallArguments: 16,\n} as const;\n\nfunction buildEnv(now: () => Date): Environment {\n const env = new Environment({\n unlistedVariablesAreDyn: true,\n enableOptionalTypes: true,\n limits: DEFAULT_LIMITS,\n });\n return registerStdLib(env, now);\n}\n\n/** Coerce cel-js's BigInt-flavored return into spec-friendly JS values. */\nfunction coerce(value: unknown): unknown {\n if (typeof value === 'bigint') {\n // BigInt → number when safe, else string to avoid silent truncation.\n if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {\n return Number(value);\n }\n return value.toString();\n }\n if (Array.isArray(value)) return value.map(coerce);\n if (value && typeof value === 'object' && !(value instanceof Date)) {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = coerce(v);\n return out;\n }\n return value;\n}\n\nfunction classifyError(err: unknown): EvalResult<never> {\n const message = err instanceof Error ? err.message : String(err);\n let kind: 'parse' | 'type' | 'runtime' | 'bounds' = 'runtime';\n if (/Exceeded max/i.test(message)) kind = 'bounds';\n else if (/parse|unexpected|syntax/i.test(message)) kind = 'parse';\n else if (/type|unknown variable|undeclared/i.test(message)) kind = 'type';\n return { ok: false, error: { kind, message } };\n}\n\nexport const celEngine: DialectEngine = {\n dialect: 'cel',\n\n compile(source: string): EvalResult<unknown> {\n try {\n // We use a wall-clock now() here purely for parse-time stdlib\n // type-checking; the function is never actually called.\n const env = buildEnv(() => new Date(0));\n const compiled = env.parse(source);\n // Surface check errors eagerly.\n const checkErrors = compiled.check?.();\n if (checkErrors && Array.isArray(checkErrors) && checkErrors.length > 0) {\n return {\n ok: false,\n error: { kind: 'type', message: checkErrors.join('; ') },\n };\n }\n return { ok: true, value: compiled.ast };\n } catch (err) {\n return classifyError(err);\n }\n },\n\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'cel') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `celEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n const source = expr.source;\n if (typeof source !== 'string' || source.length === 0) {\n // AST-only inputs: cel-js does not currently expose a public API to\n // re-execute a parsed AST without re-serializing. We persist `source`\n // as the canonical form during M9.1 and revisit AST-only execution in\n // M9.7 when we cut the spec persistence over.\n return {\n ok: false,\n error: { kind: 'parse', message: 'AST-only evaluation not yet supported; persist `source`' },\n };\n }\n\n const now = () => ctx.now ?? new Date();\n try {\n const env = buildEnv(now);\n const scope = buildScope(ctx);\n const raw = env.evaluate(source, scope);\n return { ok: true, value: coerce(raw) as T };\n } catch (err) {\n return classifyError(err);\n }\n },\n};\n","/**\n * ObjectStack standard CEL function library.\n *\n * Registered into the per-evaluation `Environment` by the CEL engine. All\n * functions are pure given a pinned `now` — that determinism is what makes\n * `objectstack build` artifacts byte-stable across runs.\n *\n * Function naming intentionally avoids the `os.` prefix because cel-js binds\n * dotted names to receiver types. Instead, the `os` namespace in CEL holds\n * *data* (`os.user`, `os.org`, `os.env`) supplied by the caller's\n * {@link EvalContext}.\n */\n\nimport type { Environment } from '@marcbachmann/cel-js';\n\nimport type { EvalContext } from './types';\n\n/** Truncate a Date to start-of-day in UTC. */\nfunction startOfDayUtc(d: Date): Date {\n const out = new Date(d.getTime());\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\n/** Add `n` days to a Date in UTC; returns a new Date. */\nfunction addDaysUtc(d: Date, n: number): Date {\n const out = new Date(d.getTime());\n out.setUTCDate(out.getUTCDate() + n);\n return out;\n}\n\n/**\n * Register the ObjectStack standard library into a CEL environment.\n *\n * The `now` resolver is closed over so each call uses the pinned\n * `EvalContext.now` (or wall-clock fallback). Implementations are kept tiny\n * and dependency-free — they're the contract surface for AI authors and must\n * stay legible.\n */\nexport function registerStdLib(\n env: Environment,\n now: () => Date,\n): Environment {\n return env\n .registerFunction('now(): google.protobuf.Timestamp', () => now())\n .registerFunction(\n 'today(): google.protobuf.Timestamp',\n () => startOfDayUtc(now()),\n )\n .registerFunction(\n 'daysFromNow(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), Number(n)),\n )\n .registerFunction(\n 'daysAgo(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), -Number(n)),\n );\n}\n\n/**\n * Build the variable scope for a single evaluation. Absent fields are simply\n * not bound — CEL macros (`has(record.foo)`) handle missing-key safely.\n */\nexport function buildScope(ctx: EvalContext): Record<string, unknown> {\n const scope: Record<string, unknown> = {};\n\n if (ctx.record !== undefined) scope.record = ctx.record;\n if (ctx.previous !== undefined) scope.previous = ctx.previous;\n if (ctx.input !== undefined) scope.input = ctx.input;\n\n // Namespaced data — written as `os.user.id`, `os.env`, etc. in CEL.\n const os: Record<string, unknown> = {};\n if (ctx.user !== undefined) os.user = ctx.user;\n if (ctx.org !== undefined) os.org = ctx.org;\n if (ctx.env !== undefined) os.env = ctx.env;\n if (Object.keys(os).length > 0) scope.os = os;\n\n if (ctx.extra !== undefined) Object.assign(scope, ctx.extra);\n\n return scope;\n}\n","/**\n * Dialect-pluggable Expression engine registry.\n *\n * Replaces the per-call-site `compileFormula` / `evaluateFormula` direct\n * imports of the deleted custom engine. Call sites now ask the registry to\n * dispatch by `expression.dialect`.\n *\n * Stub dialects (`js`, `cron`) are registered at module load with explicit\n * `dialect`-error responses so call sites get a clear message instead of\n * silent `undefined` (the old engine's anti-pattern).\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport { celEngine } from './cel-engine';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst registry = new Map<string, DialectEngine>();\n\n/** Register or replace a dialect engine. */\nexport function register(engine: DialectEngine): void {\n registry.set(engine.dialect, engine);\n}\n\n/** Look up a dialect engine without dispatching. */\nexport function getEngine(dialect: string): DialectEngine | undefined {\n return registry.get(dialect);\n}\n\n/** Whether a dialect has a real (non-stub) implementation registered. */\nexport function hasDialect(dialect: string): boolean {\n return registry.has(dialect) && !registry.get(dialect)!.dialect.startsWith('stub:');\n}\n\nfunction makeStub(dialect: string, reason: string): DialectEngine {\n return {\n dialect,\n compile: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n evaluate: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n };\n}\n\n// Real engines.\nregister(celEngine);\n\n// Stubs — phased in by later milestones (M9.5+ for `js`, M9.6 for `cron`).\nregister(makeStub('js', \"dialect 'js' not registered. Install @objectstack/plugin-js-vm\"));\nregister(makeStub('cron', \"dialect 'cron' not registered. Install @objectstack/plugin-cron\"));\n\n/**\n * The unified evaluation entry point. Replaces the old direct calls to\n * `evaluateFormula` from the deleted custom engine.\n */\nexport const ExpressionEngine = {\n register,\n getEngine,\n hasDialect,\n\n /**\n * Compile-only — parse + type-check, returning the engine-native AST. Used\n * by `objectstack compile` to normalize source into AST in artifacts.\n */\n compile(expr: Expression): EvalResult<unknown> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return {\n ok: false,\n error: { kind: 'parse', message: 'Expression.source required for compile()' },\n };\n }\n return engine.compile(expr.source);\n },\n\n /**\n * Evaluate an expression in the given context. Never throws — branch on\n * `result.ok`. Errors carry a `kind` for caller-side classification.\n */\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n return engine.evaluate<T>(expr, ctx);\n },\n};\n","/**\n * Seed-value resolver.\n *\n * `Dataset.records` accepts {@link SeedValue} = primitive | Expression | array\n * | object — install-time resolution walks the tree and replaces any\n * Expression node with its evaluated result. This is what makes\n * `close_date: cel\\`now() + duration(\"P30D\")\\`` resolve to *the customer's*\n * \"today + 30 days\" instead of the developer's compile-time clock.\n */\n\nimport { ExpressionSchema, type Expression } from '@objectstack/spec';\n\nimport type { EvalContext, EvalResult } from './types';\nimport { ExpressionEngine } from './registry';\n\nexport type SeedPrimitive = string | number | boolean | null | Date;\nexport type SeedValue = SeedPrimitive | Expression | SeedValue[] | { [key: string]: SeedValue };\n\n/** Detect an Expression-shaped object without throwing on unrelated shapes. */\nfunction isExpressionLike(value: unknown): value is Expression {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n\n/**\n * Recursively resolve a SeedValue. Records that contain Expression leaves are\n * evaluated with `ctx`; other values are passed through unchanged.\n *\n * Returns the first failure encountered. Callers (seed loader) typically\n * abort the whole record on failure rather than silently writing partial data.\n */\nexport function resolveSeed(\n value: SeedValue,\n ctx: EvalContext,\n): EvalResult<unknown> {\n if (value === null || value === undefined) {\n return { ok: true, value };\n }\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return { ok: true, value };\n }\n if (value instanceof Date) {\n return { ok: true, value };\n }\n if (Array.isArray(value)) {\n const out: unknown[] = [];\n for (const item of value) {\n const r = resolveSeed(item, ctx);\n if (!r.ok) return r;\n out.push(r.value);\n }\n return { ok: true, value: out };\n }\n if (isExpressionLike(value)) {\n return ExpressionEngine.evaluate(value, ctx);\n }\n // Plain object — recurse field-by-field.\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, SeedValue>)) {\n const r = resolveSeed(v, ctx);\n if (!r.ok) return r;\n out[k] = r.value;\n }\n return { ok: true, value: out };\n}\n\n/**\n * Resolve a single record (object of fields), pinning `ctx.now` so all\n * expressions within see one logical clock.\n */\nexport function resolveSeedRecord(\n record: Record<string, SeedValue>,\n ctx: EvalContext,\n): EvalResult<Record<string, unknown>> {\n const pinnedCtx: EvalContext = { ...ctx, now: ctx.now ?? new Date() };\n const result = resolveSeed(record, pinnedCtx) as EvalResult<Record<string, unknown>>;\n return result;\n}\n","/**\n * Build-time normalization helpers.\n *\n * The CLI `objectstack compile` step walks the assembled `objectstack.json`\n * artifact and rewrites every Expression so that:\n *\n * 1. String shorthand input is replaced by `{ dialect: 'cel', source }`.\n * 2. The persisted envelope carries an `ast` field produced by the dialect\n * engine (M9.2 deliverable). Source is retained for round-trip / debug.\n *\n * Spec layer cannot do step 2 because it must remain dependency-free; this\n * package owns the engine import and therefore the AST step.\n */\n\nimport {\n ExpressionInputSchema,\n ExpressionSchema,\n type Expression,\n type ExpressionInput,\n} from '@objectstack/spec';\n\nimport { ExpressionEngine } from './registry';\nimport type { EvalResult } from './types';\n\n/**\n * Normalize an {@link ExpressionInput} (string shorthand OR full envelope) into\n * a fully-resolved {@link Expression} carrying both `source` and `ast`.\n *\n * Returns an EvalResult so the caller can render a structured compile error\n * pointing at the offending metadata path.\n */\nexport function normalizeExpression(input: ExpressionInput): EvalResult<Expression> {\n const parsed = ExpressionInputSchema.safeParse(input);\n if (!parsed.success) {\n return {\n ok: false,\n error: { kind: 'parse', message: parsed.error.message },\n };\n }\n\n const expr = parsed.data as Expression;\n\n // Already AST-only — accept as-is.\n if (expr.ast !== undefined && expr.source === undefined) {\n return { ok: true, value: expr };\n }\n\n // Source-bearing: ask the dialect engine to compile. Failures surface here\n // as part of the build (no silent skip).\n const compiled = ExpressionEngine.compile(expr);\n if (!compiled.ok) {\n return compiled;\n }\n\n return {\n ok: true,\n value: {\n ...expr,\n ast: compiled.value,\n },\n };\n}\n\n/**\n * Walk an arbitrary JSON tree and normalize every embedded Expression in\n * place. Used by the build pipeline to traverse the assembled metadata\n * artifact. Returns the first error encountered (paired with the dotted path\n * for diagnostics) or `null` when fully clean.\n */\nexport function normalizeExpressionTree(\n root: unknown,\n path: string[] = [],\n): { path: string; error: import('./types').EvalError } | null {\n if (root === null || typeof root !== 'object') return null;\n\n if (looksLikeExpression(root)) {\n const r = normalizeExpression(root as ExpressionInput);\n if (!r.ok) return { path: path.join('.'), error: r.error };\n Object.assign(root as Record<string, unknown>, r.value);\n return null;\n }\n\n if (Array.isArray(root)) {\n for (let i = 0; i < root.length; i++) {\n const r = normalizeExpressionTree(root[i], [...path, String(i)]);\n if (r) return r;\n }\n return null;\n }\n\n for (const [k, v] of Object.entries(root as Record<string, unknown>)) {\n const r = normalizeExpressionTree(v, [...path, k]);\n if (r) return r;\n }\n return null;\n}\n\nfunction looksLikeExpression(value: unknown): boolean {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n"],"mappings":"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","names":["out","ExpressionSchema","ExpressionSchema"]}
1
+ {"version":3,"sources":["../src/cel-engine.ts","../src/stdlib.ts","../src/cron-engine.ts","../src/template-engine.ts","../src/registry.ts","../src/seed-eval.ts","../src/normalize.ts"],"sourcesContent":["/**\n * CEL dialect engine — wraps `@marcbachmann/cel-js` with the ObjectStack\n * stdlib, bounded execution limits, and result coercion.\n *\n * Why a thin wrapper:\n *\n * - cel-js returns `BigInt` for ints. The kernel and CRM expect plain\n * numbers, so we coerce at the boundary.\n * - cel-js parses dotted names as receiver-typed methods; we register\n * `now()`, `today()`, `daysFromNow()` as bare functions and let `os.*`\n * refer to context data only (see {@link buildScope}).\n * - Bounds (`maxAstNodes`, `maxDepth`, …) are enforced spec-wide so\n * third-party plugins can't ship runaway predicates.\n */\n\nimport { Environment } from '@marcbachmann/cel-js';\nimport type { Expression } from '@objectstack/spec';\n\nimport { buildScope, registerStdLib } from './stdlib';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\n/**\n * Default execution bounds. Picked conservatively — every metadata-authored\n * expression we've seen is well under these. If you hit them, the expression\n * is too complex for ObjectStack and should be moved to a hook (`dialect: js`).\n */\nexport const DEFAULT_LIMITS = {\n maxAstNodes: 256,\n maxDepth: 32,\n maxListElements: 64,\n maxMapEntries: 64,\n maxCallArguments: 16,\n} as const;\n\nfunction buildEnv(now: () => Date): Environment {\n const env = new Environment({\n unlistedVariablesAreDyn: true,\n enableOptionalTypes: true,\n limits: DEFAULT_LIMITS,\n });\n return registerStdLib(env, now);\n}\n\n/** Coerce cel-js's BigInt-flavored return into spec-friendly JS values. */\nfunction coerce(value: unknown): unknown {\n if (typeof value === 'bigint') {\n // BigInt → number when safe, else string to avoid silent truncation.\n if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {\n return Number(value);\n }\n return value.toString();\n }\n if (Array.isArray(value)) return value.map(coerce);\n if (value && typeof value === 'object' && !(value instanceof Date)) {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value)) out[k] = coerce(v);\n return out;\n }\n return value;\n}\n\nfunction classifyError(err: unknown): EvalResult<never> {\n const message = err instanceof Error ? err.message : String(err);\n let kind: 'parse' | 'type' | 'runtime' | 'bounds' = 'runtime';\n if (/Exceeded max/i.test(message)) kind = 'bounds';\n else if (/parse|unexpected|syntax/i.test(message)) kind = 'parse';\n else if (/type|unknown variable|undeclared/i.test(message)) kind = 'type';\n return { ok: false, error: { kind, message } };\n}\n\nexport const celEngine: DialectEngine = {\n dialect: 'cel',\n\n compile(source: string): EvalResult<unknown> {\n try {\n // We use a wall-clock now() here purely for parse-time stdlib\n // type-checking; the function is never actually called.\n const env = buildEnv(() => new Date(0));\n const compiled = env.parse(source);\n // Surface check errors eagerly.\n const checkErrors = compiled.check?.();\n if (checkErrors && Array.isArray(checkErrors) && checkErrors.length > 0) {\n return {\n ok: false,\n error: { kind: 'type', message: checkErrors.join('; ') },\n };\n }\n return { ok: true, value: compiled.ast };\n } catch (err) {\n return classifyError(err);\n }\n },\n\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'cel') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `celEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n const source = expr.source;\n if (typeof source !== 'string' || source.length === 0) {\n // AST-only inputs: cel-js does not currently expose a public API to\n // re-execute a parsed AST without re-serializing. We persist `source`\n // as the canonical form during M9.1 and revisit AST-only execution in\n // M9.7 when we cut the spec persistence over.\n return {\n ok: false,\n error: { kind: 'parse', message: 'AST-only evaluation not yet supported; persist `source`' },\n };\n }\n\n const now = () => ctx.now ?? new Date();\n try {\n const env = buildEnv(now);\n const scope = buildScope(ctx);\n const raw = env.evaluate(source, scope);\n return { ok: true, value: coerce(raw) as T };\n } catch (err) {\n return classifyError(err);\n }\n },\n};\n","/**\n * ObjectStack standard CEL function library.\n *\n * Registered into the per-evaluation `Environment` by the CEL engine. All\n * functions are pure given a pinned `now` — that determinism is what makes\n * `objectstack build` artifacts byte-stable across runs.\n *\n * Function naming intentionally avoids the `os.` prefix because cel-js binds\n * dotted names to receiver types. Instead, the `os` namespace in CEL holds\n * *data* (`os.user`, `os.org`, `os.env`) supplied by the caller's\n * {@link EvalContext}.\n */\n\nimport type { Environment } from '@marcbachmann/cel-js';\n\nimport type { EvalContext } from './types';\n\n/** Truncate a Date to start-of-day in UTC. */\nfunction startOfDayUtc(d: Date): Date {\n const out = new Date(d.getTime());\n out.setUTCHours(0, 0, 0, 0);\n return out;\n}\n\n/** Add `n` days to a Date in UTC; returns a new Date. */\nfunction addDaysUtc(d: Date, n: number): Date {\n const out = new Date(d.getTime());\n out.setUTCDate(out.getUTCDate() + n);\n return out;\n}\n\n/**\n * Register the ObjectStack standard library into a CEL environment.\n *\n * The `now` resolver is closed over so each call uses the pinned\n * `EvalContext.now` (or wall-clock fallback). Implementations are kept tiny\n * and dependency-free — they're the contract surface for AI authors and must\n * stay legible.\n */\nexport function registerStdLib(\n env: Environment,\n now: () => Date,\n): Environment {\n return env\n .registerFunction('now(): google.protobuf.Timestamp', () => now())\n .registerFunction(\n 'today(): google.protobuf.Timestamp',\n () => startOfDayUtc(now()),\n )\n .registerFunction(\n 'daysFromNow(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), Number(n)),\n )\n .registerFunction(\n 'daysAgo(int): google.protobuf.Timestamp',\n (n: bigint | number) => addDaysUtc(now(), -Number(n)),\n )\n // Returns true when `value` is null, undefined, empty string, or empty list.\n // Matches the intent of legacy `ISBLANK()` while staying CEL-idiomatic.\n .registerFunction(\n 'isBlank(dyn): bool',\n (value: unknown) => {\n if (value === null || value === undefined) return true;\n if (typeof value === 'string') return value.length === 0;\n if (Array.isArray(value)) return value.length === 0;\n return false;\n },\n )\n // Returns `value` when not null/undefined, otherwise the `fallback`.\n // Use this to safely concatenate optional string fields:\n // coalesce(record.salutation, '') + ' ' + coalesce(record.first_name, '')\n .registerFunction(\n 'coalesce(dyn, dyn): dyn',\n (value: unknown, fallback: unknown) =>\n (value === null || value === undefined) ? fallback : value,\n );\n}\n\n/**\n * Build the variable scope for a single evaluation. Absent fields are simply\n * not bound — CEL macros (`has(record.foo)`) handle missing-key safely.\n */\nexport function buildScope(ctx: EvalContext): Record<string, unknown> {\n const scope: Record<string, unknown> = {};\n\n if (ctx.record !== undefined) scope.record = ctx.record;\n if (ctx.previous !== undefined) scope.previous = ctx.previous;\n if (ctx.input !== undefined) scope.input = ctx.input;\n\n // Namespaced data — written as `os.user.id`, `os.env`, etc. in CEL.\n const os: Record<string, unknown> = {};\n if (ctx.user !== undefined) os.user = ctx.user;\n if (ctx.org !== undefined) os.org = ctx.org;\n if (ctx.env !== undefined) os.env = ctx.env;\n if (Object.keys(os).length > 0) scope.os = os;\n\n if (ctx.extra !== undefined) Object.assign(scope, ctx.extra);\n\n return scope;\n}\n","/**\n * Cron dialect engine.\n *\n * Validates cron expressions at compile time without depending on a parser.\n * Actual schedule firing lives in the scheduler service — this engine just\n * round-trips the expression through `Expression.evaluate`, returning the\n * source so callers can hand it to a scheduler library.\n *\n * Accepted forms:\n * - 5-field standard cron: `m h dom mon dow`\n * - 6-field extended cron: `s m h dom mon dow`\n * - Aliases: @yearly, @annually, @monthly, @weekly, @daily, @hourly, @reboot\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst ALIASES = new Set([\n '@yearly', '@annually', '@monthly', '@weekly', '@daily', '@hourly', '@reboot',\n]);\n\nfunction validate(source: string): EvalResult<string> {\n const trimmed = source.trim();\n if (trimmed.length === 0) {\n return { ok: false, error: { kind: 'parse', message: 'cron source is empty' } };\n }\n if (trimmed.startsWith('@')) {\n if (!ALIASES.has(trimmed)) {\n return {\n ok: false,\n error: { kind: 'parse', message: `unknown cron alias '${trimmed}'` },\n };\n }\n return { ok: true, value: trimmed };\n }\n const fields = trimmed.split(/\\s+/);\n if (fields.length !== 5 && fields.length !== 6) {\n return {\n ok: false,\n error: {\n kind: 'parse',\n message: `cron requires 5 or 6 space-separated fields, got ${fields.length}`,\n },\n };\n }\n // Each field must use only allowed cron characters.\n const allowed = /^[\\d*/,\\-?LWA-Z#]+$/i;\n for (let i = 0; i < fields.length; i++) {\n if (!allowed.test(fields[i])) {\n return {\n ok: false,\n error: {\n kind: 'parse',\n message: `cron field ${i + 1} contains invalid characters: '${fields[i]}'`,\n },\n };\n }\n }\n return { ok: true, value: trimmed };\n}\n\nexport const cronEngine: DialectEngine = {\n dialect: 'cron',\n\n compile(source: string): EvalResult<unknown> {\n return validate(source);\n },\n\n evaluate<T = unknown>(expr: Expression, _ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'cron') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `cronEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return { ok: false, error: { kind: 'parse', message: 'cron Expression.source required' } };\n }\n const result = validate(expr.source);\n if (!result.ok) return result as EvalResult<T>;\n // Cron expressions don't \"evaluate\" to a value at predicate time — they\n // describe a schedule. Returning the source lets schedulers consume it.\n return { ok: true, value: result.value as unknown as T };\n },\n};\n","/**\n * Template dialect engine — strict Mustache subset.\n *\n * Supports `{{path.to.value}}` interpolation only. No conditionals, no loops,\n * no helpers. The variable scope is the same as CEL (`record`, `previous`,\n * `input`, `os.user`, `os.org`, `os.env`, plus `extra`), so authors can move\n * fluidly between a CEL formula and a template body without re-learning a\n * second variable namespace.\n *\n * Why a separate dialect from CEL: templates produce strings (notification\n * subjects, prompt bodies, titleFormat). CEL is a value-typed expression\n * language. Routing them through the same envelope (`{ dialect: 'template' }`)\n * keeps the AI author rule simple — \"anything templated or computed is an\n * Expression\" — without conflating the two semantics.\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport { buildScope } from './stdlib';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst PATH_RE = /\\{\\{\\s*([\\w.[\\]]+?)\\s*\\}\\}/g;\n\nfunction resolvePath(scope: Record<string, unknown>, path: string): unknown {\n // Support `a.b.c` and `a[0].b` style. Bracket notation collapses to dotted.\n const normalized = path.replace(/\\[(\\w+)\\]/g, '.$1');\n const segments = normalized.split('.').filter(Boolean);\n let cursor: unknown = scope;\n for (const seg of segments) {\n if (cursor == null || typeof cursor !== 'object') return undefined;\n cursor = (cursor as Record<string, unknown>)[seg];\n }\n return cursor;\n}\n\nfunction stringify(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (value instanceof Date) return value.toISOString();\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n if (typeof value === 'bigint') return value.toString();\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nfunction compileTemplate(source: string): EvalResult<string[]> {\n // Compile is only a structural validity check — no helpers, no balanced\n // open/close beyond what the regex enforces.\n const matches = source.match(/\\{\\{|\\}\\}/g) ?? [];\n if (matches.length % 2 !== 0) {\n return {\n ok: false,\n error: { kind: 'parse', message: 'template has unbalanced {{ }} delimiters' },\n };\n }\n const refs: string[] = [];\n let m: RegExpExecArray | null;\n PATH_RE.lastIndex = 0;\n while ((m = PATH_RE.exec(source)) !== null) {\n refs.push(m[1]);\n }\n return { ok: true, value: refs };\n}\n\nexport const templateEngine: DialectEngine = {\n dialect: 'template',\n\n compile(source: string): EvalResult<unknown> {\n return compileTemplate(source);\n },\n\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n if (expr.dialect !== 'template') {\n return {\n ok: false,\n error: { kind: 'dialect', message: `templateEngine cannot evaluate dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return {\n ok: false,\n error: { kind: 'parse', message: 'template Expression.source required' },\n };\n }\n const check = compileTemplate(expr.source);\n if (!check.ok) return check as EvalResult<T>;\n\n const scope = buildScope(ctx);\n const out = expr.source.replace(PATH_RE, (_match, path) => {\n return stringify(resolvePath(scope, path));\n });\n return { ok: true, value: out as unknown as T };\n },\n};\n","/**\n * Dialect-pluggable Expression engine registry.\n *\n * Replaces the per-call-site `compileFormula` / `evaluateFormula` direct\n * imports of the deleted custom engine. Call sites now ask the registry to\n * dispatch by `expression.dialect`.\n *\n * Stub dialects (`js`, `cron`) are registered at module load with explicit\n * `dialect`-error responses so call sites get a clear message instead of\n * silent `undefined` (the old engine's anti-pattern).\n */\n\nimport type { Expression } from '@objectstack/spec';\n\nimport { celEngine } from './cel-engine';\nimport { cronEngine } from './cron-engine';\nimport { templateEngine } from './template-engine';\nimport type { DialectEngine, EvalContext, EvalResult } from './types';\n\nconst registry = new Map<string, DialectEngine>();\n\n/** Register or replace a dialect engine. */\nexport function register(engine: DialectEngine): void {\n registry.set(engine.dialect, engine);\n}\n\n/** Look up a dialect engine without dispatching. */\nexport function getEngine(dialect: string): DialectEngine | undefined {\n return registry.get(dialect);\n}\n\n/** Whether a dialect has a real (non-stub) implementation registered. */\nexport function hasDialect(dialect: string): boolean {\n return registry.has(dialect) && !registry.get(dialect)!.dialect.startsWith('stub:');\n}\n\nfunction makeStub(dialect: string, reason: string): DialectEngine {\n return {\n dialect,\n compile: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n evaluate: () => ({ ok: false, error: { kind: 'dialect', message: reason } }),\n };\n}\n\n// Real engines.\nregister(celEngine);\nregister(cronEngine);\nregister(templateEngine);\n\n// Stubs — `js` lives in @objectstack/plugin-js-vm (not yet shipped).\nregister(makeStub('js', \"dialect 'js' not registered. Install @objectstack/plugin-js-vm\"));\n\n/**\n * The unified evaluation entry point. Replaces the old direct calls to\n * `evaluateFormula` from the deleted custom engine.\n */\nexport const ExpressionEngine = {\n register,\n getEngine,\n hasDialect,\n\n /**\n * Compile-only — parse + type-check, returning the engine-native AST. Used\n * by `objectstack compile` to normalize source into AST in artifacts.\n */\n compile(expr: Expression): EvalResult<unknown> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n if (typeof expr.source !== 'string') {\n return {\n ok: false,\n error: { kind: 'parse', message: 'Expression.source required for compile()' },\n };\n }\n return engine.compile(expr.source);\n },\n\n /**\n * Evaluate an expression in the given context. Never throws — branch on\n * `result.ok`. Errors carry a `kind` for caller-side classification.\n */\n evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {\n const engine = registry.get(expr.dialect);\n if (!engine) {\n return {\n ok: false,\n error: { kind: 'dialect', message: `No engine registered for dialect '${expr.dialect}'` },\n };\n }\n return engine.evaluate<T>(expr, ctx);\n },\n};\n","/**\n * Seed-value resolver.\n *\n * `Dataset.records` accepts {@link SeedValue} = primitive | Expression | array\n * | object — install-time resolution walks the tree and replaces any\n * Expression node with its evaluated result. This is what makes\n * `close_date: cel\\`now() + duration(\"P30D\")\\`` resolve to *the customer's*\n * \"today + 30 days\" instead of the developer's compile-time clock.\n */\n\nimport { ExpressionSchema, type Expression } from '@objectstack/spec';\n\nimport type { EvalContext, EvalResult } from './types';\nimport { ExpressionEngine } from './registry';\n\nexport type SeedPrimitive = string | number | boolean | null | Date;\nexport type SeedValue = SeedPrimitive | Expression | SeedValue[] | { [key: string]: SeedValue };\n\n/** Detect an Expression-shaped object without throwing on unrelated shapes. */\nfunction isExpressionLike(value: unknown): value is Expression {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n\n/**\n * Recursively resolve a SeedValue. Records that contain Expression leaves are\n * evaluated with `ctx`; other values are passed through unchanged.\n *\n * Returns the first failure encountered. Callers (seed loader) typically\n * abort the whole record on failure rather than silently writing partial data.\n */\nexport function resolveSeed(\n value: SeedValue,\n ctx: EvalContext,\n): EvalResult<unknown> {\n if (value === null || value === undefined) {\n return { ok: true, value };\n }\n const t = typeof value;\n if (t === 'string' || t === 'number' || t === 'boolean') {\n return { ok: true, value };\n }\n if (value instanceof Date) {\n return { ok: true, value };\n }\n if (Array.isArray(value)) {\n const out: unknown[] = [];\n for (const item of value) {\n const r = resolveSeed(item, ctx);\n if (!r.ok) return r;\n out.push(r.value);\n }\n return { ok: true, value: out };\n }\n if (isExpressionLike(value)) {\n return ExpressionEngine.evaluate(value, ctx);\n }\n // Plain object — recurse field-by-field.\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, SeedValue>)) {\n const r = resolveSeed(v, ctx);\n if (!r.ok) return r;\n out[k] = r.value;\n }\n return { ok: true, value: out };\n}\n\n/**\n * Resolve a single record (object of fields), pinning `ctx.now` so all\n * expressions within see one logical clock.\n */\nexport function resolveSeedRecord(\n record: Record<string, SeedValue>,\n ctx: EvalContext,\n): EvalResult<Record<string, unknown>> {\n const pinnedCtx: EvalContext = { ...ctx, now: ctx.now ?? new Date() };\n const result = resolveSeed(record, pinnedCtx) as EvalResult<Record<string, unknown>>;\n return result;\n}\n","/**\n * Build-time normalization helpers.\n *\n * The CLI `objectstack compile` step walks the assembled `objectstack.json`\n * artifact and rewrites every Expression so that:\n *\n * 1. String shorthand input is replaced by `{ dialect: 'cel', source }`.\n * 2. The persisted envelope carries an `ast` field produced by the dialect\n * engine (M9.2 deliverable). Source is retained for round-trip / debug.\n *\n * Spec layer cannot do step 2 because it must remain dependency-free; this\n * package owns the engine import and therefore the AST step.\n */\n\nimport {\n ExpressionInputSchema,\n ExpressionSchema,\n type Expression,\n type ExpressionInput,\n} from '@objectstack/spec';\n\nimport { ExpressionEngine } from './registry';\nimport type { EvalResult } from './types';\n\n/**\n * Normalize an {@link ExpressionInput} (string shorthand OR full envelope) into\n * a fully-resolved {@link Expression} carrying both `source` and `ast`.\n *\n * Returns an EvalResult so the caller can render a structured compile error\n * pointing at the offending metadata path.\n */\nexport function normalizeExpression(input: ExpressionInput): EvalResult<Expression> {\n const parsed = ExpressionInputSchema.safeParse(input);\n if (!parsed.success) {\n return {\n ok: false,\n error: { kind: 'parse', message: parsed.error.message },\n };\n }\n\n const expr = parsed.data as Expression;\n\n // Already AST-only — accept as-is.\n if (expr.ast !== undefined && expr.source === undefined) {\n return { ok: true, value: expr };\n }\n\n // Source-bearing: ask the dialect engine to compile. Failures surface here\n // as part of the build (no silent skip).\n const compiled = ExpressionEngine.compile(expr);\n if (!compiled.ok) {\n return compiled;\n }\n\n return {\n ok: true,\n value: {\n ...expr,\n ast: compiled.value,\n },\n };\n}\n\n/**\n * Walk an arbitrary JSON tree and normalize every embedded Expression in\n * place. Used by the build pipeline to traverse the assembled metadata\n * artifact. Returns the first error encountered (paired with the dotted path\n * for diagnostics) or `null` when fully clean.\n */\nexport function normalizeExpressionTree(\n root: unknown,\n path: string[] = [],\n): { path: string; error: import('./types').EvalError } | null {\n if (root === null || typeof root !== 'object') return null;\n\n if (looksLikeExpression(root)) {\n const r = normalizeExpression(root as ExpressionInput);\n if (!r.ok) return { path: path.join('.'), error: r.error };\n Object.assign(root as Record<string, unknown>, r.value);\n return null;\n }\n\n if (Array.isArray(root)) {\n for (let i = 0; i < root.length; i++) {\n const r = normalizeExpressionTree(root[i], [...path, String(i)]);\n if (r) return r;\n }\n return null;\n }\n\n for (const [k, v] of Object.entries(root as Record<string, unknown>)) {\n const r = normalizeExpressionTree(v, [...path, k]);\n if (r) return r;\n }\n return null;\n}\n\nfunction looksLikeExpression(value: unknown): boolean {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const v = value as Record<string, unknown>;\n if (typeof v.dialect !== 'string') return false;\n return ExpressionSchema.safeParse(v).success;\n}\n"],"mappings":"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","names":["out","ExpressionSchema","ExpressionSchema"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/formula",
3
- "version": "4.0.4",
3
+ "version": "4.0.5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "ObjectStack canonical expression engine — CEL (cel-js) + ObjectStack stdlib + dialect registry",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@marcbachmann/cel-js": "^7.6.1",
17
- "@objectstack/spec": "4.0.4"
17
+ "@objectstack/spec": "4.0.5"
18
18
  },
19
19
  "devDependencies": {
20
20
  "typescript": "^6.0.3",
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { cronEngine } from './cron-engine';
4
+
5
+ describe('cronEngine', () => {
6
+ it('accepts standard 5-field cron', () => {
7
+ const r = cronEngine.evaluate({ dialect: 'cron', source: '0 9 * * 1-5' }, {});
8
+ expect(r.ok).toBe(true);
9
+ });
10
+
11
+ it('accepts 6-field cron', () => {
12
+ const r = cronEngine.evaluate({ dialect: 'cron', source: '0 0 9 * * 1' }, {});
13
+ expect(r.ok).toBe(true);
14
+ });
15
+
16
+ it('accepts aliases', () => {
17
+ for (const alias of ['@daily', '@hourly', '@weekly', '@monthly', '@yearly']) {
18
+ expect(cronEngine.evaluate({ dialect: 'cron', source: alias }, {}).ok).toBe(true);
19
+ }
20
+ });
21
+
22
+ it('rejects empty source', () => {
23
+ const r = cronEngine.evaluate({ dialect: 'cron', source: ' ' }, {});
24
+ expect(r.ok).toBe(false);
25
+ });
26
+
27
+ it('rejects unknown alias', () => {
28
+ const r = cronEngine.evaluate({ dialect: 'cron', source: '@whenever' }, {});
29
+ expect(r.ok).toBe(false);
30
+ });
31
+
32
+ it('rejects wrong field count', () => {
33
+ const r = cronEngine.evaluate({ dialect: 'cron', source: '* * *' }, {});
34
+ expect(r.ok).toBe(false);
35
+ });
36
+
37
+ it('rejects invalid characters', () => {
38
+ const r = cronEngine.evaluate({ dialect: 'cron', source: '* * * * !' }, {});
39
+ expect(r.ok).toBe(false);
40
+ });
41
+ });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Cron dialect engine.
3
+ *
4
+ * Validates cron expressions at compile time without depending on a parser.
5
+ * Actual schedule firing lives in the scheduler service — this engine just
6
+ * round-trips the expression through `Expression.evaluate`, returning the
7
+ * source so callers can hand it to a scheduler library.
8
+ *
9
+ * Accepted forms:
10
+ * - 5-field standard cron: `m h dom mon dow`
11
+ * - 6-field extended cron: `s m h dom mon dow`
12
+ * - Aliases: @yearly, @annually, @monthly, @weekly, @daily, @hourly, @reboot
13
+ */
14
+
15
+ import type { Expression } from '@objectstack/spec';
16
+
17
+ import type { DialectEngine, EvalContext, EvalResult } from './types';
18
+
19
+ const ALIASES = new Set([
20
+ '@yearly', '@annually', '@monthly', '@weekly', '@daily', '@hourly', '@reboot',
21
+ ]);
22
+
23
+ function validate(source: string): EvalResult<string> {
24
+ const trimmed = source.trim();
25
+ if (trimmed.length === 0) {
26
+ return { ok: false, error: { kind: 'parse', message: 'cron source is empty' } };
27
+ }
28
+ if (trimmed.startsWith('@')) {
29
+ if (!ALIASES.has(trimmed)) {
30
+ return {
31
+ ok: false,
32
+ error: { kind: 'parse', message: `unknown cron alias '${trimmed}'` },
33
+ };
34
+ }
35
+ return { ok: true, value: trimmed };
36
+ }
37
+ const fields = trimmed.split(/\s+/);
38
+ if (fields.length !== 5 && fields.length !== 6) {
39
+ return {
40
+ ok: false,
41
+ error: {
42
+ kind: 'parse',
43
+ message: `cron requires 5 or 6 space-separated fields, got ${fields.length}`,
44
+ },
45
+ };
46
+ }
47
+ // Each field must use only allowed cron characters.
48
+ const allowed = /^[\d*/,\-?LWA-Z#]+$/i;
49
+ for (let i = 0; i < fields.length; i++) {
50
+ if (!allowed.test(fields[i])) {
51
+ return {
52
+ ok: false,
53
+ error: {
54
+ kind: 'parse',
55
+ message: `cron field ${i + 1} contains invalid characters: '${fields[i]}'`,
56
+ },
57
+ };
58
+ }
59
+ }
60
+ return { ok: true, value: trimmed };
61
+ }
62
+
63
+ export const cronEngine: DialectEngine = {
64
+ dialect: 'cron',
65
+
66
+ compile(source: string): EvalResult<unknown> {
67
+ return validate(source);
68
+ },
69
+
70
+ evaluate<T = unknown>(expr: Expression, _ctx: EvalContext): EvalResult<T> {
71
+ if (expr.dialect !== 'cron') {
72
+ return {
73
+ ok: false,
74
+ error: { kind: 'dialect', message: `cronEngine cannot evaluate dialect '${expr.dialect}'` },
75
+ };
76
+ }
77
+ if (typeof expr.source !== 'string') {
78
+ return { ok: false, error: { kind: 'parse', message: 'cron Expression.source required' } };
79
+ }
80
+ const result = validate(expr.source);
81
+ if (!result.ok) return result as EvalResult<T>;
82
+ // Cron expressions don't "evaluate" to a value at predicate time — they
83
+ // describe a schedule. Returning the source lets schedulers consume it.
84
+ return { ok: true, value: result.value as unknown as T };
85
+ },
86
+ };
package/src/index.ts CHANGED
@@ -11,6 +11,8 @@
11
11
 
12
12
  export { ExpressionEngine, getEngine, hasDialect, register } from './registry';
13
13
  export { celEngine, DEFAULT_LIMITS } from './cel-engine';
14
+ export { cronEngine } from './cron-engine';
15
+ export { templateEngine } from './template-engine';
14
16
  export { registerStdLib, buildScope } from './stdlib';
15
17
  export { resolveSeed, resolveSeedRecord } from './seed-eval';
16
18
  export { normalizeExpression, normalizeExpressionTree } from './normalize';
@@ -17,11 +17,25 @@ describe('ExpressionEngine registry', () => {
17
17
  if (!r.ok) expect(r.error.kind).toBe('dialect');
18
18
  });
19
19
 
20
- it('returns dialect error for cron stub', () => {
20
+ it('routes cron dialect to cronEngine (validates schedule)', () => {
21
21
  const expr: Expression = { dialect: 'cron', source: '* * * * *' };
22
22
  const r = ExpressionEngine.evaluate(expr, {});
23
+ expect(r.ok).toBe(true);
24
+ if (r.ok) expect(r.value).toBe('* * * * *');
25
+ });
26
+
27
+ it('cron rejects malformed source', () => {
28
+ const r = ExpressionEngine.evaluate({ dialect: 'cron', source: 'not a cron' }, {});
23
29
  expect(r.ok).toBe(false);
24
- if (!r.ok) expect(r.error.kind).toBe('dialect');
30
+ if (!r.ok) expect(r.error.kind).toBe('parse');
31
+ });
32
+
33
+ it('routes template dialect to templateEngine', () => {
34
+ const r = ExpressionEngine.evaluate(
35
+ { dialect: 'template', source: 'Hello {{record.name}}' },
36
+ { record: { name: 'World' } },
37
+ );
38
+ expect(r).toEqual({ ok: true, value: 'Hello World' });
25
39
  });
26
40
 
27
41
  it('returns dialect error for unknown dialect', () => {
package/src/registry.ts CHANGED
@@ -13,6 +13,8 @@
13
13
  import type { Expression } from '@objectstack/spec';
14
14
 
15
15
  import { celEngine } from './cel-engine';
16
+ import { cronEngine } from './cron-engine';
17
+ import { templateEngine } from './template-engine';
16
18
  import type { DialectEngine, EvalContext, EvalResult } from './types';
17
19
 
18
20
  const registry = new Map<string, DialectEngine>();
@@ -42,10 +44,11 @@ function makeStub(dialect: string, reason: string): DialectEngine {
42
44
 
43
45
  // Real engines.
44
46
  register(celEngine);
47
+ register(cronEngine);
48
+ register(templateEngine);
45
49
 
46
- // Stubs — phased in by later milestones (M9.5+ for `js`, M9.6 for `cron`).
50
+ // Stubs — `js` lives in @objectstack/plugin-js-vm (not yet shipped).
47
51
  register(makeStub('js', "dialect 'js' not registered. Install @objectstack/plugin-js-vm"));
48
- register(makeStub('cron', "dialect 'cron' not registered. Install @objectstack/plugin-cron"));
49
52
 
50
53
  /**
51
54
  * The unified evaluation entry point. Replaces the old direct calls to
package/src/stdlib.ts CHANGED
@@ -54,6 +54,25 @@ export function registerStdLib(
54
54
  .registerFunction(
55
55
  'daysAgo(int): google.protobuf.Timestamp',
56
56
  (n: bigint | number) => addDaysUtc(now(), -Number(n)),
57
+ )
58
+ // Returns true when `value` is null, undefined, empty string, or empty list.
59
+ // Matches the intent of legacy `ISBLANK()` while staying CEL-idiomatic.
60
+ .registerFunction(
61
+ 'isBlank(dyn): bool',
62
+ (value: unknown) => {
63
+ if (value === null || value === undefined) return true;
64
+ if (typeof value === 'string') return value.length === 0;
65
+ if (Array.isArray(value)) return value.length === 0;
66
+ return false;
67
+ },
68
+ )
69
+ // Returns `value` when not null/undefined, otherwise the `fallback`.
70
+ // Use this to safely concatenate optional string fields:
71
+ // coalesce(record.salutation, '') + ' ' + coalesce(record.first_name, '')
72
+ .registerFunction(
73
+ 'coalesce(dyn, dyn): dyn',
74
+ (value: unknown, fallback: unknown) =>
75
+ (value === null || value === undefined) ? fallback : value,
57
76
  );
58
77
  }
59
78
 
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { templateEngine } from './template-engine';
4
+
5
+ describe('templateEngine', () => {
6
+ it('substitutes simple paths', () => {
7
+ const r = templateEngine.evaluate(
8
+ { dialect: 'template', source: 'Hi {{record.name}}!' },
9
+ { record: { name: 'Lisa' } },
10
+ );
11
+ expect(r).toEqual({ ok: true, value: 'Hi Lisa!' });
12
+ });
13
+
14
+ it('renders nested paths', () => {
15
+ const r = templateEngine.evaluate(
16
+ { dialect: 'template', source: '{{record.account.name}} ({{record.account.tier}})' },
17
+ { record: { account: { name: 'Acme', tier: 'gold' } } },
18
+ );
19
+ expect(r).toEqual({ ok: true, value: 'Acme (gold)' });
20
+ });
21
+
22
+ it('renders empty for missing paths', () => {
23
+ const r = templateEngine.evaluate(
24
+ { dialect: 'template', source: 'X={{record.missing}}Y' },
25
+ { record: {} },
26
+ );
27
+ expect(r).toEqual({ ok: true, value: 'X=Y' });
28
+ });
29
+
30
+ it('exposes os.user / os.org / os.env', () => {
31
+ const r = templateEngine.evaluate(
32
+ { dialect: 'template', source: '{{os.user.id}}@{{os.org.id}}' },
33
+ { user: { id: 'u1' }, org: { id: 'o1' } },
34
+ );
35
+ expect(r).toEqual({ ok: true, value: 'u1@o1' });
36
+ });
37
+
38
+ it('rejects unbalanced delimiters', () => {
39
+ const r = templateEngine.compile('Hi {{name');
40
+ expect(r.ok).toBe(false);
41
+ });
42
+
43
+ it('refuses non-template dialect', () => {
44
+ const r = templateEngine.evaluate(
45
+ { dialect: 'cel', source: '1' } as never,
46
+ {},
47
+ );
48
+ expect(r.ok).toBe(false);
49
+ });
50
+
51
+ it('handles bracket notation', () => {
52
+ const r = templateEngine.evaluate(
53
+ { dialect: 'template', source: '{{record.tags[0]}}' },
54
+ { record: { tags: ['hot', 'cold'] } },
55
+ );
56
+ expect(r).toEqual({ ok: true, value: 'hot' });
57
+ });
58
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Template dialect engine — strict Mustache subset.
3
+ *
4
+ * Supports `{{path.to.value}}` interpolation only. No conditionals, no loops,
5
+ * no helpers. The variable scope is the same as CEL (`record`, `previous`,
6
+ * `input`, `os.user`, `os.org`, `os.env`, plus `extra`), so authors can move
7
+ * fluidly between a CEL formula and a template body without re-learning a
8
+ * second variable namespace.
9
+ *
10
+ * Why a separate dialect from CEL: templates produce strings (notification
11
+ * subjects, prompt bodies, titleFormat). CEL is a value-typed expression
12
+ * language. Routing them through the same envelope (`{ dialect: 'template' }`)
13
+ * keeps the AI author rule simple — "anything templated or computed is an
14
+ * Expression" — without conflating the two semantics.
15
+ */
16
+
17
+ import type { Expression } from '@objectstack/spec';
18
+
19
+ import { buildScope } from './stdlib';
20
+ import type { DialectEngine, EvalContext, EvalResult } from './types';
21
+
22
+ const PATH_RE = /\{\{\s*([\w.[\]]+?)\s*\}\}/g;
23
+
24
+ function resolvePath(scope: Record<string, unknown>, path: string): unknown {
25
+ // Support `a.b.c` and `a[0].b` style. Bracket notation collapses to dotted.
26
+ const normalized = path.replace(/\[(\w+)\]/g, '.$1');
27
+ const segments = normalized.split('.').filter(Boolean);
28
+ let cursor: unknown = scope;
29
+ for (const seg of segments) {
30
+ if (cursor == null || typeof cursor !== 'object') return undefined;
31
+ cursor = (cursor as Record<string, unknown>)[seg];
32
+ }
33
+ return cursor;
34
+ }
35
+
36
+ function stringify(value: unknown): string {
37
+ if (value === null || value === undefined) return '';
38
+ if (value instanceof Date) return value.toISOString();
39
+ if (typeof value === 'string') return value;
40
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
41
+ if (typeof value === 'bigint') return value.toString();
42
+ try {
43
+ return JSON.stringify(value);
44
+ } catch {
45
+ return String(value);
46
+ }
47
+ }
48
+
49
+ function compileTemplate(source: string): EvalResult<string[]> {
50
+ // Compile is only a structural validity check — no helpers, no balanced
51
+ // open/close beyond what the regex enforces.
52
+ const matches = source.match(/\{\{|\}\}/g) ?? [];
53
+ if (matches.length % 2 !== 0) {
54
+ return {
55
+ ok: false,
56
+ error: { kind: 'parse', message: 'template has unbalanced {{ }} delimiters' },
57
+ };
58
+ }
59
+ const refs: string[] = [];
60
+ let m: RegExpExecArray | null;
61
+ PATH_RE.lastIndex = 0;
62
+ while ((m = PATH_RE.exec(source)) !== null) {
63
+ refs.push(m[1]);
64
+ }
65
+ return { ok: true, value: refs };
66
+ }
67
+
68
+ export const templateEngine: DialectEngine = {
69
+ dialect: 'template',
70
+
71
+ compile(source: string): EvalResult<unknown> {
72
+ return compileTemplate(source);
73
+ },
74
+
75
+ evaluate<T = unknown>(expr: Expression, ctx: EvalContext): EvalResult<T> {
76
+ if (expr.dialect !== 'template') {
77
+ return {
78
+ ok: false,
79
+ error: { kind: 'dialect', message: `templateEngine cannot evaluate dialect '${expr.dialect}'` },
80
+ };
81
+ }
82
+ if (typeof expr.source !== 'string') {
83
+ return {
84
+ ok: false,
85
+ error: { kind: 'parse', message: 'template Expression.source required' },
86
+ };
87
+ }
88
+ const check = compileTemplate(expr.source);
89
+ if (!check.ok) return check as EvalResult<T>;
90
+
91
+ const scope = buildScope(ctx);
92
+ const out = expr.source.replace(PATH_RE, (_match, path) => {
93
+ return stringify(resolvePath(scope, path));
94
+ });
95
+ return { ok: true, value: out as unknown as T };
96
+ },
97
+ };