@autochitect/engine 1.1.2 → 1.1.4
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 +72 -88
- package/index.browser.mjs +125 -0
- package/index.d.ts +2 -2
- package/index.mjs +7 -0
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -8,27 +8,88 @@ A 245KB WebAssembly cost estimation engine. Define cost models in a purpose-buil
|
|
|
8
8
|
npm install @autochitect/engine
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Node.js
|
|
12
12
|
|
|
13
13
|
```javascript
|
|
14
14
|
import { createEngine } from '@autochitect/engine';
|
|
15
15
|
|
|
16
|
-
const engine = await createEngine(
|
|
17
|
-
new URL('@autochitect/engine/engine.wasm', import.meta.url)
|
|
18
|
-
);
|
|
16
|
+
const engine = await createEngine();
|
|
19
17
|
|
|
20
18
|
const result = engine.estimate(`
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
seats: Input
|
|
20
|
+
base_price_per_seat = 12
|
|
21
|
+
storage_gb: Input
|
|
22
|
+
storage_price_per_gb = 0.50
|
|
23
|
+
|
|
24
|
+
seat_cost = seats * base_price_per_seat
|
|
25
|
+
storage_cost = storage_gb * storage_price_per_gb
|
|
26
|
+
subtotal = seat_cost + storage_cost
|
|
27
|
+
|
|
28
|
+
discount_rate = IF(seats > 50, 0.20, IF(seats > 20, 0.10, 0))
|
|
29
|
+
discount = subtotal * discount_rate
|
|
30
|
+
monthly_total = subtotal - discount
|
|
31
|
+
annual_total = monthly_total * 12
|
|
25
32
|
`, {
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
seats: 35,
|
|
34
|
+
storage_gb: 200,
|
|
28
35
|
});
|
|
29
36
|
|
|
30
37
|
console.log(result.results);
|
|
31
|
-
// {
|
|
38
|
+
// {
|
|
39
|
+
// seats: 35, base_price_per_seat: 12, storage_gb: 200, storage_price_per_gb: 0.5,
|
|
40
|
+
// seat_cost: 420, storage_cost: 100, subtotal: 520,
|
|
41
|
+
// discount_rate: 0.1, discount: 52, monthly_total: 468, annual_total: 5616
|
|
42
|
+
// }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Browser
|
|
46
|
+
|
|
47
|
+
No install needed — paste this into an HTML file and open it.
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<script type="module">
|
|
51
|
+
import { createEngine } from 'https://esm.sh/@autochitect/engine';
|
|
52
|
+
|
|
53
|
+
const engine = await createEngine(
|
|
54
|
+
'https://unpkg.com/@autochitect/engine/engine.wasm'
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const result = engine.estimate(`
|
|
58
|
+
seats: Input
|
|
59
|
+
base_price_per_seat = 12
|
|
60
|
+
storage_gb: Input
|
|
61
|
+
storage_price_per_gb = 0.50
|
|
62
|
+
|
|
63
|
+
seat_cost = seats * base_price_per_seat
|
|
64
|
+
storage_cost = storage_gb * storage_price_per_gb
|
|
65
|
+
subtotal = seat_cost + storage_cost
|
|
66
|
+
|
|
67
|
+
discount_rate = IF(seats > 50, 0.20, IF(seats > 20, 0.10, 0))
|
|
68
|
+
discount = subtotal * discount_rate
|
|
69
|
+
monthly_total = subtotal - discount
|
|
70
|
+
annual_total = monthly_total * 12
|
|
71
|
+
`, {
|
|
72
|
+
seats: 35,
|
|
73
|
+
storage_gb: 200,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log(result.results);
|
|
77
|
+
// {
|
|
78
|
+
// seats: 35, base_price_per_seat: 12, storage_gb: 200, storage_price_per_gb: 0.5,
|
|
79
|
+
// seat_cost: 420, storage_cost: 100, subtotal: 520,
|
|
80
|
+
// discount_rate: 0.1, discount: 52, monthly_total: 468, annual_total: 5616
|
|
81
|
+
// }
|
|
82
|
+
</script>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If you use a bundler (Vite, webpack, etc.), import normally:
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
import { createEngine } from '@autochitect/engine';
|
|
89
|
+
|
|
90
|
+
const engine = await createEngine(
|
|
91
|
+
new URL('@autochitect/engine/engine.wasm', import.meta.url)
|
|
92
|
+
);
|
|
32
93
|
```
|
|
33
94
|
|
|
34
95
|
## What you get
|
|
@@ -41,8 +102,6 @@ console.log(result.results);
|
|
|
41
102
|
|
|
42
103
|
## The DSL
|
|
43
104
|
|
|
44
|
-
The language is deliberately small. It's designed for cost estimation and financial modeling specifically.
|
|
45
|
-
|
|
46
105
|
```
|
|
47
106
|
# Inputs — bind to external data (your JSON)
|
|
48
107
|
revenue: Input("annual_revenue")
|
|
@@ -72,81 +131,6 @@ cumulative = SCAN(monthly, 0, LAMBDA(acc, m, acc + m))
|
|
|
72
131
|
|
|
73
132
|
**MAP** transforms arrays element-wise. **SCAN** accumulates (like reduce, but returns intermediate results). **LAMBDA** defines inline functions with named parameters.
|
|
74
133
|
|
|
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.
|
|
76
|
-
|
|
77
|
-
## Node.js usage
|
|
78
|
-
|
|
79
|
-
```javascript
|
|
80
|
-
import { createEngine } from '@autochitect/engine';
|
|
81
|
-
import { createRequire } from 'module';
|
|
82
|
-
|
|
83
|
-
const require = createRequire(import.meta.url);
|
|
84
|
-
const wasmPath = require.resolve('@autochitect/engine/engine.wasm');
|
|
85
|
-
const engine = await createEngine(wasmPath);
|
|
86
|
-
|
|
87
|
-
const result = engine.estimate(`
|
|
88
|
-
headcount: Input
|
|
89
|
-
avg_salary = 85000
|
|
90
|
-
benefits_rate = 0.3
|
|
91
|
-
|
|
92
|
-
labor_cost = headcount * avg_salary
|
|
93
|
-
benefits = labor_cost * benefits_rate
|
|
94
|
-
total_people_cost = labor_cost + benefits
|
|
95
|
-
`, {
|
|
96
|
-
headcount: 12,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
console.log(result.results);
|
|
100
|
-
// {
|
|
101
|
-
// headcount: 12,
|
|
102
|
-
// avg_salary: 85000,
|
|
103
|
-
// benefits_rate: 0.3,
|
|
104
|
-
// labor_cost: 1020000,
|
|
105
|
-
// benefits: 306000,
|
|
106
|
-
// total_people_cost: 1326000
|
|
107
|
-
// }
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## Browser usage
|
|
111
|
-
|
|
112
|
-
```html
|
|
113
|
-
<script type="module">
|
|
114
|
-
import { createEngine } from './node_modules/@autochitect/engine/index.mjs';
|
|
115
|
-
|
|
116
|
-
const engine = await createEngine(
|
|
117
|
-
new URL('./node_modules/@autochitect/engine/engine.wasm', import.meta.url)
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const result = engine.estimate(`
|
|
121
|
-
units: Input
|
|
122
|
-
price_per_unit: Input
|
|
123
|
-
discount_rate: Input
|
|
124
|
-
|
|
125
|
-
subtotal = units * price_per_unit
|
|
126
|
-
discount = subtotal * discount_rate
|
|
127
|
-
total = subtotal - discount
|
|
128
|
-
`, {
|
|
129
|
-
units: 1000,
|
|
130
|
-
price_per_unit: 49.99,
|
|
131
|
-
discount_rate: 0.15,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
console.log(result.results);
|
|
135
|
-
// { units: 1000, price_per_unit: 49.99, discount_rate: 0.15,
|
|
136
|
-
// subtotal: 49990, discount: 7498.5, total: 42491.5 }
|
|
137
|
-
</script>
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
When using a bundler (Vite, webpack, etc.), import the WASM file directly:
|
|
141
|
-
|
|
142
|
-
```javascript
|
|
143
|
-
import { createEngine } from '@autochitect/engine';
|
|
144
|
-
|
|
145
|
-
const engine = await createEngine(
|
|
146
|
-
new URL('@autochitect/engine/engine.wasm', import.meta.url)
|
|
147
|
-
);
|
|
148
|
-
```
|
|
149
|
-
|
|
150
134
|
## Options
|
|
151
135
|
|
|
152
136
|
```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
|
|
24
|
+
estimate(source: string, inputs?: Record<string, unknown> | object, options?: EstimateOptions): EstimateResult;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function createEngine(wasmSource
|
|
27
|
+
export function createEngine(wasmSource?: string | URL | Response | ArrayBuffer): Promise<Engine>;
|
package/index.mjs
CHANGED
|
@@ -82,6 +82,13 @@ export async function createEngine(wasmSource) {
|
|
|
82
82
|
const memoryRef = { current: null };
|
|
83
83
|
const imports = { wasi_snapshot_preview1: buildWasiImports(memoryRef) };
|
|
84
84
|
|
|
85
|
+
if (wasmSource == null && typeof globalThis.process !== 'undefined') {
|
|
86
|
+
const { fileURLToPath } = await import('url');
|
|
87
|
+
const { dirname, join } = await import('path');
|
|
88
|
+
const fs = await import('fs');
|
|
89
|
+
wasmSource = join(dirname(fileURLToPath(import.meta.url)), 'engine.wasm');
|
|
90
|
+
}
|
|
91
|
+
|
|
85
92
|
let bytes;
|
|
86
93
|
if (wasmSource instanceof ArrayBuffer || ArrayBuffer.isView(wasmSource)) {
|
|
87
94
|
bytes = wasmSource;
|
package/package.json
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autochitect/engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
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
|
}
|