yaml-flow 3.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +81 -20
  2. package/board-live-cards-cli.js +37 -0
  3. package/browser/card-compute.js +132 -431
  4. package/browser/live-cards.js +41 -27
  5. package/browser/live-cards.schema.json +59 -77
  6. package/dist/card-compute/index.cjs +135 -415
  7. package/dist/card-compute/index.cjs.map +1 -1
  8. package/dist/card-compute/index.d.cts +52 -49
  9. package/dist/card-compute/index.d.ts +52 -49
  10. package/dist/card-compute/index.js +134 -415
  11. package/dist/card-compute/index.js.map +1 -1
  12. package/dist/cli/board-live-cards-cli.cjs +2379 -0
  13. package/dist/cli/board-live-cards-cli.cjs.map +1 -0
  14. package/dist/cli/board-live-cards-cli.d.cts +213 -0
  15. package/dist/cli/board-live-cards-cli.d.ts +213 -0
  16. package/dist/cli/board-live-cards-cli.js +2332 -0
  17. package/dist/cli/board-live-cards-cli.js.map +1 -0
  18. package/dist/{constants-B2zqu10b.d.ts → constants-DuzE5n03.d.ts} +2 -2
  19. package/dist/{constants-DJZU1pwJ.d.cts → constants-ozjf1Ejw.d.cts} +2 -2
  20. package/dist/continuous-event-graph/index.cjs +201 -448
  21. package/dist/continuous-event-graph/index.cjs.map +1 -1
  22. package/dist/continuous-event-graph/index.d.cts +16 -340
  23. package/dist/continuous-event-graph/index.d.ts +16 -340
  24. package/dist/continuous-event-graph/index.js +198 -448
  25. package/dist/continuous-event-graph/index.js.map +1 -1
  26. package/dist/event-graph/index.cjs +4 -4
  27. package/dist/event-graph/index.cjs.map +1 -1
  28. package/dist/event-graph/index.d.cts +5 -5
  29. package/dist/event-graph/index.d.ts +5 -5
  30. package/dist/event-graph/index.js +4 -4
  31. package/dist/event-graph/index.js.map +1 -1
  32. package/dist/index.cjs +278 -533
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +8 -7
  35. package/dist/index.d.ts +8 -7
  36. package/dist/index.js +278 -533
  37. package/dist/index.js.map +1 -1
  38. package/dist/inference/index.cjs +138 -19
  39. package/dist/inference/index.cjs.map +1 -1
  40. package/dist/inference/index.d.cts +2 -2
  41. package/dist/inference/index.d.ts +2 -2
  42. package/dist/inference/index.js +138 -19
  43. package/dist/inference/index.js.map +1 -1
  44. package/dist/journal-BJDjWb5Q.d.cts +343 -0
  45. package/dist/journal-B_2JnBMF.d.ts +343 -0
  46. package/dist/step-machine/index.cjs +18 -1
  47. package/dist/step-machine/index.cjs.map +1 -1
  48. package/dist/step-machine/index.d.cts +2 -2
  49. package/dist/step-machine/index.d.ts +2 -2
  50. package/dist/step-machine/index.js +18 -1
  51. package/dist/step-machine/index.js.map +1 -1
  52. package/dist/stores/file.d.cts +1 -1
  53. package/dist/stores/file.d.ts +1 -1
  54. package/dist/stores/index.d.cts +1 -1
  55. package/dist/stores/index.d.ts +1 -1
  56. package/dist/stores/localStorage.d.cts +1 -1
  57. package/dist/stores/localStorage.d.ts +1 -1
  58. package/dist/stores/memory.d.cts +1 -1
  59. package/dist/stores/memory.d.ts +1 -1
  60. package/dist/{types-BwvgvlOO.d.cts → types-BzLD8bjb.d.cts} +1 -1
  61. package/dist/{types-ClRA8hzC.d.ts → types-C2eJ7DAV.d.ts} +1 -1
  62. package/dist/{types-DEj7OakX.d.cts → types-CMFSIjpc.d.cts} +39 -4
  63. package/dist/{types-DEj7OakX.d.ts → types-CMFSIjpc.d.ts} +39 -4
  64. package/dist/{types-FZ_eyErS.d.cts → types-ycun84cq.d.cts} +1 -0
  65. package/dist/{types-FZ_eyErS.d.ts → types-ycun84cq.d.ts} +1 -0
  66. package/dist/{validate-DEZ2Ymdb.d.ts → validate-DJQTQ6bP.d.ts} +1 -1
  67. package/dist/{validate-DqKTZg_o.d.cts → validate-ke92Cleg.d.cts} +1 -1
  68. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +22 -0
  69. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +16 -0
  70. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +15 -0
  71. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +15 -0
  72. package/examples/browser/boards/portfolio-tracker/fetch-prices.js +43 -0
  73. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +7 -0
  74. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +189 -0
  75. package/examples/browser/livecards-browser/index.html +688 -0
  76. package/examples/browser/step-machine-browser/index.html +367 -0
  77. package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
  78. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
  79. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
  80. package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
  81. package/examples/cli/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
  82. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
  83. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
  84. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
  85. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
  86. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
  87. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
  88. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
  89. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
  90. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
  91. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
  92. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
  93. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
  94. package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +36 -0
  95. package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +30 -0
  96. package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +19 -0
  97. package/examples/cli/step-machine-demo/step-cli-echo-y.js +15 -0
  98. package/examples/cli/step-machine-demo/step2-double-cli.js +39 -0
  99. package/examples/cli/step-machine-demo/two-step-math-handlers.js +32 -0
  100. package/examples/cli/step-machine-demo/two-step-math.flow.yaml +31 -0
  101. package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +24 -0
  102. package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +35 -0
  103. package/examples/index.html +792 -0
  104. package/examples/ingest.js +733 -0
  105. package/examples/npm-libs/batch/batch-step-machine.ts +121 -0
  106. package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +215 -0
  107. package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +555 -0
  108. package/examples/npm-libs/continuous-event-graph/portfolio-tracker.ts +287 -0
  109. package/examples/npm-libs/continuous-event-graph/reactive-monitoring.ts +265 -0
  110. package/examples/npm-libs/continuous-event-graph/reactive-pipeline.ts +168 -0
  111. package/examples/npm-libs/continuous-event-graph/soc-incident-board.ts +287 -0
  112. package/examples/npm-libs/continuous-event-graph/stock-dashboard.ts +229 -0
  113. package/examples/npm-libs/event-graph/ci-cd-pipeline.ts +243 -0
  114. package/examples/npm-libs/event-graph/executor-diamond.ts +165 -0
  115. package/examples/npm-libs/event-graph/executor-pipeline.ts +161 -0
  116. package/examples/npm-libs/event-graph/research-pipeline.ts +137 -0
  117. package/examples/npm-libs/flows/ai-conversation.yaml +116 -0
  118. package/examples/npm-libs/flows/order-processing.yaml +143 -0
  119. package/examples/npm-libs/flows/simple-greeting.yaml +54 -0
  120. package/examples/npm-libs/graph-of-graphs/multi-stage-etl.ts +307 -0
  121. package/examples/npm-libs/graph-of-graphs/url-processing-pipeline.ts +254 -0
  122. package/examples/npm-libs/inference/azure-deployment.ts +149 -0
  123. package/examples/npm-libs/inference/copilot-cli.ts +138 -0
  124. package/examples/npm-libs/inference/data-pipeline.ts +145 -0
  125. package/examples/npm-libs/inference/pluggable-adapters.ts +254 -0
  126. package/examples/npm-libs/node/ai-conversation.ts +195 -0
  127. package/examples/npm-libs/node/simple-greeting.ts +101 -0
  128. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
  129. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
  130. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
  131. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
  132. package/examples/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
  133. package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
  134. package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
  135. package/examples/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
  136. package/examples/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
  137. package/examples/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
  138. package/examples/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
  139. package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
  140. package/examples/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
  141. package/examples/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
  142. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
  143. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
  144. package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
  145. package/package.json +14 -2
  146. package/schema/board-status.schema.json +118 -0
  147. package/schema/flow.schema.json +5 -0
  148. package/schema/live-cards.schema.json +59 -77
  149. package/step-machine-cli.js +674 -0
@@ -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/npm-libs/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,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/npm-libs/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
+ }