@autochitect/engine 1.1.1 → 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 +106 -1
- package/engine.wasm +0 -0
- package/index.browser.mjs +125 -0
- package/index.d.ts +2 -2
- package/package.json +12 -2
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
|
-
|
|
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
|
package/engine.wasm
CHANGED
|
Binary file
|
|
@@ -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
|
|
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.
|
|
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
|
|
39
|
+
"url": "git+ssh://git@github.com/autochitect/engine.git"
|
|
30
40
|
},
|
|
31
41
|
"homepage": "https://autochitect.com/engine"
|
|
32
42
|
}
|