@autochitect/engine 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CostForecast
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # forecaster
2
+
3
+ A 245KB WebAssembly financial modeling engine. Write cost models in a purpose-built DSL, get instant evaluation with a full dependency graph. Runs entirely client-side — no server, no latency, no data leaving the browser.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install forecaster
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```javascript
14
+ import { createEngine } from 'forecaster';
15
+
16
+ const engine = await createEngine(
17
+ new URL('forecaster/engine.wasm', import.meta.url)
18
+ );
19
+
20
+ const result = engine.evaluate(`
21
+ revenue: Input
22
+ cost_ratio: Input
23
+ costs = revenue * cost_ratio
24
+ profit = revenue - costs
25
+ `, {
26
+ revenue: 500000,
27
+ cost_ratio: 0.6
28
+ });
29
+
30
+ console.log(result.results);
31
+ // { revenue: 500000, cost_ratio: 0.6, costs: 300000, profit: 200000 }
32
+ ```
33
+
34
+ ## What you get
35
+
36
+ **`result.results`** — computed values for every variable.
37
+
38
+ **`result.graph`** — the full dependency DAG showing how each value was derived. Nodes have `kind` (input, constant, formula, map, scan) and edges show data flow. Use this to build audit trails, trace calculations back to source inputs, or render interactive visualizations.
39
+
40
+ **`result.errors`** / **`result.warnings`** — with line and column numbers.
41
+
42
+ ## The DSL
43
+
44
+ The language is deliberately small. It's not a spreadsheet formula language — it's designed for financial modeling specifically.
45
+
46
+ ```
47
+ # Declarations
48
+ revenue: Input("annual_revenue")
49
+ tax_rate: Const(0.21)
50
+ growth: Param
51
+
52
+ # Formulas
53
+ gross_profit = revenue * (1 - cost_ratio)
54
+ net_income = gross_profit * (1 - tax_rate)
55
+
56
+ # Array operations
57
+ periods = SEQUENCE(12, 1, 1)
58
+ monthly = MAP(periods, LAMBDA(p, revenue / 12 * POWER(1 + growth, p)))
59
+
60
+ # Accumulation
61
+ cumulative = SCAN(monthly, 0, LAMBDA(acc, m, acc + m))
62
+ ```
63
+
64
+ **Inputs** bind to external data (your JSON). **Constants** are fixed values. **References** alias other variables.
65
+
66
+ **MAP** transforms arrays element-wise. **SCAN** accumulates (like reduce, but returns intermediate results). **LAMBDA** defines inline functions with named parameters.
67
+
68
+ The engine compiles this into a directed acyclic graph, topologically sorts it, and evaluates in one pass. The graph is immutable — switching scenarios just swaps the input context, so it's instant.
69
+
70
+ ## Browser usage
71
+
72
+ ```html
73
+ <script type="module">
74
+ import { createEngine } from './node_modules/forecaster/index.mjs';
75
+ const engine = await createEngine('./node_modules/forecaster/engine.wasm');
76
+ const result = engine.evaluate('x = 2 + 2', {});
77
+ document.body.textContent = JSON.stringify(result.results);
78
+ </script>
79
+ ```
80
+
81
+ ## Node.js usage
82
+
83
+ ```javascript
84
+ import { createEngine } from 'forecaster';
85
+ import { fileURLToPath } from 'url';
86
+ import { dirname, join } from 'path';
87
+
88
+ const __dirname = dirname(fileURLToPath(import.meta.url));
89
+ const wasmPath = join(__dirname, 'node_modules/forecaster/engine.wasm');
90
+ const engine = await createEngine(wasmPath);
91
+ ```
92
+
93
+ ## Options
94
+
95
+ ```javascript
96
+ // Skip the dependency graph for faster evaluation
97
+ engine.evaluate(source, inputs, { graph: false });
98
+ ```
99
+
100
+ ## License
101
+
102
+ MIT
package/engine.wasm ADDED
Binary file
package/index.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ export interface EvalResult {
2
+ success: boolean;
3
+ results: Record<string, number | number[] | string | null>;
4
+ errors: Array<{ message: string; line: number; col: number }>;
5
+ warnings: Array<{ message: string; line: number; col: number }>;
6
+ graph?: {
7
+ nodes: Array<{
8
+ id: number;
9
+ name: string;
10
+ kind: 'constant' | 'input' | 'param' | 'formula' | 'map' | 'scan';
11
+ value: number | number[] | string | null;
12
+ expression: string | null;
13
+ inputKey: string | null;
14
+ }>;
15
+ edges: Array<{ source: number; target: number }>;
16
+ };
17
+ }
18
+
19
+ export interface EvalOptions {
20
+ graph?: boolean;
21
+ }
22
+
23
+ export interface Engine {
24
+ evaluate(source: string, inputs?: Record<string, unknown>, options?: EvalOptions): EvalResult;
25
+ }
26
+
27
+ export function createEngine(wasmSource: string | Response | ArrayBuffer): Promise<Engine>;
package/index.mjs ADDED
@@ -0,0 +1,129 @@
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 (typeof wasmSource === 'string') {
92
+ if (typeof fetch !== 'undefined') {
93
+ bytes = await (await fetch(wasmSource)).arrayBuffer();
94
+ } else {
95
+ const fs = await import('fs');
96
+ const path = await import('path');
97
+ bytes = fs.readFileSync(wasmSource);
98
+ }
99
+ } else {
100
+ throw new Error('wasmSource must be a URL string, Response, or ArrayBuffer');
101
+ }
102
+
103
+ const { instance } = await WebAssembly.instantiate(bytes, imports);
104
+ memoryRef.current = instance.exports.memory;
105
+
106
+ function writeString(str) {
107
+ const enc = new TextEncoder().encode(str);
108
+ const ptr = instance.exports.wasm_alloc(enc.length);
109
+ if (ptr === 0) throw new Error('WASM allocation failed');
110
+ new Uint8Array(memoryRef.current.buffer, ptr, enc.length).set(enc);
111
+ return { ptr, len: enc.length };
112
+ }
113
+
114
+ return {
115
+ evaluate(source, inputs = {}, options = {}) {
116
+ const includeGraph = options.graph !== false;
117
+ const src = writeString(source);
118
+ const json = writeString(JSON.stringify(inputs));
119
+ const resultLen = instance.exports.wasm_eval(
120
+ src.ptr, src.len,
121
+ json.ptr, json.len,
122
+ includeGraph ? 1 : 0
123
+ );
124
+ const resultPtr = instance.exports.wasm_result_ptr();
125
+ const resultBytes = new Uint8Array(memoryRef.current.buffer, resultPtr, resultLen);
126
+ return JSON.parse(new TextDecoder().decode(resultBytes));
127
+ },
128
+ };
129
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@autochitect/engine",
3
+ "version": "1.0.0",
4
+ "description": "A 245KB WebAssembly financial modeling engine. Define cost models in a purpose-built DSL, get instant evaluation with full dependency tracing. No server required.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "index.mjs",
8
+ "types": "index.d.ts",
9
+ "files": [
10
+ "index.mjs",
11
+ "index.d.ts",
12
+ "engine.wasm",
13
+ "LICENSE",
14
+ "README.md"
15
+ ],
16
+ "keywords": [
17
+ "financial-modeling",
18
+ "cost-estimation",
19
+ "wasm",
20
+ "webassembly",
21
+ "dsl",
22
+ "spreadsheet",
23
+ "calculation-engine",
24
+ "scenario-analysis",
25
+ "dependency-graph"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git@github.com:autochitect/engine.git"
30
+ },
31
+ "homepage": "https://autochitect.com/engine"
32
+ }