@particle-academy/fancy-flow 0.58.0 → 0.63.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 (63) hide show
  1. package/dist/{HumanPrompt-Bw09OyS3.d.ts → HumanPrompt-BKThiyjB.d.ts} +1 -1
  2. package/dist/{HumanPrompt-DV5-xnDa.d.cts → HumanPrompt-Bpp3JiVH.d.cts} +1 -1
  3. package/dist/{chunk-M7M6GLFV.js → chunk-5H54OTKT.js} +3 -3
  4. package/dist/{chunk-M7M6GLFV.js.map → chunk-5H54OTKT.js.map} +1 -1
  5. package/dist/{chunk-MX4SJDNI.js → chunk-EO6444T2.js} +4 -4
  6. package/dist/{chunk-MX4SJDNI.js.map → chunk-EO6444T2.js.map} +1 -1
  7. package/dist/{chunk-HBE4LVPX.js → chunk-JF6WCRBU.js} +9 -7
  8. package/dist/chunk-JF6WCRBU.js.map +1 -0
  9. package/dist/{chunk-LGLOBK6Q.js → chunk-MBVX4ZRB.js} +3 -3
  10. package/dist/{chunk-LGLOBK6Q.js.map → chunk-MBVX4ZRB.js.map} +1 -1
  11. package/dist/{chunk-FGYG5JVW.js → chunk-MEIDPBKE.js} +3 -3
  12. package/dist/{chunk-FGYG5JVW.js.map → chunk-MEIDPBKE.js.map} +1 -1
  13. package/dist/{chunk-A3TLZS5B.js → chunk-RIAFHQT5.js} +3 -3
  14. package/dist/{chunk-A3TLZS5B.js.map → chunk-RIAFHQT5.js.map} +1 -1
  15. package/dist/{chunk-BHM5HGXH.js → chunk-UM4C46AF.js} +38 -14
  16. package/dist/chunk-UM4C46AF.js.map +1 -0
  17. package/dist/connectors.d.cts +1 -1
  18. package/dist/connectors.d.ts +1 -1
  19. package/dist/durable.cjs +7 -5
  20. package/dist/durable.cjs.map +1 -1
  21. package/dist/durable.js +2 -2
  22. package/dist/engine.cjs +138 -29
  23. package/dist/engine.cjs.map +1 -1
  24. package/dist/engine.d.cts +3 -2
  25. package/dist/engine.d.ts +3 -2
  26. package/dist/engine.js +6 -5
  27. package/dist/engine.js.map +1 -1
  28. package/dist/expr-BfXDrs4H.d.cts +149 -0
  29. package/dist/expr-BfXDrs4H.d.ts +149 -0
  30. package/dist/fields/react-fancy.d.cts +2 -2
  31. package/dist/fields/react-fancy.d.ts +2 -2
  32. package/dist/index.cjs +44 -16
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +7 -84
  35. package/dist/index.d.ts +7 -84
  36. package/dist/index.js +12 -12
  37. package/dist/index.js.map +1 -1
  38. package/dist/registry/index.d.cts +3 -3
  39. package/dist/registry/index.d.ts +3 -3
  40. package/dist/{registry-1qhmRtvC.d.cts → registry-CntAdUcY.d.cts} +1 -1
  41. package/dist/{registry-D2qocEsY.d.ts → registry-DYoV-MQi.d.ts} +1 -1
  42. package/dist/registry.cjs +7 -5
  43. package/dist/registry.cjs.map +1 -1
  44. package/dist/registry.js +2 -2
  45. package/dist/runtime.cjs +7 -5
  46. package/dist/runtime.cjs.map +1 -1
  47. package/dist/runtime.js +3 -3
  48. package/dist/schema.cjs +7 -5
  49. package/dist/schema.cjs.map +1 -1
  50. package/dist/schema.js +2 -2
  51. package/dist/screens.cjs +7 -5
  52. package/dist/screens.cjs.map +1 -1
  53. package/dist/screens.js +4 -4
  54. package/dist/{types-CLSUZW8c.d.ts → types-Ckhz-YwC.d.ts} +7 -2
  55. package/dist/{types-BLVpNNLN.d.cts → types-D71SKA5A.d.cts} +7 -2
  56. package/dist/ux.cjs +7 -5
  57. package/dist/ux.cjs.map +1 -1
  58. package/dist/ux.d.cts +1 -1
  59. package/dist/ux.d.ts +1 -1
  60. package/dist/ux.js +1 -1
  61. package/package.json +2 -2
  62. package/dist/chunk-BHM5HGXH.js.map +0 -1
  63. package/dist/chunk-HBE4LVPX.js.map +0 -1
