yaml-flow 2.4.0 → 2.6.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/README.md CHANGED
@@ -983,6 +983,208 @@ const restored = restore(data); // → LiveGraph (validates shape)
983
983
 
984
984
  ---
985
985
 
986
+ ## LLM Inference
987
+
988
+ Pluggable AI-assisted completion detection. The caller provides the LLM via an `InferenceAdapter` — yaml-flow builds the prompt, parses the response, and applies the results. Core stays pure; inference is opt-in.
989
+
990
+ ```typescript
991
+ import {
992
+ buildInferencePrompt, inferCompletions, applyInferences, inferAndApply,
993
+ } from 'yaml-flow/inference';
994
+ ```
995
+
996
+ ### Inference Hints on Nodes
997
+
998
+ Add optional `inference` metadata to any `TaskConfig`:
999
+
1000
+ ```typescript
1001
+ const config = {
1002
+ settings: { completion: 'all-tasks' },
1003
+ tasks: {
1004
+ 'infra-provisioned': {
1005
+ provides: ['infra-ready'],
1006
+ inference: {
1007
+ criteria: 'All Azure resources provisioned successfully',
1008
+ keywords: ['azure', 'deployment', 'provisioning'],
1009
+ suggestedChecks: ['scan logs for "Deployment Succeeded"'],
1010
+ autoDetectable: true, // LLM will analyze this node
1011
+ },
1012
+ },
1013
+ 'app-deployed': {
1014
+ requires: ['infra-ready'],
1015
+ provides: ['app-ready'],
1016
+ inference: {
1017
+ criteria: 'Health check returns HTTP 200',
1018
+ autoDetectable: true,
1019
+ },
1020
+ },
1021
+ 'monitoring': { // no inference → LLM skips it
1022
+ requires: ['app-ready'],
1023
+ provides: ['monitored'],
1024
+ },
1025
+ },
1026
+ };
1027
+ ```
1028
+
1029
+ ### Pluggable Adapter
1030
+
1031
+ Implement one method — `analyze(prompt) → string`:
1032
+
1033
+ ```typescript
1034
+ import type { InferenceAdapter } from 'yaml-flow/inference';
1035
+
1036
+ // OpenAI
1037
+ const openaiAdapter: InferenceAdapter = {
1038
+ analyze: async (prompt) => {
1039
+ const res = await openai.chat.completions.create({
1040
+ model: 'gpt-4o', messages: [{ role: 'user', content: prompt }],
1041
+ });
1042
+ return res.choices[0].message.content ?? '[]';
1043
+ },
1044
+ };
1045
+
1046
+ // Anthropic
1047
+ const claudeAdapter: InferenceAdapter = {
1048
+ analyze: async (prompt) => {
1049
+ const res = await anthropic.messages.create({
1050
+ model: 'claude-sonnet-4-20250514', max_tokens: 1024,
1051
+ messages: [{ role: 'user', content: prompt }],
1052
+ });
1053
+ return res.content[0].type === 'text' ? res.content[0].text : '[]';
1054
+ },
1055
+ };
1056
+
1057
+ // Any HTTP endpoint
1058
+ const customAdapter: InferenceAdapter = {
1059
+ analyze: async (prompt) => {
1060
+ const res = await fetch('https://my-llm/analyze', {
1061
+ method: 'POST', body: JSON.stringify({ prompt }),
1062
+ });
1063
+ return (await res.json()).response;
1064
+ },
1065
+ };
1066
+ ```
1067
+
1068
+ ### Built-in Adapter Factories
1069
+
1070
+ Zero-boilerplate adapters for common patterns:
1071
+
1072
+ ```typescript
1073
+ import { createCliAdapter, createHttpAdapter } from 'yaml-flow/inference';
1074
+
1075
+ // Ollama via HTTP
1076
+ const ollama = createHttpAdapter({
1077
+ url: 'http://localhost:11434/api/generate',
1078
+ buildBody: (prompt) => ({ model: 'llama3', prompt, stream: false }),
1079
+ extractResponse: (json) => json.response,
1080
+ });
1081
+
1082
+ // Ollama via CLI
1083
+ const ollamaCli = createCliAdapter({
1084
+ command: 'ollama',
1085
+ args: (prompt) => ['run', 'llama3', prompt],
1086
+ });
1087
+
1088
+ // Simon Willison's llm CLI (stdin mode for long prompts)
1089
+ const llm = createCliAdapter({
1090
+ command: 'llm',
1091
+ args: () => ['--no-stream'],
1092
+ stdin: true,
1093
+ });
1094
+
1095
+ // Custom Python script
1096
+ const custom = createCliAdapter({
1097
+ command: 'python',
1098
+ args: (prompt) => ['scripts/infer.py', '--json', prompt],
1099
+ cwd: '/path/to/project',
1100
+ env: { MODEL: 'gpt-4o' },
1101
+ timeout: 60_000,
1102
+ });
1103
+ ```
1104
+
1105
+ **`createCliAdapter(options)`** — spawns a child process, captures stdout:
1106
+ | Option | Type | Description |
1107
+ |--------|------|-------------|
1108
+ | `command` | `string` | Executable to run (`ollama`, `llm`, `python`, `gh`, …) |
1109
+ | `args` | `(prompt) => string[]` | Build argument list from the prompt |
1110
+ | `stdin` | `boolean` | Pipe prompt via stdin instead of args (default: `false`) |
1111
+ | `timeout` | `number` | Kill after N ms (default: `30000`) |
1112
+ | `cwd` | `string` | Working directory |
1113
+ | `env` | `Record<string, string>` | Extra environment variables |
1114
+
1115
+ **`createHttpAdapter(options)`** — POSTs to an HTTP endpoint:
1116
+ | Option | Type | Description |
1117
+ |--------|------|-------------|
1118
+ | `url` | `string` | Endpoint URL |
1119
+ | `headers` | `Record<string, string>` | Request headers |
1120
+ | `buildBody` | `(prompt) => object` | Build request body (default: `{ prompt }`) |
1121
+ | `extractResponse` | `(json) => string` | Extract text from response JSON |
1122
+ | `timeout` | `number` | Abort after N ms (default: `30000`) |
1123
+ ```
1124
+
1125
+ ### Three APIs: Build → Suggest → Apply
1126
+
1127
+ ```typescript
1128
+ import { createLiveGraph } from 'yaml-flow/continuous-event-graph';
1129
+ import { buildInferencePrompt, inferCompletions, applyInferences } from 'yaml-flow/inference';
1130
+
1131
+ let live = createLiveGraph(config);
1132
+
1133
+ // 1. BUILD: Generate the prompt (pure, sync)
1134
+ const prompt = buildInferencePrompt(live, {
1135
+ context: 'Deployment log: "Deployment Succeeded", health check: HTTP 200',
1136
+ });
1137
+
1138
+ // 2. SUGGEST: Ask the LLM (async)
1139
+ const result = await inferCompletions(live, adapter, {
1140
+ threshold: 0.8,
1141
+ context: 'Deployment log: ...',
1142
+ });
1143
+ result.suggestions; // [{ taskName, confidence, reasoning, detectionMethod }]
1144
+
1145
+ // 3. APPLY: Accept high-confidence suggestions (pure, sync)
1146
+ live = applyInferences(live, result, 0.8); // only applies >= 80% confidence
1147
+ ```
1148
+
1149
+ ### One-Shot Convenience
1150
+
1151
+ ```typescript
1152
+ import { inferAndApply } from 'yaml-flow/inference';
1153
+
1154
+ const { live: updated, applied, skipped, inference } = await inferAndApply(
1155
+ live, adapter, { threshold: 0.8, context: 'deployment logs...' }
1156
+ );
1157
+
1158
+ console.log('Auto-completed:', applied.map(s => s.taskName));
1159
+ console.log('Skipped (low confidence):', skipped.map(s => `${s.taskName} (${s.confidence})`));
1160
+ ```
1161
+
1162
+ ### Inference API Reference
1163
+
1164
+ | Function | Description |
1165
+ |---|---|
1166
+ | `buildInferencePrompt(live, opts?)` | Build LLM prompt from graph state (pure, sync) |
1167
+ | `inferCompletions(live, adapter, opts?)` | Ask LLM to suggest completions (async) |
1168
+ | `applyInferences(live, result, threshold?)` | Apply suggestions above threshold (pure, sync) |
1169
+ | `inferAndApply(live, adapter, opts?)` | Infer + apply in one step (async, convenience) |
1170
+ | `createCliAdapter(opts)` | Factory: adapter that spawns a CLI command |
1171
+ | `createHttpAdapter(opts)` | Factory: adapter that POSTs to an HTTP endpoint |
1172
+
1173
+ ### Inference Types
1174
+
1175
+ | Type | Description |
1176
+ |---|---|
1177
+ | `InferenceAdapter` | `{ analyze(prompt: string): Promise<string> }` — pluggable LLM bridge |
1178
+ | `InferenceHints` | `criteria`, `keywords`, `suggestedChecks`, `autoDetectable` on a TaskConfig |
1179
+ | `InferenceOptions` | `threshold`, `scope`, `context`, `systemPrompt` |
1180
+ | `InferenceResult` | `suggestions[]`, `promptUsed`, `rawResponse`, `analyzedNodes` |
1181
+ | `InferredCompletion` | `taskName`, `confidence`, `reasoning`, `detectionMethod: 'llm-inferred'` |
1182
+ | `InferAndApplyResult` | `live`, `inference`, `applied[]`, `skipped[]` |
1183
+ | `CliAdapterOptions` | `command`, `args`, `stdin`, `timeout`, `cwd`, `env` |
1184
+ | `HttpAdapterOptions` | `url`, `headers`, `buildBody`, `extractResponse`, `timeout` |
1185
+
1186
+ ---
1187
+
986
1188
  ## Loading & Exporting Graph Configs
987
1189
 
988
1190
  ```typescript
