@wizzard-packages/core 0.5.0 → 1.0.0-canary-20260920100815

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 (71) hide show
  1. package/README.md +34 -11
  2. package/dist/{expr-C9w_odUt.d.cts → expr-BWJP4t1N.d.cts} +3 -6
  3. package/dist/{expr-C9w_odUt.d.ts → expr-BWJP4t1N.d.ts} +3 -6
  4. package/dist/expr-builder.cjs.map +1 -0
  5. package/dist/{v1/expr-builder.d.cts → expr-builder.d.cts} +7 -1
  6. package/dist/{v1/expr-builder.d.ts → expr-builder.d.ts} +7 -1
  7. package/dist/expr-builder.js.map +1 -0
  8. package/dist/{flow-kC0pucDF.d.cts → flow-CMgwuFDk.d.cts} +1 -1
  9. package/dist/{flow-B6E6c5Gc.d.ts → flow-DdUymTJD.d.ts} +1 -1
  10. package/dist/graph.cjs.map +1 -0
  11. package/dist/{v1/graph.d.cts → graph.d.cts} +8 -2
  12. package/dist/{v1/graph.d.ts → graph.d.ts} +8 -2
  13. package/dist/graph.js.map +1 -0
  14. package/dist/{v1/groups.cjs → groups.cjs} +138 -36
  15. package/dist/groups.cjs.map +1 -0
  16. package/dist/{v1/groups.d.ts → groups.d.cts} +10 -4
  17. package/dist/{v1/groups.d.cts → groups.d.ts} +10 -4
  18. package/dist/{v1/groups.js → groups.js} +138 -36
  19. package/dist/groups.js.map +1 -0
  20. package/dist/index.cjs +936 -791
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +272 -453
  23. package/dist/index.d.ts +272 -453
  24. package/dist/index.js +907 -787
  25. package/dist/index.js.map +1 -1
  26. package/dist/{navigate-DUUWXjAr.d.cts → navigate-BZAZpSkt.d.cts} +19 -7
  27. package/dist/{navigate-DJkRL8XS.d.ts → navigate-BZTpZZWJ.d.ts} +19 -7
  28. package/dist/{v1/session.cjs → session.cjs} +71 -27
  29. package/dist/session.cjs.map +1 -0
  30. package/dist/{v1/session.d.cts → session.d.cts} +9 -3
  31. package/dist/{v1/session.d.ts → session.d.ts} +9 -3
  32. package/dist/session.js +178 -0
  33. package/dist/session.js.map +1 -0
  34. package/dist/snapshot.cjs.map +1 -0
  35. package/dist/{v1/snapshot.d.ts → snapshot.d.cts} +10 -4
  36. package/dist/{v1/snapshot.d.cts → snapshot.d.ts} +10 -4
  37. package/dist/snapshot.js.map +1 -0
  38. package/dist/validate-flow.cjs +378 -0
  39. package/dist/validate-flow.cjs.map +1 -0
  40. package/dist/{v1/validate-flow.d.cts → validate-flow.d.cts} +19 -2
  41. package/dist/{v1/validate-flow.d.ts → validate-flow.d.ts} +19 -2
  42. package/dist/validate-flow.js +375 -0
  43. package/dist/validate-flow.js.map +1 -0
  44. package/package.json +30 -30
  45. package/dist/v1/expr-builder.cjs.map +0 -1
  46. package/dist/v1/expr-builder.js.map +0 -1
  47. package/dist/v1/graph.cjs.map +0 -1
  48. package/dist/v1/graph.js.map +0 -1
  49. package/dist/v1/groups.cjs.map +0 -1
  50. package/dist/v1/groups.js.map +0 -1
  51. package/dist/v1/index.cjs +0 -812
  52. package/dist/v1/index.cjs.map +0 -1
  53. package/dist/v1/index.d.cts +0 -274
  54. package/dist/v1/index.d.ts +0 -274
  55. package/dist/v1/index.js +0 -783
  56. package/dist/v1/index.js.map +0 -1
  57. package/dist/v1/session.cjs.map +0 -1
  58. package/dist/v1/session.js +0 -134
  59. package/dist/v1/session.js.map +0 -1
  60. package/dist/v1/snapshot.cjs.map +0 -1
  61. package/dist/v1/snapshot.js.map +0 -1
  62. package/dist/v1/validate-flow.cjs +0 -124
  63. package/dist/v1/validate-flow.cjs.map +0 -1
  64. package/dist/v1/validate-flow.js +0 -121
  65. package/dist/v1/validate-flow.js.map +0 -1
  66. /package/dist/{v1/expr-builder.cjs → expr-builder.cjs} +0 -0
  67. /package/dist/{v1/expr-builder.js → expr-builder.js} +0 -0
  68. /package/dist/{v1/graph.cjs → graph.cjs} +0 -0
  69. /package/dist/{v1/graph.js → graph.js} +0 -0
  70. /package/dist/{v1/snapshot.cjs → snapshot.cjs} +0 -0
  71. /package/dist/{v1/snapshot.js → snapshot.js} +0 -0
package/README.md CHANGED
@@ -11,10 +11,15 @@ rather than two implementations of the same rules.
11
11
 
12
12
  ## Install
13
13
 
14
+ <!-- example:install-core -->
15
+
16
+ <!-- prettier-ignore -->
14
17
  ```bash
15
18
  pnpm add @wizzard-packages/core@canary
16
19
  ```
17
20
 
21
+ <!-- /example -->
22
+
18
23
  ## A flow
19
24
 
20
25
  `defineFlow` and `step` are the typed way to write that JSON. They add no runtime behaviour —
@@ -25,7 +30,7 @@ pnpm add @wizzard-packages/core@canary
25
30
 
26
31
  <!-- prettier-ignore -->
27
32
  ```ts
28
- import { defineFlow, step } from '@wizzard-packages/core/v1';
33
+ import { defineFlow, step } from '@wizzard-packages/core';
29
34
 
30
35
  /**
31
36
  * The smallest flow that is still a wizard: two steps, one field, and a value
@@ -50,7 +55,7 @@ export const signup = defineFlow({
50
55
  ## Running it
51
56
 
52
57
  ```ts
53
- import { createWizard } from '@wizzard-packages/core/v1';
58
+ import { createWizard } from '@wizzard-packages/core';
54
59
 
55
60
  import { signup } from './flow';
56
61
 
@@ -70,20 +75,38 @@ value, or one data path.
70
75
  Anything a flow cannot serialize — a validator, a predicate, an async guard — is a **named**
71
76
  entry in `registry`, so `JSON.stringify(flow)` always round-trips.
72
77
 
78
+ What throws is a mistake in the program, never an outcome of the wizard: a `$ref` the registry
79
+ does not hold, a group step with no traversal installed. Every such failure is a `WizardError`
80
+ with `code`, `op`, `path`, `fix` and `url`, where `url` is the page for that code under
81
+ `https://zizzx.github.io/wizzard-packages/errors/`.
82
+
83
+ An expression that nests deeper than 256 levels, counting every object and list, throws
84
+ `expr-too-deep` when the evaluator reaches it, instead of overflowing the call stack. Nothing
85
+ written by hand comes near that; it is for a document that was pasted or sent from outside, and
86
+ `validateFlow` reports it before anything runs, together with a function or a cycle at any depth
87
+ anywhere in the flow. An operand of the wrong shape, such as `{ $and: null }` or `{ $get: 123 }`,
88
+ throws `expr-invalid-operand` rather than a bare `TypeError`, and `validateFlow` reports that too.
89
+
90
+ A plugin's failure has no caller to throw to, so the engine logs it in the same four sentences
91
+ instead and carries on. A plugin that throws in `init`, or throws or rejects in `onCommit` or
92
+ `onAttempt`, is switched off for good (`plugin-disabled`); one that fails in `afterNavigate` is
93
+ reported and stays on, because the move has already happened (`after-navigate-threw`); a teardown
94
+ that fails during `destroy()` is reported and the rest still run (`plugin-teardown-failed`).
95
+
73
96
  ## Entries
