yaml-flow 5.0.0 → 5.2.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 (105) hide show
  1. package/{examples/example-board/reusable-server-runtime.js → board-livecards-server-runtime.js} +103 -24
  2. package/{examples/example-board/reusable-board-runtime-client.js → browser/board-livecards-runtime-client.js} +6 -2
  3. package/browser/{board-livegraph-runtime.js → board-livegraph-engine.js} +212 -16
  4. package/browser/board-livegraph-engine.js.map +1 -0
  5. package/browser/live-cards.js +362 -38
  6. package/browser/live-cards.schema.json +20 -4
  7. package/dist/board-livegraph-runtime/index.cjs +210 -14
  8. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  9. package/dist/board-livegraph-runtime/index.d.cts +49 -5
  10. package/dist/board-livegraph-runtime/index.d.ts +49 -5
  11. package/dist/board-livegraph-runtime/index.js +209 -15
  12. package/dist/board-livegraph-runtime/index.js.map +1 -1
  13. package/dist/card-compute/index.cjs +63 -7
  14. package/dist/card-compute/index.cjs.map +1 -1
  15. package/dist/card-compute/index.d.cts +2 -2
  16. package/dist/card-compute/index.d.ts +2 -2
  17. package/dist/card-compute/index.js +63 -7
  18. package/dist/card-compute/index.js.map +1 -1
  19. package/dist/cli/board-live-cards-cli.cjs +664 -75
  20. package/dist/cli/board-live-cards-cli.cjs.map +1 -1
  21. package/dist/cli/board-live-cards-cli.d.cts +33 -5
  22. package/dist/cli/board-live-cards-cli.d.ts +33 -5
  23. package/dist/cli/board-live-cards-cli.js +661 -76
  24. package/dist/cli/board-live-cards-cli.js.map +1 -1
  25. package/dist/{constants-ozjf1Ejw.d.cts → constants-BzZUyYlp.d.cts} +1 -1
  26. package/dist/{constants-DuzE5n03.d.ts → constants-oCEbNpul.d.ts} +1 -1
  27. package/dist/continuous-event-graph/index.cjs +47 -14
  28. package/dist/continuous-event-graph/index.cjs.map +1 -1
  29. package/dist/continuous-event-graph/index.d.cts +9 -9
  30. package/dist/continuous-event-graph/index.d.ts +9 -9
  31. package/dist/continuous-event-graph/index.js +47 -14
  32. package/dist/continuous-event-graph/index.js.map +1 -1
  33. package/dist/event-graph/index.cjs +29 -12
  34. package/dist/event-graph/index.cjs.map +1 -1
  35. package/dist/event-graph/index.d.cts +5 -5
  36. package/dist/event-graph/index.d.ts +5 -5
  37. package/dist/event-graph/index.js +29 -12
  38. package/dist/event-graph/index.js.map +1 -1
  39. package/dist/index.cjs +93 -20
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +7 -7
  42. package/dist/index.d.ts +7 -7
  43. package/dist/index.js +93 -20
  44. package/dist/index.js.map +1 -1
  45. package/dist/inference/index.cjs +29 -12
  46. package/dist/inference/index.cjs.map +1 -1
  47. package/dist/inference/index.d.cts +2 -2
  48. package/dist/inference/index.d.ts +2 -2
  49. package/dist/inference/index.js +29 -12
  50. package/dist/inference/index.js.map +1 -1
  51. package/dist/{journal-NLYuqege.d.ts → journal-9HEgs7dU.d.ts} +1 -1
  52. package/dist/{journal-DRfJiheM.d.cts → journal-B-JCfQnh.d.cts} +1 -1
  53. package/dist/{live-cards-bridge-Or7fdEJV.d.ts → live-cards-bridge-CeNxiVcm.d.ts} +6 -2
  54. package/dist/{live-cards-bridge-vGJ6tMzN.d.cts → live-cards-bridge-z_rJCSbi.d.cts} +6 -2
  55. package/dist/{schedule-CMcZe5Ny.d.ts → schedule-Cszq9LYY.d.ts} +1 -1
  56. package/dist/{schedule-CiucyCan.d.cts → schedule-qWNL0RQh.d.cts} +1 -1
  57. package/dist/{types-CMFSIjpc.d.cts → types-BBhqYGhE.d.cts} +4 -0
  58. package/dist/{types-CMFSIjpc.d.ts → types-BBhqYGhE.d.ts} +4 -0
  59. package/dist/{types-BzLD8bjb.d.cts → types-CHSdoAAA.d.cts} +1 -1
  60. package/dist/{types-C2eJ7DAV.d.ts → types-CoW0gQl3.d.ts} +1 -1
  61. package/dist/{validate-DJQTQ6bP.d.ts → validate-BAVzUJWa.d.ts} +1 -1
  62. package/dist/{validate-ke92Cleg.d.cts → validate-Dbu7ygys.d.cts} +1 -1
  63. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +28 -0
  64. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +28 -0
  65. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +187 -0
  66. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +139 -5
  67. package/examples/example-board/agent-instructions-cardlayout.md +28 -0
  68. package/examples/example-board/agent-instructions.md +603 -0
  69. package/examples/example-board/cards/card-concentration.json +42 -0
  70. package/examples/example-board/cards/card-market-prices.json +51 -0
  71. package/examples/example-board/cards/card-portfolio-action.json +19 -0
  72. package/examples/example-board/cards/card-portfolio-risks.json +19 -0
  73. package/examples/example-board/cards/card-portfolio-value.json +62 -0
  74. package/examples/example-board/cards/card-portfolio.json +44 -0
  75. package/examples/example-board/demo-chat-handler.js +373 -33
  76. package/examples/example-board/demo-server-config.json +5 -0
  77. package/examples/example-board/demo-server.js +83 -7
  78. package/examples/example-board/demo-shell-browser.html +75 -207
  79. package/examples/example-board/demo-shell-with-server.html +14 -9
  80. package/examples/example-board/demo-shell.html +1 -1
  81. package/examples/example-board/demo-task-executor.js +259 -41
  82. package/package.json +6 -2
  83. package/schema/live-cards.schema.json +20 -4
  84. package/browser/board-livegraph-runtime.js.map +0 -1
  85. package/examples/example-board/board.yaml +0 -23
  86. package/examples/example-board/bootstrap_payload.json +0 -1
  87. package/examples/example-board/cards/card-chain-region-alert.json +0 -39
  88. package/examples/example-board/cards/card-chain-region-totals.json +0 -26
  89. package/examples/example-board/cards/card-chain-top-region.json +0 -24
  90. package/examples/example-board/cards/card-ex-actions.json +0 -32
  91. package/examples/example-board/cards/card-ex-chart.json +0 -30
  92. package/examples/example-board/cards/card-ex-filter.json +0 -36
  93. package/examples/example-board/cards/card-ex-filtered-by-preference.json +0 -59
  94. package/examples/example-board/cards/card-ex-form.json +0 -91
  95. package/examples/example-board/cards/card-ex-list.json +0 -22
  96. package/examples/example-board/cards/card-ex-markdown.json +0 -17
  97. package/examples/example-board/cards/card-ex-metric.json +0 -19
  98. package/examples/example-board/cards/card-ex-narrative.json +0 -36
  99. package/examples/example-board/cards/card-ex-source-http.json +0 -28
  100. package/examples/example-board/cards/card-ex-source.json +0 -21
  101. package/examples/example-board/cards/card-ex-status.json +0 -35
  102. package/examples/example-board/cards/card-ex-table.json +0 -30
  103. package/examples/example-board/cards/card-ex-todo.json +0 -29
  104. package/examples/example-board/mock.db +0 -15
  105. package/examples/example-board/reusable-runtime-artifacts-adapter.js +0 -233
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+
6
+ function parseArgs(argv) {
7
+ const inIdx = argv.indexOf('--in');
8
+ const outIdx = argv.indexOf('--out');
9
+ const errIdx = argv.indexOf('--err');
10
+ const inFile = inIdx !== -1 ? argv[inIdx + 1] : undefined;
11
+ const outFile = outIdx !== -1 ? argv[outIdx + 1] : undefined;
12
+ const errFile = errIdx !== -1 ? argv[errIdx + 1] : undefined;
13
+ if (!inFile || !outFile || !errFile) {
14
+ console.error('Usage: <adapter> run-inference --in <input.json> --out <output.json> --err <error.txt>');
15
+ process.exit(1);
16
+ }
17
+ return { inFile, outFile, errFile };
18
+ }
19
+
20
+ const envBoardDir = (process.env.BOARD_DIR ?? '').trim();
21
+
22
+ function resolveSyncTmpFileCandidates(payload) {
23
+ const fileName = payload?.context?.card_data?.llm_task_completion_inference?.sync_tmp_file;
24
+ if (typeof fileName !== 'string' || !fileName.trim()) return [];
25
+
26
+ const cleaned = fileName.trim();
27
+ if (path.isAbsolute(cleaned)) {
28
+ return [cleaned];
29
+ }
30
+
31
+ return [
32
+ envBoardDir ? path.join(envBoardDir, cleaned) : '',
33
+ path.join(process.cwd(), cleaned),
34
+ path.join(process.cwd(), 'board-runtime', cleaned),
35
+ path.join(process.cwd(), '..', 'board-runtime', cleaned),
36
+ ].filter(Boolean);
37
+ }
38
+
39
+ function getReadableTmpFile(tmpCandidates) {
40
+ for (const tmpFile of tmpCandidates) {
41
+ if (!fs.existsSync(tmpFile)) continue;
42
+ const content = fs.readFileSync(tmpFile, 'utf-8').trim();
43
+ if (content) return { tmpFile, content };
44
+ }
45
+ return undefined;
46
+ }
47
+
48
+ function waitForTmpSyncInput(tmpCandidates, timeoutMs = 120000) {
49
+ const started = Date.now();
50
+
51
+ return new Promise((resolve, reject) => {
52
+ const interval = setInterval(() => {
53
+ if (Date.now() - started > timeoutMs) {
54
+ clearInterval(interval);
55
+ reject(new Error('Timed out waiting for sync tmp input'));
56
+ }
57
+
58
+ const ready = getReadableTmpFile(tmpCandidates);
59
+ if (!ready) return;
60
+
61
+ clearInterval(interval);
62
+ fs.writeFileSync(ready.tmpFile, '', 'utf-8');
63
+ resolve(ready);
64
+ }, 250);
65
+ });
66
+ }
67
+
68
+ function toNumber(value) {
69
+ const n = Number(value);
70
+ return Number.isFinite(n) ? n : 0;
71
+ }
72
+
73
+ function buildRiskAssessment(table, totalValue) {
74
+ const rows = Array.isArray(table) ? table : [];
75
+ const total = toNumber(totalValue);
76
+ const withWeights = rows.map((row) => {
77
+ const v = toNumber(row?.value);
78
+ return { symbol: String(row?.symbol ?? ''), value: v, weight: total > 0 ? v / total : 0 };
79
+ });
80
+ withWeights.sort((a, b) => b.weight - a.weight);
81
+ const largest = withWeights[0] ?? { symbol: 'N/A', weight: 0, value: 0 };
82
+ const concentrationPct = Math.round(largest.weight * 1000) / 10;
83
+ const concentrationFlag = largest.weight > 0.6;
84
+ const breadthFlag = withWeights.length < 3;
85
+
86
+ const statusText = concentrationFlag
87
+ ? `High concentration risk: ${largest.symbol} at ${concentrationPct}% of portfolio value.`
88
+ : `Risk appears moderate: largest position ${largest.symbol} at ${concentrationPct}% of portfolio value.`;
89
+
90
+ const evidence = [
91
+ `positions=${withWeights.length}`,
92
+ `largest=${largest.symbol}`,
93
+ `largest_weight=${concentrationPct}%`,
94
+ `total_value=${Math.round(total * 100) / 100}`,
95
+ `breadth_ok=${withWeights.length >= 3}`,
96
+ `concentration_ok=${largest.weight <= 0.6}`,
97
+ `risk_flag=${breadthFlag || concentrationFlag}`,
98
+ ].join('; ');
99
+
100
+ return { isTaskCompleted: true, reason: statusText, evidence };
101
+ }
102
+
103
+ function buildRebalancingPlan(table, totalValue, riskAssessment) {
104
+ const rows = Array.isArray(table) ? table : [];
105
+ const total = toNumber(totalValue);
106
+ const targetWeight = rows.length > 0 ? 1 / rows.length : 0;
107
+ const moves = rows
108
+ .map((row) => {
109
+ const symbol = String(row?.symbol ?? 'UNKNOWN');
110
+ const currentValue = toNumber(row?.value);
111
+ const currentWeight = total > 0 ? currentValue / total : 0;
112
+ const deltaWeight = targetWeight - currentWeight;
113
+ return {
114
+ symbol,
115
+ currentWeight,
116
+ deltaWeight,
117
+ action: deltaWeight > 0.03 ? 'BUY' : (deltaWeight < -0.03 ? 'SELL' : 'HOLD'),
118
+ };
119
+ })
120
+ .sort((a, b) => Math.abs(b.deltaWeight) - Math.abs(a.deltaWeight));
121
+
122
+ const topMoves = moves
123
+ .filter((m) => m.action !== 'HOLD')
124
+ .slice(0, 3)
125
+ .map((m) => `${m.action} ${m.symbol} (${Math.round(m.deltaWeight * 1000) / 10}pp)`);
126
+
127
+ const summary = topMoves.length > 0
128
+ ? `Rebalance toward equal-weight profile: ${topMoves.join(', ')}.`
129
+ : 'Current allocations are close to equal-weight; no major rebalance needed.';
130
+
131
+ const evidence = [
132
+ `positions=${rows.length}`,
133
+ `target_weight=${Math.round(targetWeight * 1000) / 10}%`,
134
+ `risk_assessment=${typeof riskAssessment === 'string' ? riskAssessment : 'n/a'}`,
135
+ ].join('; ');
136
+
137
+ return { isTaskCompleted: true, reason: summary, evidence };
138
+ }
139
+
140
+ async function main() {
141
+ const command = process.argv[2];
142
+ if (command !== 'run-inference') {
143
+ console.error(`Unknown command: ${command ?? '(none)'}`);
144
+ process.exit(1);
145
+ }
146
+
147
+ const { inFile, outFile, errFile } = parseArgs(process.argv.slice(3));
148
+
149
+ try {
150
+ const payload = JSON.parse(fs.readFileSync(inFile, 'utf-8'));
151
+ const tmpCandidates = resolveSyncTmpFileCandidates(payload);
152
+ if (tmpCandidates.length > 0) {
153
+ await waitForTmpSyncInput(tmpCandidates);
154
+ }
155
+
156
+ const taskName = String(payload?.taskName ?? '');
157
+ const context = payload?.context ?? {};
158
+ // Inputs arrive under context.requires (the card's declared dependencies)
159
+ const requires = context?.requires ?? {};
160
+ const table = requires?.table?.rows ?? requires?.table;
161
+ const totalValue = requires?.totalValue;
162
+ const riskAssessment = requires?.riskAssessment;
163
+
164
+ let result;
165
+ if (taskName === 'portfolio-risk-assessment') {
166
+ result = buildRiskAssessment(table, totalValue);
167
+ } else if (taskName === 'rebalancing-strategy') {
168
+ result = buildRebalancingPlan(table, totalValue, riskAssessment);
169
+ } else {
170
+ result = {
171
+ isTaskCompleted: true,
172
+ reason: `Inference completed for ${taskName || 'unknown-task'}`,
173
+ evidence: 'deterministic-demo-adapter',
174
+ };
175
+ }
176
+
177
+ fs.writeFileSync(outFile, JSON.stringify(result), 'utf-8');
178
+ fs.writeFileSync(errFile, '', 'utf-8');
179
+ } catch (err) {
180
+ const message = err instanceof Error ? err.message : String(err);
181
+ fs.writeFileSync(errFile, message, 'utf-8');
182
+ fs.writeFileSync(outFile, JSON.stringify({ isTaskCompleted: false, reason: message, evidence: '' }), 'utf-8');
183
+ process.exit(1);
184
+ }
185
+ }
186
+
187
+ void main();
@@ -31,6 +31,9 @@ const RUNTIME_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), 'portfolio-tracker-')
31
31
  const BOARD = path.join(RUNTIME_ROOT, 'board-runtime');
