@autochitect/engine 1.1.2 → 1.1.3

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
@@ -39,6 +39,17 @@ console.log(result.results);
39
39
 
40
40
  **`result.errors`** / **`result.warnings`** — with line and column numbers.
41
41
 
42
+ ## Why use this
43
+
44
+ | Instead of... | You get... |
45
+ |---|---|
46
+ | Building a calculation backend | 245KB client-side engine, zero infrastructure |
47
+ | Exporting Excel/Google Sheets | Version-controlled DSL, real-time interactivity |
48
+ | Spreadsheet-in-browser libraries | Just the engine — no grid UI overhead |
49
+ | Python/R models behind an API | Embeddable in any web app, no server round-trips |
50
+
51
+ The engine compiles your model into a directed acyclic graph, topologically sorts it, and estimates in one pass. Switching scenarios just swaps the input context — sub-millisecond.
52
+
42
53
  ## The DSL
43
54
 
44
55
  The language is deliberately small. It's designed for cost estimation and financial modeling specifically.
@@ -72,7 +83,90 @@ cumulative = SCAN(monthly, 0, LAMBDA(acc, m, acc + m))
72
83
 
73
84
  **MAP** transforms arrays element-wise. **SCAN** accumulates (like reduce, but returns intermediate results). **LAMBDA** defines inline functions with named parameters.
74
85
 
75
- The engine compiles this into a directed acyclic graph, topologically sorts it, and estimates in one pass. The graph is immutable — switching scenarios just swaps the input context, so it's instant.
86
+ ## Real-world examples
87
+
88
+ ### SaaS pricing calculator
89
+
90
+ ```javascript
91
+ const result = engine.estimate(`
92
+ seats: Input
93
+ base_price_per_seat = 12
94
+ storage_gb: Input
95
+ storage_price_per_gb = 0.50
96
+
97
+ seat_cost = seats * base_price_per_seat
98
+ storage_cost = storage_gb * storage_price_per_gb
99
+ subtotal = seat_cost + storage_cost
100
+
101
+ # Volume discount
102
+ discount_rate = IF(seats > 50, 0.20, IF(seats > 20, 0.10, 0))
103
+ discount = subtotal * discount_rate
104
+ monthly_total = subtotal - discount
105
+ annual_total = monthly_total * 12
106
+ `, {
107
+ seats: 35,
108
+ storage_gb: 200,
109
+ });
110
+
111
+ console.log(result.results.monthly_total); // 455
112
+ console.log(result.results.discount_rate); // 0.1 (10% for 35 seats)
113
+ ```
114
+
115
+ ### Construction cost estimation
116
+
117
+ ```javascript
118
+ const result = engine.estimate(`
119
+ square_footage: Input
120
+ base_cost_per_sqft: Input
121
+ num_floors: Input
122
+
123
+ material_cost = square_footage * base_cost_per_sqft
124
+ labor_cost = material_cost * 0.60
125
+ floor_premium = (material_cost + labor_cost) * ((num_floors - 1) * 0.08)
126
+
127
+ permits = (material_cost + labor_cost) * 0.03
128
+ insurance = (material_cost + labor_cost) * 0.02
129
+
130
+ total = material_cost + labor_cost + floor_premium + permits + insurance
131
+ cost_per_sqft = total / square_footage
132
+ `, {
133
+ square_footage: 5000,
134
+ base_cost_per_sqft: 150,
135
+ num_floors: 3,
136
+ });
137
+
138
+ console.log(result.results.total); // 1,452,000
139
+ console.log(result.results.cost_per_sqft); // 290.40
140
+ ```
141
+
142
+ ### Loan amortization projection
143
+
144
+ ```javascript
145
+ const result = engine.estimate(`
146
+ principal: Input
147
+ annual_rate: Input
148
+ months: Input
149
+
150
+ monthly_rate = annual_rate / 12
151
+ payment = principal * (monthly_rate * POWER(1 + monthly_rate, months))
152
+ / (POWER(1 + monthly_rate, months) - 1)
153
+
154
+ periods = SEQUENCE(months, 1, 1)
155
+ balances = SCAN(periods, principal, LAMBDA(bal, p,
156
+ bal * (1 + monthly_rate) - payment
157
+ ))
158
+
159
+ total_paid = payment * months
160
+ total_interest = total_paid - principal
161
+ `, {
162
+ principal: 400000,
163
+ annual_rate: 0.065,
164
+ months: 360,
165
+ });
166
+
167
+ console.log(result.results.payment); // ~2,528.27/month
168
+ console.log(result.results.total_interest); // ~510,177
169
+ ```
76
170
 
