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.
- package/README.md +44 -23
- package/dist/{constants-B_ftYTTE.d.ts → constants-B2zqu10b.d.ts} +7 -57
- package/dist/{constants-CiyHX8L-.d.cts → constants-DJZU1pwJ.d.cts} +7 -57
- package/dist/continuous-event-graph/index.cjs +1161 -182
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +567 -48
- package/dist/continuous-event-graph/index.d.ts +567 -48
- package/dist/continuous-event-graph/index.js +1151 -183
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +35 -11
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.d.cts +14 -5
- package/dist/event-graph/index.d.ts +14 -5
- package/dist/event-graph/index.js +34 -11
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +945 -414
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +936 -415
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +31 -7
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.d.cts +2 -2
- package/dist/inference/index.d.ts +2 -2
- package/dist/inference/index.js +31 -7
- package/dist/inference/index.js.map +1 -1
- package/dist/{types-CxJg9Jrt.d.cts → types-BwvgvlOO.d.cts} +2 -2
- package/dist/{types-BuEo3wVG.d.ts → types-ClRA8hzC.d.ts} +2 -2
- package/dist/{types-BpWrH1sf.d.cts → types-DEj7OakX.d.cts} +14 -4
- package/dist/{types-BpWrH1sf.d.ts → types-DEj7OakX.d.ts} +14 -4
- package/dist/validate-DEZ2Ymdb.d.ts +53 -0
- package/dist/validate-DqKTZg_o.d.cts +53 -0
- package/examples/batch/batch-step-machine.ts +121 -0
- package/examples/browser/index.html +367 -0
- package/examples/continuous-event-graph/live-cards-board.ts +215 -0
- package/examples/continuous-event-graph/live-portfolio-dashboard.ts +555 -0
- package/examples/continuous-event-graph/portfolio-tracker.ts +287 -0
- package/examples/continuous-event-graph/reactive-monitoring.ts +265 -0
- package/examples/continuous-event-graph/reactive-pipeline.ts +168 -0
- package/examples/continuous-event-graph/soc-incident-board.ts +287 -0
- package/examples/continuous-event-graph/stock-dashboard.ts +229 -0
- package/examples/event-graph/ci-cd-pipeline.ts +243 -0
- package/examples/event-graph/executor-diamond.ts +165 -0
- package/examples/event-graph/executor-pipeline.ts +161 -0
- package/examples/event-graph/research-pipeline.ts +137 -0
- package/examples/flows/ai-conversation.yaml +116 -0
- package/examples/flows/order-processing.yaml +143 -0
- package/examples/flows/simple-greeting.yaml +54 -0
- package/examples/graph-of-graphs/multi-stage-etl.ts +307 -0
- package/examples/graph-of-graphs/url-processing-pipeline.ts +254 -0
- package/examples/inference/azure-deployment.ts +149 -0
- package/examples/inference/copilot-cli.ts +138 -0
- package/examples/inference/data-pipeline.ts +145 -0
- package/examples/inference/pluggable-adapters.ts +254 -0
- package/examples/ingest.js +733 -0
- package/examples/node/ai-conversation.ts +195 -0
- package/examples/node/simple-greeting.ts +101 -0
- 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);
|