yaml-flow 5.4.2 → 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 (199) hide show
  1. package/board-live-cards-cli.js +2 -2
  2. package/board-livecards-server-runtime.js +486 -547
  3. package/browser/asset-integrity.json +10 -0
  4. package/browser/board-livegraph-engine.js +2 -1676
  5. package/browser/board-livegraph-engine.js.map +1 -1
  6. package/browser/live-cards.js +347 -26
  7. package/browser/live-cards.schema.json +418 -132
  8. package/card-store.js +37 -0
  9. package/dist/batch/index.cjs +1 -108
  10. package/dist/batch/index.cjs.map +1 -1
  11. package/dist/batch/index.js +1 -106
  12. package/dist/batch/index.js.map +1 -1
  13. package/dist/board-live-cards-lib-Bg6EvCo5.d.cts +136 -0
  14. package/dist/board-live-cards-lib-jM2uYG1v.d.ts +136 -0
  15. package/dist/board-live-cards-public-CltXYgaY.d.cts +314 -0
  16. package/dist/board-live-cards-public-f-E-FAyp.d.ts +314 -0
  17. package/dist/board-livegraph-runtime/index.cjs +2 -1671
  18. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  19. package/dist/board-livegraph-runtime/index.d.cts +1 -2
  20. package/dist/board-livegraph-runtime/index.d.ts +1 -2
  21. package/dist/board-livegraph-runtime/index.js +2 -1662
  22. package/dist/board-livegraph-runtime/index.js.map +1 -1
  23. package/dist/board-livegraph-runtime/jsonata-sync.cjs +7587 -0
  24. package/dist/card-compute/index.cjs +9 -7159
  25. package/dist/card-compute/index.cjs.map +1 -1
  26. package/dist/card-compute/index.d.cts +22 -0
  27. package/dist/card-compute/index.d.ts +22 -0
  28. package/dist/card-compute/index.js +9 -7145
  29. package/dist/card-compute/index.js.map +1 -1
  30. package/dist/card-compute/jsonata-sync.cjs +7587 -0
  31. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +2 -0
  32. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -0
  33. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +24 -0
  34. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +24 -0
  35. package/dist/cli/browser-api/board-live-cards-browser-adapter.js +2 -0
  36. package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -0
  37. package/dist/cli/browser-api/card-store-browser-api.cjs +2 -0
  38. package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -0
  39. package/dist/cli/browser-api/card-store-browser-api.d.cts +26 -0
  40. package/dist/cli/browser-api/card-store-browser-api.d.ts +26 -0
  41. package/dist/cli/browser-api/card-store-browser-api.js +2 -0
  42. package/dist/cli/browser-api/card-store-browser-api.js.map +1 -0
  43. package/dist/cli/browser-api/jsonata-sync.cjs +7587 -0
  44. package/dist/cli/node/artifacts-store-cli.cjs +11 -0
  45. package/dist/cli/node/artifacts-store-cli.cjs.map +1 -0
  46. package/dist/cli/node/artifacts-store-cli.d.cts +8 -0
  47. package/dist/cli/node/artifacts-store-cli.d.ts +8 -0
  48. package/dist/cli/node/artifacts-store-cli.js +11 -0
  49. package/dist/cli/node/artifacts-store-cli.js.map +1 -0
  50. package/dist/cli/node/board-live-cards-cli.cjs +15 -0
  51. package/dist/cli/node/board-live-cards-cli.cjs.map +1 -0
  52. package/dist/cli/node/board-live-cards-cli.d.cts +20 -0
  53. package/dist/cli/node/board-live-cards-cli.d.ts +20 -0
  54. package/dist/cli/node/board-live-cards-cli.js +15 -0
  55. package/dist/cli/node/board-live-cards-cli.js.map +1 -0
  56. package/dist/cli/node/card-store-cli.cjs +8 -0
  57. package/dist/cli/node/card-store-cli.cjs.map +1 -0
  58. package/dist/cli/node/card-store-cli.d.cts +15 -0
  59. package/dist/cli/node/card-store-cli.d.ts +15 -0
  60. package/dist/cli/node/card-store-cli.js +8 -0
  61. package/dist/cli/node/card-store-cli.js.map +1 -0
  62. package/dist/cli/node/fs-board-adapter.cjs +14 -0
  63. package/dist/cli/node/fs-board-adapter.cjs.map +1 -0
  64. package/dist/cli/node/fs-board-adapter.d.cts +204 -0
  65. package/dist/cli/node/fs-board-adapter.d.ts +204 -0
  66. package/dist/cli/node/fs-board-adapter.js +14 -0
  67. package/dist/cli/node/fs-board-adapter.js.map +1 -0
  68. package/dist/cli/node/jsonata-sync.cjs +7587 -0
  69. package/dist/cli/node/source-cli-task-executor.cjs +11 -0
  70. package/dist/cli/node/source-cli-task-executor.cjs.map +1 -0
  71. package/dist/cli/node/source-cli-task-executor.d.cts +1 -0
  72. package/dist/cli/node/source-cli-task-executor.d.ts +1 -0
  73. package/dist/cli/node/source-cli-task-executor.js +11 -0
  74. package/dist/cli/node/source-cli-task-executor.js.map +1 -0
  75. package/dist/config/index.cjs +1 -79
  76. package/dist/config/index.cjs.map +1 -1
  77. package/dist/config/index.js +1 -76
  78. package/dist/config/index.js.map +1 -1
  79. package/dist/continuous-event-graph/index.cjs +2 -2129
  80. package/dist/continuous-event-graph/index.cjs.map +1 -1
  81. package/dist/continuous-event-graph/index.d.cts +81 -5
  82. package/dist/continuous-event-graph/index.d.ts +81 -5
  83. package/dist/continuous-event-graph/index.js +2 -2088
  84. package/dist/continuous-event-graph/index.js.map +1 -1
  85. package/dist/continuous-event-graph/jsonata-sync.cjs +7587 -0
  86. package/dist/event-graph/index.cjs +22 -8292
  87. package/dist/event-graph/index.cjs.map +1 -1
  88. package/dist/event-graph/index.js +22 -8237
  89. package/dist/event-graph/index.js.map +1 -1
  90. package/dist/execution-refs.cjs +2 -0
  91. package/dist/execution-refs.cjs.map +1 -0
  92. package/dist/execution-refs.d.cts +222 -0
  93. package/dist/execution-refs.d.ts +222 -0
  94. package/dist/execution-refs.js +2 -0
  95. package/dist/execution-refs.js.map +1 -0
  96. package/dist/index.cjs +29 -13221
  97. package/dist/index.cjs.map +1 -1
  98. package/dist/index.d.cts +2 -4
  99. package/dist/index.d.ts +2 -4
  100. package/dist/index.js +29 -13112
  101. package/dist/index.js.map +1 -1
  102. package/dist/inference/index.cjs +5 -617
  103. package/dist/inference/index.cjs.map +1 -1
  104. package/dist/inference/index.js +5 -610
  105. package/dist/inference/index.js.map +1 -1
  106. package/dist/jsonata-sync.cjs +7587 -0
  107. package/dist/{live-cards-bridge-x5XREkXm.d.cts → live-cards-bridge-BXbVTsna.d.cts} +27 -4
  108. package/dist/{live-cards-bridge-EQjytzI_.d.ts → live-cards-bridge-Ds28XR15.d.ts} +27 -4
  109. package/dist/pycli/quickjs-board-runtime.global.js +9 -0
  110. package/dist/pycli/quickjs-board-runtime.global.js.map +1 -0
  111. package/dist/pycli/quickjs-step-machine-runtime.global.js +5 -0
  112. package/dist/pycli/quickjs-step-machine-runtime.global.js.map +1 -0
  113. package/dist/step-machine/index.cjs +11 -7129
  114. package/dist/step-machine/index.cjs.map +1 -1
  115. package/dist/step-machine/index.js +11 -7113
  116. package/dist/step-machine/index.js.map +1 -1
  117. package/dist/storage-refs.cjs +10 -0
  118. package/dist/storage-refs.cjs.map +1 -0
  119. package/dist/storage-refs.d.cts +92 -0
  120. package/dist/storage-refs.d.ts +92 -0
  121. package/dist/storage-refs.js +10 -0
  122. package/dist/storage-refs.js.map +1 -0
  123. package/dist/stores/file.cjs +1 -114
  124. package/dist/stores/file.cjs.map +1 -1
  125. package/dist/stores/file.js +1 -112
  126. package/dist/stores/file.js.map +1 -1
  127. package/dist/stores/index.cjs +1 -231
  128. package/dist/stores/index.cjs.map +1 -1
  129. package/dist/stores/index.js +1 -227
  130. package/dist/stores/index.js.map +1 -1
  131. package/dist/stores/localStorage.cjs +1 -76
  132. package/dist/stores/localStorage.cjs.map +1 -1
  133. package/dist/stores/localStorage.js +1 -74
  134. package/dist/stores/localStorage.js.map +1 -1
  135. package/dist/stores/memory.cjs +1 -47
  136. package/dist/stores/memory.cjs.map +1 -1
  137. package/dist/stores/memory.js +1 -45
  138. package/dist/stores/memory.js.map +1 -1
  139. package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +292 -0
  140. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.js +218 -0
  141. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.py +201 -0
  142. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +25 -16
  143. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +553 -0
  144. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +365 -0
  145. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/.runtime-out +1 -0
  146. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/board-graph.json +32 -0
  147. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +53 -1
  148. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +15 -6
  149. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +6 -1
  150. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +57 -0
  151. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +1 -1
  152. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +1 -1
  153. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +7 -2
  154. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +6 -2
  155. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +97 -0
  156. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +50 -0
  157. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +44 -0
  158. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +70 -0
  159. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/reset-board-dir.py +36 -0
  160. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-demo.flow.yaml +26 -0
  161. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-handlers.py +39 -0
  162. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker-pycli.flow.yaml +80 -0
  163. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +25 -172
  164. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +40 -34
  165. package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +46 -0
  166. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker-pycli.py +77 -0
  167. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
  168. package/examples/example-board/agent-instructions.md +11 -5
  169. package/examples/example-board/demo-chat-handler.js +14 -4
  170. package/examples/example-board/demo-server-config.json +1 -0
  171. package/examples/example-board/demo-server.js +14 -7
  172. package/examples/example-board/demo-shell-browser.html +5 -4
  173. package/examples/example-board/demo-shell-with-server.html +6 -5
  174. package/examples/example-board/demo-task-executor.js +81 -35
  175. package/examples/index.html +0 -14
  176. package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -1
  177. package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
  178. package/package.json +39 -3
  179. package/schema/live-cards.schema.json +418 -132
  180. package/dist/cli/board-live-cards-cli.cjs +0 -10650
  181. package/dist/cli/board-live-cards-cli.cjs.map +0 -1
  182. package/dist/cli/board-live-cards-cli.d.cts +0 -179
  183. package/dist/cli/board-live-cards-cli.d.ts +0 -179
  184. package/dist/cli/board-live-cards-cli.js +0 -10598
  185. package/dist/cli/board-live-cards-cli.js.map +0 -1
  186. package/dist/journal-9HEgs7dU.d.ts +0 -28
  187. package/dist/journal-B-JCfQnh.d.cts +0 -28
  188. package/dist/schedule-Cszq9LYY.d.ts +0 -21
  189. package/dist/schedule-qWNL0RQh.d.cts +0 -21
  190. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +0 -22
  191. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +0 -16
  192. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +0 -28
  193. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +0 -15
  194. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +0 -15
  195. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +0 -28
  196. package/examples/browser/boards/portfolio-tracker/fetch-prices.js +0 -43
  197. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +0 -96
  198. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +0 -7
  199. 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({