77
171
  ## Node.js usage
78
172
 
@@ -147,6 +241,17 @@ const engine = await createEngine(
147
241
  );
148
242
  ```
149
243
 
244
+ ## Framework examples
245
+
246
+ Working integration examples are available in the [`examples/`](https://github.com/autochitect/engine/tree/main/examples) directory:
247
+
248
+ | Example | Description |
249
+ |---------|-------------|
250
+ | [Next.js pricing calculator](https://github.com/autochitect/engine/tree/main/examples/nextjs-pricing-calculator) | SaaS pricing page with real-time sliders and volume discounts |
251
+ | [Vue.js cost estimator](https://github.com/autochitect/engine/tree/main/examples/vue-cost-estimator) | Construction cost estimator with dependency graph visualization |
252
+ | [Strapi pricing page](https://github.com/autochitect/engine/tree/main/examples/strapi-pricing-page) | CMS-driven pricing — content in Strapi, calculations in engine |
253
+ | [Vanilla JS](https://github.com/autochitect/engine/tree/main/examples/vanilla-js) | Single HTML file, no build tools, plain DOM |
254
+
150
255
  ## Options
151
256
 
152
257
  ```javascript
@@ -0,0 +1,125 @@
1
+ const WASI_SHIM = {
2
+ fd_write: (fd, iovs_ptr, iovs_len, nwritten_ptr, mem) => {
3
+ const view = new DataView(mem.buffer);
4
+ let written = 0;
5
+ for (let i = 0; i < iovs_len; i++) {
6
+ written += view.getUint32(iovs_ptr + i * 8 + 4, true);
7
+ }
8
+ view.setUint32(nwritten_ptr, written, true);
9
+ return 0;
10
+ },
11
+ fd_close: () => 0,
12
+ fd_seek: (fd, lo, hi, whence, ptr, mem) => {
13
+ new DataView(mem.buffer).setBigUint64(ptr, 0n, true);
14
+ return 0;
15
+ },
16
+ fd_fdstat_get: (fd, buf, mem) => {
17
+ for (let i = 0; i < 24; i++) new DataView(mem.buffer).setUint8(buf + i, 0);
18
+ return 0;
19
+ },
20
+ environ_sizes_get: (cp, bp, mem) => {
21
+ const v = new DataView(mem.buffer);
22
+ v.setUint32(cp, 0, true);
23
+ v.setUint32(bp, 0, true);
24
+ return 0;
25
+ },
26
+ args_sizes_get: (ap, bp, mem) => {
27
+ const v = new DataView(mem.buffer);
28
+ v.setUint32(ap, 0, true);
29
+ v.setUint32(bp, 0, true);
30
+ return 0;
31
+ },
32
+ clock_time_get: (id, prec, ptr, mem) => {
33
+ new DataView(mem.buffer).setBigUint64(ptr, BigInt(Date.now()) * 1000000n, true);
34
+ return 0;
35
+ },
36
+ proc_exit: (code) => { throw new Error(`proc_exit(${code})`); },
37
+ };
38
+
39
+ const STUB_ERRNO = {
40
+ fd_read: 0, fd_sync: 0, fd_pread: 0, fd_pwrite: 0, fd_readdir: 0,
41
+ fd_prestat_get: 8, fd_prestat_dir_name: 28,
42
+ fd_filestat_get: 0, fd_filestat_set_size: 0, fd_filestat_set_times: 0,
43
+ environ_get: 0, args_get: 0,
44
+ path_open: 44, path_create_directory: 44, path_filestat_get: 44,
45
+ path_filestat_set_times: 44, path_link: 44, path_readlink: 44,
46
+ path_remove_directory: 44, path_rename: 44, path_symlink: 44,
47
+ path_unlink_file: 44, sched_yield: 0, poll_oneoff: 0,
48
+ sock_accept: 58, sock_recv: 58, sock_send: 58, sock_shutdown: 58,
49
+ };
50
+
51
+ function buildWasiImports(memoryRef) {
52
+ const imports = {};
53
+ for (const [name, fn] of Object.entries(WASI_SHIM)) {
54
+ if (typeof fn === 'function') {
55
+ if (name === 'proc_exit') {
56
+ imports[name] = fn;
57
+ } else {
58
+ imports[name] = (...args) => fn(...args, memoryRef.current);
59
+ }
60
+ }
61
+ }
62
+ for (const [name, errno] of Object.entries(STUB_ERRNO)) {
63
+ imports[name] = () => errno;
64
+ }
65
+ imports.clock_res_get = (id, ptr) => {
66
+ new DataView(memoryRef.current.buffer).setBigUint64(ptr, 1000000n, true);
67
+ return 0;
68
+ };
69
+ imports.random_get = (buf, len) => {
70
+ const view = new Uint8Array(memoryRef.current.buffer, buf, len);
71
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
72
+ crypto.getRandomValues(view);
73
+ } else {
74
+ for (let i = 0; i < len; i++) view[i] = Math.floor(Math.random() * 256);
75
+ }
76
+ return 0;
77
+ };
78
+ return imports;
79
+ }
80
+
81
+ export async function createEngine(wasmSource) {
82
+ const memoryRef = { current: null };
83
+ const imports = { wasi_snapshot_preview1: buildWasiImports(memoryRef) };
84
+
85
+ let bytes;
86
+ if (wasmSource instanceof ArrayBuffer || ArrayBuffer.isView(wasmSource)) {
87
+ bytes = wasmSource;
88
+ } else if (wasmSource instanceof Response || (typeof wasmSource === 'object' && typeof wasmSource.then === 'function')) {
89
+ const response = await wasmSource;
90
+ bytes = await response.arrayBuffer();
91
+ } else if (wasmSource instanceof URL) {
92
+ bytes = await (await fetch(wasmSource)).arrayBuffer();
93
+ } else if (typeof wasmSource === 'string') {
94
+ bytes = await (await fetch(wasmSource)).arrayBuffer();
95
+ } else {
96
+ throw new Error('wasmSource must be a URL, URL string, Response, or ArrayBuffer');
97
+ }
98
+
99
+ const { instance } = await WebAssembly.instantiate(bytes, imports);
100
+ memoryRef.current = instance.exports.memory;
101
+
102
+ function writeString(str) {
103
+ const enc = new TextEncoder().encode(str);
104
+ const ptr = instance.exports.wasm_alloc(enc.length);
105
+ if (ptr === 0) throw new Error('WASM allocation failed');
106
+ new Uint8Array(memoryRef.current.buffer, ptr, enc.length).set(enc);
107
+ return { ptr, len: enc.length };
108
+ }
109
+
110
+ return {
111
+ estimate(source, inputs = {}, options = {}) {
112
+ const includeGraph = options.graph !== false;
113
+ const src = writeString(source);
114
+ const json = writeString(JSON.stringify(inputs));
115
+ const resultLen = instance.exports.wasm_estimate(
116
+ src.ptr, src.len,
117
+ json.ptr, json.len,
118
+ includeGraph ? 1 : 0
119
+ );
120
+ const resultPtr = instance.exports.wasm_result_ptr();
121
+ const resultBytes = new Uint8Array(memoryRef.current.buffer, resultPtr, resultLen);
122
+ return JSON.parse(new TextDecoder().decode(resultBytes));
123
+ },
124
+ };
125
+ }
package/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export interface EstimateOptions {
21
21
  }
