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,555 @@
1
+ /**
2
+ * Live Portfolio Dashboard — Dynamic Reactive Graph Example
3
+ *
4
+ * A comprehensive example demonstrating:
5
+ * - Live cards on disk → reactive graph bridge (liveCardsToReactiveGraph)
6
+ * - Custom card handlers with cross-card data joins
7
+ * - Dynamic node lifecycle: addNode / removeNode at runtime
8
+ * - Graph wiring mutations: addRequires / removeRequires / addProvides / removeProvides
9
+ * - Multi-round data updates via push / pushAll
10
+ * - retrigger / retriggerAll for manual re-computation
11
+ * - Full disk roundtrip: card state persisted after every drain cycle
12
+ *
13
+ * Graph topology (8 initial cards):
14
+ *
15
+ * holdings ─────┐
16
+ * ├─→ valuator ─→ portfolio-value
17
+ * price-feed ───┘ │
18
+ * └─→ sector-breakdown
19
+ * news-feed ─→ sentiment
20
+ * benchmark ──(standalone)
21
+ *
22
+ * Dynamically added: allocation-chart, risk-score, daily-pnl,
23
+ * value-alert, summary, correlation, combined-view
24
+ *
25
+ * Run with: npx tsx examples/continuous-event-graph/live-portfolio-dashboard.ts
26
+ */
27
+
28
+ import * as fs from 'node:fs';
29
+ import * as path from 'node:path';
30
+ import * as os from 'node:os';
31
+
32
+ import type { LiveCard } from '../../src/continuous-event-graph/live-cards-bridge.js';
33
+ import type { TaskConfig } from '../../src/event-graph/types.js';
34
+ import type { TaskHandlerFn, TaskHandlerInput, ReactiveGraph } from '../../src/continuous-event-graph/reactive.js';
35
+ import { liveCardsToReactiveGraph } from '../../src/continuous-event-graph/live-cards-bridge.js';
36
+
37
+ // ============================================================================
38
+ // Helpers
39
+ // ============================================================================
40
+
41
+ const ts = () => new Date().toISOString();
42
+ const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms));
43
+ const log = (label: string, ...args: unknown[]) => console.log(`\n[${label}]`, ...args);
44
+
45
+ function readCard(dir: string, id: string): LiveCard {
46
+ return JSON.parse(fs.readFileSync(path.join(dir, `${id}.json`), 'utf-8'));
47
+ }
48
+
49
+ function writeCard(dir: string, card: LiveCard): void {
50
+ fs.writeFileSync(path.join(dir, `${card.id}.json`), JSON.stringify(card, null, 2));
51
+ }
52
+
53
+ function printDiskCard(dir: string, id: string) {
54
+ const card = readCard(dir, id);
55
+ const stateKeys = Object.keys(card.state ?? {});
56
+ console.log(` 📄 ${id}.json — keys: [${stateKeys.join(', ')}]`);
57
+ for (const [k, v] of Object.entries(card.state ?? {})) {
58
+ const preview = JSON.stringify(v);
59
+ console.log(` ${k}: ${preview.length > 100 ? preview.slice(0, 100) + '…' : preview}`);
60
+ }
61
+ }
62
+
63
+ // ============================================================================
64
+ // 1. Define the initial 8 live cards
65
+ // ============================================================================
66
+
67
+ const cards: LiveCard[] = [
68
+ // Sources
69
+ {
70
+ id: 'holdings', type: 'source',
71
+ meta: { title: 'Portfolio Holdings' },
72
+ data: { provides: { holdings: 'state.holdings' } },
73
+ state: {
74
+ holdings: [
75
+ { symbol: 'AAPL', shares: 50, sector: 'tech' },
76
+ { symbol: 'MSFT', shares: 30, sector: 'tech' },
77
+ { symbol: 'GOOG', shares: 20, sector: 'tech' },
78
+ { symbol: 'JPM', shares: 40, sector: 'finance' },
79
+ { symbol: 'JNJ', shares: 25, sector: 'healthcare' },
80
+ ],
81
+ },
82
+ },
83
+ {
84
+ id: 'price-feed', type: 'source',
85
+ meta: { title: 'Live Price Feed' },
86
+ data: { provides: { prices: 'state.prices' } },
87
+ state: { prices: { AAPL: 195.50, MSFT: 420.10, GOOG: 176.30, JPM: 198.20, JNJ: 155.80 } },
88
+ },
89
+ {
90
+ id: 'news-feed', type: 'source',
91
+ meta: { title: 'Market News Feed' },
92
+ state: {
93
+ headlines: [
94
+ { symbol: 'AAPL', headline: 'Apple beats Q4 estimates', sentiment: 0.8 },
95
+ { symbol: 'JPM', headline: 'JPMorgan raises dividend', sentiment: 0.6 },
96
+ { symbol: 'JNJ', headline: 'JNJ faces litigation risk', sentiment: -0.4 },
97
+ ],
98
+ },
99
+ },
100
+ {
101
+ id: 'benchmark', type: 'source',
102
+ meta: { title: 'S&P 500 Benchmark' },
103
+ state: { index: 'SPY', value: 5280.50, dailyReturn: 0.45 },
104
+ },
105
+ // Compute cards
106
+ {
107
+ id: 'valuator', type: 'card',
108
+ meta: { title: 'Position Valuator' },
109
+ data: { requires: ['holdings', 'price-feed'] },
110
+ state: {},
111
+ },
112
+ {
113
+ id: 'portfolio-value', type: 'card',
114
+ meta: { title: 'Total Portfolio Value' },
115
+ data: { requires: ['valuator'] },
116
+ state: {},
117
+ },
118
+ {
119
+ id: 'sector-breakdown', type: 'card',
120
+ meta: { title: 'Sector Breakdown' },
121
+ data: { requires: ['valuator'] },
122
+ state: {},
123
+ },
124
+ {
125
+ id: 'sentiment', type: 'card',
126
+ meta: { title: 'News Sentiment Score' },
127
+ data: { requires: ['news-feed'] },
128
+ state: {},
129
+ },
130
+ ];
131
+
132
+ // ============================================================================
133
+ // 2. Handler factory — reads engine state, computes, resolves callback
134
+ // ============================================================================
135
+
136
+ let graphRef: ReactiveGraph | null = null;
137
+
138
+ function makeHandler(
139
+ id: string,
140
+ computeFn: (engine: ReturnType<ReactiveGraph['getState']>) => Record<string, unknown>,
141
+ dir: string,
142
+ ): TaskHandlerFn {
143
+ return async (input: TaskHandlerInput) => {
144
+ const result = computeFn(graphRef!.getState());
145
+ try {
146
+ const diskCard = readCard(dir, id);
147
+ diskCard.state = { ...diskCard.state, ...result };
148
+ writeCard(dir, diskCard);
149
+ } catch { /* card may not exist yet */ }
150
+ graphRef!.resolveCallback(input.callbackToken, result);
151
+ return 'task-initiated';
152
+ };
153
+ }
154
+
155
+ function addDynamicCard(
156
+ rg: ReactiveGraph, dir: string, card: LiveCard,
157
+ computeFn: (engine: ReturnType<ReactiveGraph['getState']>) => Record<string, unknown>,
158
+ taskConfig: { requires?: string[]; provides?: string[] },
159
+ ) {
160
+ writeCard(dir, card);
161
+ rg.registerHandler(card.id, makeHandler(card.id, computeFn, dir));
162
+ rg.addNode(card.id, {
163
+ requires: taskConfig.requires,
164
+ provides: taskConfig.provides ?? [card.id],
165
+ taskHandlers: [card.id],
166
+ } as TaskConfig);
167
+ }
168
+
169
+ // ============================================================================
170
+ // 3. Main — run the full lifecycle
171
+ // ============================================================================
172
+
173
+ async function main() {
174
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'live-portfolio-'));
175
+ console.log('╔════════════════════════════════════════════════════════════╗');
176
+ console.log('║ Live Portfolio Dashboard — Reactive Graph Demo ║');
177
+ console.log('╚════════════════════════════════════════════════════════════╝');
178
+ console.log(`Card directory: ${tmpDir}`);
179
+
180
+ // Write initial cards to disk
181
+ for (const card of cards) writeCard(tmpDir, card);
182
+
183
+ // ── Phase 1: Build reactive graph from live cards ──────────────────────
184
+
185
+ log('PHASE 1', 'Building reactive graph from 8 live cards on disk');
186
+
187
+ const result = liveCardsToReactiveGraph(cards, {
188
+ cardHandlers: {
189
+ valuator: makeHandler('valuator', (engine) => {
190
+ const holdingsList = (engine.state.tasks.holdings?.data as any)?.holdings ?? [];
191
+ const priceMap = (engine.state.tasks['price-feed']?.data as any)?.prices ?? {};
192
+ return {
193
+ positions: holdingsList.map((h: any) => ({
194
+ symbol: h.symbol, shares: h.shares, sector: h.sector,
195
+ price: priceMap[h.symbol] ?? 0,
196
+ value: h.shares * (priceMap[h.symbol] ?? 0),
197
+ })),
198
+ };
199
+ }, tmpDir),
200
+ 'portfolio-value': makeHandler('portfolio-value', (engine) => {
201
+ const positions = (engine.state.tasks.valuator?.data as any)?.positions ?? [];
202
+ const totalValue = positions.reduce((s: number, p: any) => s + p.value, 0);
203
+ return { totalValue, positionCount: positions.length };
204
+ }, tmpDir),
205
+ 'sector-breakdown': makeHandler('sector-breakdown', (engine) => {
206
+ const positions = (engine.state.tasks.valuator?.data as any)?.positions ?? [];
207
+ const bySector: Record<string, number> = {};
208
+ for (const p of positions) bySector[p.sector] = (bySector[p.sector] ?? 0) + p.value;
209
+ const total = positions.reduce((s: number, p: any) => s + p.value, 0);
210
+ const sectors = Object.entries(bySector).map(([sector, value]) => ({
211
+ sector, value, pct: total > 0 ? Math.round(value / total * 10000) / 100 : 0,
212
+ }));
213
+ return { sectors, sectorCount: sectors.length };
214
+ }, tmpDir),
215
+ sentiment: makeHandler('sentiment', (engine) => {
216
+ const headlines = (engine.state.tasks['news-feed']?.data as any)?.headlines ?? [];
217
+ const avg = headlines.length > 0
218
+ ? headlines.reduce((s: number, h: any) => s + (h.sentiment ?? 0), 0) / headlines.length : 0;
219
+ return { avgSentiment: Math.round(avg * 100) / 100, headlineCount: headlines.length, bullish: avg > 0.2 };
220
+ }, tmpDir),
221
+ },
222
+ reactiveOptions: {
223
+ onDrain: (_events, live) => {
224
+ for (const [taskName, taskState] of Object.entries(live.state.tasks)) {
225
+ if (taskState.data && Object.keys(taskState.data).length > 0) {
226
+ try {
227
+ const diskCard = readCard(tmpDir, taskName);
228
+ diskCard.state = { ...diskCard.state, ...taskState.data };
229
+ writeCard(tmpDir, diskCard);
230
+ } catch { /* not on disk yet */ }
231
+ }
232
+ }
233
+ },
234
+ },
235
+ });
236
+
237
+ graphRef = result.graph;
238
+ const rg = result.graph;
239
+
240
+ // Kick off the initial cascade
241
+ rg.push({ type: 'inject-tokens', tokens: [], timestamp: ts() });
242
+ await sleep(200);
243
+
244
+ log('PHASE 1 RESULT', `${Object.keys(rg.getState().state.tasks).length} tasks completed`);
245
+ printDiskCard(tmpDir, 'portfolio-value');
246
+ printDiskCard(tmpDir, 'sector-breakdown');
247
+ printDiskCard(tmpDir, 'sentiment');
248
+
249
+ // ── Phase 2: Dynamically grow to 15 cards ──────────────────────────────
250
+
251
+ log('PHASE 2', 'Adding 7 dynamic cards → 15 total');
252
+
253
+ addDynamicCard(rg, tmpDir, {
254
+ id: 'allocation-chart', type: 'card', data: { requires: ['valuator', 'portfolio-value'] }, state: {},
255
+ }, (e) => {
256
+ const pos = (e.state.tasks.valuator?.data as any)?.positions ?? [];
257
+ const tot = (e.state.tasks['portfolio-value']?.data as any)?.totalValue ?? 0;
258
+ return { allocations: pos.map((p: any) => ({ sym: p.symbol, pct: tot > 0 ? Math.round(p.value / tot * 10000) / 100 : 0 })) };
259
+ }, { requires: ['valuator', 'portfolio-value'] });
260
+
261
+ addDynamicCard(rg, tmpDir, {
262
+ id: 'risk-score', type: 'card', data: { requires: ['valuator'] }, state: {},
263
+ }, (e) => {
264
+ const pos = (e.state.tasks.valuator?.data as any)?.positions ?? [];
265
+ const vals = pos.map((p: any) => p.value);
266
+ const total = vals.reduce((s: number, v: number) => s + v, 0);
267
+ const maxConc = total > 0 ? Math.max(...vals) / total : 0;
268
+ return { maxConcentration: Math.round(maxConc * 100) / 100, riskLevel: maxConc > 0.5 ? 'high' : maxConc > 0.3 ? 'medium' : 'low' };
269
+ }, { requires: ['valuator'] });
270
+
271
+ addDynamicCard(rg, tmpDir, {
272
+ id: 'daily-pnl', type: 'card', data: { requires: ['portfolio-value', 'benchmark'] }, state: {},
273
+ }, (e) => {
274
+ const tv = (e.state.tasks['portfolio-value']?.data as any)?.totalValue ?? 0;
275
+ const benchReturn = (e.state.tasks.benchmark?.data as any)?.dailyReturn ?? 0;
276
+ const portfolioReturn = 1.2;
277
+ return { pnl: Math.round(tv * (portfolioReturn / 100) * 100) / 100, alpha: Math.round((portfolioReturn - benchReturn) * 100) / 100 };
278
+ }, { requires: ['portfolio-value', 'benchmark'] });
279
+
280
+ addDynamicCard(rg, tmpDir, {
281
+ id: 'value-alert', type: 'card', data: { requires: ['portfolio-value'] }, state: {},
282
+ }, (e) => {
283
+ const tv = (e.state.tasks['portfolio-value']?.data as any)?.totalValue ?? 0;
284
+ return { triggered: tv > 25000, threshold: 25000, currentValue: tv };
285
+ }, { requires: ['portfolio-value'] });
286
+
287
+ addDynamicCard(rg, tmpDir, {
288
+ id: 'summary', type: 'card', data: { requires: ['portfolio-value', 'sentiment'] }, state: {},
289
+ }, (e) => {
290
+ const tv = (e.state.tasks['portfolio-value']?.data as any)?.totalValue ?? 0;
291
+ const mood = (e.state.tasks.sentiment?.data as any)?.bullish ? 'bullish' : 'bearish';
292
+ return { text: `Portfolio: $${tv.toFixed(2)} — Market: ${mood}`, totalValue: tv, mood };
293
+ }, { requires: ['portfolio-value', 'sentiment'] });
294
+
295
+ addDynamicCard(rg, tmpDir, {
296
+ id: 'correlation', type: 'card', data: { requires: ['valuator', 'benchmark'] }, state: {},
297
+ }, (e) => {
298
+ const pos = (e.state.tasks.valuator?.data as any)?.positions ?? [];
299
+ const techVal = pos.filter((p: any) => p.sector === 'tech').reduce((s: number, p: any) => s + p.value, 0);
300
+ const total = pos.reduce((s: number, p: any) => s + p.value, 0);
301
+ return { techWeight: total > 0 ? Math.round(techVal / total * 100) / 100 : 0 };
302
+ }, { requires: ['valuator', 'benchmark'] });
303
+
304
+ addDynamicCard(rg, tmpDir, {
305
+ id: 'combined-view', type: 'card', data: { requires: ['summary', 'sector-breakdown', 'risk-score'] }, state: {},
306
+ }, (e) => ({
307
+ summaryMood: (e.state.tasks.summary?.data as any)?.mood ?? '?',
308
+ sectors: (e.state.tasks['sector-breakdown']?.data as any)?.sectorCount ?? 0,
309
+ risk: (e.state.tasks['risk-score']?.data as any)?.riskLevel ?? 'unknown',
310
+ ready: true,
311
+ }), { requires: ['summary', 'sector-breakdown', 'risk-score'] });
312
+
313
+ await sleep(300);
314
+
315
+ const taskCount = Object.keys(rg.getState().state.tasks).length;
316
+ const allCompleted = Object.values(rg.getState().state.tasks).every(t => t.status === 'completed');
317
+ log('PHASE 2 RESULT', `${taskCount} cards, all completed: ${allCompleted}`);
318
+ printDiskCard(tmpDir, 'combined-view');
319
+ printDiskCard(tmpDir, 'risk-score');
320
+
321
+ // ── Phase 3: addProvides — create a new token ──────────────────────────
322
+
323
+ log('PHASE 3', 'addProvides: benchmark now also produces "market-data" token');
324
+
325
+ rg.addProvides('benchmark', ['market-data']);
326
+ console.log(' benchmark provides:', rg.getState().config.tasks.benchmark.provides);
327
+
328
+ addDynamicCard(rg, tmpDir, {
329
+ id: 'market-context', type: 'card', data: { requires: ['market-data'] }, state: {},
330
+ }, (e) => {
331
+ const bench = e.state.tasks.benchmark?.data as any ?? {};
332
+ return { indexValue: bench.value ?? 0, context: 'provided via market-data token' };
333
+ }, { requires: ['market-data'] });
334
+ await sleep(100);
335
+
336
+ log('PHASE 3 RESULT', `${Object.keys(rg.getState().state.tasks).length} cards`);
337
+ printDiskCard(tmpDir, 'market-context');
338
+
339
+ // ── Phase 4: addRequires — wire sentiment into combined-view ───────────
340
+
341
+ log('PHASE 4', 'addRequires: wiring news-feed into combined-view');
342
+
343
+ const requiresBefore = [...(rg.getState().config.tasks['combined-view'].requires ?? [])];
344
+ rg.addRequires('combined-view', ['news-feed']);
345
+ const requiresAfter = rg.getState().config.tasks['combined-view'].requires ?? [];
346
+ console.log(` combined-view requires: ${requiresBefore.join(', ')} → ${requiresAfter.join(', ')}`);
347
+
348
+ // ── Phase 5: removeProvides + removeNode ───────────────────────────────
349
+
350
+ log('PHASE 5', 'removeProvides "market-data" from benchmark, then remove 2 nodes');
351
+
352
+ rg.removeProvides('benchmark', ['market-data']);
353
+ console.log(' benchmark provides after removal:', rg.getState().config.tasks.benchmark.provides);
354
+
355
+ rg.removeNode('allocation-chart');
356
+ rg.removeNode('market-context');
357
+ console.log(` Remaining cards: ${Object.keys(rg.getState().state.tasks).length}`);
358
+
359
+ // ── Phase 6: Push updated prices → full cascade ────────────────────────
360
+
361
+ log('PHASE 6', 'Pushing round-2 prices: AAPL 201, MSFT 418.75, GOOG 180.50');
362
+
363
+ rg.push({
364
+ type: 'task-completed', taskName: 'price-feed',
365
+ data: { prices: { AAPL: 201.00, MSFT: 418.75, GOOG: 180.50, JPM: 202.10, JNJ: 153.40 } },
366
+ timestamp: ts(),
367
+ });
368
+ await sleep(200);
369
+
370
+ printDiskCard(tmpDir, 'portfolio-value');
371
+ printDiskCard(tmpDir, 'daily-pnl');
372
+
373
+ // ── Phase 7: pushAll — new holdings (add TSLA) + round-3 prices ────────
374
+
375
+ log('PHASE 7', 'pushAll: adding TSLA holding + round-3 prices');
376
+
377
+ rg.pushAll([
378
+ {
379
+ type: 'task-completed', taskName: 'holdings',
380
+ data: {
381
+ holdings: [
382
+ { symbol: 'AAPL', shares: 50, sector: 'tech' },
383
+ { symbol: 'MSFT', shares: 30, sector: 'tech' },
384
+ { symbol: 'GOOG', shares: 20, sector: 'tech' },
385
+ { symbol: 'JPM', shares: 40, sector: 'finance' },
386
+ { symbol: 'JNJ', shares: 25, sector: 'healthcare' },
387
+ { symbol: 'TSLA', shares: 10, sector: 'tech' },
388
+ ],
389
+ },
390
+ timestamp: ts(),
391
+ },
392
+ {
393
+ type: 'task-completed', taskName: 'price-feed',
394
+ data: { prices: { AAPL: 205.25, MSFT: 422.00, GOOG: 182.10, JPM: 199.80, JNJ: 157.10, TSLA: 168.75 } },
395
+ timestamp: ts(),
396
+ },
397
+ ]);
398
+ await sleep(200);
399
+
400
+ printDiskCard(tmpDir, 'portfolio-value');
401
+ printDiskCard(tmpDir, 'sector-breakdown');
402
+ printDiskCard(tmpDir, 'risk-score');
403
+
404
+ // ── Phase 8: removeRequires + re-add node with different wiring ────────
405
+
406
+ log('PHASE 8', 'removeRequires news-feed from combined-view, re-add allocation-chart (simpler wiring)');
407
+
408
+ rg.removeRequires('combined-view', ['news-feed']);
409
+ console.log(' combined-view requires:', rg.getState().config.tasks['combined-view'].requires);
410
+
411
+ addDynamicCard(rg, tmpDir, {
412
+ id: 'allocation-chart', type: 'card', data: { requires: ['valuator'] }, state: {},
413
+ }, (e) => {
414
+ const pos = (e.state.tasks.valuator?.data as any)?.positions ?? [];
415
+ const total = pos.reduce((s: number, p: any) => s + p.value, 0);
416
+ return { allocations: pos.map((p: any) => ({ sym: p.symbol, pct: total > 0 ? Math.round(p.value / total * 10000) / 100 : 0 })) };
417
+ }, { requires: ['valuator'] });
418
+ await sleep(100);
419
+
420
+ printDiskCard(tmpDir, 'allocation-chart');
421
+
422
+ // ── Phase 9: retriggerAll — bulk refresh ───────────────────────────────
423
+
424
+ log('PHASE 9', 'retriggerAll: refreshing the full computation pipeline');
425
+
426
+ rg.retriggerAll(['valuator', 'portfolio-value', 'sector-breakdown', 'sentiment']);
427
+ await sleep(200);
428
+
429
+ // ── Phase 10: Watchlist — form input source + derived price card ────────
430
+
431
+ log('PHASE 10', 'Adding watchlist (form input) + watchlist-prices (derived)');
432
+
433
+ // Watchlist is a "source" card — it has no handler, data is pushed externally
434
+ // (simulating a user typing symbols into a form)
435
+ const watchlistCard: LiveCard = {
436
+ id: 'watchlist', type: 'source',
437
+ meta: { title: 'Watchlist (User Input)' },
438
+ data: { provides: { watchlist: 'state.symbols' } },
439
+ state: { symbols: ['NVDA', 'AMD', 'AMZN'] },
440
+ };
441
+ writeCard(tmpDir, watchlistCard);
442
+ rg.registerHandler('watchlist', makeHandler('watchlist', (engine) => {
443
+ // Source handler just returns current state — data comes via external push
444
+ return {};
445
+ }, tmpDir));
446
+ rg.addNode('watchlist', {
447
+ provides: ['watchlist'],
448
+ taskHandlers: ['watchlist'],
449
+ } as TaskConfig);
450
+
451
+ // Push the initial watchlist data (simulating form submission)
452
+ rg.push({
453
+ type: 'task-completed', taskName: 'watchlist',
454
+ data: { symbols: ['NVDA', 'AMD', 'AMZN'] },
455
+ timestamp: ts(),
456
+ });
457
+ await sleep(100);
458
+
459
+ console.log(' watchlist submitted: NVDA, AMD, AMZN');
460
+
461
+ // Watchlist-prices card — reads the watchlist symbols and the price-feed,
462
+ // then extracts the latest prices for watched symbols
463
+ addDynamicCard(rg, tmpDir, {
464
+ id: 'watchlist-prices', type: 'card',
465
+ meta: { title: 'Watchlist Latest Prices' },
466
+ data: { requires: ['watchlist', 'price-feed'] }, state: {},
467
+ }, (engine) => {
468
+ const symbols: string[] = (engine.state.tasks.watchlist?.data as any)?.symbols ?? [];
469
+ const allPrices: Record<string, number> = (engine.state.tasks['price-feed']?.data as any)?.prices ?? {};
470
+ const watchPrices = symbols.map(sym => ({
471
+ symbol: sym,
472
+ price: allPrices[sym] ?? null,
473
+ available: sym in allPrices,
474
+ }));
475
+ const available = watchPrices.filter(w => w.available);
476
+ const missing = watchPrices.filter(w => !w.available);
477
+ return {
478
+ watchPrices,
479
+ availableCount: available.length,
480
+ missingCount: missing.length,
481
+ missingSymbols: missing.map(m => m.symbol),
482
+ };
483
+ }, { requires: ['watchlist', 'price-feed'] });
484
+ await sleep(100);
485
+
486
+ printDiskCard(tmpDir, 'watchlist-prices');
487
+
488
+ // Now push updated prices that include the watchlist symbols
489
+ log('PHASE 10b', 'Pushing prices that include NVDA, AMD, AMZN');
490
+ rg.push({
491
+ type: 'task-completed', taskName: 'price-feed',
492
+ data: { prices: {
493
+ AAPL: 205.25, MSFT: 422.00, GOOG: 182.10, JPM: 199.80, JNJ: 157.10, TSLA: 168.75,
494
+ NVDA: 135.50, AMD: 164.20, AMZN: 192.80,
495
+ }},
496
+ timestamp: ts(),
497
+ });
498
+ await sleep(200);
499
+
500
+ console.log(' After prices include watchlist symbols:');
501
+ printDiskCard(tmpDir, 'watchlist-prices');
502
+
503
+ // User updates watchlist (adds META, removes AMD)
504
+ log('PHASE 10c', 'User edits watchlist: [NVDA, AMZN, META]');
505
+ rg.push({
506
+ type: 'task-completed', taskName: 'watchlist',
507
+ data: { symbols: ['NVDA', 'AMZN', 'META'] },
508
+ timestamp: ts(),
509
+ });
510
+ await sleep(200);
511
+
512
+ printDiskCard(tmpDir, 'watchlist-prices');
513
+
514
+ const finalState = rg.getState();
515
+ const completedCount = Object.values(finalState.state.tasks).filter(t => t.status === 'completed').length;
516
+ const totalCards = Object.keys(finalState.state.tasks).length;
517
+
518
+ // ── Final summary ──────────────────────────────────────────────────────
519
+
520
+ console.log('\n╔════════════════════════════════════════════════════════════╗');
521
+ console.log('║ Final Dashboard ║');
522
+ console.log('╚════════════════════════════════════════════════════════════╝');
523
+ console.log(` Total cards: ${totalCards}`);
524
+ console.log(` Completed: ${completedCount}/${totalCards}`);
525
+ console.log(` Card directory: ${tmpDir}`);
526
+ console.log('\n Active cards:');
527
+ for (const name of Object.keys(finalState.state.tasks).sort()) {
528
+ const t = finalState.state.tasks[name];
529
+ const dataKeys = t.data ? Object.keys(t.data).join(', ') : '(no data)';
530
+ console.log(` ${t.status === 'completed' ? '✅' : '⏳'} ${name} — ${dataKeys}`);
531
+ }
532
+
533
+ console.log('\n Disk roundtrip verification:');
534
+ for (const name of Object.keys(finalState.state.tasks).sort()) {
535
+ const exists = fs.existsSync(path.join(tmpDir, `${name}.json`));
536
+ console.log(` ${exists ? '💾' : '❌'} ${name}.json`);
537
+ }
538
+
539
+ // ── Schedule info ──────────────────────────────────────────────────────
540
+
541
+ const sched = rg.getSchedule();
542
+ console.log(`\n Schedule: ${sched.eligible.length} eligible, ${sched.blocked.length} blocked, ${sched.unresolved.length} unresolved`);
543
+
544
+ // ── Journal stats ──────────────────────────────────────────────────────
545
+
546
+ const journal = rg.getState().journal ?? [];
547
+ console.log(` Journal: ${journal.length} events recorded`);
548
+
549
+ // Cleanup
550
+ rg.dispose();
551
+ fs.rmSync(tmpDir, { recursive: true, force: true });
552
+ console.log('\n Cleaned up. Done.');
553
+ }
554
+
555
+ main().catch(console.error);