@coldsmirk/abacus-core 0.4.1 → 0.5.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/README.md +19 -10
- package/dist/index.cjs +354 -46
- package/dist/index.d.cts +92 -40
- package/dist/index.d.ts +92 -40
- package/dist/index.js +350 -47
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Part of [abacus](https://github.com/coldsmirk/abacus). To edit expressions in an
|
|
|
18
18
|
pnpm add @coldsmirk/abacus-core
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The ZEN WebAssembly engine (`@gorules/zen-engine-wasm`) is a regular dependency and installs automatically. Node.js >=
|
|
21
|
+
The ZEN WebAssembly engine (`@gorules/zen-engine-wasm`) is a regular dependency and installs automatically. Node.js >= 24; browsers need ES2023 support (Chrome/Edge 111+, Safari 16.2+, Firefox 115+) — the bundles ship untranspiled.
|
|
22
22
|
|
|
23
23
|
## Quick start
|
|
24
24
|
|
|
@@ -131,7 +131,7 @@ configureEngine({ wasmInput: await fetch("https://cdn.example.com/zen.wasm").the
|
|
|
131
131
|
await loadEngine();
|
|
132
132
|
```
|
|
133
133
|
|
|
134
|
-
`configureEngine()` throws if called after the engine has started loading. `resetEngine()` drops the loaded engine, the cached type context, the latched error, and the configured input —
|
|
134
|
+
`configureEngine()` throws if called after the engine has started loading. `resetEngine()` drops the loaded engine, the cached type context, the latched error, and the configured input, and orphans any in-flight load — its later settlement is discarded, and the next `loadEngine()` waits for it to settle before touching the wasm initializer (which tolerates no concurrent calls). Mainly for tests, and the retry primitive for a **failed** load (reset, re-`configureEngine()`, load again). Once the wasm module has initialized successfully it stays instantiated for the lifetime of the JS realm, so a *different* `wasmInput` cannot take effect after a successful load.
|
|
135
135
|
|
|
136
136
|
## Type analysis & intelligence
|
|
137
137
|
|
|
@@ -193,7 +193,7 @@ interface ExpressionTypeSpan {
|
|
|
193
193
|
error: string | null; // a type error attached to this span, if any
|
|
194
194
|
kind: ExpressionType; // the inferred type of this span
|
|
195
195
|
nodeKind: string; // the syntax-node kind
|
|
196
|
-
span: [number, number]; // [from, to]
|
|
196
|
+
span: [number, number]; // [from, to] UTF-16 code-unit offsets
|
|
197
197
|
}
|
|
198
198
|
interface ExpressionDiagnostic {
|
|
199
199
|
from: number; // start offset
|
|
@@ -296,17 +296,20 @@ type BranchSelection =
|
|
|
296
296
|
|
|
297
297
|
### Condition trees
|
|
298
298
|
|
|
299
|
-
The flat model above is one AND-group per branch. For a structured builder UI —
|
|
299
|
+
The flat model above is one AND-group per branch. For a structured builder UI — bounded AND/OR groups of typed comparison rules — the package also models a **condition tree**, compiles it to a single canonical expression, and lifts non-empty canonical expressions back to trees:
|
|
300
300
|
|
|
301
301
|
```ts
|
|
302
302
|
interface ConditionTreeRule { // a leaf comparison
|
|
303
303
|
kind: "rule";
|
|
304
|
+
id?: string; // stable UI identity (React keys); ignored by the compiler
|
|
304
305
|
left: string; // an identifier path, like `subject`
|
|
305
306
|
operator: ConditionTreeOperator; // the compiler's full 14-operator set
|
|
306
307
|
right?: ConditionTreeValue; // per the operator's arity (see below)
|
|
308
|
+
rightElementType?: "string" | "number"; // UI hint: element type intended for an EMPTY membership array
|
|
307
309
|
}
|
|
308
|
-
interface ConditionTreeGroup { // an and/or group
|
|
310
|
+
interface ConditionTreeGroup { // an and/or group, at most 64 nested group edges below the root
|
|
309
311
|
kind: "group";
|
|
312
|
+
id?: string;
|
|
310
313
|
op: "and" | "or";
|
|
311
314
|
items: readonly ConditionTreeNode[];
|
|
312
315
|
}
|
|
@@ -315,6 +318,10 @@ type ConditionTreeNode = ConditionTreeGroup | ConditionTreeRule;
|
|
|
315
318
|
function compileConditionTree(tree: ConditionTreeGroup): string;
|
|
316
319
|
function liftConditionTree(expression: string): ConditionTreeGroup | null;
|
|
317
320
|
function conditionOperatorArity(operator: ConditionOperator): "scalar" | "array" | "none";
|
|
321
|
+
function emptyConditionGroup(): ConditionTreeGroup; // a fresh "no condition yet" and-group
|
|
322
|
+
function newConditionNodeId(): string; // a fresh id for hand-built UI nodes
|
|
323
|
+
function ensureConditionNodeIds(tree: ConditionTreeGroup): ConditionTreeGroup;
|
|
324
|
+
const MAX_CONDITION_TREE_DEPTH = 64;
|
|
318
325
|
```
|
|
319
326
|
|
|
320
327
|
```ts
|
|
@@ -339,14 +346,16 @@ A rule's `right` follows its operator's **arity** — a single scalar (`string |
|
|
|
339
346
|
|
|
340
347
|
The emitted expression is **canonical**: nested groups are always parenthesized (never relying on `and`/`or` precedence), single-item groups collapse to their item, and a rule the tree cannot represent canonically — a non-path `left`, an off-arity `right`, a value with no ZEN literal — is dropped like an invalid flat condition. A tree with nothing compilable yields `""`.
|
|
341
348
|
|
|
342
|
-
`liftConditionTree` recognizes exactly that canonical subset (whitespace-tolerantly)
|
|
349
|
+
`liftConditionTree` recognizes exactly that non-empty canonical subset (whitespace-tolerantly): every non-empty expression `compileConditionTree` emits lifts back to the same **compiled** structure. This is not an identity round-trip for an arbitrary input tree. Compilation drops incomplete or unrepresentable rules, collapses single-item groups, and returns `""` when nothing is compilable (`liftConditionTree("")` returns `null`). The UI-only fields never reach the expression either, so a compile → lift trip regenerates `id`s and **loses `rightElementType`**: an empty membership's Number intent survives while you store the tree, but not through the expression (`a in []` lifts back with the default Text intent). Persist the tree itself when incomplete editing state, exact group shape, or UI intent must survive. Anything outside the canonical expression subset — a raw hand-authored expression, mixed `and`/`or` without parentheses, groups nested beyond 64 levels, number literals the compiler could never emit (`9007199254740993` would silently round through float64, `1e999` collapses to Infinity, `-0` stringifies to `0`, `-9223372036854776000` has no faithful literal even though its positive twin does) — makes the lifter return `null` rather than guessing at structure or rewriting a value. That `null` is a host's signal to fall back to raw-expression editing instead of a structured builder; [`@coldsmirk/abacus-mantine`](https://www.npmjs.com/package/@coldsmirk/abacus-mantine)'s `<ConditionBuilder>` is built on exactly this contract.
|
|
350
|
+
|
|
351
|
+
All tree consumers share `MAX_CONDITION_TREE_DEPTH`: the root is depth 0 and up to 64 nested group edges are accepted. `compileConditionTree` and `ensureConditionNodeIds` throw `ExpressionError` for a deeper tree or an ancestor cycle, before entering their bounded recursive work; `liftConditionTree` returns `null` when expression nesting exceeds the same budget. The builder disables subgroup creation at the limit, so it cannot produce a tree the compiler would reject.
|
|
343
352
|
|
|
344
353
|
### Safety: subjects, values, and escaping
|
|
345
354
|
|
|
346
355
|
The compiler is designed not to become an expression-injection sink:
|
|
347
356
|
|
|
348
|
-
- A **subject** is emitted verbatim, so it must be a plain identifier path (`amount`, `user.age`, `items[0]`) whose segments are not ZEN reserved words (`and`, `or`, `not`, `in`, `true`, `false`, `null`). Anything else (`len(secret)`, `a or b`, a bare `true`, …) makes the condition compile to `null` and be dropped.
|
|
349
|
-
- A **value** is serialized to a ZEN literal via `toZenLiteral`, never interpolated as code. Strings are quoted _raw_ (ZEN honours no backslash escapes) by picking a delimiter the value does not contain; a value that cannot be represented
|
|
357
|
+
- A **subject** is emitted verbatim, so it must be a plain identifier path (`amount`, `user.age`, `items[0]`) whose segments are not ZEN reserved words (`and`, `or`, `not`, `in`, `true`, `false`, `null`). Every array index token must also be inside ZEN's unsigned 96-bit decimal domain (`0` through `2^96 - 1`); the next value is a parser error, so the compiler rejects it instead of poisoning a larger expression. Anything else (`len(secret)`, `a or b`, a bare `true`, …) makes the condition compile to `null` and be dropped.
|
|
358
|
+
- A **value** is serialized to a ZEN literal via `toZenLiteral`, never interpolated as code. Strings are quoted _raw_ (ZEN honours no backslash escapes) by picking a delimiter the value does not contain; a value that cannot be represented faithfully makes the condition degrade to `null`. For numbers that means passing `isZenRepresentableNumber`: inside ZEN's 96-bit decimal domain (finite, magnitude below ~7.9e28, no finer than 1e-28) **and** held identically by the engine's own context conversion — the engine converts context JS numbers lossily (roughly: integers whose text fits u64 are exact; other f64s are trimmed to ~16 significant digits), so a value like `0.1 + 0.2` or `1.2345678901234567` would compile into a literal that silently compares **unequal to itself** in a context. Such values are rejected instead.
|
|
350
359
|
|
|
351
360
|
```ts
|
|
352
361
|
function toZenLiteral(value: unknown): string; // throws if the value has no faithful ZEN representation
|
|
@@ -415,8 +424,8 @@ class ExpressionNotReadyError extends ExpressionError { // a *Sync helper calle
|
|
|
415
424
|
- **Engine lifecycle** — `loadEngine`, `isEngineReady`, `getEngineSync`, `getEngineError`, `configureEngine`, `resetEngine`
|
|
416
425
|
- **Type analysis** — `analyzeTypes` / `analyzeTypesSync`, `getDiagnostics` / `getDiagnosticsSync`, `getCompletionItems` / `getCompletionItemsSync`, `satisfiesType` / `satisfiesTypeSync`
|
|
417
426
|
- **Templates** — `analyzeTemplate` / `analyzeTemplateSync`, `getTemplateDiagnostics` / `getTemplateDiagnosticsSync`, `parseTemplateHoles`, `templateHoleAt`
|
|
418
|
-
- **Conditions** — `compileCondition`, `compileGroup`, `compileBranch`, `selectBranch`, `selectBranchWith`, `toZenLiteral`, `CONDITION_OPERATORS`, `conditionOperatorArity`
|
|
419
|
-
- **Condition trees** — `compileConditionTree`, `liftConditionTree`, `CONDITION_TREE_OPERATORS`
|
|
427
|
+
- **Conditions** — `compileCondition`, `compileGroup`, `compileBranch`, `selectBranch`, `selectBranchWith`, `toZenLiteral`, `isZenRepresentableNumber`, `CONDITION_OPERATORS`, `conditionOperatorArity`
|
|
428
|
+
- **Condition trees** — `compileConditionTree`, `liftConditionTree`, `emptyConditionGroup`, `newConditionNodeId`, `ensureConditionNodeIds`, `MAX_CONDITION_TREE_DEPTH`, `CONDITION_TREE_OPERATORS`
|
|
420
429
|
- **Localization** — `configureExpressionMessages`, `registerExpressionLocale`, `getExpressionMessages`, `enMessages`, `zhCNMessages`
|
|
421
430
|
- **Errors** — `ExpressionError`, `ExpressionNotReadyError`
|
|
422
431
|
- **Types** — `ExpressionType`, `ExpressionMode`, `TemplateHole`, `ExpressionContext`, `ExpressionEngine`, `LoadEngineOptions`, `ExpressionAnalysis`, `ExpressionTypeSpan`, `ExpressionCompletion`, `ExpressionDiagnostic`, `ExpressionMessages`, `ExpressionLocale`, `BuiltInExpressionLocale`, `ConfigureMessagesOptions`, `ConditionInput`, `FieldConditionInput`, `ExpressionConditionInput`, `ConditionGroupInput`, `ConditionBranchInput`, `ConditionOperator`, `ConditionOperatorArity`, `BranchSelection`, `ConditionTreeGroup`, `ConditionTreeNode`, `ConditionTreeRule`, `ConditionTreeOperator`, `ConditionTreeValue`, `ConditionScalar`
|