yaml-flow 5.4.0 → 6.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 (200) hide show
  1. package/board-live-cards-cli.js +2 -2
  2. package/board-livecards-server-runtime.js +488 -551
  3. package/browser/asset-integrity.json +10 -0
  4. package/browser/board-livecards-runtime-client.js +0 -6
  5. package/browser/board-livegraph-engine.js +2 -1676
  6. package/browser/board-livegraph-engine.js.map +1 -1
  7. package/browser/live-cards.js +347 -26
  8. package/browser/live-cards.schema.json +418 -132
  9. package/card-store.js +37 -0
  10. package/dist/batch/index.cjs +1 -108
  11. package/dist/batch/index.cjs.map +1 -1
  12. package/dist/batch/index.js +1 -106
  13. package/dist/batch/index.js.map +1 -1
  14. package/dist/board-live-cards-lib-Bg6EvCo5.d.cts +136 -0
  15. package/dist/board-live-cards-lib-jM2uYG1v.d.ts +136 -0
  16. package/dist/board-live-cards-public-CltXYgaY.d.cts +314 -0
  17. package/dist/board-live-cards-public-f-E-FAyp.d.ts +314 -0
  18. package/dist/board-livegraph-runtime/index.cjs +2 -1671
  19. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  20. package/dist/board-livegraph-runtime/index.d.cts +1 -2
  21. package/dist/board-livegraph-runtime/index.d.ts +1 -2
  22. package/dist/board-livegraph-runtime/index.js +2 -1662
  23. package/dist/board-livegraph-runtime/index.js.map +1 -1
  24. package/dist/board-livegraph-runtime/jsonata-sync.cjs +7587 -0
  25. package/dist/card-compute/index.cjs +9 -7159
  26. package/dist/card-compute/index.cjs.map +1 -1
  27. package/dist/card-compute/index.d.cts +22 -0
  28. package/dist/card-compute/index.d.ts +22 -0
  29. package/dist/card-compute/index.js +9 -7145
  30. package/dist/card-compute/index.js.map +1 -1
  31. package/dist/card-compute/jsonata-sync.cjs +7587 -0
  32. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +2 -0
  33. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -0
  34. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +24 -0
  35. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +24 -0
  36. package/dist/cli/browser-api/board-live-cards-browser-adapter.js +2 -0
  37. package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -0
  38. package/dist/cli/browser-api/card-store-browser-api.cjs +2 -0
  39. package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -0
  40. package/dist/cli/browser-api/card-store-browser-api.d.cts +26 -0
  41. package/dist/cli/browser-api/card-store-browser-api.d.ts +26 -0
  42. package/dist/cli/browser-api/card-store-browser-api.js +2 -0
  43. package/dist/cli/browser-api/card-store-browser-api.js.map +1 -0
  44. package/dist/cli/browser-api/jsonata-sync.cjs +7587 -0
  45. package/dist/cli/node/artifacts-store-cli.cjs +11 -0
  46. package/dist/cli/node/artifacts-store-cli.cjs.map +1 -0
  47. package/dist/cli/node/artifacts-store-cli.d.cts +8 -0
  48. package/dist/cli/node/artifacts-store-cli.d.ts +8 -0
  49. package/dist/cli/node/artifacts-store-cli.js +11 -0
  50. package/dist/cli/node/artifacts-store-cli.js.map +1 -0
  51. package/dist/cli/node/board-live-cards-cli.cjs +15 -0
  52. package/dist/cli/node/board-live-cards-cli.cjs.map +1 -0
  53. package/dist/cli/node/board-live-cards-cli.d.cts +20 -0
  54. package/dist/cli/node/board-live-cards-cli.d.ts +20 -0
  55. package/dist/cli/node/board-live-cards-cli.js +15 -0
  56. package/dist/cli/node/board-live-cards-cli.js.map +1 -0
  57. package/dist/cli/node/card-store-cli.cjs +8 -0
  58. package/dist/cli/node/card-store-cli.cjs.map +1 -0
  59. package/dist/cli/node/card-store-cli.d.cts +15 -0
  60. package/dist/cli/node/card-store-cli.d.ts +15 -0
  61. package/dist/cli/node/card-store-cli.js +8 -0
  62. package/dist/cli/node/card-store-cli.js.map +1 -0
  63. package/dist/cli/node/fs-board-adapter.cjs +14 -0
  64. package/dist/cli/node/fs-board-adapter.cjs.map +1 -0
  65. package/dist/cli/node/fs-board-adapter.d.cts +204 -0
  66. package/dist/cli/node/fs-board-adapter.d.ts +204 -0
  67. package/dist/cli/node/fs-board-adapter.js +14 -0
  68. package/dist/cli/node/fs-board-adapter.js.map +1 -0
  69. package/dist/cli/node/jsonata-sync.cjs +7587 -0
  70. package/dist/cli/node/source-cli-task-executor.cjs +11 -0
  71. package/dist/cli/node/source-cli-task-executor.cjs.map +1 -0
  72. package/dist/cli/node/source-cli-task-executor.d.cts +1 -0
  73. package/dist/cli/node/source-cli-task-executor.d.ts +1 -0
  74. package/dist/cli/node/source-cli-task-executor.js +11 -0
  75. package/dist/cli/node/source-cli-task-executor.js.map +1 -0
  76. package/dist/config/index.cjs +1 -79
  77. package/dist/config/index.cjs.map +1 -1
  78. package/dist/config/index.js +1 -76
  79. package/dist/config/index.js.map +1 -1
  80. package/dist/continuous-event-graph/index.cjs +2 -2129
  81. package/dist/continuous-event-graph/index.cjs.map +1 -1
  82. package/dist/continuous-event-graph/index.d.cts +81 -5
  83. package/dist/continuous-event-graph/index.d.ts +81 -5
  84. package/dist/continuous-event-graph/index.js +2 -2088
  85. package/dist/continuous-event-graph/index.js.map +1 -1
  86. package/dist/continuous-event-graph/jsonata-sync.cjs +7587 -0
  87. package/dist/event-graph/index.cjs +22 -8292
  88. package/dist/event-graph/index.cjs.map +1 -1
  89. package/dist/event-graph/index.js +22 -8237
  90. package/dist/event-graph/index.js.map +1 -1
  91. package/dist/execution-refs.cjs +2 -0
  92. package/dist/execution-refs.cjs.map +1 -0
  93. package/dist/execution-refs.d.cts +222 -0
  94. package/dist/execution-refs.d.ts +222 -0
  95. package/dist/execution-refs.js +2 -0
  96. package/dist/execution-refs.js.map +1 -0
  97. package/dist/index.cjs +29 -13221
  98. package/dist/index.cjs.map +1 -1
  99. package/dist/index.d.cts +2 -4
  100. package/dist/index.d.ts +2 -4
  101. package/dist/index.js +29 -13112
  102. package/dist/index.js.map +1 -1
  103. package/dist/inference/index.cjs +5 -617
  104. package/dist/inference/index.cjs.map +1 -1
  105. package/dist/inference/index.js +5 -610
  106. package/dist/inference/index.js.map +1 -1
  107. package/dist/jsonata-sync.cjs +7587 -0
  108. package/dist/{live-cards-bridge-x5XREkXm.d.cts → live-cards-bridge-BXbVTsna.d.cts} +27 -4
  109. package/dist/{live-cards-bridge-EQjytzI_.d.ts → live-cards-bridge-Ds28XR15.d.ts} +27 -4
  110. package/dist/pycli/quickjs-board-runtime.global.js +9 -0
  111. package/dist/pycli/quickjs-board-runtime.global.js.map +1 -0
  112. package/dist/pycli/quickjs-step-machine-runtime.global.js +5 -0
  113. package/dist/pycli/quickjs-step-machine-runtime.global.js.map +1 -0
  114. package/dist/step-machine/index.cjs +11 -7129
  115. package/dist/step-machine/index.cjs.map +1 -1
  116. package/dist/step-machine/index.js +11 -7113
  117. package/dist/step-machine/index.js.map +1 -1
  118. package/dist/storage-refs.cjs +10 -0
  119. package/dist/storage-refs.cjs.map +1 -0
  120. package/dist/storage-refs.d.cts +92 -0
  121. package/dist/storage-refs.d.ts +92 -0
  122. package/dist/storage-refs.js +10 -0
  123. package/dist/storage-refs.js.map +1 -0
  124. package/dist/stores/file.cjs +1 -114
  125. package/dist/stores/file.cjs.map +1 -1
  126. package/dist/stores/file.js +1 -112
  127. package/dist/stores/file.js.map +1 -1
  128. package/dist/stores/index.cjs +1 -231
  129. package/dist/stores/index.cjs.map +1 -1
  130. package/dist/stores/index.js +1 -227
  131. package/dist/stores/index.js.map +1 -1
  132. package/dist/stores/localStorage.cjs +1 -76
  133. package/dist/stores/localStorage.cjs.map +1 -1
  134. package/dist/stores/localStorage.js +1 -74
  135. package/dist/stores/localStorage.js.map +1 -1
  136. package/dist/stores/memory.cjs +1 -47
  137. package/dist/stores/memory.cjs.map +1 -1
  138. package/dist/stores/memory.js +1 -45
  139. package/dist/stores/memory.js.map +1 -1
  140. package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +292 -0
  141. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.js +218 -0
  142. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.py +201 -0
  143. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +25 -16
  144. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +553 -0
  145. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +365 -0
  146. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/.runtime-out +1 -0
  147. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/board-graph.json +32 -0
  148. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +53 -1
  149. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +15 -6
  150. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +6 -1
  151. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +57 -0
  152. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +1 -1
  153. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +1 -1
  154. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +7 -2
  155. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +6 -2
  156. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +97 -0
  157. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +50 -0
  158. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +44 -0
  159. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +70 -0
  160. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/reset-board-dir.py +36 -0
  161. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-demo.flow.yaml +26 -0
  162. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-handlers.py +39 -0
  163. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker-pycli.flow.yaml +80 -0
  164. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +25 -172
  165. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +40 -34
  166. package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +46 -0
  167. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker-pycli.py +77 -0
  168. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
  169. package/examples/example-board/agent-instructions.md +11 -5
  170. package/examples/example-board/demo-chat-handler.js +14 -4
  171. package/examples/example-board/demo-server-config.json +1 -0
  172. package/examples/example-board/demo-server.js +19 -34
  173. package/examples/example-board/demo-shell-browser.html +5 -4
  174. package/examples/example-board/demo-shell-with-server.html +10 -6
  175. package/examples/example-board/demo-task-executor.js +81 -35
  176. package/examples/index.html +0 -14
  177. package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -1
  178. package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
  179. package/package.json +39 -3
  180. package/schema/live-cards.schema.json +418 -132
  181. package/dist/cli/board-live-cards-cli.cjs +0 -10644
  182. package/dist/cli/board-live-cards-cli.cjs.map +0 -1
  183. package/dist/cli/board-live-cards-cli.d.cts +0 -179
  184. package/dist/cli/board-live-cards-cli.d.ts +0 -179
  185. package/dist/cli/board-live-cards-cli.js +0 -10592
  186. package/dist/cli/board-live-cards-cli.js.map +0 -1
  187. package/dist/journal-9HEgs7dU.d.ts +0 -28
  188. package/dist/journal-B-JCfQnh.d.cts +0 -28
  189. package/dist/schedule-Cszq9LYY.d.ts +0 -21
  190. package/dist/schedule-qWNL0RQh.d.cts +0 -21
  191. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +0 -22
  192. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +0 -16
  193. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +0 -28
  194. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +0 -15
  195. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +0 -15
  196. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +0 -28
  197. package/examples/browser/boards/portfolio-tracker/fetch-prices.js +0 -43
  198. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +0 -96
  199. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +0 -7
  200. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +0 -351