22
22
 
23
23
  export interface Engine {
24
- estimate(source: string, inputs?: Record<string, unknown>, options?: EstimateOptions): EstimateResult;
24
+ estimate(source: string, inputs?: Record<string, unknown> | object, options?: EstimateOptions): EstimateResult;
25
25
  }
26
26
 
27
- export function createEngine(wasmSource: string | Response | ArrayBuffer): Promise<Engine>;
27
+ export function createEngine(wasmSource: string | URL | Response | ArrayBuffer): Promise<Engine>;
package/package.json CHANGED
@@ -1,13 +1,23 @@
1
1
  {
2
2
  "name": "@autochitect/engine",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "A 245KB WebAssembly financial modeling engine. Define cost models in a purpose-built DSL, get instant estimates with full dependency tracing. No server required.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "index.mjs",
8
8
  "types": "index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./index.d.ts",
12
+ "browser": "./index.browser.mjs",
13
+ "import": "./index.mjs",
14
+ "default": "./index.mjs"
15
+ },
16
+ "./engine.wasm": "./engine.wasm"
17
+ },
9
18
  "files": [
10
19
  "index.mjs",
20
+ "index.browser.mjs",
11
21
  "index.d.ts",
12
22
  "engine.wasm",
13
23
  "LICENSE",
@@ -26,7 +36,7 @@
26
36
  ],
27
37
  "repository": {
28
38
  "type": "git",
29
- "url": "git@github.com:autochitect/engine.git"
39
+ "url": "git+ssh://git@github.com/autochitect/engine.git"
30
40
  },
31
41
  "homepage": "https://autochitect.com/engine"
32
42
  }