yaml-flow 5.2.5 → 5.2.8

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 (84) hide show
  1. package/README.md +6 -6
  2. package/board-livecards-server-runtime.js +260 -35
  3. package/browser/board-livegraph-engine.js +57 -32
  4. package/browser/board-livegraph-engine.js.map +1 -1
  5. package/browser/card-compute.js +17 -17
  6. package/browser/live-cards.js +139 -12
  7. package/browser/live-cards.schema.json +14 -9
  8. package/dist/board-livegraph-runtime/index.cjs +57 -32
  9. package/dist/board-livegraph-runtime/index.cjs.map +1 -1
  10. package/dist/board-livegraph-runtime/index.d.cts +1 -1
  11. package/dist/board-livegraph-runtime/index.d.ts +1 -1
  12. package/dist/board-livegraph-runtime/index.js +57 -32
  13. package/dist/board-livegraph-runtime/index.js.map +1 -1
  14. package/dist/card-compute/index.cjs +96 -38
  15. package/dist/card-compute/index.cjs.map +1 -1
  16. package/dist/card-compute/index.d.cts +13 -8
  17. package/dist/card-compute/index.d.ts +13 -8
  18. package/dist/card-compute/index.js +96 -38
  19. package/dist/card-compute/index.js.map +1 -1
  20. package/dist/cli/board-live-cards-cli.cjs +7200 -201
  21. package/dist/cli/board-live-cards-cli.cjs.map +1 -1
  22. package/dist/cli/board-live-cards-cli.d.cts +6 -6
  23. package/dist/cli/board-live-cards-cli.d.ts +6 -6
  24. package/dist/cli/board-live-cards-cli.js +7199 -201
  25. package/dist/cli/board-live-cards-cli.js.map +1 -1
  26. package/dist/continuous-event-graph/index.cjs +55 -30
  27. package/dist/continuous-event-graph/index.cjs.map +1 -1
  28. package/dist/continuous-event-graph/index.d.cts +2 -2
  29. package/dist/continuous-event-graph/index.d.ts +2 -2
  30. package/dist/continuous-event-graph/index.js +55 -30
  31. package/dist/continuous-event-graph/index.js.map +1 -1
  32. package/dist/index.cjs +121 -53
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +1 -1
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +121 -53
  37. package/dist/index.js.map +1 -1
  38. package/dist/{live-cards-bridge-CeNxiVcm.d.ts → live-cards-bridge-EQjytzI_.d.ts} +10 -5
  39. package/dist/{live-cards-bridge-z_rJCSbi.d.cts → live-cards-bridge-x5XREkXm.d.cts} +10 -5
  40. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +1 -1
  41. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +1 -1
  42. package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +1 -1
  43. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +1 -1
  44. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +2 -2
  45. package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +1 -1
  46. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +10 -10
  47. package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  48. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +1 -1
  49. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  50. package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +2 -2
  51. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +1 -1
  52. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +1 -1
  53. package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +1 -1
  54. package/examples/example-board/agent-instructions-cardlayout.md +1 -1
  55. package/examples/example-board/agent-instructions.md +271 -45
  56. package/examples/example-board/cards/card-concentration.json +8 -5
  57. package/examples/example-board/cards/card-market-prices.json +14 -9
  58. package/examples/example-board/cards/card-my-identity.json +28 -0
  59. package/examples/example-board/cards/card-portfolio-value.json +1 -1
  60. package/examples/example-board/cards/card-portfolio.json +1 -1
  61. package/examples/example-board/cards/card-rebalance-impact.json +65 -0
  62. package/examples/example-board/cards/card-rebalance-sim.json +57 -0
  63. package/examples/example-board/demo-chat-handler.js +2 -1
  64. package/examples/example-board/demo-server-config.json +6 -1
  65. package/examples/example-board/demo-server.js +79 -8
  66. package/examples/example-board/demo-shell-browser.html +6 -6
  67. package/examples/example-board/demo-shell-with-server.html +4 -4
  68. package/examples/example-board/demo-task-executor.js +436 -246
  69. package/examples/example-board/scripts/copilot_wrapper.bat +157 -0
  70. package/examples/example-board/scripts/copilot_wrapper_helper.ps1 +190 -0
  71. package/examples/example-board/scripts/workiq_wrapper.mjs +66 -0
  72. package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +5 -5
  73. package/examples/npm-libs/continuous-event-graph/soc-incident-board.ts +3 -3
  74. package/examples/npm-libs/event-graph/research-pipeline.ts +5 -5
  75. package/examples/npm-libs/graph-of-graphs/multi-stage-etl.ts +9 -9
  76. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  77. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +1 -1
  78. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  79. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +3 -3
  80. package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +1 -1
  81. package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +1 -1
  82. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +1 -1
  83. package/package.json +2 -2
  84. package/schema/live-cards.schema.json +14 -9
@@ -38,7 +38,7 @@ vocabulary:
38
38
 
39
39
  "requires": ["token-a", "token-b"],
40
40
 
41
- "sources": [
41
+ "source_defs": [
42
42
  { "bindTo": "raw", "outputFile": "my-card-raw.json", /* task-executor fields */ }
43
43
  ],
44
44
 
@@ -47,7 +47,7 @@ vocabulary:
47
47
  ],