@@ -0,0 +1,365 @@
1
+ #!/usr/bin/env python3
2
+ """portfolio-tracker.py — E2E orchestrator for the portfolio board demo.
3
+
4
+ Black-box CLI client. All board and card-store operations are performed by
5
+ shelling out to board-live-cards-cli and card-store-cli. No env vars. No
6
+ fallbacks or defaults.
7
+ """
8
+
9
+ import copy
10
+ import json
11
+ import os
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ import time
17
+ import argparse
18
+
19
+
20
+ _CLI_PARSER = argparse.ArgumentParser()
21
+ _MODE_GROUP = _CLI_PARSER.add_mutually_exclusive_group()
22
+ _MODE_GROUP.add_argument('--run-pycli', action='store_true', help='Use pycli for board/card operations (default)')
23
+ _MODE_GROUP.add_argument('--run-nodecli', action='store_true', help='Use node cli for board/card operations')
24
+ _CLI_ARGS = _CLI_PARSER.parse_args()
25
+ RUN_PYCLI = not _CLI_ARGS.run_nodecli
26
+
27
+ # ── Path resolution ────────────────────────────────────────────────────────────
28
+ _HERE = os.path.dirname(os.path.abspath(__file__))
29
+ _REPO_ROOT = os.path.normpath(os.path.join(_HERE, '..', '..', '..', '..'))
30
+
31
+ NODE = shutil.which('node')
32
+ if not NODE:
33
+ print('[ERROR] node not found on PATH', file=sys.stderr)
34
+ sys.exit(1)
35
+
36
+ BOARD_CLI = os.path.join(_REPO_ROOT, 'board-live-cards-cli.js')
37
+ CARD_STORE_CLI = os.path.join(_REPO_ROOT, 'card-store.js')
38
+ FETCH_PRICES_PY = os.path.join(_HERE, 'portfolio-tracker-fetch-prices.py')
39
+
40
+ PYTHON = shutil.which('python')
41
+ if not PYTHON:
42
+ print('[ERROR] python not found on PATH (required for portfolio-tracker-fetch-prices.py)', file=sys.stderr)
43
+ sys.exit(1)
44
+
45
+ PYTHON_RUNNER = sys.executable or PYTHON
46
+
47
+ BOARD_PYCLI = os.path.join(_REPO_ROOT, 'pycli', 'main', 'board_live_cards_pycli.py')
48
+ CARD_STORE_PYCLI = os.path.join(_REPO_ROOT, 'pycli', 'main', 'card_store_pycli.py')
49
+ QUICKJS_BUNDLE = os.path.join(_REPO_ROOT, 'dist', 'pycli', 'quickjs-board-runtime.global.js')
50
+
51
+ if RUN_PYCLI:
52
+ if not os.path.exists(BOARD_PYCLI):
53
+ print(f'[ERROR] pycli entry not found: {BOARD_PYCLI}', file=sys.stderr)
54
+ sys.exit(1)
55
+ if not os.path.exists(CARD_STORE_PYCLI):
56
+ print(f'[ERROR] pycli entry not found: {CARD_STORE_PYCLI}', file=sys.stderr)
57
+ sys.exit(1)
58
+ if not os.path.exists(QUICKJS_BUNDLE):
59
+ print(f'[ERROR] quickjs bundle not found: {QUICKJS_BUNDLE}', file=sys.stderr)
60
+ print('Run from yaml-flow root: npm run build:quickjs', file=sys.stderr)
61
+ sys.exit(1)
62
+
63
+ if RUN_PYCLI:
64
+ ACTIVE_BOARD_BIN = [PYTHON_RUNNER, BOARD_PYCLI]
65
+ ACTIVE_CARD_STORE_BIN = [PYTHON_RUNNER, CARD_STORE_PYCLI]
66
+ ACTIVE_BOARD_SUFFIX = ['--bundle', QUICKJS_BUNDLE]
67
+ else:
68
+ ACTIVE_BOARD_BIN = [NODE, BOARD_CLI]
69
+ ACTIVE_CARD_STORE_BIN = [NODE, CARD_STORE_CLI]
70
+ ACTIVE_BOARD_SUFFIX = []
71
+
72
+ # ── Runtime directories (under os.tmpdir()/experiment/) ───────────────────────
73
+ _TMP_BASE = tempfile.mkdtemp(prefix='experiment-')
74
+ CARDSTORE_DIR = os.path.join(_TMP_BASE, 'cardstore')
75
+ BOARDRUNTIME_DIR = os.path.join(_TMP_BASE, 'boardruntime')
76
+ OUTPUTS_DIR = os.path.join(_TMP_BASE, 'outputs')
77
+
78
+ CARDSTORE_REF = f'::fs-path::{CARDSTORE_DIR}'
79
+ BOARDRUNTIME_REF = f'::fs-path::{BOARDRUNTIME_DIR}'
80
+ OUTPUTS_REF = f'::fs-path::{OUTPUTS_DIR}'
81
+
82
+ # ── Inline card definitions ────────────────────────────────────────────────────
83
+ CARD_PORTFOLIO_FORM = {
84
+ "id": "portfolio-form",
85
+ "meta": {"title": "Portfolio Holdings Form"},
86
+ "provides": [{"bindTo": "holdings", "ref": "card_data.holdings"}],
87
+ "card_data": {"holdings": []},
88
+ "view": {
89
+ "elements": [
90
+ {"kind": "table", "label": "Holdings",
91
+ "data": {"bind": "card_data.holdings", "columns": ["symbol", "qty"]}}
92
+ ]
93
+ }
94
+ }
95
+
96
+ CARD_PRICE_FETCH = {
97
+ "id": "price-fetch",
98
+ "meta": {"title": "Fetch Market Prices"},
99
+ "requires": ["holdings"],
100
+ "provides": [{"bindTo": "prices", "ref": "fetched_sources.prices"}],
101
+ "card_data": {},
102
+ "source_defs": [{
103
+ "kind": "mock-quotes",
104
+ "bindTo": "prices",
105
+ "outputFile": "prices.json",
106
+ "projections": {"tickers": "requires.holdings.symbol"}
107
+ }],
108
+ "view": {
109
+ "elements": [
110
+ {"kind": "table", "label": "Market Prices",
111
+ "data": {"bind": "fetched_sources.prices"}}
112
+ ]
113
+ }
114
+ }
115
+
116
+ CARD_HOLDINGS_TABLE = {
117
+ "id": "holdings-table",
118
+ "meta": {"title": "Holdings Table"},
119
+ "requires": ["holdings", "prices"],
120
+ "provides": [{"bindTo": "table", "ref": "computed_values.table"}],
121
+ "card_data": {},
122
+ "compute": [{
123
+ "bindTo": "table",
124
+ "expr": (
125
+ '{ "rows": $map(requires.holdings, function($h) { '
126
+ '{ "symbol": $h.symbol, "qty": $h.qty, '
127
+ '"price": $lookup(requires.prices, $h.symbol), '
128
+ '"value": $h.qty * $lookup(requires.prices, $h.symbol) } }) }'
129
+ )
130
+ }],
131
+ "view": {
132
+ "elements": [
133
+ {"kind": "table", "label": "Portfolio Positions",
134
+ "data": {"bind": "computed_values.table.rows",
135
+ "columns": ["symbol", "qty", "price", "value"]}}
136
+ ]
137
+ }
138
+ }
139
+
140
+ CARD_PORTFOLIO_VALUE = {
141
+ "id": "portfolio-value",
142
+ "meta": {"title": "Portfolio Total Value"},
143
+ "requires": ["table"],
144
+ "provides": [{"bindTo": "totalValue", "ref": "computed_values.totalValue"}],
145
+ "card_data": {},
146
+ "compute": [
147
+ {"bindTo": "totalValue", "expr": "$sum(requires.table.rows.value)"}
148
+ ],
149
+ "view": {
150
+ "elements": [
151
+ {"kind": "metric", "label": "Total Portfolio Value",
152
+ "data": {"bind": "computed_values.totalValue"}}
153
+ ]
154
+ }
155
+ }
156
+
157
+ # ── Helpers ────────────────────────────────────────────────────────────────────
158
+ def set_holdings(card_json: dict, holdings: dict) -> dict:
159
+ card = copy.deepcopy(card_json)
160
+ card["card_data"]["holdings"] = [
161
+ {"symbol": symbol, "qty": qty}
162
+ for symbol, qty in holdings.items()
163
+ ]
164
+ return card
165
+
166
+
167
+ def run_board(*args):
168
+ subprocess.run([*ACTIVE_BOARD_BIN, *args, *ACTIVE_BOARD_SUFFIX], check=True, shell=False)
169
+
170
+
171
+ def run_board_with_input(*args, input_json: str):
172
+ subprocess.run(
173
+ [*ACTIVE_BOARD_BIN, *args, *ACTIVE_BOARD_SUFFIX],
174
+ input=input_json, check=True, shell=False, text=True,
175
+ )
176
+
177
+
178
+ def run_board_capture(*args) -> str:
179
+ result = subprocess.run(
180
+ [*ACTIVE_BOARD_BIN, *args, *ACTIVE_BOARD_SUFFIX],
181
+ check=True, shell=False, capture_output=True, text=True,
182
+ )
183
+ return result.stdout
184
+
185
+
186
+ def run_card_store_set(card: dict):
187
+ subprocess.run(
188
+ [*ACTIVE_CARD_STORE_BIN, 'set', '--store-ref', CARDSTORE_REF],
189
+ input=json.dumps(card),
190
+ check=True,
191
+ shell=False,
192
+ text=True,
193
+ )
194
+
195
+
196
+ def read_json(path: str):
197
+ with open(path, encoding='utf-8') as f:
198
+ return json.load(f)
199
+
200
+
201
+ def wait_for_completed(label: str, timeout_s: float = 10.0, poll_s: float = 0.5):
202
+ required_names = {'portfolio-form', 'price-fetch', 'holdings-table', 'portfolio-value'}
203
+ deadline = time.monotonic() + timeout_s
204
+ while time.monotonic() < deadline:
205
+ raw = run_board_capture('status', '--base-ref', BOARDRUNTIME_REF)
206
+ data = json.loads(raw).get('data', {})
207
+ cards = data.get('cards', [])
208
+ completed = {c['name'] for c in cards if c.get('status') == 'completed'}
209
+ if required_names.issubset(completed):
210
+ print(f'[{label}] all cards completed.')
211
+ return
212
+ time.sleep(poll_s)
213
+ # timed out — print status and exit
214
+ raw = run_board_capture('status', '--base-ref', BOARDRUNTIME_REF)
215
+ print(f'[ERROR] {label}: timed out waiting for all cards to complete.', file=sys.stderr)
216
+ print(raw, file=sys.stderr)
217
+ sys.exit(1)
218
+
219
+
220
+ # ── T0a — Create runtime directories ──────────────────────────────────────────
221
+ print('\n=== T0a: Create runtime directories ===')
222
+ for d in (CARDSTORE_DIR, BOARDRUNTIME_DIR, OUTPUTS_DIR):
223
+ os.makedirs(d)
224
+ print(f' created: {d}')
225
+
226
+ # ── T0b — Init board ───────────────────────────────────────────────────────────
227
+ print('\n=== T0b: Init board ===')
228
+ _task_executor_body = json.dumps({
229
+ 'task-executor-ref': {
230
+ 'meta': 'task-executor',
231
+ 'howToRun': 'local-python',
232
+ 'whatToRun': f'::fs-path::{FETCH_PRICES_PY}',
233
+ }
234
+ })
235
+ run_board_with_input(
236
+ 'init',
237
+ '--base-ref', BOARDRUNTIME_REF,
238
+ '--card-store-ref', CARDSTORE_REF,
239
+ '--outputs-store-ref', OUTPUTS_REF,
240
+ input_json=_task_executor_body,
241
+ )
242
+
243
+ # ── T0c — Set all cards into card store ────────────────────────────────────────
244
+ print('\n=== T0c: Set all cards into card store ===')
245
+ run_card_store_set(set_holdings(CARD_PORTFOLIO_FORM, {"AAPL": 50, "MSFT": 30}))
246
+ run_card_store_set(CARD_PRICE_FETCH)
247
+ run_card_store_set(CARD_HOLDINGS_TABLE)
248
+ run_card_store_set(CARD_PORTFOLIO_VALUE)
249
+
250
+ # ── T0d — Upsert cards to board ────────────────────────────────────────────────
251
+ print('\n=== T0d: Upsert cards to board ===')
252
+ for _card_id in ('portfolio-form', 'price-fetch', 'holdings-table', 'portfolio-value'):
253
+ run_board('upsert-card', '--base-ref', BOARDRUNTIME_REF, '--card-id', _card_id)
254
+
255
+ # ── T1 — Wait for all cards completed ──────────────────────────────────────────
256
+ print('\n=== T1: Wait for all cards completed ===')
257
+ wait_for_completed('T1')
258
+
259
+ _prices_path = os.path.join(OUTPUTS_DIR, 'data-objects', 'prices.json')
260
+ _prices_t1 = read_json(_prices_path)
261
+ assert isinstance(_prices_t1, dict) and len(_prices_t1) > 0, \
262
+ 'T1: prices.json is empty or not an object'
263
+ assert set(_prices_t1.keys()) == {'AAPL', 'MSFT'}, \
264
+ f'T1: expected keys {{AAPL, MSFT}}, got {set(_prices_t1.keys())}'
265
+ assert all(isinstance(v, (int, float)) for v in _prices_t1.values()), \
266
+ 'T1: all price values must be numbers'
267
+ print('[T1] assertion passed: prices.json has AAPL, MSFT with numeric values.')
268
+
269
+ # ── T2a — Update holdings (GOOG added) ────────────────────────────────────────
270
+ print('\n=== T2a: Update holdings (GOOG added) ===')
271
+ run_card_store_set(set_holdings(CARD_PORTFOLIO_FORM, {"AAPL": 50, "MSFT": 30, "GOOG": 100}))
272
+
273
+ # ── T2b — Upsert portfolio-form with --restart ─────────────────────────────────
274
+ print('\n=== T2b: Upsert portfolio-form --restart ===')
275
+ run_board('upsert-card', '--base-ref', BOARDRUNTIME_REF,
276
+ '--card-id', 'portfolio-form', '--restart')
277
+
278
+ # ── T2c — Wait and assert ──────────────────────────────────────────────────────
279
+ print('\n=== T2c: Wait for all cards completed ===')
280
+ wait_for_completed('T2c')
281
+
282
+ _prices_t2c = read_json(_prices_path)
283
+ assert set(_prices_t2c.keys()) == {'AAPL', 'MSFT', 'GOOG'}, \
284
+ f'T2c: expected keys {{AAPL, MSFT, GOOG}}, got {set(_prices_t2c.keys())}'
285
+
286
+ _ht_cv_path = os.path.join(OUTPUTS_DIR, 'cards', 'holdings-table', 'computed_values.json')
287
+ _ht_cv_t2c = read_json(_ht_cv_path)
288
+ assert len(_ht_cv_t2c['table']['rows']) == 3, \
289
+ f'T2c: expected 3 rows in holdings-table, got {len(_ht_cv_t2c["table"]["rows"])}'
290
+ print('[T2c] assertions passed: 3 tickers in prices, 3 rows in holdings-table.')
291
+
292
+ # ── T3 — Retrigger price-fetch, wait ──────────────────────────────────────────
293
+ print('\n=== T3: Retrigger price-fetch ===')
294
+ run_board('retrigger', '--base-ref', BOARDRUNTIME_REF, '--id', 'price-fetch')
295
+ wait_for_completed('T3')
296
+
297
+ _prices_t3 = read_json(_prices_path)
298
+ assert set(_prices_t3.keys()) == {'AAPL', 'MSFT', 'GOOG'}, \
299
+ f'T3: expected 3 tickers, got {set(_prices_t3.keys())}'
300
+ assert _prices_t3 != _prices_t2c, \
301
+ 'T3: prices must differ from T2c values after retrigger (random regeneration)'
302
+ print('[T3] assertions passed: 3 tickers, prices differ from T2c.')
303
+
304
+ # ── T4 — Rapid 5× portfolio-form updates (queue stress test) ──────────────────
305
+ print('\n=== T4: Rapid 5x portfolio-form updates ===')
306
+ for _holdings in [
307
+ {"AAPL": 50, "MSFT": 30, "GOOG": 100, "AMZN": 40}, # V3
308
+ {"AAPL": 45, "MSFT": 30, "GOOG": 110, "AMZN": 40, "TSLA": 60}, # V4
309
+ {"AAPL": 45, "MSFT": 30, "GOOG": 110, "AMZN": 100}, # V4a
310
+ {"AAPL": 45, "MSFT": 30, "GOOG": 110, "AMZN": 140, "TSLA": 60}, # V4b
311
+ {"AAPL": 40, "MSFT": 35, "GOOG": 120, "TSLA": 70}, # V5
312
+ ]:
313
+ run_card_store_set(set_holdings(CARD_PORTFOLIO_FORM, _holdings))
314
+ run_board('upsert-card', '--base-ref', BOARDRUNTIME_REF,
315
+ '--card-id', 'portfolio-form', '--restart')
316
+
317
+ wait_for_completed('T4')
318
+
319
+ _prices_t4 = read_json(_prices_path)
320
+ assert set(_prices_t4.keys()) == {'AAPL', 'MSFT', 'GOOG', 'TSLA'}, \
321
+ f'T4: expected keys {{AAPL, MSFT, GOOG, TSLA}}, got {set(_prices_t4.keys())}'
322
+ assert 'AMZN' not in _prices_t4, \
323
+ 'T4: AMZN must not be present (board must have settled on V5 holdings)'
324
+ print('[T4] assertions passed: V5 tickers only, AMZN absent.')
325
+
326
+ # ── T5 — Print final status and cross-check ────────────────────────────────────
327
+ print('\n=== T5: Print final status and cross-check ===')
328
+
329
+ # Step 1: Wait for all cards completed (stable state before any assertions)
330
+ wait_for_completed('T5')
331
+
332
+ # Step 2: Capture live CLI status
333
+ _cli_raw = run_board_capture('status', '--base-ref', BOARDRUNTIME_REF)
334
+ _cli_status = json.loads(_cli_raw)['data']
335
+
336
+ # Step 3: Read status.json from outputs store
337
+ _file_status = read_json(os.path.join(OUTPUTS_DIR, 'status.json'))
338
+
339
+ # Step 4: Cross-check CLI vs file status
340
+ assert json.dumps(_cli_status, sort_keys=True) == json.dumps(_file_status, sort_keys=True), \
341
+ 'T5: CLI status does not match status.json snapshot'
342
+ print('[T5] cross-check passed: CLI status matches status.json.')
343
+
344
+ # Step 5: Print holdings-table computed values
345
+ _ht_cv = read_json(_ht_cv_path)
346
+ print('\nFinal portfolio positions table:')
347
+ print(json.dumps(_ht_cv['table'], indent=2))
348
+
349
+ # Step 6: Totals cross-verify: holdings × prices == totalValue
350
+ V5_HOLDINGS = {"AAPL": 40, "MSFT": 35, "GOOG": 120, "TSLA": 70}
351
+ _prices_final = read_json(_prices_path)
352
+ _pv_cv = read_json(
353
+ os.path.join(OUTPUTS_DIR, 'cards', 'portfolio-value', 'computed_values.json')
354
+ )
355
+ _total_value = _pv_cv['totalValue']
356
+ _expected = sum(qty * _prices_final[sym] for sym, qty in V5_HOLDINGS.items())
357
+ assert round(_expected, 2) == round(_total_value, 2), \
358
+ f'T5: totals mismatch: expected={round(_expected, 2)}, got={round(_total_value, 2)}'
359
+ print(f'[T5] totals assertion passed: expected={round(_expected, 2)}, totalValue={round(_total_value, 2)}')
360
+
361
+ # Step 7: Print full CLI status
362
+ print('\nFinal board status:')
363
+ print(json.dumps(_cli_status, indent=2))
364
+
365
+ print('\n=== portfolio-tracker completed successfully ===')
@@ -0,0 +1,32 @@
1
+ {
2
+ "graph": {
3
+ "version": 1,
4
+ "config": {
5
+ "settings": {
6
+ "completion": "manual",
7
+ "refreshStrategy": "data-changed"
8
+ },
9
+ "tasks": {}
10
+ },
11
+ "state": {
12
+ "status": "running",
13
+ "tasks": {},
14
+ "availableOutputs": [],
15
+ "stuckDetection": {
16
+ "is_stuck": false,
17
+ "stuck_description": null,
18
+ "outputs_unresolvable": [],
19
+ "tasks_blocked": []
20
+ },
21
+ "lastUpdated": "2026-05-02T15:18:33.305Z",
22
+ "executionId": "live-1777735113305",
23
+ "executionConfig": {
24
+ "executionMode": "eligibility-mode",
25
+ "conflictStrategy": "alphabetical",
26
+ "completionStrategy": "manual"
27
+ }
28
+ },
29
+ "snapshotAt": "2026-05-02T15:18:33.305Z"
30
+ },
31
+ "lastDrainedJournalId": ""
32
+ }
@@ -6,6 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = path.dirname(__filename);
7
7
  const repoRoot = path.resolve(__dirname, '..', '..', '..', '..', '..');
