@objectstack/formula 7.5.0 → 7.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @objectstack/formula@7.5.0 build /home/runner/work/framework/framework/packages/formula
2
+ > @objectstack/formula@7.6.0 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 13.16 KB
14
- ESM dist/index.mjs.map 34.88 KB
15
- ESM ⚡️ Build success in 91ms
16
- CJS dist/index.js 14.65 KB
17
- CJS dist/index.js.map 35.85 KB
18
- CJS ⚡️ Build success in 91ms
13
+ ESM dist/index.mjs 21.98 KB
14
+ ESM dist/index.mjs.map 55.52 KB
15
+ ESM ⚡️ Build success in 76ms
16
+ CJS dist/index.js 23.69 KB
17
+ CJS dist/index.js.map 56.88 KB
18
+ CJS ⚡️ Build success in 78ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 4396ms
21
- DTS dist/index.d.mts 10.73 KB
22
- DTS dist/index.d.ts 10.73 KB
20
+ DTS ⚡️ Build success in 4811ms
21
+ DTS dist/index.d.mts 14.02 KB
22
+ DTS dist/index.d.ts 14.02 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # @objectstack/formula
2
2
 
3
+ ## 7.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c4a4cbd: ADR-0032 (phase 1): validate-by-default expression layer — no silent failure.
8
+
9
+ Kills the #1491 class where a malformed predicate (e.g. the `{record.x}`
10
+ template-brace-in-CEL mistake) silently evaluated to `false` and made a flow
11
+ "fire" with no effect:
12
+
13
+ - **service-automation**: flow `evaluateCondition` no longer swallows CEL
14
+ failures to `false` — it throws an attributed, corrective error; and
15
+ `registerFlow` now parse-validates every predicate (start/decision/edge
16
+ condition) at registration, failing loudly with the offending location +
17
+ source + the fix.
18
+ - **formula**: new shared validator — `validateExpression(role, src, schema?)`,
19
+ `introspectScope`, `CEL_STDLIB_FUNCTIONS` — with schema-aware field-existence
20
+ - did-you-mean. The `{{ }}` template engine gains a formatter whitelist
21
+ (`currency`/`number`/`percent`/`date`/`datetime`/`truncate`/`upper`/`lower`/
22
+ `default`/…) with defined value→string semantics; arbitrary logic in holes is
23
+ rejected. Plain `{{ path }}` stays back-compatible.
24
+ - **cli**: `objectstack compile` validates every flow / validation-rule /
25
+ field-formula predicate against the resolved object schema and fails the
26
+ build with located, corrective messages.
27
+ - **service-ai**: new agent-callable `validate_expression` tool so authoring
28
+ agents self-correct before committing.
29
+ - **spec**: fix the `FlowSchema` JSDoc example that taught the bad
30
+ `condition: "{amount} < 500"` single-brace form.
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [955d4c8]
35
+ - Updated dependencies [c4a4cbd]
36
+ - Updated dependencies [b046ec2]
37
+ - Updated dependencies [2170ad9]
38
+ - Updated dependencies [02d6359]
39
+ - Updated dependencies [7648242]
40
+ - Updated dependencies [8fa1e7f]
41
+ - Updated dependencies [55866f5]
42
+ - Updated dependencies [60f9c45]
43
+ - @objectstack/spec@7.6.0
44
+
3
45
  ## 7.5.0
4
46
 
5
47
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -179,21 +179,25 @@ declare const celEngine: DialectEngine;
179
179
  declare const cronEngine: DialectEngine;
180
180
 
181
181
  /**
182
- * Template dialect engine — strict Mustache subset.
182
+ * Template dialect engine — strict Mustache subset with a formatter whitelist.
183
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.
184
+ * Holes are `{{ path }}` or `{{ path | formatter[:'arg'] }}` (ADR-0032 §3).
185
+ * Holes are restricted to a **field/variable path** plus a **whitelisted
186
+ * formatter** never arbitrary CEL logic so the grammar stays small (low
187
+ * author/agent error surface), GUI-pickable (path + formatter dropdown), and
188
+ * display strings stay declarative. Real logic belongs in `Predicate`/`Expr`
189
+ * (CEL) fields, where it is validated and visible.
189
190
  *
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.
191
+ * The variable scope is the same as CEL (`record`, `previous`, `input`,
192
+ * `os.user/org/env`, plus `extra`), so authors move fluidly between a CEL
193
+ * formula and a template body without re-learning a namespace.
194
+ *
195
+ * Value→string semantics are explicit and defined per formatter (numbers,
196
+ * dates, money, percent, null), instead of implicit coercion.
195
197
  */
196
198
 
199
+ /** Public list of whitelisted template formatters (for introspection/docs). */
200
+ declare const TEMPLATE_FORMATTERS: string[];
197
201
  declare const templateEngine: DialectEngine;