74
97
 
75
98
  Separate entry points because they are separate budgets. A wizard that never draws itself
76
99
  does not carry the code that would.
77
100
 
78
- | Entry | What it is |
79
- | ---------------- | -------------------------------------------------------------- |
80
- | `/v1` | the engine: types, expressions, navigation, selectors |
81
- | `/graph` | a flow as `{ nodes, edges }`, for drawing it |
82
- | `/groups` | traversal for repeated sub-flows |
83
- | `/session` | a recorded run, and the check that a replay matches it |
84
- | `/snapshot` | serialize a run, and refuse stored JSON that cannot be trusted |
85
- | `/expr` | a builder for expressions, if you dislike writing JSON |
86
- | `/validate-flow` | checks a flow definition, for tooling and tests |
101
+ | Entry | What it is |
102
+ | ---------------- | --------------------------------------------------------------------------- |
103
+ | `/v1` | the engine: types, expressions, navigation, selectors |
104
+ | `/graph` | a flow as `{ nodes, edges }`, for drawing it |
105
+ | `/groups` | traversal for repeated sub-flows |
106
+ | `/session` | a recorded run, and the check that a replay matches it |
107
+ | `/snapshot` | serialize a run, and refuse stored JSON that cannot be trusted |
108
+ | `/expr` | a builder for expressions, if you dislike writing JSON |
109
+ | `/validate-flow` | checks a flow and its inline sub-flows, each problem with a code and a page |
87
110
 
88
111
  `.size-limit.js` holds the budget for each, with the measurement that set it.
89
112
 
@@ -58,12 +58,9 @@ interface Scope {
58
58
  }
59
59
  type Resolver = (args: Json | undefined, scope: Scope) => unknown;
60
60
  type Registry = Readonly<Record<string, Resolver>>;
61
- /** Thrown when a synchronous evaluation meets a `$ref` it cannot resolve. */
62
- declare class ExprError extends Error {
63
- }
64
61
  /**
65
- * Evaluates synchronously. Throws `ExprError` on a `$ref` that the registry
66
- * does not define, or whose resolver returns a promise.
62
+ * Evaluates synchronously. Throws a `WizardError` on a `$ref` that the
63
+ * registry does not define, or whose resolver returns a promise.
67
64
  *
68
65
  * Most flows contain no `$ref` at all, so `isSync` lets the engine take this
69
66
  * path and know which steps are reachable before the first paint.
@@ -89,4 +86,4 @@ type AsyncRegistry = Readonly<Record<string, AsyncResolver>>;
89
86
  /** Evaluates to a boolean, awaiting `$ref`. An absent expression is `true`. */
90
87
  declare function testAsync(e: Expr | undefined, scope: Scope, registry?: AsyncRegistry): Promise<boolean>;
91
88
 
92
- export { type AsyncRegistry as A, type Expr as E, type Json as J, type Registry as R, type Scope as S, type Resolver as a, ExprError as b, evaluateAsync as c, type AsyncResolver as d, evaluate as e, testAsync as f, isSync as i, test as t };
89
+ export { type AsyncRegistry as A, type Expr as E, type Json as J, type Registry as R, type Scope as S, type Resolver as a, evaluateAsync as b, type AsyncResolver as c, testAsync as d, evaluate as e, isSync as i, test as t };
@@ -58,12 +58,9 @@ interface Scope {
58
58
  }
59
59
  type Resolver = (args: Json | undefined, scope: Scope) => unknown;
60
60
  type Registry = Readonly<Record<string, Resolver>>;
61
- /** Thrown when a synchronous evaluation meets a `$ref` it cannot resolve. */
62
- declare class ExprError extends Error {
63
- }
64
61
  /**
65
- * Evaluates synchronously. Throws `ExprError` on a `$ref` that the registry
66
- * does not define, or whose resolver returns a promise.
62
+ * Evaluates synchronously. Throws a `WizardError` on a `$ref` that the
63
+ * registry does not define, or whose resolver returns a promise.
67
64
  *
68
65
  * Most flows contain no `$ref` at all, so `isSync` lets the engine take this
69
66
  * path and know which steps are reachable before the first paint.
@@ -89,4 +86,4 @@ type AsyncRegistry = Readonly<Record<string, AsyncResolver>>;
89
86
  /** Evaluates to a boolean, awaiting `$ref`. An absent expression is `true`. */
90
87
  declare function testAsync(e: Expr | undefined, scope: Scope, registry?: AsyncRegistry): Promise<boolean>;
91
88
 
92
- export { type AsyncRegistry as A, type Expr as E, type Json as J, type Registry as R, type Scope as S, type Resolver as a, ExprError as b, evaluateAsync as c, type AsyncResolver as d, evaluate as e, testAsync as f, isSync as i, test as t };
89
+ export { type AsyncRegistry as A, type Expr as E, type Json as J, type Registry as R, type Scope as S, type Resolver as a, evaluateAsync as b, type AsyncResolver as c, testAsync as d, evaluate as e, isSync as i, test as t };
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/v1/expr-builder.ts"],"names":[],"mappings":";;;AA4BO,IAAM,GAAA,GAAM,CAAC,IAAA,MAAkC,EAAE,MAAM,IAAA,EAAK;AAG5D,IAAM,GAAA,GAAM,CAAC,IAAA,EAAc,IAAA,KAChC,IAAA,KAAS,MAAA,GAAY,EAAE,IAAA,EAAM,IAAA,EAAK,GAAI,EAAE,IAAA,EAAM,MAAM,IAAA;AAE/C,IAAM,GAAA,GAAM,CAAC,CAAA,MAA6B,EAAE,MAAM,CAAA,EAAE;AACpD,IAAM,GAAA,GAAM,CAAA,GAAI,CAAA,MAAmD,EAAE,MAAM,CAAA,EAAE;AAC7E,IAAM,EAAA,GAAK,CAAA,GAAI,CAAA,MAAkD,EAAE,KAAK,CAAA,EAAE;AAC1E,IAAM,KAAA,GAAQ,CAAC,CAAA,MAA+B,EAAE,QAAQ,CAAA,EAAE;AAE1D,IAAM,EAAA,GAAK,CAAC,CAAA,EAAS,CAAA,MAA4B,EAAE,GAAA,EAAK,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAC/D,IAAM,EAAA,GAAK,CAAC,CAAA,EAAS,CAAA,MAA4B,EAAE,GAAA,EAAK,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAC/D,IAAM,EAAA,GAAK,CAAC,CAAA,EAAS,CAAA,MAA4B,EAAE,GAAA,EAAK,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAC/D,IAAM,GAAA,GAAM,CAAC,CAAA,EAAS,CAAA,MAA6B,EAAE,IAAA,EAAM,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAClE,IAAM,EAAA,GAAK,CAAC,CAAA,EAAS,CAAA,MAA4B,EAAE,GAAA,EAAK,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAC/D,IAAM,GAAA,GAAM,CAAC,CAAA,EAAS,CAAA,MAA6B,EAAE,IAAA,EAAM,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAGlE,IAAM,IAAA,GAAO,CAAC,MAAA,EAAc,GAAA,MAA8B,EAAE,GAAA,EAAK,CAAC,MAAA,EAAQ,GAAG,CAAA,EAAE","file":"expr-builder.cjs","sourcesContent":["/**\n * Expressions written as typed function calls instead of JSON.\n *\n * @module @wizzard-packages/core/expr\n */\n\nimport type { Expr, Json } from './expr';\n\n/**\n * A typed builder for the expression language.\n *\n * Every function returns exactly the JSON `evaluate` reads, so\n * `eq(get('data.plan'), 'pro')` and `{ $eq: [{ $get: 'data.plan' }, 'pro'] }`\n * are the same value and interchangeable in a flow. The builder exists so the\n * compiler sees the tree the evaluator will walk: an operand that is not an\n * expression, a wrong arity, a path with no root — all fail before the flow is\n * serialized rather than when a step is reached.\n *\n * Its own entry, deliberately. A flow that arrives as JSON was authored\n * elsewhere, and the wizard that runs it pays nothing for the functions that\n * would have written it.\n */\n\n/** A `$get` path: one of the three roots, optionally followed by a dotted path. */\nexport type Path = 'data' | 'ctx' | 'loop' | `data.${string}` | `ctx.${string}` | `loop.${string}`;\n\ntype Pair = readonly [Expr, Expr];\n\nexport const get = (path: Path): { $get: string } => ({ $get: path });\n\n/** Names a resolver in the registry. `args` is omitted, not `undefined`, so the JSON round-trips. */\nexport const ref = (name: string, args?: Json): { $ref: string; args?: Json } =>\n args === undefined ? { $ref: name } : { $ref: name, args };\n\nexport const not = (e: Expr): { $not: Expr } => ({ $not: e });\nexport const and = (...e: readonly Expr[]): { $and: readonly Expr[] } => ({ $and: e });\nexport const or = (...e: readonly Expr[]): { $or: readonly Expr[] } => ({ $or: e });\nexport const empty = (e: Expr): { $empty: Expr } => ({ $empty: e });\n\nexport const eq = (a: Expr, b: Expr): { $eq: Pair } => ({ $eq: [a, b] });\nexport const ne = (a: Expr, b: Expr): { $ne: Pair } => ({ $ne: [a, b] });\nexport const gt = (a: Expr, b: Expr): { $gt: Pair } => ({ $gt: [a, b] });\nexport const gte = (a: Expr, b: Expr): { $gte: Pair } => ({ $gte: [a, b] });\nexport const lt = (a: Expr, b: Expr): { $lt: Pair } => ({ $lt: [a, b] });\nexport const lte = (a: Expr, b: Expr): { $lte: Pair } => ({ $lte: [a, b] });\n\n/** `$in`: the needle first, then the array or string to look in. Named for the reserved word. */\nexport const isIn = (needle: Expr, hay: Expr): { $in: Pair } => ({ $in: [needle, hay] });\n"]}
@@ -1,4 +1,10 @@
1
- import { J as Json, E as Expr } from '../expr-C9w_odUt.cjs';
1
+ import { J as Json, E as Expr } from './expr-BWJP4t1N.cjs';
2
+
3
+ /**
4
+ * Expressions written as typed function calls instead of JSON.
5
+ *
6
+ * @module @wizzard-packages/core/expr
7
+ */
2
8
 