8
8
  const boardCliPath = path.join(repoRoot, 'board-live-cards-cli.js');
9
+ const cardStoreCliPath = path.join(repoRoot, 'card-store.js');
9
10
 
10
11
  export function runBoardCli(args, options = {}) {
11
12
  const { capture = false, cwd = process.cwd() } = options;
@@ -16,7 +17,6 @@ export function runBoardCli(args, options = {}) {
16
17
  stdio: capture ? 'pipe' : 'pipe',
17
18
  env: {
18
19
  ...process.env,
19
- BOARD_LIVE_CARDS_NO_SPAWN: process.env.BOARD_LIVE_CARDS_NO_SPAWN ?? '1',
20
20
  BOARD_DIR: process.env.BOARD_DIR ?? '',
21
21
  },
22
22
  });
@@ -34,6 +34,58 @@ export function runBoardCli(args, options = {}) {
34
34
  return capture ? (result.stdout ?? '') : '';
35
35
  }
36
36
 
37
+ /** Spawn CLI with JSON piped to stdin. */
38
+ export function runBoardCliWithInput(args, inputJson, options = {}) {
39
+ const { cwd = process.cwd() } = options;
40
+ const result = spawnSync(process.execPath, [boardCliPath, ...args], {
41
+ input: inputJson,
42
+ cwd,
43
+ encoding: 'utf-8',
44
+ windowsHide: true,
45
+ stdio: ['pipe', 'pipe', 'pipe'],
46
+ env: {
47
+ ...process.env,
48
+ BOARD_DIR: process.env.BOARD_DIR ?? '',
49
+ },
50
+ });
51
+
52
+ if (result.error) {
53
+ throw new Error(`Failed to launch board-live-cards-cli: ${result.error.message}`);
54
+ }
55
+
56
+ if ((result.status ?? 1) !== 0) {
57
+ const stderr = (result.stderr ?? '').trim();
58
+ const stdout = (result.stdout ?? '').trim();
59
+ throw new Error(`board-live-cards-cli failed (${result.status}): ${stderr || stdout || 'no output'}`);
60
+ }
61
+
62
+ return result.stdout ?? '';
63
+ }
64
+
65
+ /** Spawn card-store-cli with JSON piped to stdin. */
66
+ export function runCardStoreCliWithInput(args, inputJson, options = {}) {
67
+ const { cwd = process.cwd() } = options;
68
+ const result = spawnSync(process.execPath, [cardStoreCliPath, ...args], {
69
+ input: inputJson,
70
+ cwd,
71
+ encoding: 'utf-8',
72
+ windowsHide: true,
73
+ stdio: ['pipe', 'pipe', 'pipe'],
74
+ });
75
+
76
+ if (result.error) {
77
+ throw new Error(`Failed to launch card-store-cli: ${result.error.message}`);
78
+ }
79
+
80
+ if ((result.status ?? 1) !== 0) {
81
+ const stderr = (result.stderr ?? '').trim();
82
+ const stdout = (result.stdout ?? '').trim();
83
+ throw new Error(`card-store-cli failed (${result.status}): ${stderr || stdout || 'no output'}`);
84
+ }
85
+
86
+ return result.stdout ?? '';
87
+ }
88
+
37
89
  export async function readStdinJson() {
38
90
  let raw = '';
39
91
  process.stdin.setEncoding('utf-8');
@@ -1,24 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readStdinJson, runBoardCli, writeFailure, writeResult } from './_board-cli.js';
3
+ import { readStdinJson, runBoardCli, runCardStoreCliWithInput, writeFailure, writeResult } from './_board-cli.js';
4
4
 
