@lemmabase/lemma-engine 0.8.11 → 0.8.13

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 CHANGED
@@ -1,8 +1,50 @@
1
1
  # @lemmabase/lemma-engine
2
2
 
3
- Embeddable Lemma engine (WebAssembly).
3
+ > [Lemma](https://github.com/lemma/lemma) is a declarative language for business rules. **This package is the engine, compiled to WebAssembly** - runs in the browser, on Node, Bun, Deno, Cloudflare Workers, Vercel Edge, etc.
4
4
 
5
- npm `description` / `keywords` / `homepage`: edit **`NPM_BRANDING`** in `build.js` (not Cargo).
5
+ Pricing tiers, tax brackets, leave entitlement, eligibility checks, discount stacks: the rules that change, that auditors ask about, that legal writes in PDFs and engineers re-implement in operational code... Lemma is a language built specifically for your business rules. It is readable by stakeholders, executable anywhere, and impossible to drift out of sync.
6
+
7
+ ```lemma
8
+ spec pricing 2026-01-01
9
+
10
+ data money: scale
11
+ -> unit eur 1.00
12
+ -> decimals 2
13
+
14
+ data quantity : number
15
+ data is_vip : false
16
+
17
+ rule unit_price:
18
+ 20 eur
19
+ unless quantity >= 10 then 18 eur
20
+ unless quantity >= 50 then 16 eur
21
+ unless is_vip then 15 eur
22
+
23
+ rule total:
24
+ unit_price * quantity
25
+ ```
26
+
27
+ ```javascript
28
+ import { Lemma } from '@lemmabase/lemma-engine';
29
+
30
+ const engine = await Lemma();
31
+ await engine.load(pricing, 'pricing.lemma');
32
+
33
+ const response = engine.run(null, 'pricing', [], { quantity: 50, is_vip: false }, null);
34
+ // response.results.unit_price → 16 eur
35
+ // response.results.total → 800 eur
36
+ ```
37
+
38
+ The `Response` carries every rule's value (or `veto` if no result could be computed), the input snapshot, and the source location of every rule that fired, allowing you to render an audit trail in your UI.
39
+
40
+ ## Why use it from JavaScript?
41
+
42
+ - **Deterministic.** `(spec, data, effective_date) → result`. No DB, no clock, no ambient state. Same inputs → same outputs, every time.
43
+ - **Explainable.** The `Response` tells you which rules contributed and why; pair it with the [CLI](https://github.com/lemma/lemma) for a full reasoning trace.
44
+ - **Time-aware.** Multiple versions of the same spec coexist. Pass an `effective` date and the engine resolves the version in force on that day.
45
+ - **Statically checked.** Type errors, missing data, cycles, scale-family mismatches - all caught at `load()` time. Bad specs never reach `run()`.
46
+ - **Runs anywhere V8 does.** ~2 MB WASM, no native binary, no postinstall script.
47
+ - **Editor in a tab.** Includes an in-process language server and a Monaco adapter, so you can build a real Lemma editor experience client-side - diagnostics, completion, formatting... even without setting up a server.
6
48
 
7
49
  ## Install
8
50
 
@@ -10,7 +52,7 @@ npm `description` / `keywords` / `homepage`: edit **`NPM_BRANDING`** in `build.j
10
52
  npm install @lemmabase/lemma-engine
11
53
  ```
12
54
 
13
- ## Browser / bundler
55
+ ## Browser
14
56
 
15
57
  ```javascript
16
58
  import { Lemma } from '@lemmabase/lemma-engine';
@@ -18,39 +60,35 @@ import { Lemma } from '@lemmabase/lemma-engine';
18
60
  const engine = await Lemma();
19
61
  ```
20
62
 
21
- `Lemma()` initializes WASM once and returns an `Engine`. Serve over **http(s)** (not `file://`). For manual init: `init()` then `new Engine()`.
63
+ `Lemma()` initializes the WASM module once and returns an `Engine`. Serve over **http(s)**, not `file://`. For manual control: `init()` then `new Engine()`.
22
64
 
23
- If your bundler outputs IIFE (or otherwise breaks `import.meta.url`), use:
65
+ If your bundler emits IIFE, can't resolve `import.meta.url`, or refuses to ship `lemma_bg.wasm` as a separate asset, use the inlined entry - it embeds the wasm bytes in the JS bundle:
24
66
 
25
67
  ```javascript
26
68
  import { Lemma } from '@lemmabase/lemma-engine/iife';
27
-
28
- const engine = await Lemma();
29
69
  ```
30
70
 
31
- This entry embeds WASM bytes and avoids external wasm URL handling.
32
-
33
- ## esbuild auto-handler
34
-
35
- If you use esbuild JS API, plugin rewrites root import to `/iife` automatically:
71
+ esbuild users get an auto-rewriting plugin:
36
72
 
37
73
  ```javascript
38
74
  import { lemmaEngineEsbuildPlugin } from '@lemmabase/lemma-engine/esbuild';
75
+
76
+ esbuild.build({ /* ... */ plugins: [lemmaEngineEsbuildPlugin()] });
39
77
  ```
40
78
 
41
79
  ## Node
42
80
 
81
+ Identical to the browser path:
82
+
43
83
  ```javascript
44
84
  import { Lemma } from '@lemmabase/lemma-engine';
45
85
 
46
86
  const engine = await Lemma();
47
87
  ```
48
88
 
49
- Or with a preloaded buffer (e.g. no async fetch): `initSync({ module })` then `new Engine()`.
89
+ For zero-fetch startup with a preloaded module: `initSync({ module })` then `new Engine()`.
50
90
 
51
- ## LSP (browser streams)
52
-
53
- Call `init()` first. Use `LspClient`; `start()` uses the bundled LSP (no need to pass `serve`/`ServerConfig`). Optional: `start(serve, ServerConfig)` to override.
91
+ ## In-process LSP + Monaco
54
92
 
55
93
  ```javascript
56
94
  import { init } from '@lemmabase/lemma-engine';
@@ -60,23 +98,55 @@ await init();
60
98
  const client = new LspClient(monaco);
61
99
  await client.start();
62
100
  await client.initialize();
63
- client.onDiagnostics((uri, diagnostics) => { /* ... */ });
64
- client.didOpen(uri, 'lemma', 1, documentText);
101
+
102
+ client.onDiagnostics((uri, diagnostics) => { /* render */ });
103
+ client.didOpen('file:///pricing.lemma', 'lemma', 1, source);
65
104
  ```
66
105
 
67
- ## API (`Engine`)
106
+ A pre-wired Monaco adapter ships at `@lemmabase/lemma-engine/monaco`.
107
+
108
+ ## API
109
+
110
+ `Engine` (returned by `Lemma()` or `new Engine()`):
111
+
112
+ | Method | Description |
113
+ |--------|-------------|
114
+ | `load(code, attribute?)` | Parse and validate a `.lemma` spec set. Resolves on success; rejects with `EngineError[]`. |
115
+ | `load_batch(sources, dependency?)` | Load many sources in one planning pass (see `lemma.d.ts`). |
116
+ | `fetch(name)` | Download registry source only; resolves with `{ source, id }`. Does not load. Rejects with `EngineError[]`. |
117
+ | `list()` | JSON array of `ResolvedRepository`: each has `repository` and `specs` (spec sets). Each set has `name`, `repository`, and `specs` — temporal versions as full `LemmaSpec` objects (`effective_from`, `start_line`, `source_type`, …). |
118
+ | `schema(repo, name, effective?)` | `SpecSchema`; `repo` null for workspace. |
119
+ | `run(repo, name, ruleNames, data, effective?)` | Evaluate. `rules: []` runs everything; pass an array to filter. Returns a `Response`. |
120
+ | `format(code, attribute?)` | Canonical formatting; throws `EngineError` on parse error. |
121
+
122
+ Full TypeScript types are bundled - see `lemma.d.ts`.
123
+
124
+ ### Registry dependencies
125
+
126
+ Specs that reference `uses … @org/pkg` need that package available. `fetch` only downloads; call `load_batch` to load the dependency, then load your workspace:
127
+
128
+ ```javascript
129
+ import { Lemma } from '@lemmabase/lemma-engine';
130
+
131
+ const engine = await Lemma();
132
+ const { source, id } = await engine.fetch('@lemma/std');
133
+ await engine.load_batch({ '': source }, id);
134
+ await engine.load(sourceThatUsesStd, 'app.lemma');
135
+ ```
136
+
137
+ In the browser, the registry must allow your origin (CORS). Use `https` or `http://localhost` when using `fetch`.
138
+
139
+ ## Status
68
140
 
69
- | Method | |
70
- |--------|--|
71
- | `load(code, attribute)` | Promise; reject → `string[]` |
72
- | `list()` | Spec entries |
73
- | `schema(spec, effective?)` | `SpecSchema` |
74
- | `run(spec, rules, data, effective?)` | `Response` |
75
- | `format(code, attribute?)` | string or throw |
141
+ Lemma is in early development. Expect breaking changes between minor versions; **don't put it in front of paying customers yet**. Production-readiness tracking lives in the [main repo](https://github.com/lemma/lemma).
76
142
 
77
- ## Build (maintainers)
143
+ ## Related
78
144
 
79
- `node build.js`: wasm-pack `lemma.bindings.js` + `lemma_bg.wasm` copy checked-in `lemma-entry.js` / `lsp-entry.js` / `*.d.ts` into `dist/`. Do not edit generated bindings by hand.
145
+ - [`lemmabase.com`](https://lemmabase.com): public database for Lemma Specs
146
+ - [`lemma-cli`](https://crates.io/crates/lemma-cli): REPL, HTTP server, MCP server, formatter
147
+ - [`lemma-engine`](https://crates.io/crates/lemma-engine): same engine as a Rust crate
148
+ - [`lemma_engine` on Hex](https://hex.pm/packages/lemma_engine): Elixir bindings via Rustler
149
+ - VS Code / Cursor extension: search "Lemma Language" in the marketplace
80
150
 
81
151
  ## License
82
152
 
@@ -11,41 +11,54 @@ type ReadableStreamType = "bytes";
11
11
  export class Engine {
12
12
  free(): void;
13
13
  [Symbol.dispose](): void;
14
+ /**
15
+ * Download Lemma source for a registry identifier via [`crate::registry::LemmaBase`]. Returns `{ source, id }`.
16
+ * Does not load this [`WasmEngine`]; call [`Self::load_batch`], etc., yourself.
17
+ */
18
+ fetch(name: string): Promise<any>;
14
19
  /**
15
20
  * Returns formatted source string on success; throws with error message on failure.
16
21
  */
17
22
  format(code: string, attribute?: string | null): any;
18
23
  invert(_spec_name: string, _rule_name: string, _target_json: string, _provided_values_json: string): any;
19
24
  /**
20
- * Loaded specs, each paired with its planning schema.
21
- *
22
- * Each entry has `{ name, effective_from, effective_to, schema }`. The
23
- * pair describes a half-open `[effective_from, effective_to)` validity
24
- * range; `effective_from` is `null` when the first version has no
25
- * declared start, and `effective_to` is `null` for the latest version of
26
- * a name (no successor). Order matches [`Engine::list_specs_with_ranges`].
27
- *
28
- * `schema` is the same envelope returned by [`WasmEngine::schema`] for
29
- * `(name, effective_from)`; shipping it inline saves the N+1 round-trip
30
- * every consumer (playground, dashboards, docs) was doing.
25
+ * Same data as [`Engine::list`]: grouped [`ResolvedRepository`] JSON without planning.
31
26
  */
32
27
  list(): any;
33
28
  /**
34
- * Load Lemma source. Resolves with `undefined` on success; rejects with an array of
35
- * serialized errors (same shape as `EngineError` in `engine/packages/npm/lemma.d.ts`).
29
+ * Load Lemma source. Throws with an array of serialized errors
30
+ * (same shape as `EngineError` in `engine/packages/npm/lemma.d.ts`).
31
+ */
32
+ load(code: string, attribute: string): void;
33
+ /**
34
+ * Load multiple Lemma sources in one planning pass (same as [`Engine::load_batch`]).
36
35
  *
37
- * Breaking: previously rejected with an array of strings.
36
+ * `sources` is a plain object mapping path labels to source text. Labels become
37
+ * [`SourceType::Path`]; use `""` as a key for [`SourceType::Volatile`].
38
+ *
39
+ * `dependency`: when non-empty after trim, sources are tagged as that dependency id.
40
+ *
41
+ * Throws with an array of `JsError` on failure.
38
42
  */
39
- load(code: string, attribute: string): Promise<any>;
43
+ load_batch(sources: any, dependency?: string | null): void;
40
44
  constructor();
45
+ /**
46
+ * Loaded repositories (workspace and dependencies): `{ name, dependency }`.
47
+ */
48
+ repositories(): any;
41
49
  /**
42
50
  * Evaluate spec. Returns [`crate::evaluation::Response`] as a JS object. Throws on planning/runtime error.
51
+ *
52
+ * `repository`: repository qualifier (`@org/pkg`), or `null`/empty for workspace (same as
53
+ * [`Engine::run`] `repo: None`).
43
54
  */
44
- run(spec: string, rule_names: any, data_values: any, effective?: string | null): any;
55
+ run(repository: string | null | undefined, spec: string, rule_names: any, data_values: any, effective?: string | null): any;
45
56
  /**
46
57
  * Planning schema for the spec ([`crate::planning::execution_plan::SpecSchema`]). Throws on error.
58
+ *
59
+ * `repository`: qualifier string or `null`/empty for workspace ([`Engine::schema`]).
47
60
  */
48
- schema(spec: string, effective?: string | null): any;
61
+ schema(repository: string | null | undefined, spec: string, effective?: string | null): any;
49
62
  }
50
63
 
51
64
  export class IntoUnderlyingByteSource {
@@ -109,16 +122,19 @@ export interface InitOutput {
109
122
  readonly intounderlyingsource_cancel: (a: number) => void;
110
123
  readonly intounderlyingsource_pull: (a: number, b: number) => number;
111
124
  readonly __wbg_engine_free: (a: number, b: number) => void;
125
+ readonly wasmengine_fetch: (a: number, b: number, c: number) => number;
112
126
  readonly wasmengine_format: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
113
127
  readonly wasmengine_invert: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void;
114
128
  readonly wasmengine_list: (a: number, b: number) => void;
115
- readonly wasmengine_load: (a: number, b: number, c: number, d: number, e: number) => number;
129
+ readonly wasmengine_load: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
130
+ readonly wasmengine_load_batch: (a: number, b: number, c: number, d: number, e: number) => void;
116
131
  readonly wasmengine_new: () => number;
117
- readonly wasmengine_run: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
118
- readonly wasmengine_schema: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
119
- readonly __wasm_bindgen_func_elem_10702: (a: number, b: number, c: number, d: number) => void;
120
- readonly __wasm_bindgen_func_elem_2761: (a: number, b: number, c: number, d: number) => void;
121
- readonly __wasm_bindgen_func_elem_10709: (a: number, b: number, c: number, d: number) => void;
132
+ readonly wasmengine_repositories: (a: number, b: number) => void;
133
+ readonly wasmengine_run: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void;
134
+ readonly wasmengine_schema: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
135
+ readonly __wasm_bindgen_func_elem_10830: (a: number, b: number, c: number, d: number) => void;
136
+ readonly __wasm_bindgen_func_elem_2759: (a: number, b: number, c: number, d: number) => void;
137
+ readonly __wasm_bindgen_func_elem_10848: (a: number, b: number, c: number, d: number) => void;
122
138
  readonly __wbindgen_export: (a: number, b: number) => number;
123
139
  readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
124
140
  readonly __wbindgen_export3: (a: number) => void;