@sanity/workflow-engine 0.15.0 → 0.17.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.
@@ -4,6 +4,26 @@ import { conditionOutcome, runGroq as runGroq$1, evaluateConditionOutcome as eva
4
4
 
5
5
  import { parse } from "groq-js";
6
6
 
7
+ function isCascadeFired(action) {
8
+ return action.when !== void 0;
9
+ }
10
+
11
+ function deriveActivityKind(activity) {
12
+ if (activity.target !== void 0) return "manual";
13
+ const actions = activity.actions ?? [];
14
+ return actions.some(a => !isCascadeFired(a)) ? "user" : actions.some(a => (a.effects ?? []).length > 0 || a.spawn !== void 0) ? "service" : actions.length > 0 ? "receive" : "script";
15
+ }
16
+
17
+ function deriveExecutorClassification(activity) {
18
+ if (activity.target !== void 0) return "off-system";
19
+ const actions = activity.actions ?? [], cascadeFired = actions.filter(isCascadeFired).length;
20
+ return cascadeFired === actions.length ? "autonomous" : cascadeFired === 0 ? "interactive" : "hybrid";
21
+ }
22
+
23
+ function driverKind(actor) {
24
+ return actor.kind === "person" || actor.kind === "agent" ? actor.kind : "service";
25
+ }
26
+
7
27
  function errorMessage(err) {
8
28
  return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
9
29
  }
@@ -279,17 +299,17 @@ function effectNotFoundMessage(args) {
279
299
  return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
280
300
  }
281
301
 
282
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
302
+ const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
283
303
 
284
304
  function validateTag(tag) {
285
- if (!TAG_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`);
305
+ if (!LAKE_ID_SEGMENT_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS})`);
286
306
  }
287
307
 
288
308
  function tagScopeFilter() {
289
309
  return "tag == $tag";
290
310
  }
291
311
 
292
- const NonEmptyString = v.pipe(v.string(), v.nonEmpty("must not be empty"));
312
+ const NonEmptyString$1 = v.pipe(v.string(), v.nonEmpty("must not be empty"));
293
313
 
294
314
  function asPredicate(validate) {
295
315
  return value => {
@@ -303,21 +323,21 @@ function asPredicate(validate) {
303
323
 
304
324
  const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v.variant("type", [ v.object({
305
325
  type: v.literal("dataset"),
306
- id: v.pipe(NonEmptyString, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
326
+ id: v.pipe(NonEmptyString$1, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
307
327
  }), v.object({
308
328
  type: v.literal("canvas"),
309
- id: NonEmptyString
329
+ id: NonEmptyString$1
310
330
  }), v.object({
311
331
  type: v.literal("media-library"),
312
- id: NonEmptyString
332
+ id: NonEmptyString$1
313
333
  }), v.object({
314
334
  type: v.literal("dashboard"),
315
- id: NonEmptyString
335
+ id: NonEmptyString$1
316
336
  }) ]), ResourceBindingSchema = v.object({
317
- name: v.pipe(NonEmptyString, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
337
+ name: v.pipe(NonEmptyString$1, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
318
338
  resource: WorkflowResourceSchema
319
339
  }), DefinitionSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v.object({
320
- name: NonEmptyString,
340
+ name: NonEmptyString$1,
321
341
  tag: v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
322
342
  workflowResource: WorkflowResourceSchema,
323
343
  resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => new Set(bindings.map(binding => binding.name)).size === bindings.length, "duplicate resource handle name — each binding name must be unique within a deployment"))),
@@ -381,6 +401,599 @@ function actorFulfillsRole({actorRoles: actorRoles, required: required, aliases:
381
401
  return actorRoles.some(role => accepted.has(role));
382
402
  }
383
403
 
404
+ function groq(strings, ...values) {
405
+ return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
406
+ }
407
+
408
+ function serializeGroqValue(value) {
409
+ const serialized = JSON.stringify(value);
410
+ if (serialized === void 0) throw new Error(`groq tag cannot serialize ${typeof value} — interpolate JSON-representable values only`);
411
+ return serialized;
412
+ }
413
+
414
+ function desugarWorkflow(authoring) {
415
+ const issues = [];
416
+ checkReservedRoleAliasKeys(authoring.roleAliases, issues);
417
+ const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
418
+ issues: issues,
419
+ claimFields: /* @__PURE__ */ new Map,
420
+ claimedFields: /* @__PURE__ */ new Set,
421
+ roleAliases: roleAliases
422
+ }, workflowFields2 = desugarFieldEntries({
423
+ entries: authoring.fields,
424
+ path: [ "fields" ],
425
+ ctx: ctx
426
+ }), workflowLayer = layerOf(workflowFields2), stages = authoring.stages.map((stage, i) => {
427
+ const path = [ "stages", i ], stageFields2 = desugarFieldEntries({
428
+ entries: stage.fields,
429
+ path: [ ...path, "fields" ],
430
+ ctx: ctx
431
+ }), stageEnv = {
432
+ layers: [ {
433
+ scope: "stage",
434
+ entries: layerOf(stageFields2)
435
+ }, {
436
+ scope: "workflow",
437
+ entries: workflowLayer
438
+ } ]
439
+ }, activities = (stage.activities ?? []).map((activity, j) => desugarActivity({
440
+ activity: activity,
441
+ path: [ ...path, "activities", j ],
442
+ stageEnv: stageEnv,
443
+ ctx: ctx
444
+ })), transitions = (stage.transitions ?? []).map(transition => desugarTransition({
445
+ transition: transition
446
+ })), editable = desugarStageEditable({
447
+ overrides: stage.editable,
448
+ inScope: editableFieldNames({
449
+ workflowFields: workflowFields2,
450
+ stageFields: stageFields2,
451
+ activities: activities
452
+ }),
453
+ path: [ ...path, "editable" ],
454
+ ctx: ctx
455
+ });
456
+ return {
457
+ ...stripUndefined({
458
+ name: stage.name,
459
+ title: stage.title,
460
+ description: stage.description,
461
+ groups: stage.groups,
462
+ guards: stage.guards?.map(desugarGuard)
463
+ }),
464
+ ...stageFields2 ? {
465
+ fields: stageFields2
466
+ } : {},
467
+ ...activities.length > 0 ? {
468
+ activities: activities
469
+ } : {},
470
+ ...transitions.length > 0 ? {
471
+ transitions: transitions
472
+ } : {},
473
+ ...editable ? {
474
+ editable: editable
475
+ } : {}
476
+ };
477
+ });
478
+ return checkUnclaimedClaimFields(ctx), {
479
+ definition: {
480
+ ...stripUndefined({
481
+ name: authoring.name,
482
+ title: authoring.title,
483
+ description: authoring.description,
484
+ groups: authoring.groups,
485
+ lifecycle: authoring.lifecycle,
486
+ start: desugarStart(authoring.start),
487
+ initialStage: authoring.initialStage,
488
+ predicates: authoring.predicates,
489
+ roleAliases: roleAliases
490
+ }),
491
+ ...workflowFields2 ? {
492
+ fields: workflowFields2
493
+ } : {},
494
+ stages: stages
495
+ },
496
+ issues: ctx.issues
497
+ };
498
+ }
499
+
500
+ function desugarStart(start) {
501
+ if (start !== void 0) return {
502
+ kind: start.kind ?? "interactive",
503
+ ...start.filter !== void 0 ? {
504
+ filter: start.filter
505
+ } : {},
506
+ ...start.allowed !== void 0 ? {
507
+ allowed: start.allowed
508
+ } : {}
509
+ };
510
+ }
511
+
512
+ function checkReservedRoleAliasKeys(aliases, issues) {
513
+ for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
514
+ path: [ "roleAliases", key ],
515
+ message: `role alias key "${key}" uses the reserved "$" prefix — that namespace is the engine's stored spelling for the universal fulfiller. Use "*" to mean "fulfills any gate", or rename the role.`
516
+ });
517
+ }
518
+
519
+ const TODOLIST_OF = [ {
520
+ type: "string",
521
+ name: "label",
522
+ title: "Label"
523
+ }, {
524
+ type: "string",
525
+ name: "status",
526
+ title: "Status"
527
+ }, {
528
+ type: "assignee",
529
+ name: "assignee",
530
+ title: "Assignee"
531
+ }, {
532
+ type: "date",
533
+ name: "dueDate",
534
+ title: "Due date"
535
+ } ], NOTES_OF = [ {
536
+ type: "text",
537
+ name: "body",
538
+ title: "Body"
539
+ }, {
540
+ type: "actor",
541
+ name: "actor",
542
+ title: "Actor"
543
+ }, {
544
+ type: "datetime",
545
+ name: "at",
546
+ title: "At"
547
+ } ];
548
+
549
+ function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
550
+ return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
551
+ entry: entry,
552
+ path: [ ...path, i ],
553
+ ctx: ctx
554
+ }));
555
+ }
556
+
557
+ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
558
+ if (entry.type === "claim") return desugarClaimField({
559
+ entry: entry,
560
+ path: path,
561
+ ctx: ctx
562
+ });
563
+ if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
564
+ entry: entry,
565
+ path: path,
566
+ ctx: ctx
567
+ });
568
+ const editable = normalizeEditable({
569
+ editable: entry.editable,
570
+ path: [ ...path, "editable" ],
571
+ ctx: ctx
572
+ });
573
+ return {
574
+ ...stripUndefined({
575
+ type: entry.type,
576
+ name: entry.name,
577
+ title: entry.title,
578
+ description: entry.description,
579
+ group: normalizeGroup(entry.group),
580
+ required: entry.required,
581
+ initialValue: entry.initialValue,
582
+ editable: editable,
583
+ types: entry.types,
584
+ fields: entry.fields,
585
+ of: entry.of
586
+ })
587
+ };
588
+ }
589
+
590
+ function desugarClaimField({entry: entry, path: path, ctx: ctx}) {
591
+ const desugared = {
592
+ ...stripUndefined({
593
+ name: entry.name,
594
+ title: entry.title,
595
+ description: entry.description,
596
+ group: normalizeGroup(entry.group)
597
+ }),
598
+ type: "actor"
599
+ };
600
+ return ctx.claimFields.set(desugared, {
601
+ name: entry.name,
602
+ path: path
603
+ }), desugared;
604
+ }
605
+
606
+ function desugarListField({entry: entry, path: path, ctx: ctx}) {
607
+ const editable = normalizeEditable({
608
+ editable: entry.editable,
609
+ path: [ ...path, "editable" ],
610
+ ctx: ctx
611
+ });
612
+ return {
613
+ ...stripUndefined({
614
+ name: entry.name,
615
+ title: entry.title,
616
+ description: entry.description,
617
+ group: normalizeGroup(entry.group),
618
+ required: entry.required,
619
+ initialValue: entry.initialValue,
620
+ editable: editable
621
+ }),
622
+ type: "array",
623
+ of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
624
+ };
625
+ }
626
+
627
+ function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
628
+ if (editable === void 0 || editable === !0) return editable;
629
+ if (Array.isArray(editable)) {
630
+ const condition = rolesCondition(editable, ctx.roleAliases);
631
+ if (condition === void 0) {
632
+ ctx.issues.push({
633
+ path: path,
634
+ message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
635
+ });
636
+ return;
637
+ }
638
+ return condition;
639
+ }
640
+ return editable;
641
+ }
642
+
643
+ function editableFieldNames({workflowFields: workflowFields2, stageFields: stageFields2, activities: activities}) {
644
+ const names = /* @__PURE__ */ new Set, collect = entries => {
645
+ for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
646
+ };
647
+ collect(workflowFields2), collect(stageFields2);
648
+ for (const activity of activities) collect(activity.fields);
649
+ return names;
650
+ }
651
+
652
+ function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
653
+ if (overrides === void 0) return;
654
+ const out = {};
655
+ for (const [name, value] of Object.entries(overrides)) {
656
+ if (!inScope.has(name)) {
657
+ ctx.issues.push({
658
+ path: [ ...path, name ],
659
+ message: `stage editable override "${name}" does not narrow an editable field in scope — name a workflow/stage/activity field of this stage that declares \`editable\``
660
+ });
661
+ continue;
662
+ }
663
+ const normalized = normalizeEditable({
664
+ editable: value,
665
+ path: [ ...path, name ],
666
+ ctx: ctx
667
+ });
668
+ normalized !== void 0 && (out[name] = normalized);
669
+ }
670
+ return Object.keys(out).length > 0 ? out : void 0;
671
+ }
672
+
673
+ function layerOf(entries) {
674
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
675
+ }
676
+
677
+ function normalizeGroup(group) {
678
+ return group === void 0 || Array.isArray(group) ? group : [ group ];
679
+ }
680
+
681
+ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
682
+ const activityFields2 = desugarFieldEntries({
683
+ entries: activity.fields,
684
+ path: [ ...path, "fields" ],
685
+ ctx: ctx
686
+ }), env = {
687
+ layers: [ {
688
+ scope: "activity",
689
+ entries: layerOf(activityFields2)
690
+ }, ...stageEnv.layers ]
691
+ }, actions = (activity.actions ?? []).map((action, a) => desugarAction({
692
+ action: action,
693
+ path: [ ...path, "actions", a ],
694
+ env: env,
695
+ activityName: activity.name,
696
+ ctx: ctx
697
+ })), target = desugarTarget({
698
+ target: activity.target,
699
+ env: env,
700
+ path: [ ...path, "target" ],
701
+ ctx: ctx
702
+ });
703
+ return {
704
+ ...stripUndefined({
705
+ name: activity.name,
706
+ title: activity.title,
707
+ description: activity.description,
708
+ groups: activity.groups,
709
+ group: normalizeGroup(activity.group),
710
+ filter: activity.filter,
711
+ requirements: activity.requirements
712
+ }),
713
+ ...target ? {
714
+ target: target
715
+ } : {},
716
+ ...activityFields2 ? {
717
+ fields: activityFields2
718
+ } : {},
719
+ ...actions.length > 0 ? {
720
+ actions: actions
721
+ } : {}
722
+ };
723
+ }
724
+
725
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
726
+
727
+ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
728
+ if (target === void 0 || target.type === "url") return target;
729
+ const ref = typeof target.field == "string" ? {
730
+ field: target.field
731
+ } : target.field, field = resolveRef({
732
+ ref: ref,
733
+ env: env,
734
+ path: [ ...path, "field" ],
735
+ ctx: ctx
736
+ }) ?? fallbackRef(ref), entry = entryAt(env, field);
737
+ return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
738
+ path: [ ...path, "field" ],
739
+ message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
740
+ }), {
741
+ type: "field",
742
+ field: field
743
+ };
744
+ }
745
+
746
+ function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
747
+ if ("type" in action) return desugarClaimAction({
748
+ action: action,
749
+ path: path,
750
+ env: env,
751
+ ctx: ctx
752
+ });
753
+ action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
754
+ path: [ ...path, "roles" ],
755
+ message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
756
+ });
757
+ const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
758
+ ops: action.ops,
759
+ path: [ ...path, "ops" ],
760
+ env: env,
761
+ firingActivity: activityName,
762
+ ctx: ctx
763
+ }) ?? [];
764
+ return action.status !== void 0 && ops.push({
765
+ type: "status.set",
766
+ activity: activityName,
767
+ status: action.status
768
+ }), {
769
+ ...stripUndefined({
770
+ name: action.name,
771
+ title: action.title,
772
+ description: action.description,
773
+ group: normalizeGroup(action.group),
774
+ when: action.when,
775
+ params: action.params,
776
+ effects: action.effects,
777
+ spawn: action.spawn
778
+ }),
779
+ ...cascadeFired && action.roles !== void 0 && action.roles.length > 0 ? {
780
+ roles: action.roles
781
+ } : {},
782
+ ...filter ? {
783
+ filter: filter
784
+ } : {},
785
+ ...ops.length > 0 ? {
786
+ ops: ops
787
+ } : {}
788
+ };
789
+ }
790
+
791
+ function desugarClaimAction({action: action, path: path, env: env, ctx: ctx}) {
792
+ const ref = typeof action.field == "string" ? {
793
+ field: action.field
794
+ } : action.field, resolved = resolveRef({
795
+ ref: ref,
796
+ env: env,
797
+ path: [ ...path, "field" ],
798
+ ctx: ctx
799
+ });
800
+ if (resolved) {
801
+ const entry = entryAt(env, resolved);
802
+ entry && entry.type !== "actor" && ctx.issues.push({
803
+ path: [ ...path, "field" ],
804
+ message: `claim action "${action.name}" references "${resolved.field}" of kind "${entry.type}" — a claim pair needs an actor-valued entry (a "claim" or "actor" field declaration)`
805
+ }), checkShadowedClaimTarget({
806
+ actionName: action.name,
807
+ field: ref.field,
808
+ resolved: resolved,
809
+ env: env,
810
+ path: [ ...path, "field" ],
811
+ ctx: ctx
812
+ }), entry && ctx.claimedFields.add(entry);
813
+ }
814
+ const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([ noSteal, rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = resolved ? [ {
815
+ type: "field.set",
816
+ target: resolved,
817
+ value: {
818
+ type: "actor"
819
+ }
820
+ } ] : [];
821
+ return {
822
+ ...stripUndefined({
823
+ name: action.name,
824
+ title: action.title,
825
+ description: action.description,
826
+ group: normalizeGroup(action.group),
827
+ params: action.params,
828
+ effects: action.effects
829
+ }),
830
+ filter: filter,
831
+ ...ops.length > 0 ? {
832
+ ops: ops
833
+ } : {}
834
+ };
835
+ }
836
+
837
+ function checkShadowedClaimTarget({actionName: actionName, field: field, resolved: resolved, env: env, path: path, ctx: ctx}) {
838
+ const nearest = env.layers.find(l => l.entries.has(field));
839
+ nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
840
+ path: path,
841
+ message: `claim action "${actionName}" targets "${field}" at scope "${resolved.scope}", but a nearer ${nearest.scope}-scope entry of the same name shadows it in $fields — the no-steal filter would read the shadowing entry while the claim writes the "${resolved.scope}" one. Rename one of the entries or claim the nearer one`
842
+ });
843
+ }
844
+
845
+ function checkUnclaimedClaimFields(ctx) {
846
+ for (const [entry, {name: name, path: path}] of ctx.claimFields) ctx.claimedFields.has(entry) || ctx.issues.push({
847
+ path: path,
848
+ message: `claim field "${name}" is never referenced by a claim action — you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", field: "${name}" }) or use a raw actor entry`
849
+ });
850
+ }
851
+
852
+ const DEFAULT_TRANSITION_WHEN = "$allActivitiesDone";
853
+
854
+ function desugarTransition({transition: transition}) {
855
+ return {
856
+ ...stripUndefined({
857
+ name: transition.name,
858
+ title: transition.title,
859
+ description: transition.description,
860
+ to: transition.to
861
+ }),
862
+ when: transition.when ?? DEFAULT_TRANSITION_WHEN
863
+ };
864
+ }
865
+
866
+ function desugarGuard(guard) {
867
+ const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
868
+ return {
869
+ ...rest,
870
+ match: {
871
+ ...matchRest,
872
+ ...idRefs !== void 0 ? {
873
+ idRefs: idRefs.map(printGuardRead)
874
+ } : {}
875
+ },
876
+ ...metadata !== void 0 ? {
877
+ metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
878
+ } : {}
879
+ };
880
+ }
881
+
882
+ function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
883
+ if (ops) return ops.map((op, i) => desugarOp({
884
+ op: op,
885
+ path: [ ...path, i ],
886
+ env: env,
887
+ firingActivity: firingActivity,
888
+ ctx: ctx
889
+ }));
890
+ }
891
+
892
+ function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
893
+ if (op.type === "status.set") return {
894
+ type: "status.set",
895
+ activity: op.activity ?? firingActivity,
896
+ status: op.status
897
+ };
898
+ if (op.type === "audit") return desugarAuditOp({
899
+ op: op,
900
+ path: path,
901
+ env: env,
902
+ ctx: ctx
903
+ });
904
+ const target = resolveRef({
905
+ ref: op.target,
906
+ env: env,
907
+ path: [ ...path, "target" ],
908
+ ctx: ctx
909
+ }) ?? fallbackRef(op.target);
910
+ return {
911
+ ...op,
912
+ target: target
913
+ };
914
+ }
915
+
916
+ const AUDIT_STAMPS = {
917
+ actor: {
918
+ type: "actor"
919
+ },
920
+ at: {
921
+ type: "now"
922
+ }
923
+ };
924
+
925
+ function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
926
+ const target = resolveRef({
927
+ ref: op.target,
928
+ env: env,
929
+ path: [ ...path, "target" ],
930
+ ctx: ctx
931
+ }) ?? fallbackRef(op.target);
932
+ if (op.value.type !== "object") return ctx.issues.push({
933
+ path: [ ...path, "value" ],
934
+ message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
935
+ }), {
936
+ type: "field.append",
937
+ target: target,
938
+ value: op.value
939
+ };
940
+ const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
941
+ ...op.value.fields
942
+ };
943
+ for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
944
+ if (stamp in fields) {
945
+ ctx.issues.push({
946
+ path: [ ...path, "value", "fields", stamp ],
947
+ message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
948
+ });
949
+ continue;
950
+ }
951
+ fields[stamp] = source;
952
+ }
953
+ return {
954
+ type: "field.append",
955
+ target: target,
956
+ value: {
957
+ type: "object",
958
+ fields: fields
959
+ }
960
+ };
961
+ }
962
+
963
+ function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
964
+ const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
965
+ for (const layer of layers) if (layer.entries.has(ref.field)) return {
966
+ scope: layer.scope,
967
+ field: ref.field
968
+ };
969
+ const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
970
+ ctx.issues.push({
971
+ path: path,
972
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
973
+ });
974
+ }
975
+
976
+ function entryAt(env, ref) {
977
+ return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
978
+ }
979
+
980
+ function fallbackRef(ref) {
981
+ return {
982
+ scope: ref.scope ?? "workflow",
983
+ field: ref.field
984
+ };
985
+ }
986
+
987
+ function rolesCondition(roles, aliases) {
988
+ if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
989
+ }
990
+
991
+ function stripUndefined(obj) {
992
+ const out = {};
993
+ for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
994
+ return out;
995
+ }
996
+
384
997
  function isUnevaluable(result) {
385
998
  return result == null;
386
999
  }