48
48
 
49
49
  "provides": [
50
- { "bindTo": "published-token", "src": "computed_values.result" }
50
+ { "bindTo": "published-token", "ref": "computed_values.result" }
51
51
  ],
52
52
 
53
53
  "view": {
@@ -74,9 +74,10 @@ vocabulary:
74
74
  - `outputFile` → where the fetched result is cached
75
75
  - `customFields` → interpreted by the registered **task-executor** (examples: `mock`, `copilot`, `http`, `script`)
76
76
  - Produces: `fetched_sources.*`
77
+ - **`source_defs` is NOT a valid data namespace** — it is the config array of source definitions. Use `fetched_sources.*` to reference fetched data.
77
78
 
78
79
  ### Stage 2 — Compute
79
- - **Runs after sources.** Reads `requires.*`, `fetched_sources.*`, `card_data.*`.
80
+ - **Runs after source_defs.** Reads `requires.*`, `fetched_sources.*`, `card_data.*`.
80
81
  - Each entry: `{ "bindTo": "key", "expr": "<JSONata>" }`
81
82
  - Produces: `computed_values.*`
82
83
 
@@ -84,7 +85,7 @@ vocabulary:
84
85
  - **Resolved last.** Can reference all four namespaces:
85
86
  `requires.*`, `fetched_sources.*`, `card_data.*`, `computed_values.*`
86
87
  - `view.elements[].data.bind` paths are resolved here.
87
- - `provides[].src` paths are resolved here and published to the graph.
88
+ - `provides[].ref` paths are resolved here and published to the graph.
88
89
 
89
90
  ---
90
91
 
@@ -93,9 +94,9 @@ vocabulary:
93
94
  ```json
94
95
  // Publishing a token:
95
96
  "provides": [
96
- { "bindTo": "orders", "src": "fetched_sources.raw" },
97
- { "bindTo": "regionTotals","src": "computed_values.byRegion" },
98
- { "bindTo": "my-card", "src": "card_data" }
97
+ { "bindTo": "orders", "ref": "fetched_sources.raw" },
98
+ { "bindTo": "regionTotals","ref": "computed_values.byRegion" },
99
+ { "bindTo": "my-card", "ref": "card_data" }
99
100
  ]
100
101
 
101
102
  // Consuming tokens:
@@ -124,7 +125,7 @@ Every card is a live entity. Any of these events triggers automatic recompute of
124
125
 
125
126
  ## Task Completion
126
127
 
127
- Task completion is determined by one rule: **a card is complete when all non-optional `sources[]` have been fetched**.
128
+ Task completion is determined by one rule: **a card is complete when all non-optional `source_defs[]` have been fetched**.
128
129
 
129
130
  If completion requires a judgment call — e.g. "is the data sufficient?", "does this narrative indicate done?" — model it as data using the standard source → compute → provides chain (see LLM source pattern below). The card is complete when that source has been fetched.
130
131
 
@@ -272,14 +273,131 @@ Alert/callout display element.
272
273
  ### `custom`
273
274
  Custom element kind — behaviour defined by the host application.
274
275
 
276
+ ### `ref`
277
+ Indirection element that resolves its rendered kind at runtime from any namespace variable. The card author declares the data source (`bind`) and where to look up the view definition (`viewBind`). The resolved view definition can come from `card_data` (user-selectable UI), `fetched_sources` (LLM-suggested), `requires` (cross-card), or `computed_values`.
278
+
279
+ **Resolved value shape:**
280
+ - A **string** — used directly as the element kind: `"table"`, `"chart"`, `"editable-table"`, etc.
281
+ - An **object** — `{ kind, label, data: { columns, chartType, chartOptions, writeTo, ... } }` — merged with the static elemDef (static fields always win, so card authors can fence LLM suggestions).
282
+ - `null`/`undefined` — falls back to `fallbackKind` or shape inference (`array→table`, `string→text`, `object→narrative`).
283
+
284
+ **Allowed kind values** (whitelist; unknown values fall to `table`):
285
+ `table`, `editable-table`, `chart`, `metric`, `list`, `badge`, `text`, `narrative`, `markdown`, `form`, `filter`, `todo`, `alert`
286
+
287
+ ```json
288
+ { "kind": "ref",
289
+ "label": "Trade Data",
290
+ "data": {
291
+ "bind": "fetched_sources.rebalance.proposed_trades",
292
+ "viewBind": "card_data.display_mode",
293
+ "fallbackKind": "table"
294
+ }
295
+ }
296
+ ```
297
+
298
+ **Pattern 1 — User-selectable view (selector pattern)**
299
+
300
+ Pair a `form` element with a `ref` element. The form writes the chosen kind into `card_data`; the `ref` reads it back. The user can switch between table, chart, etc. with no card JSON changes:
301
+
302
+ ```json
303
+ { "kind": "form",
304
+ "data": {
305
+ "writeTo": "card_data.display_mode",
306
+ "fields": {
307
+ "type": "object",
308
+ "properties": {
309
+ "kind": { "type": "string", "title": "Display as",
310
+ "enum": ["table", "chart", "editable-table"] }
311
+ }
312
+ }
313
+ }
314
+ },
315
+ { "kind": "ref",
316
+ "data": {
317
+ "bind": "fetched_sources.rebalance.proposed_trades",
318
+ "viewBind": "card_data.display_mode",
319
+ "fallbackKind": "table"
320
+ }
321
+ }
322
+ ```
323
+
324
+ Note: `card_data.display_mode` can be a plain string (`"chart"`) or an object (`{ "kind": "chart", ... }`). Both are handled.
325
+
326
+ **Pattern 2 — LLM-suggested view (source carries `_view`)**
327
+
328
+ The LLM source response includes a `_view` key alongside the data. The `ref` element reads `_view` from the source namespace. The LLM decides chart vs table vs editable-table at runtime based on what the data looks like:
329
+
330
+ ```json
331
+ // LLM returns:
332
+ { "proposed_trades": [ {"ticker": "AAPL", "trade_value": 1084} ],
333
+ "_view": { "kind": "chart", "data": { "chartType": "bar", "columns": ["ticker","trade_value"] } }
334
+ }
335
+
336
+ // Card element:
337
+ { "kind": "ref",
338
+ "data": {
339
+ "bind": "fetched_sources.rebalance.proposed_trades",
340
+ "viewBind": "fetched_sources.rebalance._view",
341
+ "fallbackKind": "table"
342
+ }
343
+ }
344
+ ```
345
+
346
+ Add this to the `copilot` prompt template to instruct the LLM:
347
+ ```
348
+ Return JSON with shape:
349
+ {
350
+ "<data_key>": [ ... ],
351
+ "_view": {
352
+ "kind": "editable-table" | "table" | "chart",
353
+ "data": {
354
+ // for editable-table: { "writeTo": "card_data.<key>", "columns": ["field1","field2"] }
355
+ // for chart: { "chartType": "bar" | "line" | "pie", "columns": ["labelField","valueField"] }
356
+ // for table: { "columns": ["field1","field2"] }
357
+ }
358
+ }
359
+ }
360
+ Choose kind based on data shape and user intent.
361
+ ```
362
+
363
+ The `_view` key is ignored by any card that reads only `<data_key>` — it is inert to downstream cards.
364
+
365
+ **Pattern 3 — Cross-card view hint (view definition flows as a token)**
366
+
367
+ Card A provides both its data and the view hint as separate tokens. Card B requires both and uses `ref` to render whatever Card A’s LLM suggested:
368
+
369
+ ```json
370
+ // Card A provides:
371
+ "provides": [
372
+ { "bindTo": "trade_data", "ref": "fetched_sources.rebalance.proposed_trades" },
373
+ { "bindTo": "trade_view", "ref": "fetched_sources.rebalance._view" }
374
+ ]
375
+
376
+ // Card B requires and renders:
377
+ "requires": ["trade_data", "trade_view"],
378
+ "view": {
379
+ "elements": [
380
+ { "kind": "ref",
381
+ "data": {
382
+ "bind": "requires.trade_data",
383
+ "viewBind": "requires.trade_view",
384
+ "fallbackKind": "table"
385
+ }
386
+ }
387
+ ]
388
+ }
389
+ ```
390
+
391
+ Card B’s author does not need to know what kind Card A’s LLM will choose. The view decision propagates through the token graph.
392
+
275
393
  ---
276
394
 
277
395
  ## Common Card Patterns
278
396
 
279
397
  ### Root source card
280
398
  ```
281
- sources[mock/http/script] → fetched_sources.raw
282
- provides: [{ bindTo: "orders", src: "fetched_sources.raw" }]
399
+ source_defs[mock/http/script] → fetched_sources.raw
400
+ provides: [{ bindTo: "orders", ref: "fetched_sources.raw" }]
283
401
  view: table showing the raw data
284
402
  ```
285
403
 
@@ -287,7 +405,7 @@ view: table showing the raw data
287
405
  ```
288
406
  requires: ["orders"]
289
407
  compute: JSONata aggregation → computed_values.result
290
- provides: [{ bindTo: "regionTotals", src: "computed_values.result" }]
408
+ provides: [{ bindTo: "regionTotals", ref: "computed_values.result" }]
291
409
  view: table or metric
292
410
  ```
293
411
 
@@ -316,14 +434,42 @@ card-child requires "card-ex-form"
316
434
  ### LLM verdict card (completion gating via source)
317
435
  ```
318
436
  requires: ["some-data"]
319
- sources: [{ bindTo: "verdict", outputFile: "...", copilot: { prompt_template: "..." } }]
437
+ source_defs: [{ bindTo: "verdict", outputFile: "...", copilot: { prompt_template: "..." } }]
320
438
  compute: [{ bindTo: "isReady", expr: "fetched_sources.verdict.isTaskCompleted" }]
321
- provides: [{ bindTo: "readiness-verdict", src: "fetched_sources.verdict" }]
439
+ provides: [{ bindTo: "readiness-verdict", ref: "fetched_sources.verdict" }]
322
440
  view: badge(computed_values.isReady, colorMap) + text(fetched_sources.verdict.reason)
323
441
  ```
324
442
  The task executor calls the LLM, writes `{ isTaskCompleted: bool, reason: string }` to `--out`.
325
443
  The card is complete when the source is fetched. Downstream cards `requires: ["readiness-verdict"]`.
326
444
 
445
+ ### User-selectable view (selector + ref)
446
+ ```
447
+ source_defs: fetch data into fetched_sources.raw
448
+ view:
449
+ form writeTo:card_data.display_mode (enum: table, chart, editable-table)
450
+ ref bind:fetched_sources.raw viewBind:card_data.display_mode fallbackKind:table
451
+ ```
452
+ User picks the display kind from a dropdown; the `ref` element re-renders immediately.
453
+ No card JSON changes needed — the form + ref pair handles all switching.
454
+
455
+ ### LLM-suggested view
456
+ ```
457
+ source_defs: copilot source returns { data: [...], _view: { kind, data: {...} } }
458
+ view:
459
+ ref bind:fetched_sources.raw.data viewBind:fetched_sources.raw._view fallbackKind:table
460
+ ```
461
+ LLM decides chart vs table vs editable-table at runtime. `_view` is ignored by downstream
462
+ cards that only read `data`. Card author can fence with static `columns` in the ref element data.
463
+
464
+ ### Cross-card view propagation
465
+ ```
466
+ Card A provides: trade_data (fetched_sources.rebalance.proposed_trades)
467
+ + trade_view (fetched_sources.rebalance._view)
468
+ Card B requires: trade_data, trade_view
469
+ view: ref bind:requires.trade_data viewBind:requires.trade_view
470
+ ```
471
+ Card B author writes no view knowledge — Card A's LLM drives the rendering decision for both cards.
472
+
327
473
  ---
328
474
 
329
475
  ## Card Design Principles & Layout
@@ -343,21 +489,86 @@ Common customField conventions (demo executor supports `mock` and `copilot`):
343
489
  | Field | Meaning |
344
490
  |---|---|
345
491
  | `"mock": "key"` | Reads from `mock.db` by key — local dev only |
346
- | `"copilot": { "prompt_template": "...", "args": {} }` | LLM call; `{{key}}` interpolated from `_requires`, `_sourcesData`, `_computed_values`, then explicit `args` |
492
+ | `"copilot": { "prompt_template": "...", "args": {} }` | LLM call; `{{key}}` interpolated from `_projections` (named data projections declared in `projections`), then explicit `args` |
347
493
  | `"prompt_template": "..."` | Shorthand top-level LLM call (equivalent to `copilot.prompt_template`) |
348
494
  | `"http": { "url": "...", "method": "GET" }` | HTTP/REST — implement in your executor |
349
495
  | `"graphapi": { "query": "..." }` | Microsoft Graph API — implement in your executor |
350
496
  | `"script": { "path": "...", "args": {} }` | Local script — implement in your executor |
351
497
  | `"teams"`, `"mail"`, `"incidentdb"` | Any domain integration — define in your executor |
352
498
 
353
- Sources can reference upstream data in their customFields (e.g. `"url": "https://api.example.com/{{orderId}}"`) the executor receives `_requires`, `_sourcesData`, `_computed_values` to resolve such references. See [Task Executor Protocol](#task-executor-protocol) for the full `--in` payload shape.
499
+ Sources can access upstream data via the `projections` property — named JSONata projections from `card_data` or `requires` that the engine evaluates before invoking the executor. The executor receives `_projections` containing the resolved values. See [source_defs projections](#source_defs-projections) and [Task Executor Protocol](#task-executor-protocol) for details.
500
+
501
+ ### source_defs projections
502
+
503
+ The optional `projections` map lets a source definition declare which upstream data it needs. Each key maps to a JSONata expression rooted at `card_data` or `requires`. The engine evaluates these before invoking the executor and attaches the results as `_projections` on the source payload.
504
+
505
+ ```json
506
+ "source_defs": [
507
+ {
508
+ "bindTo": "quotes",
509
+ "outputFile": "quotes.json",
510
+ "projections": {
511
+ "holdings": "requires.holdings",
512
+ "topHoldings": "requires.holdings[weight > 0.05]",
513
+ "threshold": "card_data.threshold"
514
+ },
515
+ "chartApi": {
516
+ "tickersFrom": "holdings.ticker"
517
+ }
518
+ }
519
+ ]
520
+ ```
521
+
522
+ **Rules:**
523
+ - Only `card_data` and `requires` are valid namespaces in `projections` expressions
524
+ - `fetched_sources`, `computed_values`, and `source_defs` are **forbidden** in projections
525
+ - Full JSONata syntax is supported (same as `compute[].expr`)
526
+ - Sources without `projections` receive `_projections: {}` — executor must handle empty projections gracefully
527
+ - `tickersFrom: "refKey.fieldName"` reads from `_projections[refKey]` — the `projections` key must exist
354
528
 
355
529
  ### Optional source field
356
530
  - `optionalForCompletionGating: true` — marks this source as optional for default task-completion gating. If set, the card can complete even if this source hasn't been fetched yet.
357
531
 
532
+ ### Discovering supported source kinds
533
+
534
+ Rather than guessing which source `customFields` the registered executor supports, query it directly:
535
+
536
+ ```bash
537
+ node board-live-cards-cli.js describe-task-executor-capabilities --rg <boardDir>
538
+ ```
539
+
540
+ This invokes the executor's `describe-capabilities` subcommand and prints its capabilities JSON to stdout. The output includes:
541
+ - **`sourceKinds`** — every source kind the executor handles (e.g. `mock`, `copilot`, `http`, `chartApi`), each with:
542
+ - `description` — what the kind does
543
+ - `inputSchema` — the exact `customFields` the executor expects on the source entry
544
+ - `outputShape` — the shape of the JSON written to `--out`
545
+ - `example` — sample input/output pair
546
+ - **`extraSchema`** — fields available via `--extra` (board topology context)
547
+ - **`subcommands`** — supported subcommands (typically `run-source-fetch` + `describe-capabilities`)
548
+
549
+ **Use this before authoring a card** to confirm the executor handles your intended source kind and to discover the correct field names and types. If the kind is missing from the output, the executor needs extending before the card will work.
550
+
551
+ Example output (excerpt):
552
+ ```json
553
+ {
554
+ "sourceKinds": {
555
+ "mock": {
556
+ "description": "Look up a key in a hardcoded MOCK_DB dictionary.",
557
+ "inputSchema": { "mock": { "type": "string", "required": true } }
558
+ },
559
+ "copilot": {
560
+ "description": "Invoke GitHub Copilot CLI with an interpolated prompt template.",
561
+ "inputSchema": {
562
+ "copilot": { "type": "object", "properties": { "prompt_template": { "type": "string" } } }
563
+ }
564
+ }
565
+ }
566
+ }
567
+ ```
568
+
358
569
  ## LLM Calls — Use a Source
359
570
 
360
- **All LLM calls belong in sources[], handled by the task executor.** There is one mechanism for external calls — sources.
571
+ **All LLM calls belong in source_defs[], handled by the task executor.** There is one mechanism for external calls — source_defs.
361
572
 
362
573
  To incorporate LLM reasoning into a card:
363
574
 
@@ -367,7 +578,7 @@ To incorporate LLM reasoning into a card:
367
578
  4. The card provides those tokens downstream like any other.
368
579
 
369
580
  ```json
370
- "sources": [
581
+ "source_defs": [
371
582
  {
372
583
  "bindTo": "verdict",
373
584
  "outputFile": "my-card-verdict.json",
@@ -388,13 +599,13 @@ If the LLM needs computed values (which compute first), chain two cards: Card A
388
599
 
389
600
  ### CLI (recommended for authoring)
390
601
  ```bash
391
- # Validate all cards in example-board
392
- npm run validate:cards -- "examples/example-board/cards/*.json"
602
+ # Validate a single card
603
+ node board-live-cards-cli.js validate-card --card cards/my-card.json
393
604
 
394
- # Validate any glob pattern
395
- npm run validate:cards -- "path/to/cards/*.json"
605
+ # Validate all cards matching a glob
606
+ node board-live-cards-cli.js validate-card --card-glob "cards/*.json"
396
607
  ```
397
- Uses `validateLiveCardDefinition` structural + schema checks, reports all errors per file.
608
+ Checks JSON Schema structure, JSONata expression syntax in `compute[].expr`, and `provides[].ref` namespace validity. Reports per-file OK/FAIL with detailed errors. Exits with code 1 if any card fails.
398
609
 
399
610
  ### Programmatic
400
611
  ```typescript
@@ -424,24 +635,25 @@ When in doubt about allowed fields, consult:
424
635
  - `id` required, non-empty string
425
636
  - `card_data` required, must be an object
426
637
  - `requires` must be array of strings (if present)
427
- - `provides` must be array of `{ bindTo: string, src: string }` (if present)
428
- - `compute[]` each entry must have `bindTo` + `expr` strings
429
- - `sources[]` each entry must have `bindTo` + `outputFile` strings; both must be unique across the array
638
+ - `provides` must be array of `{ bindTo: string, ref: string }` (if present)
639
+ - `provides[].ref` must start with a valid namespace: `card_data`, `requires`, `fetched_sources`, or `computed_values`
640
+ - `compute[]` each entry must have `bindTo` + `expr` strings; `expr` must be valid JSONata
641
+ - `source_defs[]` each entry must have `bindTo` + `outputFile` strings; both must be unique across the array
430
642
  - `view.elements` required, non-empty; each element must have a valid `kind`
431
643
  - Top-level unknown keys are flagged as errors
432
- - Valid element `kind` values: `metric`, `table`, `editable-table`, `chart`, `form`, `filter`, `list`, `notes`, `todo`, `alert`, `narrative`, `badge`, `text`, `markdown`, `custom`
644
+ - Valid element `kind` values: `metric`, `table`, `editable-table`, `chart`, `form`, `filter`, `list`, `notes`, `todo`, `alert`, `narrative`, `badge`, `text`, `markdown`, `ref`, `custom`
433
645
 
434
646
  ---
435
647
 
436
648
  ## mock.db
437
649
 
438
- A JSON file at the board root keyed by mock name. Used by `"mock": "key"` sources. Replace with real task-executor integrations in production.
650
+ A JSON file at the board root keyed by mock name. Used by `"mock": "key"` source_defs. Replace with real task-executor integrations in production.
439
651
 
440
652
  ---
441
653
 
442
654
  ## Task Executor Protocol
443
655
 
444
- The task executor is a **card-source-driven** component — its behaviour is determined entirely by the `customFields` defined on each card's `sources[]` entries. One executor is registered for the whole board, but it must know how to handle every source kind (`mock`, `copilot`, `http`, `graphapi`, etc.) used by any card on the board. The executor is the only handler where the card's source definition directly drives what the handler needs to do. It is registered once per board:
656
+ The task executor is a **card-source-driven** component — its behaviour is determined entirely by the `customFields` defined on each card's `source_defs[]` entries. One executor is registered for the whole board, but it must know how to handle every source kind (`mock`, `copilot`, `http`, `graphapi`, etc.) used by any card on the board. The executor is the only handler where the card's source definition directly drives what the handler needs to do. It is registered once per board:
445
657
 
446
658
  ```bash
447
659
  node board-live-cards-cli.js init ./my-board --task-executor ./my-executor.js
@@ -454,7 +666,7 @@ node board-live-cards-cli.js init ./my-board --task-executor ./my-executor.js
454
666
  node <executor.js> run-source-fetch --in <source.json> --out <result.json> [--err <error.txt>]
455
667
  ```
456
668
 
457
- - **`--in`** — path to a JSON file containing a single source object (one entry from `sources[]`), enriched by the runtime with extra context fields:
669
+ - **`--in`** — path to a JSON file containing a single source object (one entry from `source_defs[]`), enriched by the runtime with extra context fields:
458
670
 
459
671
  ```json
460
672
  {
@@ -462,16 +674,12 @@ node <executor.js> run-source-fetch --in <source.json> --out <result.json> [--er
462
674
  "outputFile": "my-card-raw.json",
463
675
  "cwd": "/absolute/path/to/board",
464
676
  "boardDir": "/absolute/path/to/board",
465
- "_requires": { "token-a": { ... }, "token-b": { ... } },
466
- "_sourcesData": { "previously-fetched-source-key": { ... } },
467
- "_computed_values": { "result": "..." },
677
+ "_projections": { "holdings": [ ... ], "threshold": 0.05 },
468
678
  /* ...any other customFields from the card source definition */
469
679
  }
470
680
  ```
471
681
 
472
- - `_requires` — resolved values for all card `requires` tokens
473
- - `_sourcesData` — already-fetched sources for this card (earlier in sources[] order)
474
- - `_computed_values` — current computed_values for the card
682
+ - `_projections` — resolved values for all entries declared in the source's `projections` map (evaluated from `card_data`/`requires` before executor invocation). Empty object `{}` if `projections` was not declared.
475
683
 
476
684
  - **`--out`** — path to write the fetched value as raw JSON (any shape; stored under `fetched_sources.<bindTo>`)
477
685
  - **`--err`** — optional path to write a plain-text error message on failure
@@ -481,7 +689,7 @@ node <executor.js> run-source-fetch --in <source.json> --out <result.json> [--er
481
689
  | `customField` | Meaning |
482
690
  |---|---|
483
691
  | `"mock": "key"` | Lookup `key` in `mock.db` — local dev |
484
- | `"copilot": { "prompt_template": "..." }` | LLM call; supports `{{key}}` interpolation against `_requires`, `_sourcesData`, `_computed_values` |
692
+ | `"copilot": { "prompt_template": "..." }` | LLM call; supports `{{key}}` interpolation against `_projections` |
485
693
  | `"http": { "url": "...", "method": "GET" }` | HTTP/REST fetch |
486
694
  | `"graphapi": { "query": "..." }` | Microsoft Graph API query |
487
695
  | `"script": { "path": "...", "args": {} }` | Run a local script |
@@ -502,27 +710,45 @@ node board-live-cards-cli.js probe-source \
502
710
  --card cards/card-market-prices.json \
503
711
  --source-idx 0 \
504
712
  --rg <boardRuntimeDir> \
505
- --mock-requires '{"holdings":[{"ticker":"AAPL","quantity":10},{"ticker":"MSFT","quantity":5}]}'
713
+ --mock-projections '{"holdings":[{"ticker":"AAPL","quantity":10},{"ticker":"MSFT","quantity":5}]}'
506
714
  ```
507
715
 
508
716
  | Flag | Required | Description |
509
717
  |---|---|---|
510
718
  | `--card <card.json>` | yes | Path to the card file to probe |
511
- | `--source-idx <n>` | no (default 0) | 0-based index into `sources[]` |
719
+ | `--source-idx <n>` | no (default 0) | 0-based index into `source_defs[]` |
512
720
  | `--source-bind <name>` | no | Select source by `bindTo` name instead of index |
513
- | `--mock-requires <json>` | no | JSON string (or `@file.json`) providing the `_requires` token values the source needs. If omitted, `_requires` is `{}`. |
721
+ | `--mock-projections <json>` | no | JSON string (or `@file.json`) providing the `_projections` values the source needs. If omitted, `_projections` is `{}`. |
514
722
  | `--rg <boardDir>` | no | Board runtime directory used to locate `.task-executor`. Defaults to the card file's directory. |
515
723
  | `--out <result.json>` | no | Write the raw fetch result to this path |
516
724
 
517
725
  **Output:** the command prints a human-readable report ending with a machine-readable `[probe-source:result]` JSON line. Exit `0` = `PROBE_PASS`, exit `1` = `PROBE_FAIL`.
518
726
 
519
- **`--mock-requires` is the agent's responsibility.** Craft the minimal payload that exercises the source — for example, if the card `requires: ["holdings"]` and the source uses `tickersFrom: "holdings.ticker"`, supply `{"holdings":[{"ticker":"AAPL","quantity":1}]}`.
727
+ **`--mock-projections` is the agent's responsibility.** Craft the minimal payload that exercises the source — for example, if the card declares `"projections": { "holdings": "requires.holdings" }` and the source uses `tickersFrom: "holdings.ticker"`, supply `{"holdings":[{"ticker":"AAPL","quantity":1}]}`.
520
728
 
521
729
  **Workflow for agents authoring a new card:**
522
- 1. Author the card JSON with `sources[]`.
523
- 2. For each source, run `probe-source` with representative `--mock-requires` data.
524
- 3. If `PROBE_PASS` → proceed with `add-cards` or `upsert-card`.
525
- 4. If `PROBE_FAIL` → inspect the error, fix the source definition or executor, retry.
730
+ 1. **Discover available source kinds** — run `describe-task-executor-capabilities` to see exactly which source kinds the registered executor supports, their required `customFields`, and expected output shapes. Only use source kinds present in this output.
731
+ ```bash
732
+ node board-live-cards-cli.js describe-task-executor-capabilities --rg <boardDir>
733
+ ```
734
+ 2. **Author the card JSON** with `source_defs[]`, `compute[]`, `provides[]`, and `view` using only the source kinds confirmed in step 1.
735
+ 3. **Validate the card structure** — run `validate-card` to catch schema errors, invalid JSONata expressions, and namespace violations before attempting any live fetch.
736
+ ```bash
737
+ node board-live-cards-cli.js validate-card --card cards/my-card.json
738
+ ```
739
+ Fix any reported errors and re-run until validation passes.
740
+ 4. **Probe each source** — run `probe-source` with representative `--mock-projections` data to confirm the executor can successfully fetch each source.
741
+ 5. If `PROBE_PASS` → proceed with `upsert-card`.
742
+ 6. If `PROBE_FAIL` → inspect the error, fix the source definition or executor, retry from step 3.
743
+
744
+ **Workflow for agents editing an existing card:**
745
+ 1. Make the change to the card JSON.
746
+ 2. **Validate immediately** — run `validate-card` after every edit.
747
+ ```bash
748
+ node board-live-cards-cli.js validate-card --card cards/my-card.json
749
+ ```
750
+ 3. If validation fails, fix the reported errors and re-run — repeat until the card is clean.
751
+ 4. If the change touches a `source_defs[]` entry, also run `probe-source` to confirm the source still fetches correctly before deploying.
526
752
 
527
753
  ---
528
754
 
@@ -532,7 +758,7 @@ node board-live-cards-cli.js probe-source \
532
758
 
533
759
  ## Chat Handler Protocol
534
760
 
535
- The chat handler is a **universal LLM component** — it does not depend on card sources, card model fields, or board state. Its sole job is: read the conversation, call the LLM, write the response.
761
+ The chat handler is a **universal LLM component** — it does not depend on card source_defs, card model fields, or board state. Its sole job is: read the conversation, call the LLM, write the response.
536
762
 
537
763
  Enable chat on a card by setting `"chat": true` in the card's `view.features`:
538
764
  ```json
@@ -6,10 +6,13 @@
6
6
  "desc": "AI analysis of portfolio mix and P&L. Publishes structured insights as tokens for downstream risk and action cards."
7
7
  },
8
8
  "requires": ["positions"],
9
- "sources": [
9
+ "source_defs": [
10
10
  {
11
11
  "bindTo": "analysis",
12
12
  "outputFile": "card-concentration-analysis.json",
13
+ "projections": {
14
+ "positions": "requires.positions"
15
+ },
13
16
  "copilot": {
14
17
  "prompt_template": "You are a concise equity analyst. Use web search to get today's news and upcoming events for each ticker before answering.\n\nPortfolio positions:\n{{positions}}\n\nFields: ticker, quantity, cost_basis ($), price ($), value ($), gain_$ (unrealised P&L), gain_% (unrealised return), chg_$ (today's position P&L $), chg_pct (today's % move).\n\nIMPORTANT OUTPUT RULES:\n- Return ONLY a valid JSON object. No markdown code fences, no preamble, no trailing text.\n- Start your response with { and end with }.\n- No emojis anywhere.\n- Each value is a Markdown string using - bullet points. No sub-headings inside values.\n- Maximum 40 words per section.\n\n{\n \"mix\": \"- <dominant holding ticker, % of total, and whether that is a risk>\\n- <sector clustering in one line>\",\n \"pnl\": \"- Best: <ticker> +X% (+$Y)\\n- Worst: <ticker> +X% (+$Y)\",\n \"risks\": \"- <TICKER>: <one specific current risk with event name or date>\\n- <TICKER>: ...\\n- (one bullet per position from today's news)\",\n \"action\": \"- <one sentence: specific action and reason grounded in today's market>\"\n}"
15
18
  }
@@ -22,10 +25,10 @@
22
25
  { "bindTo": "action", "expr": "fetched_sources.analysis.action" }
23
26
  ],
24
27
  "provides": [
25
- { "bindTo": "portfolio_mix", "src": "computed_values.mix" },
26
- { "bindTo": "portfolio_pnl", "src": "computed_values.pnl" },
27
- { "bindTo": "portfolio_risks", "src": "computed_values.risks" },
28
- { "bindTo": "portfolio_action", "src": "computed_values.action" }
28
+ { "bindTo": "portfolio_mix", "ref": "computed_values.mix" },
29
+ { "bindTo": "portfolio_pnl", "ref": "computed_values.pnl" },
30
+ { "bindTo": "portfolio_risks", "ref": "computed_values.risks" },
31
+ { "bindTo": "portfolio_action", "ref": "computed_values.action" }
29
32
  ],
30
33
  "view": {
31
34
  "elements": [
@@ -6,28 +6,33 @@
6
6
  "desc": "Fetches live prices for portfolio tickers. Publishes enriched quote data for downstream portfolio value calculations."
7
7
  },
8
8
  "requires": ["holdings"],
9
- "sources": [
9
+ "source_defs": [
10
10
  {
11
11
  "bindTo": "quotes",
12
12
  "outputFile": "market-prices-quotes.json",
13
- "chartApi": {
14
- "url": "https://query1.finance.yahoo.com/v8/finance/chart/{{ticker}}?interval=1d&range=1d",
13
+ "projections": {
14
+ "url_list": "requires.holdings.ticker.('https://query1.finance.yahoo.com/v8/finance/chart/' & $ & '?interval=1d&range=1d')"
15
+ },
16
+ "url-list": {
15
17
  "headers": {
16
18
  "User-Agent": "Mozilla/5.0 (compatible; portfolio-tracker-demo/1.0)"
17
- }
18
- },
19
- "tickersFrom": "holdings.ticker",
20
- "mock": "quotes"
19
+ },
20
+ "cacheTimeout": 3600
21
+ }
21
22
  }
22
23
  ],
23
24
  "compute": [
25
+ {
26
+ "bindTo": "normalizedQuotes",
27
+ "expr": "{ \"quoteResponse\": { \"result\": $map(fetched_sources.quotes, function($r) { $r.chart.result[0].meta.{ \"symbol\": symbol, \"shortName\": longName, \"regularMarketPrice\": regularMarketPrice, \"regularMarketChange\": regularMarketChange, \"regularMarketChangePercent\": regularMarketChangePercent } }), \"error\": null } }"
28
+ },
24
29
  {
25
30
  "bindTo": "prices",
26
- "expr": "$map(fetched_sources.quotes.quoteResponse.result, function($q) { {\"ticker\": $q.symbol, \"name\": $q.shortName, \"price\": $round($q.regularMarketPrice, 2), \"change\": $round($q.regularMarketChange, 2), \"chg_pct\": $round($q.regularMarketChangePercent, 2)} })"
31
+ "expr": "$map(computed_values.normalizedQuotes.quoteResponse.result, function($q) { {\"ticker\": $q.symbol, \"name\": $q.shortName, \"price\": $round($q.regularMarketPrice, 2), \"change\": $round($q.regularMarketChange, 2), \"chg_pct\": $round($q.regularMarketChangePercent, 2)} })"
27
32
  }
28
33
  ],
29
34
  "provides": [
30
- { "bindTo": "quotes", "src": "fetched_sources.quotes" }
35
+ { "bindTo": "quotes", "ref": "computed_values.normalizedQuotes" }
31
36
  ],
32
37
  "view": {
33
38
  "elements": [
@@ -0,0 +1,28 @@
1
+ {
2
+ "id": "card-my-identity",
3
+ "meta": {
4
+ "title": "My Identity",
5
+ "tags": ["m365", "workiq"],
6
+ "desc": "Displays your M365 display name via WorkIQ (Microsoft 365 Copilot)."
7
+ },
8
+ "source_defs": [
9
+ {
10
+ "bindTo": "identity",
11
+ "outputFile": "card-my-identity.txt",
12
+ "workiq": {
13
+ "query_template": "what is my full display name. No Fluff. No Commentary. No Descriptions. Just Answer the Question"
14
+ }
15
+ }
16
+ ],
17
+ "view": {
18
+ "elements": [
19
+ { "kind": "markdown", "data": { "bind": "fetched_sources.identity" } }
20
+ ],
21
+ "layout": {
22
+ "board": { "col": 4, "order": 10 },
23
+ "canvas": { "x": 50, "y": 450, "w": 420, "h": 260 }
24
+ },
25
+ "features": { "refresh": true }
26
+ },
27
+ "card_data": {}
28
+ }
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "requires": ["holdings", "quotes"],
9
9
  "provides": [
10
- { "bindTo": "positions", "src": "computed_values.positions" }
10
+ { "bindTo": "positions", "ref": "computed_values.positions" }
11
11
  ],
12
12
  "compute": [
13
13
  {
@@ -6,7 +6,7 @@
6
6
  "desc": "Manage your stock holdings — edit tickers and quantities inline, add or delete rows. Changes propagate downstream immediately."
7
7
  },
8
8
  "provides": [
9
- { "bindTo": "holdings", "src": "card_data.holdings" }
9
+ { "bindTo": "holdings", "ref": "card_data.holdings" }
10
10
  ],
11
11
  "compute": [],
12
12
  "view": {