@@ -0,0 +1,149 @@
1
+ /**
2
+ * `{{ }}` resolution — the TypeScript twin of `FancyFlow\Nodes\Support\Expr`.
3
+ *
4
+ * ## Why this is opt-in and not wired into `runFlow`
5
+ *
6
+ * The PHP runtime resolves expressions inside its batteries-included executors.
7
+ * The JS runtime never has: `runFlow` hands `node.data.config` to your executor
8
+ * verbatim, so every host that uses `{{ }}` today already resolves it itself.
9
+ * Turning resolution on inside `runFlow` would interpolate a second time over
10
+ * values those hosts had already substituted — silently, and only for graphs
11
+ * that happen to contain `{{` in their DATA.
12
+ *
13
+ * So this exports the semantics and changes no behaviour. Call it yourself:
14
+ *
15
+ * ```ts
16
+ * const url = evaluateExpression(node.data.config.url, inputs);
17
+ * ```
18
+ *
19
+ * ## The semantics are the PHP file's, deliberately
20
+ *
21
+ * This is not a general expression language and must not grow into one. It
22
+ * resolves a dot-path against a context and nothing else — no arithmetic, no
23
+ * comparisons, no calls. Hosts that want real expressions override the executor
24
+ * (the PHP docblock points at symfony/expression-language for the same reason).
25
+ *
26
+ * Divergence here is a correctness bug rather than a style difference: the same
27
+ * graph is authored once and may run on either runtime. `suites/shared/expr`
28
+ * in `@particle-academy/fancy-conformance` is the fixture table both sides run,
29
+ * so parity is a test result instead of a claim.
30
+ */
31
+ /** Anything a context or a resolved value can be. */
32
+ type ExprValue = unknown;
33
+ /** The context an expression resolves against — the executor's `inputs`. */
34
+ type ExprContext = Record<string, ExprValue>;
35
+ /**
36
+ * What a resolution attempt ANSWERS — did the path resolve, and to what.
37
+ *
38
+ * `resolvePath` cannot express this, and that is the defect it exists to fix:
39
+ * it returns `null` both for "this path does not exist" and for "this path
40
+ * exists and holds null". One value standing for two states.
41
+ *
42
+ * At the interpolation layer the collapse is worse, because `null` stringifies
43
+ * to `""`. A consumer put it exactly:
44
+ *
45
+ * > "An unresolvable path yields `''`, so a wrong field is indistinguishable
46
+ * > from an empty one at runtime."
47
+ *
48
+ * A misspelled field renders as an empty string, which looks precisely like a
49
+ * field that is legitimately empty. The graph runs, the node succeeds, and the
50
+ * output is quietly missing a value nobody is told about — worst on
51
+ * LLM-authored graphs, where the field name was guessed in the first place.
52
+ *
53
+ * Same shape as the four `??` collapses fixed across all four runtimes on
54
+ * 2026-08-26 (absent vs null), one layer up: **presence is the only correct
55
+ * test, and a return value that cannot express presence cannot be tested for
56
+ * it.** Hence a second return channel rather than a cleverer sentinel — every
57
+ * sentinel is a legal value for somebody.
58
+ */
59
+ interface Resolution {
60
+ /** Whether the path resolved at all. `false` means it does not exist. */
61
+ resolved: boolean;
62
+ /** The value, when `resolved`. `null` when not — do not read it blind. */
63
+ value: ExprValue;
64
+ }
65
+ /** Thrown by the `"throw"` policy when a path does not resolve. */
66
+ declare class UnresolvedPathError extends Error {
67
+ readonly path: string;
68
+ constructor(path: string);
69
+ }
70
+ /**
71
+ * What evaluation does with a path that does not resolve.
72
+ *
73
+ * - `"empty"` — today's behaviour, and the DEFAULT. Interpolates to `""`; a
74
+ * whole expression yields `null`. Unchanged so that widening this API breaks
75
+ * nobody: 40 call sites across the runtimes assume it.
76
+ * - `"keep"` — leave the `{{ … }}` text in place. The failure becomes VISIBLE
77
+ * in the output without stopping the run, which is what you want for
78
+ * human-reviewed content: a rendered `{{ in.recipient_naem }}` is self-
79
+ * diagnosing in a way an absence never is.
80
+ * - `"throw"` — refuse. For hosts that would rather fail a run than deliver a
81
+ * silently incomplete result.
82
+ *
83
+ * Opt-in before default at the request of the consumer who reported it — the
84
+ * host with the most LLM-authored graphs, and so both the biggest beneficiary
85
+ * and the right place for it to break first if it is going to.
86
+ */
87
+ type UnresolvedPolicy = "empty" | "keep" | "throw";
88
+ /** Options for {@link evaluateExpression} / {@link evaluateConfig}. */
89
+ interface EvaluateOptions {
90
+ /** What to do with a path that does not resolve. Default `"empty"`. */
91
+ onUnresolved?: UnresolvedPolicy;
92
+ }
93
+ /**
94
+ * Resolve a dot-path, reporting WHETHER it resolved.
95
+ *
96
+ * The same walk as `resolvePath` — deliberately, so the two can never disagree
97
+ * about what resolves; `resolvePath` is defined in terms of this one below.
98
+ *
99
+ * A note on JS having two absent values: a key present with the value
100
+ * `undefined` reports `resolved: false`, matching `resolvePath`'s long-standing
101
+ * behaviour. It cannot arise from graph data (JSON has no `undefined`), and
102
+ * changing it would make the two functions disagree for no reachable gain.
103
+ */
104
+ declare function tryResolvePath(path: string, context: ExprContext): Resolution;
105
+ /**
106
+ * Resolve a dot-path against the context, honouring the `$json` / `$input`
107
+ * alias.
108
+ *
109
+ * Both aliases point at the `in` port value when the context has one, and at
110
+ * the whole context otherwise — the same fallback the PHP does, which is what
111
+ * makes `{{ $json.x }}` work on a trigger node that has no upstream input.
112
+ *
113
+ * A path that does not resolve returns `null`, never `undefined`: PHP has one
114
+ * absent value and JS has two, and letting the difference leak would make the
115
+ * two runtimes disagree about `{{ missing }}` for no useful reason.
116
+ */
117
+ declare function resolvePath(path: string, context: ExprContext): ExprValue;
118
+ /**
119
+ * Evaluate a template against a context.
120
+ *
121
+ * A string that is EXACTLY one expression returns the resolved value with its
122
+ * type intact — `{{ $json.count }}` gives you a number, not `"3"`. Anything
123
+ * else interpolates each run as text. That distinction is load-bearing: it is
124
+ * what lets a config field carry either a value or a sentence.
125
+ *
126
+ * Non-string templates pass through untouched, so this is safe to map over a
127
+ * whole config object.
128
+ */
129
+ declare function evaluateExpression(template: ExprValue, context: ExprContext, options?: EvaluateOptions): ExprValue;
130
+ /**
131
+ * Truthiness for branch / switch decisions.
132
+ *
133
+ * Mirrors PHP's rules rather than JavaScript's, because the graph is authored
134
+ * once and may run on either side. The two disagree in exactly the places a
135
+ * workflow hits: `"0"` and `"false"` are truthy in JS and falsy here, and an
136
+ * empty array is truthy in JS and falsy here.
137
+ */
138
+ declare function truthy(value: ExprValue): boolean;
139
+ /** Coerce a value to text the way interpolation does. */
140
+ declare function text(value: ExprValue): string;
141
+ /**
142
+ * Resolve every string in a config object, one level of nesting at a time.
143
+ *
144
+ * The convenience most hosts actually want, and the shape their hand-rolled
145
+ * version usually takes. Opt-in like the rest of this module.
146
+ */
147
+ declare function evaluateConfig<T extends Record<string, ExprValue>>(config: T, context: ExprContext, options?: EvaluateOptions): T;
148
+
149
+ export { type EvaluateOptions as E, type Resolution as R, UnresolvedPathError as U, type ExprContext as a, type ExprValue as b, type UnresolvedPolicy as c, evaluateExpression as d, evaluateConfig as e, truthy as f, tryResolvePath as g, resolvePath as r, text as t };
@@ -1,8 +1,8 @@
1
- import { H as HumanFieldRenderers, C as ConfigFieldRenderFn } from '../HumanPrompt-DV5-xnDa.cjs';
1
+ import { H as HumanFieldRenderers, C as ConfigFieldRenderFn } from '../HumanPrompt-Bpp3JiVH.cjs';
2
2
  import 'react';