32
32
  const CARDS = path.join(RUNTIME_ROOT, 'cards');
33
33
  const TMP_FILE = path.join(BOARD, 'tmp_file1');
34
+ const INFERENCE_TMP_FILE_2 = path.join(BOARD, 'tmp_file2');
35
+ const INFERENCE_TMP_FILE_3 = path.join(BOARD, 'tmp_file3');
36
+ const INFERENCE_ADAPTER = path.join(RUNTIME_ROOT, 'portfolio-tracker-inference-adapter.js');
34
37
 
35
38
  function parseArgs(argv) {
36
39
  let taskExecutor;
@@ -112,14 +115,24 @@ function statusText() {
112
115
 
113
116
  async function waitForAllCompleted(label, timeoutMs = 30000, pollMs = 500) {
114
117
  const start = Date.now();
118
+ const includeInferenceCards = fs.existsSync(path.join(CARDS, 'portfolio-risk-assessment.json'))
119
+ && fs.existsSync(path.join(CARDS, 'rebalancing-strategy.json'));
120
+
115
121
  while (Date.now() - start < timeoutMs) {
116
122
  const out = statusText();
117
- const completed = [
123
+ const requiredCards = [
118
124
  /\bcompleted\s+portfolio-form\b/.test(out),
119
125
  /\bcompleted\s+price-fetch\b/.test(out),
120
126
  /\bcompleted\s+holdings-table\b/.test(out),
121
127
  /\bcompleted\s+portfolio-value\b/.test(out),
122
- ].every(Boolean);
128
+ ];
129
+ if (includeInferenceCards) {
130
+ requiredCards.push(
131
+ /\bcompleted\s+portfolio-risk-assessment\b/.test(out),
132
+ /\bcompleted\s+rebalancing-strategy\b/.test(out),
133
+ );
134
+ }
135
+ const completed = requiredCards.every(Boolean);
123
136
 
124
137
  if (completed) {
125
138
  console.log(`${label}: all cards completed.`);
@@ -142,11 +155,39 @@ function writePrices(prices) {
142
155
  console.log(`Wrote prices: ${JSON.stringify(prices)}`);
143
156
  }
144
157
 
158
+ function releaseInferenceAdapters(label) {
159
+ if (!fs.existsSync(BOARD)) {
160
+ fs.mkdirSync(BOARD, { recursive: true });
161
+ }
162
+ const signal = JSON.stringify({ stage: label, releasedAt: new Date().toISOString() });
163
+ fs.writeFileSync(INFERENCE_TMP_FILE_2, signal, 'utf-8');
164
+ fs.writeFileSync(INFERENCE_TMP_FILE_3, signal, 'utf-8');
165
+ console.log(`Released inference adapters for ${label}`);
166
+ }
167
+
145
168
  function setupRuntimeCards() {
146
169
  fs.rmSync(CARDS, { recursive: true, force: true });
147
170
  fs.mkdirSync(RUNTIME_ROOT, { recursive: true });
148
171
  fs.cpSync(CARDS_TEMPLATE, CARDS, { recursive: true });
149
172
  fs.copyFileSync(path.join(__dirname, 'fetch-prices.js'), path.join(RUNTIME_ROOT, 'fetch-prices.js'));
173
+ fs.copyFileSync(path.join(__dirname, 'portfolio-tracker-inference-adapter.js'), INFERENCE_ADAPTER);
174
+ }
175
+
176
+ function printTaskExecutorLog() {
177
+ console.log('\n=== Task Executor Log (board-dir) ===');
178
+ const candidates = fs
179
+ .readdirSync(BOARD, { withFileTypes: true })
180
+ .filter(entry => entry.isFile() && entry.name.endsWith('.jsonl') && entry.name.includes('executor'))
181
+ .map(entry => path.join(BOARD, entry.name));
182
+ const taskExecutorLog = candidates.find(p => path.basename(p) === 'task-executor.jsonl') ?? candidates[0];
183
+ if (!taskExecutorLog) {
184
+ console.log(`No task executor log found in board-dir: ${BOARD}`);
185
+ return;
186
+ }
187
+
188
+ console.log(`Log file: ${taskExecutorLog}`);
189
+ const content = fs.readFileSync(taskExecutorLog, 'utf-8');
190
+ process.stdout.write(content || '(empty)\n');
150
191
  }
151
192
 
152
193
  (async () => {
@@ -156,9 +197,9 @@ function setupRuntimeCards() {
156
197
  fs.rmSync(BOARD, { recursive: true, force: true });
157
198
 
158
199
  if (options.taskExecutor) {
159
- cli('init', BOARD, '--task-executor', options.taskExecutor);
200
+ cli('init', BOARD, '--task-executor', options.taskExecutor, '--inference-adapter', INFERENCE_ADAPTER);
160
201
  } else {
161
- cli('init', BOARD);
202
+ cli('init', BOARD, '--inference-adapter', INFERENCE_ADAPTER);
162
203
  }
163
204
  cli('add-cards', '--rg', BOARD, '--card-glob', path.join(CARDS, '*.json'));
164
205
 
@@ -167,6 +208,7 @@ function setupRuntimeCards() {
167
208
 
168
209
  console.log('\n=== T1: Writing market prices ===');
169
210
  writePrices({ AAPL: 198.50, MSFT: 425.30, GOOG: 178.90, AMZN: 192.40, TSLA: 168.75 });
211
+ releaseInferenceAdapters('T1');
170
212
  await waitForAllCompleted('T1');
171
213
 
172
214
  console.log('\n--- T1 Status ---');
@@ -196,6 +238,7 @@ function setupRuntimeCards() {
196
238
  cli('update-card', '--rg', BOARD, '--card-id', 'portfolio-form', '--restart');
197
239
  await sleep(500);
198
240
  writePrices({ AAPL: 198.50, MSFT: 425.30, GOOG: 178.90, AMZN: 192.40, TSLA: 168.75 });
241
+ releaseInferenceAdapters('T2');
199
242
  await waitForAllCompleted('T2');
200
243
 
201
244
  console.log('\n--- T2 Status ---');
@@ -205,13 +248,104 @@ function setupRuntimeCards() {
205
248
  cli('retrigger', '--rg', BOARD, '--task', 'price-fetch');
206
249
  await sleep(500);
207
250
  writePrices({ AAPL: 205.00, MSFT: 425.30, GOOG: 178.90, AMZN: 192.40, TSLA: 168.75 });
251
+ releaseInferenceAdapters('T3');
208
252
  await waitForAllCompleted('T3');
209
253
 
210
254
  console.log('\n--- T3 Status ---');
211
255
  process.stdout.write(statusText());
212
256
 
213
- console.log('\n=== T4: Final board status ===');
257
+ console.log('\n=== T4: Rapid successive portfolio updates (3x queue stress) ===');
258
+ fs.writeFileSync(TMP_FILE, '', 'utf-8');
259
+
260
+ const portfolioFormV3 = {
261
+ id: 'portfolio-form',
262
+ meta: { title: 'Portfolio Holdings Form' },
263
+ provides: [{ bindTo: 'holdings', src: 'card_data.holdings' }],
264
+ card_data: {
265
+ holdings: [
266
+ { symbol: 'AAPL', qty: 50 },
267
+ { symbol: 'MSFT', qty: 30 },
268
+ { symbol: 'GOOG', qty: 100 },
269
+ { symbol: 'AMZN', qty: 40 },
270
+ ],
271
+ },
272
+ view: {
273
+ elements: [
274
+ { kind: 'table', label: 'Holdings', data: { bind: 'card_data.holdings', columns: ['symbol', 'qty'] } },
275
+ ],
276
+ },
277
+ };
278
+
279
+ const portfolioFormV4 = {
280
+ id: 'portfolio-form',
281
+ meta: { title: 'Portfolio Holdings Form' },
282
+ provides: [{ bindTo: 'holdings', src: 'card_data.holdings' }],
283
+ card_data: {
284
+ holdings: [
285
+ { symbol: 'AAPL', qty: 45 },
286
+ { symbol: 'MSFT', qty: 30 },
287
+ { symbol: 'GOOG', qty: 110 },
288
+ { symbol: 'TSLA', qty: 60 },
289
+ ],
290
+ },
291
+ view: {
292
+ elements: [
293
+ { kind: 'table', label: 'Holdings', data: { bind: 'card_data.holdings', columns: ['symbol', 'qty'] } },
294
+ ],
295
+ },
296
+ };
297
+
298
+ const portfolioFormV5 = {
299
+ id: 'portfolio-form',
300
+ meta: { title: 'Portfolio Holdings Form' },
301
+ provides: [{ bindTo: 'holdings', src: 'card_data.holdings' }],
302
+ card_data: {
303
+ holdings: [
304
+ { symbol: 'AAPL', qty: 40 },
305
+ { symbol: 'MSFT', qty: 35 },
306
+ { symbol: 'GOOG', qty: 120 },
307
+ { symbol: 'TSLA', qty: 70 },
308
+ ],
309
+ },
310
+ view: {
311
+ elements: [
312
+ { kind: 'table', label: 'Holdings', data: { bind: 'card_data.holdings', columns: ['symbol', 'qty'] } },
313
+ ],
314
+ },
315
+ };
316
+
317
+ // First update starts a source fetch request.
318
+ fs.writeFileSync(portfolioFormPath, JSON.stringify(portfolioFormV3, null, 2));
319
+ cli('update-card', '--rg', BOARD, '--card-id', 'portfolio-form', '--restart');
320
+
321
+ // Immediate second update should queue a newer checksum while the first request is in-flight.
322
+ fs.writeFileSync(portfolioFormPath, JSON.stringify(portfolioFormV4, null, 2));
323
+ cli('update-card', '--rg', BOARD, '--card-id', 'portfolio-form', '--restart');
324
+
325
+ // Immediate third update should overwrite queued checksum (latest-state wins).
326
+ fs.writeFileSync(portfolioFormPath, JSON.stringify(portfolioFormV5, null, 2));
327
+ cli('update-card', '--rg', BOARD, '--card-id', 'portfolio-form', '--restart');
328
+
329
+ // 7) wait for first request, then 8) write response prices for update #1 tickers.
330
+ // await readFetchRequest('T4 first fetch', ['AAPL', 'MSFT', 'GOOG', 'AMZN']);
331
+ writePrices({ AAPL: 205.00, MSFT: 425.30, GOOG: 178.90, AMZN: 192.40 });
332
+ releaseInferenceAdapters('T4-first');
333
+ await sleep(5000);
334
+
335
+ // 9) wait for second request, then 10) write response prices for update #5 tickers.
336
+ // await readFetchRequest('T4 second fetch', ['AAPL', 'MSFT', 'GOOG', 'TSLA']);
337
+ writePrices({ AAPL: 206.00, MSFT: 426.00, GOOG: 179.50, TSLA: 169.20 });
338
+ releaseInferenceAdapters('T4-second');
339
+
340
+ await waitForAllCompleted('T4');
341
+
342
+ console.log('\n--- T4 Status ---');
343
+ process.stdout.write(statusText());
344
+
345
+ console.log('\n=== T5: Final board status ===');
214
346
  process.stdout.write(statusText());
215
347
 
348
+ printTaskExecutorLog();
349
+
216
350
  console.log('\nPortfolio tracker completed successfully');
217
351
  })();
@@ -0,0 +1,28 @@
1
+ # Card Design Principles & Layout Guide
2
+
3
+ ---
4
+
5
+ ## Card Design Principles
6
+
7
+ - **Cards are not pages** — think post-it notes, not dashboards. The primary content (table, editable data) should be immediately visible, not buried below stacked metrics. Use at most one hero `metric`; collapse secondary summary figures into a single `text` line rather than multiple `metric` blocks.
8
+ - **Single responsibility** — each card answers one question. If the title needs "and", split it.
9
+ - **No redundancy across cards** — each column on a board should appear on exactly one card. If a value is already visible elsewhere, omit it; the user's eye can join cards mentally.
10
+ - **Aggregations are distinct** — a metric that summarises data from another card (total, count, average) is not redundant — it is new information. Keep it.
11
+ - **Separate input from output** — cards with editable elements (`editable-table`, `form`, `filter`) should stay lean; put heavy compute and display in a separate downstream card that `requires` the published token.
12
+ - **Propagate data, not display** — use `provides` to pass data between cards; never duplicate a `sources[]` fetch for data another card already provides.
13
+ - **KISS** — if you are unsure whether a field adds value, leave it out. A sparse card that is immediately readable is better than a dense card that requires study.
14
+
15
+ ---
16
+
17
+ ## Layout
18
+
19
+ ```json
20
+ "layout": {
21
+ "board": { "col": 4, "order": 5 },
22
+ "canvas": { "x": 300, "y": 400, "w": 280, "h": 180 }
23
+ }
24
+ ```
25
+
26
+ - `board.col` — Bootstrap 12-column span: `3`=quarter, `4`=third, `6`=half, `8`=two-thirds, `12`=full
27
+ - `board.order` — ascending integer, controls vertical sort in board view
28
+ - `canvas` — pixel coordinates/size for drag-layout (canvas mode). `h` must be tall enough for all rendered content — a card with metrics + a 4-row table typically needs 400–500px. Too small a height causes an in-card scrollbar; when in doubt, size generously.