@sanity/workflow-engine 0.15.0 → 0.16.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.
@@ -20,6 +20,26 @@ function _interopNamespaceCompat(e) {
20
20
 
21
21
  var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
22
22
 
23
+ function isCascadeFired(action) {
24
+ return action.when !== void 0;
25
+ }
26
+
27
+ function deriveActivityKind(activity) {
28
+ if (activity.target !== void 0) return "manual";
29
+ const actions = activity.actions ?? [];
30
+ return actions.some(a => !isCascadeFired(a)) ? "user" : actions.some(a => (a.effects ?? []).length > 0 || a.spawn !== void 0) ? "service" : actions.length > 0 ? "receive" : "script";
31
+ }
32
+
33
+ function deriveExecutorClassification(activity) {
34
+ if (activity.target !== void 0) return "off-system";
35
+ const actions = activity.actions ?? [], cascadeFired = actions.filter(isCascadeFired).length;
36
+ return cascadeFired === actions.length ? "autonomous" : cascadeFired === 0 ? "interactive" : "hybrid";
37
+ }
38
+
39
+ function driverKind(actor) {
40
+ return actor.kind === "person" || actor.kind === "agent" ? actor.kind : "service";
41
+ }
42
+
23
43
  function errorMessage(err) {
24
44
  return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
25
45
  }
@@ -397,6 +417,596 @@ function actorFulfillsRole({actorRoles: actorRoles, required: required, aliases:
397
417
  return actorRoles.some(role => accepted.has(role));
398
418
  }
399
419
 
420
+ function groq(strings, ...values) {
421
+ return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
422
+ }
423
+
424
+ function serializeGroqValue(value) {
425
+ const serialized = JSON.stringify(value);
426
+ if (serialized === void 0) throw new Error(`groq tag cannot serialize ${typeof value} — interpolate JSON-representable values only`);
427
+ return serialized;
428
+ }
429
+
430
+ function desugarWorkflow(authoring) {
431
+ const issues = [];
432
+ checkReservedRoleAliasKeys(authoring.roleAliases, issues);
433
+ const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
434
+ issues: issues,
435
+ claimFields: /* @__PURE__ */ new Map,
436
+ claimedFields: /* @__PURE__ */ new Set,
437
+ roleAliases: roleAliases
438
+ }, workflowFields2 = desugarFieldEntries({
439
+ entries: authoring.fields,
440
+ path: [ "fields" ],
441
+ ctx: ctx
442
+ }), workflowLayer = layerOf(workflowFields2), stages = authoring.stages.map((stage, i) => {
443
+ const path = [ "stages", i ], stageFields2 = desugarFieldEntries({
444
+ entries: stage.fields,
445
+ path: [ ...path, "fields" ],
446
+ ctx: ctx
447
+ }), stageEnv = {
448
+ layers: [ {
449
+ scope: "stage",
450
+ entries: layerOf(stageFields2)
451
+ }, {
452
+ scope: "workflow",
453
+ entries: workflowLayer
454
+ } ]
455
+ }, activities = (stage.activities ?? []).map((activity, j) => desugarActivity({
456
+ activity: activity,
457
+ path: [ ...path, "activities", j ],
458
+ stageEnv: stageEnv,
459
+ ctx: ctx
460
+ })), transitions = (stage.transitions ?? []).map(transition => desugarTransition({
461
+ transition: transition
462
+ })), editable = desugarStageEditable({
463
+ overrides: stage.editable,
464
+ inScope: editableFieldNames({
465
+ workflowFields: workflowFields2,
466
+ stageFields: stageFields2,
467
+ activities: activities
468
+ }),
469
+ path: [ ...path, "editable" ],
470
+ ctx: ctx
471
+ });
472
+ return {
473
+ ...stripUndefined({
474
+ name: stage.name,
475
+ title: stage.title,
476
+ description: stage.description,
477
+ groups: stage.groups,
478
+ guards: stage.guards?.map(desugarGuard)
479
+ }),
480
+ ...stageFields2 ? {
481
+ fields: stageFields2
482
+ } : {},
483
+ ...activities.length > 0 ? {
484
+ activities: activities
485
+ } : {},
486
+ ...transitions.length > 0 ? {
487
+ transitions: transitions
488
+ } : {},
489
+ ...editable ? {
490
+ editable: editable
491
+ } : {}
492
+ };
493
+ });
494
+ return checkUnclaimedClaimFields(ctx), {
495
+ definition: {
496
+ ...stripUndefined({
497
+ name: authoring.name,
498
+ title: authoring.title,
499
+ description: authoring.description,
500
+ groups: authoring.groups,
501
+ lifecycle: authoring.lifecycle,
502
+ start: desugarStart(authoring.start),
503
+ initialStage: authoring.initialStage,
504
+ predicates: authoring.predicates,
505
+ roleAliases: roleAliases
506
+ }),
507
+ ...workflowFields2 ? {
508
+ fields: workflowFields2
509
+ } : {},
510
+ stages: stages
511
+ },
512
+ issues: ctx.issues
513
+ };
514
+ }
515
+
516
+ function desugarStart(start) {
517
+ if (start !== void 0) return {
518
+ kind: start.kind ?? "interactive",
519
+ ...start.filter !== void 0 ? {
520
+ filter: start.filter
521
+ } : {}
522
+ };
523
+ }
524
+
525
+ function checkReservedRoleAliasKeys(aliases, issues) {
526
+ for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
527
+ path: [ "roleAliases", key ],
528
+ 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.`
529
+ });
530
+ }
531
+
532
+ const TODOLIST_OF = [ {
533
+ type: "string",
534
+ name: "label",
535
+ title: "Label"
536
+ }, {
537
+ type: "string",
538
+ name: "status",
539
+ title: "Status"
540
+ }, {
541
+ type: "assignee",
542
+ name: "assignee",
543
+ title: "Assignee"
544
+ }, {
545
+ type: "date",
546
+ name: "dueDate",
547
+ title: "Due date"
548
+ } ], NOTES_OF = [ {
549
+ type: "text",
550
+ name: "body",
551
+ title: "Body"
552
+ }, {
553
+ type: "actor",
554
+ name: "actor",
555
+ title: "Actor"
556
+ }, {
557
+ type: "datetime",
558
+ name: "at",
559
+ title: "At"
560
+ } ];
561
+
562
+ function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
563
+ return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
564
+ entry: entry,
565
+ path: [ ...path, i ],
566
+ ctx: ctx
567
+ }));
568
+ }
569
+
570
+ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
571
+ if (entry.type === "claim") return desugarClaimField({
572
+ entry: entry,
573
+ path: path,
574
+ ctx: ctx
575
+ });
576
+ if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
577
+ entry: entry,
578
+ path: path,
579
+ ctx: ctx
580
+ });
581
+ const editable = normalizeEditable({
582
+ editable: entry.editable,
583
+ path: [ ...path, "editable" ],
584
+ ctx: ctx
585
+ });
586
+ return {
587
+ ...stripUndefined({
588
+ type: entry.type,
589
+ name: entry.name,
590
+ title: entry.title,
591
+ description: entry.description,
592
+ group: normalizeGroup(entry.group),
593
+ required: entry.required,
594
+ initialValue: entry.initialValue,
595
+ editable: editable,
596
+ types: entry.types,
597
+ fields: entry.fields,
598
+ of: entry.of
599
+ })
600
+ };
601
+ }
602
+
603
+ function desugarClaimField({entry: entry, path: path, ctx: ctx}) {
604
+ const desugared = {
605
+ ...stripUndefined({
606
+ name: entry.name,
607
+ title: entry.title,
608
+ description: entry.description,
609
+ group: normalizeGroup(entry.group)
610
+ }),
611
+ type: "actor"
612
+ };
613
+ return ctx.claimFields.set(desugared, {
614
+ name: entry.name,
615
+ path: path
616
+ }), desugared;
617
+ }
618
+
619
+ function desugarListField({entry: entry, path: path, ctx: ctx}) {
620
+ const editable = normalizeEditable({
621
+ editable: entry.editable,
622
+ path: [ ...path, "editable" ],
623
+ ctx: ctx
624
+ });
625
+ return {
626
+ ...stripUndefined({
627
+ name: entry.name,
628
+ title: entry.title,
629
+ description: entry.description,
630
+ group: normalizeGroup(entry.group),
631
+ required: entry.required,
632
+ initialValue: entry.initialValue,
633
+ editable: editable
634
+ }),
635
+ type: "array",
636
+ of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
637
+ };
638
+ }
639
+
640
+ function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
641
+ if (editable === void 0 || editable === !0) return editable;
642
+ if (Array.isArray(editable)) {
643
+ const condition = rolesCondition(editable, ctx.roleAliases);
644
+ if (condition === void 0) {
645
+ ctx.issues.push({
646
+ path: path,
647
+ message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
648
+ });
649
+ return;
650
+ }
651
+ return condition;
652
+ }
653
+ return editable;
654
+ }
655
+
656
+ function editableFieldNames({workflowFields: workflowFields2, stageFields: stageFields2, activities: activities}) {
657
+ const names = /* @__PURE__ */ new Set, collect = entries => {
658
+ for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
659
+ };
660
+ collect(workflowFields2), collect(stageFields2);
661
+ for (const activity of activities) collect(activity.fields);
662
+ return names;
663
+ }
664
+
665
+ function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
666
+ if (overrides === void 0) return;
667
+ const out = {};
668
+ for (const [name, value] of Object.entries(overrides)) {
669
+ if (!inScope.has(name)) {
670
+ ctx.issues.push({
671
+ path: [ ...path, name ],
672
+ 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\``
673
+ });
674
+ continue;
675
+ }
676
+ const normalized = normalizeEditable({
677
+ editable: value,
678
+ path: [ ...path, name ],
679
+ ctx: ctx
680
+ });
681
+ normalized !== void 0 && (out[name] = normalized);
682
+ }
683
+ return Object.keys(out).length > 0 ? out : void 0;
684
+ }
685
+
686
+ function layerOf(entries) {
687
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
688
+ }
689
+
690
+ function normalizeGroup(group) {
691
+ return group === void 0 || Array.isArray(group) ? group : [ group ];
692
+ }
693
+
694
+ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
695
+ const activityFields2 = desugarFieldEntries({
696
+ entries: activity.fields,
697
+ path: [ ...path, "fields" ],
698
+ ctx: ctx
699
+ }), env = {
700
+ layers: [ {
701
+ scope: "activity",
702
+ entries: layerOf(activityFields2)
703
+ }, ...stageEnv.layers ]
704
+ }, actions = (activity.actions ?? []).map((action, a) => desugarAction({
705
+ action: action,
706
+ path: [ ...path, "actions", a ],
707
+ env: env,
708
+ activityName: activity.name,
709
+ ctx: ctx
710
+ })), target = desugarTarget({
711
+ target: activity.target,
712
+ env: env,
713
+ path: [ ...path, "target" ],
714
+ ctx: ctx
715
+ });
716
+ return {
717
+ ...stripUndefined({
718
+ name: activity.name,
719
+ title: activity.title,
720
+ description: activity.description,
721
+ groups: activity.groups,
722
+ group: normalizeGroup(activity.group),
723
+ filter: activity.filter,
724
+ requirements: activity.requirements
725
+ }),
726
+ ...target ? {
727
+ target: target
728
+ } : {},
729
+ ...activityFields2 ? {
730
+ fields: activityFields2
731
+ } : {},
732
+ ...actions.length > 0 ? {
733
+ actions: actions
734
+ } : {}
735
+ };
736
+ }
737
+
738
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
739
+
740
+ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
741
+ if (target === void 0 || target.type === "url") return target;
742
+ const ref = typeof target.field == "string" ? {
743
+ field: target.field
744
+ } : target.field, field = resolveRef({
745
+ ref: ref,
746
+ env: env,
747
+ path: [ ...path, "field" ],
748
+ ctx: ctx
749
+ }) ?? fallbackRef(ref), entry = entryAt(env, field);
750
+ return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
751
+ path: [ ...path, "field" ],
752
+ message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
753
+ }), {
754
+ type: "field",
755
+ field: field
756
+ };
757
+ }
758
+
759
+ function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
760
+ if ("type" in action) return desugarClaimAction({
761
+ action: action,
762
+ path: path,
763
+ env: env,
764
+ ctx: ctx
765
+ });
766
+ action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
767
+ path: [ ...path, "roles" ],
768
+ message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
769
+ });
770
+ const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
771
+ ops: action.ops,
772
+ path: [ ...path, "ops" ],
773
+ env: env,
774
+ firingActivity: activityName,
775
+ ctx: ctx
776
+ }) ?? [];
777
+ return action.status !== void 0 && ops.push({
778
+ type: "status.set",
779
+ activity: activityName,
780
+ status: action.status
781
+ }), {
782
+ ...stripUndefined({
783
+ name: action.name,
784
+ title: action.title,
785
+ description: action.description,
786
+ group: normalizeGroup(action.group),
787
+ when: action.when,
788
+ params: action.params,
789
+ effects: action.effects,
790
+ spawn: action.spawn
791
+ }),
792
+ ...cascadeFired && action.roles !== void 0 && action.roles.length > 0 ? {
793
+ roles: action.roles
794
+ } : {},
795
+ ...filter ? {
796
+ filter: filter
797
+ } : {},
798
+ ...ops.length > 0 ? {
799
+ ops: ops
800
+ } : {}
801
+ };
802
+ }
803
+
804
+ function desugarClaimAction({action: action, path: path, env: env, ctx: ctx}) {
805
+ const ref = typeof action.field == "string" ? {
806
+ field: action.field
807
+ } : action.field, resolved = resolveRef({
808
+ ref: ref,
809
+ env: env,
810
+ path: [ ...path, "field" ],
811
+ ctx: ctx
812
+ });
813
+ if (resolved) {
814
+ const entry = entryAt(env, resolved);
815
+ entry && entry.type !== "actor" && ctx.issues.push({
816
+ path: [ ...path, "field" ],
817
+ 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)`
818
+ }), checkShadowedClaimTarget({
819
+ actionName: action.name,
820
+ field: ref.field,
821
+ resolved: resolved,
822
+ env: env,
823
+ path: [ ...path, "field" ],
824
+ ctx: ctx
825
+ }), entry && ctx.claimedFields.add(entry);
826
+ }
827
+ const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([ noSteal, rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = resolved ? [ {
828
+ type: "field.set",
829
+ target: resolved,
830
+ value: {
831
+ type: "actor"
832
+ }
833
+ } ] : [];
834
+ return {
835
+ ...stripUndefined({
836
+ name: action.name,
837
+ title: action.title,
838
+ description: action.description,
839
+ group: normalizeGroup(action.group),
840
+ params: action.params,
841
+ effects: action.effects
842
+ }),
843
+ filter: filter,
844
+ ...ops.length > 0 ? {
845
+ ops: ops
846
+ } : {}
847
+ };
848
+ }
849
+
850
+ function checkShadowedClaimTarget({actionName: actionName, field: field, resolved: resolved, env: env, path: path, ctx: ctx}) {
851
+ const nearest = env.layers.find(l => l.entries.has(field));
852
+ nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
853
+ path: path,
854
+ 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`
855
+ });
856
+ }
857
+
858
+ function checkUnclaimedClaimFields(ctx) {
859
+ for (const [entry, {name: name, path: path}] of ctx.claimFields) ctx.claimedFields.has(entry) || ctx.issues.push({
860
+ path: path,
861
+ 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`
862
+ });
863
+ }
864
+
865
+ const DEFAULT_TRANSITION_WHEN = "$allActivitiesDone";
866
+
867
+ function desugarTransition({transition: transition}) {
868
+ return {
869
+ ...stripUndefined({
870
+ name: transition.name,
871
+ title: transition.title,
872
+ description: transition.description,
873
+ to: transition.to
874
+ }),
875
+ when: transition.when ?? DEFAULT_TRANSITION_WHEN
876
+ };
877
+ }
878
+
879
+ function desugarGuard(guard) {
880
+ const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
881
+ return {
882
+ ...rest,
883
+ match: {
884
+ ...matchRest,
885
+ ...idRefs !== void 0 ? {
886
+ idRefs: idRefs.map(printGuardRead)
887
+ } : {}
888
+ },
889
+ ...metadata !== void 0 ? {
890
+ metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
891
+ } : {}
892
+ };
893
+ }
894
+
895
+ function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
896
+ if (ops) return ops.map((op, i) => desugarOp({
897
+ op: op,
898
+ path: [ ...path, i ],
899
+ env: env,
900
+ firingActivity: firingActivity,
901
+ ctx: ctx
902
+ }));
903
+ }
904
+
905
+ function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
906
+ if (op.type === "status.set") return {
907
+ type: "status.set",
908
+ activity: op.activity ?? firingActivity,
909
+ status: op.status
910
+ };
911
+ if (op.type === "audit") return desugarAuditOp({
912
+ op: op,
913
+ path: path,
914
+ env: env,
915
+ ctx: ctx
916
+ });
917
+ const target = resolveRef({
918
+ ref: op.target,
919
+ env: env,
920
+ path: [ ...path, "target" ],
921
+ ctx: ctx
922
+ }) ?? fallbackRef(op.target);
923
+ return {
924
+ ...op,
925
+ target: target
926
+ };
927
+ }
928
+
929
+ const AUDIT_STAMPS = {
930
+ actor: {
931
+ type: "actor"
932
+ },
933
+ at: {
934
+ type: "now"
935
+ }
936
+ };
937
+
938
+ function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
939
+ const target = resolveRef({
940
+ ref: op.target,
941
+ env: env,
942
+ path: [ ...path, "target" ],
943
+ ctx: ctx
944
+ }) ?? fallbackRef(op.target);
945
+ if (op.value.type !== "object") return ctx.issues.push({
946
+ path: [ ...path, "value" ],
947
+ message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
948
+ }), {
949
+ type: "field.append",
950
+ target: target,
951
+ value: op.value
952
+ };
953
+ const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
954
+ ...op.value.fields
955
+ };
956
+ for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
957
+ if (stamp in fields) {
958
+ ctx.issues.push({
959
+ path: [ ...path, "value", "fields", stamp ],
960
+ message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
961
+ });
962
+ continue;
963
+ }
964
+ fields[stamp] = source;
965
+ }
966
+ return {
967
+ type: "field.append",
968
+ target: target,
969
+ value: {
970
+ type: "object",
971
+ fields: fields
972
+ }
973
+ };
974
+ }
975
+
976
+ function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
977
+ const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
978
+ for (const layer of layers) if (layer.entries.has(ref.field)) return {
979
+ scope: layer.scope,
980
+ field: ref.field
981
+ };
982
+ const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
983
+ ctx.issues.push({
984
+ path: path,
985
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
986
+ });
987
+ }
988
+
989
+ function entryAt(env, ref) {
990
+ return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
991
+ }
992
+
993
+ function fallbackRef(ref) {
994
+ return {
995
+ scope: ref.scope ?? "workflow",
996
+ field: ref.field
997
+ };
998
+ }
999
+
1000
+ function rolesCondition(roles, aliases) {
1001
+ if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
1002
+ }
1003
+
1004
+ function stripUndefined(obj) {
1005
+ const out = {};
1006
+ for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
1007
+ return out;
1008
+ }
1009
+
400
1010
  function isUnevaluable(result) {
401
1011
  return result == null;
402
1012
  }
@@ -416,9 +1026,9 @@ async function evaluateCondition(args) {
416
1026
 
417
1027
  async function evaluatePredicates(args) {
418
1028
  const out = {};
419
- for (const [name, groq] of Object.entries(args.predicates ?? {})) {
1029
+ for (const [name, groq2] of Object.entries(args.predicates ?? {})) {
420
1030
  const result = await runGroq({
421
- groq: groq,
1031
+ groq: groq2,
422
1032
  params: args.params,
423
1033
  snapshot: args.snapshot
424
1034
  }), outcome = groqConditionDescribe.conditionOutcome(result);
@@ -427,46 +1037,46 @@ async function evaluatePredicates(args) {
427
1037
  return out;
428
1038
  }
429
1039
 
430
- async function runGroq({groq: groq, params: params, snapshot: snapshot}) {
1040
+ async function runGroq({groq: groq2, params: params, snapshot: snapshot}) {
431
1041
  return groqConditionDescribe.runGroq({
432
- groq: groq,
1042
+ groq: groq2,
433
1043
  params: params,
434
1044
  dataset: snapshot.docs
435
1045
  });
436
1046
  }
437
1047
 
438
- function conditionSyntaxIssues(groq, boundVars) {
1048
+ function conditionSyntaxIssues(groq2, boundVars) {
439
1049
  const issues = [];
440
1050
  let tree;
441
1051
  try {
442
- tree = groqJs.parse(groq);
1052
+ tree = groqJs.parse(groq2);
443
1053
  } catch (err) {
444
1054
  issues.push(errorMessage(err));
445
1055
  }
446
- 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.'),
447
- 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(", "));
1056
+ 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."),
1057
+ 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(", "));
448
1058
  return issues;
449
1059
  }
450
1060
 
451
- function conditionParameterNames(groq) {
1061
+ function conditionParameterNames(groq2) {
452
1062
  const read = /* @__PURE__ */ new Set;
453
- return walkAstNodes(tryParseGroq(groq), node => {
1063
+ return walkAstNodes(tryParseGroq(groq2), node => {
454
1064
  node.type === "Parameter" && typeof node.name == "string" && read.add(node.name);
455
1065
  }), read;
456
1066
  }
457
1067
 
458
- function conditionFieldReadNames(groq) {
1068
+ function conditionFieldReadNames(groq2) {
459
1069
  const read = /* @__PURE__ */ new Set;
460
- return walkAstNodes(tryParseGroq(groq), node => {
1070
+ return walkAstNodes(tryParseGroq(groq2), node => {
461
1071
  if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
462
1072
  const base = node.base;
463
1073
  base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
464
1074
  }), read;
465
1075
  }
466
1076
 
467
- function conditionEffectReads(groq) {
1077
+ function conditionEffectReads(groq2) {
468
1078
  const reads = [];
469
- return walkAstNodes(tryParseGroq(groq), node => {
1079
+ return walkAstNodes(tryParseGroq(groq2), node => {
470
1080
  if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
471
1081
  const base = node.base;
472
1082
  base?.type !== "AccessAttribute" || typeof base.name != "string" || base.base?.type === "Parameter" && base.base.name === "effects" && reads.push({
@@ -476,9 +1086,34 @@ function conditionEffectReads(groq) {
476
1086
  }), reads;
477
1087
  }
478
1088
 
479
- function tryParseGroq(groq) {
1089
+ function readsRootDocument(groq2) {
1090
+ return nodeReadsRoot(tryParseGroq(groq2), 0);
1091
+ }
1092
+
1093
+ const SCOPED_CHILD = {
1094
+ Filter: "expr",
1095
+ Projection: "expr",
1096
+ Map: "expr",
1097
+ FlatMap: "expr",
1098
+ PipeFuncCall: "args"
1099
+ };
1100
+
1101
+ function nodeReadsRoot(node, depth) {
1102
+ if (Array.isArray(node)) return node.some(item => nodeReadsRoot(item, depth));
1103
+ if (typeof node != "object" || node === null) return !1;
1104
+ const typed = node;
1105
+ if (depth === 0 && typed.type === "This" || depth === 0 && typed.type === "AccessAttribute" && typed.base === void 0) return !0;
1106
+ if (typed.type === "Parent") {
1107
+ const climb = typeof typed.n == "number" ? typed.n : 1;
1108
+ return depth - climb <= 0;
1109
+ }
1110
+ const scopedChild = typeof typed.type == "string" ? SCOPED_CHILD[typed.type] : void 0;
1111
+ 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));
1112
+ }
1113
+
1114
+ function tryParseGroq(groq2) {
480
1115
  try {
481
- return groqJs.parse(groq);
1116
+ return groqJs.parse(groq2);
482
1117
  } catch {
483
1118
  return;
484
1119
  }
@@ -495,7 +1130,13 @@ function walkAstNodes(node, visit) {
495
1130
  }
496
1131
  }
497
1132
 
498
- const CONDITION_VARS = [ {
1133
+ const ACTIVITY_STATUSES = [ "active", "done", "skipped", "failed" ], TERMINAL_ACTIVITY_STATUSES = [ "done", "skipped", "failed" ];
1134
+
1135
+ function isTerminalActivityStatus(status) {
1136
+ return TERMINAL_ACTIVITY_STATUSES.includes(status);
1137
+ }
1138
+
1139
+ 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 = [ {
499
1140
  name: "self",
500
1141
  binding: "always",
501
1142
  label: "this workflow instance",
@@ -525,11 +1166,16 @@ const CONDITION_VARS = [ {
525
1166
  binding: "always",
526
1167
  label: "the current time",
527
1168
  description: "The ISO clock reading shared by every condition in one evaluation pass, so they agree on the time."
1169
+ }, {
1170
+ name: "context",
1171
+ binding: "always",
1172
+ label: "start-time context",
1173
+ 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."
528
1174
  }, {
529
1175
  name: "effects",
530
1176
  binding: "always",
531
1177
  label: "automation outputs",
532
- 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."
1178
+ 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."
533
1179
  }, {
534
1180
  name: "effectStatus",
535
1181
  binding: "always",
@@ -564,23 +1210,35 @@ const CONDITION_VARS = [ {
564
1210
  name: "can",
565
1211
  binding: "caller",
566
1212
  label: "your permissions",
567
- 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."
1213
+ 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."
568
1214
  }, {
569
1215
  name: "row",
570
1216
  binding: "spawn",
571
1217
  label: "the spawned row",
572
- 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)."
1218
+ 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)."
573
1219
  }, {
574
1220
  name: "params",
575
1221
  binding: "caller",
576
1222
  label: "the action's arguments",
577
- 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."
1223
+ 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."
578
1224
  }, {
579
1225
  name: "subworkflows",
580
1226
  binding: "always",
581
1227
  label: "the spawned subworkflows",
582
- 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`."
583
- } ], 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 = [ {
1228
+ 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`."
1229
+ } ], 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 = [ {
1230
+ name: "tag",
1231
+ description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1232
+ }, {
1233
+ name: "definition",
1234
+ description: "The `name` of the definition under evaluation (its own start.filter binds it)."
1235
+ }, {
1236
+ name: "now",
1237
+ description: "The ISO clock reading of the evaluating engine."
1238
+ }, {
1239
+ name: "fields",
1240
+ description: "The candidate initialFields by entry name, when the read surface has them (the Studio start control: yes; a per-doc picker: no). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble."
1241
+ } ], GUARD_PREDICATE_VARS = [ {
584
1242
  name: "guard",
585
1243
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
586
1244
  }, {
@@ -803,13 +1461,7 @@ function formatIssues(issues) {
803
1461
  });
804
1462
  }
805
1463
 
806
- const ACTIVITY_STATUSES = [ "pending", "active", "done", "skipped", "failed" ], TERMINAL_ACTIVITY_STATUSES = [ "done", "skipped", "failed" ];
807
-
808
- function isTerminalActivityStatus(status) {
809
- return TERMINAL_ACTIVITY_STATUSES.includes(status);
810
- }
811
-
812
- 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__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "must be a non-empty string")), PositiveInt = v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1464
+ const NonEmpty = v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "must be a non-empty string")), PositiveInt = v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
813
1465
 
814
1466
  function groqIdentifier(referencedAs) {
815
1467
  return v__namespace.pipe(v__namespace.string(), v__namespace.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`));
@@ -908,7 +1560,7 @@ const StoredFieldOpSchema = v__namespace.variant("type", [ ...opSchemas(StoredFi
908
1560
  type: v__namespace.literal("status.set"),
909
1561
  activity: NonEmpty,
910
1562
  status: picklist(ACTIVITY_STATUSES)
911
- }) ]), StoredTransitionOpSchema = StoredFieldOpSchema, AuditOpSchema = v__namespace.strictObject({
1563
+ }) ]), AuditOpSchema = v__namespace.strictObject({
912
1564
  type: v__namespace.literal("audit"),
913
1565
  target: AuthoringFieldRefSchema,
914
1566
  value: ValueExprSchema,
@@ -1036,6 +1688,15 @@ const TodoListFieldSchema = pinned()(v__namespace.strictObject(listSugarFields("
1036
1688
  bindings: v__namespace.optional(v__namespace.record(v__namespace.string(), ConditionSchema)),
1037
1689
  input: v__namespace.optional(v__namespace.record(v__namespace.string(), v__namespace.unknown())),
1038
1690
  outputs: v__namespace.optional(v__namespace.array(FieldShapeSchema))
1691
+ }), DefinitionRefSchema = v__namespace.strictObject({
1692
+ name: NonEmpty,
1693
+ version: v__namespace.optional(v__namespace.union([ PositiveInt, v__namespace.literal("latest") ]))
1694
+ }), SubworkflowsSchema = v__namespace.strictObject({
1695
+ forEach: NonEmpty,
1696
+ definition: DefinitionRefSchema,
1697
+ with: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1698
+ context: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1699
+ onExit: v__namespace.optional(picklist([ "detach", "abort" ]))
1039
1700
  }), ActionParamSchema = v__namespace.strictObject({
1040
1701
  type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
1041
1702
  name: NonEmpty,
@@ -1050,14 +1711,19 @@ function actionFields(op, group) {
1050
1711
  title: v__namespace.optional(v__namespace.string()),
1051
1712
  description: v__namespace.optional(v__namespace.string()),
1052
1713
  group: v__namespace.optional(group),
1714
+ when: v__namespace.optional(ConditionSchema),
1053
1715
  filter: v__namespace.optional(ConditionSchema),
1054
1716
  params: v__namespace.optional(v__namespace.array(ActionParamSchema)),
1055
1717
  ops: v__namespace.optional(v__namespace.array(op)),
1056
- effects: v__namespace.optional(v__namespace.array(EffectSchema))
1718
+ effects: v__namespace.optional(v__namespace.array(EffectSchema)),
1719
+ spawn: v__namespace.optional(SubworkflowsSchema)
1057
1720
  };
1058
1721
  }
1059
1722
 
1060
- const StoredActionSchema = pinned()(v__namespace.strictObject(actionFields(StoredOpSchema, StoredGroupMembershipSchema))), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v__namespace.strictObject({
1723
+ const StoredActionSchema = pinned()(v__namespace.strictObject({
1724
+ ...actionFields(StoredOpSchema, StoredGroupMembershipSchema),
1725
+ roles: v__namespace.optional(v__namespace.array(NonEmpty))
1726
+ })), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v__namespace.strictObject({
1061
1727
  ...actionFields(AuthoringOpSchema, AuthoringGroupMembershipSchema),
1062
1728
  roles: v__namespace.optional(v__namespace.array(NonEmpty)),
1063
1729
  status: v__namespace.optional(TerminalActivityStatus)
@@ -1072,35 +1738,19 @@ const StoredActionSchema = pinned()(v__namespace.strictObject(actionFields(Store
1072
1738
  filter: v__namespace.optional(ConditionSchema),
1073
1739
  params: v__namespace.optional(v__namespace.array(ActionParamSchema)),
1074
1740
  effects: v__namespace.optional(v__namespace.array(EffectSchema))
1075
- })), AuthoringActionSchema = pinned()(v__namespace.union([ RawAuthoringActionSchema, ClaimActionSchema ])), DefinitionRefSchema = v__namespace.strictObject({
1076
- name: NonEmpty,
1077
- version: v__namespace.optional(v__namespace.union([ PositiveInt, v__namespace.literal("latest") ]))
1078
- }), SubworkflowsSchema = v__namespace.strictObject({
1079
- forEach: NonEmpty,
1080
- definition: DefinitionRefSchema,
1081
- with: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1082
- context: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1083
- onExit: v__namespace.optional(picklist([ "detach", "abort" ]))
1084
- });
1741
+ })), AuthoringActionSchema = pinned()(v__namespace.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema));
1085
1742
 
1086
- function activityFields({field: field, action: action, op: op, target: target, group: group}) {
1743
+ function activityFields({field: field, action: action, target: target, group: group}) {
1087
1744
  return {
1088
1745
  name: NonEmpty,
1089
1746
  title: v__namespace.optional(v__namespace.string()),
1090
1747
  description: v__namespace.optional(v__namespace.string()),
1091
1748
  groups: v__namespace.optional(v__namespace.array(GroupSchema)),
1092
1749
  group: v__namespace.optional(group),
1093
- kind: v__namespace.optional(picklist(ACTIVITY_KINDS)),
1094
1750
  target: v__namespace.optional(target),
1095
- activation: v__namespace.optional(picklist([ "auto", "manual" ])),
1096
1751
  filter: v__namespace.optional(ConditionSchema),
1097
1752
  requirements: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1098
- completeWhen: v__namespace.optional(ConditionSchema),
1099
- failWhen: v__namespace.optional(ConditionSchema),
1100
- ops: v__namespace.optional(v__namespace.array(op)),
1101
- effects: v__namespace.optional(v__namespace.array(EffectSchema)),
1102
1753
  actions: v__namespace.optional(v__namespace.array(action)),
1103
- subworkflows: v__namespace.optional(SubworkflowsSchema),
1104
1754
  fields: v__namespace.optional(v__namespace.array(field))
1105
1755
  };
1106
1756
  }
@@ -1108,30 +1758,26 @@ function activityFields({field: field, action: action, op: op, target: target, g
1108
1758
  const StoredActivitySchema = pinned()(v__namespace.strictObject(activityFields({
1109
1759
  field: FieldEntrySchema,
1110
1760
  action: StoredActionSchema,
1111
- op: StoredOpSchema,
1112
1761
  target: StoredManualTargetSchema,
1113
1762
  group: StoredGroupMembershipSchema
1114
1763
  }))), AuthoringActivitySchema = pinned()(v__namespace.strictObject(activityFields({
1115
1764
  field: AuthoringFieldEntrySchema,
1116
1765
  action: AuthoringActionSchema,
1117
- op: AuthoringOpSchema,
1118
1766
  target: AuthoringManualTargetSchema,
1119
1767
  group: AuthoringGroupMembershipSchema
1120
1768
  })));
1121
1769
 
1122
- function transitionFields(op, filter) {
1770
+ function transitionFields(when) {
1123
1771
  return {
1124
1772
  name: NonEmpty,
1125
1773
  title: v__namespace.optional(v__namespace.string()),
1126
1774
  description: v__namespace.optional(v__namespace.string()),
1127
1775
  to: NonEmpty,
1128
- filter: filter,
1129
- ops: v__namespace.optional(v__namespace.array(op)),
1130
- effects: v__namespace.optional(v__namespace.array(EffectSchema))
1776
+ when: when
1131
1777
  };
1132
1778
  }
1133
1779
 
1134
- const StoredTransitionSchema = pinned()(v__namespace.strictObject(transitionFields(StoredTransitionOpSchema, ConditionSchema))), AuthoringTransitionOpSchema = v__namespace.variant("type", [ ...opSchemas(AuthoringFieldRefSchema), AuditOpSchema ]), AuthoringTransitionSchema = pinned()(v__namespace.strictObject(transitionFields(AuthoringTransitionOpSchema, v__namespace.optional(ConditionSchema)))), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardReadPath = v__namespace.pipe(NonEmpty, v__namespace.check(path => !/[\r\n\u2028\u2029]/.test(path), "a guard read path cannot contain a line break")), GuardReadSchema = v__namespace.variant("type", [ v__namespace.strictObject({
1780
+ const StoredTransitionSchema = pinned()(v__namespace.strictObject(transitionFields(ConditionSchema))), AuthoringTransitionSchema = pinned()(v__namespace.strictObject(transitionFields(v__namespace.optional(ConditionSchema)))), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardReadPath = v__namespace.pipe(NonEmpty, v__namespace.check(path => !/[\r\n\u2028\u2029]/.test(path), "a guard read path cannot contain a line break")), GuardReadSchema = v__namespace.variant("type", [ v__namespace.strictObject({
1135
1781
  type: v__namespace.literal("self")
1136
1782
  }), v__namespace.strictObject({
1137
1783
  type: v__namespace.literal("now")
@@ -1193,16 +1839,25 @@ const StoredStageSchema = pinned()(v__namespace.strictObject(stageFields({
1193
1839
  transition: AuthoringTransitionSchema,
1194
1840
  guard: AuthoringGuardSchema,
1195
1841
  editable: AuthoringEditableSchema
1196
- }))), RoleAliasesSchema = v__namespace.record(NonEmpty, v__namespace.pipe(v__namespace.array(NonEmpty), v__namespace.minLength(1, "a role alias must list at least one fulfilling role"))), WORKFLOW_LIFECYCLES = [ "standalone", "child" ];
1842
+ }))), RoleAliasesSchema = v__namespace.record(NonEmpty, v__namespace.pipe(v__namespace.array(NonEmpty), v__namespace.minLength(1, "a role alias must list at least one fulfilling role"))), WORKFLOW_LIFECYCLES = [ "standalone", "child" ], START_KINDS = [ "interactive", "autonomous" ];
1843
+
1844
+ function startFields(kind) {
1845
+ return {
1846
+ kind: kind,
1847
+ filter: v__namespace.optional(ConditionSchema)
1848
+ };
1849
+ }
1850
+
1851
+ const StoredStartSchema = pinned()(v__namespace.strictObject(startFields(picklist(START_KINDS)))), AuthoringStartSchema = pinned()(v__namespace.strictObject(startFields(v__namespace.optional(picklist(START_KINDS)))));
1197
1852
 
1198
- function workflowFields(field, stage) {
1853
+ function workflowFields({field: field, stage: stage, start: start}) {
1199
1854
  return {
1200
1855
  name: NonEmpty,
1201
1856
  title: NonEmpty,
1202
1857
  description: v__namespace.optional(v__namespace.string()),
1203
1858
  groups: v__namespace.optional(v__namespace.array(GroupSchema)),
1204
1859
  lifecycle: v__namespace.optional(picklist(WORKFLOW_LIFECYCLES)),
1205
- applicableWhen: v__namespace.optional(ConditionSchema),
1860
+ start: v__namespace.optional(start),
1206
1861
  initialStage: NonEmpty,
1207
1862
  fields: v__namespace.optional(v__namespace.array(field)),
1208
1863
  stages: v__namespace.pipe(v__namespace.array(stage), v__namespace.minLength(1, "must declare at least one stage")),
@@ -1211,7 +1866,11 @@ function workflowFields(field, stage) {
1211
1866
  };
1212
1867
  }
1213
1868
 
1214
- const WorkflowDefinitionSchema = pinned()(v__namespace.strictObject(workflowFields(FieldEntrySchema, StoredStageSchema)));
1869
+ const WorkflowDefinitionSchema = pinned()(v__namespace.strictObject(workflowFields({
1870
+ field: FieldEntrySchema,
1871
+ stage: StoredStageSchema,
1872
+ start: StoredStartSchema
1873
+ })));
1215
1874
 
1216
1875
  function parseStoredDefinition(input, label) {
1217
1876
  return parseOrThrow({
@@ -1221,12 +1880,28 @@ function parseStoredDefinition(input, label) {
1221
1880
  });
1222
1881
  }
1223
1882
 
1224
- const AuthoringWorkflowSchema = pinned()(v__namespace.strictObject(workflowFields(AuthoringFieldEntrySchema, AuthoringStageSchema))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
1883
+ const AuthoringWorkflowSchema = pinned()(v__namespace.strictObject(workflowFields({
1884
+ field: AuthoringFieldEntrySchema,
1885
+ stage: AuthoringStageSchema,
1886
+ start: AuthoringStartSchema
1887
+ }))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
1225
1888
 
1226
1889
  function isStartableDefinition(definition) {
1227
1890
  return definition.lifecycle !== "child";
1228
1891
  }
1229
1892
 
1893
+ function startKindOf(definition) {
1894
+ return definition.start?.kind ?? "interactive";
1895
+ }
1896
+
1897
+ function isSubjectEntry(entry) {
1898
+ return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
1899
+ }
1900
+
1901
+ function isInputSourced(entry) {
1902
+ return entry.initialValue?.type === "input";
1903
+ }
1904
+
1230
1905
  function formatValidationError(label, issues) {
1231
1906
  const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1232
1907
  return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
@@ -1329,12 +2004,6 @@ function checkActivities({def: def, i: i, activityNames: activityNames, issues:
1329
2004
  }
1330
2005
 
1331
2006
  function checkStatusSetTargets({activity: activity, activityNames: activityNames, path: path, issues: issues}) {
1332
- checkStatusSetOps({
1333
- ops: activity.ops,
1334
- activityNames: activityNames,
1335
- path: [ ...path, "ops" ],
1336
- issues: issues
1337
- });
1338
2007
  for (const [a, action] of (activity.actions ?? []).entries()) checkStatusSetOps({
1339
2008
  ops: action.ops,
1340
2009
  activityNames: activityNames,
@@ -1382,25 +2051,11 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
1382
2051
 
1383
2052
  function effectNameSites(def) {
1384
2053
  const sites = [];
1385
- for (const [i, stage] of def.stages.entries()) {
1386
- for (const [j, activity] of (stage.activities ?? []).entries()) {
1387
- collectEffects({
1388
- effects: activity.effects,
1389
- path: [ "stages", i, "activities", j, "effects" ],
1390
- sites: sites
1391
- });
1392
- for (const [a, action] of (activity.actions ?? []).entries()) collectEffects({
1393
- effects: action.effects,
1394
- path: [ "stages", i, "activities", j, "actions", a, "effects" ],
1395
- sites: sites
1396
- });
1397
- }
1398
- for (const [k, t] of (stage.transitions ?? []).entries()) collectEffects({
1399
- effects: t.effects,
1400
- path: [ "stages", i, "transitions", k, "effects" ],
1401
- sites: sites
1402
- });
1403
- }
2054
+ 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({
2055
+ effects: action.effects,
2056
+ path: [ "stages", i, "activities", j, "actions", a, "effects" ],
2057
+ sites: sites
2058
+ });
1404
2059
  return sites;
1405
2060
  }
1406
2061
 
@@ -1487,16 +2142,16 @@ function checkPredicates(def, issues) {
1487
2142
  path: [ "predicates", name ],
1488
2143
  message: `predicate "${name}" shadows the built-in $${name} — pick another name`
1489
2144
  });
1490
- for (const [name, groq] of Object.entries(def.predicates ?? {})) checkPredicateBody({
2145
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkPredicateBody({
1491
2146
  name: name,
1492
- groq: groq,
2147
+ groq: groq2,
1493
2148
  names: names,
1494
2149
  issues: issues
1495
2150
  });
1496
2151
  }
1497
2152
 
1498
- function checkPredicateBody({name: name, groq: groq, names: names, issues: issues}) {
1499
- const reads = conditionParameterNames(groq);
2153
+ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issues}) {
2154
+ const reads = conditionParameterNames(groq2);
1500
2155
  for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
1501
2156
  path: [ "predicates", name ],
1502
2157
  message: `predicate "${name}" reads $${caller} — predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
@@ -1522,11 +2177,11 @@ function checkUnboundCallerVars(def, issues) {
1522
2177
  }
1523
2178
 
1524
2179
  function unboundCallerVars(policy) {
1525
- return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : [ "can" ];
2180
+ return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
1526
2181
  }
1527
2182
 
1528
2183
  function callerVarMessage(site, name) {
1529
- 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`;
2184
+ 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`;
1530
2185
  }
1531
2186
 
1532
2187
  function conditionSites(def) {
@@ -1559,23 +2214,11 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
1559
2214
  names: entryNames(activity.fields)
1560
2215
  })) ];
1561
2216
  for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
1562
- groq: t.filter,
1563
- path: [ "stages", i, "transitions", k, "filter" ],
1564
- label: `transition "${t.name}" filter`,
2217
+ groq: t.when,
2218
+ path: [ "stages", i, "transitions", k, "when" ],
2219
+ label: `transition "${t.name}" when`,
1565
2220
  policy: "cascade",
1566
2221
  fields: stageFields2
1567
- }), collectBindingSites({
1568
- effects: t.effects,
1569
- path: [ "stages", i, "transitions", k, "effects" ],
1570
- label: `transition "${t.name}"`,
1571
- fields: stageFields2,
1572
- sites: sites
1573
- }), collectOpWhereSites({
1574
- ops: t.ops,
1575
- path: [ "stages", i, "transitions", k, "ops" ],
1576
- label: `transition "${t.name}"`,
1577
- fields: stageFields2,
1578
- sites: sites
1579
2222
  });
1580
2223
  collectEditableSites({
1581
2224
  entries: stage.fields,
@@ -1609,11 +2252,14 @@ function collectEditableSites({entries: entries, path: path, labelPrefix: labelP
1609
2252
  });
1610
2253
  }
1611
2254
 
1612
- function collectOpWhereSites({ops: ops, path: path, label: label, fields: fields, sites: sites}) {
2255
+ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
1613
2256
  for (const [o, op] of (ops ?? []).entries()) op.where !== void 0 && sites.push({
1614
2257
  groq: op.where,
1615
2258
  path: [ ...path, o, "where" ],
1616
2259
  label: `${label} ${op.type} where`,
2260
+ ...policy !== void 0 ? {
2261
+ policy: policy
2262
+ } : {},
1617
2263
  fields: fields
1618
2264
  });
1619
2265
  }
@@ -1623,36 +2269,21 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
1623
2269
  scope: "activity",
1624
2270
  names: entryNames(activity.fields)
1625
2271
  }, ...stageFields2 ];
1626
- for (const gate of [ "filter", "completeWhen", "failWhen" ]) {
1627
- const groq = activity[gate];
1628
- groq !== void 0 && sites.push({
1629
- groq: groq,
1630
- path: [ ...path, gate ],
1631
- label: `activity "${activity.name}".${gate}`,
1632
- policy: "cascade",
1633
- fields: fields
1634
- });
1635
- }
1636
- for (const [name, groq] of Object.entries(activity.requirements ?? {})) sites.push({
1637
- groq: groq,
2272
+ activity.filter !== void 0 && sites.push({
2273
+ groq: activity.filter,
2274
+ path: [ ...path, "filter" ],
2275
+ label: `activity "${activity.name}".filter`,
2276
+ policy: "cascade",
2277
+ fields: fields
2278
+ });
2279
+ for (const [name, groq2] of Object.entries(activity.requirements ?? {})) sites.push({
2280
+ groq: groq2,
1638
2281
  path: [ ...path, "requirements", name ],
1639
2282
  label: `activity "${activity.name}" requirement "${name}"`,
1640
2283
  policy: "caller-bound",
1641
2284
  fields: fields
1642
2285
  });
1643
- collectBindingSites({
1644
- effects: activity.effects,
1645
- path: [ ...path, "effects" ],
1646
- label: `activity "${activity.name}"`,
1647
- fields: fields,
1648
- sites: sites
1649
- }), collectOpWhereSites({
1650
- ops: activity.ops,
1651
- path: [ ...path, "ops" ],
1652
- label: `activity "${activity.name}"`,
1653
- fields: fields,
1654
- sites: sites
1655
- }), collectEditableSites({
2286
+ collectEditableSites({
1656
2287
  entries: activity.fields,
1657
2288
  path: [ ...path, "fields" ],
1658
2289
  labelPrefix: `activity "${activity.name}"`,
@@ -1663,59 +2294,77 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
1663
2294
  path: path,
1664
2295
  fields: fields,
1665
2296
  sites: sites
1666
- }), collectSubworkflowSites({
1667
- activity: activity,
1668
- path: path,
1669
- fields: fields,
1670
- sites: sites
1671
2297
  });
1672
2298
  }
1673
2299
 
1674
2300
  function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
1675
- for (const [a, action] of (activity.actions ?? []).entries()) action.filter !== void 0 && sites.push({
1676
- groq: action.filter,
1677
- path: [ ...path, "actions", a, "filter" ],
1678
- label: `action "${action.name}" filter`,
1679
- policy: "caller-bound",
1680
- fields: fields
1681
- }), collectBindingSites({
1682
- effects: action.effects,
1683
- path: [ ...path, "actions", a, "effects" ],
1684
- label: `action "${action.name}"`,
1685
- fields: fields,
1686
- sites: sites
1687
- }), collectOpWhereSites({
1688
- ops: action.ops,
1689
- path: [ ...path, "actions", a, "ops" ],
1690
- label: `action "${action.name}"`,
1691
- fields: fields,
1692
- sites: sites
1693
- });
2301
+ for (const [a, action] of (activity.actions ?? []).entries()) {
2302
+ const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
2303
+ action.when !== void 0 && sites.push({
2304
+ groq: action.when,
2305
+ path: [ ...actionPath, "when" ],
2306
+ label: `action "${action.name}" when`,
2307
+ policy: "cascade",
2308
+ fields: fields
2309
+ }), action.filter !== void 0 && sites.push({
2310
+ groq: action.filter,
2311
+ path: [ ...actionPath, "filter" ],
2312
+ label: `action "${action.name}" filter`,
2313
+ policy: cascadeFired ? "cascade" : "caller-bound",
2314
+ fields: fields
2315
+ });
2316
+ const payloadPolicy = cascadeFired ? "triggered-payload" : void 0;
2317
+ collectBindingSites({
2318
+ effects: action.effects,
2319
+ path: [ ...actionPath, "effects" ],
2320
+ label: `action "${action.name}"`,
2321
+ policy: payloadPolicy,
2322
+ fields: fields,
2323
+ sites: sites
2324
+ }), collectOpWhereSites({
2325
+ ops: action.ops,
2326
+ path: [ ...actionPath, "ops" ],
2327
+ label: `action "${action.name}"`,
2328
+ policy: payloadPolicy,
2329
+ fields: fields,
2330
+ sites: sites
2331
+ }), collectSpawnSites({
2332
+ action: action,
2333
+ path: actionPath,
2334
+ fields: fields,
2335
+ sites: sites
2336
+ });
2337
+ }
1694
2338
  }
1695
2339
 
1696
- function collectSubworkflowSites({activity: activity, path: path, fields: fields, sites: sites}) {
1697
- const sub = activity.subworkflows;
1698
- if (sub === void 0) return;
1699
- const subPath = [ ...path, "subworkflows" ];
2340
+ function collectSpawnSites({action: action, path: path, fields: fields, sites: sites}) {
2341
+ const spawn = action.spawn;
2342
+ if (spawn === void 0) return;
2343
+ const spawnPath = [ ...path, "spawn" ];
1700
2344
  sites.push({
1701
- groq: sub.forEach,
1702
- path: [ ...subPath, "forEach" ],
1703
- label: `activity "${activity.name}".subworkflows.forEach`,
2345
+ groq: spawn.forEach,
2346
+ path: [ ...spawnPath, "forEach" ],
2347
+ label: `action "${action.name}".spawn.forEach`,
2348
+ policy: "cascade",
1704
2349
  fields: fields
1705
2350
  });
1706
- for (const [group, record] of [ [ "with", sub.with ], [ "context", sub.context ] ]) for (const [key, groq] of Object.entries(record ?? {})) sites.push({
1707
- groq: groq,
1708
- path: [ ...subPath, group, key ],
1709
- label: `activity "${activity.name}".subworkflows.${group} "${key}"`,
2351
+ for (const [group, record] of [ [ "with", spawn.with ], [ "context", spawn.context ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2352
+ groq: groq2,
2353
+ path: [ ...spawnPath, group, key ],
2354
+ label: `action "${action.name}".spawn.${group} "${key}"`,
2355
+ policy: "triggered-payload",
1710
2356
  fields: fields
1711
2357
  });
1712
2358
  }
1713
2359
 
1714
- function collectBindingSites({effects: effects, path: path, label: label, fields: fields, sites: sites}) {
1715
- for (const [e, effect] of (effects ?? []).entries()) for (const [key, groq] of Object.entries(effect.bindings ?? {})) sites.push({
1716
- groq: groq,
2360
+ function collectBindingSites({effects: effects, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
2361
+ for (const [e, effect] of (effects ?? []).entries()) for (const [key, groq2] of Object.entries(effect.bindings ?? {})) sites.push({
2362
+ groq: groq2,
1717
2363
  path: [ ...path, e, "bindings", key ],
1718
2364
  label: `${label} effect "${effect.name}" binding "${key}"`,
2365
+ ...policy !== void 0 ? {
2366
+ policy: policy
2367
+ } : {},
1719
2368
  fields: fields
1720
2369
  });
1721
2370
  }
@@ -1727,36 +2376,76 @@ function checkAssigneesEntries(def, issues) {
1727
2376
  });
1728
2377
  }
1729
2378
 
1730
- function activityShape(activity) {
1731
- return {
1732
- hasActions: (activity.actions ?? []).length > 0,
1733
- hasEffects: (activity.effects ?? []).length > 0,
1734
- hasCompleteWhen: activity.completeWhen !== void 0,
1735
- hasSubworkflows: activity.subworkflows !== void 0
1736
- };
2379
+ function checkActivityTerminalPaths(def, issues) {
2380
+ for (const [i, stage] of def.stages.entries()) {
2381
+ const resolvable = terminallyResolvableActivities(stage);
2382
+ for (const [j, activity] of (stage.activities ?? []).entries()) resolvable.has(activity.name) || issues.push({
2383
+ path: [ "stages", i, "activities", j ],
2384
+ 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`
2385
+ });
2386
+ }
1737
2387
  }
1738
2388
 
1739
- const KIND_SHAPE_RULES = {
1740
- user: s => s.hasActions ? [] : [ "needs at least one action for a person to fire" ],
1741
- service: s => s.hasEffects ? [] : [ "needs at least one effect — the automated work it performs" ],
1742
- 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),
1743
- 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),
1744
- 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)
1745
- };
2389
+ function terminallyResolvableActivities(stage) {
2390
+ 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));
2391
+ return new Set(terminalSets.map(op => op.activity));
2392
+ }
1746
2393
 
1747
- function checkActivityKinds(def, issues) {
1748
- for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) {
1749
- const path = [ "stages", i, "activities", j ];
1750
- if (activity.target !== void 0 && activity.kind !== "manual" && issues.push({
1751
- path: [ ...path, "target" ],
1752
- message: `activity "${activity.name}" has a \`target\` but is not \`kind: "manual"\` — a target is the deep-link for off-system manual work`
1753
- }), activity.kind !== void 0) for (const fault of KIND_SHAPE_RULES[activity.kind](activityShape(activity))) issues.push({
1754
- path: [ ...path, "kind" ],
1755
- message: `activity "${activity.name}" declares kind "${activity.kind}" but ${fault}`
2394
+ function checkTerminalStageActivities(def, issues) {
2395
+ for (const [i, stage] of def.stages.entries()) (stage.transitions ?? []).length > 0 || (stage.activities ?? []).length === 0 || issues.push({
2396
+ path: [ "stages", i, "activities" ],
2397
+ 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`
2398
+ });
2399
+ }
2400
+
2401
+ function storedRolesIssue(action) {
2402
+ if (action.roles !== void 0) {
2403
+ 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`;
2404
+ 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`;
2405
+ }
2406
+ }
2407
+
2408
+ function checkStoredRolesPlacement(def, issues) {
2409
+ for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) {
2410
+ const message = storedRolesIssue(action);
2411
+ message !== void 0 && issues.push({
2412
+ path: [ "stages", i, "activities", j, "actions", a, "roles" ],
2413
+ message: message
1756
2414
  });
1757
2415
  }
1758
2416
  }
1759
2417
 
2418
+ function checkTriggeredActionParams(def, issues) {
2419
+ 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({
2420
+ path: [ "stages", i, "activities", j, "actions", a, "params" ],
2421
+ 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`
2422
+ });
2423
+ }
2424
+
2425
+ function checkStart(def, issues) {
2426
+ if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2427
+ path: [ "start" ],
2428
+ 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'`"
2429
+ }), checkStartFilterReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
2430
+ path: [ "fields", n, "required" ],
2431
+ 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`
2432
+ });
2433
+ }
2434
+
2435
+ function checkStartFilterReads(def, issues) {
2436
+ const filter = def.start?.filter;
2437
+ if (filter === void 0) return;
2438
+ const bindable = new Set((def.fields ?? []).filter(isInputSourced).map(entry => entry.name)), declared = new Set((def.fields ?? []).map(entry => entry.name));
2439
+ for (const name of conditionFieldReadNames(filter)) bindable.has(name) || issues.push({
2440
+ path: [ "start", "filter" ],
2441
+ message: declared.has(name) ? `start.filter reads $fields.${name}, but "${name}" is not an \`input\`-sourced entry — $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after any read-side evaluation), so the read is GROQ null and the filter silently never passes. Bindable (input) fields: ${knownList(bindable)}` : `start.filter reads $fields.${name}, but no workflow-scope field entry named "${name}" is declared — $fields binds only the caller's input entries, so the read is GROQ null and the filter silently never passes. Bindable (input) fields: ${knownList(bindable)}`
2442
+ });
2443
+ readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2444
+ path: [ "start", "filter" ],
2445
+ 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 the filter could never pass. Declare a required subject entry, or gate on $fields / the dataset instead"
2446
+ });
2447
+ }
2448
+
1760
2449
  function checkGroups(def, issues) {
1761
2450
  const workflowGroups = checkGroupDeclarations({
1762
2451
  groups: def.groups,
@@ -1873,7 +2562,7 @@ function checkConditionFieldReads(def, issues) {
1873
2562
  path: site.path,
1874
2563
  message: undeclaredFieldReadMessage(site, name)
1875
2564
  });
1876
- for (const [name, groq] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq)) everywhere.has(read) || issues.push({
2565
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq2)) everywhere.has(read) || issues.push({
1877
2566
  path: [ "predicates", name ],
1878
2567
  message: noFieldAnywhereMessage(`predicate "${name}"`, read)
1879
2568
  });
@@ -1977,38 +2666,29 @@ function seedEarlierSiblingTarget(args) {
1977
2666
  });
1978
2667
  }
1979
2668
 
2669
+ function opSites(def) {
2670
+ const sites = [];
2671
+ 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({
2672
+ ops: action.ops,
2673
+ path: [ "stages", i, "activities", j, "actions", a, "ops" ],
2674
+ label: `action "${action.name}"`,
2675
+ stage: stage,
2676
+ activity: activity
2677
+ });
2678
+ return sites;
2679
+ }
2680
+
1980
2681
  function checkFieldReadOpValues(def, issues) {
1981
2682
  const workflowEntries = def.fields ?? [];
1982
- for (const [i, stage] of def.stages.entries()) {
1983
- const shared = {
1984
- workflowEntries: workflowEntries,
1985
- stageEntries: stage.fields ?? [],
1986
- issues: issues
1987
- };
1988
- for (const [k, t] of (stage.transitions ?? []).entries()) checkOpsFieldReads({
1989
- ...shared,
1990
- ops: t.ops,
1991
- path: [ "stages", i, "transitions", k, "ops" ],
1992
- label: `transition "${t.name}"`
1993
- });
1994
- for (const [j, activity] of (stage.activities ?? []).entries()) {
1995
- const activityNames = entryNames(activity.fields), activityPath = [ "stages", i, "activities", j ];
1996
- checkOpsFieldReads({
1997
- ...shared,
1998
- ops: activity.ops,
1999
- path: [ ...activityPath, "ops" ],
2000
- label: `activity "${activity.name}"`,
2001
- activityNames: activityNames
2002
- });
2003
- for (const [a, action] of (activity.actions ?? []).entries()) checkOpsFieldReads({
2004
- ...shared,
2005
- ops: action.ops,
2006
- path: [ ...activityPath, "actions", a, "ops" ],
2007
- label: `action "${action.name}"`,
2008
- activityNames: activityNames
2009
- });
2010
- }
2011
- }
2683
+ for (const site of opSites(def)) checkOpsFieldReads({
2684
+ workflowEntries: workflowEntries,
2685
+ stageEntries: site.stage.fields ?? [],
2686
+ issues: issues,
2687
+ ops: site.ops,
2688
+ path: site.path,
2689
+ label: site.label,
2690
+ activityNames: entryNames(site.activity.fields)
2691
+ });
2012
2692
  }
2013
2693
 
2014
2694
  function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
@@ -2061,6 +2741,49 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activit
2061
2741
  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)"}`;
2062
2742
  }
2063
2743
 
2744
+ function checkUpdateWhereOps(def, issues) {
2745
+ const workflow = def.fields ?? [];
2746
+ for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
2747
+ if (op.type !== "field.updateWhere") continue;
2748
+ const scopes = {
2749
+ workflow: workflow,
2750
+ stage: site.stage.fields ?? [],
2751
+ activity: site.activity.fields ?? []
2752
+ };
2753
+ checkUpdateWhereTargetKind({
2754
+ op: op,
2755
+ path: [ ...site.path, o ],
2756
+ label: site.label,
2757
+ scopes: scopes,
2758
+ issues: issues
2759
+ }), checkUpdateWhereMergeKeys({
2760
+ op: op,
2761
+ path: [ ...site.path, o ],
2762
+ label: site.label,
2763
+ issues: issues
2764
+ });
2765
+ }
2766
+ }
2767
+
2768
+ function checkUpdateWhereTargetKind({op: op, path: path, label: label, scopes: scopes, issues: issues}) {
2769
+ const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
2770
+ target === void 0 || target.type === "array" || issues.push({
2771
+ path: [ ...path, "target" ],
2772
+ 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)}`
2773
+ });
2774
+ }
2775
+
2776
+ function rowOpsHint(type) {
2777
+ return type === "doc.refs" || type === "assignees" ? `; to add or drop ${type} rows use field.append / field.removeWhere` : "";
2778
+ }
2779
+
2780
+ function checkUpdateWhereMergeKeys({op: op, path: path, label: label, issues: issues}) {
2781
+ if (op.value.type === "object") for (const key of Object.keys(op.value.fields)) key !== "_key" && key !== "_type" || issues.push({
2782
+ path: [ ...path, "value", "fields", key ],
2783
+ 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`
2784
+ });
2785
+ }
2786
+
2064
2787
  function checkGuardFieldReads(def, issues) {
2065
2788
  const reads = {
2066
2789
  workflowEntries: def.fields ?? [],
@@ -2258,9 +2981,11 @@ function checkWorkflowInvariants(def) {
2258
2981
  stageNames: stageNames,
2259
2982
  issues: issues
2260
2983
  }), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
2261
- checkRequiredField(def, issues), checkPredicates(def, issues), checkUnboundCallerVars(def, issues),
2262
- checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues), checkFieldReadOpValues(def, issues),
2263
- checkGuardFieldReads(def, issues), checkAssigneesEntries(def, issues), checkActivityKinds(def, issues),
2984
+ checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
2985
+ checkUnboundCallerVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
2986
+ checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
2987
+ checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
2988
+ checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
2264
2989
  checkGroups(def, issues), issues;
2265
2990
  }
2266
2991
 
@@ -2290,6 +3015,8 @@ exports.CONDITION_VARS = CONDITION_VARS;
2290
3015
 
2291
3016
  exports.ContractViolationError = ContractViolationError;
2292
3017
 
3018
+ exports.DEFAULT_TRANSITION_WHEN = DEFAULT_TRANSITION_WHEN;
3019
+
2293
3020
  exports.DOCUMENT_VALUE_PERMISSIONS = DOCUMENT_VALUE_PERMISSIONS;
2294
3021
 
2295
3022
  exports.DRIVER_KINDS = DRIVER_KINDS;
@@ -2300,6 +3027,8 @@ exports.DefinitionNotFoundError = DefinitionNotFoundError;
2300
3027
 
2301
3028
  exports.EFFECTS_READ = EFFECTS_READ;
2302
3029
 
3030
+ exports.EXECUTOR_CLASSIFICATIONS = EXECUTOR_CLASSIFICATIONS;
3031
+
2303
3032
  exports.EffectNotFoundError = EffectNotFoundError;
2304
3033
 
2305
3034
  exports.EffectSchema = EffectSchema;
@@ -2322,6 +3051,8 @@ exports.RESERVED_CONDITION_VARS = RESERVED_CONDITION_VARS;
2322
3051
 
2323
3052
  exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
2324
3053
 
3054
+ exports.START_FILTER_VARS = START_FILTER_VARS;
3055
+
2325
3056
  exports.StoredFieldOpSchema = StoredFieldOpSchema;
2326
3057
 
2327
3058
  exports.WORKFLOW_DEFINITION_TYPE = WORKFLOW_DEFINITION_TYPE;
@@ -2342,6 +3073,8 @@ exports.clientConfigFromResource = clientConfigFromResource;
2342
3073
 
2343
3074
  exports.conditionEffectReads = conditionEffectReads;
2344
3075
 
3076
+ exports.conditionFieldReadNames = conditionFieldReadNames;
3077
+
2345
3078
  exports.conditionParameterNames = conditionParameterNames;
2346
3079
 
2347
3080
  exports.conditionSyntaxIssues = conditionSyntaxIssues;
@@ -2350,6 +3083,14 @@ exports.datasetResourceParts = datasetResourceParts;
2350
3083
 
2351
3084
  exports.definitionDocId = definitionDocId;
2352
3085
 
3086
+ exports.deriveActivityKind = deriveActivityKind;
3087
+
3088
+ exports.deriveExecutorClassification = deriveExecutorClassification;
3089
+
3090
+ exports.desugarWorkflow = desugarWorkflow;
3091
+
3092
+ exports.driverKind = driverKind;
3093
+
2353
3094
  exports.errorMessage = errorMessage;
2354
3095
 
2355
3096
  exports.evaluateCondition = evaluateCondition;
@@ -2358,8 +3099,6 @@ exports.evaluateConditionOutcome = evaluateConditionOutcome;
2358
3099
 
2359
3100
  exports.evaluatePredicates = evaluatePredicates;
2360
3101
 
2361
- exports.expandRequiredRoles = expandRequiredRoles;
2362
-
2363
3102
  exports.extractDocumentId = extractDocumentId;
2364
3103
 
2365
3104
  exports.formatIssuePath = formatIssuePath;
@@ -2376,20 +3115,28 @@ exports.gdrResourcePrefix = gdrResourcePrefix;
2376
3115
 
2377
3116
  exports.gdrUri = gdrUri;
2378
3117
 
3118
+ exports.groq = groq;
3119
+
2379
3120
  exports.groupMembershipNames = groupMembershipNames;
2380
3121
 
2381
3122
  exports.isBareSeedId = isBareSeedId;
2382
3123
 
3124
+ exports.isCascadeFired = isCascadeFired;
3125
+
2383
3126
  exports.isGdr = isGdr;
2384
3127
 
2385
3128
  exports.isGdrUri = isGdrUri;
2386
3129
 
2387
3130
  exports.isGuardReadExpr = isGuardReadExpr;
2388
3131
 
3132
+ exports.isInputSourced = isInputSourced;
3133
+
2389
3134
  exports.isNotesEntry = isNotesEntry;
2390
3135
 
2391
3136
  exports.isStartableDefinition = isStartableDefinition;
2392
3137
 
3138
+ exports.isSubjectEntry = isSubjectEntry;
3139
+
2393
3140
  exports.isTerminalActivityStatus = isTerminalActivityStatus;
2394
3141
 
2395
3142
  exports.isTodoListEntry = isTodoListEntry;
@@ -2400,8 +3147,6 @@ exports.isUnevaluable = isUnevaluable;
2400
3147
 
2401
3148
  exports.labelFor = labelFor;
2402
3149
 
2403
- exports.normalizeRoleAliases = normalizeRoleAliases;
2404
-
2405
3150
  exports.parseGdr = parseGdr;
2406
3151
 
2407
3152
  exports.parseOrThrow = parseOrThrow;
@@ -2410,7 +3155,7 @@ exports.parseResourceGdr = parseResourceGdr;
2410
3155
 
2411
3156
  exports.parseStoredDefinition = parseStoredDefinition;
2412
3157
 
2413
- exports.printGuardRead = printGuardRead;
3158
+ exports.readsRootDocument = readsRootDocument;
2414
3159
 
2415
3160
  exports.refCanvas = refCanvas;
2416
3161
 
@@ -2444,6 +3189,8 @@ exports.sameResource = sameResource;
2444
3189
 
2445
3190
  exports.selfGdr = selfGdr;
2446
3191
 
3192
+ exports.startKindOf = startKindOf;
3193
+
2447
3194
  exports.tagScopeFilter = tagScopeFilter;
2448
3195
 
2449
3196
  exports.toBareId = toBareId;