yaml-flow 5.4.2 → 7.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 (252) hide show
  1. package/board-live-cards-cli.js +6 -6
  2. package/browser/asset-integrity.json +10 -0
  3. package/browser/board-livecards-client.js +2 -0
  4. package/browser/board-livecards-client.js.map +1 -0
  5. package/browser/board-livecards-localstorage.js +10 -0
  6. package/browser/board-livecards-localstorage.js.map +1 -0
  7. package/browser/board-livegraph-engine.js +2 -1676
  8. package/browser/board-livegraph-engine.js.map +1 -1
  9. package/browser/card-compute.js +28 -28
  10. package/browser/compute-jsonata.js +5 -0
  11. package/browser/compute-jsonata.js.map +1 -0
  12. package/browser/live-cards.js +561 -129
  13. package/browser/live-cards.schema.json +418 -132
  14. package/card-store.js +37 -0
  15. package/dist/batch/index.cjs +1 -108
  16. package/dist/batch/index.cjs.map +1 -1
  17. package/dist/batch/index.js +1 -106
  18. package/dist/batch/index.js.map +1 -1
  19. package/dist/board-live-cards-lib-Bg6EvCo5.d.cts +136 -0
  20. package/dist/board-live-cards-lib-jM2uYG1v.d.ts +136 -0
  21. package/dist/board-live-cards-public-CW5074xr.d.cts +318 -0
  22. package/dist/board-live-cards-public-hnZo0mAf.d.ts +318 -0
  23. package/dist/board-livegraph-runtime/index.cjs +2 -1671
  24. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  25. package/dist/board-livegraph-runtime/index.d.cts +12 -11
  26. package/dist/board-livegraph-runtime/index.d.ts +12 -11
  27. package/dist/board-livegraph-runtime/index.js +2 -1662
  28. package/dist/board-livegraph-runtime/index.js.map +1 -1
  29. package/dist/board-livegraph-runtime/jsonata-sync.cjs +7623 -0
  30. package/dist/card-compute/index.cjs +9 -7159
  31. package/dist/card-compute/index.cjs.map +1 -1
  32. package/dist/card-compute/index.d.cts +27 -1
  33. package/dist/card-compute/index.d.ts +27 -1
  34. package/dist/card-compute/index.js +9 -7145
  35. package/dist/card-compute/index.js.map +1 -1
  36. package/dist/card-compute/jsonata-sync.cjs +7623 -0
  37. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs +3 -0
  38. package/dist/cli/browser-api/board-live-cards-browser-adapter.cjs.map +1 -0
  39. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.cts +37 -0
  40. package/dist/cli/browser-api/board-live-cards-browser-adapter.d.ts +37 -0
  41. package/dist/cli/browser-api/board-live-cards-browser-adapter.js +3 -0
  42. package/dist/cli/browser-api/board-live-cards-browser-adapter.js.map +1 -0
  43. package/dist/cli/browser-api/card-store-browser-api.cjs +2 -0
  44. package/dist/cli/browser-api/card-store-browser-api.cjs.map +1 -0
  45. package/dist/cli/browser-api/card-store-browser-api.d.cts +26 -0
  46. package/dist/cli/browser-api/card-store-browser-api.d.ts +26 -0
  47. package/dist/cli/browser-api/card-store-browser-api.js +2 -0
  48. package/dist/cli/browser-api/card-store-browser-api.js.map +1 -0
  49. package/dist/cli/browser-api/jsonata-sync.cjs +7623 -0
  50. package/dist/cli/node/artifacts-store-cli.cjs +11 -0
  51. package/dist/cli/node/artifacts-store-cli.cjs.map +1 -0
  52. package/dist/cli/node/artifacts-store-cli.d.cts +8 -0
  53. package/dist/cli/node/artifacts-store-cli.d.ts +8 -0
  54. package/dist/cli/node/artifacts-store-cli.js +11 -0
  55. package/dist/cli/node/artifacts-store-cli.js.map +1 -0
  56. package/dist/cli/node/board-live-cards-cli.cjs +15 -0
  57. package/dist/cli/node/board-live-cards-cli.cjs.map +1 -0
  58. package/dist/cli/node/board-live-cards-cli.d.cts +20 -0
  59. package/dist/cli/node/board-live-cards-cli.d.ts +20 -0
  60. package/dist/cli/node/board-live-cards-cli.js +15 -0
  61. package/dist/cli/node/board-live-cards-cli.js.map +1 -0
  62. package/dist/cli/node/card-store-cli.cjs +8 -0
  63. package/dist/cli/node/card-store-cli.cjs.map +1 -0
  64. package/dist/cli/node/card-store-cli.d.cts +15 -0
  65. package/dist/cli/node/card-store-cli.d.ts +15 -0
  66. package/dist/cli/node/card-store-cli.js +8 -0
  67. package/dist/cli/node/card-store-cli.js.map +1 -0
  68. package/dist/cli/node/execution-adapter.cjs +3 -0
  69. package/dist/cli/node/execution-adapter.cjs.map +1 -0
  70. package/dist/cli/node/execution-adapter.d.cts +174 -0
  71. package/dist/cli/node/execution-adapter.d.ts +174 -0
  72. package/dist/cli/node/execution-adapter.js +3 -0
  73. package/dist/cli/node/execution-adapter.js.map +1 -0
  74. package/dist/cli/node/fs-board-adapter.cjs +14 -0
  75. package/dist/cli/node/fs-board-adapter.cjs.map +1 -0
  76. package/dist/cli/node/fs-board-adapter.d.cts +204 -0
  77. package/dist/cli/node/fs-board-adapter.d.ts +204 -0
  78. package/dist/cli/node/fs-board-adapter.js +14 -0
  79. package/dist/cli/node/fs-board-adapter.js.map +1 -0
  80. package/dist/cli/node/jsonata-sync.cjs +7623 -0
  81. package/dist/cli/node/source-cli-task-executor.cjs +11 -0
  82. package/dist/cli/node/source-cli-task-executor.cjs.map +1 -0
  83. package/dist/cli/node/source-cli-task-executor.d.cts +1 -0
  84. package/dist/cli/node/source-cli-task-executor.d.ts +1 -0
  85. package/dist/cli/node/source-cli-task-executor.js +11 -0
  86. package/dist/cli/node/source-cli-task-executor.js.map +1 -0
  87. package/dist/config/index.cjs +1 -79
  88. package/dist/config/index.cjs.map +1 -1
  89. package/dist/config/index.js +1 -76
  90. package/dist/config/index.js.map +1 -1
  91. package/dist/continuous-event-graph/index.cjs +2 -2129
  92. package/dist/continuous-event-graph/index.cjs.map +1 -1
  93. package/dist/continuous-event-graph/index.d.cts +81 -5
  94. package/dist/continuous-event-graph/index.d.ts +81 -5
  95. package/dist/continuous-event-graph/index.js +2 -2088
  96. package/dist/continuous-event-graph/index.js.map +1 -1
  97. package/dist/continuous-event-graph/jsonata-sync.cjs +7623 -0
  98. package/dist/event-graph/index.cjs +22 -8292
  99. package/dist/event-graph/index.cjs.map +1 -1
  100. package/dist/event-graph/index.js +22 -8237
  101. package/dist/event-graph/index.js.map +1 -1
  102. package/dist/execution-refs.cjs +3 -0
  103. package/dist/execution-refs.cjs.map +1 -0
  104. package/dist/execution-refs.d.cts +260 -0
  105. package/dist/execution-refs.d.ts +260 -0
  106. package/dist/execution-refs.js +3 -0
  107. package/dist/execution-refs.js.map +1 -0
  108. package/dist/index.cjs +29 -13221
  109. package/dist/index.cjs.map +1 -1
  110. package/dist/index.d.cts +2 -4
  111. package/dist/index.d.ts +2 -4
  112. package/dist/index.js +29 -13112
  113. package/dist/index.js.map +1 -1
  114. package/dist/inference/index.cjs +5 -617
  115. package/dist/inference/index.cjs.map +1 -1
  116. package/dist/inference/index.js +5 -610
  117. package/dist/inference/index.js.map +1 -1
  118. package/dist/jsonata-sync.cjs +7623 -0
  119. package/dist/{live-cards-bridge-x5XREkXm.d.cts → live-cards-bridge-BXbVTsna.d.cts} +27 -4
  120. package/dist/{live-cards-bridge-EQjytzI_.d.ts → live-cards-bridge-Ds28XR15.d.ts} +27 -4
  121. package/dist/server-runtime/index.cjs +9 -0
  122. package/dist/server-runtime/index.cjs.map +1 -0
  123. package/dist/server-runtime/index.d.cts +31 -0
  124. package/dist/server-runtime/index.d.ts +31 -0
  125. package/dist/server-runtime/index.js +9 -0
  126. package/dist/server-runtime/index.js.map +1 -0
  127. package/dist/server-runtime/jsonata-sync.cjs +7623 -0
  128. package/dist/step-machine/index.cjs +11 -7129
  129. package/dist/step-machine/index.cjs.map +1 -1
  130. package/dist/step-machine/index.js +11 -7113
  131. package/dist/step-machine/index.js.map +1 -1
  132. package/dist/step-machine-public/index.cjs +2 -0
  133. package/dist/step-machine-public/index.cjs.map +1 -0
  134. package/dist/step-machine-public/index.d.cts +159 -0
  135. package/dist/step-machine-public/index.d.ts +159 -0
  136. package/dist/step-machine-public/index.js +2 -0
  137. package/dist/step-machine-public/index.js.map +1 -0
  138. package/dist/step-machine-public/jsonata-sync.cjs +7623 -0
  139. package/dist/storage-refs.cjs +10 -0
  140. package/dist/storage-refs.cjs.map +1 -0
  141. package/dist/storage-refs.d.cts +93 -0
  142. package/dist/storage-refs.d.ts +93 -0
  143. package/dist/storage-refs.js +10 -0
  144. package/dist/storage-refs.js.map +1 -0
  145. package/dist/stores/file.cjs +1 -114
  146. package/dist/stores/file.cjs.map +1 -1
  147. package/dist/stores/file.js +1 -112
  148. package/dist/stores/file.js.map +1 -1
  149. package/dist/stores/index.cjs +1 -231
  150. package/dist/stores/index.cjs.map +1 -1
  151. package/dist/stores/index.js +1 -227
  152. package/dist/stores/index.js.map +1 -1
  153. package/dist/stores/localStorage.cjs +1 -76
  154. package/dist/stores/localStorage.cjs.map +1 -1
  155. package/dist/stores/localStorage.js +1 -74
  156. package/dist/stores/localStorage.js.map +1 -1
  157. package/dist/stores/memory.cjs +1 -47
  158. package/dist/stores/memory.cjs.map +1 -1
  159. package/dist/stores/memory.js +1 -45
  160. package/dist/stores/memory.js.map +1 -1
  161. package/dist/types-B1ZRa4aI.d.ts +147 -0
  162. package/dist/types-BxEFcVK9.d.cts +147 -0
  163. package/examples/browser/boards/portfolio-tracker/portfolio-t4.js +291 -0
  164. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.js +218 -0
  165. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-fetch-prices.py +201 -0
  166. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-http-test.js +357 -0
  167. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +25 -16
  168. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-public.js +552 -0
  169. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.js +300 -0
  170. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-server.py +617 -0
  171. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-sse-worker.js +48 -0
  172. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.py +366 -0
  173. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/.runtime-out +1 -0
  174. package/examples/cli/step-machine-cli/portfolio-tracker/--base-ref/board-graph.json +32 -0
  175. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +70 -3
  176. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +16 -11
  177. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +9 -8
  178. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/poll-status-cli.js +49 -0
  179. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +2 -6
  180. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +4 -8
  181. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +3 -7
  182. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +9 -8
  183. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +12 -17
  184. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +2 -6
  185. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/_board_pycli.py +107 -0
  186. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/add-cards.py +51 -0
  187. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/init-board.py +45 -0
  188. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/poll-status.py +71 -0
  189. package/examples/cli/step-machine-cli/portfolio-tracker/handlers-py/reset-board-dir.py +36 -0
  190. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-demo.flow.yaml +26 -0
  191. package/examples/cli/step-machine-cli/portfolio-tracker/inline-python-handlers.py +39 -0
  192. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker-pycli.flow.yaml +80 -0
  193. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +36 -187
  194. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +40 -34
  195. package/examples/cli/step-machine-cli/portfolio-tracker/run-inline-python-demo-pycli.py +43 -0
  196. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker-pycli.py +77 -0
  197. package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
  198. package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +8 -13
  199. package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +33 -9
  200. package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +3 -1
  201. package/examples/cli/step-machine-demo/step2-double-cli.js +6 -12
  202. package/examples/cli/step-machine-demo/two-step-math.flow.yaml +66 -4
  203. package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +13 -5
  204. package/examples/example-board/agent-instructions.md +11 -5
  205. package/examples/example-board/cards/_index.json +47 -0
  206. package/examples/example-board/cards/card-market-prices.json +33 -9
  207. package/examples/example-board/cards/card-my-identity.json +30 -6
  208. package/examples/example-board/cards/card-portfolio-action.json +24 -6
  209. package/examples/example-board/cards/card-portfolio-intelligence.json +97 -0
  210. package/examples/example-board/cards/card-portfolio-risks.json +24 -6
  211. package/examples/example-board/cards/card-portfolio-value.json +38 -10
  212. package/examples/example-board/cards/card-portfolio.json +57 -13
  213. package/examples/example-board/cards/card-rebalance-impact.json +22 -6
  214. package/examples/example-board/cards/card-rebalance-sim.json +66 -15
  215. package/examples/example-board/demo-chat-handler.js +14 -4
  216. package/examples/example-board/demo-server-config.json +1 -0
  217. package/examples/example-board/demo-server.js +366 -68
  218. package/examples/example-board/demo-shell-localstorage.html +774 -0
  219. package/examples/example-board/demo-shell-with-server.html +20 -37
  220. package/examples/example-board/demo-shell.html +5 -4
  221. package/examples/example-board/demo-task-executor.js +273 -275
  222. package/examples/index.html +0 -14
  223. package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +0 -1
  224. package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +1 -2
  225. package/package.json +46 -8
  226. package/schema/live-cards.schema.json +418 -132
  227. package/step-machine-cli.js +43 -310
  228. package/board-livecards-server-runtime.js +0 -1574
  229. package/browser/board-livecards-runtime-client.js +0 -263
  230. package/dist/cli/board-live-cards-cli.cjs +0 -10650
  231. package/dist/cli/board-live-cards-cli.cjs.map +0 -1
  232. package/dist/cli/board-live-cards-cli.d.cts +0 -179
  233. package/dist/cli/board-live-cards-cli.d.ts +0 -179
  234. package/dist/cli/board-live-cards-cli.js +0 -10598
  235. package/dist/cli/board-live-cards-cli.js.map +0 -1
  236. package/dist/journal-9HEgs7dU.d.ts +0 -28
  237. package/dist/journal-B-JCfQnh.d.cts +0 -28
  238. package/dist/schedule-Cszq9LYY.d.ts +0 -21
  239. package/dist/schedule-qWNL0RQh.d.cts +0 -21
  240. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +0 -22
  241. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +0 -16
  242. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +0 -28
  243. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +0 -15
  244. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +0 -15
  245. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +0 -28
  246. package/examples/browser/boards/portfolio-tracker/fetch-prices.js +0 -43
  247. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +0 -96
  248. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +0 -7
  249. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +0 -351
  250. package/examples/cli/step-machine-demo/two-step-math-handlers.js +0 -32
  251. package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +0 -24
  252. package/examples/example-board/demo-shell-browser.html +0 -674
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/card-compute/schema-validator.ts","../../../src/card-compute/index.ts","../../../src/cli/common/board-live-cards-lib.ts","../../../src/cli/browser-api/storage-localstorage-adapters.ts","../../../src/cli/browser-api/card-store-browser-api.ts"],"names":["_require","createRequire","createCardStore","adapter","onWarn","loadIndex","applyJsonPath","obj","jsonPath","value","segments","out","target","i","key","cur","next","id","entry","cards","card","result","snapshotChecksumIndex","localIndex","changed","cardKey","index","existingById","existingByKey","e","resolvedKey","checksum","current","stableJson","k","fnv32a","str","seed","h","computeStableJsonHashBrowser","a","b","c","d","n","createLocalStorageKvStorage","prefix","raw","prefix2","fullPrefix","lsKey","deepMergeObjects","patch","v","head","tail","nested","createLocalStorageJsonStorage","kv","segment","existing","createLocalStorageCardStorageAdapter","json","cardId","createBrowserCardStoreApi","namespace","store"],"mappings":"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","file":"card-store-browser-api.cjs","sourcesContent":["/**\n * schema-validator — Full JSON Schema validation for LiveCards nodes.\n *\n * Uses AJV to validate against the published live-cards.schema.json.\n * For a lightweight sync check without AJV, use `CardCompute.validate()` instead.\n *\n * @example\n * ```typescript\n * import { validateLiveCardSchema } from 'yaml-flow/card-compute';\n *\n * const result = validateLiveCardSchema(node);\n * if (!result.ok) console.error(result.errors);\n * ```\n */\n\nimport type { ValidationResult } from './index.js';\nimport liveCardsSchema from '../../schema/live-cards.schema.json';\nimport Ajv from 'ajv';\nimport addFormats from 'ajv-formats';\nimport { createRequire } from 'module';\nconst _require = createRequire(import.meta.url);\nconst jsonata: (expr: string) => { evaluate: (data: unknown) => unknown } = _require('./jsonata-sync.cjs');\n\ntype AjvValidateFunction = {\n (data: unknown): boolean;\n errors?: Array<{ instancePath?: string; message?: string }> | null;\n};\n\nlet _compiled: AjvValidateFunction | null = null;\n\nconst KNOWN_NAMESPACES = [\n 'card_data',\n 'requires',\n 'fetched_sources',\n 'computed_values',\n 'source_defs',\n] as const;\n\ntype KnownNamespace = typeof KNOWN_NAMESPACES[number];\n\nconst NAMESPACE_REFERENCE_RE = /\\b(card_data|requires|fetched_sources|computed_values|source_defs)\\b/g;\nconst ROOT_PATH_NAMESPACE_RE = /^\\s*(card_data|requires|fetched_sources|computed_values|source_defs)(\\.|$)/;\n\nfunction referencedNamespaces(expression: string): Set<KnownNamespace> {\n const namespaces = new Set<KnownNamespace>();\n let match: RegExpExecArray | null;\n NAMESPACE_REFERENCE_RE.lastIndex = 0;\n while ((match = NAMESPACE_REFERENCE_RE.exec(expression)) !== null) {\n namespaces.add(match[1] as KnownNamespace);\n }\n return namespaces;\n}\n\nfunction parseRootPathNamespace(pathValue: string): KnownNamespace | null {\n const match = ROOT_PATH_NAMESPACE_RE.exec(pathValue);\n return match ? (match[1] as KnownNamespace) : null;\n}\n\nfunction validateJsonataExprWithNamespaces(\n expr: string,\n path: string,\n allowedNamespaces: Set<KnownNamespace>,\n errors: string[],\n): void {\n try {\n jsonata(expr);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push(`${path}: invalid JSONata expression (${message})`);\n return;\n }\n\n const usedNamespaces = referencedNamespaces(expr);\n for (const namespace of usedNamespaces) {\n if (!allowedNamespaces.has(namespace)) {\n errors.push(`${path}: disallowed namespace \"${namespace}\" in expression`);\n }\n }\n}\n\nfunction walkViewPathReferences(\n value: unknown,\n path: string,\n errors: string[],\n): void {\n if (Array.isArray(value)) {\n value.forEach((entry, index) => {\n walkViewPathReferences(entry, `${path}/${index}`, errors);\n });\n return;\n }\n\n if (typeof value === 'string') {\n const rootNamespace = parseRootPathNamespace(value);\n if (!rootNamespace) return;\n if (!new Set<KnownNamespace>(['card_data', 'requires', 'computed_values']).has(rootNamespace)) {\n errors.push(`${path}: disallowed namespace \"${rootNamespace}\" in view reference`);\n }\n return;\n }\n\n if (!value || typeof value !== 'object') return;\n\n const record = value as Record<string, unknown>;\n for (const [key, next] of Object.entries(record)) {\n walkViewPathReferences(next, `${path}/${key}`, errors);\n }\n}\n\nfunction getValidator(): AjvValidateFunction {\n if (_compiled) return _compiled;\n const ajv = new Ajv({ allErrors: true });\n addFormats(ajv);\n _compiled = ajv.compile(liveCardsSchema);\n return _compiled;\n}\n\n/**\n * Validate a node against the full LiveCards JSON Schema (draft-07).\n *\n * Requires `ajv` and `ajv-formats` to be installed.\n * Returns the same `ValidationResult` shape as `CardCompute.validate()`.\n */\nexport function validateLiveCardSchema(\n node: unknown,\n): ValidationResult {\n const validate = getValidator();\n const valid = validate(node);\n\n const errors = (validate.errors ?? []).map(e => {\n const path = e.instancePath || '/';\n return `${path}: ${e.message ?? 'unknown error'}`;\n });\n\n // JSON Schema draft-07 cannot enforce per-property uniqueness across array items.\n // Check bindTo and outputFile uniqueness here after the AJV structural pass.\n if (node && typeof node === 'object' && !Array.isArray(node)) {\n const source_defs = (node as Record<string, unknown>).source_defs;\n if (Array.isArray(source_defs)) {\n const bindTos = new Set<string>();\n const outputFiles = new Set<string>();\n source_defs.forEach((src, i) => {\n if (!src || typeof src !== 'object' || Array.isArray(src)) return;\n const s = src as Record<string, unknown>;\n if (typeof s.bindTo === 'string' && s.bindTo) {\n if (bindTos.has(s.bindTo)) {\n errors.push(`/source_defs/${i}/bindTo: bindTo \"${s.bindTo}\" must be unique across all source_defs`);\n }\n bindTos.add(s.bindTo);\n }\n if (typeof s.outputFile === 'string' && s.outputFile) {\n if (outputFiles.has(s.outputFile)) {\n errors.push(`/source_defs/${i}/outputFile: outputFile \"${s.outputFile}\" must be unique across all source_defs`);\n }\n outputFiles.add(s.outputFile);\n }\n });\n }\n }\n\n if (!valid || errors.length > 0) return { ok: false, errors };\n return { ok: true, errors: [] };\n}\n\n/**\n * Validate JSONata expressions in compute[] by compiling with the same parser used at runtime.\n */\nexport function validateLiveCardRuntimeExpressions(\n node: unknown,\n): ValidationResult {\n const errors: string[] = [];\n\n if (!node || typeof node !== 'object' || Array.isArray(node)) {\n return { ok: true, errors: [] };\n }\n\n const asRecord = node as Record<string, unknown>;\n\n const compute = asRecord.compute;\n if (Array.isArray(compute)) {\n compute.forEach((step, i) => {\n if (!step || typeof step !== 'object' || Array.isArray(step)) return;\n const expr = (step as Record<string, unknown>).expr;\n if (typeof expr !== 'string' || expr.trim().length === 0) return;\n validateJsonataExprWithNamespaces(\n expr,\n `/compute/${i}/expr`,\n new Set<KnownNamespace>(['card_data', 'requires', 'fetched_sources', 'computed_values']),\n errors,\n );\n });\n }\n\n // Validate provides[].ref paths use a valid root namespace.\n const VALID_PROVIDES_SRC_NAMESPACES = new Set<KnownNamespace>([\n 'card_data', 'requires', 'fetched_sources', 'computed_values',\n ]);\n const provides = asRecord.provides;\n if (Array.isArray(provides)) {\n provides.forEach((entry, i) => {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return;\n const ref = (entry as Record<string, unknown>).ref;\n if (typeof ref !== 'string' || ref.trim().length === 0) return;\n const rootNamespace = parseRootPathNamespace(ref);\n if (rootNamespace === null) {\n errors.push(`/provides/${i}/ref: path \"${ref}\" must start with a valid namespace (${[...VALID_PROVIDES_SRC_NAMESPACES].join(', ')})`);\n } else if (!VALID_PROVIDES_SRC_NAMESPACES.has(rootNamespace)) {\n errors.push(`/provides/${i}/ref: disallowed namespace \"${rootNamespace}\" in path \"${ref}\" (valid: ${[...VALID_PROVIDES_SRC_NAMESPACES].join(', ')})`);\n }\n });\n }\n\n const view = asRecord.view;\n if (view && typeof view === 'object' && !Array.isArray(view)) {\n walkViewPathReferences(view, '/view', errors);\n }\n\n // Validate source_defs[i].projections values: each must be a JSONata expression rooted at\n // card_data or requires only. fetched_sources/computed_values/source_defs are not\n // valid here because sources run before those namespaces exist.\n const VALID_PROJECTION_NAMESPACES = new Set<KnownNamespace>(['card_data', 'requires']);\n const source_defs = asRecord.source_defs;\n if (Array.isArray(source_defs)) {\n source_defs.forEach((srcDef, i) => {\n if (!srcDef || typeof srcDef !== 'object' || Array.isArray(srcDef)) return;\n const projections = (srcDef as Record<string, unknown>).projections;\n if (!projections || typeof projections !== 'object' || Array.isArray(projections)) return;\n for (const [key, exprVal] of Object.entries(projections as Record<string, unknown>)) {\n if (typeof exprVal !== 'string' || exprVal.trim().length === 0) continue;\n validateJsonataExprWithNamespaces(\n exprVal,\n `/source_defs/${i}/projections/${key}`,\n VALID_PROJECTION_NAMESPACES,\n errors,\n );\n }\n });\n }\n\n return { ok: errors.length === 0, errors };\n}\n\nexport function validateLiveCard(\n node: unknown,\n): ValidationResult {\n return validateLiveCardDefinition(node);\n}\n\n/**\n * Full validation for live card definitions:\n * 1) JSON Schema structure/contract checks\n * 2) Runtime JSONata parser compatibility checks for compute expressions\n */\nexport function validateLiveCardDefinition(\n node: unknown,\n): ValidationResult {\n const schema = validateLiveCardSchema(node);\n if (!schema.ok) return schema;\n\n const runtime = validateLiveCardRuntimeExpressions(node);\n if (!runtime.ok) return { ok: false, errors: runtime.errors };\n\n return { ok: true, errors: [] };\n}\n","/**\n * card-compute — JSONata-powered compute engine for LiveCards nodes.\n *\n * Isomorphic: works in browser, Node.js, and bundlers.\n * No DOM dependency. Compute expressions are JSONata strings.\n *\n * @example\n * ```typescript\n * import { CardCompute } from 'yaml-flow/card-compute';\n *\n * const node = {\n * id: 'sales',\n * card_data: { data: [{ revenue: 100 }, { revenue: 200 }] },\n * compute: [\n * { bindTo: 'total', expr: '$sum(card_data.data.revenue)' },\n * { bindTo: 'avg', expr: '$average(card_data.data.revenue)' },\n * ],\n * };\n * await CardCompute.run(node);\n * // node.computed_values.total === 300\n * // node.computed_values.avg === 150\n * ```\n *\n * Expressions are evaluated against { card_data, requires, fetched_sources, computed_values }.\n * computed_values is ephemeral — never persisted to disk.\n */\n\nimport { createRequire } from 'module';\nconst _require = createRequire(import.meta.url);\nconst jsonata: (expr: string) => { evaluate: (data: unknown) => unknown } = _require('./jsonata-sync.cjs');\nconst jsonataSync = jsonata;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A source definition: cli writes to outputFile; bindTo names the fetched_sources.* key in compute context. Both bindTo and outputFile must be unique across source_defs in a card. */\nexport interface ComputeSource {\n bindTo: string;\n outputFile: string;\n cli?: string;\n // Deprecated alias retained for compatibility with older cards.\n script?: string;\n optionalForCompletionGating?: boolean;\n /** Named data projections: each key maps to a JSONata expression rooted at card_data or requires.\n * The engine evaluates these before spawning the executor and passes results as _projections. */\n projections?: Record<string, string>;\n [key: string]: unknown;\n}\n\n/** Options for CardCompute.run() */\nexport interface RunOptions {\n /** Pre-loaded source results map (keyed by bindTo). Use in browser or when caller loads files. */\n sourcesData?: Record<string, unknown>;\n}\n\n/** A single compute step: bindTo names the computed_values key; expr is a JSONata expression. */\nexport interface ComputeStep {\n bindTo: string;\n expr: string;\n}\n\n/** Minimal node shape expected by CardCompute. */\nexport interface ComputeNode {\n id?: string;\n card_data?: Record<string, unknown>;\n requires?: Record<string, unknown>;\n source_defs?: ComputeSource[];\n compute?: ComputeStep[];\n computed_values?: Record<string, unknown>;\n /** Ephemeral: populated by run() from sourcesData option. Never persisted. */\n _sourcesData?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Deep path utilities\n// ---------------------------------------------------------------------------\n\nfunction deepGet(obj: unknown, path: string): unknown {\n if (!path || !obj) return undefined;\n const parts = path.split('.');\n let cur: unknown = obj;\n for (let i = 0; i < parts.length; i++) {\n if (cur == null) return undefined;\n cur = (cur as Record<string, unknown>)[parts[i]];\n }\n return cur;\n}\n\nfunction deepSet(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let cur: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};\n cur = cur[parts[i]] as Record<string, unknown>;\n }\n cur[parts[parts.length - 1]] = value;\n}\n\n// ---------------------------------------------------------------------------\n// Engine — JSONata-based async evaluation\n// ---------------------------------------------------------------------------\n\n/**\n * Run all compute steps on a node.\n * Each step's expr is evaluated against { card_data, requires, fetched_sources, computed_values }.\n * Results are written to node.computed_values[bindTo].\n * computed_values and _sourcesData are reset on each call — ephemeral, never persisted.\n *\n * @param options.sourcesData Pre-loaded map of { [bindTo]: data } for fetched_sources namespace.\n * In Node/CLI: loaded from outputFiles by the caller (card-handler).\n * In browser: passed in by the caller (e.g. from fetch results).\n */\nasync function run(node: ComputeNode, options?: RunOptions): Promise<ComputeNode> {\n if (!node?.compute?.length) return node;\n if (!node.card_data) node.card_data = {};\n node.computed_values = {};\n node._sourcesData = options?.sourcesData ?? {};\n\n // Context passed to JSONata. Structural keys take precedence over any user data.\n const _requires = node.requires ?? {};\n const ctx: Record<string, unknown> = {\n card_data: node.card_data,\n requires: _requires,\n expects_data: _requires, // alias: same reference as requires\n fetched_sources: node._sourcesData,\n data: node.computed_values, // alias: same reference as computed_values\n computed_values: node.computed_values,\n };\n\n for (const step of node.compute) {\n try {\n const val = await jsonata(step.expr).evaluate(ctx);\n deepSet(node.computed_values, step.bindTo, val);\n ctx.computed_values = node.computed_values; // subsequent steps see earlier results\n // ctx.data is the same reference as node.computed_values — already in sync\n } catch (err) {\n console.error(`CardCompute.run error on \"${node.id ?? '?'}.${step.bindTo}\":`, err);\n }\n }\n\n return node;\n}\n\n/**\n * Synchronous version of run() — uses a vendored sync JSONata build\n * (async/await stripped from jsonata.js since all built-in functions\n * are CPU-only).\n *\n * Same semantics as `run()`: evaluates all compute steps, populates\n * `node.computed_values`, returns the mutated node.\n *\n * @returns `{ ok: true, node }` when all steps evaluated successfully.\n * `{ ok: false, node }` is currently never returned but reserved\n * for future use if an expression requires true async evaluation.\n */\nfunction runSync(\n node: ComputeNode,\n options?: RunOptions,\n): { ok: boolean; node: ComputeNode; errors?: Array<{ bindTo: string; error: string }> } {\n if (!node?.compute?.length) return { ok: true, node };\n if (!node.card_data) node.card_data = {};\n node.computed_values = {};\n node._sourcesData = options?.sourcesData ?? {};\n\n const _requires2 = node.requires ?? {};\n const ctx: Record<string, unknown> = {\n card_data: node.card_data,\n requires: _requires2,\n expects_data: _requires2, // alias: same reference as requires\n fetched_sources: node._sourcesData,\n data: node.computed_values, // alias: same reference as computed_values\n computed_values: node.computed_values,\n };\n\n const errors: Array<{ bindTo: string; error: string }> = [];\n for (const step of node.compute) {\n try {\n const val = jsonataSync(step.expr).evaluate(ctx);\n deepSet(node.computed_values, step.bindTo, val);\n ctx.computed_values = node.computed_values;\n // ctx.data is the same reference as node.computed_values — already in sync\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n errors.push({ bindTo: step.bindTo, error: msg });\n console.error(`CardCompute.runSync error on \"${node.id ?? '?'}.${step.bindTo}\":`, err);\n }\n }\n\n return errors.length > 0 ? { ok: true, node, errors } : { ok: true, node };\n}\n\n/**\n * Evaluate a single JSONata expression against a node's context.\n * Context is { card_data, requires, fetched_sources, computed_values }.\n */\nasync function evalExpr(\n expr: string,\n node: ComputeNode,\n vars?: Record<string, unknown>,\n): Promise<unknown> {\n const ctx: Record<string, unknown> = {\n ...(vars ?? {}),\n card_data: node.card_data ?? {},\n requires: node.requires ?? {},\n fetched_sources: node._sourcesData ?? {},\n computed_values: node.computed_values ?? {},\n };\n return jsonata(expr).evaluate(ctx);\n}\n\n// ---------------------------------------------------------------------------\n// resolve — synchronous deep-get from node\n// ---------------------------------------------------------------------------\n\nfunction resolve(node: ComputeNode, path: string): unknown {\n if (path.startsWith('fetched_sources.')) {\n return deepGet(node._sourcesData ?? {}, path.slice('fetched_sources.'.length));\n }\n\n return deepGet(node, path);\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/** Result of validateNode — ok: true means valid, ok: false has errors[]. */\nexport interface ValidationResult {\n ok: boolean;\n errors: string[];\n}\n\nconst VALID_ELEMENT_KINDS = new Set([\n 'metric', 'table', 'editable-table', 'chart', 'form', 'filter', 'list',\n 'notes', 'todo', 'alert', 'narrative', 'badge', 'text',\n 'markdown', 'ref', 'custom', 'actions',\n]);\n\nconst ALLOWED_KEYS = new Set(['id', 'meta', 'requires', 'provides', 'view', 'card_data', 'compute', 'source_defs']);\n\nfunction validateNode(node: unknown): ValidationResult {\n const errors: string[] = [];\n\n if (!node || typeof node !== 'object' || Array.isArray(node)) {\n return { ok: false, errors: ['Node must be a non-null object'] };\n }\n\n const n = node as Record<string, unknown>;\n\n if (typeof n.id !== 'string' || !n.id) errors.push('id: required, must be a non-empty string');\n\n for (const key of Object.keys(n)) {\n if (!ALLOWED_KEYS.has(key)) errors.push(`Unknown top-level key: \"${key}\"`);\n }\n\n if (n.card_data == null || typeof n.card_data !== 'object' || Array.isArray(n.card_data)) {\n errors.push('card_data: required, must be an object');\n }\n\n if (n.meta != null) {\n if (typeof n.meta !== 'object' || Array.isArray(n.meta)) {\n errors.push('meta: must be an object');\n } else {\n const meta = n.meta as Record<string, unknown>;\n if (meta.title != null && typeof meta.title !== 'string') errors.push('meta.title: must be a string');\n if (meta.tags != null && !Array.isArray(meta.tags)) errors.push('meta.tags: must be an array');\n }\n }\n\n if (n.requires != null && !Array.isArray(n.requires)) errors.push('requires: must be an array of strings');\n\n if (n.provides != null) {\n if (!Array.isArray(n.provides)) {\n errors.push('provides: must be an array of { bindTo, ref } bindings');\n } else {\n (n.provides as unknown[]).forEach((p, i) => {\n if (!p || typeof p !== 'object' || Array.isArray(p)) {\n errors.push(`provides[${i}]: must be an object with bindTo and ref`);\n } else {\n const b = p as Record<string, unknown>;\n if (typeof b.bindTo !== 'string' || !b.bindTo) errors.push(`provides[${i}]: missing required \"bindTo\" string`);\n if (typeof b.ref !== 'string' || !b.ref) errors.push(`provides[${i}]: missing required \"ref\" string`);\n }\n });\n }\n }\n\n // compute — ordered array of { bindTo, expr } steps\n if (n.compute != null) {\n if (!Array.isArray(n.compute)) {\n errors.push('compute: must be an array of compute steps');\n } else {\n (n.compute as unknown[]).forEach((step, i) => {\n if (!step || typeof step !== 'object' || Array.isArray(step)) {\n errors.push(`compute[${i}]: must be a compute step object`);\n } else {\n const s = step as Record<string, unknown>;\n if (typeof s.bindTo !== 'string' || !s.bindTo) errors.push(`compute[${i}]: missing required \"bindTo\" property`);\n if (typeof s.expr !== 'string' || !s.expr) errors.push(`compute[${i}]: missing required \"expr\" string (JSONata expression)`);\n }\n });\n }\n }\n\n if (n.source_defs != null) {\n if (!Array.isArray(n.source_defs)) {\n errors.push('source_defs: must be an array');\n } else {\n const bindTos = new Set<string>();\n const outputFiles = new Set<string>();\n (n.source_defs as unknown[]).forEach((src, i) => {\n if (!src || typeof src !== 'object' || Array.isArray(src)) {\n errors.push(`source_defs[${i}]: must be an object`);\n } else {\n const s = src as Record<string, unknown>;\n if (typeof s.bindTo !== 'string' || !s.bindTo) {\n errors.push(`source_defs[${i}]: missing required \"bindTo\" property`);\n } else {\n if (bindTos.has(s.bindTo)) {\n errors.push(`source_defs[${i}]: bindTo \"${s.bindTo}\" is not unique across source_defs`);\n }\n bindTos.add(s.bindTo);\n }\n if (typeof s.outputFile !== 'string' || !s.outputFile) {\n errors.push(`source_defs[${i}]: missing required \"outputFile\" property`);\n } else {\n if (outputFiles.has(s.outputFile)) {\n errors.push(`source_defs[${i}]: outputFile \"${s.outputFile}\" is not unique across source_defs`);\n }\n outputFiles.add(s.outputFile);\n }\n if (s.optionalForCompletionGating != null && typeof s.optionalForCompletionGating !== 'boolean') {\n errors.push(`source_defs[${i}]: optionalForCompletionGating must be a boolean`);\n }\n }\n });\n }\n }\n\n if (n.view != null) {\n if (typeof n.view !== 'object' || Array.isArray(n.view)) {\n errors.push('view: must be an object');\n } else {\n const view = n.view as Record<string, unknown>;\n if (!Array.isArray(view.elements) || view.elements.length === 0) {\n errors.push('view.elements: required, must be a non-empty array');\n } else {\n (view.elements as Record<string, unknown>[]).forEach((elem, i) => {\n if (!elem || typeof elem !== 'object') { errors.push(`view.elements[${i}]: must be an object`); return; }\n if (!elem.kind || typeof elem.kind !== 'string') {\n errors.push(`view.elements[${i}].kind: required, must be a string`);\n } else if (!VALID_ELEMENT_KINDS.has(elem.kind as string)) {\n errors.push(`view.elements[${i}].kind: unknown kind \"${elem.kind}\". Valid: ${[...VALID_ELEMENT_KINDS].join(', ')}`);\n }\n if (elem.data != null && (typeof elem.data !== 'object' || Array.isArray(elem.data))) {\n errors.push(`view.elements[${i}].data: must be an object`);\n }\n });\n }\n if (view.layout != null && (typeof view.layout !== 'object' || Array.isArray(view.layout))) errors.push('view.layout: must be an object');\n if (view.features != null && (typeof view.features !== 'object' || Array.isArray(view.features))) errors.push('view.features: must be an object');\n }\n }\n\n return { ok: errors.length === 0, errors };\n}\n\n/**\n * Enrich source_defs with execution context for template interpolation and prompt rendering.\n * Pure function: no side effects, returns new enriched source_defs array.\n * \n * @param source_defs - Array of source definitions\n * @param context - Execution context containing requires, sourcesData, computed_values\n * @returns Promise resolving to a new array of source_defs with _projections attached.\n * Each _projections entry is the evaluated result of the corresponding projections expression.\n */\nasync function enrichSources(\n source_defs: any[] | undefined,\n context: {\n card_data?: Record<string, any>;\n requires?: Record<string, any>;\n sourcesData?: Record<string, any>; // unused post-projections, kept for call-site compat\n computed_values?: Record<string, any>; // unused post-projections, kept for call-site compat\n }\n): Promise<any[]> {\n if (!source_defs || source_defs.length === 0) return [];\n\n const evalCtx = {\n card_data: context.card_data ?? {},\n requires: context.requires ?? {},\n };\n\n return Promise.all(\n source_defs.map(async (src: any) => {\n const _projections: Record<string, unknown> = {};\n if (src.projections && typeof src.projections === 'object' && !Array.isArray(src.projections)) {\n for (const [key, expr] of Object.entries(src.projections as Record<string, string>)) {\n if (typeof expr === 'string' && expr.trim().length > 0) {\n try {\n _projections[key] = await jsonata(expr).evaluate(evalCtx);\n } catch {\n _projections[key] = undefined;\n }\n }\n }\n }\n return { ...src, _projections };\n })\n );\n}\n\nfunction enrichSourcesSync(\n source_defs: any[] | undefined,\n context: {\n card_data?: Record<string, any>;\n requires?: Record<string, any>;\n }\n): any[] {\n if (!source_defs || source_defs.length === 0) return [];\n\n const evalCtx = {\n card_data: context.card_data ?? {},\n requires: context.requires ?? {},\n };\n\n return source_defs.map((src: any) => {\n const _projections: Record<string, unknown> = {};\n if (src.projections && typeof src.projections === 'object' && !Array.isArray(src.projections)) {\n for (const [key, expr] of Object.entries(src.projections as Record<string, string>)) {\n if (typeof expr === 'string' && expr.trim().length > 0) {\n try {\n _projections[key] = jsonataSync(expr).evaluate(evalCtx);\n } catch {\n _projections[key] = undefined;\n }\n }\n }\n }\n return { ...src, _projections };\n });\n}\n\nexport const CardCompute = {\n run,\n runSync,\n eval: evalExpr,\n resolve,\n validate: validateNode,\n enrichSources,\n enrichSourcesSync,\n};\n\nexport {\n validateLiveCard,\n validateLiveCardSchema,\n validateLiveCardRuntimeExpressions,\n validateLiveCardDefinition,\n} from './schema-validator.js';\n\nexport default CardCompute;\n\n","/**\n * board-live-cards-lib — Pure logic library for the board-live-cards CLI.\n *\n * Merged from:\n * board-live-cards-all-stores.ts\n * board-live-cards-lib-types.ts\n * board-live-cards-lib-board-status.ts\n * board-live-cards-lib-card-handler.ts\n * board-live-cards-cli-board-commands.ts\n * board-live-cards-cli-card-commands.ts\n * board-live-cards-cli-callbacks.ts\n *\n * Zero platform imports. All storage is injected via adapter interfaces.\n * Safe for Node, browser, and neutral (V8/PyMiniRacer) bundles.\n */\n\nimport type { KVStorage, BlobStorage, KindValueRef } from './storage-interface.js';\nimport { serializeRef } from './storage-interface.js';\nimport { parseExecutionRef, serializeExecutionRef } from './execution-interface.js';\nimport type { ExecutionRef } from './execution-interface.js';\nimport type { GraphEvent, TaskConfig, GraphConfig } from '../../event-graph/types.js';\nimport type { LiveGraph, LiveGraphSnapshot } from '../../continuous-event-graph/types.js';\nimport { schedule } from '../../continuous-event-graph/schedule.js';\nimport type { TaskHandlerFn } from '../../continuous-event-graph/reactive.js';\nimport { CardCompute } from '../../card-compute/index.js';\nimport type { ComputeNode, ComputeStep, ComputeSource } from '../../card-compute/index.js';\nexport type { DispatchResult, InvocationAdapter } from './process-interface.js';\n\n// ============================================================================\n// ---- from board-live-cards-all-stores.ts ----\n// ============================================================================\n\n// ============================================================================\n// Card store — types\n// ============================================================================\n\nexport interface LiveCard {\n id: string;\n [key: string]: unknown;\n}\n\nexport interface CardIndexEntry {\n /** Storage-specific address (file path, Cosmos doc id, localStorage key). */\n key: string;\n /** Checksum of card content — computed by the adapter at write time. */\n checksum: string;\n updatedAt: string;\n}\n\nexport type CardIndex = Record<string, CardIndexEntry>;\nexport type CardChecksumIndex = Record<string, string>;\n\n/**\n * Per-card entry stored in the card-upsert KV cache (one key per cardId).\n * Lives alongside the board journal — NOT inside the board snapshot.\n * Purpose: dedup gate to avoid redundant task-upsert journal entries.\n *\n * Write order: journal.append() THEN kv.write() — so a crash between the two\n * leaves the journal entry intact (board is correct) and the KV stale (next\n * upsert will see \"changed\" and re-append; addNode is idempotent in the board).\n */\nexport interface CardUpsertIndexEntry {\n /** Logical reference to the card blob — absolute path for fs, blob name for cloud. */\n blobRef: string;\n /** SHA-256 of stable-JSON-serialised taskConfig. Dedup key. */\n taskConfigHash: string;\n updatedAt: string;\n}\n\n// ============================================================================\n// CardStorageAdapter — injected by the caller\n// ============================================================================\n\nexport interface CardStorageAdapter {\n readIndex(): CardIndex | null;\n writeIndex(index: CardIndex): void;\n readCard(key: string): LiveCard | null;\n /** Write card content; returns checksum of what was written. */\n writeCard(key: string, card: LiveCard): string;\n cardExists(key: string): boolean;\n defaultCardKey(cardId: string): string;\n}\n\n// ============================================================================\n// CardStore — board one-cycle (read-only)\n// ============================================================================\n\nexport interface CardStore {\n readCard(id: string): LiveCard | null;\n readCardKey(id: string): string | null;\n readAllCards(): LiveCard[];\n readChecksumIndex(): CardChecksumIndex;\n changedSince(snapshotChecksumIndex: CardChecksumIndex): string[];\n}\n\n// ============================================================================\n// CardAdminStore — CLI write interface\n// ============================================================================\n\nexport interface CardUpsertValidation {\n ok: boolean;\n error?: string;\n}\n\nexport interface CardAdminStore extends CardStore {\n validateUpsert(id: string, cardKey: string): CardUpsertValidation;\n writeCard(id: string, card: LiveCard, cardKey?: string): void;\n patchCard(id: string, jsonPath: string, value: unknown): void;\n removeCard(id: string): void;\n readIndex(): CardIndex;\n}\n\n// ============================================================================\n// createCardStore — pure logic factory\n// ============================================================================\n\nexport function createCardStore(adapter: CardStorageAdapter, onWarn?: (msg: string) => void): CardAdminStore {\n function loadIndex(): CardIndex {\n return adapter.readIndex() ?? {};\n }\n\n function applyJsonPath(obj: Record<string, unknown>, jsonPath: string, value: unknown): Record<string, unknown> {\n const segments = String(jsonPath || '').split('.').filter(Boolean);\n if (segments.length === 0) {\n return (value && typeof value === 'object' && !Array.isArray(value))\n ? value as Record<string, unknown>\n : { value };\n }\n\n const out: Record<string, unknown> = { ...obj };\n let target: Record<string, unknown> = out;\n for (let i = 0; i < segments.length - 1; i++) {\n const key = segments[i];\n const cur = target[key];\n const next = (cur && typeof cur === 'object' && !Array.isArray(cur))\n ? { ...(cur as Record<string, unknown>) }\n : {};\n target[key] = next;\n target = next;\n }\n target[segments[segments.length - 1]] = value;\n return out;\n }\n\n return {\n readCard(id: string): LiveCard | null {\n const entry = loadIndex()[id];\n if (!entry || !adapter.cardExists(entry.key)) return null;\n return adapter.readCard(entry.key);\n },\n\n readCardKey(id: string): string | null {\n return loadIndex()[id]?.key ?? null;\n },\n\n readAllCards(): LiveCard[] {\n const cards: LiveCard[] = [];\n for (const [id, entry] of Object.entries(loadIndex())) {\n if (!adapter.cardExists(entry.key)) continue;\n const card = adapter.readCard(entry.key);\n if (card) cards.push(card);\n else onWarn?.(`[card-store] could not read card \"${id}\" at key \"${entry.key}\"`);\n }\n return cards;\n },\n\n readChecksumIndex(): CardChecksumIndex {\n const result: CardChecksumIndex = {};\n for (const [id, entry] of Object.entries(loadIndex())) result[id] = entry.checksum;\n return result;\n },\n\n changedSince(snapshotChecksumIndex: CardChecksumIndex): string[] {\n const localIndex = loadIndex();\n const changed: string[] = [];\n for (const [id, entry] of Object.entries(localIndex)) {\n if (snapshotChecksumIndex[id] !== entry.checksum) changed.push(id);\n }\n for (const id of Object.keys(snapshotChecksumIndex)) {\n if (!localIndex[id]) changed.push(id);\n }\n return changed;\n },\n\n validateUpsert(id: string, cardKey: string): CardUpsertValidation {\n const index = loadIndex();\n const existingById = index[id];\n const existingByKey = Object.entries(index).find(([, e]) => e.key === cardKey);\n if (existingById && existingById.key !== cardKey)\n return { ok: false, error: `Card id \"${id}\" is already mapped to key \"${existingById.key}\", cannot remap to \"${cardKey}\"` };\n if (existingByKey && existingByKey[0] !== id)\n return { ok: false, error: `Key \"${cardKey}\" is already mapped to card id \"${existingByKey[0]}\", cannot remap to \"${id}\"` };\n return { ok: true };\n },\n\n writeCard(id: string, card: LiveCard, cardKey?: string): void {\n const index = loadIndex();\n const resolvedKey = cardKey ?? index[id]?.key ?? adapter.defaultCardKey(id);\n const checksum = adapter.writeCard(resolvedKey, card);\n index[id] = { key: resolvedKey, checksum, updatedAt: new Date().toISOString() };\n adapter.writeIndex(index);\n },\n\n patchCard(id: string, jsonPath: string, value: unknown): void {\n const index = loadIndex();\n const entry = index[id];\n if (!entry || !adapter.cardExists(entry.key)) {\n throw new Error(`card \"${id}\" not found`);\n }\n const current = adapter.readCard(entry.key);\n if (!current || typeof current !== 'object' || Array.isArray(current)) {\n throw new Error(`card \"${id}\" is not patchable`);\n }\n const next = applyJsonPath(current as Record<string, unknown>, jsonPath, value) as LiveCard;\n const checksum = adapter.writeCard(entry.key, next);\n index[id] = { key: entry.key, checksum, updatedAt: new Date().toISOString() };\n adapter.writeIndex(index);\n },\n\n removeCard(id: string): void {\n const index = loadIndex();\n if (!index[id]) return;\n delete index[id];\n adapter.writeIndex(index);\n },\n\n readIndex(): CardIndex {\n return loadIndex();\n },\n };\n}\n\n// ============================================================================\n// FetchedSourcesStore\n// ============================================================================\n\nexport interface FetchedSourcesStore {\n /** Read committed source content. Returns parsed JSON or raw string; null if not yet committed. */\n readSourceData(cardId: string, outputFile: string): unknown;\n /** Stage incoming source data under deliveryToken. resolveRef converts the ref to content bytes. */\n ingestSourceDataStaged(cardId: string, outputFile: string, ref: KindValueRef, deliveryToken: string): void;\n /** Move staged data to live position. Returns false if staged entry is absent (stale delivery). */\n commitSourceData(cardId: string, outputFile: string, deliveryToken: string): boolean;\n /** True if live (committed) source data exists for this outputFile. */\n hasSource(cardId: string, outputFile: string): boolean;\n}\n\nexport function createFetchedSourcesStore(\n blob: BlobStorage,\n resolveRef: (ref: KindValueRef) => string,\n): FetchedSourcesStore {\n return {\n readSourceData(cardId, outputFile): unknown {\n const raw = blob.read(`${cardId}/${outputFile}`);\n if (raw == null) return null;\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try { return JSON.parse(trimmed); } catch { return trimmed; }\n },\n ingestSourceDataStaged(cardId, outputFile, ref, deliveryToken): void {\n const content = resolveRef(ref);\n blob.write(`${cardId}/.staged/${deliveryToken}/${outputFile}`, content);\n },\n commitSourceData(cardId, outputFile, deliveryToken): boolean {\n const stagedKey = `${cardId}/.staged/${deliveryToken}/${outputFile}`;\n const content = blob.read(stagedKey);\n if (content == null) return false;\n blob.write(`${cardId}/${outputFile}`, content);\n blob.remove(stagedKey);\n return true;\n },\n hasSource(cardId, outputFile): boolean {\n return blob.exists(`${cardId}/${outputFile}`);\n },\n };\n}\n\n// ============================================================================\n// Journal store — types\n// ============================================================================\n\nexport interface JournalEntry {\n id: string;\n event: GraphEvent;\n}\n\nexport interface JournalStorageAdapter {\n readAllEntries(): JournalEntry[];\n appendEntry(entry: JournalEntry): void;\n generateId(): string;\n}\n\nexport interface JournalStore {\n readEntriesAfterCursor(cursor: string): { events: GraphEvent[]; newCursor: string };\n pendingCount(cursor: string): number;\n}\n\nexport interface JournalAdminStore extends JournalStore {\n appendEvent(event: GraphEvent): void;\n}\n\nexport function createJournalStore(adapter: JournalStorageAdapter): JournalAdminStore {\n function entriesAfterCursor(cursor: string): JournalEntry[] {\n const all = adapter.readAllEntries();\n if (!cursor) return all;\n const idx = all.findIndex(e => e.id === cursor);\n return idx === -1 ? all : all.slice(idx + 1);\n }\n\n return {\n readEntriesAfterCursor(cursor: string): { events: GraphEvent[]; newCursor: string } {\n const entries = entriesAfterCursor(cursor);\n if (entries.length === 0) return { events: [], newCursor: cursor };\n return { events: entries.map(e => e.event), newCursor: entries[entries.length - 1].id };\n },\n\n pendingCount(cursor: string): number {\n return entriesAfterCursor(cursor).length;\n },\n\n appendEvent(event: GraphEvent): void {\n adapter.appendEntry({ id: adapter.generateId(), event });\n },\n };\n}\n\n// ============================================================================\n// ExecutionRequest store\n// ============================================================================\n\nexport interface ExecutionRequestEntry {\n taskKind: string;\n payload: unknown;\n}\n\nexport interface ExecutionRequestStore {\n appendEntries(journalId: string, entries: ExecutionRequestEntry[]): void;\n dispatchEntriesForJournalId(journalId: string, processorFn: (entry: ExecutionRequestEntry) => void): void;\n}\n\nexport function createExecutionRequestStore(\n kv: KVStorage,\n onDispatchFailed: (entry: ExecutionRequestEntry, error: string) => void,\n): ExecutionRequestStore {\n return {\n appendEntries(journalId: string, entries: ExecutionRequestEntry[]): void {\n if (!journalId || entries.length === 0) return;\n const existing = (kv.read(journalId) as ExecutionRequestEntry[] | null) ?? [];\n kv.write(journalId, [...existing, ...entries]);\n },\n\n dispatchEntriesForJournalId(journalId: string, processorFn: (entry: ExecutionRequestEntry) => void): void {\n if (!journalId) return;\n const entries = kv.read(journalId) as ExecutionRequestEntry[] | null;\n if (!entries || entries.length === 0) return;\n for (const entry of entries) {\n try { processorFn(entry); } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n try { onDispatchFailed(entry, msg); } catch { /* guard against failure in error handler */ }\n }\n }\n kv.delete(journalId);\n },\n };\n}\n\n// ============================================================================\n// StateSnapshot store\n// ============================================================================\n\nexport const SNAPSHOT_SCHEMA_VERSION_V1 = 'v1';\n\nexport const BOARD_GRAPH_KEY = 'board/graph';\nexport const BOARD_LAST_JOURNAL_PROCESSED_ID_KEY = 'board/lastJournalProcessedId';\n\nexport function cardRuntimeKey(cardId: string): string {\n return `cards/${cardId}/runtime`;\n}\n\nexport function cardFetchedSourcesManifestKey(cardId: string): string {\n return `cards/${cardId}/fetched-sources-manifest`;\n}\n\nexport interface CardRuntimeSnapshot {\n _sources: Record<string, { lastRequestedAt?: string; lastFetchedAt?: string; queueRequestedAt?: string }>;\n _lastExecutionCount?: number;\n}\n\nexport interface CardRuntimeStore {\n readRuntime(cardId: string): CardRuntimeSnapshot;\n writeRuntime(cardId: string, state: CardRuntimeSnapshot): void;\n}\n\nexport function createCardRuntimeStore(kv: KVStorage): CardRuntimeStore {\n return {\n readRuntime(cardId) {\n return (kv.read(cardRuntimeKey(cardId)) as CardRuntimeSnapshot | null) ?? { _sources: {} };\n },\n writeRuntime(cardId, state) {\n kv.write(cardRuntimeKey(cardId), state);\n },\n };\n}\n\nexport interface FetchedSourceManifestEntry {\n outputFile: string;\n blobRef: string;\n fetchedAt: string;\n sourceChecksum?: string;\n contentType?: string;\n sizeBytes?: number;\n}\n\nexport interface StateSnapshotReadView {\n version: string | null;\n values: Record<string, unknown>;\n}\n\nexport interface StateSnapshotCommitEnvelope {\n schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION_V1;\n expectedVersion: string | null;\n commitId: string;\n committedAt: string;\n deleteKeys: string[];\n shallowMerge: Record<string, unknown>;\n}\n\nexport interface StateSnapshotCommitSuccess {\n ok: true;\n newVersion: string;\n}\n\nexport interface StateSnapshotCommitVersionMismatch {\n ok: false;\n reason: 'version-mismatch';\n currentVersion: string | null;\n}\n\nexport type StateSnapshotCommitResult =\n | StateSnapshotCommitSuccess\n | StateSnapshotCommitVersionMismatch;\n\nexport interface StateSnapshotStorageAdapter {\n readValues(scopeId: string): StateSnapshotReadView;\n writeValues(scopeId: string, nextValues: Record<string, unknown>, deletedKeys: string[]): string;\n}\n\nexport interface StateSnapshotStore {\n readSnapshot(scopeId: string): StateSnapshotReadView;\n commitSnapshot(scopeId: string, envelope: StateSnapshotCommitEnvelope): StateSnapshotCommitResult;\n}\n\nexport function applyStateSnapshotCommitEnvelope(\n current: Record<string, unknown>,\n envelope: Pick<StateSnapshotCommitEnvelope, 'deleteKeys' | 'shallowMerge'>,\n): Record<string, unknown> {\n const next: Record<string, unknown> = { ...current };\n for (const key of envelope.deleteKeys) {\n delete next[key];\n }\n return { ...next, ...envelope.shallowMerge };\n}\n\nexport function createStateSnapshotStore(adapter: StateSnapshotStorageAdapter): StateSnapshotStore {\n return {\n readSnapshot(scopeId: string): StateSnapshotReadView {\n return adapter.readValues(scopeId);\n },\n\n commitSnapshot(scopeId: string, envelope: StateSnapshotCommitEnvelope): StateSnapshotCommitResult {\n if (envelope.schemaVersion !== SNAPSHOT_SCHEMA_VERSION_V1) {\n throw new Error(`Unsupported snapshot schema version: ${envelope.schemaVersion}`);\n }\n const current = adapter.readValues(scopeId);\n if (current.version !== envelope.expectedVersion) {\n return { ok: false, reason: 'version-mismatch', currentVersion: current.version };\n }\n const nextValues = applyStateSnapshotCommitEnvelope(current.values, envelope);\n const newVersion = adapter.writeValues(scopeId, nextValues, envelope.deleteKeys);\n return { ok: true, newVersion };\n },\n };\n}\n\n// ============================================================================\n// BoardConfigStore\n// ============================================================================\n\nexport interface BoardConfigStore {\n readTaskExecutorRef(): ExecutionRef | undefined;\n writeTaskExecutorRef(ref: ExecutionRef): void;\n readChatHandlerRef(): ExecutionRef | undefined;\n writeChatHandlerRef(ref: ExecutionRef): void;\n readCardStoreRef(): string | null;\n writeCardStoreRef(ref: string): void;\n readOutputsStoreRef(): string | null;\n writeOutputsStoreRef(ref: string): void;\n /** @deprecated use readChatHandlerRef */\n readChatHandler(): string | undefined;\n /** @deprecated use writeChatHandlerRef */\n writeChatHandler(value: string): void;\n}\n\nexport function createBoardConfigStore(kv: KVStorage): BoardConfigStore {\n function readKey(key: string): string | null {\n const v = kv.read(key);\n if (v == null) return null;\n return typeof v === 'string' ? v : JSON.stringify(v);\n }\n\n return {\n readTaskExecutorRef(): ExecutionRef | undefined {\n const raw = readKey('task-executor');\n if (!raw?.trim()) return undefined;\n return parseExecutionRef(raw.trim());\n },\n\n writeTaskExecutorRef(ref: ExecutionRef): void {\n kv.write('task-executor', serializeExecutionRef(ref));\n },\n\n readChatHandlerRef(): ExecutionRef | undefined {\n const raw = readKey('chat-handler');\n if (!raw?.trim()) return undefined;\n return parseExecutionRef(raw.trim());\n },\n\n writeChatHandlerRef(ref: ExecutionRef): void {\n kv.write('chat-handler', serializeExecutionRef(ref));\n },\n\n readCardStoreRef(): string | null {\n return readKey('card-store-ref');\n },\n\n writeCardStoreRef(ref: string): void {\n kv.write('card-store-ref', ref);\n },\n\n readOutputsStoreRef(): string | null {\n return readKey('outputs-store-ref');\n },\n\n writeOutputsStoreRef(ref: string): void {\n kv.write('outputs-store-ref', ref);\n },\n\n readChatHandler(): string | undefined {\n return readKey('chat-handler')?.trim() || undefined;\n },\n\n writeChatHandler(value: string): void {\n kv.write('chat-handler', value);\n },\n };\n}\n\n// ============================================================================\n// PublishedOutputsStore\n// ============================================================================\n\nexport type OutputStoreEvent =\n | { kind: 'computed_values'; cardId: string; values: Record<string, unknown> }\n | { kind: 'data_object'; key: string; payload: unknown }\n | { kind: 'status'; status: unknown };\n\nexport interface PublishedOutputsStore {\n writeComputedValues(cardId: string, values: Record<string, unknown>): void;\n readComputedValues(cardId: string): unknown | null;\n readAllComputedValues(): Record<string, unknown>;\n writeDataObjects(data: Record<string, unknown>): void;\n readDataObject(key: string): unknown | null;\n readAllDataObjects(): Record<string, unknown>;\n writeStatusSnapshot(status: unknown): void;\n readStatusSnapshot(): unknown | null;\n}\n\nexport function createPublishedOutputsStore(kv: KVStorage): PublishedOutputsStore {\n return {\n writeComputedValues(cardId, values) {\n kv.write(`cards/${cardId}/computed_values`, values);\n },\n readComputedValues(cardId) { return kv.read(`cards/${cardId}/computed_values`); },\n readAllComputedValues() {\n const out: Record<string, unknown> = {};\n for (const key of kv.listKeys('cards/')) {\n const m = key.match(/^cards\\/([^/]+)\\/computed_values$/);\n if (m) out[m[1]] = kv.read(key);\n }\n return out;\n },\n writeDataObjects(data) {\n for (const [token, payload] of Object.entries(data)) {\n if (!token) continue;\n kv.write(`data-objects/${token}`, payload);\n }\n },\n readDataObject(key) { return kv.read(`data-objects/${key}`); },\n readAllDataObjects() {\n const out: Record<string, unknown> = {};\n for (const key of kv.listKeys('data-objects/')) {\n out[key.slice('data-objects/'.length)] = kv.read(key);\n }\n return out;\n },\n writeStatusSnapshot(status) {\n kv.write('status', status);\n },\n readStatusSnapshot() { return kv.read('status'); },\n };\n}\n\n// ============================================================================\n// Future-facing blob and read-model cache interfaces\n// ============================================================================\n\nexport interface FetchedSourcesBlobStore {\n readBlob(blobRef: string): Promise<unknown | null>;\n}\n\nexport interface PublishedBoardStatusCache {\n writeStatusBestEffort(scopeId: string, statusPayload: unknown): Promise<void>;\n readStatus(scopeId: string): Promise<unknown | null>;\n}\n\n// ============================================================================\n// ---- from board-live-cards-lib-types.ts ----\n// ============================================================================\n\nexport interface SourceRuntimeEntry {\n lastRequestedAt?: string;\n lastFetchedAt?: string;\n lastError?: string;\n queueRequestedAt?: string;\n}\n\nexport type FetchRuntimeEntry = SourceRuntimeEntry;\n\nexport interface SourceTokenPayload {\n cbk: string;\n rg: string;\n br: string;\n cid: string;\n b: string;\n d: string;\n cs?: string;\n rqt: string;\n}\n\nexport function isSourceInFlight(entry: FetchRuntimeEntry | undefined): boolean {\n if (!entry?.lastRequestedAt) return false;\n return !entry.lastFetchedAt || entry.lastFetchedAt < entry.lastRequestedAt;\n}\n\nexport function decideSourceAction(\n entry: FetchRuntimeEntry | undefined,\n queueRequestedAt: string,\n): 'dispatch' | 'in-flight' | 'idle' {\n if (!entry?.lastRequestedAt) return 'dispatch';\n const inFlight = isSourceInFlight(entry);\n if (inFlight) return 'in-flight';\n if (!entry.lastFetchedAt) return 'dispatch';\n if (entry.lastFetchedAt < queueRequestedAt) return 'dispatch';\n return 'idle';\n}\n\nexport function nextEntryAfterFetchDelivery<T extends FetchRuntimeEntry>(\n entry: T,\n fetchedAt: string,\n): T {\n const next = { ...entry, lastFetchedAt: fetchedAt };\n delete (next as FetchRuntimeEntry).lastError;\n return next as T;\n}\n\nexport function nextEntryAfterFetchFailure<T extends FetchRuntimeEntry>(\n entry: T,\n reason: string,\n): T {\n const next = { ...entry, lastError: reason };\n delete (next as FetchRuntimeEntry).lastFetchedAt;\n return next as T;\n}\n\nexport interface CardHandlerAdapters {\n cardStore: CardStore;\n cardRuntimeStore: CardRuntimeStore;\n fetchedSourcesStore: FetchedSourcesStore;\n outputStore: PublishedOutputsStore;\n executionRequestStore: ExecutionRequestStore;\n}\n\nexport interface CommandResponse<T extends Record<string, unknown> = Record<string, unknown>> {\n status: 'success' | 'error';\n data: T;\n error?: string;\n}\n\nexport const Resp = {\n success<T extends Record<string, unknown>>(data: T): CommandResponse<T> {\n return { status: 'success', data };\n },\n\n error(error: string, data: Record<string, unknown> = {}): CommandResponse {\n return { status: 'error', data, error };\n },\n\n getStatus(r: CommandResponse): 'success' | 'error' {\n return r.status;\n },\n\n getData<T extends Record<string, unknown>>(r: CommandResponse<T>): T {\n return r.data;\n },\n\n isSuccess(r: CommandResponse): boolean {\n return r.status === 'success';\n },\n} as const;\n\n// ============================================================================\n// ---- from board-live-cards-lib-board-status.ts ----\n// ============================================================================\n\nexport interface BoardStatusCard {\n name: string;\n status: string;\n error?: {\n message: string;\n code?: string;\n at?: string;\n source?: 'task-runtime' | 'source-fetch' | 'timeout' | 'unknown';\n };\n requires: string[];\n requires_satisfied: string[];\n requires_missing: string[];\n provides_declared: string[];\n provides_runtime: string[];\n blocked_by: string[];\n unblocks: string[];\n runtime: {\n attempt_count: number;\n restart_count: number;\n in_progress_since: string | null;\n last_transition_at: string | null;\n last_completed_at: string | null;\n last_restarted_at: string | null;\n status_age_ms: number | null;\n };\n}\n\nexport interface BoardStatusObject {\n schema_version: 'v1';\n meta: {\n board: {\n path: string;\n };\n };\n summary: {\n card_count: number;\n completed: number;\n eligible: number;\n pending: number;\n blocked: number;\n unresolved: number;\n failed?: number;\n in_progress?: number;\n orphan_cards?: number;\n topology?: {\n edge_count: number;\n max_fan_out_card: string | null;\n max_fan_out: number;\n };\n };\n cards: BoardStatusCard[];\n}\n\nexport function buildBoardStatusObject(boardPath: string, live: LiveGraph): BoardStatusObject {\n const taskState = live.state.tasks;\n const taskConfig = live.config.tasks;\n const cardNames = Object.keys(taskState);\n const sched = schedule(live);\n\n const statusCounts = {\n completed: 0,\n failed: 0,\n in_progress: 0,\n pending: 0,\n blocked: 0,\n unresolved: 0,\n };\n\n const waitingByCard = new Map<string, string[]>();\n for (const p of sched.pending) waitingByCard.set(p.taskName, p.waitingOn);\n for (const u of sched.unresolved) waitingByCard.set(u.taskName, u.missingTokens);\n for (const b of sched.blocked) waitingByCard.set(b.taskName, b.failedTokens);\n\n const dependentsByToken = new Map<string, string[]>();\n for (const [name, cfg] of Object.entries(taskConfig)) {\n for (const token of cfg.requires ?? []) {\n const dependents = dependentsByToken.get(token) ?? [];\n dependents.push(name);\n dependentsByToken.set(token, dependents);\n }\n }\n\n const cards: BoardStatusCard[] = cardNames.sort().map((name) => {\n const state = taskState[name] as {\n status: string;\n data?: Record<string, unknown>;\n error?: string;\n startedAt?: string;\n completedAt?: string;\n failedAt?: string;\n lastUpdated?: string;\n executionCount?: number;\n retryCount?: number;\n };\n const cfg = taskConfig[name] ?? { requires: [], provides: [] };\n\n if (state.status === 'completed') statusCounts.completed += 1;\n else if (state.status === 'failed') statusCounts.failed += 1;\n else if (state.status === 'in-progress') statusCounts.in_progress += 1;\n\n const requires = cfg.requires ?? [];\n const provides = cfg.provides ?? [];\n const runtimeKeys = Object.keys(state.data ?? {}).sort();\n const requiresSatisfied = requires.filter(token => live.state.availableOutputs.includes(token));\n const requiresMissing = requires.filter(token => !live.state.availableOutputs.includes(token));\n const blockedBy = waitingByCard.get(name) ?? requiresMissing;\n\n const unblocks = new Set<string>();\n for (const token of provides) {\n for (const dependent of dependentsByToken.get(token) ?? []) {\n if (dependent !== name) unblocks.add(dependent);\n }\n }\n\n const lastFailureAt = state.failedAt;\n const error = state.error\n ? {\n message: state.error,\n code: 'TASK_FAILED',\n at: lastFailureAt,\n source: 'task-runtime' as const,\n }\n : undefined;\n\n return {\n name,\n status: state.status,\n error,\n requires,\n requires_satisfied: requiresSatisfied,\n requires_missing: requiresMissing,\n provides_declared: provides,\n provides_runtime: runtimeKeys,\n blocked_by: blockedBy,\n unblocks: Array.from(unblocks).sort(),\n runtime: {\n attempt_count: state.executionCount ?? 0,\n restart_count: state.retryCount ?? 0,\n in_progress_since: state.status === 'in-progress' ? (state.startedAt ?? null) : null,\n last_transition_at: state.lastUpdated ?? null,\n last_completed_at: state.completedAt ?? null,\n last_restarted_at: state.startedAt ?? null,\n // Keep status snapshots immutable across reads: this field must not depend on wall-clock pull time.\n status_age_ms: state.lastUpdated ? 0 : null,\n },\n };\n });\n\n statusCounts.pending = sched.pending.length;\n statusCounts.blocked = sched.blocked.length;\n statusCounts.unresolved = sched.unresolved.length;\n\n const fanOut = cards\n .map(c => ({ name: c.name, fanOut: c.unblocks.length }))\n .sort((a, b) => b.fanOut - a.fanOut || a.name.localeCompare(b.name));\n const maxFanOut = fanOut.length > 0 ? fanOut[0] : { name: null, fanOut: 0 };\n\n const allRequires = new Set<string>();\n for (const cfg of Object.values(taskConfig)) {\n for (const r of cfg.requires ?? []) allRequires.add(r);\n }\n let orphanCards = 0;\n for (const [name, cfg] of Object.entries(taskConfig)) {\n const requiresNone = (cfg.requires ?? []).length === 0;\n const providesList = cfg.provides ?? [];\n const feedsAny = providesList.some(p => (dependentsByToken.get(p) ?? []).some(d => d !== name));\n if (requiresNone && !feedsAny) orphanCards += 1;\n }\n\n return {\n schema_version: 'v1',\n meta: { board: { path: boardPath } },\n summary: {\n card_count: cardNames.length,\n completed: statusCounts.completed,\n eligible: sched.eligible.length,\n pending: statusCounts.pending,\n blocked: statusCounts.blocked,\n unresolved: statusCounts.unresolved,\n failed: statusCounts.failed,\n in_progress: statusCounts.in_progress,\n orphan_cards: orphanCards,\n topology: {\n edge_count: Array.from(allRequires).length,\n max_fan_out_card: maxFanOut.name,\n max_fan_out: maxFanOut.fanOut,\n },\n },\n cards,\n };\n}\n\n// ============================================================================\n// ---- from board-live-cards-lib-card-handler.ts ----\n// ============================================================================\n\nfunction nowHighRes(): string {\n return new Date().toISOString();\n}\n\nexport function createCardHandlerFn(\n baseRef: KindValueRef,\n journalId: string,\n adapters: CardHandlerAdapters,\n taskCompletedFn: (taskName: string, data: Record<string, unknown>) => void,\n _taskFailedFn: (taskName: string, error: string) => void,\n writeComputedValuesFn?: (cardId: string, values: Record<string, unknown>) => void,\n writeDataObjectsFn?: (data: Record<string, unknown>) => void,\n): TaskHandlerFn {\n return async (input) => {\n const pendingRequests: ExecutionRequestEntry[] = [];\n const card = adapters.cardStore.readCard(input.nodeId);\n if (!card) return 'task-initiate-failure';\n\n const cardId = card.id as string;\n const cardState = (card.card_data ?? {}) as Record<string, unknown>;\n const allSources: ComputeSource[] = (card.source_defs ?? []) as ComputeSource[];\n const requiredSources = allSources.filter(s => s.optionalForCompletionGating !== true);\n\n let state: CardRuntimeSnapshot = adapters.cardRuntimeStore.readRuntime(cardId);\n let dirty = false;\n\n const flush = (): void => {\n if (!dirty) return;\n adapters.cardRuntimeStore.writeRuntime(cardId, state);\n dirty = false;\n };\n\n const getSourceEntry = (outputFile: string): SourceRuntimeEntry =>\n ({ ...(state._sources[outputFile] ?? {}) });\n const setSourceEntry = (outputFile: string, entry: SourceRuntimeEntry): void => {\n state._sources[outputFile] = entry; dirty = true;\n };\n\n const currentExecutionCount = input.taskState?.executionCount ?? 0;\n const lastExecCount = state._lastExecutionCount;\n if (typeof lastExecCount === 'number' && lastExecCount !== currentExecutionCount) {\n state._sources = {}; dirty = true;\n }\n if (lastExecCount !== currentExecutionCount) {\n state._lastExecutionCount = currentExecutionCount; dirty = true;\n }\n\n\n if (input.update) {\n const u = input.update;\n const outputFile = u.outputFile as string;\n if (outputFile) {\n const entry = getSourceEntry(outputFile);\n if (u.failure) {\n setSourceEntry(outputFile, nextEntryAfterFetchFailure(entry, (u.reason as string | undefined) ?? 'unknown'));\n } else {\n const incomingRqt = u.rqt as string;\n if (!entry.lastFetchedAt || incomingRqt > entry.lastFetchedAt) {\n const deliveryToken = typeof u.deliveryToken === 'string' ? u.deliveryToken : undefined;\n if (deliveryToken) {\n adapters.fetchedSourcesStore.commitSourceData(cardId, outputFile, deliveryToken);\n }\n setSourceEntry(outputFile, nextEntryAfterFetchDelivery(entry, incomingRqt));\n }\n }\n flush();\n }\n }\n\n const sourcesData: Record<string, unknown> = {};\n for (const src of allSources) {\n if (src.outputFile) {\n const content = adapters.fetchedSourcesStore.readSourceData(cardId, src.outputFile as string);\n if (content !== null) {\n sourcesData[src.bindTo] = content;\n }\n }\n }\n\n const requires: Record<string, unknown> = {};\n for (const [token, taskData] of Object.entries(input.state ?? {})) {\n if (taskData !== null && typeof taskData === 'object' && !Array.isArray(taskData)) {\n const unwrapped = (taskData as Record<string, unknown>)[token];\n requires[token] = unwrapped !== undefined ? unwrapped : taskData;\n } else {\n requires[token] = taskData;\n }\n }\n\n const computeNode: ComputeNode = {\n id: cardId,\n card_data: { ...cardState },\n requires,\n source_defs: allSources,\n compute: card.compute as ComputeStep[] | undefined,\n };\n computeNode._sourcesData = sourcesData;\n if (card.compute) {\n CardCompute.runSync(computeNode, { sourcesData });\n }\n\n (writeComputedValuesFn ?? adapters.outputStore.writeComputedValues.bind(adapters.outputStore))(cardId, computeNode.computed_values ?? {});\n\n const enrichedCard = { ...card };\n const enrichedSources = CardCompute.enrichSourcesSync(\n Array.isArray(card.source_defs) ? card.source_defs : undefined,\n {\n card_data: card.card_data as Record<string, unknown>,\n requires,\n },\n );\n\n const dir = baseRef.value;\n enrichedCard.source_defs = Array.isArray(enrichedSources)\n ? enrichedSources.map(src => ({\n ...src,\n boardDir: typeof src.boardDir === 'string' && src.boardDir ? src.boardDir : dir,\n }))\n : enrichedSources;\n\n const now = nowHighRes();\n const runQueuedAt = input.update ? undefined : now;\n\n const undeliveredRequired = requiredSources.filter(s => {\n const outputFile = s.outputFile;\n if (typeof outputFile !== 'string' || !outputFile) return true;\n let entry = getSourceEntry(outputFile);\n if (runQueuedAt) {\n entry = { ...entry, queueRequestedAt: runQueuedAt };\n setSourceEntry(outputFile, entry);\n }\n const qrt = entry.queueRequestedAt ?? entry.lastRequestedAt ?? now;\n const action = decideSourceAction(entry, qrt);\n if (action === 'in-flight') return false;\n return action === 'dispatch';\n });\n\n flush();\n\n if (undeliveredRequired.length > 0) {\n let stampedAny = false;\n let dispatchRqt = now;\n for (const src of undeliveredRequired) {\n const outputFile = src.outputFile;\n if (typeof outputFile !== 'string' || !outputFile) continue;\n const entry = getSourceEntry(outputFile);\n const queuedAt = entry.queueRequestedAt ?? now;\n setSourceEntry(outputFile, { ...entry, lastRequestedAt: queuedAt });\n dispatchRqt = queuedAt;\n stampedAny = true;\n }\n if (stampedAny) flush();\n if (!stampedAny) return 'task-initiated';\n\n pendingRequests.push({ taskKind: 'source-fetch', payload: { boardRef: serializeRef(baseRef), enrichedCard: enrichedCard as Record<string, unknown>, callbackToken: input.callbackToken, rqt: dispatchRqt } });\n adapters.executionRequestStore.appendEntries(journalId, pendingRequests);\n return 'task-initiated';\n }\n\n const providesBindings = (card.provides ?? []) as { bindTo: string; ref: string }[];\n const data: Record<string, unknown> = {};\n for (const { bindTo, ref } of providesBindings) {\n data[bindTo] = CardCompute.resolve(computeNode, ref);\n }\n\n (writeDataObjectsFn ?? adapters.outputStore.writeDataObjects.bind(adapters.outputStore))(data);\n\n const undeliveredOptional = allSources.filter(s => {\n if (s.optionalForCompletionGating !== true) return false;\n const entry = getSourceEntry(s.outputFile as string);\n if (!entry.lastRequestedAt) return true;\n if (!entry.lastFetchedAt) return true;\n return entry.lastFetchedAt <= entry.lastRequestedAt;\n });\n if (undeliveredOptional.length > 0) {\n pendingRequests.push({ taskKind: 'source-fetch', payload: { boardRef: serializeRef(baseRef), enrichedCard: enrichedCard as Record<string, unknown>, callbackToken: input.callbackToken, rqt: now } });\n }\n\n taskCompletedFn(input.nodeId, data);\n if (pendingRequests.length > 0) adapters.executionRequestStore.appendEntries(journalId, pendingRequests);\n return 'task-initiated';\n };\n}\n\n// ============================================================================\n// ---- pure constants / codecs lifted from board-live-cards-cli.ts ----\n// ============================================================================\n\nexport const EMPTY_CONFIG: GraphConfig = { settings: { completion: 'manual', refreshStrategy: 'data-changed' }, tasks: {} } as GraphConfig;\n\n/** Envelope stored in the snapshot store — wraps the LiveGraph snapshot with journal pointer. */\nexport interface BoardEnvelope {\n lastDrainedJournalId: string;\n graph: LiveGraphSnapshot;\n}\n\nexport function boardEnvelopeToSnapshotEntries(envelope: BoardEnvelope): Record<string, unknown> {\n return {\n [BOARD_GRAPH_KEY]: envelope.graph,\n [BOARD_LAST_JOURNAL_PROCESSED_ID_KEY]: envelope.lastDrainedJournalId,\n };\n}\n\nexport function snapshotEntriesToBoardEnvelope(entries: Record<string, unknown>): BoardEnvelope {\n const graph = entries[BOARD_GRAPH_KEY] as LiveGraphSnapshot | undefined;\n const lastDrainedJournalId = entries[BOARD_LAST_JOURNAL_PROCESSED_ID_KEY] as string | undefined;\n if (!graph || typeof graph !== 'object') {\n throw new Error(`State snapshot is missing required key: ${BOARD_GRAPH_KEY}`);\n }\n return {\n graph,\n lastDrainedJournalId: typeof lastDrainedJournalId === 'string' ? lastDrainedJournalId : '',\n };\n}\n\nexport interface CardInventoryEntry {\n cardId: string;\n cardFilePath: string;\n addedAt: string;\n}\n\nexport interface CardInventoryIndex {\n byCardId: Map<string, CardInventoryEntry>;\n byCardPath: Map<string, CardInventoryEntry>;\n}\n\n/**\n * Transform a LiveCard into a TaskConfig for the reactive graph.\n * Every card gets handler: 'card-handler'.\n */\nexport function liveCardToTaskConfig(card: LiveCard): TaskConfig {\n const requires = card.requires as string[] | undefined;\n const provides = (card.provides as Array<{ bindTo: string }> | undefined)?.map(p => p.bindTo) ?? [];\n\n return {\n requires: requires && requires.length > 0 ? requires : undefined,\n provides,\n taskHandlers: ['card-handler'],\n description: (card.meta as { title?: string } | undefined)?.title ?? card.id,\n };\n}\n","/**\n * storage-localstorage-adapters.ts\n *\n * Browser localStorage implementations of the board-live-cards storage primitives:\n * BlobStorage — localStorage keys prefixed with `${prefix}:blob:`\n * KVStorage — localStorage keys prefixed with `${prefix}:kv:`, values JSON-encoded\n * JournalStorageAdapter — single localStorage key holding a JSON array of entries\n * CardStorageAdapter — KV-backed, compatible with createCardStore()\n *\n * No Node imports. Requires globalThis.localStorage (browser / jsdom environment).\n */\n\nimport type { BlobStorage, KVStorage, JSONStorage } from '../common/storage-interface.js';\nimport type { JournalStorageAdapter, CardStorageAdapter, JournalEntry, LiveCard, CardIndex } from '../common/board-live-cards-lib.js';\n\n// ============================================================================\n// Stable JSON + sync hash\n// Used for card dedup and snapshot versioning. Not security-sensitive.\n// ============================================================================\n\nfunction stableJson(value: unknown): string {\n if (value === null || value === undefined || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) return `[${(value as unknown[]).map(stableJson).join(',')}]`;\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n return `{${keys.map(k => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(',')}}`;\n}\n\nfunction fnv32a(str: string, seed: number): number {\n let h = seed >>> 0;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return h;\n}\n\n/**\n * Synchronous stable content hash for browser environments.\n * Uses four FNV-1a 32-bit passes to produce 32 hex chars.\n * Deterministic and cross-session stable; NOT cryptographically secure.\n */\nexport function computeStableJsonHashBrowser(value: unknown): string {\n const str = stableJson(value);\n const a = fnv32a(str, 0x811c9dc5);\n const b = fnv32a(str, 0xdeadbeef);\n const c = fnv32a(str, 0x01234567);\n const d = fnv32a(str, 0xfeedface);\n return [a, b, c, d].map(n => n.toString(16).padStart(8, '0')).join('');\n}\n\n// ============================================================================\n// createLocalStorageBlobStorage\n// ============================================================================\n\nexport function createLocalStorageBlobStorage(prefix: string): BlobStorage {\n function key(k: string): string { return `${prefix}:blob:${k}`; }\n const textEncoder = new TextEncoder();\n\n function encodeBytes(bytes: Uint8Array): string {\n if (typeof btoa === 'function') {\n let bin = '';\n for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);\n return btoa(bin);\n }\n return '';\n }\n\n function decodeBytes(encoded: string): Uint8Array {\n if (typeof atob === 'function') {\n const bin = atob(encoded);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n return new Uint8Array();\n }\n\n return {\n read(k: string): string | null {\n return globalThis.localStorage.getItem(key(k));\n },\n write(k: string, content: string): void {\n globalThis.localStorage.setItem(key(k), content);\n },\n exists(k: string): boolean {\n return globalThis.localStorage.getItem(key(k)) !== null;\n },\n remove(k: string): void {\n globalThis.localStorage.removeItem(key(k));\n },\n\n readBytes(k: string): Uint8Array | null {\n const raw = globalThis.localStorage.getItem(key(k));\n if (raw === null) return null;\n try {\n const parsed = JSON.parse(raw) as { __kind?: string; data?: string };\n if (parsed && parsed.__kind === 'bytes-b64' && typeof parsed.data === 'string') {\n return decodeBytes(parsed.data);\n }\n } catch {\n // fall through to plain text path\n }\n return textEncoder.encode(raw);\n },\n\n writeBytes(k: string, content: Uint8Array): void {\n // Store binary payloads as base64 envelope to avoid lossy UTF-8 coercion.\n const envelope = JSON.stringify({ __kind: 'bytes-b64', data: encodeBytes(content) });\n globalThis.localStorage.setItem(key(k), envelope);\n },\n\n listKeys(prefix2?: string): string[] {\n const marker = key(prefix2 ?? '');\n const out: string[] = [];\n for (let i = 0; i < globalThis.localStorage.length; i++) {\n const k = globalThis.localStorage.key(i);\n if (k && k.startsWith(marker)) out.push(k.slice(key('').length));\n }\n return out.sort();\n },\n\n stat(k: string) {\n const raw = globalThis.localStorage.getItem(key(k));\n if (raw === null) return null;\n let size = textEncoder.encode(raw).byteLength;\n try {\n const parsed = JSON.parse(raw) as { __kind?: string; data?: string };\n if (parsed && parsed.__kind === 'bytes-b64' && typeof parsed.data === 'string') {\n size = decodeBytes(parsed.data).byteLength;\n }\n } catch {\n // plain text path\n }\n return { key: k, size };\n },\n };\n}\n\n// ============================================================================\n// createLocalStorageKvStorage\n// ============================================================================\n\nexport function createLocalStorageKvStorage(prefix: string): KVStorage {\n function key(k: string): string { return `${prefix}:kv:${k}`; }\n\n return {\n read(k: string): unknown | null {\n const raw = globalThis.localStorage.getItem(key(k));\n if (raw === null) return null;\n try { return JSON.parse(raw); } catch { return null; }\n },\n write(k: string, value: unknown): void {\n globalThis.localStorage.setItem(key(k), JSON.stringify(value));\n },\n delete(k: string): void {\n globalThis.localStorage.removeItem(key(k));\n },\n listKeys(prefix2?: string): string[] {\n const fullPrefix = key(prefix2 ?? '');\n const result: string[] = [];\n for (let i = 0; i < globalThis.localStorage.length; i++) {\n const lsKey = globalThis.localStorage.key(i);\n if (lsKey !== null && lsKey.startsWith(fullPrefix)) {\n // Strip the outer prefix + ':kv:' to return the logical key\n result.push(lsKey.slice(key('').length));\n }\n }\n return result;\n },\n };\n}\n\nfunction deepMergeObjects(target: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(patch)) {\n if (v !== null && typeof v === 'object' && !Array.isArray(v) &&\n result[k] !== null && typeof result[k] === 'object' && !Array.isArray(result[k])) {\n result[k] = deepMergeObjects(result[k] as Record<string, unknown>, v as Record<string, unknown>);\n } else {\n result[k] = v;\n }\n }\n return result;\n}\n\nfunction applyJsonPath(obj: Record<string, unknown>, segments: string[], value: unknown): Record<string, unknown> {\n if (segments.length === 0) return obj;\n const [head, ...tail] = segments;\n if (tail.length === 0) return { ...obj, [head]: value };\n const nested = (obj[head] !== null && typeof obj[head] === 'object' && !Array.isArray(obj[head]))\n ? (obj[head] as Record<string, unknown>)\n : {};\n return { ...obj, [head]: applyJsonPath(nested, tail, value) };\n}\n\nexport function createLocalStorageJsonStorage(prefix: string): JSONStorage {\n const kv = createLocalStorageKvStorage(prefix);\n return {\n read: (key) => kv.read(key),\n get(key, jsonPath) {\n const obj = kv.read(key);\n if (obj === null) return null;\n let current: unknown = obj;\n for (const segment of jsonPath.split('.').filter(Boolean)) {\n if (current === null || typeof current !== 'object' || Array.isArray(current)) return null;\n current = (current as Record<string, unknown>)[segment] ?? null;\n }\n return current ?? null;\n },\n write: (key, value) => kv.write(key, value),\n delete: (key) => kv.delete(key),\n listKeys: (prefix2?) => kv.listKeys(prefix2),\n shallowMerge(key, patch) {\n const existing = (kv.read(key) as Record<string, unknown> | null) ?? {};\n kv.write(key, { ...existing, ...patch });\n },\n deepMerge(key, patch) {\n const existing = (kv.read(key) as Record<string, unknown> | null) ?? {};\n kv.write(key, deepMergeObjects(existing, patch));\n },\n patch(key, jsonPath, value) {\n const existing = (kv.read(key) as Record<string, unknown> | null) ?? {};\n const segments = jsonPath.split('.').filter(Boolean);\n kv.write(key, applyJsonPath(existing, segments, value));\n },\n };\n}\n\n// ============================================================================\n// createLocalStorageJournalStorageAdapter\n// All entries stored as a JSON array under a single localStorage key.\n// ============================================================================\n\nexport function createLocalStorageJournalStorageAdapter(storageKey: string): JournalStorageAdapter {\n function load(): JournalEntry[] {\n const raw = globalThis.localStorage.getItem(storageKey);\n if (!raw) return [];\n try { return JSON.parse(raw) as JournalEntry[]; } catch { return []; }\n }\n\n function save(entries: JournalEntry[]): void {\n globalThis.localStorage.setItem(storageKey, JSON.stringify(entries));\n }\n\n return {\n readAllEntries(): JournalEntry[] {\n return load();\n },\n appendEntry(entry: JournalEntry): void {\n const entries = load();\n entries.push(entry);\n save(entries);\n },\n generateId(): string {\n return globalThis.crypto.randomUUID();\n },\n };\n}\n\n// ============================================================================\n// createLocalStorageCardStorageAdapter\n// Mirrors createFsCardStorageAdapter — KV-backed, cards keyed by cardId.\n// ============================================================================\n\nexport function createLocalStorageCardStorageAdapter(prefix: string): CardStorageAdapter {\n const json = createLocalStorageJsonStorage(prefix);\n\n return {\n readIndex(): CardIndex | null {\n return json.read('_index') as CardIndex | null;\n },\n writeIndex(index: CardIndex): void {\n json.write('_index', index);\n },\n readCard(id: string): LiveCard | null {\n return json.read(id) as LiveCard | null;\n },\n writeCard(id: string, card: LiveCard): string {\n json.write(id, card);\n return computeStableJsonHashBrowser(card);\n },\n cardExists(id: string): boolean {\n return json.read(id) !== null;\n },\n defaultCardKey(cardId: string): string {\n return cardId;\n },\n };\n}\n","/**\n * card-store-browser-api.ts\n *\n * Simple browser-facing card store API.\n * Wraps createCardStore() + createLocalStorageCardStorageAdapter()\n * into a minimal read/write interface suitable for browser consumption.\n */\n\nimport { createCardStore } from '../common/board-live-cards-lib.js';\nimport type { LiveCard } from '../common/board-live-cards-lib.js';\nimport { createLocalStorageCardStorageAdapter } from './storage-localstorage-adapters.js';\n\nexport type { LiveCard };\n\nexport interface BrowserCardStoreApi {\n getCard(id: string): LiveCard | null;\n getAllCards(): LiveCard[];\n upsertCard(card: LiveCard): void;\n removeCard(id: string): void;\n}\n\n/**\n * Create a browser card store backed by localStorage.\n *\n * @param namespace - localStorage key prefix (e.g. 'my-board:cards').\n * Multiple stores can coexist by using distinct namespaces.\n */\nexport function createBrowserCardStoreApi(namespace: string): BrowserCardStoreApi {\n const adapter = createLocalStorageCardStorageAdapter(namespace);\n const store = createCardStore(adapter);\n\n return {\n getCard(id: string): LiveCard | null {\n return store.readCard(id);\n },\n getAllCards(): LiveCard[] {\n return store.readAllCards();\n },\n upsertCard(card: LiveCard): void {\n const key = adapter.defaultCardKey(card.id);\n store.writeCard(card.id, card, key);\n },\n removeCard(id: string): void {\n store.removeCard(id);\n },\n };\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import { L as LiveCard } from '../../board-live-cards-lib-Bg6EvCo5.cjs';
2
+ import '../../types-BBhqYGhE.cjs';
3
+
4
+ /**
5
+ * card-store-browser-api.ts
6
+ *
7
+ * Simple browser-facing card store API.
8
+ * Wraps createCardStore() + createLocalStorageCardStorageAdapter()
9
+ * into a minimal read/write interface suitable for browser consumption.
10
+ */
11
+
12
+ interface BrowserCardStoreApi {
13
+ getCard(id: string): LiveCard | null;
14
+ getAllCards(): LiveCard[];
15
+ upsertCard(card: LiveCard): void;
16
+ removeCard(id: string): void;
17
+ }
18
+ /**
19
+ * Create a browser card store backed by localStorage.
20
+ *
21
+ * @param namespace - localStorage key prefix (e.g. 'my-board:cards').
22
+ * Multiple stores can coexist by using distinct namespaces.
23
+ */
24
+ declare function createBrowserCardStoreApi(namespace: string): BrowserCardStoreApi;
25
+
26
+ export { type BrowserCardStoreApi, LiveCard, createBrowserCardStoreApi };
@@ -0,0 +1,26 @@
1
+ import { L as LiveCard } from '../../board-live-cards-lib-jM2uYG1v.js';
2
+ import '../../types-BBhqYGhE.js';
3
+
4
+ /**
5
+ * card-store-browser-api.ts
6
+ *
7
+ * Simple browser-facing card store API.
8
+ * Wraps createCardStore() + createLocalStorageCardStorageAdapter()
9
+ * into a minimal read/write interface suitable for browser consumption.
10
+ */
11
+
12
+ interface BrowserCardStoreApi {
13
+ getCard(id: string): LiveCard | null;
14
+ getAllCards(): LiveCard[];
15
+ upsertCard(card: LiveCard): void;
16
+ removeCard(id: string): void;
17
+ }
18
+ /**
19
+ * Create a browser card store backed by localStorage.
20
+ *
21
+ * @param namespace - localStorage key prefix (e.g. 'my-board:cards').
22
+ * Multiple stores can coexist by using distinct namespaces.
23
+ */
24
+ declare function createBrowserCardStoreApi(namespace: string): BrowserCardStoreApi;
25
+
26
+ export { type BrowserCardStoreApi, LiveCard, createBrowserCardStoreApi };
@@ -0,0 +1,2 @@
1
+ import {createRequire}from'module';import'ajv-formats';var x=createRequire(import.meta.url);x("./jsonata-sync.cjs");var v=createRequire(import.meta.url);v("./jsonata-sync.cjs");function y(n,o){function e(){return n.readIndex()??{}}function r(t,s,i){let a=String(s||"").split(".").filter(Boolean);if(a.length===0)return i&&typeof i=="object"&&!Array.isArray(i)?i:{value:i};let u={...t},c=u;for(let d=0;d<a.length-1;d++){let l=a[d],f=c[l],m=f&&typeof f=="object"&&!Array.isArray(f)?{...f}:{};c[l]=m,c=m;}return c[a[a.length-1]]=i,u}return {readCard(t){let s=e()[t];return !s||!n.cardExists(s.key)?null:n.readCard(s.key)},readCardKey(t){return e()[t]?.key??null},readAllCards(){let t=[];for(let[s,i]of Object.entries(e())){if(!n.cardExists(i.key))continue;let a=n.readCard(i.key);a?t.push(a):o?.(`[card-store] could not read card "${s}" at key "${i.key}"`);}return t},readChecksumIndex(){let t={};for(let[s,i]of Object.entries(e()))t[s]=i.checksum;return t},changedSince(t){let s=e(),i=[];for(let[a,u]of Object.entries(s))t[a]!==u.checksum&&i.push(a);for(let a of Object.keys(t))s[a]||i.push(a);return i},validateUpsert(t,s){let i=e(),a=i[t],u=Object.entries(i).find(([,c])=>c.key===s);return a&&a.key!==s?{ok:false,error:`Card id "${t}" is already mapped to key "${a.key}", cannot remap to "${s}"`}:u&&u[0]!==t?{ok:false,error:`Key "${s}" is already mapped to card id "${u[0]}", cannot remap to "${t}"`}:{ok:true}},writeCard(t,s,i){let a=e(),u=i??a[t]?.key??n.defaultCardKey(t),c=n.writeCard(u,s);a[t]={key:u,checksum:c,updatedAt:new Date().toISOString()},n.writeIndex(a);},patchCard(t,s,i){let a=e(),u=a[t];if(!u||!n.cardExists(u.key))throw new Error(`card "${t}" not found`);let c=n.readCard(u.key);if(!c||typeof c!="object"||Array.isArray(c))throw new Error(`card "${t}" is not patchable`);let d=r(c,s,i),l=n.writeCard(u.key,d);a[t]={key:u.key,checksum:l,updatedAt:new Date().toISOString()},n.writeIndex(a);},removeCard(t){let s=e();s[t]&&(delete s[t],n.writeIndex(s));},readIndex(){return e()}}}function p(n){if(n==null||typeof n!="object")return JSON.stringify(n);if(Array.isArray(n))return `[${n.map(p).join(",")}]`;let o=n;return `{${Object.keys(o).sort().map(r=>`${JSON.stringify(r)}:${p(o[r])}`).join(",")}}`}function g(n,o){let e=o>>>0;for(let r=0;r<n.length;r++)e^=n.charCodeAt(r),e=Math.imul(e,16777619)>>>0;return e}function E(n){let o=p(n),e=g(o,2166136261),r=g(o,3735928559),t=g(o,19088743),s=g(o,4277009102);return [e,r,t,s].map(i=>i.toString(16).padStart(8,"0")).join("")}function A(n){function o(e){return `${n}:kv:${e}`}return {read(e){let r=globalThis.localStorage.getItem(o(e));if(r===null)return null;try{return JSON.parse(r)}catch{return null}},write(e,r){globalThis.localStorage.setItem(o(e),JSON.stringify(r));},delete(e){globalThis.localStorage.removeItem(o(e));},listKeys(e){let r=o(e??""),t=[];for(let s=0;s<globalThis.localStorage.length;s++){let i=globalThis.localStorage.key(s);i!==null&&i.startsWith(r)&&t.push(i.slice(o("").length));}return t}}}function h(n,o){let e={...n};for(let[r,t]of Object.entries(o))t!==null&&typeof t=="object"&&!Array.isArray(t)&&e[r]!==null&&typeof e[r]=="object"&&!Array.isArray(e[r])?e[r]=h(e[r],t):e[r]=t;return e}function S(n,o,e){if(o.length===0)return n;let[r,...t]=o;if(t.length===0)return {...n,[r]:e};let s=n[r]!==null&&typeof n[r]=="object"&&!Array.isArray(n[r])?n[r]:{};return {...n,[r]:S(s,t,e)}}function _(n){let o=A(n);return {read:e=>o.read(e),get(e,r){let t=o.read(e);if(t===null)return null;let s=t;for(let i of r.split(".").filter(Boolean)){if(s===null||typeof s!="object"||Array.isArray(s))return null;s=s[i]??null;}return s??null},write:(e,r)=>o.write(e,r),delete:e=>o.delete(e),listKeys:e=>o.listKeys(e),shallowMerge(e,r){let t=o.read(e)??{};o.write(e,{...t,...r});},deepMerge(e,r){let t=o.read(e)??{};o.write(e,h(t,r));},patch(e,r,t){let s=o.read(e)??{},i=r.split(".").filter(Boolean);o.write(e,S(s,i,t));}}}function k(n){let o=_(n);return {readIndex(){return o.read("_index")},writeIndex(e){o.write("_index",e);},readCard(e){return o.read(e)},writeCard(e,r){return o.write(e,r),E(r)},cardExists(e){return o.read(e)!==null},defaultCardKey(e){return e}}}function he(n){let o=k(n),e=y(o);return {getCard(r){return e.readCard(r)},getAllCards(){return e.readAllCards()},upsertCard(r){let t=o.defaultCardKey(r.id);e.writeCard(r.id,r,t);},removeCard(r){e.removeCard(r);}}}export{he as createBrowserCardStoreApi};//# sourceMappingURL=card-store-browser-api.js.map
2
+ //# sourceMappingURL=card-store-browser-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/card-compute/schema-validator.ts","../../../src/card-compute/index.ts","../../../src/cli/common/board-live-cards-lib.ts","../../../src/cli/browser-api/storage-localstorage-adapters.ts","../../../src/cli/browser-api/card-store-browser-api.ts"],"names":["_require","createRequire","createCardStore","adapter","onWarn","loadIndex","applyJsonPath","obj","jsonPath","value","segments","out","target","i","key","cur","next","id","entry","cards","card","result","snapshotChecksumIndex","localIndex","changed","cardKey","index","existingById","existingByKey","e","resolvedKey","checksum","current","stableJson","k","fnv32a","str","seed","h","computeStableJsonHashBrowser","a","b","c","d","n","createLocalStorageKvStorage","prefix","raw","prefix2","fullPrefix","lsKey","deepMergeObjects","patch","v","head","tail","nested","createLocalStorageJsonStorage","kv","segment","existing","createLocalStorageCardStorageAdapter","json","cardId","createBrowserCardStoreApi","namespace","store"],"mappings":"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","file":"card-store-browser-api.js","sourcesContent":["/**\n * schema-validator — Full JSON Schema validation for LiveCards nodes.\n *\n * Uses AJV to validate against the published live-cards.schema.json.\n * For a lightweight sync check without AJV, use `CardCompute.validate()` instead.\n *\n * @example\n * ```typescript\n * import { validateLiveCardSchema } from 'yaml-flow/card-compute';\n *\n * const result = validateLiveCardSchema(node);\n * if (!result.ok) console.error(result.errors);\n * ```\n */\n\nimport type { ValidationResult } from './index.js';\nimport liveCardsSchema from '../../schema/live-cards.schema.json';\nimport Ajv from 'ajv';\nimport addFormats from 'ajv-formats';\nimport { createRequire } from 'module';\nconst _require = createRequire(import.meta.url);\nconst jsonata: (expr: string) => { evaluate: (data: unknown) => unknown } = _require('./jsonata-sync.cjs');\n\ntype AjvValidateFunction = {\n (data: unknown): boolean;\n errors?: Array<{ instancePath?: string; message?: string }> | null;\n};\n\nlet _compiled: AjvValidateFunction | null = null;\n\nconst KNOWN_NAMESPACES = [\n 'card_data',\n 'requires',\n 'fetched_sources',\n 'computed_values',\n 'source_defs',\n] as const;\n\ntype KnownNamespace = typeof KNOWN_NAMESPACES[number];\n\nconst NAMESPACE_REFERENCE_RE = /\\b(card_data|requires|fetched_sources|computed_values|source_defs)\\b/g;\nconst ROOT_PATH_NAMESPACE_RE = /^\\s*(card_data|requires|fetched_sources|computed_values|source_defs)(\\.|$)/;\n\nfunction referencedNamespaces(expression: string): Set<KnownNamespace> {\n const namespaces = new Set<KnownNamespace>();\n let match: RegExpExecArray | null;\n NAMESPACE_REFERENCE_RE.lastIndex = 0;\n while ((match = NAMESPACE_REFERENCE_RE.exec(expression)) !== null) {\n namespaces.add(match[1] as KnownNamespace);\n }\n return namespaces;\n}\n\nfunction parseRootPathNamespace(pathValue: string): KnownNamespace | null {\n const match = ROOT_PATH_NAMESPACE_RE.exec(pathValue);\n return match ? (match[1] as KnownNamespace) : null;\n}\n\nfunction validateJsonataExprWithNamespaces(\n expr: string,\n path: string,\n allowedNamespaces: Set<KnownNamespace>,\n errors: string[],\n): void {\n try {\n jsonata(expr);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n errors.push(`${path}: invalid JSONata expression (${message})`);\n return;\n }\n\n const usedNamespaces = referencedNamespaces(expr);\n for (const namespace of usedNamespaces) {\n if (!allowedNamespaces.has(namespace)) {\n errors.push(`${path}: disallowed namespace \"${namespace}\" in expression`);\n }\n }\n}\n\nfunction walkViewPathReferences(\n value: unknown,\n path: string,\n errors: string[],\n): void {\n if (Array.isArray(value)) {\n value.forEach((entry, index) => {\n walkViewPathReferences(entry, `${path}/${index}`, errors);\n });\n return;\n }\n\n if (typeof value === 'string') {\n const rootNamespace = parseRootPathNamespace(value);\n if (!rootNamespace) return;\n if (!new Set<KnownNamespace>(['card_data', 'requires', 'computed_values']).has(rootNamespace)) {\n errors.push(`${path}: disallowed namespace \"${rootNamespace}\" in view reference`);\n }\n return;\n }\n\n if (!value || typeof value !== 'object') return;\n\n const record = value as Record<string, unknown>;\n for (const [key, next] of Object.entries(record)) {\n walkViewPathReferences(next, `${path}/${key}`, errors);\n }\n}\n\nfunction getValidator(): AjvValidateFunction {\n if (_compiled) return _compiled;\n const ajv = new Ajv({ allErrors: true });\n addFormats(ajv);\n _compiled = ajv.compile(liveCardsSchema);\n return _compiled;\n}\n\n/**\n * Validate a node against the full LiveCards JSON Schema (draft-07).\n *\n * Requires `ajv` and `ajv-formats` to be installed.\n * Returns the same `ValidationResult` shape as `CardCompute.validate()`.\n */\nexport function validateLiveCardSchema(\n node: unknown,\n): ValidationResult {\n const validate = getValidator();\n const valid = validate(node);\n\n const errors = (validate.errors ?? []).map(e => {\n const path = e.instancePath || '/';\n return `${path}: ${e.message ?? 'unknown error'}`;\n });\n\n // JSON Schema draft-07 cannot enforce per-property uniqueness across array items.\n // Check bindTo and outputFile uniqueness here after the AJV structural pass.\n if (node && typeof node === 'object' && !Array.isArray(node)) {\n const source_defs = (node as Record<string, unknown>).source_defs;\n if (Array.isArray(source_defs)) {\n const bindTos = new Set<string>();\n const outputFiles = new Set<string>();\n source_defs.forEach((src, i) => {\n if (!src || typeof src !== 'object' || Array.isArray(src)) return;\n const s = src as Record<string, unknown>;\n if (typeof s.bindTo === 'string' && s.bindTo) {\n if (bindTos.has(s.bindTo)) {\n errors.push(`/source_defs/${i}/bindTo: bindTo \"${s.bindTo}\" must be unique across all source_defs`);\n }\n bindTos.add(s.bindTo);\n }\n if (typeof s.outputFile === 'string' && s.outputFile) {\n if (outputFiles.has(s.outputFile)) {\n errors.push(`/source_defs/${i}/outputFile: outputFile \"${s.outputFile}\" must be unique across all source_defs`);\n }\n outputFiles.add(s.outputFile);\n }\n });\n }\n }\n\n if (!valid || errors.length > 0) return { ok: false, errors };\n return { ok: true, errors: [] };\n}\n\n/**\n * Validate JSONata expressions in compute[] by compiling with the same parser used at runtime.\n */\nexport function validateLiveCardRuntimeExpressions(\n node: unknown,\n): ValidationResult {\n const errors: string[] = [];\n\n if (!node || typeof node !== 'object' || Array.isArray(node)) {\n return { ok: true, errors: [] };\n }\n\n const asRecord = node as Record<string, unknown>;\n\n const compute = asRecord.compute;\n if (Array.isArray(compute)) {\n compute.forEach((step, i) => {\n if (!step || typeof step !== 'object' || Array.isArray(step)) return;\n const expr = (step as Record<string, unknown>).expr;\n if (typeof expr !== 'string' || expr.trim().length === 0) return;\n validateJsonataExprWithNamespaces(\n expr,\n `/compute/${i}/expr`,\n new Set<KnownNamespace>(['card_data', 'requires', 'fetched_sources', 'computed_values']),\n errors,\n );\n });\n }\n\n // Validate provides[].ref paths use a valid root namespace.\n const VALID_PROVIDES_SRC_NAMESPACES = new Set<KnownNamespace>([\n 'card_data', 'requires', 'fetched_sources', 'computed_values',\n ]);\n const provides = asRecord.provides;\n if (Array.isArray(provides)) {\n provides.forEach((entry, i) => {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return;\n const ref = (entry as Record<string, unknown>).ref;\n if (typeof ref !== 'string' || ref.trim().length === 0) return;\n const rootNamespace = parseRootPathNamespace(ref);\n if (rootNamespace === null) {\n errors.push(`/provides/${i}/ref: path \"${ref}\" must start with a valid namespace (${[...VALID_PROVIDES_SRC_NAMESPACES].join(', ')})`);\n } else if (!VALID_PROVIDES_SRC_NAMESPACES.has(rootNamespace)) {\n errors.push(`/provides/${i}/ref: disallowed namespace \"${rootNamespace}\" in path \"${ref}\" (valid: ${[...VALID_PROVIDES_SRC_NAMESPACES].join(', ')})`);\n }\n });\n }\n\n const view = asRecord.view;\n if (view && typeof view === 'object' && !Array.isArray(view)) {\n walkViewPathReferences(view, '/view', errors);\n }\n\n // Validate source_defs[i].projections values: each must be a JSONata expression rooted at\n // card_data or requires only. fetched_sources/computed_values/source_defs are not\n // valid here because sources run before those namespaces exist.\n const VALID_PROJECTION_NAMESPACES = new Set<KnownNamespace>(['card_data', 'requires']);\n const source_defs = asRecord.source_defs;\n if (Array.isArray(source_defs)) {\n source_defs.forEach((srcDef, i) => {\n if (!srcDef || typeof srcDef !== 'object' || Array.isArray(srcDef)) return;\n const projections = (srcDef as Record<string, unknown>).projections;\n if (!projections || typeof projections !== 'object' || Array.isArray(projections)) return;\n for (const [key, exprVal] of Object.entries(projections as Record<string, unknown>)) {\n if (typeof exprVal !== 'string' || exprVal.trim().length === 0) continue;\n validateJsonataExprWithNamespaces(\n exprVal,\n `/source_defs/${i}/projections/${key}`,\n VALID_PROJECTION_NAMESPACES,\n errors,\n );\n }\n });\n }\n\n return { ok: errors.length === 0, errors };\n}\n\nexport function validateLiveCard(\n node: unknown,\n): ValidationResult {\n return validateLiveCardDefinition(node);\n}\n\n/**\n * Full validation for live card definitions:\n * 1) JSON Schema structure/contract checks\n * 2) Runtime JSONata parser compatibility checks for compute expressions\n */\nexport function validateLiveCardDefinition(\n node: unknown,\n): ValidationResult {\n const schema = validateLiveCardSchema(node);\n if (!schema.ok) return schema;\n\n const runtime = validateLiveCardRuntimeExpressions(node);\n if (!runtime.ok) return { ok: false, errors: runtime.errors };\n\n return { ok: true, errors: [] };\n}\n","/**\n * card-compute — JSONata-powered compute engine for LiveCards nodes.\n *\n * Isomorphic: works in browser, Node.js, and bundlers.\n * No DOM dependency. Compute expressions are JSONata strings.\n *\n * @example\n * ```typescript\n * import { CardCompute } from 'yaml-flow/card-compute';\n *\n * const node = {\n * id: 'sales',\n * card_data: { data: [{ revenue: 100 }, { revenue: 200 }] },\n * compute: [\n * { bindTo: 'total', expr: '$sum(card_data.data.revenue)' },\n * { bindTo: 'avg', expr: '$average(card_data.data.revenue)' },\n * ],\n * };\n * await CardCompute.run(node);\n * // node.computed_values.total === 300\n * // node.computed_values.avg === 150\n * ```\n *\n * Expressions are evaluated against { card_data, requires, fetched_sources, computed_values }.\n * computed_values is ephemeral — never persisted to disk.\n */\n\nimport { createRequire } from 'module';\nconst _require = createRequire(import.meta.url);\nconst jsonata: (expr: string) => { evaluate: (data: unknown) => unknown } = _require('./jsonata-sync.cjs');\nconst jsonataSync = jsonata;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A source definition: cli writes to outputFile; bindTo names the fetched_sources.* key in compute context. Both bindTo and outputFile must be unique across source_defs in a card. */\nexport interface ComputeSource {\n bindTo: string;\n outputFile: string;\n cli?: string;\n // Deprecated alias retained for compatibility with older cards.\n script?: string;\n optionalForCompletionGating?: boolean;\n /** Named data projections: each key maps to a JSONata expression rooted at card_data or requires.\n * The engine evaluates these before spawning the executor and passes results as _projections. */\n projections?: Record<string, string>;\n [key: string]: unknown;\n}\n\n/** Options for CardCompute.run() */\nexport interface RunOptions {\n /** Pre-loaded source results map (keyed by bindTo). Use in browser or when caller loads files. */\n sourcesData?: Record<string, unknown>;\n}\n\n/** A single compute step: bindTo names the computed_values key; expr is a JSONata expression. */\nexport interface ComputeStep {\n bindTo: string;\n expr: string;\n}\n\n/** Minimal node shape expected by CardCompute. */\nexport interface ComputeNode {\n id?: string;\n card_data?: Record<string, unknown>;\n requires?: Record<string, unknown>;\n source_defs?: ComputeSource[];\n compute?: ComputeStep[];\n computed_values?: Record<string, unknown>;\n /** Ephemeral: populated by run() from sourcesData option. Never persisted. */\n _sourcesData?: Record<string, unknown>;\n [key: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Deep path utilities\n// ---------------------------------------------------------------------------\n\nfunction deepGet(obj: unknown, path: string): unknown {\n if (!path || !obj) return undefined;\n const parts = path.split('.');\n let cur: unknown = obj;\n for (let i = 0; i < parts.length; i++) {\n if (cur == null) return undefined;\n cur = (cur as Record<string, unknown>)[parts[i]];\n }\n return cur;\n}\n\nfunction deepSet(obj: Record<string, unknown>, path: string, value: unknown): void {\n const parts = path.split('.');\n let cur: Record<string, unknown> = obj;\n for (let i = 0; i < parts.length - 1; i++) {\n if (cur[parts[i]] == null || typeof cur[parts[i]] !== 'object') cur[parts[i]] = {};\n cur = cur[parts[i]] as Record<string, unknown>;\n }\n cur[parts[parts.length - 1]] = value;\n}\n\n// ---------------------------------------------------------------------------\n// Engine — JSONata-based async evaluation\n// ---------------------------------------------------------------------------\n\n/**\n * Run all compute steps on a node.\n * Each step's expr is evaluated against { card_data, requires, fetched_sources, computed_values }.\n * Results are written to node.computed_values[bindTo].\n * computed_values and _sourcesData are reset on each call — ephemeral, never persisted.\n *\n * @param options.sourcesData Pre-loaded map of { [bindTo]: data } for fetched_sources namespace.\n * In Node/CLI: loaded from outputFiles by the caller (card-handler).\n * In browser: passed in by the caller (e.g. from fetch results).\n */\nasync function run(node: ComputeNode, options?: RunOptions): Promise<ComputeNode> {\n if (!node?.compute?.length) return node;\n if (!node.card_data) node.card_data = {};\n node.computed_values = {};\n node._sourcesData = options?.sourcesData ?? {};\n\n // Context passed to JSONata. Structural keys take precedence over any user data.\n const _requires = node.requires ?? {};\n const ctx: Record<string, unknown> = {\n card_data: node.card_data,\n requires: _requires,\n expects_data: _requires, // alias: same reference as requires\n fetched_sources: node._sourcesData,\n data: node.computed_values, // alias: same reference as computed_values\n computed_values: node.computed_values,\n };\n\n for (const step of node.compute) {\n try {\n const val = await jsonata(step.expr).evaluate(ctx);\n deepSet(node.computed_values, step.bindTo, val);\n ctx.computed_values = node.computed_values; // subsequent steps see earlier results\n // ctx.data is the same reference as node.computed_values — already in sync\n } catch (err) {\n console.error(`CardCompute.run error on \"${node.id ?? '?'}.${step.bindTo}\":`, err);\n }\n }\n\n return node;\n}\n\n/**\n * Synchronous version of run() — uses a vendored sync JSONata build\n * (async/await stripped from jsonata.js since all built-in functions\n * are CPU-only).\n *\n * Same semantics as `run()`: evaluates all compute steps, populates\n * `node.computed_values`, returns the mutated node.\n *\n * @returns `{ ok: true, node }` when all steps evaluated successfully.\n * `{ ok: false, node }` is currently never returned but reserved\n * for future use if an expression requires true async evaluation.\n */\nfunction runSync(\n node: ComputeNode,\n options?: RunOptions,\n): { ok: boolean; node: ComputeNode; errors?: Array<{ bindTo: string; error: string }> } {\n if (!node?.compute?.length) return { ok: true, node };\n if (!node.card_data) node.card_data = {};\n node.computed_values = {};\n node._sourcesData = options?.sourcesData ?? {};\n\n const _requires2 = node.requires ?? {};\n const ctx: Record<string, unknown> = {\n card_data: node.card_data,\n requires: _requires2,\n expects_data: _requires2, // alias: same reference as requires\n fetched_sources: node._sourcesData,\n data: node.computed_values, // alias: same reference as computed_values\n computed_values: node.computed_values,\n };\n\n const errors: Array<{ bindTo: string; error: string }> = [];\n for (const step of node.compute) {\n try {\n const val = jsonataSync(step.expr).evaluate(ctx);\n deepSet(node.computed_values, step.bindTo, val);\n ctx.computed_values = node.computed_values;\n // ctx.data is the same reference as node.computed_values — already in sync\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n errors.push({ bindTo: step.bindTo, error: msg });\n console.error(`CardCompute.runSync error on \"${node.id ?? '?'}.${step.bindTo}\":`, err);\n }\n }\n\n return errors.length > 0 ? { ok: true, node, errors } : { ok: true, node };\n}\n\n/**\n * Evaluate a single JSONata expression against a node's context.\n * Context is { card_data, requires, fetched_sources, computed_values }.\n */\nasync function evalExpr(\n expr: string,\n node: ComputeNode,\n vars?: Record<string, unknown>,\n): Promise<unknown> {\n const ctx: Record<string, unknown> = {\n ...(vars ?? {}),\n card_data: node.card_data ?? {},\n requires: node.requires ?? {},\n fetched_sources: node._sourcesData ?? {},\n computed_values: node.computed_values ?? {},\n };\n return jsonata(expr).evaluate(ctx);\n}\n\n// ---------------------------------------------------------------------------\n// resolve — synchronous deep-get from node\n// ---------------------------------------------------------------------------\n\nfunction resolve(node: ComputeNode, path: string): unknown {\n if (path.startsWith('fetched_sources.')) {\n return deepGet(node._sourcesData ?? {}, path.slice('fetched_sources.'.length));\n }\n\n return deepGet(node, path);\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/** Result of validateNode — ok: true means valid, ok: false has errors[]. */\nexport interface ValidationResult {\n ok: boolean;\n errors: string[];\n}\n\nconst VALID_ELEMENT_KINDS = new Set([\n 'metric', 'table', 'editable-table', 'chart', 'form', 'filter', 'list',\n 'notes', 'todo', 'alert', 'narrative', 'badge', 'text',\n 'markdown', 'ref', 'custom', 'actions',\n]);\n\nconst ALLOWED_KEYS = new Set(['id', 'meta', 'requires', 'provides', 'view', 'card_data', 'compute', 'source_defs']);\n\nfunction validateNode(node: unknown): ValidationResult {\n const errors: string[] = [];\n\n if (!node || typeof node !== 'object' || Array.isArray(node)) {\n return { ok: false, errors: ['Node must be a non-null object'] };\n }\n\n const n = node as Record<string, unknown>;\n\n if (typeof n.id !== 'string' || !n.id) errors.push('id: required, must be a non-empty string');\n\n for (const key of Object.keys(n)) {\n if (!ALLOWED_KEYS.has(key)) errors.push(`Unknown top-level key: \"${key}\"`);\n }\n\n if (n.card_data == null || typeof n.card_data !== 'object' || Array.isArray(n.card_data)) {\n errors.push('card_data: required, must be an object');\n }\n\n if (n.meta != null) {\n if (typeof n.meta !== 'object' || Array.isArray(n.meta)) {\n errors.push('meta: must be an object');\n } else {\n const meta = n.meta as Record<string, unknown>;\n if (meta.title != null && typeof meta.title !== 'string') errors.push('meta.title: must be a string');\n if (meta.tags != null && !Array.isArray(meta.tags)) errors.push('meta.tags: must be an array');\n }\n }\n\n if (n.requires != null && !Array.isArray(n.requires)) errors.push('requires: must be an array of strings');\n\n if (n.provides != null) {\n if (!Array.isArray(n.provides)) {\n errors.push('provides: must be an array of { bindTo, ref } bindings');\n } else {\n (n.provides as unknown[]).forEach((p, i) => {\n if (!p || typeof p !== 'object' || Array.isArray(p)) {\n errors.push(`provides[${i}]: must be an object with bindTo and ref`);\n } else {\n const b = p as Record<string, unknown>;\n if (typeof b.bindTo !== 'string' || !b.bindTo) errors.push(`provides[${i}]: missing required \"bindTo\" string`);\n if (typeof b.ref !== 'string' || !b.ref) errors.push(`provides[${i}]: missing required \"ref\" string`);\n }\n });\n }\n }\n\n // compute — ordered array of { bindTo, expr } steps\n if (n.compute != null) {\n if (!Array.isArray(n.compute)) {\n errors.push('compute: must be an array of compute steps');\n } else {\n (n.compute as unknown[]).forEach((step, i) => {\n if (!step || typeof step !== 'object' || Array.isArray(step)) {\n errors.push(`compute[${i}]: must be a compute step object`);\n } else {\n const s = step as Record<string, unknown>;\n if (typeof s.bindTo !== 'string' || !s.bindTo) errors.push(`compute[${i}]: missing required \"bindTo\" property`);\n if (typeof s.expr !== 'string' || !s.expr) errors.push(`compute[${i}]: missing required \"expr\" string (JSONata expression)`);\n }\n });\n }\n }\n\n if (n.source_defs != null) {\n if (!Array.isArray(n.source_defs)) {\n errors.push('source_defs: must be an array');\n } else {\n const bindTos = new Set<string>();\n const outputFiles = new Set<string>();\n (n.source_defs as unknown[]).forEach((src, i) => {\n if (!src || typeof src !== 'object' || Array.isArray(src)) {\n errors.push(`source_defs[${i}]: must be an object`);\n } else {\n const s = src as Record<string, unknown>;\n if (typeof s.bindTo !== 'string' || !s.bindTo) {\n errors.push(`source_defs[${i}]: missing required \"bindTo\" property`);\n } else {\n if (bindTos.has(s.bindTo)) {\n errors.push(`source_defs[${i}]: bindTo \"${s.bindTo}\" is not unique across source_defs`);\n }\n bindTos.add(s.bindTo);\n }\n if (typeof s.outputFile !== 'string' || !s.outputFile) {\n errors.push(`source_defs[${i}]: missing required \"outputFile\" property`);\n } else {\n if (outputFiles.has(s.outputFile)) {\n errors.push(`source_defs[${i}]: outputFile \"${s.outputFile}\" is not unique across source_defs`);\n }\n outputFiles.add(s.outputFile);\n }\n if (s.optionalForCompletionGating != null && typeof s.optionalForCompletionGating !== 'boolean') {\n errors.push(`source_defs[${i}]: optionalForCompletionGating must be a boolean`);\n }\n }\n });\n }\n }\n\n if (n.view != null) {\n if (typeof n.view !== 'object' || Array.isArray(n.view)) {\n errors.push('view: must be an object');\n } else {\n const view = n.view as Record<string, unknown>;\n if (!Array.isArray(view.elements) || view.elements.length === 0) {\n errors.push('view.elements: required, must be a non-empty array');\n } else {\n (view.elements as Record<string, unknown>[]).forEach((elem, i) => {\n if (!elem || typeof elem !== 'object') { errors.push(`view.elements[${i}]: must be an object`); return; }\n if (!elem.kind || typeof elem.kind !== 'string') {\n errors.push(`view.elements[${i}].kind: required, must be a string`);\n } else if (!VALID_ELEMENT_KINDS.has(elem.kind as string)) {\n errors.push(`view.elements[${i}].kind: unknown kind \"${elem.kind}\". Valid: ${[...VALID_ELEMENT_KINDS].join(', ')}`);\n }\n if (elem.data != null && (typeof elem.data !== 'object' || Array.isArray(elem.data))) {\n errors.push(`view.elements[${i}].data: must be an object`);\n }\n });\n }\n if (view.layout != null && (typeof view.layout !== 'object' || Array.isArray(view.layout))) errors.push('view.layout: must be an object');\n if (view.features != null && (typeof view.features !== 'object' || Array.isArray(view.features))) errors.push('view.features: must be an object');\n }\n }\n\n return { ok: errors.length === 0, errors };\n}\n\n/**\n * Enrich source_defs with execution context for template interpolation and prompt rendering.\n * Pure function: no side effects, returns new enriched source_defs array.\n * \n * @param source_defs - Array of source definitions\n * @param context - Execution context containing requires, sourcesData, computed_values\n * @returns Promise resolving to a new array of source_defs with _projections attached.\n * Each _projections entry is the evaluated result of the corresponding projections expression.\n */\nasync function enrichSources(\n source_defs: any[] | undefined,\n context: {\n card_data?: Record<string, any>;\n requires?: Record<string, any>;\n sourcesData?: Record<string, any>; // unused post-projections, kept for call-site compat\n computed_values?: Record<string, any>; // unused post-projections, kept for call-site compat\n }\n): Promise<any[]> {\n if (!source_defs || source_defs.length === 0) return [];\n\n const evalCtx = {\n card_data: context.card_data ?? {},\n requires: context.requires ?? {},\n };\n\n return Promise.all(\n source_defs.map(async (src: any) => {\n const _projections: Record<string, unknown> = {};\n if (src.projections && typeof src.projections === 'object' && !Array.isArray(src.projections)) {\n for (const [key, expr] of Object.entries(src.projections as Record<string, string>)) {\n if (typeof expr === 'string' && expr.trim().length > 0) {\n try {\n _projections[key] = await jsonata(expr).evaluate(evalCtx);\n } catch {\n _projections[key] = undefined;\n }\n }\n }\n }\n return { ...src, _projections };\n })\n );\n}\n\nfunction enrichSourcesSync(\n source_defs: any[] | undefined,\n context: {\n card_data?: Record<string, any>;\n requires?: Record<string, any>;\n }\n): any[] {\n if (!source_defs || source_defs.length === 0) return [];\n\n const evalCtx = {\n card_data: context.card_data ?? {},\n requires: context.requires ?? {},\n };\n\n return source_defs.map((src: any) => {\n const _projections: Record<string, unknown> = {};\n if (src.projections && typeof src.projections === 'object' && !Array.isArray(src.projections)) {\n for (const [key, expr] of Object.entries(src.projections as Record<string, string>)) {\n if (typeof expr === 'string' && expr.trim().length > 0) {\n try {\n _projections[key] = jsonataSync(expr).evaluate(evalCtx);\n } catch {\n _projections[key] = undefined;\n }\n }\n }\n }\n return { ...src, _projections };\n });\n}\n\nexport const CardCompute = {\n run,\n runSync,\n eval: evalExpr,\n resolve,\n validate: validateNode,\n enrichSources,\n enrichSourcesSync,\n};\n\nexport {\n validateLiveCard,\n validateLiveCardSchema,\n validateLiveCardRuntimeExpressions,\n validateLiveCardDefinition,\n} from './schema-validator.js';\n\nexport default CardCompute;\n\n","/**\n * board-live-cards-lib — Pure logic library for the board-live-cards CLI.\n *\n * Merged from:\n * board-live-cards-all-stores.ts\n * board-live-cards-lib-types.ts\n * board-live-cards-lib-board-status.ts\n * board-live-cards-lib-card-handler.ts\n * board-live-cards-cli-board-commands.ts\n * board-live-cards-cli-card-commands.ts\n * board-live-cards-cli-callbacks.ts\n *\n * Zero platform imports. All storage is injected via adapter interfaces.\n * Safe for Node, browser, and neutral (V8/PyMiniRacer) bundles.\n */\n\nimport type { KVStorage, BlobStorage, KindValueRef } from './storage-interface.js';\nimport { serializeRef } from './storage-interface.js';\nimport { parseExecutionRef, serializeExecutionRef } from './execution-interface.js';\nimport type { ExecutionRef } from './execution-interface.js';\nimport type { GraphEvent, TaskConfig, GraphConfig } from '../../event-graph/types.js';\nimport type { LiveGraph, LiveGraphSnapshot } from '../../continuous-event-graph/types.js';\nimport { schedule } from '../../continuous-event-graph/schedule.js';\nimport type { TaskHandlerFn } from '../../continuous-event-graph/reactive.js';\nimport { CardCompute } from '../../card-compute/index.js';\nimport type { ComputeNode, ComputeStep, ComputeSource } from '../../card-compute/index.js';\nexport type { DispatchResult, InvocationAdapter } from './process-interface.js';\n\n// ============================================================================\n// ---- from board-live-cards-all-stores.ts ----\n// ============================================================================\n\n// ============================================================================\n// Card store — types\n// ============================================================================\n\nexport interface LiveCard {\n id: string;\n [key: string]: unknown;\n}\n\nexport interface CardIndexEntry {\n /** Storage-specific address (file path, Cosmos doc id, localStorage key). */\n key: string;\n /** Checksum of card content — computed by the adapter at write time. */\n checksum: string;\n updatedAt: string;\n}\n\nexport type CardIndex = Record<string, CardIndexEntry>;\nexport type CardChecksumIndex = Record<string, string>;\n\n/**\n * Per-card entry stored in the card-upsert KV cache (one key per cardId).\n * Lives alongside the board journal — NOT inside the board snapshot.\n * Purpose: dedup gate to avoid redundant task-upsert journal entries.\n *\n * Write order: journal.append() THEN kv.write() — so a crash between the two\n * leaves the journal entry intact (board is correct) and the KV stale (next\n * upsert will see \"changed\" and re-append; addNode is idempotent in the board).\n */\nexport interface CardUpsertIndexEntry {\n /** Logical reference to the card blob — absolute path for fs, blob name for cloud. */\n blobRef: string;\n /** SHA-256 of stable-JSON-serialised taskConfig. Dedup key. */\n taskConfigHash: string;\n updatedAt: string;\n}\n\n// ============================================================================\n// CardStorageAdapter — injected by the caller\n// ============================================================================\n\nexport interface CardStorageAdapter {\n readIndex(): CardIndex | null;\n writeIndex(index: CardIndex): void;\n readCard(key: string): LiveCard | null;\n /** Write card content; returns checksum of what was written. */\n writeCard(key: string, card: LiveCard): string;\n cardExists(key: string): boolean;\n defaultCardKey(cardId: string): string;\n}\n\n// ============================================================================\n// CardStore — board one-cycle (read-only)\n// ============================================================================\n\nexport interface CardStore {\n readCard(id: string): LiveCard | null;\n readCardKey(id: string): string | null;\n readAllCards(): LiveCard[];\n readChecksumIndex(): CardChecksumIndex;\n changedSince(snapshotChecksumIndex: CardChecksumIndex): string[];\n}\n\n// ============================================================================\n// CardAdminStore — CLI write interface\n// ============================================================================\n\nexport interface CardUpsertValidation {\n ok: boolean;\n error?: string;\n}\n\nexport interface CardAdminStore extends CardStore {\n validateUpsert(id: string, cardKey: string): CardUpsertValidation;\n writeCard(id: string, card: LiveCard, cardKey?: string): void;\n patchCard(id: string, jsonPath: string, value: unknown): void;\n removeCard(id: string): void;\n readIndex(): CardIndex;\n}\n\n// ============================================================================\n// createCardStore — pure logic factory\n// ============================================================================\n\nexport function createCardStore(adapter: CardStorageAdapter, onWarn?: (msg: string) => void): CardAdminStore {\n function loadIndex(): CardIndex {\n return adapter.readIndex() ?? {};\n }\n\n function applyJsonPath(obj: Record<string, unknown>, jsonPath: string, value: unknown): Record<string, unknown> {\n const segments = String(jsonPath || '').split('.').filter(Boolean);\n if (segments.length === 0) {\n return (value && typeof value === 'object' && !Array.isArray(value))\n ? value as Record<string, unknown>\n : { value };\n }\n\n const out: Record<string, unknown> = { ...obj };\n let target: Record<string, unknown> = out;\n for (let i = 0; i < segments.length - 1; i++) {\n const key = segments[i];\n const cur = target[key];\n const next = (cur && typeof cur === 'object' && !Array.isArray(cur))\n ? { ...(cur as Record<string, unknown>) }\n : {};\n target[key] = next;\n target = next;\n }\n target[segments[segments.length - 1]] = value;\n return out;\n }\n\n return {\n readCard(id: string): LiveCard | null {\n const entry = loadIndex()[id];\n if (!entry || !adapter.cardExists(entry.key)) return null;\n return adapter.readCard(entry.key);\n },\n\n readCardKey(id: string): string | null {\n return loadIndex()[id]?.key ?? null;\n },\n\n readAllCards(): LiveCard[] {\n const cards: LiveCard[] = [];\n for (const [id, entry] of Object.entries(loadIndex())) {\n if (!adapter.cardExists(entry.key)) continue;\n const card = adapter.readCard(entry.key);\n if (card) cards.push(card);\n else onWarn?.(`[card-store] could not read card \"${id}\" at key \"${entry.key}\"`);\n }\n return cards;\n },\n\n readChecksumIndex(): CardChecksumIndex {\n const result: CardChecksumIndex = {};\n for (const [id, entry] of Object.entries(loadIndex())) result[id] = entry.checksum;\n return result;\n },\n\n changedSince(snapshotChecksumIndex: CardChecksumIndex): string[] {\n const localIndex = loadIndex();\n const changed: string[] = [];\n for (const [id, entry] of Object.entries(localIndex)) {\n if (snapshotChecksumIndex[id] !== entry.checksum) changed.push(id);\n }\n for (const id of Object.keys(snapshotChecksumIndex)) {\n if (!localIndex[id]) changed.push(id);\n }\n return changed;\n },\n\n validateUpsert(id: string, cardKey: string): CardUpsertValidation {\n const index = loadIndex();\n const existingById = index[id];\n const existingByKey = Object.entries(index).find(([, e]) => e.key === cardKey);\n if (existingById && existingById.key !== cardKey)\n return { ok: false, error: `Card id \"${id}\" is already mapped to key \"${existingById.key}\", cannot remap to \"${cardKey}\"` };\n if (existingByKey && existingByKey[0] !== id)\n return { ok: false, error: `Key \"${cardKey}\" is already mapped to card id \"${existingByKey[0]}\", cannot remap to \"${id}\"` };\n return { ok: true };\n },\n\n writeCard(id: string, card: LiveCard, cardKey?: string): void {\n const index = loadIndex();\n const resolvedKey = cardKey ?? index[id]?.key ?? adapter.defaultCardKey(id);\n const checksum = adapter.writeCard(resolvedKey, card);\n index[id] = { key: resolvedKey, checksum, updatedAt: new Date().toISOString() };\n adapter.writeIndex(index);\n },\n\n patchCard(id: string, jsonPath: string, value: unknown): void {\n const index = loadIndex();\n const entry = index[id];\n if (!entry || !adapter.cardExists(entry.key)) {\n throw new Error(`card \"${id}\" not found`);\n }\n const current = adapter.readCard(entry.key);\n if (!current || typeof current !== 'object' || Array.isArray(current)) {\n throw new Error(`card \"${id}\" is not patchable`);\n }\n const next = applyJsonPath(current as Record<string, unknown>, jsonPath, value) as LiveCard;\n const checksum = adapter.writeCard(entry.key, next);\n index[id] = { key: entry.key, checksum, updatedAt: new Date().toISOString() };\n adapter.writeIndex(index);\n },\n\n removeCard(id: string): void {\n const index = loadIndex();\n if (!index[id]) return;\n delete index[id];\n adapter.writeIndex(index);\n },\n\n readIndex(): CardIndex {\n return loadIndex();\n },\n };\n}\n\n// ============================================================================\n// FetchedSourcesStore\n// ============================================================================\n\nexport interface FetchedSourcesStore {\n /** Read committed source content. Returns parsed JSON or raw string; null if not yet committed. */\n readSourceData(cardId: string, outputFile: string): unknown;\n /** Stage incoming source data under deliveryToken. resolveRef converts the ref to content bytes. */\n ingestSourceDataStaged(cardId: string, outputFile: string, ref: KindValueRef, deliveryToken: string): void;\n /** Move staged data to live position. Returns false if staged entry is absent (stale delivery). */\n commitSourceData(cardId: string, outputFile: string, deliveryToken: string): boolean;\n /** True if live (committed) source data exists for this outputFile. */\n hasSource(cardId: string, outputFile: string): boolean;\n}\n\nexport function createFetchedSourcesStore(\n blob: BlobStorage,\n resolveRef: (ref: KindValueRef) => string,\n): FetchedSourcesStore {\n return {\n readSourceData(cardId, outputFile): unknown {\n const raw = blob.read(`${cardId}/${outputFile}`);\n if (raw == null) return null;\n const trimmed = raw.trim();\n if (!trimmed) return null;\n try { return JSON.parse(trimmed); } catch { return trimmed; }\n },\n ingestSourceDataStaged(cardId, outputFile, ref, deliveryToken): void {\n const content = resolveRef(ref);\n blob.write(`${cardId}/.staged/${deliveryToken}/${outputFile}`, content);\n },\n commitSourceData(cardId, outputFile, deliveryToken): boolean {\n const stagedKey = `${cardId}/.staged/${deliveryToken}/${outputFile}`;\n const content = blob.read(stagedKey);\n if (content == null) return false;\n blob.write(`${cardId}/${outputFile}`, content);\n blob.remove(stagedKey);\n return true;\n },\n hasSource(cardId, outputFile): boolean {\n return blob.exists(`${cardId}/${outputFile}`);\n },\n };\n}\n\n// ============================================================================\n// Journal store — types\n// ============================================================================\n\nexport interface JournalEntry {\n id: string;\n event: GraphEvent;\n}\n\nexport interface JournalStorageAdapter {\n readAllEntries(): JournalEntry[];\n appendEntry(entry: JournalEntry): void;\n generateId(): string;\n}\n\nexport interface JournalStore {\n readEntriesAfterCursor(cursor: string): { events: GraphEvent[]; newCursor: string };\n pendingCount(cursor: string): number;\n}\n\nexport interface JournalAdminStore extends JournalStore {\n appendEvent(event: GraphEvent): void;\n}\n\nexport function createJournalStore(adapter: JournalStorageAdapter): JournalAdminStore {\n function entriesAfterCursor(cursor: string): JournalEntry[] {\n const all = adapter.readAllEntries();\n if (!cursor) return all;\n const idx = all.findIndex(e => e.id === cursor);\n return idx === -1 ? all : all.slice(idx + 1);\n }\n\n return {\n readEntriesAfterCursor(cursor: string): { events: GraphEvent[]; newCursor: string } {\n const entries = entriesAfterCursor(cursor);\n if (entries.length === 0) return { events: [], newCursor: cursor };\n return { events: entries.map(e => e.event), newCursor: entries[entries.length - 1].id };\n },\n\n pendingCount(cursor: string): number {\n return entriesAfterCursor(cursor).length;\n },\n\n appendEvent(event: GraphEvent): void {\n adapter.appendEntry({ id: adapter.generateId(), event });\n },\n };\n}\n\n// ============================================================================\n// ExecutionRequest store\n// ============================================================================\n\nexport interface ExecutionRequestEntry {\n taskKind: string;\n payload: unknown;\n}\n\nexport interface ExecutionRequestStore {\n appendEntries(journalId: string, entries: ExecutionRequestEntry[]): void;\n dispatchEntriesForJournalId(journalId: string, processorFn: (entry: ExecutionRequestEntry) => void): void;\n}\n\nexport function createExecutionRequestStore(\n kv: KVStorage,\n onDispatchFailed: (entry: ExecutionRequestEntry, error: string) => void,\n): ExecutionRequestStore {\n return {\n appendEntries(journalId: string, entries: ExecutionRequestEntry[]): void {\n if (!journalId || entries.length === 0) return;\n const existing = (kv.read(journalId) as ExecutionRequestEntry[] | null) ?? [];\n kv.write(journalId, [...existing, ...entries]);\n },\n\n dispatchEntriesForJournalId(journalId: string, processorFn: (entry: ExecutionRequestEntry) => void): void {\n if (!journalId) return;\n const entries = kv.read(journalId) as ExecutionRequestEntry[] | null;\n if (!entries || entries.length === 0) return;\n for (const entry of entries) {\n try { processorFn(entry); } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n try { onDispatchFailed(entry, msg); } catch { /* guard against failure in error handler */ }\n }\n }\n kv.delete(journalId);\n },\n };\n}\n\n// ============================================================================\n// StateSnapshot store\n// ============================================================================\n\nexport const SNAPSHOT_SCHEMA_VERSION_V1 = 'v1';\n\nexport const BOARD_GRAPH_KEY = 'board/graph';\nexport const BOARD_LAST_JOURNAL_PROCESSED_ID_KEY = 'board/lastJournalProcessedId';\n\nexport function cardRuntimeKey(cardId: string): string {\n return `cards/${cardId}/runtime`;\n}\n\nexport function cardFetchedSourcesManifestKey(cardId: string): string {\n return `cards/${cardId}/fetched-sources-manifest`;\n}\n\nexport interface CardRuntimeSnapshot {\n _sources: Record<string, { lastRequestedAt?: string; lastFetchedAt?: string; queueRequestedAt?: string }>;\n _lastExecutionCount?: number;\n}\n\nexport interface CardRuntimeStore {\n readRuntime(cardId: string): CardRuntimeSnapshot;\n writeRuntime(cardId: string, state: CardRuntimeSnapshot): void;\n}\n\nexport function createCardRuntimeStore(kv: KVStorage): CardRuntimeStore {\n return {\n readRuntime(cardId) {\n return (kv.read(cardRuntimeKey(cardId)) as CardRuntimeSnapshot | null) ?? { _sources: {} };\n },\n writeRuntime(cardId, state) {\n kv.write(cardRuntimeKey(cardId), state);\n },\n };\n}\n\nexport interface FetchedSourceManifestEntry {\n outputFile: string;\n blobRef: string;\n fetchedAt: string;\n sourceChecksum?: string;\n contentType?: string;\n sizeBytes?: number;\n}\n\nexport interface StateSnapshotReadView {\n version: string | null;\n values: Record<string, unknown>;\n}\n\nexport interface StateSnapshotCommitEnvelope {\n schemaVersion: typeof SNAPSHOT_SCHEMA_VERSION_V1;\n expectedVersion: string | null;\n commitId: string;\n committedAt: string;\n deleteKeys: string[];\n shallowMerge: Record<string, unknown>;\n}\n\nexport interface StateSnapshotCommitSuccess {\n ok: true;\n newVersion: string;\n}\n\nexport interface StateSnapshotCommitVersionMismatch {\n ok: false;\n reason: 'version-mismatch';\n currentVersion: string | null;\n}\n\nexport type StateSnapshotCommitResult =\n | StateSnapshotCommitSuccess\n | StateSnapshotCommitVersionMismatch;\n\nexport interface StateSnapshotStorageAdapter {\n readValues(scopeId: string): StateSnapshotReadView;\n writeValues(scopeId: string, nextValues: Record<string, unknown>, deletedKeys: string[]): string;\n}\n\nexport interface StateSnapshotStore {\n readSnapshot(scopeId: string): StateSnapshotReadView;\n commitSnapshot(scopeId: string, envelope: StateSnapshotCommitEnvelope): StateSnapshotCommitResult;\n}\n\nexport function applyStateSnapshotCommitEnvelope(\n current: Record<string, unknown>,\n envelope: Pick<StateSnapshotCommitEnvelope, 'deleteKeys' | 'shallowMerge'>,\n): Record<string, unknown> {\n const next: Record<string, unknown> = { ...current };\n for (const key of envelope.deleteKeys) {\n delete next[key];\n }\n return { ...next, ...envelope.shallowMerge };\n}\n\nexport function createStateSnapshotStore(adapter: StateSnapshotStorageAdapter): StateSnapshotStore {\n return {\n readSnapshot(scopeId: string): StateSnapshotReadView {\n return adapter.readValues(scopeId);\n },\n\n commitSnapshot(scopeId: string, envelope: StateSnapshotCommitEnvelope): StateSnapshotCommitResult {\n if (envelope.schemaVersion !== SNAPSHOT_SCHEMA_VERSION_V1) {\n throw new Error(`Unsupported snapshot schema version: ${envelope.schemaVersion}`);\n }\n const current = adapter.readValues(scopeId);\n if (current.version !== envelope.expectedVersion) {\n return { ok: false, reason: 'version-mismatch', currentVersion: current.version };\n }\n const nextValues = applyStateSnapshotCommitEnvelope(current.values, envelope);\n const newVersion = adapter.writeValues(scopeId, nextValues, envelope.deleteKeys);\n return { ok: true, newVersion };\n },\n };\n}\n\n// ============================================================================\n// BoardConfigStore\n// ============================================================================\n\nexport interface BoardConfigStore {\n readTaskExecutorRef(): ExecutionRef | undefined;\n writeTaskExecutorRef(ref: ExecutionRef): void;\n readChatHandlerRef(): ExecutionRef | undefined;\n writeChatHandlerRef(ref: ExecutionRef): void;\n readCardStoreRef(): string | null;\n writeCardStoreRef(ref: string): void;\n readOutputsStoreRef(): string | null;\n writeOutputsStoreRef(ref: string): void;\n /** @deprecated use readChatHandlerRef */\n readChatHandler(): string | undefined;\n /** @deprecated use writeChatHandlerRef */\n writeChatHandler(value: string): void;\n}\n\nexport function createBoardConfigStore(kv: KVStorage): BoardConfigStore {\n function readKey(key: string): string | null {\n const v = kv.read(key);\n if (v == null) return null;\n return typeof v === 'string' ? v : JSON.stringify(v);\n }\n\n return {\n readTaskExecutorRef(): ExecutionRef | undefined {\n const raw = readKey('task-executor');\n if (!raw?.trim()) return undefined;\n return parseExecutionRef(raw.trim());\n },\n\n writeTaskExecutorRef(ref: ExecutionRef): void {\n kv.write('task-executor', serializeExecutionRef(ref));\n },\n\n readChatHandlerRef(): ExecutionRef | undefined {\n const raw = readKey('chat-handler');\n if (!raw?.trim()) return undefined;\n return parseExecutionRef(raw.trim());\n },\n\n writeChatHandlerRef(ref: ExecutionRef): void {\n kv.write('chat-handler', serializeExecutionRef(ref));\n },\n\n readCardStoreRef(): string | null {\n return readKey('card-store-ref');\n },\n\n writeCardStoreRef(ref: string): void {\n kv.write('card-store-ref', ref);\n },\n\n readOutputsStoreRef(): string | null {\n return readKey('outputs-store-ref');\n },\n\n writeOutputsStoreRef(ref: string): void {\n kv.write('outputs-store-ref', ref);\n },\n\n readChatHandler(): string | undefined {\n return readKey('chat-handler')?.trim() || undefined;\n },\n\n writeChatHandler(value: string): void {\n kv.write('chat-handler', value);\n },\n };\n}\n\n// ============================================================================\n// PublishedOutputsStore\n// ============================================================================\n\nexport type OutputStoreEvent =\n | { kind: 'computed_values'; cardId: string; values: Record<string, unknown> }\n | { kind: 'data_object'; key: string; payload: unknown }\n | { kind: 'status'; status: unknown };\n\nexport interface PublishedOutputsStore {\n writeComputedValues(cardId: string, values: Record<string, unknown>): void;\n readComputedValues(cardId: string): unknown | null;\n readAllComputedValues(): Record<string, unknown>;\n writeDataObjects(data: Record<string, unknown>): void;\n readDataObject(key: string): unknown | null;\n readAllDataObjects(): Record<string, unknown>;\n writeStatusSnapshot(status: unknown): void;\n readStatusSnapshot(): unknown | null;\n}\n\nexport function createPublishedOutputsStore(kv: KVStorage): PublishedOutputsStore {\n return {\n writeComputedValues(cardId, values) {\n kv.write(`cards/${cardId}/computed_values`, values);\n },\n readComputedValues(cardId) { return kv.read(`cards/${cardId}/computed_values`); },\n readAllComputedValues() {\n const out: Record<string, unknown> = {};\n for (const key of kv.listKeys('cards/')) {\n const m = key.match(/^cards\\/([^/]+)\\/computed_values$/);\n if (m) out[m[1]] = kv.read(key);\n }\n return out;\n },\n writeDataObjects(data) {\n for (const [token, payload] of Object.entries(data)) {\n if (!token) continue;\n kv.write(`data-objects/${token}`, payload);\n }\n },\n readDataObject(key) { return kv.read(`data-objects/${key}`); },\n readAllDataObjects() {\n const out: Record<string, unknown> = {};\n for (const key of kv.listKeys('data-objects/')) {\n out[key.slice('data-objects/'.length)] = kv.read(key);\n }\n return out;\n },\n writeStatusSnapshot(status) {\n kv.write('status', status);\n },\n readStatusSnapshot() { return kv.read('status'); },\n };\n}\n\n// ============================================================================\n// Future-facing blob and read-model cache interfaces\n// ============================================================================\n\nexport interface FetchedSourcesBlobStore {\n readBlob(blobRef: string): Promise<unknown | null>;\n}\n\nexport interface PublishedBoardStatusCache {\n writeStatusBestEffort(scopeId: string, statusPayload: unknown): Promise<void>;\n readStatus(scopeId: string): Promise<unknown | null>;\n}\n\n// ============================================================================\n// ---- from board-live-cards-lib-types.ts ----\n// ============================================================================\n\nexport interface SourceRuntimeEntry {\n lastRequestedAt?: string;\n lastFetchedAt?: string;\n lastError?: string;\n queueRequestedAt?: string;\n}\n\nexport type FetchRuntimeEntry = SourceRuntimeEntry;\n\nexport interface SourceTokenPayload {\n cbk: string;\n rg: string;\n br: string;\n cid: string;\n b: string;\n d: string;\n cs?: string;\n rqt: string;\n}\n\nexport function isSourceInFlight(entry: FetchRuntimeEntry | undefined): boolean {\n if (!entry?.lastRequestedAt) return false;\n return !entry.lastFetchedAt || entry.lastFetchedAt < entry.lastRequestedAt;\n}\n\nexport function decideSourceAction(\n entry: FetchRuntimeEntry | undefined,\n queueRequestedAt: string,\n): 'dispatch' | 'in-flight' | 'idle' {\n if (!entry?.lastRequestedAt) return 'dispatch';\n const inFlight = isSourceInFlight(entry);\n if (inFlight) return 'in-flight';\n if (!entry.lastFetchedAt) return 'dispatch';\n if (entry.lastFetchedAt < queueRequestedAt) return 'dispatch';\n return 'idle';\n}\n\nexport function nextEntryAfterFetchDelivery<T extends FetchRuntimeEntry>(\n entry: T,\n fetchedAt: string,\n): T {\n const next = { ...entry, lastFetchedAt: fetchedAt };\n delete (next as FetchRuntimeEntry).lastError;\n return next as T;\n}\n\nexport function nextEntryAfterFetchFailure<T extends FetchRuntimeEntry>(\n entry: T,\n reason: string,\n): T {\n const next = { ...entry, lastError: reason };\n delete (next as FetchRuntimeEntry).lastFetchedAt;\n return next as T;\n}\n\nexport interface CardHandlerAdapters {\n cardStore: CardStore;\n cardRuntimeStore: CardRuntimeStore;\n fetchedSourcesStore: FetchedSourcesStore;\n outputStore: PublishedOutputsStore;\n executionRequestStore: ExecutionRequestStore;\n}\n\nexport interface CommandResponse<T extends Record<string, unknown> = Record<string, unknown>> {\n status: 'success' | 'error';\n data: T;\n error?: string;\n}\n\nexport const Resp = {\n success<T extends Record<string, unknown>>(data: T): CommandResponse<T> {\n return { status: 'success', data };\n },\n\n error(error: string, data: Record<string, unknown> = {}): CommandResponse {\n return { status: 'error', data, error };\n },\n\n getStatus(r: CommandResponse): 'success' | 'error' {\n return r.status;\n },\n\n getData<T extends Record<string, unknown>>(r: CommandResponse<T>): T {\n return r.data;\n },\n\n isSuccess(r: CommandResponse): boolean {\n return r.status === 'success';\n },\n} as const;\n\n// ============================================================================\n// ---- from board-live-cards-lib-board-status.ts ----\n// ============================================================================\n\nexport interface BoardStatusCard {\n name: string;\n status: string;\n error?: {\n message: string;\n code?: string;\n at?: string;\n source?: 'task-runtime' | 'source-fetch' | 'timeout' | 'unknown';\n };\n requires: string[];\n requires_satisfied: string[];\n requires_missing: string[];\n provides_declared: string[];\n provides_runtime: string[];\n blocked_by: string[];\n unblocks: string[];\n runtime: {\n attempt_count: number;\n restart_count: number;\n in_progress_since: string | null;\n last_transition_at: string | null;\n last_completed_at: string | null;\n last_restarted_at: string | null;\n status_age_ms: number | null;\n };\n}\n\nexport interface BoardStatusObject {\n schema_version: 'v1';\n meta: {\n board: {\n path: string;\n };\n };\n summary: {\n card_count: number;\n completed: number;\n eligible: number;\n pending: number;\n blocked: number;\n unresolved: number;\n failed?: number;\n in_progress?: number;\n orphan_cards?: number;\n topology?: {\n edge_count: number;\n max_fan_out_card: string | null;\n max_fan_out: number;\n };\n };\n cards: BoardStatusCard[];\n}\n\nexport function buildBoardStatusObject(boardPath: string, live: LiveGraph): BoardStatusObject {\n const taskState = live.state.tasks;\n const taskConfig = live.config.tasks;\n const cardNames = Object.keys(taskState);\n const sched = schedule(live);\n\n const statusCounts = {\n completed: 0,\n failed: 0,\n in_progress: 0,\n pending: 0,\n blocked: 0,\n unresolved: 0,\n };\n\n const waitingByCard = new Map<string, string[]>();\n for (const p of sched.pending) waitingByCard.set(p.taskName, p.waitingOn);\n for (const u of sched.unresolved) waitingByCard.set(u.taskName, u.missingTokens);\n for (const b of sched.blocked) waitingByCard.set(b.taskName, b.failedTokens);\n\n const dependentsByToken = new Map<string, string[]>();\n for (const [name, cfg] of Object.entries(taskConfig)) {\n for (const token of cfg.requires ?? []) {\n const dependents = dependentsByToken.get(token) ?? [];\n dependents.push(name);\n dependentsByToken.set(token, dependents);\n }\n }\n\n const cards: BoardStatusCard[] = cardNames.sort().map((name) => {\n const state = taskState[name] as {\n status: string;\n data?: Record<string, unknown>;\n error?: string;\n startedAt?: string;\n completedAt?: string;\n failedAt?: string;\n lastUpdated?: string;\n executionCount?: number;\n retryCount?: number;\n };\n const cfg = taskConfig[name] ?? { requires: [], provides: [] };\n\n if (state.status === 'completed') statusCounts.completed += 1;\n else if (state.status === 'failed') statusCounts.failed += 1;\n else if (state.status === 'in-progress') statusCounts.in_progress += 1;\n\n const requires = cfg.requires ?? [];\n const provides = cfg.provides ?? [];\n const runtimeKeys = Object.keys(state.data ?? {}).sort();\n const requiresSatisfied = requires.filter(token => live.state.availableOutputs.includes(token));\n const requiresMissing = requires.filter(token => !live.state.availableOutputs.includes(token));\n const blockedBy = waitingByCard.get(name) ?? requiresMissing;\n\n const unblocks = new Set<string>();\n for (const token of provides) {\n for (const dependent of dependentsByToken.get(token) ?? []) {\n if (dependent !== name) unblocks.add(dependent);\n }\n }\n\n const lastFailureAt = state.failedAt;\n const error = state.error\n ? {\n message: state.error,\n code: 'TASK_FAILED',\n at: lastFailureAt,\n source: 'task-runtime' as const,\n }\n : undefined;\n\n return {\n name,\n status: state.status,\n error,\n requires,\n requires_satisfied: requiresSatisfied,\n requires_missing: requiresMissing,\n provides_declared: provides,\n provides_runtime: runtimeKeys,\n blocked_by: blockedBy,\n unblocks: Array.from(unblocks).sort(),\n runtime: {\n attempt_count: state.executionCount ?? 0,\n restart_count: state.retryCount ?? 0,\n in_progress_since: state.status === 'in-progress' ? (state.startedAt ?? null) : null,\n last_transition_at: state.lastUpdated ?? null,\n last_completed_at: state.completedAt ?? null,\n last_restarted_at: state.startedAt ?? null,\n // Keep status snapshots immutable across reads: this field must not depend on wall-clock pull time.\n status_age_ms: state.lastUpdated ? 0 : null,\n },\n };\n });\n\n statusCounts.pending = sched.pending.length;\n statusCounts.blocked = sched.blocked.length;\n statusCounts.unresolved = sched.unresolved.length;\n\n const fanOut = cards\n .map(c => ({ name: c.name, fanOut: c.unblocks.length }))\n .sort((a, b) => b.fanOut - a.fanOut || a.name.localeCompare(b.name));\n const maxFanOut = fanOut.length > 0 ? fanOut[0] : { name: null, fanOut: 0 };\n\n const allRequires = new Set<string>();\n for (const cfg of Object.values(taskConfig)) {\n for (const r of cfg.requires ?? []) allRequires.add(r);\n }\n let orphanCards = 0;\n for (const [name, cfg] of Object.entries(taskConfig)) {\n const requiresNone = (cfg.requires ?? []).length === 0;\n const providesList = cfg.provides ?? [];\n const feedsAny = providesList.some(p => (dependentsByToken.get(p) ?? []).some(d => d !== name));\n if (requiresNone && !feedsAny) orphanCards += 1;\n }\n\n return {\n schema_version: 'v1',\n meta: { board: { path: boardPath } },\n summary: {\n card_count: cardNames.length,\n completed: statusCounts.completed,\n eligible: sched.eligible.length,\n pending: statusCounts.pending,\n blocked: statusCounts.blocked,\n unresolved: statusCounts.unresolved,\n failed: statusCounts.failed,\n in_progress: statusCounts.in_progress,\n orphan_cards: orphanCards,\n topology: {\n edge_count: Array.from(allRequires).length,\n max_fan_out_card: maxFanOut.name,\n max_fan_out: maxFanOut.fanOut,\n },\n },\n cards,\n };\n}\n\n// ============================================================================\n// ---- from board-live-cards-lib-card-handler.ts ----\n// ============================================================================\n\nfunction nowHighRes(): string {\n return new Date().toISOString();\n}\n\nexport function createCardHandlerFn(\n baseRef: KindValueRef,\n journalId: string,\n adapters: CardHandlerAdapters,\n taskCompletedFn: (taskName: string, data: Record<string, unknown>) => void,\n _taskFailedFn: (taskName: string, error: string) => void,\n writeComputedValuesFn?: (cardId: string, values: Record<string, unknown>) => void,\n writeDataObjectsFn?: (data: Record<string, unknown>) => void,\n): TaskHandlerFn {\n return async (input) => {\n const pendingRequests: ExecutionRequestEntry[] = [];\n const card = adapters.cardStore.readCard(input.nodeId);\n if (!card) return 'task-initiate-failure';\n\n const cardId = card.id as string;\n const cardState = (card.card_data ?? {}) as Record<string, unknown>;\n const allSources: ComputeSource[] = (card.source_defs ?? []) as ComputeSource[];\n const requiredSources = allSources.filter(s => s.optionalForCompletionGating !== true);\n\n let state: CardRuntimeSnapshot = adapters.cardRuntimeStore.readRuntime(cardId);\n let dirty = false;\n\n const flush = (): void => {\n if (!dirty) return;\n adapters.cardRuntimeStore.writeRuntime(cardId, state);\n dirty = false;\n };\n\n const getSourceEntry = (outputFile: string): SourceRuntimeEntry =>\n ({ ...(state._sources[outputFile] ?? {}) });\n const setSourceEntry = (outputFile: string, entry: SourceRuntimeEntry): void => {\n state._sources[outputFile] = entry; dirty = true;\n };\n\n const currentExecutionCount = input.taskState?.executionCount ?? 0;\n const lastExecCount = state._lastExecutionCount;\n if (typeof lastExecCount === 'number' && lastExecCount !== currentExecutionCount) {\n state._sources = {}; dirty = true;\n }\n if (lastExecCount !== currentExecutionCount) {\n state._lastExecutionCount = currentExecutionCount; dirty = true;\n }\n\n\n if (input.update) {\n const u = input.update;\n const outputFile = u.outputFile as string;\n if (outputFile) {\n const entry = getSourceEntry(outputFile);\n if (u.failure) {\n setSourceEntry(outputFile, nextEntryAfterFetchFailure(entry, (u.reason as string | undefined) ?? 'unknown'));\n } else {\n const incomingRqt = u.rqt as string;\n if (!entry.lastFetchedAt || incomingRqt > entry.lastFetchedAt) {\n const deliveryToken = typeof u.deliveryToken === 'string' ? u.deliveryToken : undefined;\n if (deliveryToken) {\n adapters.fetchedSourcesStore.commitSourceData(cardId, outputFile, deliveryToken);\n }\n setSourceEntry(outputFile, nextEntryAfterFetchDelivery(entry, incomingRqt));\n }\n }\n flush();\n }\n }\n\n const sourcesData: Record<string, unknown> = {};\n for (const src of allSources) {\n if (src.outputFile) {\n const content = adapters.fetchedSourcesStore.readSourceData(cardId, src.outputFile as string);\n if (content !== null) {\n sourcesData[src.bindTo] = content;\n }\n }\n }\n\n const requires: Record<string, unknown> = {};\n for (const [token, taskData] of Object.entries(input.state ?? {})) {\n if (taskData !== null && typeof taskData === 'object' && !Array.isArray(taskData)) {\n const unwrapped = (taskData as Record<string, unknown>)[token];\n requires[token] = unwrapped !== undefined ? unwrapped : taskData;\n } else {\n requires[token] = taskData;\n }\n }\n\n const computeNode: ComputeNode = {\n id: cardId,\n card_data: { ...cardState },\n requires,\n source_defs: allSources,\n compute: card.compute as ComputeStep[] | undefined,\n };\n computeNode._sourcesData = sourcesData;\n if (card.compute) {\n CardCompute.runSync(computeNode, { sourcesData });\n }\n\n (writeComputedValuesFn ?? adapters.outputStore.writeComputedValues.bind(adapters.outputStore))(cardId, computeNode.computed_values ?? {});\n\n const enrichedCard = { ...card };\n const enrichedSources = CardCompute.enrichSourcesSync(\n Array.isArray(card.source_defs) ? card.source_defs : undefined,\n {\n card_data: card.card_data as Record<string, unknown>,\n requires,\n },\n );\n\n const dir = baseRef.value;\n enrichedCard.source_defs = Array.isArray(enrichedSources)\n ? enrichedSources.map(src => ({\n ...src,\n boardDir: typeof src.boardDir === 'string' && src.boardDir ? src.boardDir : dir,\n }))\n : enrichedSources;\n\n const now = nowHighRes();\n const runQueuedAt = input.update ? undefined : now;\n\n const undeliveredRequired = requiredSources.filter(s => {\n const outputFile = s.outputFile;\n if (typeof outputFile !== 'string' || !outputFile) return true;\n let entry = getSourceEntry(outputFile);\n if (runQueuedAt) {\n entry = { ...entry, queueRequestedAt: runQueuedAt };\n setSourceEntry(outputFile, entry);\n }\n const qrt = entry.queueRequestedAt ?? entry.lastRequestedAt ?? now;\n const action = decideSourceAction(entry, qrt);\n if (action === 'in-flight') return false;\n return action === 'dispatch';\n });\n\n flush();\n\n if (undeliveredRequired.length > 0) {\n let stampedAny = false;\n let dispatchRqt = now;\n for (const src of undeliveredRequired) {\n const outputFile = src.outputFile;\n if (typeof outputFile !== 'string' || !outputFile) continue;\n const entry = getSourceEntry(outputFile);\n const queuedAt = entry.queueRequestedAt ?? now;\n setSourceEntry(outputFile, { ...entry, lastRequestedAt: queuedAt });\n dispatchRqt = queuedAt;\n stampedAny = true;\n }\n if (stampedAny) flush();\n if (!stampedAny) return 'task-initiated';\n\n pendingRequests.push({ taskKind: 'source-fetch', payload: { boardRef: serializeRef(baseRef), enrichedCard: enrichedCard as Record<string, unknown>, callbackToken: input.callbackToken, rqt: dispatchRqt } });\n adapters.executionRequestStore.appendEntries(journalId, pendingRequests);\n return 'task-initiated';\n }\n\n const providesBindings = (card.provides ?? []) as { bindTo: string; ref: string }[];\n const data: Record<string, unknown> = {};\n for (const { bindTo, ref } of providesBindings) {\n data[bindTo] = CardCompute.resolve(computeNode, ref);\n }\n\n (writeDataObjectsFn ?? adapters.outputStore.writeDataObjects.bind(adapters.outputStore))(data);\n\n const undeliveredOptional = allSources.filter(s => {\n if (s.optionalForCompletionGating !== true) return false;\n const entry = getSourceEntry(s.outputFile as string);\n if (!entry.lastRequestedAt) return true;\n if (!entry.lastFetchedAt) return true;\n return entry.lastFetchedAt <= entry.lastRequestedAt;\n });\n if (undeliveredOptional.length > 0) {\n pendingRequests.push({ taskKind: 'source-fetch', payload: { boardRef: serializeRef(baseRef), enrichedCard: enrichedCard as Record<string, unknown>, callbackToken: input.callbackToken, rqt: now } });\n }\n\n taskCompletedFn(input.nodeId, data);\n if (pendingRequests.length > 0) adapters.executionRequestStore.appendEntries(journalId, pendingRequests);\n return 'task-initiated';\n };\n}\n\n// ============================================================================\n// ---- pure constants / codecs lifted from board-live-cards-cli.ts ----\n// ============================================================================\n\nexport const EMPTY_CONFIG: GraphConfig = { settings: { completion: 'manual', refreshStrategy: 'data-changed' }, tasks: {} } as GraphConfig;\n\n/** Envelope stored in the snapshot store — wraps the LiveGraph snapshot with journal pointer. */\nexport interface BoardEnvelope {\n lastDrainedJournalId: string;\n graph: LiveGraphSnapshot;\n}\n\nexport function boardEnvelopeToSnapshotEntries(envelope: BoardEnvelope): Record<string, unknown> {\n return {\n [BOARD_GRAPH_KEY]: envelope.graph,\n [BOARD_LAST_JOURNAL_PROCESSED_ID_KEY]: envelope.lastDrainedJournalId,\n };\n}\n\nexport function snapshotEntriesToBoardEnvelope(entries: Record<string, unknown>): BoardEnvelope {\n const graph = entries[BOARD_GRAPH_KEY] as LiveGraphSnapshot | undefined;\n const lastDrainedJournalId = entries[BOARD_LAST_JOURNAL_PROCESSED_ID_KEY] as string | undefined;\n if (!graph || typeof graph !== 'object') {\n throw new Error(`State snapshot is missing required key: ${BOARD_GRAPH_KEY}`);\n }\n return {\n graph,\n lastDrainedJournalId: typeof lastDrainedJournalId === 'string' ? lastDrainedJournalId : '',\n };\n}\n\nexport interface CardInventoryEntry {\n cardId: string;\n cardFilePath: string;\n addedAt: string;\n}\n\nexport interface CardInventoryIndex {\n byCardId: Map<string, CardInventoryEntry>;\n byCardPath: Map<string, CardInventoryEntry>;\n}\n\n/**\n * Transform a LiveCard into a TaskConfig for the reactive graph.\n * Every card gets handler: 'card-handler'.\n */\nexport function liveCardToTaskConfig(card: LiveCard): TaskConfig {\n const requires = card.requires as string[] | undefined;\n const provides = (card.provides as Array<{ bindTo: string }> | undefined)?.map(p => p.bindTo) ?? [];\n\n return {\n requires: requires && requires.length > 0 ? requires : undefined,\n provides,\n taskHandlers: ['card-handler'],\n description: (card.meta as { title?: string } | undefined)?.title ?? card.id,\n };\n}\n","/**\n * storage-localstorage-adapters.ts\n *\n * Browser localStorage implementations of the board-live-cards storage primitives:\n * BlobStorage — localStorage keys prefixed with `${prefix}:blob:`\n * KVStorage — localStorage keys prefixed with `${prefix}:kv:`, values JSON-encoded\n * JournalStorageAdapter — single localStorage key holding a JSON array of entries\n * CardStorageAdapter — KV-backed, compatible with createCardStore()\n *\n * No Node imports. Requires globalThis.localStorage (browser / jsdom environment).\n */\n\nimport type { BlobStorage, KVStorage, JSONStorage } from '../common/storage-interface.js';\nimport type { JournalStorageAdapter, CardStorageAdapter, JournalEntry, LiveCard, CardIndex } from '../common/board-live-cards-lib.js';\n\n// ============================================================================\n// Stable JSON + sync hash\n// Used for card dedup and snapshot versioning. Not security-sensitive.\n// ============================================================================\n\nfunction stableJson(value: unknown): string {\n if (value === null || value === undefined || typeof value !== 'object') return JSON.stringify(value);\n if (Array.isArray(value)) return `[${(value as unknown[]).map(stableJson).join(',')}]`;\n const obj = value as Record<string, unknown>;\n const keys = Object.keys(obj).sort();\n return `{${keys.map(k => `${JSON.stringify(k)}:${stableJson(obj[k])}`).join(',')}}`;\n}\n\nfunction fnv32a(str: string, seed: number): number {\n let h = seed >>> 0;\n for (let i = 0; i < str.length; i++) {\n h ^= str.charCodeAt(i);\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return h;\n}\n\n/**\n * Synchronous stable content hash for browser environments.\n * Uses four FNV-1a 32-bit passes to produce 32 hex chars.\n * Deterministic and cross-session stable; NOT cryptographically secure.\n */\nexport function computeStableJsonHashBrowser(value: unknown): string {\n const str = stableJson(value);\n const a = fnv32a(str, 0x811c9dc5);\n const b = fnv32a(str, 0xdeadbeef);\n const c = fnv32a(str, 0x01234567);\n const d = fnv32a(str, 0xfeedface);\n return [a, b, c, d].map(n => n.toString(16).padStart(8, '0')).join('');\n}\n\n// ============================================================================\n// createLocalStorageBlobStorage\n// ============================================================================\n\nexport function createLocalStorageBlobStorage(prefix: string): BlobStorage {\n function key(k: string): string { return `${prefix}:blob:${k}`; }\n const textEncoder = new TextEncoder();\n\n function encodeBytes(bytes: Uint8Array): string {\n if (typeof btoa === 'function') {\n let bin = '';\n for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);\n return btoa(bin);\n }\n return '';\n }\n\n function decodeBytes(encoded: string): Uint8Array {\n if (typeof atob === 'function') {\n const bin = atob(encoded);\n const out = new Uint8Array(bin.length);\n for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);\n return out;\n }\n return new Uint8Array();\n }\n\n return {\n read(k: string): string | null {\n return globalThis.localStorage.getItem(key(k));\n },\n write(k: string, content: string): void {\n globalThis.localStorage.setItem(key(k), content);\n },\n exists(k: string): boolean {\n return globalThis.localStorage.getItem(key(k)) !== null;\n },\n remove(k: string): void {\n globalThis.localStorage.removeItem(key(k));\n },\n\n readBytes(k: string): Uint8Array | null {\n const raw = globalThis.localStorage.getItem(key(k));\n if (raw === null) return null;\n try {\n const parsed = JSON.parse(raw) as { __kind?: string; data?: string };\n if (parsed && parsed.__kind === 'bytes-b64' && typeof parsed.data === 'string') {\n return decodeBytes(parsed.data);\n }\n } catch {\n // fall through to plain text path\n }\n return textEncoder.encode(raw);\n },\n\n writeBytes(k: string, content: Uint8Array): void {\n // Store binary payloads as base64 envelope to avoid lossy UTF-8 coercion.\n const envelope = JSON.stringify({ __kind: 'bytes-b64', data: encodeBytes(content) });\n globalThis.localStorage.setItem(key(k), envelope);\n },\n\n listKeys(prefix2?: string): string[] {\n const marker = key(prefix2 ?? '');\n const out: string[] = [];\n for (let i = 0; i < globalThis.localStorage.length; i++) {\n const k = globalThis.localStorage.key(i);\n if (k && k.startsWith(marker)) out.push(k.slice(key('').length));\n }\n return out.sort();\n },\n\n stat(k: string) {\n const raw = globalThis.localStorage.getItem(key(k));\n if (raw === null) return null;\n let size = textEncoder.encode(raw).byteLength;\n try {\n const parsed = JSON.parse(raw) as { __kind?: string; data?: string };\n if (parsed && parsed.__kind === 'bytes-b64' && typeof parsed.data === 'string') {\n size = decodeBytes(parsed.data).byteLength;\n }\n } catch {\n // plain text path\n }\n return { key: k, size };\n },\n };\n}\n\n// ============================================================================\n// createLocalStorageKvStorage\n// ============================================================================\n\nexport function createLocalStorageKvStorage(prefix: string): KVStorage {\n function key(k: string): string { return `${prefix}:kv:${k}`; }\n\n return {\n read(k: string): unknown | null {\n const raw = globalThis.localStorage.getItem(key(k));\n if (raw === null) return null;\n try { return JSON.parse(raw); } catch { return null; }\n },\n write(k: string, value: unknown): void {\n globalThis.localStorage.setItem(key(k), JSON.stringify(value));\n },\n delete(k: string): void {\n globalThis.localStorage.removeItem(key(k));\n },\n listKeys(prefix2?: string): string[] {\n const fullPrefix = key(prefix2 ?? '');\n const result: string[] = [];\n for (let i = 0; i < globalThis.localStorage.length; i++) {\n const lsKey = globalThis.localStorage.key(i);\n if (lsKey !== null && lsKey.startsWith(fullPrefix)) {\n // Strip the outer prefix + ':kv:' to return the logical key\n result.push(lsKey.slice(key('').length));\n }\n }\n return result;\n },\n };\n}\n\nfunction deepMergeObjects(target: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = { ...target };\n for (const [k, v] of Object.entries(patch)) {\n if (v !== null && typeof v === 'object' && !Array.isArray(v) &&\n result[k] !== null && typeof result[k] === 'object' && !Array.isArray(result[k])) {\n result[k] = deepMergeObjects(result[k] as Record<string, unknown>, v as Record<string, unknown>);\n } else {\n result[k] = v;\n }\n }\n return result;\n}\n\nfunction applyJsonPath(obj: Record<string, unknown>, segments: string[], value: unknown): Record<string, unknown> {\n if (segments.length === 0) return obj;\n const [head, ...tail] = segments;\n if (tail.length === 0) return { ...obj, [head]: value };\n const nested = (obj[head] !== null && typeof obj[head] === 'object' && !Array.isArray(obj[head]))\n ? (obj[head] as Record<string, unknown>)\n : {};\n return { ...obj, [head]: applyJsonPath(nested, tail, value) };\n}\n\nexport function createLocalStorageJsonStorage(prefix: string): JSONStorage {\n const kv = createLocalStorageKvStorage(prefix);\n return {\n read: (key) => kv.read(key),\n get(key, jsonPath) {\n const obj = kv.read(key);\n if (obj === null) return null;\n let current: unknown = obj;\n for (const segment of jsonPath.split('.').filter(Boolean)) {\n if (current === null || typeof current !== 'object' || Array.isArray(current)) return null;\n current = (current as Record<string, unknown>)[segment] ?? null;\n }\n return current ?? null;\n },\n write: (key, value) => kv.write(key, value),\n delete: (key) => kv.delete(key),\n listKeys: (prefix2?) => kv.listKeys(prefix2),\n shallowMerge(key, patch) {\n const existing = (kv.read(key) as Record<string, unknown> | null) ?? {};\n kv.write(key, { ...existing, ...patch });\n },\n deepMerge(key, patch) {\n const existing = (kv.read(key) as Record<string, unknown> | null) ?? {};\n kv.write(key, deepMergeObjects(existing, patch));\n },\n patch(key, jsonPath, value) {\n const existing = (kv.read(key) as Record<string, unknown> | null) ?? {};\n const segments = jsonPath.split('.').filter(Boolean);\n kv.write(key, applyJsonPath(existing, segments, value));\n },\n };\n}\n\n// ============================================================================\n// createLocalStorageJournalStorageAdapter\n// All entries stored as a JSON array under a single localStorage key.\n// ============================================================================\n\nexport function createLocalStorageJournalStorageAdapter(storageKey: string): JournalStorageAdapter {\n function load(): JournalEntry[] {\n const raw = globalThis.localStorage.getItem(storageKey);\n if (!raw) return [];\n try { return JSON.parse(raw) as JournalEntry[]; } catch { return []; }\n }\n\n function save(entries: JournalEntry[]): void {\n globalThis.localStorage.setItem(storageKey, JSON.stringify(entries));\n }\n\n return {\n readAllEntries(): JournalEntry[] {\n return load();\n },\n appendEntry(entry: JournalEntry): void {\n const entries = load();\n entries.push(entry);\n save(entries);\n },\n generateId(): string {\n return globalThis.crypto.randomUUID();\n },\n };\n}\n\n// ============================================================================\n// createLocalStorageCardStorageAdapter\n// Mirrors createFsCardStorageAdapter — KV-backed, cards keyed by cardId.\n// ============================================================================\n\nexport function createLocalStorageCardStorageAdapter(prefix: string): CardStorageAdapter {\n const json = createLocalStorageJsonStorage(prefix);\n\n return {\n readIndex(): CardIndex | null {\n return json.read('_index') as CardIndex | null;\n },\n writeIndex(index: CardIndex): void {\n json.write('_index', index);\n },\n readCard(id: string): LiveCard | null {\n return json.read(id) as LiveCard | null;\n },\n writeCard(id: string, card: LiveCard): string {\n json.write(id, card);\n return computeStableJsonHashBrowser(card);\n },\n cardExists(id: string): boolean {\n return json.read(id) !== null;\n },\n defaultCardKey(cardId: string): string {\n return cardId;\n },\n };\n}\n","/**\n * card-store-browser-api.ts\n *\n * Simple browser-facing card store API.\n * Wraps createCardStore() + createLocalStorageCardStorageAdapter()\n * into a minimal read/write interface suitable for browser consumption.\n */\n\nimport { createCardStore } from '../common/board-live-cards-lib.js';\nimport type { LiveCard } from '../common/board-live-cards-lib.js';\nimport { createLocalStorageCardStorageAdapter } from './storage-localstorage-adapters.js';\n\nexport type { LiveCard };\n\nexport interface BrowserCardStoreApi {\n getCard(id: string): LiveCard | null;\n getAllCards(): LiveCard[];\n upsertCard(card: LiveCard): void;\n removeCard(id: string): void;\n}\n\n/**\n * Create a browser card store backed by localStorage.\n *\n * @param namespace - localStorage key prefix (e.g. 'my-board:cards').\n * Multiple stores can coexist by using distinct namespaces.\n */\nexport function createBrowserCardStoreApi(namespace: string): BrowserCardStoreApi {\n const adapter = createLocalStorageCardStorageAdapter(namespace);\n const store = createCardStore(adapter);\n\n return {\n getCard(id: string): LiveCard | null {\n return store.readCard(id);\n },\n getAllCards(): LiveCard[] {\n return store.readAllCards();\n },\n upsertCard(card: LiveCard): void {\n const key = adapter.defaultCardKey(card.id);\n store.writeCard(card.id, card, key);\n },\n removeCard(id: string): void {\n store.removeCard(id);\n },\n };\n}\n"]}