@@ -1041,6 +1243,12 @@ import {
1041
1243
  getUpstream, getDownstream,
1042
1244
  } from 'yaml-flow/continuous-event-graph';
1043
1245
 
1246
+ // LLM Inference (AI-assisted completion detection)
1247
+ import {
1248
+ buildInferencePrompt, inferCompletions, applyInferences, inferAndApply,
1249
+ } from 'yaml-flow/inference';
1250
+ import type { InferenceAdapter, InferenceResult, InferenceOptions } from 'yaml-flow/inference';
1251
+
1044
1252
  // Backward compatibility (v1 names → v2)
1045
1253
  import { FlowEngine, createEngine } from 'yaml-flow'; // aliases for StepMachine, createStepMachine
1046
1254
  ```
@@ -1113,6 +1321,10 @@ See the [examples/](./examples) directory:
1113
1321
  | [URL Pipeline](./examples/graph-of-graphs/url-processing-pipeline.ts) | Graph-of-Graphs | Outer event-graph → batch × inner event-graph per item |
1114
1322
  | [Multi-Stage ETL](./examples/graph-of-graphs/multi-stage-etl.ts) | Graph-of-Graphs | Mixed modes: event-graph outer → step-machine + event-graph subs |
1115
1323
  | [Stock Dashboard](./examples/continuous-event-graph/stock-dashboard.ts) | Continuous Event Graph | Runtime mutations, token drain, upstream/downstream, snapshot |
1324
+ | [Azure Deployment](./examples/inference/azure-deployment.ts) | Inference | LLM analyzes deployment logs, auto-completes checkpoints |
1325
+ | [Data Pipeline](./examples/inference/data-pipeline.ts) | Inference | Iterative inference — evidence arrives in waves |
1326
+ | [Pluggable Adapters](./examples/inference/pluggable-adapters.ts) | Inference | OpenAI, Anthropic, Azure, CLI, HTTP adapter factories |
1327
+ | [Copilot CLI](./examples/inference/copilot-cli.ts) | Inference | GitHub Copilot CLI as inference adapter via `createCliAdapter` |
1116
1328
  | [Order Processing](./examples/flows/order-processing.yaml) | Step Machine | YAML flow definition |
1117
1329
  | [Browser Demo](./examples/browser/index.html) | Step Machine | In-browser usage |
1118
1330
 
@@ -0,0 +1,446 @@
1
+ // card_compute.js — Pure JSON expression evaluator for LiveCards nodes
2
+ //
3
+ // Isomorphic: works in browser (global), Node.js (require), and ESM (import).
4
+ // No DOM dependency. Usable on both client and server.
5
+ //
6
+ // API:
7
+ // CardCompute.run(node) → mutates node.state with computed values, returns node
8
+ // CardCompute.eval(expr, node) → evaluates a single compute_expr, returns value
9
+ // CardCompute.resolve(node, path) → deep-get a state path like "state.foo.bar"
10
+ // CardCompute.registerFunction(name, fn) → add custom compute function
11
+ // CardCompute.functions → read-only map of all registered functions
12
+ //
13
+ // Compute declarations (node.compute):
14
+ // {
15
+ // "total_value": { "fn": "sum", "input": "state.raw_quotes" },
16
+ // "avg_price": { "fn": "avg", "input": "state.raw_quotes" },
17
+ // "direction": { "fn": "if", "cond": { "fn": "gt", "input": ["state.latest", "state.prev"] }, "then": "up", "else": "down" }
18
+ // }
19
+
20
+ (function (root, factory) {
21
+ if (typeof module === 'object' && module.exports) {
22
+ module.exports = factory(); // Node / CommonJS
23
+ } else if (typeof define === 'function' && define.amd) {
24
+ define(factory); // AMD
25
+ } else {
26
+ root.CardCompute = factory(); // Browser global
27
+ }
28
+ }(typeof globalThis !== 'undefined' ? globalThis : this, function () {
29
+ 'use strict';
30
+
31
+ // ===========================================================================
32
+ // Deep path utilities
33
+ // ===========================================================================
34
+
35
+ function _deepGet(obj, path) {
36
+ if (!path || !obj) return undefined;
37
+ const parts = path.split('.');
38
+ let cur = obj;
39
+ for (let i = 0; i < parts.length; i++) {
40
+ if (cur == null) return undefined;
41
+ cur = cur[parts[i]];
42
+ }
43
+ return cur;
44
+ }
45
+
46
+ function _deepSet(obj, path, value) {
47
+ const parts = path.split('.');
48
+ let cur = obj;
49
+ for (let i = 0; i < parts.length - 1; i++) {
50
+ if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};
51
+ cur = cur[parts[i]];
52
+ }
53
+ cur[parts[parts.length - 1]] = value;
54
+ }
55
+
56
+ function resolve(node, path) {
57
+ if (!path) return undefined;
58
+ return _deepGet(node, path);
59
+ }
60
+
61
+ // ===========================================================================
62
+ // Built-in function registry
63
+ // ===========================================================================
64
+
65
+ var _fns = {};
66
+
67
+ // ---- Aggregates ----
68
+
69
+ _fns.sum = function (input, _eval, opts) {
70
+ var a = Array.isArray(input) ? input : [];
71
+ return opts.field
72
+ ? a.reduce(function (s, r) { return s + (Number(r[opts.field]) || 0); }, 0)
73
+ : a.reduce(function (s, v) { return s + (Number(v) || 0); }, 0);
74
+ };
75
+
76
+ _fns.avg = function (input, _eval, opts) {
77
+ var s = _fns.sum(input, _eval, opts);
78
+ var n = Array.isArray(input) ? input.length : 1;
79
+ return n ? s / n : 0;
80
+ };
81
+
82
+ _fns.min = function (input, _eval, opts) {
83
+ var a = Array.isArray(input) ? input : [];
84
+ var vals = opts.field ? a.map(function (r) { return Number(r[opts.field]); }) : a.map(Number);
85
+ return vals.length ? Math.min.apply(null, vals) : 0;
86
+ };
87
+
88
+ _fns.max = function (input, _eval, opts) {
89
+ var a = Array.isArray(input) ? input : [];
90
+ var vals = opts.field ? a.map(function (r) { return Number(r[opts.field]); }) : a.map(Number);
91
+ return vals.length ? Math.max.apply(null, vals) : 0;
92
+ };
93
+
94
+ _fns.count = function (input) {
95
+ return Array.isArray(input) ? input.length : (input != null ? 1 : 0);
96
+ };
97
+
98
+ _fns.first = function (input) {
99
+ return Array.isArray(input) ? input[0] : input;
100
+ };
101
+
102
+ _fns.last = function (input) {
103
+ return Array.isArray(input) ? input[input.length - 1] : input;
104
+ };
105
+
106
+ // ---- Math ----
107
+
108
+ _fns.add = function (input) {
109
+ var a = Array.isArray(input) ? input : [];
110
+ return a.reduce(function (s, v) { return s + Number(v); }, 0);
111
+ };
112
+
113
+ _fns.sub = function (input) {
114
+ var a = Array.isArray(input) ? input : [];
115
+ return a.length >= 2 ? Number(a[0]) - Number(a[1]) : 0;
116
+ };
117
+
118
+ _fns.mul = function (input) {
119
+ var a = Array.isArray(input) ? input : [];
120
+ return a.reduce(function (s, v) { return s * Number(v); }, 1);
121
+ };
122
+
123
+ _fns.div = function (input) {
124
+ var a = Array.isArray(input) ? input : [];
125
+ return a.length >= 2 && Number(a[1]) !== 0 ? Number(a[0]) / Number(a[1]) : 0;
126
+ };
127
+
128
+ _fns.round = function (input, _eval, opts) {
129
+ var decimals = opts.decimals != null ? opts.decimals : 0;
130
+ var factor = Math.pow(10, decimals);
131
+ return Math.round(Number(input) * factor) / factor;
132
+ };
133
+
134
+ _fns.abs = function (input) { return Math.abs(Number(input)); };
135
+
136
+ _fns.mod = function (input) {
137
+ var a = Array.isArray(input) ? input : [];
138
+ return a.length >= 2 ? Number(a[0]) % Number(a[1]) : 0;
139
+ };
140
+
141
+ // ---- Compare ----
142
+
143
+ _fns.gt = function (input) { var a = Array.isArray(input) ? input : []; return a.length >= 2 && Number(a[0]) > Number(a[1]); };
144
+ _fns.gte = function (input) { var a = Array.isArray(input) ? input : []; return a.length >= 2 && Number(a[0]) >= Number(a[1]); };
145
+ _fns.lt = function (input) { var a = Array.isArray(input) ? input : []; return a.length >= 2 && Number(a[0]) < Number(a[1]); };
146
+ _fns.lte = function (input) { var a = Array.isArray(input) ? input : []; return a.length >= 2 && Number(a[0]) <= Number(a[1]); };
147
+ _fns.eq = function (input) { var a = Array.isArray(input) ? input : []; return a.length >= 2 && a[0] === a[1]; };
148
+ _fns.neq = function (input) { var a = Array.isArray(input) ? input : []; return a.length >= 2 && a[0] !== a[1]; };
149
+
150
+ // ---- Logic ----
151
+
152
+ _fns.and = function (input) { var a = Array.isArray(input) ? input : []; return a.every(Boolean); };
153
+ _fns.or = function (input) { var a = Array.isArray(input) ? input : []; return a.some(Boolean); };
154
+ _fns.not = function (input) { return !input; };
155
+ // "if" is handled specially in evalExpr
156
+
157
+ // ---- String ----
158
+
159
+ _fns.concat = function (input) {
160
+ var a = Array.isArray(input) ? input : [];
161
+ return a.map(function (v) { return v != null ? String(v) : ''; }).join('');
162
+ };
163
+
164
+ _fns.upper = function (input) { return String(input || '').toUpperCase(); };
165
+ _fns.lower = function (input) { return String(input || '').toLowerCase(); };
166
+
167
+ _fns.template = function (input, _eval, opts) {
168
+ var t = String(opts.format || '');
169
+ if (input && typeof input === 'object') {
170
+ Object.keys(input).forEach(function (k) {
171
+ t = t.split('{{' + k + '}}').join(input[k] != null ? String(input[k]) : '');
172
+ });
173
+ }
174
+ return t;
175
+ };
176
+
177
+ _fns.join = function (input, _eval, opts) {
178
+ var a = Array.isArray(input) ? input : [];
179
+ var sep = opts.separator != null ? opts.separator : ', ';
180
+ return a.map(function (v) { return v != null ? String(v) : ''; }).join(sep);
181
+ };
182
+
183
+ _fns.split = function (input, _eval, opts) {
184
+ var sep = opts.separator != null ? opts.separator : ',';
185
+ return String(input || '').split(sep).map(function (s) { return s.trim(); });
186
+ };
187
+
188
+ _fns.trim = function (input) { return String(input || '').trim(); };
189
+
190
+ // ---- Collection ----
191
+
192
+ _fns.pluck = function (input, _eval, opts) {
193
+ return Array.isArray(input) ? input.map(function (r) { return r[opts.field]; }) : [];
194
+ };
195
+
196
+ _fns.filter = function (input, evalFn, opts) {
197
+ // Handled specially in evalExpr for the where clause; fallback for simple truthy filter
198
+ if (!Array.isArray(input)) return [];
199
+ if (opts.field) return input.filter(function (r) { return !!r[opts.field]; });
200
+ return input.filter(Boolean);
201
+ };
202
+
203
+ _fns.map = function (input) {
204
+ // Handled specially in evalExpr for the apply clause
205
+ return Array.isArray(input) ? input.slice() : [];
206
+ };
207
+
208
+ _fns.sort = function (input, _eval, opts) {
209
+ var a = Array.isArray(input) ? input.slice() : [];
210
+ var f = opts.field;
211
+ var dir = opts.direction === 'desc' ? -1 : 1;
212
+ if (f) return a.sort(function (x, y) { return x[f] > y[f] ? dir : x[f] < y[f] ? -dir : 0; });
213
+ return a.sort(function (x, y) { return x > y ? dir : x < y ? -dir : 0; });
214
+ };
215
+
216
+ _fns.slice = function (input, _eval, opts) {
217
+ return Array.isArray(input) ? input.slice(opts.start || 0, opts.end) : input;
218
+ };
219
+
220
+ _fns.flat = function (input, _eval, opts) {
221
+ var depth = opts.depth != null ? opts.depth : 1;
222
+ return Array.isArray(input) ? input.flat(depth) : [input];
223
+ };
224
+
225
+ _fns.unique = function (input) {
226
+ if (!Array.isArray(input)) return [input];
227
+ // For primitives, use Set. For objects, use JSON comparison.
228
+ var seen = new Set();
229
+ return input.filter(function (v) {
230
+ var key = typeof v === 'object' ? JSON.stringify(v) : v;
231
+ if (seen.has(key)) return false;
232
+ seen.add(key);
233
+ return true;
234
+ });
235
+ };
236
+
237
+ _fns.group = function (input, _eval, opts) {
238
+ var a = Array.isArray(input) ? input : [];
239
+ var g = {};
240
+ a.forEach(function (r) {
241
+ var k = String(r[opts.field] || '');
242
+ if (!g[k]) g[k] = [];
243
+ g[k].push(r);
244
+ });
245
+ return g;
246
+ };
247
+
248
+ _fns.flatten_keys = function (input) {
249
+ // { a: [1,2], b: [3] } → [{ key: "a", value: 1 }, { key: "a", value: 2 }, { key: "b", value: 3 }]
250
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return [];
251
+ var result = [];
252
+ Object.keys(input).forEach(function (k) {
253
+ var vals = Array.isArray(input[k]) ? input[k] : [input[k]];
254
+ vals.forEach(function (v) { result.push({ key: k, value: v }); });
255
+ });
256
+ return result;
257
+ };
258
+
259
+ _fns.entries = function (input) {
260
+ if (!input || typeof input !== 'object' || Array.isArray(input)) return [];
261
+ return Object.keys(input).map(function (k) { return { key: k, value: input[k] }; });
262
+ };
263
+
264
+ _fns.from_entries = function (input) {
265
+ if (!Array.isArray(input)) return {};
266
+ var obj = {};
267
+ input.forEach(function (item) { if (item.key != null) obj[item.key] = item.value; });
268
+ return obj;
269
+ };
270
+
271
+ _fns.length = function (input) {
272
+ if (Array.isArray(input)) return input.length;
273
+ if (typeof input === 'string') return input.length;
274
+ if (input && typeof input === 'object') return Object.keys(input).length;
275
+ return 0;
276
+ };
277
+
278
+ // ---- Lookup ----
279
+
280
+ _fns.get = function (input, _eval, opts) {
281
+ return _deepGet(input, opts.field || opts.path || '');
282
+ };
283
+
284
+ _fns.default = function (input, _eval, opts) {
285
+ return input != null ? input : opts.value;
286
+ };
287
+
288
+ _fns.coalesce = function (input) {
289
+ var a = Array.isArray(input) ? input : [];
290
+ for (var i = 0; i < a.length; i++) { if (a[i] != null) return a[i]; }
291
+ return null;
292
+ };
293
+
294
+ // ---- Date ----
295
+
296
+ _fns.now = function () { return new Date().toISOString(); };
297
+
298
+ _fns.diff_days = function (input) {
299
+ var a = Array.isArray(input) ? input : [];
300
+ return a.length >= 2 ? Math.floor((new Date(a[0]) - new Date(a[1])) / 86400000) : 0;
301
+ };
302
+
303
+ _fns.format_date = function (input, _eval, opts) {
304
+ try {
305
+ var d = new Date(input);
306
+ if (opts.format === 'iso') return d.toISOString();
307
+ if (opts.format === 'date') return d.toLocaleDateString();
308
+ if (opts.format === 'time') return d.toLocaleTimeString();
309
+ return d.toLocaleDateString();
310
+ } catch (e) {
311
+ return String(input);
312
+ }
313
+ };
314
+
315
+ _fns.parse_date = function (input) {
316
+ try { return new Date(input).toISOString(); } catch (e) { return null; }
317
+ };
318
+
319
+ // ---- Type ----
320
+
321
+ _fns.to_number = function (input) { return Number(input) || 0; };
322
+ _fns.to_string = function (input) { return input != null ? String(input) : ''; };
323
+ _fns.to_bool = function (input) { return !!input; };
324
+ _fns.type_of = function (input) { return Array.isArray(input) ? 'array' : typeof input; };
325
+ _fns.is_null = function (input) { return input == null; };
326
+ _fns.is_empty = function (input) {
327
+ if (input == null) return true;
328
+ if (Array.isArray(input)) return input.length === 0;
329
+ if (typeof input === 'string') return input.length === 0;
330
+ if (typeof input === 'object') return Object.keys(input).length === 0;
331
+ return false;
332
+ };
333
+
334
+ // ===========================================================================
335
+ // Expression evaluator
336
+ // ===========================================================================
337
+
338
+ var _customFns = {};
339
+
340
+ function evalExpr(expr, node) {
341
+ if (expr == null) return expr;
342
+
343
+ // Literal values pass through
344
+ if (typeof expr !== 'object' || Array.isArray(expr)) return expr;
345
+
346
+ // Must have fn to be an expression
347
+ if (!expr.fn) return expr;
348
+
349
+ // Resolve input
350
+ var input = expr.input;
351
+ if (typeof input === 'string' && input.startsWith('state.')) {
352
+ input = resolve(node, input);
353
+ } else if (Array.isArray(input)) {
354
+ input = input.map(function (v) {
355
+ if (typeof v === 'string' && v.startsWith('state.')) return resolve(node, v);
356
+ if (v && typeof v === 'object' && v.fn) return evalExpr(v, node);
357
+ return v;
358
+ });
359
+ } else if (input && typeof input === 'object' && input.fn) {
360
+ input = evalExpr(input, node);
361
+ }
362
+
363
+ // Special: if
364
+ if (expr.fn === 'if') {
365
+ var cond = evalExpr(expr.cond, node);
366
+ if (cond) {
367
+ return (expr.then && typeof expr.then === 'object' && expr.then.fn) ? evalExpr(expr.then, node) : expr.then;
368
+ } else {
369
+ return (expr.else && typeof expr.else === 'object' && expr.else.fn) ? evalExpr(expr.else, node) : expr.else;
370
+ }
371
+ }
372
+
373
+ // Special: filter with where clause
374
+ if (expr.fn === 'filter' && Array.isArray(input) && expr.where) {
375
+ return input.filter(function (item) {
376
+ var tmp = { state: Object.assign({}, node.state, { $: item }) };
377
+ return evalExpr(expr.where, tmp);
378
+ });
379
+ }
380
+
381
+ // Special: map with apply clause
382
+ if (expr.fn === 'map' && Array.isArray(input) && expr.apply) {
383
+ return input.map(function (item) {
384
+ var tmp = { state: Object.assign({}, node.state, { $: item }) };
385
+ return evalExpr(expr.apply, tmp);
386
+ });
387
+ }
388
+
389
+ // Look up function
390
+ var fn = _customFns[expr.fn] || _fns[expr.fn];
391
+ if (!fn) {
392
+ console.warn('CardCompute: unknown function "' + expr.fn + '"');
393
+ return undefined;
394
+ }
395
+
396
+ return fn(input, evalExpr, expr);
397
+ }
398
+
399
+ // ===========================================================================
400
+ // run — evaluate all compute declarations on a node
401
+ // ===========================================================================
402
+
403
+ function run(node) {
404
+ if (!node || !node.compute) return node;
405
+ if (!node.state) node.state = {};
406
+
407
+ var keys = Object.keys(node.compute);
408
+ for (var i = 0; i < keys.length; i++) {
409
+ var key = keys[i];
410
+ try {
411
+ var val = evalExpr(node.compute[key], node);
412
+ _deepSet(node.state, key, val);
413
+ } catch (e) {
414
+ console.error('CardCompute.run error on "' + (node.id || '?') + '.' + key + '":', e);
415
+ }
416
+ }
417
+
418
+ return node;
419
+ }
420
+
421
+ // ===========================================================================
422
+ // registerFunction — extend the vocabulary
423
+ // ===========================================================================
424
+
425
+ function registerFunction(name, fn) {
426
+ _customFns[name] = fn;
427
+ }
428
+
429
+ // ===========================================================================
430
+ // Export
431
+ // ===========================================================================
432
+
433
+ return {
434
+ run: run,
435
+ eval: evalExpr,
436
+ resolve: resolve,
437
+ registerFunction: registerFunction,
438
+ get functions() {
439
+ var all = {};
440
+ Object.keys(_fns).forEach(function (k) { all[k] = _fns[k]; });
441
+ Object.keys(_customFns).forEach(function (k) { all[k] = _customFns[k]; });
442
+ return all;
443
+ }
444
+ };
445
+
446
+ }));