@@ -400,9 +1013,9 @@ async function evaluateCondition(args) {
400
1013
 
401
1014
  async function evaluatePredicates(args) {
402
1015
  const out = {};
403
- for (const [name, groq] of Object.entries(args.predicates ?? {})) {
1016
+ for (const [name, groq2] of Object.entries(args.predicates ?? {})) {
404
1017
  const result = await runGroq({
405
- groq: groq,
1018
+ groq: groq2,
406
1019
  params: args.params,
407
1020
  snapshot: args.snapshot
408
1021
  }), outcome = conditionOutcome(result);
@@ -411,46 +1024,77 @@ async function evaluatePredicates(args) {
411
1024
  return out;
412
1025
  }
413
1026
 
414
- async function runGroq({groq: groq, params: params, snapshot: snapshot}) {
1027
+ async function runGroq({groq: groq2, params: params, snapshot: snapshot}) {
415
1028
  return runGroq$1({
416
- groq: groq,
1029
+ groq: groq2,
417
1030
  params: params,
418
1031
  dataset: snapshot.docs
419
1032
  });
420
1033
  }
421
1034
 
422
- function conditionSyntaxIssues(groq, boundVars) {
1035
+ function conditionSyntaxIssues(groq2, boundVars) {
423
1036
  const issues = [];
424
1037
  let tree;
425
1038
  try {
426
- tree = parse(groq);
1039
+ tree = parse(groq2);
427
1040
  } catch (err) {
428
1041
  issues.push(errorMessage(err));
429
1042
  }
430
- if (/\*\s*\[\s*_type\b/.test(groq) && issues.push('condition scans by `_type` — that\'s a discovery query, not a predicate. Conditions evaluate against the in-memory snapshot (instance + ancestors + field-declared docs). To bring extra docs in scope, declare a `doc.ref` (or `doc.refs`) field entry on the workflow or this stage. For lake scans like "all articles in this release", use `subworkflows.forEach` instead.'),
431
- boundVars !== void 0 && tree !== void 0) for (const name of conditionParameterNames(groq)) boundVars.includes(name) || issues.push(`reads $${name}, which this scope does not bind — an unbound variable evaluates to GROQ null, so the condition silently never matches. Bound here: ` + boundVars.map(n => `$${n}`).join(", "));
1043
+ if (/\*\s*\[\s*_type\b/.test(groq2) && issues.push("condition scans by `_type` — that's a discovery query, not a predicate. Conditions evaluate against the in-memory snapshot (instance + ancestors + field-declared docs). To bring extra docs in scope, declare a `doc.ref` (or `doc.refs`) field entry on the workflow or this stage. For lake scans like \"all articles in this release\", use a spawn action's `forEach` instead."),
1044
+ boundVars !== void 0 && tree !== void 0) for (const name of conditionParameterNames(groq2)) boundVars.includes(name) || issues.push(`reads $${name}, which this scope does not bind — an unbound variable evaluates to GROQ null, so the condition silently never matches. Bound here: ` + boundVars.map(n => `$${n}`).join(", "));
432
1045
  return issues;
433
1046
  }
434
1047
 
435
- function conditionParameterNames(groq) {
1048
+ function conditionParameterNames(groq2) {
436
1049
  const read = /* @__PURE__ */ new Set;
437
- return walkAstNodes(tryParseGroq(groq), node => {
1050
+ return walkAstNodes(tryParseGroq(groq2), node => {
438
1051
  node.type === "Parameter" && typeof node.name == "string" && read.add(node.name);
439
1052
  }), read;
440
1053
  }
441
1054
 
442
- function conditionFieldReadNames(groq) {
443
- const read = /* @__PURE__ */ new Set;
444
- return walkAstNodes(tryParseGroq(groq), node => {
445
- if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
446
- const base = node.base;
447
- base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
448
- }), read;
1055
+ function conditionFieldReadNames(groq2) {
1056
+ return new Set(conditionFieldReads(groq2).map(read => read.name));
449
1057
  }
450
1058
 
451
- function conditionEffectReads(groq) {
1059
+ function conditionFieldReads(groq2) {
1060
+ const reads = /* @__PURE__ */ new Map;
1061
+ return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
1062
+ }
1063
+
1064
+ function collectFieldReads(node, reads) {
1065
+ if (Array.isArray(node)) {
1066
+ for (const item of node) collectFieldReads(item, reads);
1067
+ return;
1068
+ }
1069
+ if (typeof node != "object" || node === null) return;
1070
+ const chain = fieldsAttributeChain(node);
1071
+ if (chain !== void 0) {
1072
+ const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
1073
+ reads.set(`${name}.${path ?? ""}`, {
1074
+ name: name,
1075
+ path: path
1076
+ });
1077
+ return;
1078
+ }
1079
+ for (const value of Object.values(node)) collectFieldReads(value, reads);
1080
+ }
1081
+
1082
+ function fieldsAttributeChain(node) {
1083
+ const names = [];
1084
+ let current = node;
1085
+ for (;typeof current == "object" && current !== null; ) {
1086
+ const typed = current;
1087
+ if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
1088
+ names.unshift(typed.name);
1089
+ const base = typed.base;
1090
+ if (base?.type === "Parameter" && base.name === "fields") return names;
1091
+ current = base;
1092
+ }
1093
+ }
1094
+
1095
+ function conditionEffectReads(groq2) {
452
1096
  const reads = [];
453
- return walkAstNodes(tryParseGroq(groq), node => {
1097
+ return walkAstNodes(tryParseGroq(groq2), node => {
454
1098
  if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
455
1099
  const base = node.base;
456
1100
  base?.type !== "AccessAttribute" || typeof base.name != "string" || base.base?.type === "Parameter" && base.base.name === "effects" && reads.push({
@@ -460,9 +1104,34 @@ function conditionEffectReads(groq) {
460
1104
  }), reads;
461
1105
  }
462
1106
 
463
- function tryParseGroq(groq) {
1107
+ function readsRootDocument(groq2) {
1108
+ return nodeReadsRoot(tryParseGroq(groq2), 0);
1109
+ }
1110
+
1111
+ const SCOPED_CHILD = {
1112
+ Filter: "expr",
1113
+ Projection: "expr",
1114
+ Map: "expr",
1115
+ FlatMap: "expr",
1116
+ PipeFuncCall: "args"
1117
+ };
1118
+
1119
+ function nodeReadsRoot(node, depth) {
1120
+ if (Array.isArray(node)) return node.some(item => nodeReadsRoot(item, depth));
1121
+ if (typeof node != "object" || node === null) return !1;
1122
+ const typed = node;
1123
+ if (depth === 0 && typed.type === "This" || depth === 0 && typed.type === "AccessAttribute" && typed.base === void 0) return !0;
1124
+ if (typed.type === "Parent") {
1125
+ const climb = typeof typed.n == "number" ? typed.n : 1;
1126
+ return depth - climb <= 0;
1127
+ }
1128
+ const scopedChild = typeof typed.type == "string" ? SCOPED_CHILD[typed.type] : void 0;
1129
+ return scopedChild !== void 0 ? Object.entries(node).some(([key, value]) => nodeReadsRoot(value, key === scopedChild ? depth + 1 : depth)) : Object.values(node).some(value => nodeReadsRoot(value, depth));
1130
+ }
1131
+
1132
+ function tryParseGroq(groq2) {
464
1133
  try {
465
- return parse(groq);
1134
+ return parse(groq2);
466
1135
  } catch {
467
1136
  return;
468
1137
  }
@@ -479,7 +1148,13 @@ function walkAstNodes(node, visit) {
479
1148
  }
480
1149
  }
481
1150
 
482
- const CONDITION_VARS = [ {
1151
+ const ACTIVITY_STATUSES = [ "active", "done", "skipped", "failed" ], TERMINAL_ACTIVITY_STATUSES = [ "done", "skipped", "failed" ];
1152
+
1153
+ function isTerminalActivityStatus(status) {
1154
+ return TERMINAL_ACTIVITY_STATUSES.includes(status);
1155
+ }
1156
+
1157
+ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], CONDITION_VARS = [ {
483
1158
  name: "self",
484
1159
  binding: "always",
485
1160
  label: "this workflow instance",
@@ -488,7 +1163,7 @@ const CONDITION_VARS = [ {
488
1163
  name: "fields",
489
1164
  binding: "always",
490
1165
  label: "the workflow's fields",
491
- description: "Declared field entries rendered by name (`$fields.<name>` is the value, no envelope). Stage/activity scopes overlay lexically; `doc.ref` values render the hydrated document when the snapshot holds it."
1166
+ description: "Declared field entries rendered by name (`$fields.<name>` is the value, no wrapper). Stage/activity scopes overlay lexically. What a read puts in your hand follows the declared kind: a singular `doc.ref` DEREFERENCES into the hydrated document (lake `_id`/`_type` plus content fields — which may themselves be named `id`/`type`), while `doc.refs` elements and `release.ref` stay REFERENCES — `{id, type[, releaseName]}` with `id` a GDR URI — because identity reads (membership, counting, joins) must stay total without hydrating every target, and targets may live in resources the evaluation cannot fetch from. Conditions evaluate against the in-memory snapshot only, so a dereferencing plural kind would silently hole wherever hydration lagged."
492
1167
  }, {
493
1168
  name: "parent",
494
1169
  binding: "always",
@@ -509,11 +1184,16 @@ const CONDITION_VARS = [ {
509
1184
  binding: "always",
510
1185
  label: "the current time",
511
1186
  description: "The ISO clock reading shared by every condition in one evaluation pass, so they agree on the time."
1187
+ }, {
1188
+ name: "context",
1189
+ binding: "always",
1190
+ label: "start-time context",
1191
+ description: "The instance's `context` bag: values seeded at `startInstance` plus a parent's `spawn.context` handoff, by entry name. Written only at start — never mutated afterwards."
512
1192
  }, {
513
1193
  name: "effects",
514
1194
  binding: "always",
515
1195
  label: "automation outputs",
516
- description: "The `effectsContext` map: stable handler params seeded at start plus completed effects' outputs namespaced by effect name (`$effects['<effect>'].<output>`). Handler fuel — transition filters should read instance/lake state (or $effectStatus) instead."
1196
+ description: "Completed effects' outputs, namespaced by effect name (`$effects['<effect>'].<output>`) — each effect's latest completed run wins. Handler fuel — transition triggers should read instance/lake state (or $effectStatus) instead."
517
1197
  }, {
518
1198
  name: "effectStatus",
519
1199
  binding: "always",
@@ -548,29 +1228,169 @@ const CONDITION_VARS = [ {
548
1228
  name: "can",
549
1229
  binding: "caller",
550
1230
  label: "your permissions",
551
- description: "Advisory per-permission booleans computed from the caller's grants; `undefined` without grants. Bound wherever grants ride the evaluation: the projection's rendered scope (action filters, activity requirements, editability predicates) and the fireAction/editField commit gates. Deploy rejects it at every site that evaluates without grants: transition filters, activity `filter`/`completeWhen`/`failWhen`, effect bindings, where-op `where`s, and the subworkflow `forEach`/`with`/`context` sites."
1231
+ description: "Advisory per-permission booleans computed from the caller's grants; `undefined` without grants. Bound wherever grants ride the evaluation: the projection's rendered scope (fireAction-action filters, activity requirements, editability predicates) and the fireAction/editField commit gates. Deploy rejects it at every site that evaluates without grants: transition `when`s, activity filters, cascade-fired actions' `when`/`filter`, effect bindings, where-op `where`s, and the spawn `forEach`/`with`/`context` sites."
552
1232
  }, {
553
1233
  name: "row",
554
1234
  binding: "spawn",
555
1235
  label: "the spawned row",
556
- description: "One `subworkflows.forEach` result row, bound while its `with` map evaluates — and the row under test while a where-op `where` evaluates (per row)."
1236
+ description: "One `spawn.forEach` result row, bound while its `with` map evaluates — and the row under test while a where-op `where` evaluates (per row). Deploy rejects a read at every other site (including `spawn.forEach` itself and `spawn.context`), where it is GROQ null."
557
1237
  }, {
558
1238
  name: "params",
559
1239
  binding: "caller",
560
1240
  label: "the action's arguments",
561
- description: "The firing action's args — they hold values only while that action's effect bindings and where-op `where`s evaluate (activity/transition boundaries bind `{}`; subworkflow sites don't bind it at all). Deploy rejects a read in the cascade gates and in the caller-bound projection (action filters, requirements, editable predicates): args exist only once the caller fires the action, after those sites have evaluated."
1241
+ description: "The firing action's args — they hold values only while a fireAction-fired action's effect bindings and where-op `where`s evaluate (spawn sites don't bind it at all, and a cascade-fired action has no caller to supply args). Deploy rejects a read in the cascade gates, in a cascade-fired action's payload, and in the caller-bound projection (action filters, requirements, editable predicates): args exist only once the caller fires the action, after those sites have evaluated."
562
1242
  }, {
563
1243
  name: "subworkflows",
564
1244
  binding: "always",
565
1245
  label: "the spawned subworkflows",
566
- description: "Every row of the instance's subworkflow registry, faceted by `activity`/`definition`/`rowKey`/`status` (`'active'|'done'|'aborted'`) with `current` marking the open stage entry's cohort and `stage` the child's current stage. Usable anywhere — transition filters, requirements, any stage's gates; the implicit spawn gate is `count($subworkflows[activity == <name> && current && status == 'active']) == 0`."
567
- } ], RESERVED_CONDITION_VARS = CONDITION_VARS.map(v2 => v2.name), FILTER_SCOPE_VARS = CONDITION_VARS.filter(v2 => v2.binding === "always").map(v2 => v2.name), CALLER_BOUND_VARS = CONDITION_VARS.filter(v2 => v2.binding === "caller").map(v2 => v2.name), GUARD_PREDICATE_VARS = [ {
1246
+ description: "Every row of the instance's subworkflow registry, faceted by `activity`/`action`/`definition`/`rowKey`/`status` (`'active'|'done'|'aborted'`) with `current` marking the open stage entry's cohort and `stage` the child's current stage. Usable anywhere — transition `when`s, requirements, any stage's gates; the settled gate is `count($subworkflows[activity == <name> && current && status == 'active']) == 0`."
1247
+ } ], RESERVED_CONDITION_VARS = CONDITION_VARS.map(v2 => v2.name), FILTER_SCOPE_VARS = CONDITION_VARS.filter(v2 => v2.binding === "always").map(v2 => v2.name), CALLER_BOUND_VARS = CONDITION_VARS.filter(v2 => v2.binding === "caller").map(v2 => v2.name), START_FILTER_VARS = [ {
1248
+ name: "tag",
1249
+ description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1250
+ }, {
1251
+ name: "definition",
1252
+ description: "The `name` of the definition under evaluation (its own start block binds it)."
1253
+ }, {
1254
+ name: "now",
1255
+ description: "The ISO clock reading of the evaluating engine."
1256
+ } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1257
+ name: "fields",
1258
+ description: "The caller's input entries by name (`initialFields` — at `startInstance`, the values the start would seed; at a pre-flight, the values gathered so far, so a read of a not-yet-supplied entry is GROQ null). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble — a singular `doc.ref` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
1259
+ } ], GUARD_PREDICATE_VARS = [ {
568
1260
  name: "guard",
569
1261
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
570
1262
  }, {
571
1263
  name: "mutation",
572
1264
  description: "The attempted mutation — `mutation.action` is the write kind being gated."
573
- } ], ACTOR_KINDS = [ "person", "agent", "system" ];
1265
+ } ], NonEmptyString = v.pipe(v.string(), v.minLength(1, "must be a non-empty string"));
1266
+
1267
+ function isParseableInstant(value) {
1268
+ return typeof value == "string" && !Number.isNaN(Date.parse(value));
1269
+ }
1270
+
1271
+ const IsoTimestamp = v.pipe(v.string(), v.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
1272
+
1273
+ function tolerantObject() {
1274
+ return (entries, ..._exact) => v.looseObject(entries);
1275
+ }
1276
+
1277
+ function tolerantEntries() {
1278
+ return (entries, ..._exact) => entries;
1279
+ }
1280
+
1281
+ function schemaTreeShape(schema) {
1282
+ return walkSchemaShape(schema, /* @__PURE__ */ new Set);
1283
+ }
1284
+
1285
+ function walkSchemaShape(node, path) {
1286
+ if (typeof node != "object" || node === null) return "unknown";
1287
+ if (path.has(node)) return "(circular)";
1288
+ path.add(node);
1289
+ try {
1290
+ const schema = node;
1291
+ return containerShape(schema, path) ?? leafShape(schema);
1292
+ } finally {
1293
+ path.delete(node);
1294
+ }
1295
+ }
1296
+
1297
+ const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
1298
+ union: schema.options.map(option => walkSchemaShape(option, path))
1299
+ }), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
1300
+ strict_object: objectWalker,
1301
+ loose_object: objectWalker,
1302
+ object: objectWalker,
1303
+ array: (schema, path) => ({
1304
+ array: walkSchemaShape(schema.item, path)
1305
+ }),
1306
+ record: (schema, path) => ({
1307
+ record: walkSchemaShape(schema.value, path)
1308
+ }),
1309
+ union: unionWalker,
1310
+ variant: unionWalker,
1311
+ lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
1312
+ exact_optional: unwrapWalker,
1313
+ optional: unwrapWalker,
1314
+ nullable: unwrapWalker
1315
+ };
1316
+
1317
+ function containerShape(schema, path) {
1318
+ return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
1319
+ }
1320
+
1321
+ function objectEntriesShape(entries, path) {
1322
+ const out = {};
1323
+ for (const [key, entry] of Object.entries(entries)) {
1324
+ const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
1325
+ out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
1326
+ }
1327
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
1328
+ }
1329
+
1330
+ function isOptionalEntry(entry) {
1331
+ if (typeof entry != "object" || entry === null) return !1;
1332
+ const kind = entry.type;
1333
+ return kind === "exact_optional" || kind === "optional";
1334
+ }
1335
+
1336
+ const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
1337
+
1338
+ function leafShape(schema) {
1339
+ if (schema.type === "picklist") return schema.options.join(" | ");
1340
+ if (schema.type === "literal") return `literal ${String(schema.literal)}`;
1341
+ if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
1342
+ if (!LEAF_KINDS.has(String(schema.type))) throw new Error(`schemaTreeShape: unhandled schema kind "${String(schema.type)}" — extend the walker before regenerating the model ledger`);
1343
+ return String(schema.type);
1344
+ }
1345
+
1346
+ function formatValidationError(label, issues) {
1347
+ const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1348
+ return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1349
+ }
1350
+
1351
+ function issuesFromValibot(issues) {
1352
+ return issues.map(issue => ({
1353
+ path: issue.path ? issue.path.map(item => item.key) : [],
1354
+ message: issue.message
1355
+ }));
1356
+ }
1357
+
1358
+ function formatIssuePath(path) {
1359
+ let out = "";
1360
+ for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1361
+ return out;
1362
+ }
1363
+
1364
+ class PersistedDocShapeError extends WorkflowError {
1365
+ documentId;
1366
+ documentType;
1367
+ issues;
1368
+ constructor(args) {
1369
+ super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
1370
+ this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
1371
+ this.issues = args.issues;
1372
+ }
1373
+ }
1374
+
1375
+ function parsePersistedDoc(args) {
1376
+ const result = v.safeParse(args.schema, args.doc);
1377
+ if (!result.success) throw new PersistedDocShapeError({
1378
+ documentId: documentIdOf(args.doc),
1379
+ documentType: args.docType,
1380
+ issues: issuesFromValibot(result.issues)
1381
+ });
1382
+ return result.output;
1383
+ }
1384
+
1385
+ function documentIdOf(doc) {
1386
+ if (typeof doc == "object" && doc !== null) {
1387
+ const id = doc._id;
1388
+ if (typeof id == "string") return id;
1389
+ }
1390
+ return "(unknown id)";
1391
+ }
1392
+
1393
+ const ACTOR_KINDS = [ "person", "agent", "system" ];
574
1394
 
575
1395
  function releaseDocId(releaseName) {
576
1396
  return `_.releases.${releaseName}`;
@@ -617,29 +1437,28 @@ class FieldValueShapeError extends WorkflowError {
617
1437
  }
618
1438
  }
619
1439
 
620
- const GdrShape = v.looseObject({
621
- id: v.pipe(v.string(), v.check(s => isGdrUri(s), "must be a GDR URI")),
622
- type: v.pipe(v.string(), v.minLength(1))
623
- }), ReleaseRefShape = v.pipe(v.looseObject({
624
- id: v.pipe(v.string(), v.check(s => isGdrUri(s), "must be a GDR URI")),
1440
+ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
1441
+ id: GdrUriSchema,
1442
+ type: NonEmptyString
1443
+ }), ReleaseRefShape = v.pipe(tolerantObject()({
1444
+ id: GdrUriSchema,
625
1445
  type: v.literal("system.release"),
626
- releaseName: v.pipe(v.string(), v.minLength(1))
627
- }), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = v.looseObject({
1446
+ releaseName: NonEmptyString
1447
+ }), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = tolerantObject()({
628
1448
  kind: v.picklist(ACTOR_KINDS),
629
- id: v.pipe(v.string(), v.minLength(1)),
630
- roles: v.optional(v.array(v.string())),
631
- onBehalfOf: v.optional(v.string())
632
- }), AssigneeShape = v.union([ v.looseObject({
1449
+ id: NonEmptyString,
1450
+ roles: v.exactOptional(v.array(v.string())),
1451
+ onBehalfOf: v.exactOptional(v.string())
1452
+ }), AssigneeShape = v.union([ tolerantObject()({
633
1453
  type: v.literal("user"),
634
- id: v.pipe(v.string(), v.minLength(1))
635
- }), v.looseObject({
1454
+ id: NonEmptyString
1455
+ }), tolerantObject()({
636
1456
  type: v.literal("role"),
637
- role: v.pipe(v.string(), v.minLength(1))
638
- }) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableDateTime = v.union([ v.null(), v.pipe(v.string(), v.check(s => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")) ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, valueSchemas = {
1457
+ role: NonEmptyString
1458
+ }) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, fieldValueSchemas = {
639
1459
  "doc.ref": v.union([ v.null(), GdrShape ]),
640
1460
  "doc.refs": v.array(GdrShape),
641
1461
  "release.ref": v.union([ v.null(), ReleaseRefShape ]),
642
- query: v.any(),
643
1462
  string: NullableString,
644
1463
  text: NullableString,
645
1464
  number: NullableNumber,
@@ -650,6 +1469,9 @@ const GdrShape = v.looseObject({
650
1469
  actor: v.union([ v.null(), ActorShape ]),
651
1470
  assignee: v.union([ v.null(), AssigneeShape ]),
652
1471
  assignees: v.array(AssigneeShape)
1472
+ }, valueSchemas = {
1473
+ ...fieldValueSchemas,
1474
+ query: v.any()
653
1475
  };
654
1476
 
655
1477
  function shapeValueSchema(shape, leaf) {
@@ -736,7 +1558,7 @@ function isBareSeedId(id) {
736
1558
 
737
1559
  const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a bare document id, a GDR URI, or a portable `@<alias>:<id>` reference")), AuthoringGdrShape = v.looseObject({
738
1560
  id: AuthoringRefId,
739
- type: v.pipe(v.string(), v.minLength(1))
1561
+ type: NonEmptyString
740
1562
  }), seedValueSchemas = {
741
1563
  ...valueSchemas,
742
1564
  "doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
@@ -744,7 +1566,7 @@ const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a b
744
1566
  "release.ref": v.union([ v.null(), v.looseObject({
745
1567
  id: AuthoringRefId,
746
1568
  type: v.literal("system.release"),
747
- releaseName: v.pipe(v.string(), v.minLength(1))
1569
+ releaseName: NonEmptyString
748
1570
  }) ])
749
1571
  };
750
1572
 
@@ -787,13 +1609,7 @@ function formatIssues(issues) {
787
1609
  });
788
1610
  }
789
1611
 
790
- const ACTIVITY_STATUSES = [ "pending", "active", "done", "skipped", "failed" ], TERMINAL_ACTIVITY_STATUSES = [ "done", "skipped", "failed" ];
791
-
792
- function isTerminalActivityStatus(status) {
793
- return TERMINAL_ACTIVITY_STATUSES.includes(status);
794
- }
795
-
796
- const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], NonEmpty = v.pipe(v.string(), v.minLength(1, "must be a non-empty string")), PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1612
+ const NonEmpty = NonEmptyString, PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
797
1613
 
798
1614
  function groqIdentifier(referencedAs) {
799
1615
  return v.pipe(v.string(), v.regex(GROQ_IDENTIFIER, `must be a GROQ-safe identifier (letters, digits, underscore; not starting with a digit) because it is referenced as ${referencedAs} in GROQ conditions`));
@@ -892,7 +1708,7 @@ const StoredFieldOpSchema = v.variant("type", [ ...opSchemas(StoredFieldRefSchem
892
1708
  type: v.literal("status.set"),
893
1709
  activity: NonEmpty,
894
1710
  status: picklist(ACTIVITY_STATUSES)
895
- }) ]), StoredTransitionOpSchema = StoredFieldOpSchema, AuditOpSchema = v.strictObject({
1711
+ }) ]), AuditOpSchema = v.strictObject({
896
1712
  type: v.literal("audit"),
897
1713
  target: AuthoringFieldRefSchema,
898
1714
  value: ValueExprSchema,
@@ -1020,6 +1836,15 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
1020
1836
  bindings: v.optional(v.record(v.string(), ConditionSchema)),
1021
1837
  input: v.optional(v.record(v.string(), v.unknown())),
1022
1838
  outputs: v.optional(v.array(FieldShapeSchema))
1839
+ }), DefinitionRefSchema = v.strictObject({
1840
+ name: NonEmpty,
1841
+ version: v.optional(v.union([ PositiveInt, v.literal("latest") ]))
1842
+ }), SubworkflowsSchema = v.strictObject({
1843
+ forEach: NonEmpty,
1844
+ definition: DefinitionRefSchema,
1845
+ with: v.optional(v.record(NonEmpty, ConditionSchema)),
1846
+ context: v.optional(v.record(NonEmpty, ConditionSchema)),
1847
+ onExit: v.optional(picklist([ "detach", "abort" ]))
1023
1848
  }), ActionParamSchema = v.strictObject({
1024
1849
  type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
1025
1850
  name: NonEmpty,
@@ -1034,14 +1859,19 @@ function actionFields(op, group) {
1034
1859
  title: v.optional(v.string()),
1035
1860
  description: v.optional(v.string()),
1036
1861
  group: v.optional(group),
1862
+ when: v.optional(ConditionSchema),
1037
1863
  filter: v.optional(ConditionSchema),
1038
1864
  params: v.optional(v.array(ActionParamSchema)),
1039
1865
  ops: v.optional(v.array(op)),
1040
- effects: v.optional(v.array(EffectSchema))
1866
+ effects: v.optional(v.array(EffectSchema)),
1867
+ spawn: v.optional(SubworkflowsSchema)
1041
1868
  };
1042
1869
  }
1043
1870
 
1044
- const StoredActionSchema = pinned()(v.strictObject(actionFields(StoredOpSchema, StoredGroupMembershipSchema))), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v.strictObject({
1871
+ const StoredActionSchema = pinned()(v.strictObject({
1872
+ ...actionFields(StoredOpSchema, StoredGroupMembershipSchema),
1873
+ roles: v.optional(v.array(NonEmpty))
1874
+ })), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v.strictObject({
1045
1875
  ...actionFields(AuthoringOpSchema, AuthoringGroupMembershipSchema),
1046
1876
  roles: v.optional(v.array(NonEmpty)),
1047
1877
  status: v.optional(TerminalActivityStatus)
@@ -1056,35 +1886,19 @@ const StoredActionSchema = pinned()(v.strictObject(actionFields(StoredOpSchema,
1056
1886
  filter: v.optional(ConditionSchema),
1057
1887
  params: v.optional(v.array(ActionParamSchema)),
1058
1888
  effects: v.optional(v.array(EffectSchema))
1059
- })), AuthoringActionSchema = pinned()(v.union([ RawAuthoringActionSchema, ClaimActionSchema ])), DefinitionRefSchema = v.strictObject({
1060
- name: NonEmpty,
1061
- version: v.optional(v.union([ PositiveInt, v.literal("latest") ]))
1062
- }), SubworkflowsSchema = v.strictObject({
1063
- forEach: NonEmpty,
1064
- definition: DefinitionRefSchema,
1065
- with: v.optional(v.record(NonEmpty, ConditionSchema)),
1066
- context: v.optional(v.record(NonEmpty, ConditionSchema)),
1067
- onExit: v.optional(picklist([ "detach", "abort" ]))
1068
- });
1889
+ })), AuthoringActionSchema = pinned()(v.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema));
1069
1890
 
1070
- function activityFields({field: field, action: action, op: op, target: target, group: group}) {
1891
+ function activityFields({field: field, action: action, target: target, group: group}) {
1071
1892
  return {
1072
1893
  name: NonEmpty,
1073
1894
  title: v.optional(v.string()),
1074
1895
  description: v.optional(v.string()),
1075
1896
  groups: v.optional(v.array(GroupSchema)),
1076
1897
  group: v.optional(group),
1077
- kind: v.optional(picklist(ACTIVITY_KINDS)),
1078
1898
  target: v.optional(target),
1079
- activation: v.optional(picklist([ "auto", "manual" ])),
1080
1899
  filter: v.optional(ConditionSchema),
1081
1900
  requirements: v.optional(v.record(NonEmpty, ConditionSchema)),
1082
- completeWhen: v.optional(ConditionSchema),
1083
- failWhen: v.optional(ConditionSchema),
1084
- ops: v.optional(v.array(op)),
1085
- effects: v.optional(v.array(EffectSchema)),
1086
1901
  actions: v.optional(v.array(action)),
1087
- subworkflows: v.optional(SubworkflowsSchema),
1088
1902
  fields: v.optional(v.array(field))
1089
1903
  };
1090
1904
  }
@@ -1092,30 +1906,26 @@ function activityFields({field: field, action: action, op: op, target: target, g
1092
1906
  const StoredActivitySchema = pinned()(v.strictObject(activityFields({
1093
1907
  field: FieldEntrySchema,
1094
1908
  action: StoredActionSchema,
1095
- op: StoredOpSchema,
1096
1909
  target: StoredManualTargetSchema,
1097
1910
  group: StoredGroupMembershipSchema
1098
1911
  }))), AuthoringActivitySchema = pinned()(v.strictObject(activityFields({
1099
1912
  field: AuthoringFieldEntrySchema,
1100
1913
  action: AuthoringActionSchema,
1101
- op: AuthoringOpSchema,
1102
1914
  target: AuthoringManualTargetSchema,
1103
1915
  group: AuthoringGroupMembershipSchema
1104
1916
  })));
1105
1917
 
1106
- function transitionFields(op, filter) {
1918
+ function transitionFields(when) {
1107
1919
  return {
1108
1920
  name: NonEmpty,
1109
1921
  title: v.optional(v.string()),
1110
1922
  description: v.optional(v.string()),
1111
1923
  to: NonEmpty,
1112
- filter: filter,
1113
- ops: v.optional(v.array(op)),
1114
- effects: v.optional(v.array(EffectSchema))
1924
+ when: when
1115
1925
  };
1116
1926
  }
1117
1927
 
1118
- const StoredTransitionSchema = pinned()(v.strictObject(transitionFields(StoredTransitionOpSchema, ConditionSchema))), AuthoringTransitionOpSchema = v.variant("type", [ ...opSchemas(AuthoringFieldRefSchema), AuditOpSchema ]), AuthoringTransitionSchema = pinned()(v.strictObject(transitionFields(AuthoringTransitionOpSchema, v.optional(ConditionSchema)))), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardReadPath = v.pipe(NonEmpty, v.check(path => !/[\r\n\u2028\u2029]/.test(path), "a guard read path cannot contain a line break")), GuardReadSchema = v.variant("type", [ v.strictObject({
1928
+ const StoredTransitionSchema = pinned()(v.strictObject(transitionFields(ConditionSchema))), AuthoringTransitionSchema = pinned()(v.strictObject(transitionFields(v.optional(ConditionSchema)))), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardReadPath = v.pipe(NonEmpty, v.check(path => !/[\r\n\u2028\u2029]/.test(path), "a guard read path cannot contain a line break")), GuardReadSchema = v.variant("type", [ v.strictObject({
1119
1929
  type: v.literal("self")
1120
1930
  }), v.strictObject({
1121
1931
  type: v.literal("now")
@@ -1177,16 +1987,26 @@ const StoredStageSchema = pinned()(v.strictObject(stageFields({
1177
1987
  transition: AuthoringTransitionSchema,
1178
1988
  guard: AuthoringGuardSchema,
1179
1989
  editable: AuthoringEditableSchema
1180
- }))), RoleAliasesSchema = v.record(NonEmpty, v.pipe(v.array(NonEmpty), v.minLength(1, "a role alias must list at least one fulfilling role"))), WORKFLOW_LIFECYCLES = [ "standalone", "child" ];
1990
+ }))), RoleAliasesSchema = v.record(NonEmpty, v.pipe(v.array(NonEmpty), v.minLength(1, "a role alias must list at least one fulfilling role"))), WORKFLOW_LIFECYCLES = [ "standalone", "child" ], START_KINDS = [ "interactive", "autonomous" ];
1991
+
1992
+ function startFields(kind) {
1993
+ return {
1994
+ kind: kind,
1995
+ filter: v.optional(ConditionSchema),
1996
+ allowed: v.optional(ConditionSchema)
1997
+ };
1998
+ }
1181
1999
 
1182
- function workflowFields(field, stage) {
2000
+ const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KINDS)))), AuthoringStartSchema = pinned()(v.strictObject(startFields(v.optional(picklist(START_KINDS)))));
2001
+
2002
+ function workflowFields({field: field, stage: stage, start: start}) {
1183
2003
  return {
1184
2004
  name: NonEmpty,
1185
2005
  title: NonEmpty,
1186
2006
  description: v.optional(v.string()),
1187
2007
  groups: v.optional(v.array(GroupSchema)),
1188
2008
  lifecycle: v.optional(picklist(WORKFLOW_LIFECYCLES)),
1189
- applicableWhen: v.optional(ConditionSchema),
2009
+ start: v.optional(start),
1190
2010
  initialStage: NonEmpty,
1191
2011
  fields: v.optional(v.array(field)),
1192
2012
  stages: v.pipe(v.array(stage), v.minLength(1, "must declare at least one stage")),
@@ -1195,7 +2015,11 @@ function workflowFields(field, stage) {
1195
2015
  };
1196
2016
  }
1197
2017
 
1198
- const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields(FieldEntrySchema, StoredStageSchema)));
2018
+ const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields({
2019
+ field: FieldEntrySchema,
2020
+ stage: StoredStageSchema,
2021
+ start: StoredStartSchema
2022
+ })));
1199
2023
 
1200
2024
  function parseStoredDefinition(input, label) {
1201
2025
  return parseOrThrow({
@@ -1205,28 +2029,26 @@ function parseStoredDefinition(input, label) {
1205
2029
  });
1206
2030
  }
1207
2031
 
1208
- const AuthoringWorkflowSchema = pinned()(v.strictObject(workflowFields(AuthoringFieldEntrySchema, AuthoringStageSchema))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
2032
+ const AuthoringWorkflowSchema = pinned()(v.strictObject(workflowFields({
2033
+ field: AuthoringFieldEntrySchema,
2034
+ stage: AuthoringStageSchema,
2035
+ start: AuthoringStartSchema
2036
+ }))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
1209
2037
 
1210
2038
  function isStartableDefinition(definition) {
1211
2039
  return definition.lifecycle !== "child";
1212
2040
  }
1213
2041
 
1214
- function formatValidationError(label, issues) {
1215
- const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1216
- return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
2042
+ function startKindOf(definition) {
2043
+ return definition.start?.kind ?? "interactive";
1217
2044
  }
1218
2045
 
1219
- function issuesFromValibot(issues) {
1220
- return issues.map(issue => ({
1221
- path: issue.path ? issue.path.map(item => item.key) : [],
1222
- message: issue.message
1223
- }));
2046
+ function isSubjectEntry(entry) {
2047
+ return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
1224
2048
  }
1225
2049
 
1226
- function formatIssuePath(path) {
1227
- let out = "";
1228
- for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1229
- return out;
2050
+ function isInputSourced(entry) {
2051
+ return entry.initialValue?.type === "input";
1230
2052
  }
1231
2053
 
1232
2054
  function parseOrThrow({schema: schema, input: input, label: label}) {
@@ -1257,6 +2079,13 @@ function checkDuplicates({names: names, what: what, issues: issues}) {
1257
2079
  return seen;
1258
2080
  }
1259
2081
 
2082
+ function checkDefinitionName(def, issues) {
2083
+ LAKE_ID_SEGMENT_RE.test(def.name) || issues.push({
2084
+ path: [ "name" ],
2085
+ message: `definition name "${def.name}" does not match the definition-name grammar ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS}) — the name is interpolated into every deployed document id (\`<tag>.<name>.v<version>\`), which the engine keeps to the same segment grammar as the tag`
2086
+ });
2087
+ }
2088
+
1260
2089
  function checkStages(def, issues) {
1261
2090
  const stageNames = checkDuplicates({
1262
2091
  names: def.stages.map((stage, i) => ({
@@ -1313,12 +2142,6 @@ function checkActivities({def: def, i: i, activityNames: activityNames, issues:
1313
2142
  }
1314
2143
 
1315
2144
  function checkStatusSetTargets({activity: activity, activityNames: activityNames, path: path, issues: issues}) {
1316
- checkStatusSetOps({
1317
- ops: activity.ops,
1318
- activityNames: activityNames,
1319
- path: [ ...path, "ops" ],
1320
- issues: issues
1321
- });
1322
2145
  for (const [a, action] of (activity.actions ?? []).entries()) checkStatusSetOps({
1323
2146
  ops: action.ops,
1324
2147
  activityNames: activityNames,
@@ -1366,25 +2189,11 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
1366
2189
 
1367
2190
  function effectNameSites(def) {
1368
2191
  const sites = [];
1369
- for (const [i, stage] of def.stages.entries()) {
1370
- for (const [j, activity] of (stage.activities ?? []).entries()) {
1371
- collectEffects({
1372
- effects: activity.effects,
1373
- path: [ "stages", i, "activities", j, "effects" ],
1374
- sites: sites
1375
- });
1376
- for (const [a, action] of (activity.actions ?? []).entries()) collectEffects({
1377
- effects: action.effects,
1378
- path: [ "stages", i, "activities", j, "actions", a, "effects" ],
1379
- sites: sites
1380
- });
1381
- }
1382
- for (const [k, t] of (stage.transitions ?? []).entries()) collectEffects({
1383
- effects: t.effects,
1384
- path: [ "stages", i, "transitions", k, "effects" ],
1385
- sites: sites
1386
- });
1387
- }
2192
+ for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) collectEffects({
2193
+ effects: action.effects,
2194
+ path: [ "stages", i, "activities", j, "actions", a, "effects" ],
2195
+ sites: sites
2196
+ });
1388
2197
  return sites;
1389
2198
  }
1390
2199
 
@@ -1413,6 +2222,10 @@ function checkGuardNames(def, issues) {
1413
2222
  what: "guard name (the guard lake _id derives from it — unique per definition)",
1414
2223
  issues: issues
1415
2224
  });
2225
+ for (const {name: name, path: path} of sites) LAKE_ID_SEGMENT_RE.test(name) || issues.push({
2226
+ path: path,
2227
+ message: `guard name "${name}" does not match the guard-name grammar ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS}) — the guard's lake _id derives from it at stage entry, so an invalid name would wedge the entering commit with an opaque lake error instead of failing here`
2228
+ });
1416
2229
  }
1417
2230
 
1418
2231
  function fieldScopes(def) {
@@ -1471,19 +2284,23 @@ function checkPredicates(def, issues) {
1471
2284
  path: [ "predicates", name ],
1472
2285
  message: `predicate "${name}" shadows the built-in $${name} — pick another name`
1473
2286
  });
1474
- for (const [name, groq] of Object.entries(def.predicates ?? {})) checkPredicateBody({
2287
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkPredicateBody({
1475
2288
  name: name,
1476
- groq: groq,
2289
+ groq: groq2,
1477
2290
  names: names,
1478
2291
  issues: issues
1479
2292
  });
1480
2293
  }
1481
2294
 
1482
- function checkPredicateBody({name: name, groq: groq, names: names, issues: issues}) {
1483
- const reads = conditionParameterNames(groq);
2295
+ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issues}) {
2296
+ const reads = conditionParameterNames(groq2);
1484
2297
  for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
1485
2298
  path: [ "predicates", name ],
1486
- message: `predicate "${name}" reads $${caller} — predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
2299
+ message: `predicate "${name}" reads $${caller} — predicates pre-evaluate caller-free; compose caller gates at the condition site instead`
2300
+ });
2301
+ reads.has("row") && issues.push({
2302
+ path: [ "predicates", name ],
2303
+ message: `predicate "${name}" reads $row — ${ROW_BINDING_CLAUSE}; predicates pre-evaluate once per instance, so the read is GROQ null and the predicate can never hold`
1487
2304
  });
1488
2305
  for (const other of names) other === name || !reads.has(other) || issues.push({
1489
2306
  path: [ "predicates", name ],
@@ -1491,32 +2308,43 @@ function checkPredicateBody({name: name, groq: groq, names: names, issues: issue
1491
2308
  });
1492
2309
  }
1493
2310
 
1494
- function entryNames(entries) {
1495
- return new Set((entries ?? []).map(entry => entry.name));
2311
+ function entryMap(entries) {
2312
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
1496
2313
  }
1497
2314
 
1498
- function checkUnboundCallerVars(def, issues) {
2315
+ function checkUnboundConditionVars(def, issues) {
1499
2316
  for (const site of conditionSites(def)) {
1500
2317
  const reads = conditionParameterNames(site.groq);
1501
- for (const name of unboundCallerVars(site.policy)) reads.has(name) && issues.push({
2318
+ for (const name of unboundVarsAt(site)) reads.has(name) && issues.push({
1502
2319
  path: site.path,
1503
- message: callerVarMessage(site, name)
2320
+ message: name === "row" ? rowVarMessage(site) : callerVarMessage(site, name)
1504
2321
  });
1505
2322
  }
1506
2323
  }
1507
2324
 
2325
+ function unboundVarsAt(site) {
2326
+ const callerVars = unboundCallerVars(site.policy);
2327
+ return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
2328
+ }
2329
+
1508
2330
  function unboundCallerVars(policy) {
1509
- return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : [ "can" ];
2331
+ return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
2332
+ }
2333
+
2334
+ const ROW_BINDING_CLAUSE = "$row (the discovered row in a spawn projection; the stored row under test in a where-op) is bound only while a spawn `with` projection or a where-op `where` evaluates";
2335
+
2336
+ function rowVarMessage(site) {
2337
+ return `${site.label} reads $row — ${ROW_BINDING_CLAUSE}; this site never binds it, so the read is GROQ null and the condition can never resolve. Move the per-row read into \`spawn.with\` or a where-op, or gate on instance state instead`;
1510
2338
  }
1511
2339
 
1512
2340
  function callerVarMessage(site, name) {
1513
- return site.policy === "cascade" ? `${site.label} reads $${name} — the cascade evaluates transition filters and activity gates (filter/completeWhen/failWhen) with no caller bound ($assigned is constant false, the other caller vars hold no value); gate on instance state (e.g. a field an action wrote) instead` : site.policy === "caller-bound" ? `${site.label} reads $${name} — $params (the firing action's args) is bound only while the action's effect bindings and where-op \`where\`s evaluate; this site never binds it, so the condition could never pass. Bind $params in an effect binding or a where-op instead, or gate on a field an action wrote` : `${site.label} reads $can — $can (the caller's grants) is bound only in the caller-bound projection (action filters, requirements, editable predicates) and never holds a value in cascade conditions; move the check to one of those sites or drop $can`;
2341
+ return site.policy === "cascade" ? `${site.label} reads $${name} — cascade gates (transition \`when\`s, activity \`filter\`s, a cascade-fired action's \`when\`/\`filter\`) must resolve identically no matter whose token drives the cascade ($assigned is constant false, the other caller vars hold no value); gate on instance state (e.g. a field an action wrote), or pin executing identities with \`roles\`` : site.policy === "caller-bound" ? `${site.label} reads $${name} — $params (the firing action's args) is bound only while the action's effect bindings and where-op \`where\`s evaluate; this site never binds it, so the condition could never pass. Bind $params in an effect binding or a where-op instead, or gate on a field an action wrote` : site.policy === "triggered-payload" && name === "params" ? `${site.label} reads $params — a cascade-fired action has no caller to supply args, so $params never holds a value in its payload; read fields or effect outputs instead` : `${site.label} reads $can — $can (the caller's grants) is bound only in the caller-bound projection (action filters, requirements, editable predicates) and never holds a value in cascade conditions; move the check to one of those sites or drop $can`;
1514
2342
  }
1515
2343
 
1516
2344
  function conditionSites(def) {
1517
2345
  const sites = [], workflowLayer = {
1518
2346
  scope: "workflow",
1519
- names: entryNames(def.fields)
2347
+ entries: entryMap(def.fields)
1520
2348
  };
1521
2349
  collectEditableSites({
1522
2350
  entries: def.fields,
@@ -1537,35 +2365,24 @@ function conditionSites(def) {
1537
2365
  function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
1538
2366
  const stageLayer = {
1539
2367
  scope: "stage",
1540
- names: entryNames(stage.fields)
2368
+ entries: entryMap(stage.fields)
1541
2369
  }, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
1542
2370
  scope: "activity",
1543
- names: entryNames(activity.fields)
2371
+ entries: entryMap(activity.fields)
1544
2372
  })) ];
1545
2373
  for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
1546
- groq: t.filter,
1547
- path: [ "stages", i, "transitions", k, "filter" ],
1548
- label: `transition "${t.name}" filter`,
2374
+ groq: t.when,
2375
+ path: [ "stages", i, "transitions", k, "when" ],
2376
+ label: `transition "${t.name}" when`,
1549
2377
  policy: "cascade",
1550
2378
  fields: stageFields2
1551
- }), collectBindingSites({
1552
- effects: t.effects,
1553
- path: [ "stages", i, "transitions", k, "effects" ],
1554
- label: `transition "${t.name}"`,
1555
- fields: stageFields2,
1556
- sites: sites
1557
- }), collectOpWhereSites({
1558
- ops: t.ops,
1559
- path: [ "stages", i, "transitions", k, "ops" ],
1560
- label: `transition "${t.name}"`,
1561
- fields: stageFields2,
1562
- sites: sites
1563
2379
  });
1564
2380
  collectEditableSites({
1565
2381
  entries: stage.fields,
1566
2382
  path: [ "stages", i, "fields" ],
1567
2383
  labelPrefix: `stage "${stage.name}"`,
1568
2384
  fields: editableWindow,
2385
+ unionWindow: !0,
1569
2386
  sites: sites
1570
2387
  });
1571
2388
  for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
@@ -1573,70 +2390,64 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
1573
2390
  path: [ "stages", i, "editable", name ],
1574
2391
  label: `stage "${stage.name}" editable override "${name}"`,
1575
2392
  policy: "caller-bound",
1576
- fields: editableWindow
2393
+ fields: editableWindow,
2394
+ unionWindow: !0
1577
2395
  });
1578
2396
  for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
1579
2397
  activity: activity,
1580
2398
  path: [ "stages", i, "activities", j ],
1581
2399
  stageFields: stageFields2,
2400
+ workflowLayer: workflowLayer,
1582
2401
  sites: sites
1583
2402
  });
1584
2403
  }
1585
2404
 
1586
- function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
2405
+ function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, unionWindow: unionWindow, sites: sites}) {
1587
2406
  for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
1588
2407
  groq: entry.editable,
1589
2408
  path: [ ...path, n, "editable" ],
1590
2409
  label: `${labelPrefix} field "${entry.name}" editable`,
1591
2410
  policy: "caller-bound",
1592
- fields: fields
2411
+ fields: fields,
2412
+ ...unionWindow === !0 ? {
2413
+ unionWindow: !0
2414
+ } : {}
1593
2415
  });
1594
2416
  }
1595
2417
 
1596
- function collectOpWhereSites({ops: ops, path: path, label: label, fields: fields, sites: sites}) {
2418
+ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
1597
2419
  for (const [o, op] of (ops ?? []).entries()) op.where !== void 0 && sites.push({
1598
2420
  groq: op.where,
1599
2421
  path: [ ...path, o, "where" ],
1600
2422
  label: `${label} ${op.type} where`,
2423
+ ...policy !== void 0 ? {
2424
+ policy: policy
2425
+ } : {},
2426
+ bindsRow: !0,
1601
2427
  fields: fields
1602
2428
  });
1603
2429
  }
1604
2430
 
1605
- function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
2431
+ function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, workflowLayer: workflowLayer, sites: sites}) {
1606
2432
  const fields = [ {
1607
2433
  scope: "activity",
1608
- names: entryNames(activity.fields)
2434
+ entries: entryMap(activity.fields)
1609
2435
  }, ...stageFields2 ];
1610
- for (const gate of [ "filter", "completeWhen", "failWhen" ]) {
1611
- const groq = activity[gate];
1612
- groq !== void 0 && sites.push({
1613
- groq: groq,
1614
- path: [ ...path, gate ],
1615
- label: `activity "${activity.name}".${gate}`,
1616
- policy: "cascade",
1617
- fields: fields
1618
- });
1619
- }
1620
- for (const [name, groq] of Object.entries(activity.requirements ?? {})) sites.push({
1621
- groq: groq,
2436
+ activity.filter !== void 0 && sites.push({
2437
+ groq: activity.filter,
2438
+ path: [ ...path, "filter" ],
2439
+ label: `activity "${activity.name}".filter`,
2440
+ policy: "cascade",
2441
+ fields: fields
2442
+ });
2443
+ for (const [name, groq2] of Object.entries(activity.requirements ?? {})) sites.push({
2444
+ groq: groq2,
1622
2445
  path: [ ...path, "requirements", name ],
1623
2446
  label: `activity "${activity.name}" requirement "${name}"`,
1624
2447
  policy: "caller-bound",
1625
2448
  fields: fields
1626
2449
  });
1627
- collectBindingSites({
1628
- effects: activity.effects,
1629
- path: [ ...path, "effects" ],
1630
- label: `activity "${activity.name}"`,
1631
- fields: fields,
1632
- sites: sites
1633
- }), collectOpWhereSites({
1634
- ops: activity.ops,
1635
- path: [ ...path, "ops" ],
1636
- label: `activity "${activity.name}"`,
1637
- fields: fields,
1638
- sites: sites
1639
- }), collectEditableSites({
2450
+ collectEditableSites({
1640
2451
  entries: activity.fields,
1641
2452
  path: [ ...path, "fields" ],
1642
2453
  labelPrefix: `activity "${activity.name}"`,
@@ -1646,60 +2457,84 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
1646
2457
  activity: activity,
1647
2458
  path: path,
1648
2459
  fields: fields,
1649
- sites: sites
1650
- }), collectSubworkflowSites({
1651
- activity: activity,
1652
- path: path,
1653
- fields: fields,
2460
+ workflowLayer: workflowLayer,
1654
2461
  sites: sites
1655
2462
  });
1656
2463
  }
1657
2464
 
1658
- function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
1659
- for (const [a, action] of (activity.actions ?? []).entries()) action.filter !== void 0 && sites.push({
1660
- groq: action.filter,
1661
- path: [ ...path, "actions", a, "filter" ],
1662
- label: `action "${action.name}" filter`,
1663
- policy: "caller-bound",
1664
- fields: fields
1665
- }), collectBindingSites({
1666
- effects: action.effects,
1667
- path: [ ...path, "actions", a, "effects" ],
1668
- label: `action "${action.name}"`,
1669
- fields: fields,
1670
- sites: sites
1671
- }), collectOpWhereSites({
1672
- ops: action.ops,
1673
- path: [ ...path, "actions", a, "ops" ],
1674
- label: `action "${action.name}"`,
1675
- fields: fields,
1676
- sites: sites
1677
- });
2465
+ function collectActionConditionSites({activity: activity, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2466
+ for (const [a, action] of (activity.actions ?? []).entries()) {
2467
+ const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
2468
+ action.when !== void 0 && sites.push({
2469
+ groq: action.when,
2470
+ path: [ ...actionPath, "when" ],
2471
+ label: `action "${action.name}" when`,
2472
+ policy: "cascade",
2473
+ fields: fields
2474
+ }), action.filter !== void 0 && sites.push({
2475
+ groq: action.filter,
2476
+ path: [ ...actionPath, "filter" ],
2477
+ label: `action "${action.name}" filter`,
2478
+ policy: cascadeFired ? "cascade" : "caller-bound",
2479
+ fields: fields
2480
+ });
2481
+ const payloadPolicy = cascadeFired ? "triggered-payload" : void 0;
2482
+ collectBindingSites({
2483
+ effects: action.effects,
2484
+ path: [ ...actionPath, "effects" ],
2485
+ label: `action "${action.name}"`,
2486
+ policy: payloadPolicy,
2487
+ fields: fields,
2488
+ sites: sites
2489
+ }), collectOpWhereSites({
2490
+ ops: action.ops,
2491
+ path: [ ...actionPath, "ops" ],
2492
+ label: `action "${action.name}"`,
2493
+ policy: payloadPolicy,
2494
+ fields: fields,
2495
+ sites: sites
2496
+ }), collectSpawnSites({
2497
+ action: action,
2498
+ path: actionPath,
2499
+ fields: fields,
2500
+ workflowLayer: workflowLayer,
2501
+ sites: sites
2502
+ });
2503
+ }
1678
2504
  }
1679
2505
 
1680
- function collectSubworkflowSites({activity: activity, path: path, fields: fields, sites: sites}) {
1681
- const sub = activity.subworkflows;
1682
- if (sub === void 0) return;
1683
- const subPath = [ ...path, "subworkflows" ];
2506
+ function collectSpawnSites({action: action, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2507
+ const spawn = action.spawn;
2508
+ if (spawn === void 0) return;
2509
+ const spawnPath = [ ...path, "spawn" ];
1684
2510
  sites.push({
1685
- groq: sub.forEach,
1686
- path: [ ...subPath, "forEach" ],
1687
- label: `activity "${activity.name}".subworkflows.forEach`,
1688
- fields: fields
1689
- });
1690
- for (const [group, record] of [ [ "with", sub.with ], [ "context", sub.context ] ]) for (const [key, groq] of Object.entries(record ?? {})) sites.push({
1691
- groq: groq,
1692
- path: [ ...subPath, group, key ],
1693
- label: `activity "${activity.name}".subworkflows.${group} "${key}"`,
2511
+ groq: spawn.forEach,
2512
+ path: [ ...spawnPath, "forEach" ],
2513
+ label: `action "${action.name}".spawn.forEach`,
2514
+ policy: "cascade",
2515
+ fields: [ workflowLayer ],
2516
+ windowNote: "spawn.forEach evaluates against workflow-scope $fields only stage/activity fields are never bound at discovery time"
2517
+ });
2518
+ for (const [group, record, bindsRow] of [ [ "with", spawn.with, !0 ], [ "context", spawn.context, !1 ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2519
+ groq: groq2,
2520
+ path: [ ...spawnPath, group, key ],
2521
+ label: `action "${action.name}".spawn.${group} "${key}"`,
2522
+ policy: "triggered-payload",
2523
+ ...bindsRow ? {
2524
+ bindsRow: !0
2525
+ } : {},
1694
2526
  fields: fields
1695
2527
  });
1696
2528
  }
1697
2529
 
1698
- function collectBindingSites({effects: effects, path: path, label: label, fields: fields, sites: sites}) {
1699
- for (const [e, effect] of (effects ?? []).entries()) for (const [key, groq] of Object.entries(effect.bindings ?? {})) sites.push({
1700
- groq: groq,
2530
+ function collectBindingSites({effects: effects, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
2531
+ for (const [e, effect] of (effects ?? []).entries()) for (const [key, groq2] of Object.entries(effect.bindings ?? {})) sites.push({
2532
+ groq: groq2,
1701
2533
  path: [ ...path, e, "bindings", key ],
1702
2534
  label: `${label} effect "${effect.name}" binding "${key}"`,
2535
+ ...policy !== void 0 ? {
2536
+ policy: policy
2537
+ } : {},
1703
2538
  fields: fields
1704
2539
  });
1705
2540
  }
@@ -1711,34 +2546,102 @@ function checkAssigneesEntries(def, issues) {
1711
2546
  });
1712
2547
  }
1713
2548
 
1714
- function activityShape(activity) {
1715
- return {
1716
- hasActions: (activity.actions ?? []).length > 0,
1717
- hasEffects: (activity.effects ?? []).length > 0,
1718
- hasCompleteWhen: activity.completeWhen !== void 0,
1719
- hasSubworkflows: activity.subworkflows !== void 0
1720
- };
2549
+ function checkActivityTerminalPaths(def, issues) {
2550
+ for (const [i, stage] of def.stages.entries()) {
2551
+ const resolvable = terminallyResolvableActivities(stage);
2552
+ for (const [j, activity] of (stage.activities ?? []).entries()) resolvable.has(activity.name) || issues.push({
2553
+ path: [ "stages", i, "activities", j ],
2554
+ message: `activity "${activity.name}" has no path to a terminal status — no action in stage "${stage.name}" resolves it (\`status: 'done'\` on one of its actions, a \`{when: <condition>, status: 'done'}\` trigger, or a sibling's \`status.set\`), so it can never finish and the stage's \`$allActivitiesDone\` gate would wedge`
2555
+ });
2556
+ }
1721
2557
  }
1722
2558
 
1723
- const KIND_SHAPE_RULES = {
1724
- user: s => s.hasActions ? [] : [ "needs at least one action for a person to fire" ],
1725
- service: s => s.hasEffects ? [] : [ "needs at least one effect — the automated work it performs" ],
1726
- manual: s => [ s.hasActions || s.hasCompleteWhen ? void 0 : "needs a way to complete — an action to acknowledge it, or a `completeWhen` predicate to verify it (otherwise it auto-resolves on activation)", s.hasSubworkflows ? "is off-system work, not a fan-out — drop `subworkflows` or move it to its own activity" : void 0 ].filter(m => m !== void 0),
1727
- receive: s => [ s.hasCompleteWhen || s.hasSubworkflows ? void 0 : "needs a `completeWhen` (or `subworkflows`) condition to wait on", s.hasActions ? "has no actor, so it cannot carry actions" : void 0 ].filter(m => m !== void 0),
1728
- script: s => [ s.hasActions ? "runs inline with no actor, so it cannot carry actions" : void 0, s.hasCompleteWhen ? "resolves on activation, so it cannot carry a `completeWhen` (that is a `receive`/`service` wait)" : void 0, s.hasSubworkflows ? "resolves on activation, so it cannot fan out with `subworkflows`" : void 0 ].filter(m => m !== void 0)
1729
- };
2559
+ function terminallyResolvableActivities(stage) {
2560
+ const terminalSets = (stage.activities ?? []).flatMap(activity => activity.actions ?? []).flatMap(action => action.ops ?? []).filter(op => op.type === "status.set").filter(op => TERMINAL_ACTIVITY_STATUSES.includes(op.status));
2561
+ return new Set(terminalSets.map(op => op.activity));
2562
+ }
1730
2563
 
1731
- function checkActivityKinds(def, issues) {
1732
- for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) {
1733
- const path = [ "stages", i, "activities", j ];
1734
- if (activity.target !== void 0 && activity.kind !== "manual" && issues.push({
1735
- path: [ ...path, "target" ],
1736
- message: `activity "${activity.name}" has a \`target\` but is not \`kind: "manual"\` — a target is the deep-link for off-system manual work`
1737
- }), activity.kind !== void 0) for (const fault of KIND_SHAPE_RULES[activity.kind](activityShape(activity))) issues.push({
1738
- path: [ ...path, "kind" ],
1739
- message: `activity "${activity.name}" declares kind "${activity.kind}" but ${fault}`
2564
+ function checkTerminalStageActivities(def, issues) {
2565
+ for (const [i, stage] of def.stages.entries()) (stage.transitions ?? []).length > 0 || (stage.activities ?? []).length === 0 || issues.push({
2566
+ path: [ "stages", i, "activities" ],
2567
+ message: `stage "${stage.name}" is terminal (no transitions) but declares activities — entering a terminal stage completes the instance, so they can never run. Move the work to a \`when\` action in the source stage (it commits in the hop that moves here), or give the stage a transition out`
2568
+ });
2569
+ }
2570
+
2571
+ function storedRolesIssue(action) {
2572
+ if (action.roles !== void 0) {
2573
+ if (action.roles.length === 0) return `action "${action.name}" stores \`roles: []\`, which names no roles — the runtime reads an empty pin as "anyone may execute", the opposite of a pin. Omit \`roles\` to allow any identity, or list at least one role`;
2574
+ if (action.when === void 0) return `action "${action.name}" stores \`roles\` but is fireAction-fired (no \`when\`) — the stored pin is only read on cascade fires. Author \`roles\` normally (it desugars into the action's \`filter\`), or add \`when\` to make it a trigger`;
2575
+ }
2576
+ }
2577
+
2578
+ function checkStoredRolesPlacement(def, issues) {
2579
+ for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) {
2580
+ const message = storedRolesIssue(action);
2581
+ message !== void 0 && issues.push({
2582
+ path: [ "stages", i, "activities", j, "actions", a, "roles" ],
2583
+ message: message
2584
+ });
2585
+ }
2586
+ }
2587
+
2588
+ function checkTriggeredActionParams(def, issues) {
2589
+ for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
2590
+ path: [ "stages", i, "activities", j, "actions", a, "params" ],
2591
+ message: `action "${action.name}" declares params but is cascade-fired (\`when\`) — no caller ever supplies args to a trigger. Drop the params, or drop \`when\` to make it a fireAction-fired action`
2592
+ });
2593
+ }
2594
+
2595
+ function checkStart(def, issues) {
2596
+ if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2597
+ path: [ "start" ],
2598
+ message: "a spawn-only (lifecycle 'child') definition declares `start` — children are instantiated by a parent's `spawn`, never started standalone, so the block would never apply. Remove `start`, or drop `lifecycle: 'child'`"
2599
+ }), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
2600
+ path: [ "fields", n, "required" ],
2601
+ message: `start.kind 'autonomous' means runs are initiated by a system reacting to a document, so every required input must be derivable from that triggering document — required entry "${entry.name}" (kind "${entry.type}") is not a document reference. Make it optional, seed it another way (query/literal), or declare it doc.ref / doc.refs`
2602
+ });
2603
+ }
2604
+
2605
+ function checkStartFilterReads(def, issues) {
2606
+ const filter = def.start?.filter;
2607
+ filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
2608
+ path: [ "start", "filter" ],
2609
+ message: "start.filter reads $fields, but the filter is browse-time-pure — a start surface evaluates it per document, before any inputs exist, so $fields cannot be bound. Move the input-dependent rule to start.allowed, the start-time permission predicate that binds $fields and is enforced by startInstance"
2610
+ }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2611
+ path: [ "start", "filter" ],
2612
+ message: "start.filter reads the candidate document (its root), but the definition declares no required subject entry (a required doc.ref / doc.refs input) — a read surface would never have a document to bind as root, so every root read is GROQ null and the filter silently misevaluates. Declare a required subject entry, or gate on the dataset instead"
2613
+ }));
2614
+ }
2615
+
2616
+ function checkStartAllowedReads(def, issues) {
2617
+ const allowed = def.start?.allowed;
2618
+ if (allowed === void 0) return;
2619
+ const bindable = new Map((def.fields ?? []).filter(isInputSourced).map(entry => [ entry.name, entry ])), declared = new Set((def.fields ?? []).map(entry => entry.name)), nullVerdict = `so the read is GROQ null and null semantics decide the verdict (refuse-all or vacuously-allow, by the expression's shape), never the caller's real input. Bindable (input) fields: ${knownList(bindable.keys())}`;
2620
+ for (const read of conditionFieldReads(allowed)) {
2621
+ const entry = bindable.get(read.name);
2622
+ if (entry === void 0) {
2623
+ issues.push({
2624
+ path: [ "start", "allowed" ],
2625
+ message: declared.has(read.name) ? `start.allowed reads $fields.${read.name}, but "${read.name}" is not an \`input\`-sourced entry — $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after the start gate), ${nullVerdict}` : `start.allowed reads $fields.${read.name}, but no workflow-scope field entry named "${read.name}" is declared — $fields binds only the caller's input entries, ${nullVerdict}`
2626
+ });
2627
+ continue;
2628
+ }
2629
+ pushFieldReadPathIssue({
2630
+ where: "start.allowed",
2631
+ read: {
2632
+ field: read.name,
2633
+ path: read.path
2634
+ },
2635
+ target: entry,
2636
+ path: [ "start", "allowed" ],
2637
+ issues: issues,
2638
+ nodes: START_ALLOWED_VALUE_NODES
1740
2639
  });
1741
2640
  }
2641
+ readsRootDocument(allowed) && issues.push({
2642
+ path: [ "start", "allowed" ],
2643
+ message: "start.allowed reads the candidate document as its root, but no root ever binds in the start-allowed context — startInstance holds inputs, not a loaded document, so the read is GROQ null and null semantics decide the verdict, never the workflow's real state. Read the subject as $fields.<entry> (its GDR URI is $fields.<entry>.id); a per-document visibility rule belongs in start.filter"
2644
+ });
1742
2645
  }
1743
2646
 
1744
2647
  function checkGroups(def, issues) {
@@ -1848,39 +2751,80 @@ function checkGroupMembership({group: group, reachable: reachable, path: path, l
1848
2751
  }
1849
2752
 
1850
2753
  function checkConditionFieldReads(def, issues) {
1851
- const everywhere = allDeclaredFieldNames(def);
1852
- for (const site of conditionSites(def)) for (const name of conditionFieldReadNames(site.groq)) fieldVisibleAtSite({
2754
+ const everywhere = allDeclaredFieldEntries(def);
2755
+ for (const site of conditionSites(def)) checkSiteFieldReads({
1853
2756
  site: site,
1854
- name: name,
1855
- everywhere: everywhere
1856
- }) || issues.push({
1857
- path: site.path,
1858
- message: undeclaredFieldReadMessage(site, name)
2757
+ everywhere: everywhere,
2758
+ issues: issues
1859
2759
  });
1860
- for (const [name, groq] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq)) everywhere.has(read) || issues.push({
1861
- path: [ "predicates", name ],
1862
- message: noFieldAnywhereMessage(`predicate "${name}"`, read)
2760
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkSiteFieldReads({
2761
+ site: {
2762
+ groq: groq2,
2763
+ path: [ "predicates", name ],
2764
+ label: `predicate "${name}"`,
2765
+ fields: "context-dependent"
2766
+ },
2767
+ everywhere: everywhere,
2768
+ issues: issues
1863
2769
  });
1864
2770
  }
1865
2771
 
1866
- function allDeclaredFieldNames(def) {
1867
- const names = /* @__PURE__ */ new Set;
1868
- for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) names.add(entry.name);
1869
- return names;
2772
+ function checkSiteFieldReads({site: site, everywhere: everywhere, issues: issues}) {
2773
+ for (const read of conditionFieldReads(site.groq)) {
2774
+ const targets = readTargets({
2775
+ site: site,
2776
+ read: read,
2777
+ everywhere: everywhere
2778
+ });
2779
+ if (targets === "undeclared") {
2780
+ issues.push({
2781
+ path: site.path,
2782
+ message: undeclaredFieldReadMessage(site, read.name)
2783
+ });
2784
+ continue;
2785
+ }
2786
+ const path = read.path;
2787
+ path !== void 0 && (targets.some(target => fieldValuePathIssue({
2788
+ target: target,
2789
+ path: path
2790
+ }) === void 0) || pushFieldReadPathIssue({
2791
+ where: site.label,
2792
+ read: {
2793
+ field: read.name,
2794
+ path: path
2795
+ },
2796
+ target: targets[0],
2797
+ path: site.path,
2798
+ issues: issues
2799
+ }));
2800
+ }
2801
+ }
2802
+
2803
+ function readTargets({site: site, read: read, everywhere: everywhere}) {
2804
+ if (site.fields === "context-dependent") return everywhere.get(read.name) ?? "undeclared";
2805
+ const layers = site.fields.filter(layer => layer.entries.has(read.name));
2806
+ if (layers.length === 0) return "undeclared";
2807
+ const entries = layers.map(layer => layer.entries.get(read.name));
2808
+ return site.unionWindow === !0 ? entries : [ entries[0] ];
1870
2809
  }
1871
2810
 
1872
- function fieldVisibleAtSite({site: site, name: name, everywhere: everywhere}) {
1873
- return site.fields === "context-dependent" ? everywhere.has(name) : site.fields.some(layer => layer.names.has(name));
2811
+ function allDeclaredFieldEntries(def) {
2812
+ const declared = /* @__PURE__ */ new Map;
2813
+ for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) {
2814
+ const list = declared.get(entry.name);
2815
+ list === void 0 ? declared.set(entry.name, [ entry ]) : list.push(entry);
2816
+ }
2817
+ return declared;
1874
2818
  }
1875
2819
 
1876
2820
  function noFieldAnywhereMessage(label, name) {
1877
- return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently fails closed`;
2821
+ return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently misevaluates`;
1878
2822
  }
1879
2823
 
1880
2824
  function undeclaredFieldReadMessage(site, name) {
1881
2825
  if (site.fields === "context-dependent") return noFieldAnywhereMessage(site.label, name);
1882
- const visible = site.fields.flatMap(layer => [ ...layer.names ].map(n => `${layer.scope}:${n}`));
1883
- return `${site.label} reads $fields.${name}, but no field entry named "${name}" is lexically visible here — the read evaluates to GROQ null, so the condition silently fails closed. Visible: ${visible.join(", ") || "(none)"}`;
2826
+ const visible = site.fields.flatMap(layer => [ ...layer.entries.keys() ].map(n => `${layer.scope}:${n}`)), window = site.windowNote === void 0 ? `no field entry named "${name}" is lexically visible here` : `"${name}" is not in this site's window (${site.windowNote})`;
2827
+ return `${site.label} reads $fields.${name}, but ${window} — the read evaluates to GROQ null, so the condition silently misevaluates. Visible: ${visible.join(", ") || "(none)"}`;
1884
2828
  }
1885
2829
 
1886
2830
  function checkFieldReadSeeds(def, issues) {
@@ -1961,38 +2905,29 @@ function seedEarlierSiblingTarget(args) {
1961
2905
  });
1962
2906
  }
1963
2907
 
2908
+ function opSites(def) {
2909
+ const sites = [];
2910
+ for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) sites.push({
2911
+ ops: action.ops,
2912
+ path: [ "stages", i, "activities", j, "actions", a, "ops" ],
2913
+ label: `action "${action.name}"`,
2914
+ stage: stage,
2915
+ activity: activity
2916
+ });
2917
+ return sites;
2918
+ }
2919
+
1964
2920
  function checkFieldReadOpValues(def, issues) {
1965
2921
  const workflowEntries = def.fields ?? [];
1966
- for (const [i, stage] of def.stages.entries()) {
1967
- const shared = {
1968
- workflowEntries: workflowEntries,
1969
- stageEntries: stage.fields ?? [],
1970
- issues: issues
1971
- };
1972
- for (const [k, t] of (stage.transitions ?? []).entries()) checkOpsFieldReads({
1973
- ...shared,
1974
- ops: t.ops,
1975
- path: [ "stages", i, "transitions", k, "ops" ],
1976
- label: `transition "${t.name}"`
1977
- });
1978
- for (const [j, activity] of (stage.activities ?? []).entries()) {
1979
- const activityNames = entryNames(activity.fields), activityPath = [ "stages", i, "activities", j ];
1980
- checkOpsFieldReads({
1981
- ...shared,
1982
- ops: activity.ops,
1983
- path: [ ...activityPath, "ops" ],
1984
- label: `activity "${activity.name}"`,
1985
- activityNames: activityNames
1986
- });
1987
- for (const [a, action] of (activity.actions ?? []).entries()) checkOpsFieldReads({
1988
- ...shared,
1989
- ops: action.ops,
1990
- path: [ ...activityPath, "actions", a, "ops" ],
1991
- label: `action "${action.name}"`,
1992
- activityNames: activityNames
1993
- });
1994
- }
1995
- }
2922
+ for (const site of opSites(def)) checkOpsFieldReads({
2923
+ workflowEntries: workflowEntries,
2924
+ stageEntries: site.stage.fields ?? [],
2925
+ issues: issues,
2926
+ ops: site.ops,
2927
+ path: site.path,
2928
+ label: site.label,
2929
+ activityEntries: entryMap(site.activity.fields)
2930
+ });
1996
2931
  }
1997
2932
 
1998
2933
  function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
@@ -2011,7 +2946,7 @@ function fieldReadsIn(value, path) {
2011
2946
  } ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
2012
2947
  }
2013
2948
 
2014
- function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityNames: activityNames, issues: issues}) {
2949
+ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
2015
2950
  const hosts = [ {
2016
2951
  scope: "stage",
2017
2952
  entries: stageEntries
@@ -2026,7 +2961,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2026
2961
  read: read,
2027
2962
  where: where,
2028
2963
  hosts: hosts,
2029
- activityNames: activityNames
2964
+ activityEntries: activityEntries
2030
2965
  })
2031
2966
  });
2032
2967
  return;
@@ -2040,11 +2975,54 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2040
2975
  });
2041
2976
  }
2042
2977
 
2043
- function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityNames: activityNames}) {
2044
- const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityNames?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
2978
+ function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityEntries: activityEntries}) {
2979
+ const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityEntries?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
2045
2980
  return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty.${activityHint} Known: ${known.join(", ") || "(none)"}`;
2046
2981
  }
2047
2982
 
2983
+ function checkUpdateWhereOps(def, issues) {
2984
+ const workflow = def.fields ?? [];
2985
+ for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
2986
+ if (op.type !== "field.updateWhere") continue;
2987
+ const scopes = {
2988
+ workflow: workflow,
2989
+ stage: site.stage.fields ?? [],
2990
+ activity: site.activity.fields ?? []
2991
+ };
2992
+ checkUpdateWhereTargetKind({
2993
+ op: op,
2994
+ path: [ ...site.path, o ],
2995
+ label: site.label,
2996
+ scopes: scopes,
2997
+ issues: issues
2998
+ }), checkUpdateWhereMergeKeys({
2999
+ op: op,
3000
+ path: [ ...site.path, o ],
3001
+ label: site.label,
3002
+ issues: issues
3003
+ });
3004
+ }
3005
+ }
3006
+
3007
+ function checkUpdateWhereTargetKind({op: op, path: path, label: label, scopes: scopes, issues: issues}) {
3008
+ const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
3009
+ target === void 0 || target.type === "array" || issues.push({
3010
+ path: [ ...path, "target" ],
3011
+ message: `${label} field.updateWhere targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — updateWhere merges declared row sub-fields, so its target must be an \`array\` entry${rowOpsHint(target.type)}`
3012
+ });
3013
+ }
3014
+
3015
+ function rowOpsHint(type) {
3016
+ return type === "doc.refs" || type === "assignees" ? `; to add or drop ${type} rows use field.append / field.removeWhere` : "";
3017
+ }
3018
+
3019
+ function checkUpdateWhereMergeKeys({op: op, path: path, label: label, issues: issues}) {
3020
+ if (op.value.type === "object") for (const key of Object.keys(op.value.fields)) key !== "_key" && key !== "_type" || issues.push({
3021
+ path: [ ...path, "value", "fields", key ],
3022
+ message: `${label} field.updateWhere merge writes the reserved row key "${key}" — row identity and bookkeeping are engine-stamped, and a merge would restamp every matched row with the same literal`
3023
+ });
3024
+ }
3025
+
2048
3026
  function checkGuardFieldReads(def, issues) {
2049
3027
  const reads = {
2050
3028
  workflowEntries: def.fields ?? [],
@@ -2106,11 +3084,14 @@ function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, pa
2106
3084
  });
2107
3085
  }
2108
3086
 
2109
- function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
3087
+ function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues, nodes: nodes}) {
2110
3088
  if (read.path === void 0) return;
2111
3089
  const pathIssue = fieldValuePathIssue({
2112
3090
  target: target,
2113
- path: read.path
3091
+ path: read.path,
3092
+ ...nodes ? {
3093
+ nodes: nodes
3094
+ } : {}
2114
3095
  });
2115
3096
  pathIssue !== void 0 && issues.push({
2116
3097
  path: path,
@@ -2181,22 +3162,26 @@ const SCALAR = {
2181
3162
  kind: "rows",
2182
3163
  of: shape.of ?? []
2183
3164
  })
3165
+ }, START_ALLOWED_VALUE_NODES = {
3166
+ ...VALUE_NODES,
3167
+ "doc.ref": () => GDR_VALUE
2184
3168
  };
2185
3169
 
2186
- function valueNodeFor(shape) {
2187
- return Object.hasOwn(VALUE_NODES, shape.type) ? VALUE_NODES[shape.type](shape) : void 0;
3170
+ function valueNodeFor(shape, nodes) {
3171
+ return Object.hasOwn(nodes, shape.type) ? nodes[shape.type](shape) : void 0;
2188
3172
  }
2189
3173
 
2190
3174
  const INDEX_SEGMENT = /^\d+$/;
2191
3175
 
2192
- function stepIntoValueNode(node, segment) {
3176
+ function stepIntoValueNode(args) {
3177
+ const {node: node, segment: segment, nodes: nodes} = args;
2193
3178
  switch (node.kind) {
2194
3179
  case "scalar":
2195
3180
  return "the value is a scalar with no sub-paths";
2196
3181
 
2197
3182
  case "object":
2198
3183
  {
2199
- const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
3184
+ const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub, nodes);
2200
3185
  return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
2201
3186
  }
2202
3187
 
@@ -2217,18 +3202,24 @@ function stepIntoValueNode(node, segment) {
2217
3202
  }
2218
3203
  }
2219
3204
 
2220
- function fieldValuePathIssue({target: target, path: path}) {
2221
- let node = valueNodeFor(target);
3205
+ function fieldValuePathIssue({target: target, path: path, nodes: nodes = VALUE_NODES}) {
3206
+ let node = valueNodeFor(target, nodes);
2222
3207
  if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
2223
3208
  for (const segment of path.split(".")) {
2224
- const next = stepIntoValueNode(node, segment);
3209
+ const next = stepIntoValueNode({
3210
+ node: node,
3211
+ segment: segment,
3212
+ nodes: nodes
3213
+ });
2225
3214
  if (typeof next == "string") return `at segment "${segment}": ${next}`;
2226
3215
  node = next;
2227
3216
  }
2228
3217
  }
2229
3218
 
2230
3219
  function checkWorkflowInvariants(def) {
2231
- const issues = [], stageNames = checkStages(def, issues);
3220
+ const issues = [];
3221
+ checkDefinitionName(def, issues);
3222
+ const stageNames = checkStages(def, issues);
2232
3223
  return checkInitialStage({
2233
3224
  def: def,
2234
3225
  stageNames: stageNames,
@@ -2242,10 +3233,12 @@ function checkWorkflowInvariants(def) {
2242
3233
  stageNames: stageNames,
2243
3234
  issues: issues
2244
3235
  }), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
2245
- checkRequiredField(def, issues), checkPredicates(def, issues), checkUnboundCallerVars(def, issues),
2246
- checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues), checkFieldReadOpValues(def, issues),
2247
- checkGuardFieldReads(def, issues), checkAssigneesEntries(def, issues), checkActivityKinds(def, issues),
3236
+ checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
3237
+ checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3238
+ checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
3239
+ checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
3240
+ checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
2248
3241
  checkGroups(def, issues), issues;
2249
3242
  }
2250
3243
 
2251
- export { ACTIVITY_KINDS, ACTOR_KINDS, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EffectNotFoundError, EffectSchema, FIELD_READ, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GroupSchema, InstanceNotFoundError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, expandRequiredRoles, extractDocumentId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groupMembershipNames, isBareSeedId, isGdr, isGdrUri, isGuardReadExpr, isNotesEntry, isStartableDefinition, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, normalizeRoleAliases, parseGdr, parseOrThrow, parseResourceGdr, parseStoredDefinition, printGuardRead, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, selfGdr, tagScopeFilter, toBareId, toPhysicalGdr, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
3244
+ export { ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, MUTATION_GUARD_ACTIONS, NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, START_ALLOWED_VARS, START_FILTER_VARS, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };