yaml-flow 3.0.0 → 3.1.1

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.
Files changed (59) hide show
  1. package/README.md +44 -23
  2. package/dist/{constants-B_ftYTTE.d.ts → constants-B2zqu10b.d.ts} +7 -57
  3. package/dist/{constants-CiyHX8L-.d.cts → constants-DJZU1pwJ.d.cts} +7 -57
  4. package/dist/continuous-event-graph/index.cjs +1161 -182
  5. package/dist/continuous-event-graph/index.cjs.map +1 -1
  6. package/dist/continuous-event-graph/index.d.cts +567 -48
  7. package/dist/continuous-event-graph/index.d.ts +567 -48
  8. package/dist/continuous-event-graph/index.js +1151 -183
  9. package/dist/continuous-event-graph/index.js.map +1 -1
  10. package/dist/event-graph/index.cjs +35 -11
  11. package/dist/event-graph/index.cjs.map +1 -1
  12. package/dist/event-graph/index.d.cts +14 -5
  13. package/dist/event-graph/index.d.ts +14 -5
  14. package/dist/event-graph/index.js +34 -11
  15. package/dist/event-graph/index.js.map +1 -1
  16. package/dist/index.cjs +945 -414
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +5 -4
  19. package/dist/index.d.ts +5 -4
  20. package/dist/index.js +936 -415
  21. package/dist/index.js.map +1 -1
  22. package/dist/inference/index.cjs +31 -7
  23. package/dist/inference/index.cjs.map +1 -1
  24. package/dist/inference/index.d.cts +2 -2
  25. package/dist/inference/index.d.ts +2 -2
  26. package/dist/inference/index.js +31 -7
  27. package/dist/inference/index.js.map +1 -1
  28. package/dist/{types-CxJg9Jrt.d.cts → types-BwvgvlOO.d.cts} +2 -2
  29. package/dist/{types-BuEo3wVG.d.ts → types-ClRA8hzC.d.ts} +2 -2
  30. package/dist/{types-BpWrH1sf.d.cts → types-DEj7OakX.d.cts} +14 -4
  31. package/dist/{types-BpWrH1sf.d.ts → types-DEj7OakX.d.ts} +14 -4
  32. package/dist/validate-DEZ2Ymdb.d.ts +53 -0
  33. package/dist/validate-DqKTZg_o.d.cts +53 -0
  34. package/examples/batch/batch-step-machine.ts +121 -0
  35. package/examples/browser/index.html +367 -0
  36. package/examples/continuous-event-graph/live-cards-board.ts +215 -0
  37. package/examples/continuous-event-graph/live-portfolio-dashboard.ts +555 -0
  38. package/examples/continuous-event-graph/portfolio-tracker.ts +287 -0
  39. package/examples/continuous-event-graph/reactive-monitoring.ts +265 -0
  40. package/examples/continuous-event-graph/reactive-pipeline.ts +168 -0
  41. package/examples/continuous-event-graph/soc-incident-board.ts +287 -0
  42. package/examples/continuous-event-graph/stock-dashboard.ts +229 -0
  43. package/examples/event-graph/ci-cd-pipeline.ts +243 -0
  44. package/examples/event-graph/executor-diamond.ts +165 -0
  45. package/examples/event-graph/executor-pipeline.ts +161 -0
  46. package/examples/event-graph/research-pipeline.ts +137 -0
  47. package/examples/flows/ai-conversation.yaml +116 -0
  48. package/examples/flows/order-processing.yaml +143 -0
  49. package/examples/flows/simple-greeting.yaml +54 -0
  50. package/examples/graph-of-graphs/multi-stage-etl.ts +307 -0
  51. package/examples/graph-of-graphs/url-processing-pipeline.ts +254 -0
  52. package/examples/inference/azure-deployment.ts +149 -0
  53. package/examples/inference/copilot-cli.ts +138 -0
  54. package/examples/inference/data-pipeline.ts +145 -0
  55. package/examples/inference/pluggable-adapters.ts +254 -0
  56. package/examples/ingest.js +733 -0
  57. package/examples/node/ai-conversation.ts +195 -0
  58. package/examples/node/simple-greeting.ts +101 -0
  59. package/package.json +3 -2
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Batch Example: Process tickets through a Step Machine flow
3
+ *
4
+ * Demonstrates:
5
+ * - batch() with step-machine processor
6
+ * - Concurrency control (3 slots)
7
+ * - Progress tracking
8
+ * - Mixed success/failure handling
9
+ *
10
+ * Run with: npx tsx examples/batch/batch-step-machine.ts
11
+ */
12
+
13
+ import { batch } from '../../src/batch/index.js';
14
+ import { createStepMachine } from '../../src/step-machine/index.js';
15
+ import type { StepFlowConfig, StepHandler } from '../../src/step-machine/types.js';
16
+
17
+ // ============================================================================
18
+ // 1. Define a simple flow
19
+ // ============================================================================
20
+
21
+ const ticketFlow: StepFlowConfig = {
22
+ id: 'support-ticket',
23
+ settings: { start_step: 'classify', max_total_steps: 10 },
24
+ steps: {
25
+ classify: {
26
+ produces_data: ['category'],
27
+ transitions: { billing: 'handle', technical: 'handle', unknown: 'escalate' },
28
+ },
29
+ handle: {
30
+ expects_data: ['category'],
31
+ produces_data: ['resolution'],
32
+ transitions: { resolved: 'done', failed: 'escalate' },
33
+ },
34
+ escalate: {
35
+ expects_data: ['category'],
36
+ produces_data: ['escalation_id'],
37
+ transitions: { done: 'done' },
38
+ },
39
+ },
40
+ terminal_states: {
41
+ done: { return_intent: 'resolved', return_artifacts: ['resolution', 'escalation_id'] },
42
+ },
43
+ };
44
+
45
+ const handlers: Record<string, StepHandler> = {
46
+ classify: async (input) => {
47
+ const msg = (input.message as string) || '';
48
+ if (msg.includes('bill') || msg.includes('charge')) return { result: 'billing', data: { category: 'billing' } };
49
+ if (msg.includes('crash') || msg.includes('error')) return { result: 'technical', data: { category: 'technical' } };
50
+ return { result: 'unknown', data: { category: 'unknown' } };
51
+ },
52
+ handle: async (input) => {
53
+ // Simulate occasional failure
54
+ if (Math.random() < 0.2) return { result: 'failed' };
55
+ return { result: 'resolved', data: { resolution: `Resolved ${input.category} issue` } };
56
+ },
57
+ escalate: async (input) => {
58
+ return { result: 'done', data: { escalation_id: `ESC-${Date.now()}` } };
59
+ },
60
+ };
61
+
62
+ // ============================================================================
63
+ // 2. Batch of tickets
64
+ // ============================================================================
65
+
66
+ const tickets = [
67
+ { id: 'T-001', message: 'I was double-charged on my bill' },
68
+ { id: 'T-002', message: 'App crashes on startup' },
69
+ { id: 'T-003', message: 'How do I change my password?' },
70
+ { id: 'T-004', message: 'Billing error on invoice #1234' },
71
+ { id: 'T-005', message: 'Error 500 on checkout page' },
72
+ { id: 'T-006', message: 'Cannot access my account' },
73
+ { id: 'T-007', message: 'Refund not processed on my bill' },
74
+ { id: 'T-008', message: 'App throws error on login' },
75
+ ];
76
+
77
+ // ============================================================================
78
+ // 3. Run batch
79
+ // ============================================================================
80
+
81
+ async function main() {
82
+ console.log(`Processing ${tickets.length} tickets with 3 concurrent slots\n`);
83
+
84
+ const result = await batch(tickets, {
85
+ concurrency: 3,
86
+
87
+ processor: async (ticket) => {
88
+ const machine = createStepMachine(ticketFlow, handlers);
89
+ return machine.run({ message: ticket.message });
90
+ },
91
+
92
+ onItemComplete: (ticket, flowResult) => {
93
+ console.log(` ✓ ${ticket.id}: ${flowResult.intent} — ${flowResult.stepHistory.join(' → ')}`);
94
+ },
95
+
96
+ onItemError: (ticket, error) => {
97
+ console.log(` ✗ ${ticket.id}: ${error.message}`);
98
+ },
99
+
100
+ onProgress: (p) => {
101
+ if (p.percent % 25 === 0) {
102
+ console.log(` [${p.percent}%] ${p.completed + p.failed}/${p.total} done, ${p.active} active`);
103
+ }
104
+ },
105
+ });
106
+
107
+ console.log(`\n${'='.repeat(50)}`);
108
+ console.log(`Results: ${result.completed} completed, ${result.failed} failed (${result.durationMs}ms)`);
109
+
110
+ // Show per-item breakdown
111
+ for (const item of result.items) {
112
+ const ticket = item.item;
113
+ if (item.status === 'completed') {
114
+ console.log(` ${ticket.id}: ${item.result?.intent} (${item.durationMs}ms)`);
115
+ } else {
116
+ console.log(` ${ticket.id}: FAILED — ${item.error?.message}`);
117
+ }
118
+ }
119
+ }
120
+
121
+ main().catch(console.error);
@@ -0,0 +1,367 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>yaml-flow Browser Example</title>
7
+ <style>
8
+ * { box-sizing: border-box; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ max-width: 800px;
12
+ margin: 40px auto;
13
+ padding: 20px;
14
+ background: #f5f5f5;
15
+ }
16
+ h1 { color: #333; }
17
+ .card {
18
+ background: white;
19
+ border-radius: 8px;
20
+ padding: 20px;
21
+ margin: 20px 0;
22
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
23
+ }
24
+ .step-list {
25
+ display: flex;
26
+ gap: 10px;
27
+ flex-wrap: wrap;
28
+ margin: 10px 0;
29
+ }
30
+ .step {
31
+ padding: 8px 16px;
32
+ border-radius: 20px;
33
+ background: #e0e0e0;
34
+ font-size: 14px;
35
+ }
36
+ .step.active {
37
+ background: #2196F3;
38
+ color: white;
39
+ }
40
+ .step.completed {
41
+ background: #4CAF50;
42
+ color: white;
43
+ }
44
+ button {
45
+ background: #2196F3;
46
+ color: white;
47
+ border: none;
48
+ padding: 12px 24px;
49
+ border-radius: 6px;
50
+ cursor: pointer;
51
+ font-size: 16px;
52
+ margin: 5px;
53
+ }
54
+ button:hover { background: #1976D2; }
55
+ button:disabled { background: #ccc; cursor: not-allowed; }
56
+ .log {
57
+ background: #263238;
58
+ color: #aed581;
59
+ padding: 15px;
60
+ border-radius: 6px;
61
+ font-family: 'Consolas', monospace;
62
+ font-size: 13px;
63
+ max-height: 300px;
64
+ overflow-y: auto;
65
+ }
66
+ .log-entry { margin: 5px 0; }
67
+ .log-entry.error { color: #ef5350; }
68
+ .log-entry.transition { color: #64B5F6; }
69
+ .result {
70
+ padding: 15px;
71
+ border-radius: 6px;
72
+ margin-top: 15px;
73
+ }
74
+ .result.success { background: #C8E6C9; }
75
+ .result.error { background: #FFCDD2; }
76
+ input {
77
+ padding: 10px;
78
+ border: 1px solid #ddd;
79
+ border-radius: 4px;
80
+ font-size: 16px;
81
+ width: 200px;
82
+ }
83
+ </style>
84
+ </head>
85
+ <body>
86
+ <h1>🔄 yaml-flow Browser Demo</h1>
87
+
88
+ <div class="card">
89
+ <h2>Simple Greeting Flow</h2>
90
+ <p>Enter your name and run the flow to see it in action.</p>
91
+
92
+ <div style="margin: 15px 0;">
93
+ <input type="text" id="nameInput" placeholder="Enter your name" value="Developer">
94
+ <button onclick="runFlow()">Run Flow</button>
95
+ <button onclick="clearLog()">Clear Log</button>
96
+ </div>
97
+
98
+ <h3>Steps</h3>
99
+ <div class="step-list" id="stepList">
100
+ <div class="step" data-step="greet">greet</div>
101
+ <div class="step" data-step="validate">validate</div>
102
+ <div class="step" data-step="personalize">personalize</div>
103
+ <div class="step" data-step="success_state">✓ success</div>
104
+ </div>
105
+ </div>
106
+
107
+ <div class="card">
108
+ <h3>Execution Log</h3>
109
+ <div class="log" id="log"></div>
110
+
111
+ <div id="result"></div>
112
+ </div>
113
+
114
+ <script type="module">
115
+ // Import from CDN (in production) or local build
116
+ // For this demo, we'll inline a simplified version
117
+
118
+ // ============= Inline yaml-flow (simplified for demo) =============
119
+
120
+ class MemoryStore {
121
+ constructor() {
122
+ this.runs = new Map();
123
+ this.data = new Map();
124
+ }
125
+ async saveRunState(runId, state) { this.runs.set(runId, {...state}); }
126
+ async loadRunState(runId) { return this.runs.get(runId) || null; }
127
+ async deleteRunState(runId) { this.runs.delete(runId); this.data.delete(runId); }
128
+ async setData(runId, key, value) {
129
+ if (!this.data.has(runId)) this.data.set(runId, {});
130
+ this.data.get(runId)[key] = value;
131
+ }
132
+ async getData(runId, key) { return this.data.get(runId)?.[key]; }
133
+ async getAllData(runId) { return {...(this.data.get(runId) || {})}; }
134
+ async clearData(runId) { this.data.delete(runId); }
135
+ }
136
+
137
+ class FlowEngine {
138
+ constructor(flow, handlers, options = {}) {
139
+ this.flow = flow;
140
+ this.handlers = handlers;
141
+ this.store = options.store || new MemoryStore();
142
+ this.components = options.components || {};
143
+ this.options = options;
144
+ }
145
+
146
+ async run(initialData = {}) {
147
+ const runId = crypto.randomUUID();
148
+ const startedAt = Date.now();
149
+
150
+ const runState = {
151
+ runId,
152
+ currentStep: this.flow.settings.start_step,
153
+ status: 'running',
154
+ stepHistory: [],
155
+ iterationCounts: {},
156
+ retryCounts: {},
157
+ startedAt,
158
+ updatedAt: startedAt,
159
+ };
160
+
161
+ await this.store.saveRunState(runId, runState);
162
+ for (const [key, value] of Object.entries(initialData)) {
163
+ await this.store.setData(runId, key, value);
164
+ }
165
+
166
+ let iterations = 0;
167
+ const maxSteps = this.flow.settings.max_total_steps || 100;
168
+
169
+ while (iterations < maxSteps) {
170
+ const currentStep = runState.currentStep;
171
+
172
+ // Check terminal state
173
+ const terminalState = this.flow.terminal_states[currentStep];
174
+ if (terminalState) {
175
+ const allData = await this.store.getAllData(runId);
176
+ return {
177
+ runId,
178
+ status: 'completed',
179
+ intent: terminalState.return_intent,
180
+ data: this.extractReturnData(terminalState.return_artifacts, allData),
181
+ finalStep: currentStep,
182
+ stepHistory: runState.stepHistory,
183
+ durationMs: Date.now() - startedAt,
184
+ };
185
+ }
186
+
187
+ const stepConfig = this.flow.steps[currentStep];
188
+ const handler = this.handlers[currentStep];
189
+
190
+ // Build input
191
+ const allData = await this.store.getAllData(runId);
192
+ const input = stepConfig.expects_data
193
+ ? Object.fromEntries(stepConfig.expects_data.map(k => [k, allData[k]]))
194
+ : allData;
195
+
196
+ // Execute
197
+ this.options.onStep?.(currentStep, 'start');
198
+ const result = await handler(input, { runId, stepName: currentStep, components: this.components, store: this.store });
199
+ this.options.onStep?.(currentStep, result.result);
200
+
201
+ // Store output
202
+ if (result.data) {
203
+ for (const [key, value] of Object.entries(result.data)) {
204
+ await this.store.setData(runId, key, value);
205
+ }
206
+ }
207
+
208
+ // Transition
209
+ const nextStep = stepConfig.transitions[result.result];
210
+ this.options.onTransition?.(currentStep, nextStep);
211
+
212
+ runState.stepHistory.push(currentStep);
213
+ runState.currentStep = nextStep;
214
+ iterations++;
215
+ }
216
+
217
+ return { runId, status: 'max_iterations', data: {}, stepHistory: runState.stepHistory };
218
+ }
219
+
220
+ extractReturnData(artifacts, allData) {
221
+ if (!artifacts || artifacts === false) return {};
222
+ if (typeof artifacts === 'string') return { [artifacts]: allData[artifacts] };
223
+ if (Array.isArray(artifacts)) return Object.fromEntries(artifacts.map(k => [k, allData[k]]));
224
+ return allData;
225
+ }
226
+ }
227
+
228
+ // ============= Flow Definition =============
229
+
230
+ const flow = {
231
+ settings: {
232
+ start_step: 'greet',
233
+ max_total_steps: 10
234
+ },
235
+ steps: {
236
+ greet: {
237
+ produces_data: ['greeting', 'user_name'],
238
+ transitions: { success: 'validate', failure: 'error_state' }
239
+ },
240
+ validate: {
241
+ expects_data: ['greeting', 'user_name'],
242
+ produces_data: ['is_valid'],
243
+ transitions: { success: 'personalize', invalid: 'error_state', failure: 'error_state' }
244
+ },
245
+ personalize: {
246
+ expects_data: ['greeting', 'user_name'],
247
+ produces_data: ['final_message'],
248
+ transitions: { success: 'success_state', failure: 'error_state' }
249
+ }
250
+ },
251
+ terminal_states: {
252
+ success_state: { return_intent: 'success', return_artifacts: ['final_message', 'user_name'] },
253
+ error_state: { return_intent: 'error', return_artifacts: false }
254
+ }
255
+ };
256
+
257
+ // ============= Step Handlers =============
258
+
259
+ const handlers = {
260
+ async greet(input) {
261
+ const userName = input.initial_name || 'World';
262
+ await sleep(300); // Simulate async work
263
+ return {
264
+ result: 'success',
265
+ data: { greeting: 'Hello', user_name: userName }
266
+ };
267
+ },
268
+
269
+ async validate(input) {
270
+ const { greeting, user_name } = input;
271
+ await sleep(200);
272
+ const isValid = greeting && user_name;
273
+ return {
274
+ result: isValid ? 'success' : 'invalid',
275
+ data: { is_valid: isValid }
276
+ };
277
+ },
278
+
279
+ async personalize(input) {
280
+ const { greeting, user_name } = input;
281
+ await sleep(200);
282
+ return {
283
+ result: 'success',
284
+ data: { final_message: `${greeting}, ${user_name}! Welcome to yaml-flow in the browser! 🎉` }
285
+ };
286
+ }
287
+ };
288
+
289
+ function sleep(ms) {
290
+ return new Promise(resolve => setTimeout(resolve, ms));
291
+ }
292
+
293
+ // ============= UI Functions =============
294
+
295
+ const logEl = document.getElementById('log');
296
+ const resultEl = document.getElementById('result');
297
+ const stepEls = document.querySelectorAll('.step');
298
+
299
+ function log(message, type = '') {
300
+ const entry = document.createElement('div');
301
+ entry.className = `log-entry ${type}`;
302
+ entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
303
+ logEl.appendChild(entry);
304
+ logEl.scrollTop = logEl.scrollHeight;
305
+ }
306
+
307
+ function updateStep(stepName, status) {
308
+ stepEls.forEach(el => {
309
+ if (el.dataset.step === stepName) {
310
+ el.classList.remove('active', 'completed');
311
+ el.classList.add(status);
312
+ }
313
+ });
314
+ }
315
+
316
+ function resetSteps() {
317
+ stepEls.forEach(el => el.classList.remove('active', 'completed'));
318
+ }
319
+
320
+ window.clearLog = function() {
321
+ logEl.innerHTML = '';
322
+ resultEl.innerHTML = '';
323
+ resetSteps();
324
+ };
325
+
326
+ window.runFlow = async function() {
327
+ resetSteps();
328
+ resultEl.innerHTML = '';
329
+
330
+ const name = document.getElementById('nameInput').value || 'World';
331
+ log(`Starting flow with name: "${name}"`);
332
+
333
+ const engine = new FlowEngine(flow, handlers, {
334
+ store: new MemoryStore(),
335
+ onStep: (step, status) => {
336
+ log(`Step [${step}]: ${status}`);
337
+ updateStep(step, status === 'start' ? 'active' : 'completed');
338
+ },
339
+ onTransition: (from, to) => {
340
+ log(`Transition: ${from} → ${to}`, 'transition');
341
+ }
342
+ });
343
+
344
+ try {
345
+ const result = await engine.run({ initial_name: name });
346
+
347
+ log(`Flow completed: ${result.intent}`);
348
+ updateStep(result.finalStep, 'completed');
349
+
350
+ resultEl.innerHTML = `
351
+ <div class="result ${result.intent === 'success' ? 'success' : 'error'}">
352
+ <strong>Result:</strong> ${result.intent}<br>
353
+ <strong>Message:</strong> ${result.data.final_message || 'N/A'}<br>
354
+ <strong>Duration:</strong> ${result.durationMs}ms<br>
355
+ <strong>Steps:</strong> ${result.stepHistory.join(' → ')} → ${result.finalStep}
356
+ </div>
357
+ `;
358
+ } catch (error) {
359
+ log(`Error: ${error.message}`, 'error');
360
+ resultEl.innerHTML = `<div class="result error">Error: ${error.message}</div>`;
361
+ }
362
+ };
363
+
364
+ log('yaml-flow browser demo loaded. Click "Run Flow" to start.');
365
+ </script>
366
+ </body>
367
+ </html>
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Live Cards → Reactive Graph Example: Board Dashboard
3
+ *
4
+ * Demonstrates `liveCardsToReactiveGraph` — the bridge that converts
5
+ * live card / source JSON definitions into a fully wired ReactiveGraph.
6
+ *
7
+ * This example shows both overloads:
8
+ * 1. Flat array of cards → reactive graph
9
+ * 2. LiveBoard object → reactive graph (with board-level id/settings)
10
+ *
11
+ * Features exercised:
12
+ * - Source nodes with pre-populated state (static data feeds)
13
+ * - Custom sourceHandlers (simulated API fetch)
14
+ * - Card nodes with compute expressions (sum, avg, count, template)
15
+ * - Cross-card data flow via data.requires / data.provides
16
+ * - LiveBoard overload — board.id, board.settings forwarded to GraphConfig
17
+ * - validateReactiveGraph on the resulting graph
18
+ *
19
+ * Run with: npx tsx examples/continuous-event-graph/live-cards-board.ts
20
+ */
21
+
22
+ import {
23
+ liveCardsToReactiveGraph,
24
+ validateReactiveGraph,
25
+ } from '../../src/continuous-event-graph/index.js';
26
+ import type { LiveCard, LiveBoard } from '../../src/continuous-event-graph/index.js';
27
+
28
+ // ============================================================================
29
+ // 1. Flat cards array → Reactive Graph
30
+ // ============================================================================
31
+
32
+ console.log('=== Part 1: Flat cards array ===\n');
33
+
34
+ const cards: LiveCard[] = [
35
+ {
36
+ id: 'market-feed',
37
+ type: 'source',
38
+ meta: { title: 'Live Market Prices' },
39
+ state: { prices: [142.5, 305.8, 89.2, 211.0, 178.3] },
40
+ source: { kind: 'static', bindTo: 'state.prices' },
41
+ },
42
+ {
43
+ id: 'stats',
44
+ type: 'card',
45
+ meta: { title: 'Price Statistics' },
46
+ state: {},
47
+ data: { requires: ['market-feed'] },
48
+ compute: {
49
+ total: { fn: 'sum', input: 'state.market-feed.prices' },
50
+ avg: { fn: 'avg', input: 'state.market-feed.prices' },
51
+ count: { fn: 'count', input: 'state.market-feed.prices' },
52
+ },
53
+ },
54
+ {
55
+ id: 'summary',
56
+ type: 'card',
57
+ meta: { title: 'Summary Label' },
58
+ state: {},
59
+ data: { requires: ['stats'] },
60
+ compute: {
61
+ label: {
62
+ fn: 'template',
63
+ input: 'state.stats',
64
+ format: '{{count}} stocks — total ${{total}}, avg ${{avg}}',
65
+ },
66
+ },
67
+ },
68
+ ];
69
+
70
+ const flatResult = liveCardsToReactiveGraph(cards, {
71
+ });
72
+
73
+ console.log('Graph ID:', flatResult.config.id);
74
+ console.log('Tasks:', Object.keys(flatResult.config.tasks).join(', '));
75
+ console.log('Pushing trigger event...\n');
76
+
77
+ flatResult.graph.push({
78
+ type: 'inject-tokens',
79
+ tokens: [],
80
+ timestamp: new Date().toISOString(),
81
+ });
82
+
83
+ await sleep(1000);
84
+
85
+ const flatState = flatResult.graph.getState();
86
+ for (const [name, task] of Object.entries(flatState.state.tasks)) {
87
+ const hash = task.lastDataHash ? ` (hash: ${task.lastDataHash.slice(0, 8)}…)` : '';
88
+ console.log(` ${name}: ${task.status}${hash}`);
89
+ }
90
+ console.log(` Outputs: [${flatState.state.availableOutputs.join(', ')}]`);
91
+
92
+ flatResult.graph.dispose();
93
+
94
+ // ============================================================================
95
+ // 2. LiveBoard → Reactive Graph
96
+ // ============================================================================
97
+
98
+ console.log('\n=== Part 2: LiveBoard overload ===\n');
99
+
100
+ const board: LiveBoard = {
101
+ id: 'portfolio-board',
102
+ title: 'Portfolio Analytics Dashboard',
103
+ mode: 'board',
104
+ positions: {
105
+ 'equity-feed': { x: 0, y: 0, w: 300, h: 200 },
106
+ 'bond-feed': { x: 320, y: 0, w: 300, h: 200 },
107
+ 'portfolio-mix': { x: 160, y: 240, w: 300, h: 200 },
108
+ 'risk-summary': { x: 160, y: 480, w: 300, h: 200 },
109
+ },
110
+ settings: {
111
+ completion: 'manual',
112
+ },
113
+ nodes: [
114
+ {
115
+ id: 'equity-feed',
116
+ type: 'source',
117
+ meta: { title: 'Equity Prices' },
118
+ state: {},
119
+ source: { kind: 'api', bindTo: 'state.raw', url_template: 'https://api.example.com/equity' },
120
+ },
121
+ {
122
+ id: 'bond-feed',
123
+ type: 'source',
124
+ meta: { title: 'Bond Yields' },
125
+ state: { yields: [3.2, 4.1, 2.8, 5.0] },
126
+ source: { kind: 'static', bindTo: 'state.yields' },
127
+ },
128
+ {
129
+ id: 'portfolio-mix',
130
+ type: 'card',
131
+ meta: { title: 'Portfolio Mix Calculator' },
132
+ state: {},
133
+ data: { requires: ['equity-feed', 'bond-feed'] },
134
+ compute: {
135
+ equity_total: { fn: 'sum', input: 'state.equity-feed.prices' },
136
+ bond_total: { fn: 'sum', input: 'state.bond-feed.yields' },
137
+ },
138
+ },
139
+ {
140
+ id: 'risk-summary',
141
+ type: 'card',
142
+ meta: { title: 'Risk Summary' },
143
+ state: {},
144
+ data: { requires: ['portfolio-mix'] },
145
+ compute: {
146
+ label: {
147
+ fn: 'template',
148
+ input: 'state.portfolio-mix',
149
+ format: 'Equities: ${{equity_total}} | Bonds: {{bond_total}}%',
150
+ },
151
+ },
152
+ },
153
+ ],
154
+ };
155
+
156
+ const boardResult = liveCardsToReactiveGraph(board, {
157
+ // Custom handler for the API-based source — simulates a fetch
158
+ sourceHandlers: {
159
+ 'equity-feed': async ({ callbackToken }) => {
160
+ console.log(' [equity-feed] Simulating API fetch...');
161
+ await sleep(100);
162
+ // Use the graph to resolve — bridge wires this up automatically
163
+ boardResult.graph.resolveCallback(callbackToken, { prices: [155.2, 310.4, 92.1, 220.5] });
164
+ return 'task-initiated' as const;
165
+ },
166
+ },
167
+ });
168
+
169
+ console.log('Board ID:', boardResult.config.id);
170
+ console.log('Completion:', boardResult.config.settings.completion);
171
+ console.log('Tasks:', Object.keys(boardResult.config.tasks).join(', '));
172
+ console.log('Cards in map:', boardResult.cards.size);
173
+ console.log();
174
+
175
+ // Push and run
176
+ boardResult.graph.push({
177
+ type: 'inject-tokens',
178
+ tokens: [],
179
+ timestamp: new Date().toISOString(),
180
+ });
181
+
182
+ await sleep(1500);
183
+
184
+ const boardState = boardResult.graph.getState();
185
+ console.log('Final task states:');
186
+ for (const [name, task] of Object.entries(boardState.state.tasks)) {
187
+ const hash = task.lastDataHash ? ` (hash: ${task.lastDataHash.slice(0, 8)}…)` : '';
188
+ console.log(` ${name}: ${task.status} (executed ${task.executionCount}x)${hash}`);
189
+ }
190
+ console.log(` Outputs: [${boardState.state.availableOutputs.join(', ')}]`);
191
+
192
+ // ============================================================================
193
+ // 3. Validate the reactive graph
194
+ // ============================================================================
195
+
196
+ console.log('\n=== Validation ===');
197
+ const validation = validateReactiveGraph({
198
+ graph: boardResult.graph,
199
+ handlers: boardResult.handlers,
200
+ });
201
+ console.log(` Valid: ${validation.valid} (${validation.issues.length} issues)`);
202
+ for (const issue of validation.issues) {
203
+ console.log(` [${issue.severity}] ${issue.code}: ${issue.message}`);
204
+ }
205
+
206
+ boardResult.graph.dispose();
207
+ console.log('\nDone.');
208
+
209
+ // ============================================================================
210
+ // Util
211
+ // ============================================================================
212
+
213
+ function sleep(ms: number): Promise<void> {
214
+ return new Promise(resolve => setTimeout(resolve, ms));
215
+ }