198
202
 
199
203
  /**
@@ -271,4 +275,72 @@ declare function normalizeExpressionTree(root: unknown, path?: string[]): {
271
275
  error: EvalError;
272
276
  } | null;
273
277
 
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 };
278
+ /**
279
+ * Shared expression validator (ADR-0032 §Decision 1/5).
280
+ *
281
+ * One validator, used by every author surface — `objectstack build`,
282
+ * `registerFlow`/metadata registration, and the agent-callable
283
+ * `validate_expression` tool — so a malformed expression is caught the same
284
+ * way everywhere, with a message written for **self-correction** (Decision 1d):
285
+ * it states what is wrong AND the correct form.
286
+ *
287
+ * Field roles map to dialects (Decision 2):
288
+ * - `predicate` → bare CEL returning bool (`record.rating >= 4`)
289
+ * - `value` → bare CEL of any type (`daysFromNow(3)`)
290
+ * - `template` → text with `{{ path }}` holes (`Hot lead: {{ record.name }}`)
291
+ *
292
+ * The #1 author error (human or LLM) is wrapping a field reference in single
293
+ * `{…}` braces inside a CEL field — `{x}` parses as a CEL map literal and fails.
294
+ * This validator detects that specific mistake and returns the exact fix.
295
+ */
296
+ type FieldRole = 'predicate' | 'value' | 'template';
297
+ /**
298
+ * Loose input accepted by the validator: a bare string, or any object exposing
299
+ * `dialect`/`source` (the Expression envelope, or a not-yet-narrowed value from
300
+ * a `config.condition` / `edge.condition` field). Kept structural so call sites
301
+ * need not pre-narrow to the strict {@link Expression} dialect union.
302
+ */
303
+ type ExprInput = string | {
304
+ dialect?: string;
305
+ source?: string;
306
+ } | null | undefined;
307
+ /** Optional schema context for field-existence checks (Decision 1b, v1). */
308
+ interface ExprSchemaHint {
309
+ /** Object the expression is authored against (for error text). */
310
+ objectName?: string;
311
+ /** Known top-level field names, so `record.<field>` can be checked. */
312
+ fields?: readonly string[];
313
+ }
314
+ interface ExprValidationError {
315
+ /** Self-correcting message: what is wrong + the correct form. */
316
+ message: string;
317
+ /** The offending source, echoed for location. */
318
+ source: string;
319
+ }
320
+ interface ExprValidationResult {
321
+ ok: boolean;
322
+ errors: ExprValidationError[];
323
+ }
324
+ /** The dialect a field role expects (Decision 2). */
325
+ declare function expectedDialect(role: FieldRole): 'cel' | 'template';
326
+ /**
327
+ * Validate one expression for a given field role. Never throws — returns a
328
+ * structured result. Call sites decide whether to throw (build/registration)
329
+ * or report (agent tool).
330
+ */
331
+ declare function validateExpression(role: FieldRole, input: ExprInput, schema?: ExprSchemaHint): ExprValidationResult;
332
+ /**
333
+ * Introspect what an author (esp. an agent) may use in a field (Decision 1e):
334
+ * the expected dialect, the in-scope field references, and the callable
335
+ * functions. Feeds the authoring context so the model does not guess.
336
+ */
337
+ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
338
+ dialect: 'cel' | 'template';
339
+ fields: string[];
340
+ roots: string[];
341
+ functions: string[];
342
+ };
343
+ /** Public catalog of CEL stdlib functions available in expressions. */
344
+ declare const CEL_STDLIB_FUNCTIONS: string[];
345
+
346
+ export { CEL_STDLIB_FUNCTIONS, DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, type ExprSchemaHint, type ExprValidationError, type ExprValidationResult, ExpressionEngine, type FieldRole, type SeedPrimitive, type SeedValue, TEMPLATE_FORMATTERS, buildScope, celEngine, cronEngine, expectedDialect, getEngine, hasDialect, introspectScope, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };
package/dist/index.d.ts CHANGED
@@ -179,21 +179,25 @@ declare const celEngine: DialectEngine;
179
179
  declare const cronEngine: DialectEngine;
180
180
 
181
181
  /**
182
- * Template dialect engine — strict Mustache subset.
182
+ * Template dialect engine — strict Mustache subset with a formatter whitelist.
183
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.
184
+ * Holes are `{{ path }}` or `{{ path | formatter[:'arg'] }}` (ADR-0032 §3).
185
+ * Holes are restricted to a **field/variable path** plus a **whitelisted
186
+ * formatter** never arbitrary CEL logic so the grammar stays small (low
187
+ * author/agent error surface), GUI-pickable (path + formatter dropdown), and
188
+ * display strings stay declarative. Real logic belongs in `Predicate`/`Expr`
189
+ * (CEL) fields, where it is validated and visible.
189
190
  *
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.
191
+ * The variable scope is the same as CEL (`record`, `previous`, `input`,
192
+ * `os.user/org/env`, plus `extra`), so authors move fluidly between a CEL
193
+ * formula and a template body without re-learning a namespace.
194
+ *
195
+ * Value→string semantics are explicit and defined per formatter (numbers,
196
+ * dates, money, percent, null), instead of implicit coercion.
195
197
  */