5
5
  try {
6
6
  const input = await readStdinJson();
7
7
  const boardDir = String(input.BOARD_DIR ?? '').trim();
8
- const cardsGlob = String(input.CARDS_GLOB ?? '').trim();
8
+ const cards = Array.isArray(input.CARDS) ? input.CARDS : [];
9
9
 
10
- if (!boardDir || !cardsGlob) {
11
- writeFailure('BOARD_DIR and CARDS_GLOB are required');
10
+ if (!boardDir || cards.length === 0) {
11
+ writeFailure('BOARD_DIR and CARDS (array) are required');
12
12
  process.exit(0);
13
13
  }
14
14
 
15
- runBoardCli(['upsert-card', '--rg', boardDir, '--card-glob', cardsGlob]);
15
+ const baseRef = `::fs-path::${boardDir}`;
16
+
17
+ // Write all cards to the card store in one call
18
+ runCardStoreCliWithInput(
19
+ ['set', '--store-ref', baseRef],
20
+ JSON.stringify(cards),
21
+ );
22
+
23
+ // Upsert all cards at once
24
+ runBoardCli(['upsert-card', '--base-ref', baseRef, '--all']);
16
25
 
17
26
  writeResult({
18
27
  result: 'success',
19
28
  data: {
20
29
  board_dir: boardDir,
21
- cards_glob: cardsGlob,
30
+ count: cards.length,
22
31
  },
23
32
  });
24
33
  } catch (error) {
@@ -11,7 +11,12 @@ try {
11
11
  process.exit(0);
12
12
  }
13
13
 
14
- runBoardCli(['init', boardDir]);
14
+ runBoardCli([
15
+ 'init',
16
+ '--base-ref', `::fs-path::${boardDir}`,
17
+ '--card-store-ref', `::fs-path::${boardDir}`,
18
+ '--outputs-store-ref', `::fs-path::${boardDir}`,
19
+ ]);
15
20
  writeResult({
16
21
  result: 'success',
17
22
  data: {
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readStdinJson, runBoardCli, writeFailure, writeResult } from './_board-cli.js';
4
+
5
+ function sleep(ms) {
6
+ return new Promise((resolve) => setTimeout(resolve, ms));
7
+ }
8
+
9
+ try {
10
+ const input = await readStdinJson();
11
+ const boardDir = String(input.BOARD_DIR ?? '').trim();
12
+ const expectedCardCount = Number(input.EXPECTED_CARD_COUNT ?? 0);
13
+ const timeoutMs = Number(input.TIMEOUT_MS ?? 30000);
14
+ const pollMs = Number(input.POLL_MS ?? 500);
15
+
16
+ if (!boardDir || expectedCardCount <= 0) {
17
+ writeFailure('BOARD_DIR and EXPECTED_CARD_COUNT are required');
18
+ process.exit(0);
19
+ }
20
+
21
+ const started = Date.now();
22
+
23
+ while (Date.now() - started < timeoutMs) {
24
+ const statusJson = runBoardCli(['status', '--base-ref', `::fs-path::${boardDir}`], { capture: true });
25
+ let cards = [];
26
+ try {
27
+ cards = JSON.parse(statusJson)?.data?.cards ?? [];
28
+ } catch { /* ignore parse errors */ }
29
+
30
+ const completedCount = cards.filter(c => c.status === 'completed').length;
31
+
32
+ if (cards.length >= expectedCardCount && completedCount >= expectedCardCount) {
33
+ writeResult({
34
+ result: 'success',
35
+ data: {
36
+ completed: true,
37
+ card_count: cards.length,
38
+ completed_count: completedCount,
39
+ },
40
+ });
41
+ process.exit(0);
42
+ }
43
+
44
+ await sleep(pollMs);
45
+ }
46
+
47
+ writeResult({
48
+ result: 'timeout',
49
+ data: {
50
+ completed: false,
51
+ error: `timed out waiting for ${expectedCardCount} cards to complete`,
52
+ },
53
+ });
54
+ } catch (error) {
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ writeFailure(message);
57
+ }
@@ -12,7 +12,7 @@ try {
12
12
  process.exit(0);
13
13
  }
14
14
 
15
- runBoardCli(['retrigger', '--rg', boardDir, '--task', task]);
15
+ runBoardCli(['retrigger', '--base-ref', `::fs-path::${boardDir}`, '--id', task]);
16
16
 
17
17
  writeResult({
18
18
  result: 'success',
@@ -11,7 +11,7 @@ try {
11
11
  process.exit(0);
12
12
  }
13
13
 
14
- const status = runBoardCli(['status', '--rg', boardDir], { capture: true });
14
+ const status = runBoardCli(['status', '--base-ref', `::fs-path::${boardDir}`], { capture: true });
15
15
 
16
16
  writeResult({
17
17
  result: 'success',
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as fs from 'node:fs';
4
4
  import * as path from 'node:path';
5
- import { readStdinJson, runBoardCli, writeFailure, writeResult } from './_board-cli.js';
5
+ import { readStdinJson, runBoardCli, runCardStoreCliWithInput, writeFailure, writeResult } from './_board-cli.js';
6
6
 
7
7
  try {
8
8
  const input = await readStdinJson();
@@ -22,7 +22,12 @@ try {
22
22
  card.card_data.holdings = holdings;
23
23
  fs.writeFileSync(cardPath, `${JSON.stringify(card, null, 2)}\n`, 'utf-8');
24
24
 
25
- runBoardCli(['upsert-card', '--rg', boardDir, '--card', cardPath, '--restart']);
25
+ const baseRef = `::fs-path::${boardDir}`;
26
+ runCardStoreCliWithInput(
27
+ ['set', '--store-ref', baseRef],
28
+ JSON.stringify(card),
29
+ );
30
+ runBoardCli(['upsert-card', '--base-ref', baseRef, '--card-id', card.id, '--restart']);
26
31
 
27
32
  writeResult({
28
33
  result: 'success',
@@ -22,8 +22,12 @@ try {
22
22
  const started = Date.now();
23
23
 
24
24
  while (Date.now() - started < timeoutMs) {
25
- const status = runBoardCli(['status', '--rg', boardDir], { capture: true });
26
- const complete = tasks.every((task) => new RegExp(`\\bcompleted\\s+${task}\\b`).test(status));
25
+ const statusJson = runBoardCli(['status', '--base-ref', `::fs-path::${boardDir}`], { capture: true });
26
+ let cards = [];
27
+ try {
28
+ cards = JSON.parse(statusJson)?.data?.cards ?? [];
29
+ } catch { /* ignore parse errors */ }
30
+ const complete = tasks.every((task) => cards.some(c => c.name === task && c.status === 'completed'));
27
31
 
28
32
  if (complete) {
29
33
  writeResult({