@objectstack/formula 7.5.0 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +57 -0
- package/dist/index.d.mts +84 -12
- package/dist/index.d.ts +84 -12
- package/dist/index.js +331 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +325 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/cel-engine.test.ts +65 -0
- package/src/cel-engine.ts +73 -2
- package/src/index.ts +5 -1
- package/src/template-engine.ts +194 -42
- package/src/template-formatters.test.ts +55 -0
- package/src/validate.test.ts +73 -0
- package/src/validate.ts +207 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @objectstack/formula@7.
|
|
2
|
+
> @objectstack/formula@7.7.0 build /home/runner/work/framework/framework/packages/formula
|
|
3
3
|
> tsup --config ../../tsup.config.ts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
12
|
[34mCJS[39m Build start
|
|
13
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
14
|
-
[32mESM[39m [1mdist/index.mjs.map [22m[
|
|
15
|
-
[32mESM[39m ⚡️ Build success in
|
|
16
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
17
|
-
[32mCJS[39m [1mdist/index.js.map [22m[
|
|
18
|
-
[32mCJS[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m23.06 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/index.mjs.map [22m[32m59.71 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 137ms
|
|
16
|
+
[32mCJS[39m [1mdist/index.js [22m[32m24.77 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js.map [22m[32m61.07 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 142ms
|
|
19
19
|
[34mDTS[39m Build start
|
|
20
|
-
[32mDTS[39m ⚡️ Build success in
|
|
21
|
-
[32mDTS[39m [1mdist/index.d.mts [22m[
|
|
22
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
20
|
+
[32mDTS[39m ⚡️ Build success in 4685ms
|
|
21
|
+
[32mDTS[39m [1mdist/index.d.mts [22m[32m14.02 KB[39m
|
|
22
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m14.02 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# @objectstack/formula
|
|
2
2
|
|
|
3
|
+
## 7.7.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 825ab06: fix(formula): hydrate string-serialized numeric fields in CEL comparisons (#1534)
|
|
8
|
+
|
|
9
|
+
Numeric fields that serialize as strings — `Field.rating(allowHalf)` → `"5.0"`, `Field.currency(scale)` → `"250000.00"`, `Field.percent` — made comparisons like `record.rating >= 4` fault under strict CEL with `no such overload: dyn >= int`. In flow decision/edge conditions this silently dead-ended the run (no edge matched), and in objectql `applyFormulaPlan` it swallowed to `null`.
|
|
10
|
+
|
|
11
|
+
The CEL engine now retries an evaluation **once** with purely-numeric strings hydrated to numbers, but only after a `no such overload` fault — so a comparison that already type-checks is never re-interpreted (a zip like `"02134"` stays a string in `record.zip == "02134"`). Because both the automation condition path (`service-automation` `evaluateCondition`) and the objectql formula path route through `ExpressionEngine.evaluate`, both are fixed consistently. A genuinely non-numeric operand (e.g. `record.rating >= 4` where `rating` is `"high"`) still faults loudly rather than being silently rescued.
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [b391955]
|
|
14
|
+
- Updated dependencies [f06b64e]
|
|
15
|
+
- Updated dependencies [023bf93]
|
|
16
|
+
- @objectstack/spec@7.7.0
|
|
17
|
+
|
|
18
|
+
## 7.6.0
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- c4a4cbd: ADR-0032 (phase 1): validate-by-default expression layer — no silent failure.
|
|
23
|
+
|
|
24
|
+
Kills the #1491 class where a malformed predicate (e.g. the `{record.x}`
|
|
25
|
+
template-brace-in-CEL mistake) silently evaluated to `false` and made a flow
|
|
26
|
+
"fire" with no effect:
|
|
27
|
+
|
|
28
|
+
- **service-automation**: flow `evaluateCondition` no longer swallows CEL
|
|
29
|
+
failures to `false` — it throws an attributed, corrective error; and
|
|
30
|
+
`registerFlow` now parse-validates every predicate (start/decision/edge
|
|
31
|
+
condition) at registration, failing loudly with the offending location +
|
|
32
|
+
source + the fix.
|
|
33
|
+
- **formula**: new shared validator — `validateExpression(role, src, schema?)`,
|
|
34
|
+
`introspectScope`, `CEL_STDLIB_FUNCTIONS` — with schema-aware field-existence
|
|
35
|
+
- did-you-mean. The `{{ }}` template engine gains a formatter whitelist
|
|
36
|
+
(`currency`/`number`/`percent`/`date`/`datetime`/`truncate`/`upper`/`lower`/
|
|
37
|
+
`default`/…) with defined value→string semantics; arbitrary logic in holes is
|
|
38
|
+
rejected. Plain `{{ path }}` stays back-compatible.
|
|
39
|
+
- **cli**: `objectstack compile` validates every flow / validation-rule /
|
|
40
|
+
field-formula predicate against the resolved object schema and fails the
|
|
41
|
+
build with located, corrective messages.
|
|
42
|
+
- **service-ai**: new agent-callable `validate_expression` tool so authoring
|
|
43
|
+
agents self-correct before committing.
|
|
44
|
+
- **spec**: fix the `FlowSchema` JSDoc example that taught the bad
|
|
45
|
+
`condition: "{amount} < 500"` single-brace form.
|
|
46
|
+
|
|
47
|
+
### Patch Changes
|
|
48
|
+
|
|
49
|
+
- Updated dependencies [955d4c8]
|
|
50
|
+
- Updated dependencies [c4a4cbd]
|
|
51
|
+
- Updated dependencies [b046ec2]
|
|
52
|
+
- Updated dependencies [2170ad9]
|
|
53
|
+
- Updated dependencies [02d6359]
|
|
54
|
+
- Updated dependencies [7648242]
|
|
55
|
+
- Updated dependencies [8fa1e7f]
|
|
56
|
+
- Updated dependencies [55866f5]
|
|
57
|
+
- Updated dependencies [60f9c45]
|
|
58
|
+
- @objectstack/spec@7.6.0
|
|
59
|
+
|
|
3
60
|
## 7.5.0
|
|
4
61
|
|
|
5
62
|
### 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
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
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
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
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
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
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
|
-
|
|
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 };
|