196
198
 
199
+ /** Public list of whitelisted template formatters (for introspection/docs). */
200
+ declare const TEMPLATE_FORMATTERS: string[];
197
201
  declare const templateEngine: DialectEngine;
198
202
 
199
203
  /**
@@ -271,4 +275,72 @@ declare function normalizeExpressionTree(root: unknown, path?: string[]): {
271
275
  error: EvalError;
272
276
  } | null;
273
277
 
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 };
278
+ /**
279
+ * Shared expression validator (ADR-0032 §Decision 1/5).
280
+ *
281
+ * One validator, used by every author surface — `objectstack build`,
282
+ * `registerFlow`/metadata registration, and the agent-callable
283
+ * `validate_expression` tool — so a malformed expression is caught the same
284
+ * way everywhere, with a message written for **self-correction** (Decision 1d):
285
+ * it states what is wrong AND the correct form.
286
+ *
287
+ * Field roles map to dialects (Decision 2):
288
+ * - `predicate` → bare CEL returning bool (`record.rating >= 4`)
289
+ * - `value` → bare CEL of any type (`daysFromNow(3)`)
290
+ * - `template` → text with `{{ path }}` holes (`Hot lead: {{ record.name }}`)
291
+ *
292
+ * The #1 author error (human or LLM) is wrapping a field reference in single
293
+ * `{…}` braces inside a CEL field — `{x}` parses as a CEL map literal and fails.
294
+ * This validator detects that specific mistake and returns the exact fix.
295
+ */
296
+ type FieldRole = 'predicate' | 'value' | 'template';
297
+ /**
298
+ * Loose input accepted by the validator: a bare string, or any object exposing
299
+ * `dialect`/`source` (the Expression envelope, or a not-yet-narrowed value from
300
+ * a `config.condition` / `edge.condition` field). Kept structural so call sites
301
+ * need not pre-narrow to the strict {@link Expression} dialect union.
302
+ */
303
+ type ExprInput = string | {
304
+ dialect?: string;
305
+ source?: string;
306
+ } | null | undefined;
307
+ /** Optional schema context for field-existence checks (Decision 1b, v1). */
308
+ interface ExprSchemaHint {
309
+ /** Object the expression is authored against (for error text). */
310
+ objectName?: string;
311
+ /** Known top-level field names, so `record.<field>` can be checked. */
312
+ fields?: readonly string[];
313
+ }
314
+ interface ExprValidationError {
315
+ /** Self-correcting message: what is wrong + the correct form. */
316
+ message: string;
317
+ /** The offending source, echoed for location. */
318
+ source: string;
319
+ }
320
+ interface ExprValidationResult {
321
+ ok: boolean;
322
+ errors: ExprValidationError[];
323
+ }
324
+ /** The dialect a field role expects (Decision 2). */
325
+ declare function expectedDialect(role: FieldRole): 'cel' | 'template';
326
+ /**
327
+ * Validate one expression for a given field role. Never throws — returns a
328
+ * structured result. Call sites decide whether to throw (build/registration)
329
+ * or report (agent tool).
330
+ */
331
+ declare function validateExpression(role: FieldRole, input: ExprInput, schema?: ExprSchemaHint): ExprValidationResult;
332
+ /**
333
+ * Introspect what an author (esp. an agent) may use in a field (Decision 1e):
334
+ * the expected dialect, the in-scope field references, and the callable
335
+ * functions. Feeds the authoring context so the model does not guess.
336
+ */
337
+ declare function introspectScope(role: FieldRole, schema?: ExprSchemaHint): {
338
+ dialect: 'cel' | 'template';
339
+ fields: string[];
340
+ roots: string[];
341
+ functions: string[];
342
+ };
343
+ /** Public catalog of CEL stdlib functions available in expressions. */
344
+ declare const CEL_STDLIB_FUNCTIONS: string[];
345
+
346
+ export { CEL_STDLIB_FUNCTIONS, DEFAULT_LIMITS, type DialectEngine, type EvalContext, type EvalError, type EvalResult, type ExprSchemaHint, type ExprValidationError, type ExprValidationResult, ExpressionEngine, type FieldRole, type SeedPrimitive, type SeedValue, TEMPLATE_FORMATTERS, buildScope, celEngine, cronEngine, expectedDialect, getEngine, hasDialect, introspectScope, normalizeExpression, normalizeExpressionTree, register, registerStdLib, resolveSeed, resolveSeedRecord, templateEngine, validateExpression };