@sanity/workflow-engine 0.1.0 → 0.2.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.
package/dist/define.js CHANGED
@@ -1,67 +1,53 @@
1
- import { z } from "zod";
2
- const TASK_STATUSES = ["pending", "active", "done", "skipped", "failed"], STATE_SCOPES = ["workflow", "stage", "task"], DOCUMENT_VALUE_PERMISSIONS = ["create", "read", "update"], MUTATION_GUARD_ACTIONS = [
3
- "create",
4
- "update",
5
- "delete",
6
- "publish",
7
- "unpublish"
8
- ];
1
+ import * as v from "valibot";
9
2
  function knownList(ids) {
10
3
  return [...ids].map((s) => `"${s}"`).join(", ");
11
4
  }
12
- function checkStagesAndTasks(def, ctx) {
5
+ function checkStagesAndTasks(def, issues) {
13
6
  const stageIds = /* @__PURE__ */ new Set();
14
7
  for (const [i, stage] of def.stages.entries())
15
- stageIds.has(stage.id) && ctx.addIssue({
16
- code: "custom",
8
+ stageIds.has(stage.id) && issues.push({
17
9
  path: ["stages", i, "id"],
18
10
  message: `duplicate stage id "${stage.id}" (already declared earlier)`
19
- }), stageIds.add(stage.id), stage.kind === "terminal" && stage.transitions && stage.transitions.length > 0 && ctx.addIssue({
20
- code: "custom",
11
+ }), stageIds.add(stage.id), stage.kind === "terminal" && stage.transitions && stage.transitions.length > 0 && issues.push({
21
12
  path: ["stages", i, "transitions"],
22
13
  message: `terminal stage "${stage.id}" must not declare transitions`
23
- }), checkDuplicateTaskIds(stage, i, ctx);
14
+ }), checkDuplicateTaskIds(stage, i, issues);
24
15
  return stageIds;
25
16
  }
26
- function checkDuplicateTaskIds(stage, i, ctx) {
17
+ function checkDuplicateTaskIds(stage, i, issues) {
27
18
  const taskIds = /* @__PURE__ */ new Set();
28
19
  for (const [j, task] of (stage.tasks ?? []).entries())
29
- taskIds.has(task.id) && ctx.addIssue({
30
- code: "custom",
20
+ taskIds.has(task.id) && issues.push({
31
21
  path: ["stages", i, "tasks", j, "id"],
32
22
  message: `duplicate task id "${task.id}" in stage "${stage.id}"`
33
23
  }), taskIds.add(task.id);
34
24
  }
35
- function checkInitialStage(def, stageIds, ctx) {
36
- stageIds.has(def.initialStageId) || ctx.addIssue({
37
- code: "custom",
25
+ function checkInitialStage(def, stageIds, issues) {
26
+ stageIds.has(def.initialStageId) || issues.push({
38
27
  path: ["initialStageId"],
39
28
  message: `initialStageId "${def.initialStageId}" is not a declared stage. Known stages: ${knownList(stageIds)}`
40
29
  });
41
30
  }
42
- function checkTransitionTargets(def, stageIds, ctx) {
31
+ function checkTransitionTargets(def, stageIds, issues) {
43
32
  for (const [i, stage] of def.stages.entries())
44
33
  for (const [k, t] of (stage.transitions ?? []).entries())
45
- stageIds.has(t.to) || ctx.addIssue({
46
- code: "custom",
34
+ stageIds.has(t.to) || issues.push({
47
35
  path: ["stages", i, "transitions", k, "to"],
48
36
  message: `transition target "${t.to}" is not a declared stage. Known stages: ${knownList(stageIds)}`
49
37
  });
50
38
  }
51
- function collectPredicateIds(def, ctx) {
39
+ function collectPredicateIds(def, issues) {
52
40
  const predicateIds = /* @__PURE__ */ new Set();
53
41
  for (const [i, p] of (def.predicates ?? []).entries())
54
- predicateIds.has(p.id) && ctx.addIssue({
55
- code: "custom",
42
+ predicateIds.has(p.id) && issues.push({
56
43
  path: ["predicates", i, "id"],
57
44
  message: `duplicate predicate id "${p.id}"`
58
45
  }), predicateIds.add(p.id);
59
46
  return predicateIds;
60
47
  }
61
- function checkFilterRefs(def, predicateIds, ctx) {
48
+ function checkFilterRefs(def, predicateIds, issues) {
62
49
  const visit = (g, path) => {
63
- g === void 0 || typeof g == "string" || predicateIds.has(g.ref) || ctx.addIssue({
64
- code: "custom",
50
+ g === void 0 || typeof g == "string" || predicateIds.has(g.ref) || issues.push({
65
51
  path: [...path, "ref"],
66
52
  message: `Filter references predicate "${g.ref}" which is not declared. Known predicates: ${knownList(predicateIds) || "(none)"}`
67
53
  });
@@ -77,112 +63,127 @@ function checkFilterRefs(def, predicateIds, ctx) {
77
63
  visit(t.filter, ["stages", i, "transitions", k, "filter"]);
78
64
  }
79
65
  }
80
- function checkWorkflowInvariants(def, ctx) {
81
- const stageIds = checkStagesAndTasks(def, ctx);
82
- checkInitialStage(def, stageIds, ctx), checkTransitionTargets(def, stageIds, ctx);
83
- const predicateIds = collectPredicateIds(def, ctx);
84
- checkFilterRefs(def, predicateIds, ctx);
66
+ function checkWorkflowInvariants(def) {
67
+ const issues = [], stageIds = checkStagesAndTasks(def, issues);
68
+ checkInitialStage(def, stageIds, issues), checkTransitionTargets(def, stageIds, issues);
69
+ const predicateIds = collectPredicateIds(def, issues);
70
+ return checkFilterRefs(def, predicateIds, issues), issues;
85
71
  }
86
- const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.object({
87
- id: z.string().regex(
88
- /^(dataset|canvas|media-library|dashboard):/,
89
- "must be a GDR URI of form '<scheme>:<...id-parts>' where scheme is one of: dataset, canvas, media-library, dashboard"
72
+ const TASK_STATUSES = ["pending", "active", "done", "skipped", "failed"], STATE_SCOPES = ["workflow", "stage", "task"], DOCUMENT_VALUE_PERMISSIONS = ["create", "read", "update"], MUTATION_GUARD_ACTIONS = [
73
+ "create",
74
+ "update",
75
+ "delete",
76
+ "publish",
77
+ "unpublish"
78
+ ], NonEmpty = v.pipe(v.string(), v.minLength(1, "must be a non-empty string")), PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1));
79
+ function picklist(options) {
80
+ return v.picklist(
81
+ options,
82
+ `Invalid option: expected one of ${options.map((o) => `"${o}"`).join("|")}`
83
+ );
84
+ }
85
+ const GdrSchema = v.strictObject({
86
+ id: v.pipe(
87
+ v.string(),
88
+ v.regex(
89
+ /^(dataset|canvas|media-library|dashboard):/,
90
+ "must be a GDR URI of form '<scheme>:<...id-parts>' where scheme is one of: dataset, canvas, media-library, dashboard"
91
+ )
90
92
  ),
91
93
  type: NonEmpty
92
- }).strict(), SourceSchema = z.lazy(
93
- () => z.union([
94
- z.object({
95
- source: z.literal("literal"),
96
- value: z.union([z.string(), z.number(), z.boolean(), z.null()])
97
- }).strict(),
98
- z.object({ source: z.literal("param"), paramId: NonEmpty }).strict(),
99
- z.object({ source: z.literal("actor") }).strict(),
100
- z.object({ source: z.literal("now") }).strict(),
101
- z.object({ source: z.literal("self") }).strict(),
102
- z.object({ source: z.literal("stageId") }).strict(),
103
- z.object({
104
- source: z.literal("stateRead"),
105
- scope: z.enum(STATE_SCOPES),
94
+ }), SourceSchema = v.lazy(
95
+ () => v.union([
96
+ v.strictObject({
97
+ source: v.literal("literal"),
98
+ value: v.union([v.string(), v.number(), v.boolean(), v.null()])
99
+ }),
100
+ v.strictObject({ source: v.literal("param"), paramId: NonEmpty }),
101
+ v.strictObject({ source: v.literal("actor") }),
102
+ v.strictObject({ source: v.literal("now") }),
103
+ v.strictObject({ source: v.literal("self") }),
104
+ v.strictObject({ source: v.literal("stageId") }),
105
+ v.strictObject({
106
+ source: v.literal("stateRead"),
107
+ scope: picklist(STATE_SCOPES),
106
108
  slotId: NonEmpty,
107
- path: z.string().optional()
108
- }).strict(),
109
+ path: v.optional(v.string())
110
+ }),
109
111
  // effectsContext lookup — reads from `instance.effectsContext[id === ID].value`.
110
112
  // Used by chained effects where one handler's output feeds another's binding.
111
- z.object({
112
- source: z.literal("effectOutput"),
113
+ v.strictObject({
114
+ source: v.literal("effectOutput"),
113
115
  contextId: NonEmpty,
114
- path: z.string().optional()
115
- }).strict(),
116
- z.object({ source: z.literal("row"), path: z.string().optional() }).strict(),
117
- z.object({
118
- source: z.literal("parentState"),
116
+ path: v.optional(v.string())
117
+ }),
118
+ v.strictObject({ source: v.literal("row"), path: v.optional(v.string()) }),
119
+ v.strictObject({
120
+ source: v.literal("parentState"),
119
121
  slotId: NonEmpty,
120
- path: z.string().optional()
121
- }).strict(),
122
+ path: v.optional(v.string())
123
+ }),
122
124
  // Compound object source — resolves each field's Source recursively
123
125
  // and packages the result into a JSON object. Lets ops append rich
124
126
  // rows to `checklist` / `notes` / `assignees` slots, e.g.
125
127
  // `{ source: "object", fields: { signer: { source: "actor" },
126
128
  // at: { source: "now" } } }`.
127
- z.object({
128
- source: z.literal("object"),
129
- fields: z.record(NonEmpty, SourceSchema)
130
- }).strict()
129
+ v.strictObject({
130
+ source: v.literal("object"),
131
+ fields: v.record(NonEmpty, SourceSchema)
132
+ })
131
133
  ])
132
- ), ScopeKeySchema = z.object({
133
- scope: z.enum(STATE_SCOPES),
134
+ ), ScopeKeySchema = v.strictObject({
135
+ scope: picklist(STATE_SCOPES),
134
136
  slotId: NonEmpty
135
- }).strict(), OpPredicateSchema = z.lazy(
136
- () => z.union([
137
- z.object({
137
+ }), OpPredicateSchema = v.lazy(
138
+ () => v.union([
139
+ v.strictObject({
138
140
  field: NonEmpty,
139
141
  equals: SourceSchema
140
- }).strict(),
141
- z.object({ all: z.array(OpPredicateSchema) }).strict(),
142
- z.object({ any: z.array(OpPredicateSchema) }).strict()
142
+ }),
143
+ v.strictObject({ all: v.array(OpPredicateSchema) }),
144
+ v.strictObject({ any: v.array(OpPredicateSchema) })
143
145
  ])
144
- ), OpSchema = z.discriminatedUnion("type", [
145
- z.object({
146
- type: z.literal("workflow.op.state.set"),
146
+ ), OpSchema = v.variant("type", [
147
+ v.strictObject({
148
+ type: v.literal("workflow.op.state.set"),
147
149
  target: ScopeKeySchema,
148
150
  value: SourceSchema
149
- }).strict(),
150
- z.object({
151
- type: z.literal("workflow.op.state.append"),
151
+ }),
152
+ v.strictObject({
153
+ type: v.literal("workflow.op.state.append"),
152
154
  target: ScopeKeySchema,
153
155
  item: SourceSchema
154
- }).strict(),
155
- z.object({
156
- type: z.literal("workflow.op.state.updateWhere"),
156
+ }),
157
+ v.strictObject({
158
+ type: v.literal("workflow.op.state.updateWhere"),
157
159
  target: ScopeKeySchema,
158
160
  where: OpPredicateSchema,
159
- merge: z.record(NonEmpty, SourceSchema)
160
- }).strict(),
161
- z.object({
162
- type: z.literal("workflow.op.state.removeWhere"),
161
+ merge: v.record(NonEmpty, SourceSchema)
162
+ }),
163
+ v.strictObject({
164
+ type: v.literal("workflow.op.state.removeWhere"),
163
165
  target: ScopeKeySchema,
164
166
  where: OpPredicateSchema
165
- }).strict(),
166
- z.object({
167
- type: z.literal("workflow.op.status.set"),
167
+ }),
168
+ v.strictObject({
169
+ type: v.literal("workflow.op.status.set"),
168
170
  taskId: NonEmpty,
169
- status: z.enum(TASK_STATUSES)
170
- }).strict()
171
- ]), StateSlotSourceSchema = z.union([
172
- z.object({ kind: z.literal("init") }).strict(),
173
- z.object({
174
- kind: z.literal("query"),
171
+ status: picklist(TASK_STATUSES)
172
+ })
173
+ ]), StateSlotSourceSchema = v.union([
174
+ v.strictObject({ kind: v.literal("init") }),
175
+ v.strictObject({
176
+ kind: v.literal("query"),
175
177
  query: NonEmpty,
176
- resource: z.union([z.literal("workflow"), z.literal("subject"), GdrSchema]).optional()
177
- }).strict(),
178
- z.object({ kind: z.literal("write") }).strict(),
179
- z.object({ kind: z.literal("computed"), compute: NonEmpty }).strict(),
178
+ resource: v.optional(v.union([v.literal("workflow"), v.literal("subject"), GdrSchema]))
179
+ }),
180
+ v.strictObject({ kind: v.literal("write") }),
180
181
  /**
181
182
  * Pre-populate the slot at resolution time with a constant value.
182
183
  * Useful for stage- or task-scope slots that should start non-empty
183
184
  * (e.g. a default headline, a starting tally).
184
185
  */
185
- z.object({ kind: z.literal("literal"), value: z.unknown() }).strict(),
186
+ v.strictObject({ kind: v.literal("literal"), value: v.unknown() }),
186
187
  /**
187
188
  * Pre-populate by reading another already-resolved slot. `scope:
188
189
  * "workflow"` reads from the workflow-scope `state[]` (resolved at
@@ -191,18 +192,13 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
191
192
  * Optional `path` selects a nested field (e.g. `path: "id"` on a
192
193
  * doc.ref slot).
193
194
  */
194
- z.object({
195
- kind: z.literal("stateRead"),
196
- scope: z.union([z.literal("workflow"), z.literal("stage")]),
195
+ v.strictObject({
196
+ kind: v.literal("stateRead"),
197
+ scope: v.union([v.literal("workflow"), v.literal("stage")]),
197
198
  slotId: NonEmpty,
198
- path: z.string().optional()
199
- }).strict()
200
- ]), StateSlotBase = {
201
- id: NonEmpty,
202
- title: z.string().optional(),
203
- description: z.string().optional(),
204
- source: StateSlotSourceSchema
205
- }, StateSlotKindSchema = z.enum([
199
+ path: v.optional(v.string())
200
+ })
201
+ ]), StateSlotKindSchema = picklist([
206
202
  "workflow.state.doc.ref",
207
203
  "workflow.state.doc.refs",
208
204
  // Release reference. A workflow declares this slot to say "I target a
@@ -220,47 +216,48 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
220
216
  "workflow.state.checklist",
221
217
  "workflow.state.notes",
222
218
  "workflow.state.assignees"
223
- ]), StateSlotSchema = z.discriminatedUnion(
224
- "type",
225
- StateSlotKindSchema.options.map(
226
- (kind) => z.object({ type: z.literal(kind), ...StateSlotBase }).strict()
227
- )
228
- ), FilterSchema = z.union([
219
+ ]), StateSlotSchema = v.strictObject({
220
+ type: StateSlotKindSchema,
221
+ id: NonEmpty,
222
+ title: v.optional(v.string()),
223
+ description: v.optional(v.string()),
224
+ source: StateSlotSourceSchema
225
+ }), FilterSchema = v.union([
229
226
  // Inline GROQ — only reserved params allowed at runtime.
230
227
  NonEmpty,
231
228
  // Reference to a named predicate, optionally with named args.
232
- z.object({
229
+ v.strictObject({
233
230
  ref: NonEmpty,
234
- args: z.record(z.string(), z.unknown()).optional()
235
- }).strict()
236
- ]), PredicateParamSchema = z.object({
231
+ args: v.optional(v.record(v.string(), v.unknown()))
232
+ })
233
+ ]), PredicateParamSchema = v.strictObject({
237
234
  id: NonEmpty,
238
- title: z.string().optional(),
239
- description: z.string().optional(),
240
- type: z.enum(["string", "number", "boolean", "string[]", "reference"]),
241
- enum: z.array(z.string()).optional()
242
- }).strict(), PredicateSchema = z.object({
243
- type: z.literal("workflow.predicate").optional(),
235
+ title: v.optional(v.string()),
236
+ description: v.optional(v.string()),
237
+ type: picklist(["string", "number", "boolean", "string[]", "reference"]),
238
+ enum: v.optional(v.array(v.string()))
239
+ }), PredicateSchema = v.strictObject({
240
+ type: v.optional(v.literal("workflow.predicate")),
244
241
  id: NonEmpty,
245
- title: z.string().optional(),
246
- description: z.string().optional(),
242
+ title: v.optional(v.string()),
243
+ description: v.optional(v.string()),
247
244
  groq: NonEmpty,
248
- params: z.array(PredicateParamSchema).optional()
249
- }).strict(), EffectSchema = z.object({
250
- type: z.literal("workflow.effect").optional(),
245
+ params: v.optional(v.array(PredicateParamSchema))
246
+ }), EffectSchema = v.strictObject({
247
+ type: v.optional(v.literal("workflow.effect")),
251
248
  id: NonEmpty,
252
- title: z.string().optional(),
253
- description: z.string().optional(),
249
+ title: v.optional(v.string()),
250
+ description: v.optional(v.string()),
254
251
  // Bindings resolve typed Source values at commit time, producing
255
252
  // concrete JSON in the queued effect's `params`. NO GROQ.
256
- bindings: z.record(z.string(), SourceSchema).optional(),
257
- input: z.record(z.string(), z.unknown()).optional()
258
- }).strict(), TerminalTaskStatus = z.enum(["done", "skipped", "failed"]), PermissionSchema = z.enum(DOCUMENT_VALUE_PERMISSIONS), ActionParamSchema = z.object({
259
- type: z.literal("workflow.action.param").optional(),
253
+ bindings: v.optional(v.record(v.string(), SourceSchema)),
254
+ input: v.optional(v.record(v.string(), v.unknown()))
255
+ }), TerminalTaskStatus = picklist(["done", "skipped", "failed"]), PermissionSchema = picklist(DOCUMENT_VALUE_PERMISSIONS), ActionParamSchema = v.strictObject({
256
+ type: v.optional(v.literal("workflow.action.param")),
260
257
  id: NonEmpty,
261
- title: z.string().optional(),
262
- description: z.string().optional(),
263
- paramType: z.enum([
258
+ title: v.optional(v.string()),
259
+ description: v.optional(v.string()),
260
+ paramType: picklist([
264
261
  "string",
265
262
  "number",
266
263
  "boolean",
@@ -271,15 +268,15 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
271
268
  "doc.refs",
272
269
  "json"
273
270
  ]),
274
- required: z.boolean().optional()
275
- }).strict(), ActionSchema = z.object({
276
- type: z.literal("workflow.action").optional(),
271
+ required: v.optional(v.boolean())
272
+ }), ActionSchema = v.strictObject({
273
+ type: v.optional(v.literal("workflow.action")),
277
274
  id: NonEmpty,
278
- title: z.string().optional(),
279
- description: z.string().optional(),
280
- filter: FilterSchema.optional(),
275
+ title: v.optional(v.string()),
276
+ description: v.optional(v.string()),
277
+ filter: v.optional(FilterSchema),
281
278
  /** Caller-supplied params, validated before ops/effects run. */
282
- params: z.array(ActionParamSchema).optional(),
279
+ params: v.optional(v.array(ActionParamSchema)),
283
280
  /**
284
281
  * Status the task flips to when this action fires. Omit to leave
285
282
  * the task status unchanged — used by intermediate `claim` /
@@ -289,7 +286,7 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
289
286
  * mid-flip back to `active` outside of the `workflow.op.status.set`
290
287
  * op explicit path.
291
288
  */
292
- setStatus: TerminalTaskStatus.optional(),
289
+ setStatus: v.optional(TerminalTaskStatus),
293
290
  /**
294
291
  * Inline state-mutation primitives executed during the action commit.
295
292
  * Each op runs against the in-memory mutation (deterministic, no
@@ -297,72 +294,74 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
297
294
  * op applies. Logged on `history[]` as `workflow.history.opApplied`
298
295
  * and surfaced on the `ActionResult.ranOps`.
299
296
  */
300
- ops: z.array(OpSchema).optional(),
301
- effects: z.array(EffectSchema).optional(),
297
+ ops: v.optional(v.array(OpSchema)),
298
+ effects: v.optional(v.array(EffectSchema)),
302
299
  /**
303
300
  * Role gating — OR-match against `Actor.roles[]`. If omitted, no role
304
301
  * gate. `"*"` in Actor.roles always matches (bench / all-access).
305
302
  */
306
- roles: z.array(NonEmpty).optional(),
303
+ roles: v.optional(v.array(NonEmpty)),
307
304
  /**
308
305
  * When true, the actor must appear in the task's `assignees[]` (as a
309
306
  * user-kind assignee matching by id) for the action to be allowed.
310
307
  */
311
- requireAssignment: z.boolean().optional(),
308
+ requireAssignment: v.optional(v.boolean()),
312
309
  /**
313
310
  * Which permission on the workflow.instance document the actor must
314
311
  * hold (per supplied `grants`). Defaults to "update". Only checked
315
312
  * when grants are supplied at evaluate/fireAction time.
316
313
  */
317
- requiredPermission: PermissionSchema.optional()
318
- }).strict(), CompletionPolicySchema = z.union([
319
- z.object({ kind: z.literal("all") }).strict(),
320
- z.object({ kind: z.literal("any") }).strict(),
321
- z.object({
322
- kind: z.literal("count"),
323
- n: z.number().int().positive()
324
- }).strict()
325
- ]), LogicalDefinitionRefSchema = z.object({
314
+ requiredPermission: v.optional(PermissionSchema)
315
+ }), CompletionPolicySchema = v.union([
316
+ v.strictObject({ kind: v.literal("all") }),
317
+ v.strictObject({ kind: v.literal("any") }),
318
+ v.strictObject({
319
+ kind: v.literal("count"),
320
+ n: PositiveInt
321
+ })
322
+ ]), LogicalDefinitionRefSchema = v.strictObject({
326
323
  workflowId: NonEmpty,
327
- version: z.union([z.number().int().positive(), z.literal("latest")]).optional()
328
- }).strict(), SpawnForEachSchema = z.object({
324
+ version: v.optional(v.union([PositiveInt, v.literal("latest")]))
325
+ }), SpawnForEachSchema = v.strictObject({
329
326
  groq: NonEmpty,
330
- as: z.object({
331
- initialState: z.array(
332
- z.object({
333
- type: StateSlotKindSchema,
334
- id: NonEmpty,
335
- value: SourceSchema
336
- }).strict()
337
- ).optional(),
338
- effectsContext: z.record(z.string(), SourceSchema).optional()
339
- }).strict()
340
- }).strict(), SpawnSchema = z.object({
341
- type: z.literal("workflow.spawn").optional(),
342
- id: z.string().optional(),
343
- title: z.string().optional(),
344
- description: z.string().optional(),
327
+ as: v.strictObject({
328
+ initialState: v.optional(
329
+ v.array(
330
+ v.strictObject({
331
+ type: StateSlotKindSchema,
332
+ id: NonEmpty,
333
+ value: SourceSchema
334
+ })
335
+ )
336
+ ),
337
+ effectsContext: v.optional(v.record(v.string(), SourceSchema))
338
+ })
339
+ }), SpawnSchema = v.strictObject({
340
+ type: v.optional(v.literal("workflow.spawn")),
341
+ id: v.optional(v.string()),
342
+ title: v.optional(v.string()),
343
+ description: v.optional(v.string()),
345
344
  definitionRef: LogicalDefinitionRefSchema,
346
345
  forEach: SpawnForEachSchema,
347
- completionPolicy: CompletionPolicySchema.optional()
348
- }).strict(), AssigneeSchema = z.discriminatedUnion("kind", [
349
- z.object({ kind: z.literal("user"), id: NonEmpty }).strict(),
350
- z.object({ kind: z.literal("role"), role: NonEmpty }).strict()
351
- ]), TaskSchema = z.object({
352
- type: z.literal("workflow.task").optional(),
346
+ completionPolicy: v.optional(CompletionPolicySchema)
347
+ }), AssigneeSchema = v.variant("kind", [
348
+ v.strictObject({ kind: v.literal("user"), id: NonEmpty }),
349
+ v.strictObject({ kind: v.literal("role"), role: NonEmpty })
350
+ ]), TaskSchema = v.strictObject({
351
+ type: v.optional(v.literal("workflow.task")),
353
352
  id: NonEmpty,
354
- title: z.string().optional(),
355
- description: z.string().optional(),
356
- assignees: z.array(AssigneeSchema).optional(),
357
- filter: FilterSchema.optional(),
358
- invokeOn: z.enum(["stageEnter", "manual"]).optional(),
353
+ title: v.optional(v.string()),
354
+ description: v.optional(v.string()),
355
+ assignees: v.optional(v.array(AssigneeSchema)),
356
+ filter: v.optional(FilterSchema),
357
+ invokeOn: v.optional(picklist(["stageEnter", "manual"])),
359
358
  /**
360
359
  * Auto-completion predicate — same shape as `transition.filter`. When
361
360
  * truthy at activation or on any subsequent cascade, the engine
362
361
  * flips the task to `done` with a `{ kind: "system", id: "engine.completeWhen" }`
363
362
  * actor. A full filter, not just a wall-clock deadline.
364
363
  */
365
- completeWhen: FilterSchema.optional(),
364
+ completeWhen: v.optional(FilterSchema),
366
365
  /**
367
366
  * Auto-failure predicate — symmetric to `completeWhen`. Same shape,
368
367
  * same evaluation cadence (activation + every cascade), but flips
@@ -372,72 +371,75 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
372
371
  * wins — failure is the more notable signal and should not be
373
372
  * silently swallowed.
374
373
  */
375
- failWhen: FilterSchema.optional(),
376
- effects: z.array(EffectSchema).optional(),
377
- actions: z.array(ActionSchema).optional(),
378
- spawns: SpawnSchema.optional(),
379
- contentRefs: z.array(z.unknown()).optional(),
374
+ failWhen: v.optional(FilterSchema),
375
+ effects: v.optional(v.array(EffectSchema)),
376
+ actions: v.optional(v.array(ActionSchema)),
377
+ spawns: v.optional(SpawnSchema),
378
+ contentRefs: v.optional(v.array(v.unknown())),
380
379
  /** Task-scoped state slots. Resolved at task activation time. */
381
- state: z.array(StateSlotSchema).optional()
382
- }).strict(), TransitionSchema = z.object({
383
- type: z.literal("workflow.transition").optional(),
380
+ state: v.optional(v.array(StateSlotSchema))
381
+ }), TransitionSchema = v.strictObject({
382
+ type: v.optional(v.literal("workflow.transition")),
384
383
  id: NonEmpty,
385
- title: z.string().optional(),
386
- description: z.string().optional(),
384
+ title: v.optional(v.string()),
385
+ description: v.optional(v.string()),
387
386
  to: NonEmpty,
388
- on: z.string().optional(),
389
- filter: FilterSchema.optional(),
390
- effects: z.array(EffectSchema).optional()
391
- }).strict(), GuardActionSchema = z.enum(MUTATION_GUARD_ACTIONS), GuardMatchSchema = z.object({
387
+ on: v.optional(v.string()),
388
+ filter: v.optional(FilterSchema),
389
+ effects: v.optional(v.array(EffectSchema))
390
+ }), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardMatchSchema = v.strictObject({
392
391
  /** Subject `_type`(s); empty matches any type. */
393
- types: z.array(NonEmpty).optional(),
392
+ types: v.optional(v.array(NonEmpty)),
394
393
  /** Target docs as `Source`s resolved at deploy to bare ids + the resource. */
395
- idRefs: z.array(SourceSchema).optional(),
394
+ idRefs: v.optional(v.array(SourceSchema)),
396
395
  /** Glob id patterns (bare, resource-local). */
397
- idPatterns: z.array(NonEmpty).optional(),
398
- actions: z.array(GuardActionSchema).min(1, "a guard must match at least one action")
399
- }).strict(), GuardSchema = z.object({
400
- name: z.string().optional(),
401
- description: z.string().optional(),
396
+ idPatterns: v.optional(v.array(NonEmpty)),
397
+ actions: v.pipe(
398
+ v.array(GuardActionSchema),
399
+ v.minLength(1, "a guard must match at least one action")
400
+ )
401
+ }), GuardSchema = v.strictObject({
402
+ name: v.optional(v.string()),
403
+ description: v.optional(v.string()),
402
404
  match: GuardMatchSchema,
403
405
  /**
404
406
  * Lake GROQ predicate. Reads `document.before`/`document.after`, `mutation`,
405
407
  * `guard`, and `identity()`. Bare ids/fields only — no GDRs. Omitted or
406
408
  * empty means UNCONDITIONAL DENY.
407
409
  */
408
- predicate: z.string().optional(),
410
+ predicate: v.optional(v.string()),
409
411
  /**
410
412
  * Projected workflow state the predicate reads as `guard.metadata.*`. Values
411
413
  * are `Source`s resolved at deploy and re-synced by the engine. Copied data,
412
414
  * never a cross-resource pointer.
413
415
  */
414
- metadata: z.record(NonEmpty, SourceSchema).optional()
415
- }).strict(), StageKindSchema = z.enum(["initial", "intermediate", "terminal", "waiting"]), StageSchema = z.object({
416
- type: z.literal("workflow.stage").optional(),
416
+ metadata: v.optional(v.record(NonEmpty, SourceSchema))
417
+ }), StageKindSchema = picklist(["initial", "intermediate", "terminal", "waiting"]), StageSchema = v.strictObject({
418
+ type: v.optional(v.literal("workflow.stage")),
417
419
  id: NonEmpty,
418
- title: z.string().optional(),
419
- description: z.string().optional(),
420
- kind: StageKindSchema.optional(),
421
- tasks: z.array(TaskSchema).optional(),
422
- transitions: z.array(TransitionSchema).optional(),
423
- completion: FilterSchema.optional(),
424
- onEnter: z.array(EffectSchema).optional(),
425
- onExit: z.array(EffectSchema).optional(),
420
+ title: v.optional(v.string()),
421
+ description: v.optional(v.string()),
422
+ kind: v.optional(StageKindSchema),
423
+ tasks: v.optional(v.array(TaskSchema)),
424
+ transitions: v.optional(v.array(TransitionSchema)),
425
+ completion: v.optional(FilterSchema),
426
+ onEnter: v.optional(v.array(EffectSchema)),
427
+ onExit: v.optional(v.array(EffectSchema)),
426
428
  /**
427
429
  * Lake mutation guards active while this stage holds. Each compiles to a
428
430
  * `temp.system.guard` doc deployed on stage entry and retracted on exit.
429
431
  * Plural because one stage's intent may span resources (one guard each).
430
432
  */
431
- guards: z.array(GuardSchema).optional(),
432
- contentRefs: z.array(z.unknown()).optional(),
433
+ guards: v.optional(v.array(GuardSchema)),
434
+ contentRefs: v.optional(v.array(v.unknown())),
433
435
  /** Stage-scoped state slots. Resolved at stage entry and persisted on
434
436
  * the StageEntry. */
435
- state: z.array(StateSlotSchema).optional()
436
- }).strict(), WorkflowDefinitionShape = z.object({
437
+ state: v.optional(v.array(StateSlotSchema))
438
+ }), WorkflowDefinitionSchema = v.strictObject({
437
439
  workflowId: NonEmpty,
438
- version: z.number().int().positive(),
440
+ version: PositiveInt,
439
441
  title: NonEmpty,
440
- description: z.string().optional(),
442
+ description: v.optional(v.string()),
441
443
  initialStageId: NonEmpty,
442
444
  /**
443
445
  * Workflow-level state slots. Persist for the instance lifetime.
@@ -445,16 +447,22 @@ const NonEmpty = z.string().min(1, "must be a non-empty string"), GdrSchema = z.
445
447
  * `*[_id == $self][0].state[key == "<key>"][0].value`. Init-sourced
446
448
  * slots are supplied at `startInstance` via `initialState`.
447
449
  */
448
- state: z.array(StateSlotSchema).optional(),
449
- stages: z.array(StageSchema).min(1, "must declare at least one stage"),
450
- predicates: z.array(PredicateSchema).optional()
451
- }).strict(), WorkflowDefinitionSchema = WorkflowDefinitionShape.superRefine(checkWorkflowInvariants);
452
- function formatZodError(error, label) {
453
- const lines = error.issues.map((issue) => ` - ${issue.path.length === 0 ? "(root)" : formatPath(issue.path)}: ${issue.message}`);
454
- return `${label} failed validation (${error.issues.length} issue${error.issues.length === 1 ? "" : "s"}):
450
+ state: v.optional(v.array(StateSlotSchema)),
451
+ stages: v.pipe(v.array(StageSchema), v.minLength(1, "must declare at least one stage")),
452
+ predicates: v.optional(v.array(PredicateSchema))
453
+ });
454
+ function formatValidationError(label, issues) {
455
+ const lines = issues.map((issue) => ` - ${issue.path.length === 0 ? "(root)" : formatPath(issue.path)}: ${issue.message}`);
456
+ return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):
455
457
  ${lines.join(`
456
458
  `)}`;
457
459
  }
460
+ function issuesFromValibot(issues) {
461
+ return issues.map((issue) => ({
462
+ path: issue.path ? issue.path.map((item) => item.key) : [],
463
+ message: issue.message
464
+ }));
465
+ }
458
466
  function formatPath(path) {
459
467
  let out = "";
460
468
  for (const seg of path)
@@ -462,11 +470,9 @@ function formatPath(path) {
462
470
  return out;
463
471
  }
464
472
  function defineWorkflow(definition) {
465
- return parseOrThrow(
466
- WorkflowDefinitionSchema,
467
- definition,
468
- typeof definition.workflowId == "string" ? `defineWorkflow("${definition.workflowId}")` : "defineWorkflow"
469
- );
473
+ const workflowId = definition.workflowId, label = typeof workflowId == "string" ? `defineWorkflow("${workflowId}")` : "defineWorkflow", parsed = parseOrThrow(WorkflowDefinitionSchema, definition, label), issues = checkWorkflowInvariants(parsed);
474
+ if (issues.length > 0) throw new Error(formatValidationError(label, issues));
475
+ return parsed;
470
476
  }
471
477
  function defineStage(stage) {
472
478
  return parseOrThrow(StageSchema, stage, labelFor("defineStage", stage, "id"));
@@ -501,15 +507,15 @@ function defineEffectDescriptor(descriptor) {
501
507
  return descriptor;
502
508
  }
503
509
  function parseOrThrow(schema, input, label) {
504
- const result = schema.safeParse(input);
510
+ const result = v.safeParse(schema, input);
505
511
  if (!result.success)
506
- throw new Error(formatZodError(result.error, label));
507
- return result.data;
512
+ throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
513
+ return result.output;
508
514
  }
509
515
  function labelFor(fn, value, key) {
510
516
  if (value && typeof value == "object" && key in value) {
511
- const v = value[key];
512
- if (typeof v == "string") return `${fn}("${v}")`;
517
+ const raw = value[key];
518
+ if (typeof raw == "string") return `${fn}("${raw}")`;
513
519
  }
514
520
  return fn;
515
521
  }