3
3
  import '../types-JFYjPJAG.cjs';
4
4
  import '@xyflow/react';
5
- import '../types-BLVpNNLN.cjs';
5
+ import '../types-D71SKA5A.cjs';
6
6
  import '../pause-9iT4tCEV.cjs';
7
7
 
8
8
  /**
@@ -1,8 +1,8 @@
1
- import { H as HumanFieldRenderers, C as ConfigFieldRenderFn } from '../HumanPrompt-Bw09OyS3.js';
1
+ import { H as HumanFieldRenderers, C as ConfigFieldRenderFn } from '../HumanPrompt-BKThiyjB.js';
2
2
  import 'react';
3
3
  import '../types-JFYjPJAG.js';
4
4
  import '@xyflow/react';
5
- import '../types-CLSUZW8c.js';
5
+ import '../types-Ckhz-YwC.js';
6
6
  import '../pause-9iT4tCEV.js';
7
7
 
8
8
  /**
package/dist/index.cjs CHANGED
@@ -11445,7 +11445,7 @@ function activatedPorts(node, result) {
11445
11445
  return { ports: [r.__port], value: r.value };
11446
11446
  }
11447
11447
  if (typeof r.branch === "string") {
11448
- return { ports: [r.branch], value: r.value ?? r };
11448
+ return { ports: [r.branch], value: Object.prototype.hasOwnProperty.call(r, "value") ? r.value : r };
11449
11449
  }
11450
11450
  }
11451
11451
  const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
@@ -11607,8 +11607,9 @@ var KINDS = [
11607
11607
  // ───────────── Triggers ─────────────
11608
11608
  {
11609
11609
  name: "@particle-academy/manual_trigger",
11610
- // returns the inputs it was started with
11611
- emits: "input",
11610
+ // Returns the raw inputs MAP, not the `in` port -- flat at an entry
11611
+ // point, port-keyed the moment the node has an inbound edge.
11612
+ emits: "input-map-merged",
11612
11613
  aliases: ["manual_trigger", "@fancy/manual_trigger"],
11613
11614
  category: "trigger",
11614
11615
  label: "Manual",
@@ -11641,8 +11642,9 @@ var KINDS = [
11641
11642
  { path: "cron", type: "string", description: "The cron expression that fired." },
11642
11643
  { path: "timezone", type: "string", description: "The timezone it was evaluated in." }
11643
11644
  ],
11644
- // merges its inputs into the TOP level beside cron/timezone
11645
- emits: "inputs-merged",
11645
+ // Merges $ctx->inputs ITSELF beside cron/timezone -- the MAP, whose
11646
+ // shape depends on position, not each port's payload the way `merge` does.
11647
+ emits: "input-map-merged",
11646
11648
  aliases: ["schedule_trigger", "@fancy/schedule_trigger"],
11647
11649
  category: "trigger",
11648
11650
  label: "Schedule",
@@ -15874,9 +15876,19 @@ function interpolate(template, resolve) {
15874
15876
  }
15875
15877
  }
15876
15878
  var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
15877
- function resolvePath(path, context) {
15879
+ var UnresolvedPathError = class extends Error {
15880
+ constructor(path) {
15881
+ super(
15882
+ `Expression path "${path}" did not resolve. Under the "throw" policy an unresolvable path is an error rather than an empty string.`
15883
+ );
15884
+ this.path = path;
15885
+ this.name = "UnresolvedPathError";
15886
+ }
15887
+ };
15888
+ function tryResolvePath(path, context) {
15889
+ const unresolved = { resolved: false, value: null };
15878
15890
  const trimmed = path.trim();
15879
- if (trimmed === "") return null;
15891
+ if (trimmed === "") return unresolved;
15880
15892
  const segments = trimmed.split(".");
15881
15893
  let cursor;
15882
15894
  const head = segments[0];
@@ -15887,19 +15899,33 @@ function resolvePath(path, context) {
15887
15899
  cursor = context;
15888
15900
  }
15889
15901
  for (const segment of segments) {
15890
- if (cursor === null || cursor === void 0) return null;
15891
- if (typeof cursor !== "object") return null;
15902
+ if (cursor === null || cursor === void 0) return unresolved;
15903
+ if (typeof cursor !== "object") return unresolved;
15892
15904
  const next = cursor[segment];
15893
- if (next === void 0) return null;
15905
+ if (next === void 0) return unresolved;
15894
15906
  cursor = next;
15895
15907
  }
15896
- return cursor === void 0 ? null : cursor;
15908
+ return { resolved: true, value: cursor === void 0 ? null : cursor };
15909
+ }
15910
+ function resolvePath(path, context) {
15911
+ return tryResolvePath(path, context).value;
15897
15912
  }
15898
- function evaluateExpression(template, context) {
15913
+ function evaluateExpression(template, context, options = {}) {
15899
15914
  if (typeof template !== "string") return template;
15915
+ const policy = options.onUnresolved ?? "empty";
15900
15916
  const whole = wholeExpression(template.trim());
15901
- if (whole !== null) return resolvePath(whole, context);
15902
- return interpolate(template, (path) => stringify(resolvePath(path, context)));
15917
+ if (whole !== null) {
15918
+ const r = tryResolvePath(whole, context);
15919
+ if (r.resolved) return r.value;
15920
+ if (policy === "throw") throw new UnresolvedPathError(whole);
15921
+ return policy === "keep" ? template : null;
15922
+ }
15923
+ return interpolate(template, (path) => {
15924
+ const r = tryResolvePath(path, context);
15925
+ if (r.resolved) return stringify(r.value);
15926
+ if (policy === "throw") throw new UnresolvedPathError(path);
15927
+ return policy === "keep" ? `{{${path}}}` : "";
15928
+ });
15903
15929
  }
15904
15930
  function truthy(value) {
15905
15931
  if (typeof value === "boolean") return value;
@@ -15923,10 +15949,10 @@ function stringify(value) {
15923
15949
  return "";
15924
15950
  }
15925
15951
  }
15926
- function evaluateConfig(config, context) {
15952
+ function evaluateConfig(config, context, options = {}) {
15927
15953
  const out = {};
15928
15954
  for (const [key, value] of Object.entries(config)) {
15929
- out[key] = Array.isArray(value) ? value.map((v2) => evaluateExpression(v2, context)) : value !== null && typeof value === "object" ? evaluateConfig(value, context) : evaluateExpression(value, context);
15955
+ out[key] = Array.isArray(value) ? value.map((v2) => evaluateExpression(v2, context, options)) : value !== null && typeof value === "object" ? evaluateConfig(value, context, options) : evaluateExpression(value, context, options);
15930
15956
  }
15931
15957
  return out;
15932
15958
  }
@@ -15963,6 +15989,7 @@ exports.RegistryNode = RegistryNode;
15963
15989
  exports.RichInputPreview = RichInputPreview;
15964
15990
  exports.SubgraphNode = SubgraphNode;
15965
15991
  exports.TriggerNode = TriggerNode;
15992
+ exports.UnresolvedPathError = UnresolvedPathError;
15966
15993
  exports.WORKFLOW_SCHEMA_URL = WORKFLOW_SCHEMA_URL;
15967
15994
  exports.WORKFLOW_SCHEMA_VERSION = WORKFLOW_SCHEMA_VERSION;
15968
15995
  exports.alignNodes = alignNodes;
@@ -16014,6 +16041,7 @@ exports.runCohort = runCohort;
16014
16041
  exports.runFlow = runFlow;
16015
16042
  exports.text = text;
16016
16043
  exports.truthy = truthy;
16044
+ exports.tryResolvePath = tryResolvePath;
16017
16045
  exports.useFlowEditor = useFlowEditor;
16018
16046
  exports.useFlowEditorOptional = useFlowEditorOptional;
16019
16047
  exports.useFlowHistory = useFlowHistory;