3
9
  /**
4
10
  * A typed builder for the expression language.
@@ -1,4 +1,10 @@
1
- import { J as Json, E as Expr } from '../expr-C9w_odUt.js';
1
+ import { J as Json, E as Expr } from './expr-BWJP4t1N.js';
2
+
3
+ /**
4
+ * Expressions written as typed function calls instead of JSON.
5
+ *
6
+ * @module @wizzard-packages/core/expr
7
+ */
2
8
 
3
9
  /**
4
10
  * A typed builder for the expression language.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/v1/expr-builder.ts"],"names":[],"mappings":";AA4BO,IAAM,GAAA,GAAM,CAAC,IAAA,MAAkC,EAAE,MAAM,IAAA,EAAK;AAG5D,IAAM,GAAA,GAAM,CAAC,IAAA,EAAc,IAAA,KAChC,IAAA,KAAS,MAAA,GAAY,EAAE,IAAA,EAAM,IAAA,EAAK,GAAI,EAAE,IAAA,EAAM,MAAM,IAAA;AAE/C,IAAM,GAAA,GAAM,CAAC,CAAA,MAA6B,EAAE,MAAM,CAAA,EAAE;AACpD,IAAM,GAAA,GAAM,CAAA,GAAI,CAAA,MAAmD,EAAE,MAAM,CAAA,EAAE;AAC7E,IAAM,EAAA,GAAK,CAAA,GAAI,CAAA,MAAkD,EAAE,KAAK,CAAA,EAAE;AAC1E,IAAM,KAAA,GAAQ,CAAC,CAAA,MAA+B,EAAE,QAAQ,CAAA,EAAE;AAE1D,IAAM,EAAA,GAAK,CAAC,CAAA,EAAS,CAAA,MAA4B,EAAE,GAAA,EAAK,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAC/D,IAAM,EAAA,GAAK,CAAC,CAAA,EAAS,CAAA,MAA4B,EAAE,GAAA,EAAK,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAC/D,IAAM,EAAA,GAAK,CAAC,CAAA,EAAS,CAAA,MAA4B,EAAE,GAAA,EAAK,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAC/D,IAAM,GAAA,GAAM,CAAC,CAAA,EAAS,CAAA,MAA6B,EAAE,IAAA,EAAM,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAClE,IAAM,EAAA,GAAK,CAAC,CAAA,EAAS,CAAA,MAA4B,EAAE,GAAA,EAAK,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAC/D,IAAM,GAAA,GAAM,CAAC,CAAA,EAAS,CAAA,MAA6B,EAAE,IAAA,EAAM,CAAC,CAAA,EAAG,CAAC,CAAA,EAAE;AAGlE,IAAM,IAAA,GAAO,CAAC,MAAA,EAAc,GAAA,MAA8B,EAAE,GAAA,EAAK,CAAC,MAAA,EAAQ,GAAG,CAAA,EAAE","file":"expr-builder.js","sourcesContent":["/**\n * Expressions written as typed function calls instead of JSON.\n *\n * @module @wizzard-packages/core/expr\n */\n\nimport type { Expr, Json } from './expr';\n\n/**\n * A typed builder for the expression language.\n *\n * Every function returns exactly the JSON `evaluate` reads, so\n * `eq(get('data.plan'), 'pro')` and `{ $eq: [{ $get: 'data.plan' }, 'pro'] }`\n * are the same value and interchangeable in a flow. The builder exists so the\n * compiler sees the tree the evaluator will walk: an operand that is not an\n * expression, a wrong arity, a path with no root — all fail before the flow is\n * serialized rather than when a step is reached.\n *\n * Its own entry, deliberately. A flow that arrives as JSON was authored\n * elsewhere, and the wizard that runs it pays nothing for the functions that\n * would have written it.\n */\n\n/** A `$get` path: one of the three roots, optionally followed by a dotted path. */\nexport type Path = 'data' | 'ctx' | 'loop' | `data.${string}` | `ctx.${string}` | `loop.${string}`;\n\ntype Pair = readonly [Expr, Expr];\n\nexport const get = (path: Path): { $get: string } => ({ $get: path });\n\n/** Names a resolver in the registry. `args` is omitted, not `undefined`, so the JSON round-trips. */\nexport const ref = (name: string, args?: Json): { $ref: string; args?: Json } =>\n args === undefined ? { $ref: name } : { $ref: name, args };\n\nexport const not = (e: Expr): { $not: Expr } => ({ $not: e });\nexport const and = (...e: readonly Expr[]): { $and: readonly Expr[] } => ({ $and: e });\nexport const or = (...e: readonly Expr[]): { $or: readonly Expr[] } => ({ $or: e });\nexport const empty = (e: Expr): { $empty: Expr } => ({ $empty: e });\n\nexport const eq = (a: Expr, b: Expr): { $eq: Pair } => ({ $eq: [a, b] });\nexport const ne = (a: Expr, b: Expr): { $ne: Pair } => ({ $ne: [a, b] });\nexport const gt = (a: Expr, b: Expr): { $gt: Pair } => ({ $gt: [a, b] });\nexport const gte = (a: Expr, b: Expr): { $gte: Pair } => ({ $gte: [a, b] });\nexport const lt = (a: Expr, b: Expr): { $lt: Pair } => ({ $lt: [a, b] });\nexport const lte = (a: Expr, b: Expr): { $lte: Pair } => ({ $lte: [a, b] });\n\n/** `$in`: the needle first, then the array or string to look in. Named for the reserved word. */\nexport const isIn = (needle: Expr, hay: Expr): { $in: Pair } => ({ $in: [needle, hay] });\n"]}
@@ -1,4 +1,4 @@
1
- import { E as Expr, J as Json } from './expr-C9w_odUt.cjs';
1
+ import { E as Expr, J as Json } from './expr-BWJP4t1N.cjs';
2
2
 
3
3
  /**
4
4
  * The flow definition. Pure JSON — no functions, no class instances, nothing
@@ -1,4 +1,4 @@
1
- import { E as Expr, J as Json } from './expr-C9w_odUt.js';
1
+ import { E as Expr, J as Json } from './expr-BWJP4t1N.js';
2
2
 
3
3
  /**
4
4
  * The flow definition. Pure JSON — no functions, no class instances, nothing
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/v1/flow.ts","../src/v1/graph.ts"],"names":[],"mappings":"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","file":"graph.cjs","sourcesContent":["import type { Expr, Json } from './expr';\n\n/**\n * The flow definition. Pure JSON — no functions, no class instances, nothing\n * that `JSON.stringify` would drop. The same object can be written by hand,\n * served by a backend, or generated; the engine cannot tell which.\n *\n * Everything that cannot be serialized is named instead: `view` names a\n * component, `$ref` names a function, both looked up in a registry the host\n * supplies.\n */\nexport interface FlowDefinition {\n id: string;\n /** Bumped by the producer when the shape changes; used to reject stale patches. */\n version?: number;\n /** Default linear order. A step missing from `order` is reachable only via `on.next`. */\n order?: readonly string[];\n /** Keyed by id, not an array, so a patch can address one step without index maths. */\n steps: Readonly<Record<string, StepDef>>;\n policy?: NavigationPolicy;\n validate?: { on?: 'change' | 'blur' | 'next' | 'manual'; debounceMs?: number };\n}\n\nexport type NavigationPolicy = 'sequential' | 'visited' | 'free';\n\nexport type Target = string | { to: string; when?: Expr };\n\ninterface StepBase {\n label?: string;\n /** Reachability. A step whose `when` is false is skipped, not shown disabled. */\n when?: Expr;\n guards?: { enter?: Expr; exit?: Expr };\n on?: { next?: Target | readonly Target[]; back?: Target | 'auto' };\n /** Key of this step's slice of `data`. Defaults to the step id. */\n slice?: string;\n /** Field schema for the host to render. The engine never reads it. */\n ui?: Json;\n /** Loaded on enter, with an abort signal. */\n load?: { $ref: string; args?: Json };\n /** The body of this step arrives later, from the host. */\n deferred?: boolean;\n /**\n * What to forget when this step is left, forwards or backwards. Data is\n * kept by default: a person who goes back and returns expects their answers\n * to still be there. `true` drops the step's whole slice (`slice`, or the\n * step id); a list drops those `data` paths, in the same path language as\n * `set()`. Nothing is cleared on completion — that data is the submission.\n */\n clearOnLeave?: readonly string[] | true;\n}\n\nexport interface AtomStep extends StepBase {\n view?: string;\n validate?: { $ref: string; args?: Json };\n}\n\n/**\n * A sub-flow. With `repeat` it is also the loop construct — the same machinery,\n * because a loop is a sub-flow entered once per item. Iteration state lives in\n * the runtime frame, never in the definition, so the flow stays static and\n * diffable.\n */\nexport interface GroupStep extends StepBase {\n flow: string | FlowDefinition;\n repeat?: { over: Expr; keyBy?: string };\n /** Parent expressions piped into the child's `ctx`. */\n input?: Readonly<Record<string, Expr>>;\n}\n\nexport type StepDef = AtomStep | GroupStep;\n\nexport const isGroup = (s: StepDef): s is GroupStep => 'flow' in s;\n\n/** Sentinel target meaning the flow is finished. */\nexport const END = '@end' as const;\n","/**\n * A flow definition as nodes and edges, for drawing it.\n *\n * @module @wizzard-packages/core/graph\n */\n\nimport {\n END,\n isGroup,\n type FlowDefinition,\n type GroupStep,\n type StepDef,\n type Target,\n} from './flow';\n\nimport type { Expr } from './expr';\n\n/**\n * A flow's transition graph.\n *\n * Structure only, deliberately: no coordinates, no sizes, no layout. A flow is\n * already a pure JSON value, so its graph can be one too, and keeping positions\n * out is what lets this ship inside a published package while the site that\n * draws it is free to pull in a layout library the package never pays for.\n *\n * It is also why this is its own entry. A wizard that never draws itself should\n * not carry the code that would.\n */\nexport interface FlowGraph {\n nodes: readonly GraphNode[];\n edges: readonly GraphEdge[];\n}\n\nexport interface GraphNode {\n id: string;\n kind: 'step' | 'group' | 'end';\n label?: string;\n /** The step's own reachability predicate, verbatim, for the renderer to label. */\n when?: Expr;\n /** The body arrives later from the host, so there is nothing to draw inside yet. */\n deferred?: boolean;\n /**\n * The step is absent from a declared `order`, so nothing falls through to it\n * and it is reachable only via an explicit `on.next`.\n */\n offOrder?: boolean;\n group?: GroupNode;\n}\n\nexport interface GroupNode {\n /** The sub-flow's id, whether it was inlined or named by reference. */\n flowId: string;\n /** Present with `repeat`: the group runs once per item in this expression. */\n repeat?: Expr;\n /** The sub-flow drawn in place. Absent exactly when `opaque` says why. */\n graph?: FlowGraph;\n /**\n * Why the sub-flow could not be drawn. A group with no nested graph and no\n * reason would render as an ordinary box and read as working software, so\n * the reason is part of the data and the renderer is expected to show it.\n */\n opaque?: 'unresolved' | 'cycle' | 'too-deep';\n}\n\nexport interface GraphEdge {\n from: string;\n /** A step id, or `END`. */\n to: string;\n /**\n * `next`/`back` come from an explicit `on`; `order` is the default\n * fall-through the resolver uses when a step declares no `on.next`.\n */\n kind: 'next' | 'back' | 'order';\n /** The condition under which this edge is taken. Absent means unconditional. */\n when?: Expr;\n /** `to` names neither a step in this flow nor `END`. */\n dangling?: boolean;\n}\n\n/**\n * How deep sub-flows are followed.\n *\n * A flow can arrive as pasted, untrusted JSON. The cycle guard in `toGroup`\n * catches a sub-flow that names an ancestor, but a chain of thirty-two\n * *distinct* ones has no cycle in it and would still walk the stack down.\n * Nothing legitimate nests that far.\n */\nconst MAX_DEPTH = 32;\n\n/**\n * Builds the graph of `flow`.\n *\n * `subFlows` resolves `GroupStep.flow` when it is given by id, mirroring\n * `validateFlow(flow, registry?)`. Without it, referenced sub-flows are still\n * drawn -- as opaque nodes that say so.\n */\nexport function buildGraph(\n flow: FlowDefinition,\n subFlows?: Readonly<Record<string, FlowDefinition>>\n): FlowGraph {\n return build(flow, subFlows, [flow.id]);\n}\n\nfunction build(\n flow: FlowDefinition,\n subFlows: Readonly<Record<string, FlowDefinition>> | undefined,\n drawing: readonly string[]\n): FlowGraph {\n const entries = Object.entries(flow.steps);\n const nodes: GraphNode[] = entries.map(([id, step]) =>\n toNode(id, step, flow.order, subFlows, drawing)\n );\n const edges = [...explicitEdges(entries, flow), ...fallThroughEdges(flow)];\n\n // One terminal node, and only when something actually reaches it.\n if (edges.some((e) => e.to === END)) nodes.push({ id: END, kind: 'end' });\n\n return { nodes, edges };\n}\n\nfunction toNode(\n id: string,\n step: StepDef,\n order: readonly string[] | undefined,\n subFlows: Readonly<Record<string, FlowDefinition>> | undefined,\n drawing: readonly string[]\n): GraphNode {\n return {\n id,\n kind: isGroup(step) ? 'group' : 'step',\n ...(step.label !== undefined && { label: step.label }),\n ...(step.when !== undefined && { when: step.when }),\n ...(step.deferred === true && { deferred: true }),\n ...(order !== undefined && !order.includes(id) && { offOrder: true }),\n ...(isGroup(step) && { group: toGroup(step, subFlows, drawing) }),\n };\n}\n\nfunction toGroup(\n step: GroupStep,\n subFlows: Readonly<Record<string, FlowDefinition>> | undefined,\n drawing: readonly string[]\n): GroupNode {\n // `flow` is either the definition itself or its id, and both cases need the\n // id, so the narrowing is written out twice rather than cast once.\n const flowId = typeof step.flow === 'string' ? step.flow : step.flow.id;\n const sub = typeof step.flow === 'string' ? subFlows?.[step.flow] : step.flow;\n const repeat = step.repeat === undefined ? {} : { repeat: step.repeat.over };\n\n if (sub === undefined) return { flowId, ...repeat, opaque: 'unresolved' };\n // A sub-flow that names an ancestor by id would otherwise recurse forever.\n if (drawing.includes(flowId)) return { flowId, ...repeat, opaque: 'cycle' };\n if (drawing.length >= MAX_DEPTH) return { flowId, ...repeat, opaque: 'too-deep' };\n\n return { flowId, ...repeat, graph: build(sub, subFlows, [...drawing, flowId]) };\n}\n\nfunction explicitEdges(\n entries: readonly (readonly [string, StepDef])[],\n flow: FlowDefinition\n): GraphEdge[] {\n const edges: GraphEdge[] = [];\n for (const [id, step] of entries) {\n const next = step.on?.next;\n if (next !== undefined) {\n const targets: readonly Target[] = Array.isArray(next) ? next : [next as Target];\n for (const t of targets) edges.push(toEdge(id, t, 'next', flow));\n }\n\n const back = step.on?.back;\n // `auto` is the resolver walking `order` backwards at run time against the\n // state of the moment. There is no fixed target, and drawing a guess would\n // be worse than drawing nothing.\n if (back !== undefined && back !== 'auto') edges.push(toEdge(id, back, 'back', flow));\n }\n return edges;\n}\n\n/**\n * The default path: a step with no `on.next` falls through to the next entry in\n * `order` whose `when` passes, and ends the flow when nothing is left.\n *\n * Which is why one edge per pair is not enough. Statically we cannot know which\n * `when` holds, so every step up to and including the first unconditional one\n * is genuinely reachable from here, and a lone edge to the immediate neighbour\n * would hide the skip that `when` exists to express. By the same argument, a\n * tail of conditional steps can all be false, so the flow can end there too.\n *\n * The edge to `END` carries no `when`. Expressing it would mean negating every\n * guard it skipped past, and a synthesized compound predicate on a drawing is\n * harder to trust than an unlabelled fallback arrow.\n */\nfunction fallThroughEdges(flow: FlowDefinition): GraphEdge[] {\n const walk = flow.order ?? Object.keys(flow.steps);\n const edges: GraphEdge[] = [];\n\n for (const [from, step] of Object.entries(flow.steps)) {\n if (step.on?.next !== undefined) continue;\n\n const at = walk.indexOf(from);\n // Absent from `order`, so the resolver has nothing to walk and ends the\n // flow -- the same `END` it returns past the last ordered step. Drawing\n // nothing here would leave the step looking like a dead end on a graph\n // whose whole job is to show where a flow goes.\n if (at === -1) {\n edges.push({ from, to: END, kind: 'order' });\n continue;\n }\n\n let ends = true;\n for (let j = at + 1; j < walk.length; j++) {\n const to = walk[j];\n if (to === undefined) continue;\n const target = flow.steps[to];\n if (target === undefined) continue;\n\n edges.push({\n from,\n to,\n kind: 'order',\n ...(target.when !== undefined && { when: target.when }),\n });\n if (target.when === undefined) {\n ends = false;\n break;\n }\n }\n if (ends) edges.push({ from, to: END, kind: 'order' });\n }\n return edges;\n}\n\nfunction toEdge(from: string, t: Target, kind: 'next' | 'back', flow: FlowDefinition): GraphEdge {\n const to = typeof t === 'string' ? t : t.to;\n const when = typeof t === 'string' ? undefined : t.when;\n return {\n from,\n to,\n kind,\n ...(when !== undefined && { when }),\n // A branch pointing at nothing is a flow bug. It stays on the graph so it\n // can be seen, rather than vanishing into a step that has one fewer arrow.\n ...(to !== END && !(to in flow.steps) && { dangling: true }),\n };\n}\n"]}
@@ -1,5 +1,11 @@
1
- import { F as FlowDefinition } from '../flow-kC0pucDF.cjs';
2
- import { E as Expr } from '../expr-C9w_odUt.cjs';
1
+ import { F as FlowDefinition } from './flow-CMgwuFDk.cjs';
2
+ import { E as Expr } from './expr-BWJP4t1N.cjs';
3
+
4
+ /**
5
+ * A flow definition as nodes and edges, for drawing it.
6
+ *
7
+ * @module @wizzard-packages/core/graph
8
+ */
3
9
 
4
10
  /**
5
11
  * A flow's transition graph.
@@ -1,5 +1,11 @@
1
- import { F as FlowDefinition } from '../flow-B6E6c5Gc.js';
2
- import { E as Expr } from '../expr-C9w_odUt.js';
1
+ import { F as FlowDefinition } from './flow-DdUymTJD.js';
2
+ import { E as Expr } from './expr-BWJP4t1N.js';
3
+
4
+ /**
5
+ * A flow definition as nodes and edges, for drawing it.
6
+ *
7
+ * @module @wizzard-packages/core/graph
8
+ */
3
9
 
4
10
  /**
5
11
  * A flow's transition graph.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/v1/flow.ts","../src/v1/graph.ts"],"names":[],"mappings":"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","file":"graph.js","sourcesContent":["import type { Expr, Json } from './expr';\n\n/**\n * The flow definition. Pure JSON — no functions, no class instances, nothing\n * that `JSON.stringify` would drop. The same object can be written by hand,\n * served by a backend, or generated; the engine cannot tell which.\n *\n * Everything that cannot be serialized is named instead: `view` names a\n * component, `$ref` names a function, both looked up in a registry the host\n * supplies.\n */\nexport interface FlowDefinition {\n id: string;\n /** Bumped by the producer when the shape changes; used to reject stale patches. */\n version?: number;\n /** Default linear order. A step missing from `order` is reachable only via `on.next`. */\n order?: readonly string[];\n /** Keyed by id, not an array, so a patch can address one step without index maths. */\n steps: Readonly<Record<string, StepDef>>;\n policy?: NavigationPolicy;\n validate?: { on?: 'change' | 'blur' | 'next' | 'manual'; debounceMs?: number };\n}\n\nexport type NavigationPolicy = 'sequential' | 'visited' | 'free';\n\nexport type Target = string | { to: string; when?: Expr };\n\ninterface StepBase {\n label?: string;\n /** Reachability. A step whose `when` is false is skipped, not shown disabled. */\n when?: Expr;\n guards?: { enter?: Expr; exit?: Expr };\n on?: { next?: Target | readonly Target[]; back?: Target | 'auto' };\n /** Key of this step's slice of `data`. Defaults to the step id. */\n slice?: string;\n /** Field schema for the host to render. The engine never reads it. */\n ui?: Json;\n /** Loaded on enter, with an abort signal. */\n load?: { $ref: string; args?: Json };\n /** The body of this step arrives later, from the host. */\n deferred?: boolean;\n /**\n * What to forget when this step is left, forwards or backwards. Data is\n * kept by default: a person who goes back and returns expects their answers\n * to still be there. `true` drops the step's whole slice (`slice`, or the\n * step id); a list drops those `data` paths, in the same path language as\n * `set()`. Nothing is cleared on completion — that data is the submission.\n */\n clearOnLeave?: readonly string[] | true;\n}\n\nexport interface AtomStep extends StepBase {\n view?: string;\n validate?: { $ref: string; args?: Json };\n}\n\n/**\n * A sub-flow. With `repeat` it is also the loop construct — the same machinery,\n * because a loop is a sub-flow entered once per item. Iteration state lives in\n * the runtime frame, never in the definition, so the flow stays static and\n * diffable.\n */\nexport interface GroupStep extends StepBase {\n flow: string | FlowDefinition;\n repeat?: { over: Expr; keyBy?: string };\n /** Parent expressions piped into the child's `ctx`. */\n input?: Readonly<Record<string, Expr>>;\n}\n\nexport type StepDef = AtomStep | GroupStep;\n\nexport const isGroup = (s: StepDef): s is GroupStep => 'flow' in s;\n\n/** Sentinel target meaning the flow is finished. */\nexport const END = '@end' as const;\n","/**\n * A flow definition as nodes and edges, for drawing it.\n *\n * @module @wizzard-packages/core/graph\n */\n\nimport {\n END,\n isGroup,\n type FlowDefinition,\n type GroupStep,\n type StepDef,\n type Target,\n} from './flow';\n\nimport type { Expr } from './expr';\n\n/**\n * A flow's transition graph.\n *\n * Structure only, deliberately: no coordinates, no sizes, no layout. A flow is\n * already a pure JSON value, so its graph can be one too, and keeping positions\n * out is what lets this ship inside a published package while the site that\n * draws it is free to pull in a layout library the package never pays for.\n *\n * It is also why this is its own entry. A wizard that never draws itself should\n * not carry the code that would.\n */\nexport interface FlowGraph {\n nodes: readonly GraphNode[];\n edges: readonly GraphEdge[];\n}\n\nexport interface GraphNode {\n id: string;\n kind: 'step' | 'group' | 'end';\n label?: string;\n /** The step's own reachability predicate, verbatim, for the renderer to label. */\n when?: Expr;\n /** The body arrives later from the host, so there is nothing to draw inside yet. */\n deferred?: boolean;\n /**\n * The step is absent from a declared `order`, so nothing falls through to it\n * and it is reachable only via an explicit `on.next`.\n */\n offOrder?: boolean;\n group?: GroupNode;\n}\n\nexport interface GroupNode {\n /** The sub-flow's id, whether it was inlined or named by reference. */\n flowId: string;\n /** Present with `repeat`: the group runs once per item in this expression. */\n repeat?: Expr;\n /** The sub-flow drawn in place. Absent exactly when `opaque` says why. */\n graph?: FlowGraph;\n /**\n * Why the sub-flow could not be drawn. A group with no nested graph and no\n * reason would render as an ordinary box and read as working software, so\n * the reason is part of the data and the renderer is expected to show it.\n */\n opaque?: 'unresolved' | 'cycle' | 'too-deep';\n}\n\nexport interface GraphEdge {\n from: string;\n /** A step id, or `END`. */\n to: string;\n /**\n * `next`/`back` come from an explicit `on`; `order` is the default\n * fall-through the resolver uses when a step declares no `on.next`.\n */\n kind: 'next' | 'back' | 'order';\n /** The condition under which this edge is taken. Absent means unconditional. */\n when?: Expr;\n /** `to` names neither a step in this flow nor `END`. */\n dangling?: boolean;\n}\n\n/**\n * How deep sub-flows are followed.\n *\n * A flow can arrive as pasted, untrusted JSON. The cycle guard in `toGroup`\n * catches a sub-flow that names an ancestor, but a chain of thirty-two\n * *distinct* ones has no cycle in it and would still walk the stack down.\n * Nothing legitimate nests that far.\n */\nconst MAX_DEPTH = 32;\n\n/**\n * Builds the graph of `flow`.\n *\n * `subFlows` resolves `GroupStep.flow` when it is given by id, mirroring\n * `validateFlow(flow, registry?)`. Without it, referenced sub-flows are still\n * drawn -- as opaque nodes that say so.\n */\nexport function buildGraph(\n flow: FlowDefinition,\n subFlows?: Readonly<Record<string, FlowDefinition>>\n): FlowGraph {\n return build(flow, subFlows, [flow.id]);\n}\n\nfunction build(\n flow: FlowDefinition,\n subFlows: Readonly<Record<string, FlowDefinition>> | undefined,\n drawing: readonly string[]\n): FlowGraph {\n const entries = Object.entries(flow.steps);\n const nodes: GraphNode[] = entries.map(([id, step]) =>\n toNode(id, step, flow.order, subFlows, drawing)\n );\n const edges = [...explicitEdges(entries, flow), ...fallThroughEdges(flow)];\n\n // One terminal node, and only when something actually reaches it.\n if (edges.some((e) => e.to === END)) nodes.push({ id: END, kind: 'end' });\n\n return { nodes, edges };\n}\n\nfunction toNode(\n id: string,\n step: StepDef,\n order: readonly string[] | undefined,\n subFlows: Readonly<Record<string, FlowDefinition>> | undefined,\n drawing: readonly string[]\n): GraphNode {\n return {\n id,\n kind: isGroup(step) ? 'group' : 'step',\n ...(step.label !== undefined && { label: step.label }),\n ...(step.when !== undefined && { when: step.when }),\n ...(step.deferred === true && { deferred: true }),\n ...(order !== undefined && !order.includes(id) && { offOrder: true }),\n ...(isGroup(step) && { group: toGroup(step, subFlows, drawing) }),\n };\n}\n\nfunction toGroup(\n step: GroupStep,\n subFlows: Readonly<Record<string, FlowDefinition>> | undefined,\n drawing: readonly string[]\n): GroupNode {\n // `flow` is either the definition itself or its id, and both cases need the\n // id, so the narrowing is written out twice rather than cast once.\n const flowId = typeof step.flow === 'string' ? step.flow : step.flow.id;\n const sub = typeof step.flow === 'string' ? subFlows?.[step.flow] : step.flow;\n const repeat = step.repeat === undefined ? {} : { repeat: step.repeat.over };\n\n if (sub === undefined) return { flowId, ...repeat, opaque: 'unresolved' };\n // A sub-flow that names an ancestor by id would otherwise recurse forever.\n if (drawing.includes(flowId)) return { flowId, ...repeat, opaque: 'cycle' };\n if (drawing.length >= MAX_DEPTH) return { flowId, ...repeat, opaque: 'too-deep' };\n\n return { flowId, ...repeat, graph: build(sub, subFlows, [...drawing, flowId]) };\n}\n\nfunction explicitEdges(\n entries: readonly (readonly [string, StepDef])[],\n flow: FlowDefinition\n): GraphEdge[] {\n const edges: GraphEdge[] = [];\n for (const [id, step] of entries) {\n const next = step.on?.next;\n if (next !== undefined) {\n const targets: readonly Target[] = Array.isArray(next) ? next : [next as Target];\n for (const t of targets) edges.push(toEdge(id, t, 'next', flow));\n }\n\n const back = step.on?.back;\n // `auto` is the resolver walking `order` backwards at run time against the\n // state of the moment. There is no fixed target, and drawing a guess would\n // be worse than drawing nothing.\n if (back !== undefined && back !== 'auto') edges.push(toEdge(id, back, 'back', flow));\n }\n return edges;\n}\n\n/**\n * The default path: a step with no `on.next` falls through to the next entry in\n * `order` whose `when` passes, and ends the flow when nothing is left.\n *\n * Which is why one edge per pair is not enough. Statically we cannot know which\n * `when` holds, so every step up to and including the first unconditional one\n * is genuinely reachable from here, and a lone edge to the immediate neighbour\n * would hide the skip that `when` exists to express. By the same argument, a\n * tail of conditional steps can all be false, so the flow can end there too.\n *\n * The edge to `END` carries no `when`. Expressing it would mean negating every\n * guard it skipped past, and a synthesized compound predicate on a drawing is\n * harder to trust than an unlabelled fallback arrow.\n */\nfunction fallThroughEdges(flow: FlowDefinition): GraphEdge[] {\n const walk = flow.order ?? Object.keys(flow.steps);\n const edges: GraphEdge[] = [];\n\n for (const [from, step] of Object.entries(flow.steps)) {\n if (step.on?.next !== undefined) continue;\n\n const at = walk.indexOf(from);\n // Absent from `order`, so the resolver has nothing to walk and ends the\n // flow -- the same `END` it returns past the last ordered step. Drawing\n // nothing here would leave the step looking like a dead end on a graph\n // whose whole job is to show where a flow goes.\n if (at === -1) {\n edges.push({ from, to: END, kind: 'order' });\n continue;\n }\n\n let ends = true;\n for (let j = at + 1; j < walk.length; j++) {\n const to = walk[j];\n if (to === undefined) continue;\n const target = flow.steps[to];\n if (target === undefined) continue;\n\n edges.push({\n from,\n to,\n kind: 'order',\n ...(target.when !== undefined && { when: target.when }),\n });\n if (target.when === undefined) {\n ends = false;\n break;\n }\n }\n if (ends) edges.push({ from, to: END, kind: 'order' });\n }\n return edges;\n}\n\nfunction toEdge(from: string, t: Target, kind: 'next' | 'back', flow: FlowDefinition): GraphEdge {\n const to = typeof t === 'string' ? t : t.to;\n const when = typeof t === 'string' ? undefined : t.when;\n return {\n from,\n to,\n kind,\n ...(when !== undefined && { when }),\n // A branch pointing at nothing is a flow bug. It stays on the graph so it\n // can be seen, rather than vanishing into a step that has one fewer arrow.\n ...(to !== END && !(to in flow.steps) && { dangling: true }),\n };\n}\n"]}
@@ -1,8 +1,77 @@
1
1
  'use strict';
2
2
 
3
+ // src/v1/diagnostic.ts
4
+ var DOCS = "https://zizzx.github.io/wizzard-packages/errors/";
5
+ var explain = (code, [what, why, fix]) => `[wizzard] ${what}. ${why}. ${fix}. ${DOCS}${code}`;
6
+ var WizardError = class extends Error {
7
+ /**
8
+ * Every core entry is bundled on its own, so `/v1`, `/groups` and
9
+ * `/validate-flow` each carry a copy of this class, and so does a second
10
+ * install of the package. Identity is the shape, not the constructor, so
11
+ * `instanceof` holds across all of them.
12
+ */
13
+ static [Symbol.hasInstance](value) {
14
+ return value instanceof Error && value.name === "WizardError" && typeof value.code === "string";
15
+ }
16
+ constructor(code, op, what, why, fix, path) {
17
+ const url = DOCS + code;
18
+ super(`[wizzard] ${what}. ${why}. ${fix}. ${url}`);
19
+ this.name = "WizardError";
20
+ this.code = code;
21
+ this.op = op;
22
+ this.path = path;
23
+ this.fix = fix;
24
+ this.url = url;
25
+ }
26
+ };
27
+ var notRegistered = (ref, op, path) => new WizardError("resolver-not-registered", op, ...notRegisteredText(ref), path);
28
+ var notRegisteredText = (ref) => [
29
+ `no resolver is registered as "${ref}"`,
30
+ "The flow names it in a $ref, and the registry it is evaluated against has no entry by that name",
31
+ `Add ${ref} to the registry, or correct the name in the flow`
32
+ ];
33
+ var MAX_EXPR_DEPTH = 256;
34
+ var tooDeepText = [
35
+ `an expression nests deeper than ${MAX_EXPR_DEPTH} levels`,
36
+ "Each level is a call on the stack, and a deeper one would overflow it",
37
+ "Flatten it: $and and $or take any number of operands"
38
+ ];
39
+ var invalidOperandText = (op, v) => {
40
+ const want = op === "$get" || op === "$ref" ? typeof v === "string" || "a string" : op === "$and" || op === "$or" ? Array.isArray(v) || "a list" : op === "$not" || op === "$empty" || Array.isArray(v) && v.length === 2 || "a list of two operands";
41
+ if (want === true) return void 0;
42
+ const got = v == null ? String(v) : Array.isArray(v) ? `a list of ${v.length}` : typeof v === "object" ? "an object" : `a ${typeof v}`;
43
+ return [
44
+ `${op} takes ${want}, not ${got}`,
45
+ "Any other shape fails inside the evaluator with no reason given",
46
+ `Give ${op} ${want}`
47
+ ];
48
+ };
49
+ var unknownOperatorText = (first, key = first ?? "{}") => [
50
+ `"${key}" is not an operator`,
51
+ "An expression object names its operation with a key, and none of its keys is an operator",
52
+ `Replace ${key} with an operator from the expressions guide, or check it for a typo`
53
+ ];
54
+
3
55
  // src/v1/expr.ts
4
- var ExprError = class extends Error {
56
+ var unknownOperator = (e, op) => new WizardError("expr-unknown-operator", op, ...unknownOperatorText(Object.keys(e)[0]));
57
+ var operand = (e, op, where) => {
58
+ const v = e[op];
59
+ const text = invalidOperandText(op, v);
60
+ if (text) throw new WizardError("expr-invalid-operand", where, ...text);
61
+ return v;
5
62
  };
63
+ var PAIRS = ["$eq", "$ne", "$gt", "$gte", "$lt", "$lte", "$in"];
64
+ function compare(op, a, b) {
65
+ if (op === "$eq") return a === b;
66
+ if (op === "$ne") return a !== b;
67
+ if (op === "$gt") return a > b;
68
+ if (op === "$gte") return a >= b;
69
+ if (op === "$lt") return a < b;
70
+ if (op === "$lte") return a <= b;
71
+ if (typeof b === "string") return b.includes(String(a));
72
+ return Array.isArray(b) && b.includes(a);
73
+ }
74
+ var tooDeep = (op) => new WizardError("expr-too-deep", op, ...tooDeepText);
6
75
  var isNode = (e) => typeof e === "object" && e !== null && !Array.isArray(e);
7
76
  function read(path, scope) {
8
77
  const dot = path.indexOf(".");
@@ -26,33 +95,42 @@ function empty(v) {
26
95
  return false;
27
96
  }
28
97
  function evaluate(e, scope, registry) {
29
- if (!isNode(e)) return Array.isArray(e) ? e.map((x) => evaluate(x, scope, registry)) : e;
30
- const ev = (x) => evaluate(x, scope, registry);
31
- if ("$get" in e) return read(e.$get, scope);
98
+ return run(e, scope, registry, 0);
99
+ }
100
+ function run(e, scope, registry, depth) {
101
+ if (typeof e !== "object" || e === null) return e;
102
+ if (depth >= MAX_EXPR_DEPTH) throw tooDeep("evaluate");
103
+ const ev = (x) => run(x, scope, registry, depth + 1);
104
+ if (!isNode(e)) return e.map(ev);
105
+ const ev2 = (x) => run(x, scope, registry, depth + 2);
106
+ if ("$get" in e) return read(operand(e, "$get", "evaluate"), scope);
32
107
  if ("$not" in e) return !ev(e.$not);
33
- if ("$and" in e) return e.$and.every(ev);
34
- if ("$or" in e) return e.$or.some(ev);
108
+ if ("$and" in e) return operand(e, "$and", "evaluate").every(ev2);
109
+ if ("$or" in e) return operand(e, "$or", "evaluate").some(ev2);
35
110
  if ("$empty" in e) return empty(ev(e.$empty));
36
- if ("$eq" in e) return ev(e.$eq[0]) === ev(e.$eq[1]);
37
- if ("$ne" in e) return ev(e.$ne[0]) !== ev(e.$ne[1]);
38
- if ("$gt" in e) return ev(e.$gt[0]) > ev(e.$gt[1]);
39
- if ("$gte" in e) return ev(e.$gte[0]) >= ev(e.$gte[1]);
40
- if ("$lt" in e) return ev(e.$lt[0]) < ev(e.$lt[1]);
41
- if ("$lte" in e) return ev(e.$lte[0]) <= ev(e.$lte[1]);
42
- if ("$in" in e) {
43
- const needle = ev(e.$in[0]);
44
- const hay = ev(e.$in[1]);
45
- if (typeof hay === "string") return hay.includes(String(needle));
46
- return Array.isArray(hay) && hay.includes(needle);
111
+ const op = PAIRS.find((k) => k in e);
112
+ if (op) {
113
+ const [a, b] = operand(e, op, "evaluate");
114
+ const left = ev2(a);
115
+ return compare(op, left, ev2(b));
47
116
  }
48
117
  if ("$ref" in e) {
49
- const fn = registry?.[e.$ref];
50
- if (!fn) throw new ExprError(`unknown resolver: ${e.$ref}`);
118
+ const name = operand(e, "$ref", "evaluate");
119
+ const fn = registry?.[name];
120
+ if (!fn) throw notRegistered(name, "evaluate");
51
121
  const out = fn(e.args, scope);
52
- if (out instanceof Promise) throw new ExprError(`resolver is async: ${e.$ref}`);
122
+ if (out instanceof Promise) {
123
+ throw new WizardError(
124
+ "resolver-is-async",
125
+ "evaluate",
126
+ `resolver "${name}" returned a promise`,
127
+ "This expression is evaluated synchronously - a when, a transition guard or a repeat source - and cannot wait for it",
128
+ `Make ${name} synchronous, or move the asynchronous work into the step's validate or load`
129
+ );
130
+ }
53
131
  return out;
54
132
  }
55
- throw new ExprError(`unknown operator: ${Object.keys(e)[0]}`);
133
+ throw unknownOperator(e, "evaluate");
56
134
  }
57
135
  function test(e, scope, registry) {
58
136
  return e === void 0 ? true : Boolean(evaluate(e, scope, registry));
@@ -75,6 +153,29 @@ function getPath(source, path) {
75
153
  }
76
154
 
77
155
  // src/v1/resolve.ts
156
+ var reported = /* @__PURE__ */ new WeakMap();
157
+ var holds = (flow, when, scope, registry, at) => {
158
+ try {
159
+ return test(when, scope, registry);
160
+ } catch (error) {
161
+ const seen = reported.get(flow) ?? /* @__PURE__ */ new Set();
162
+ reported.set(flow, seen);
163
+ const once = `${at}
164
+ ${String(error)}`;
165
+ if (!seen.has(once)) {
166
+ seen.add(once);
167
+ console.error(
168
+ explain("when-threw", [
169
+ `the when of ${at} threw, so it is read as false`,
170
+ "A when says whether a step is there, and one that throws would otherwise stop every move",
171
+ "Fix the expression, or run validateFlow on the flow before the wizard is created"
172
+ ]),
173
+ error
174
+ );
175
+ }
176
+ return false;
177
+ }
178
+ };
78
179
  function resolveNext(flow, state, scope, registry) {
79
180
  const current = state.stack[state.stack.length - 1]?.step;
80
181
  if (current === void 0) return firstReachable(flow, scope, registry, 0);
@@ -83,14 +184,18 @@ function resolveNext(flow, state, scope, registry) {
83
184
  const explicit = step2.on?.next;
84
185
  if (explicit !== void 0) {
85
186
  const targets = Array.isArray(explicit) ? explicit : [explicit];
187
+ let closed;
86
188
  for (const t of targets) {
87
189
  const to = typeof t === "string" ? t : t.to;
88
190
  const guard = typeof t === "string" ? void 0 : t.when;
89
- if (!test(guard, scope, registry)) continue;
191
+ if (!holds(flow, guard, scope, registry, `the transition of step "${current}" to "${to}"`))
192
+ continue;
90
193
  if (to === END) return END;
91
- if (flow.steps[to] && test(flow.steps[to].when, scope, registry)) return to;
194
+ if (flow.steps[to] && holds(flow, flow.steps[to].when, scope, registry, `step "${to}"`))
195
+ return to;
196
+ closed ?? (closed = to);
92
197
  }
93
- return END;
198
+ return closed ?? null;
94
199
  }
95
200
  const order = effectiveOrder(flow);
96
201
  const at = order.indexOf(current);
@@ -103,7 +208,7 @@ function firstReachable(flow, scope, registry, from) {
103
208
  const id = order[i];
104
209
  if (id === void 0) continue;
105
210
  const step2 = flow.steps[id];
106
- if (step2 && test(step2.when, scope, registry)) return id;
211
+ if (step2 && holds(flow, step2.when, scope, registry, `step "${id}"`)) return id;
107
212
  }
108
213
  return END;
109
214
  }
@@ -123,21 +228,18 @@ function resolveBack(flow, state, scope, registry) {
123
228
  const id = order[i];
124
229
  if (id === void 0) continue;
125
230
  const step2 = flow.steps[id];
126
- if (step2 && test(step2.when, scope, registry)) return id;
231
+ if (step2 && holds(flow, step2.when, scope, registry, `step "${id}"`)) return id;
127
232
  }
128
233
  return null;
129
234
  }
130
- function reachable(flow, scope, registry) {
131
- const order = effectiveOrder(flow);
132
- return order.filter((id) => {
133
- const step2 = flow.steps[id];
134
- return step2 !== void 0 && test(step2.when, scope, registry);
135
- });
235
+ function enterable(flow, id, scope, registry) {
236
+ const step2 = flow.steps[id];
237
+ return step2 !== void 0 && holds(flow, step2.when, scope, registry, `step "${id}"`);
136
238
  }
137
239
 
138
240
  // src/v1/groups.ts
139
241
  var MAX_DEPTH = 32;
140
- var DOCS = "https://github.com/ZizzX/wizzard-packages/blob/main/docs/errors.md";
242
+ var DOCS2 = "https://zizzx.github.io/wizzard-packages/errors/";
141
243
  var rootScope = (state) => ({ data: state.data, ctx: state.ctx });
142
244
  var stateAt = (state, frame) => ({
143
245
  ...state,
@@ -161,12 +263,12 @@ function itemsOf(id, repeat, scope, registry) {
161
263
  for (let i = 0; i < raw.length; i++) {
162
264
  const value = getPath(raw[i], keyBy);
163
265
  if (value === void 0 || value === null || value === "") {
164
- return `[wizzard] item ${i} of repeat group "${id}" has no key at "${keyBy}". A repeat frame stores the item's key and nothing else, and undefined, null and "" name no item. Give item ${i} a "${keyBy}", or drop keyBy to identify items by position. ${DOCS}#repeat-keys`;
266
+ return `[wizzard] item ${i} of repeat group "${id}" has no key at "${keyBy}". A repeat frame stores the item's key and nothing else, and undefined, null and "" name no item. Give item ${i} a "${keyBy}", or drop keyBy to identify items by position. ${DOCS2}repeat-keys`;
165
267
  }
166
268
  const key = String(value);
167
269
  const first = seen.get(key);
168
270
  if (first !== void 0) {
169
- return `[wizzard] repeat keys collide in group "${id}": "${key}" at ${first} and ${i}. Keys are compared as strings, so 1 and "1" are one key, and a frame naming "${key}" could mean either item. Make "${keyBy}" unique across the items, or drop keyBy to identify items by position. ${DOCS}#repeat-keys`;
271
+ return `[wizzard] repeat keys collide in group "${id}": "${key}" at ${first} and ${i}. Keys are compared as strings, so 1 and "1" are one key, and a frame naming "${key}" could mean either item. Make "${keyBy}" unique across the items, or drop keyBy to identify items by position. ${DOCS2}repeat-keys`;
170
272
  }
171
273
  seen.set(key, i);
172
274
  keys.push(key);
@@ -249,7 +351,7 @@ var notReachable = (id) => ({ ok: false, reason: "not-reachable", by: id });
249
351
  function enter(levels, id, step2, state, registry, subFlows) {
250
352
  const level = levels[levels.length - 1];
251
353
  if (level === void 0) return { kind: "past" };
252
- if (!reachable(level.flow, level.scope, registry).includes(id)) return { kind: "hidden" };
354
+ if (!enterable(level.flow, id, level.scope, registry)) return { kind: "hidden" };
253
355
  const sub = subFlowOf(step2, subFlows);
254
356
  if (sub === void 0) return { kind: "past" };
255
357
  if (levels.some((l) => l.flow.id === sub.id)) return { kind: "refused" };