@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.
@@ -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
  }
@@ -381,6 +401,596 @@ 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
+ };
507
+ }
508
+
509
+ function checkReservedRoleAliasKeys(aliases, issues) {
510
+ for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
511
+ path: [ "roleAliases", key ],
512
+ 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.`
513
+ });
514
+ }
515
+
516
+ const TODOLIST_OF = [ {
517
+ type: "string",
518
+ name: "label",
519
+ title: "Label"
520
+ }, {
521
+ type: "string",
522
+ name: "status",
523
+ title: "Status"
524
+ }, {
525
+ type: "assignee",
526
+ name: "assignee",
527
+ title: "Assignee"
528
+ }, {
529
+ type: "date",
530
+ name: "dueDate",
531
+ title: "Due date"
532
+ } ], NOTES_OF = [ {
533
+ type: "text",
534
+ name: "body",
535
+ title: "Body"
536
+ }, {
537
+ type: "actor",
538
+ name: "actor",
539
+ title: "Actor"
540
+ }, {
541
+ type: "datetime",
542
+ name: "at",
543
+ title: "At"
544
+ } ];
545
+
546
+ function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
547
+ return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
548
+ entry: entry,
549
+ path: [ ...path, i ],
550
+ ctx: ctx
551
+ }));
552
+ }
553
+
554
+ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
555
+ if (entry.type === "claim") return desugarClaimField({
556
+ entry: entry,
557
+ path: path,
558
+ ctx: ctx
559
+ });
560
+ if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
561
+ entry: entry,
562
+ path: path,
563
+ ctx: ctx
564
+ });
565
+ const editable = normalizeEditable({
566
+ editable: entry.editable,
567
+ path: [ ...path, "editable" ],
568
+ ctx: ctx
569
+ });
570
+ return {
571
+ ...stripUndefined({
572
+ type: entry.type,
573
+ name: entry.name,
574
+ title: entry.title,
575
+ description: entry.description,
576
+ group: normalizeGroup(entry.group),
577
+ required: entry.required,
578
+ initialValue: entry.initialValue,
579
+ editable: editable,
580
+ types: entry.types,
581
+ fields: entry.fields,
582
+ of: entry.of
583
+ })
584
+ };
585
+ }
586
+
587
+ function desugarClaimField({entry: entry, path: path, ctx: ctx}) {
588
+ const desugared = {
589
+ ...stripUndefined({
590
+ name: entry.name,
591
+ title: entry.title,
592
+ description: entry.description,
593
+ group: normalizeGroup(entry.group)
594
+ }),
595
+ type: "actor"
596
+ };
597
+ return ctx.claimFields.set(desugared, {
598
+ name: entry.name,
599
+ path: path
600
+ }), desugared;
601
+ }
602
+
603
+ function desugarListField({entry: entry, path: path, ctx: ctx}) {
604
+ const editable = normalizeEditable({
605
+ editable: entry.editable,
606
+ path: [ ...path, "editable" ],
607
+ ctx: ctx
608
+ });
609
+ return {
610
+ ...stripUndefined({
611
+ name: entry.name,
612
+ title: entry.title,
613
+ description: entry.description,
614
+ group: normalizeGroup(entry.group),
615
+ required: entry.required,
616
+ initialValue: entry.initialValue,
617
+ editable: editable
618
+ }),
619
+ type: "array",
620
+ of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
621
+ };
622
+ }
623
+
624
+ function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
625
+ if (editable === void 0 || editable === !0) return editable;
626
+ if (Array.isArray(editable)) {
627
+ const condition = rolesCondition(editable, ctx.roleAliases);
628
+ if (condition === void 0) {
629
+ ctx.issues.push({
630
+ path: path,
631
+ message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
632
+ });
633
+ return;
634
+ }
635
+ return condition;
636
+ }
637
+ return editable;
638
+ }
639
+
640
+ function editableFieldNames({workflowFields: workflowFields2, stageFields: stageFields2, activities: activities}) {
641
+ const names = /* @__PURE__ */ new Set, collect = entries => {
642
+ for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
643
+ };
644
+ collect(workflowFields2), collect(stageFields2);
645
+ for (const activity of activities) collect(activity.fields);
646
+ return names;
647
+ }
648
+
649
+ function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
650
+ if (overrides === void 0) return;
651
+ const out = {};
652
+ for (const [name, value] of Object.entries(overrides)) {
653
+ if (!inScope.has(name)) {
654
+ ctx.issues.push({
655
+ path: [ ...path, name ],
656
+ 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\``
657
+ });
658
+ continue;
659
+ }
660
+ const normalized = normalizeEditable({
661
+ editable: value,
662
+ path: [ ...path, name ],
663
+ ctx: ctx
664
+ });
665
+ normalized !== void 0 && (out[name] = normalized);
666
+ }
667
+ return Object.keys(out).length > 0 ? out : void 0;
668
+ }
669
+
670
+ function layerOf(entries) {
671
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
672
+ }
673
+
674
+ function normalizeGroup(group) {
675
+ return group === void 0 || Array.isArray(group) ? group : [ group ];
676
+ }
677
+
678
+ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
679
+ const activityFields2 = desugarFieldEntries({
680
+ entries: activity.fields,
681
+ path: [ ...path, "fields" ],
682
+ ctx: ctx
683
+ }), env = {
684
+ layers: [ {
685
+ scope: "activity",
686
+ entries: layerOf(activityFields2)
687
+ }, ...stageEnv.layers ]
688
+ }, actions = (activity.actions ?? []).map((action, a) => desugarAction({
689
+ action: action,
690
+ path: [ ...path, "actions", a ],
691
+ env: env,
692
+ activityName: activity.name,
693
+ ctx: ctx
694
+ })), target = desugarTarget({
695
+ target: activity.target,
696
+ env: env,
697
+ path: [ ...path, "target" ],
698
+ ctx: ctx
699
+ });
700
+ return {
701
+ ...stripUndefined({
702
+ name: activity.name,
703
+ title: activity.title,
704
+ description: activity.description,
705
+ groups: activity.groups,
706
+ group: normalizeGroup(activity.group),
707
+ filter: activity.filter,
708
+ requirements: activity.requirements
709
+ }),
710
+ ...target ? {
711
+ target: target
712
+ } : {},
713
+ ...activityFields2 ? {
714
+ fields: activityFields2
715
+ } : {},
716
+ ...actions.length > 0 ? {
717
+ actions: actions
718
+ } : {}
719
+ };
720
+ }
721
+
722
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
723
+
724
+ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
725
+ if (target === void 0 || target.type === "url") return target;
726
+ const ref = typeof target.field == "string" ? {
727
+ field: target.field
728
+ } : target.field, field = resolveRef({
729
+ ref: ref,
730
+ env: env,
731
+ path: [ ...path, "field" ],
732
+ ctx: ctx
733
+ }) ?? fallbackRef(ref), entry = entryAt(env, field);
734
+ return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
735
+ path: [ ...path, "field" ],
736
+ message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
737
+ }), {
738
+ type: "field",
739
+ field: field
740
+ };
741
+ }
742
+
743
+ function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
744
+ if ("type" in action) return desugarClaimAction({
745
+ action: action,
746
+ path: path,
747
+ env: env,
748
+ ctx: ctx
749
+ });
750
+ action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
751
+ path: [ ...path, "roles" ],
752
+ message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
753
+ });
754
+ const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
755
+ ops: action.ops,
756
+ path: [ ...path, "ops" ],
757
+ env: env,
758
+ firingActivity: activityName,
759
+ ctx: ctx
760
+ }) ?? [];
761
+ return action.status !== void 0 && ops.push({
762
+ type: "status.set",
763
+ activity: activityName,
764
+ status: action.status
765
+ }), {
766
+ ...stripUndefined({
767
+ name: action.name,
768
+ title: action.title,
769
+ description: action.description,
770
+ group: normalizeGroup(action.group),
771
+ when: action.when,
772
+ params: action.params,
773
+ effects: action.effects,
774
+ spawn: action.spawn
775
+ }),
776
+ ...cascadeFired && action.roles !== void 0 && action.roles.length > 0 ? {
777
+ roles: action.roles
778
+ } : {},
779
+ ...filter ? {
780
+ filter: filter
781
+ } : {},
782
+ ...ops.length > 0 ? {
783
+ ops: ops
784
+ } : {}
785
+ };
786
+ }
787
+
788
+ function desugarClaimAction({action: action, path: path, env: env, ctx: ctx}) {
789
+ const ref = typeof action.field == "string" ? {
790
+ field: action.field
791
+ } : action.field, resolved = resolveRef({
792
+ ref: ref,
793
+ env: env,
794
+ path: [ ...path, "field" ],
795
+ ctx: ctx
796
+ });
797
+ if (resolved) {
798
+ const entry = entryAt(env, resolved);
799
+ entry && entry.type !== "actor" && ctx.issues.push({
800
+ path: [ ...path, "field" ],
801
+ 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)`
802
+ }), checkShadowedClaimTarget({
803
+ actionName: action.name,
804
+ field: ref.field,
805
+ resolved: resolved,
806
+ env: env,
807
+ path: [ ...path, "field" ],
808
+ ctx: ctx
809
+ }), entry && ctx.claimedFields.add(entry);
810
+ }
811
+ const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([ noSteal, rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = resolved ? [ {
812
+ type: "field.set",
813
+ target: resolved,
814
+ value: {
815
+ type: "actor"
816
+ }
817
+ } ] : [];
818
+ return {
819
+ ...stripUndefined({
820
+ name: action.name,
821
+ title: action.title,
822
+ description: action.description,
823
+ group: normalizeGroup(action.group),
824
+ params: action.params,
825
+ effects: action.effects
826
+ }),
827
+ filter: filter,
828
+ ...ops.length > 0 ? {
829
+ ops: ops
830
+ } : {}
831
+ };
832
+ }
833
+
834
+ function checkShadowedClaimTarget({actionName: actionName, field: field, resolved: resolved, env: env, path: path, ctx: ctx}) {
835
+ const nearest = env.layers.find(l => l.entries.has(field));
836
+ nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
837
+ path: path,
838
+ 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`
839
+ });
840
+ }
841
+
842
+ function checkUnclaimedClaimFields(ctx) {
843
+ for (const [entry, {name: name, path: path}] of ctx.claimFields) ctx.claimedFields.has(entry) || ctx.issues.push({
844
+ path: path,
845
+ 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`
846
+ });
847
+ }
848
+
849
+ const DEFAULT_TRANSITION_WHEN = "$allActivitiesDone";
850
+
851
+ function desugarTransition({transition: transition}) {
852
+ return {
853
+ ...stripUndefined({
854
+ name: transition.name,
855
+ title: transition.title,
856
+ description: transition.description,
857
+ to: transition.to
858
+ }),
859
+ when: transition.when ?? DEFAULT_TRANSITION_WHEN
860
+ };
861
+ }
862
+
863
+ function desugarGuard(guard) {
864
+ const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
865
+ return {
866
+ ...rest,
867
+ match: {
868
+ ...matchRest,
869
+ ...idRefs !== void 0 ? {
870
+ idRefs: idRefs.map(printGuardRead)
871
+ } : {}
872
+ },
873
+ ...metadata !== void 0 ? {
874
+ metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
875
+ } : {}
876
+ };
877
+ }
878
+
879
+ function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
880
+ if (ops) return ops.map((op, i) => desugarOp({
881
+ op: op,
882
+ path: [ ...path, i ],
883
+ env: env,
884
+ firingActivity: firingActivity,
885
+ ctx: ctx
886
+ }));
887
+ }
888
+
889
+ function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
890
+ if (op.type === "status.set") return {
891
+ type: "status.set",
892
+ activity: op.activity ?? firingActivity,
893
+ status: op.status
894
+ };
895
+ if (op.type === "audit") return desugarAuditOp({
896
+ op: op,
897
+ path: path,
898
+ env: env,
899
+ ctx: ctx
900
+ });
901
+ const target = resolveRef({
902
+ ref: op.target,
903
+ env: env,
904
+ path: [ ...path, "target" ],
905
+ ctx: ctx
906
+ }) ?? fallbackRef(op.target);
907
+ return {
908
+ ...op,
909
+ target: target
910
+ };
911
+ }
912
+
913
+ const AUDIT_STAMPS = {
914
+ actor: {
915
+ type: "actor"
916
+ },
917
+ at: {
918
+ type: "now"
919
+ }
920
+ };
921
+
922
+ function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
923
+ const target = resolveRef({
924
+ ref: op.target,
925
+ env: env,
926
+ path: [ ...path, "target" ],
927
+ ctx: ctx
928
+ }) ?? fallbackRef(op.target);
929
+ if (op.value.type !== "object") return ctx.issues.push({
930
+ path: [ ...path, "value" ],
931
+ message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
932
+ }), {
933
+ type: "field.append",
934
+ target: target,
935
+ value: op.value
936
+ };
937
+ const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
938
+ ...op.value.fields
939
+ };
940
+ for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
941
+ if (stamp in fields) {
942
+ ctx.issues.push({
943
+ path: [ ...path, "value", "fields", stamp ],
944
+ message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
945
+ });
946
+ continue;
947
+ }
948
+ fields[stamp] = source;
949
+ }
950
+ return {
951
+ type: "field.append",
952
+ target: target,
953
+ value: {
954
+ type: "object",
955
+ fields: fields
956
+ }
957
+ };
958
+ }
959
+
960
+ function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
961
+ const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
962
+ for (const layer of layers) if (layer.entries.has(ref.field)) return {
963
+ scope: layer.scope,
964
+ field: ref.field
965
+ };
966
+ const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
967
+ ctx.issues.push({
968
+ path: path,
969
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
970
+ });
971
+ }
972
+
973
+ function entryAt(env, ref) {
974
+ return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
975
+ }
976
+
977
+ function fallbackRef(ref) {
978
+ return {
979
+ scope: ref.scope ?? "workflow",
980
+ field: ref.field
981
+ };
982
+ }
983
+
984
+ function rolesCondition(roles, aliases) {
985
+ if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
986
+ }
987
+
988
+ function stripUndefined(obj) {
989
+ const out = {};
990
+ for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
991
+ return out;
992
+ }
993
+
384
994
  function isUnevaluable(result) {
385
995
  return result == null;
386
996
  }
@@ -400,9 +1010,9 @@ async function evaluateCondition(args) {
400
1010
 
401
1011
  async function evaluatePredicates(args) {
402
1012
  const out = {};
403
- for (const [name, groq] of Object.entries(args.predicates ?? {})) {
1013
+ for (const [name, groq2] of Object.entries(args.predicates ?? {})) {
404
1014
  const result = await runGroq({
405
- groq: groq,
1015
+ groq: groq2,
406
1016
  params: args.params,
407
1017
  snapshot: args.snapshot
408
1018
  }), outcome = conditionOutcome(result);
@@ -411,46 +1021,46 @@ async function evaluatePredicates(args) {
411
1021
  return out;
412
1022
  }
413
1023
 
414
- async function runGroq({groq: groq, params: params, snapshot: snapshot}) {
1024
+ async function runGroq({groq: groq2, params: params, snapshot: snapshot}) {
415
1025
  return runGroq$1({
416
- groq: groq,
1026
+ groq: groq2,
417
1027
  params: params,
418
1028
  dataset: snapshot.docs
419
1029
  });
420
1030
  }
421
1031
 
422
- function conditionSyntaxIssues(groq, boundVars) {
1032
+ function conditionSyntaxIssues(groq2, boundVars) {
423
1033
  const issues = [];
424
1034
  let tree;
425
1035
  try {
426
- tree = parse(groq);
1036
+ tree = parse(groq2);
427
1037
  } catch (err) {
428
1038
  issues.push(errorMessage(err));
429
1039
  }
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(", "));
1040
+ 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."),
1041
+ 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
1042
  return issues;
433
1043
  }
434
1044
 
435
- function conditionParameterNames(groq) {
1045
+ function conditionParameterNames(groq2) {
436
1046
  const read = /* @__PURE__ */ new Set;
437
- return walkAstNodes(tryParseGroq(groq), node => {
1047
+ return walkAstNodes(tryParseGroq(groq2), node => {
438
1048
  node.type === "Parameter" && typeof node.name == "string" && read.add(node.name);
439
1049
  }), read;
440
1050
  }
441
1051
 
442
- function conditionFieldReadNames(groq) {
1052
+ function conditionFieldReadNames(groq2) {
443
1053
  const read = /* @__PURE__ */ new Set;
444
- return walkAstNodes(tryParseGroq(groq), node => {
1054
+ return walkAstNodes(tryParseGroq(groq2), node => {
445
1055
  if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
446
1056
  const base = node.base;
447
1057
  base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
448
1058
  }), read;
449
1059
  }
450
1060
 
451
- function conditionEffectReads(groq) {
1061
+ function conditionEffectReads(groq2) {
452
1062
  const reads = [];
453
- return walkAstNodes(tryParseGroq(groq), node => {
1063
+ return walkAstNodes(tryParseGroq(groq2), node => {
454
1064
  if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
455
1065
  const base = node.base;
456
1066
  base?.type !== "AccessAttribute" || typeof base.name != "string" || base.base?.type === "Parameter" && base.base.name === "effects" && reads.push({
@@ -460,9 +1070,34 @@ function conditionEffectReads(groq) {
460
1070
  }), reads;
461
1071
  }
462
1072
 
463
- function tryParseGroq(groq) {
1073
+ function readsRootDocument(groq2) {
1074
+ return nodeReadsRoot(tryParseGroq(groq2), 0);
1075
+ }
1076
+
1077
+ const SCOPED_CHILD = {
1078
+ Filter: "expr",
1079
+ Projection: "expr",
1080
+ Map: "expr",
1081
+ FlatMap: "expr",
1082
+ PipeFuncCall: "args"
1083
+ };
1084
+
1085
+ function nodeReadsRoot(node, depth) {
1086
+ if (Array.isArray(node)) return node.some(item => nodeReadsRoot(item, depth));
1087
+ if (typeof node != "object" || node === null) return !1;
1088
+ const typed = node;
1089
+ if (depth === 0 && typed.type === "This" || depth === 0 && typed.type === "AccessAttribute" && typed.base === void 0) return !0;
1090
+ if (typed.type === "Parent") {
1091
+ const climb = typeof typed.n == "number" ? typed.n : 1;
1092
+ return depth - climb <= 0;
1093
+ }
1094
+ const scopedChild = typeof typed.type == "string" ? SCOPED_CHILD[typed.type] : void 0;
1095
+ 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));
1096
+ }
1097
+
1098
+ function tryParseGroq(groq2) {
464
1099
  try {
465
- return parse(groq);
1100
+ return parse(groq2);
466
1101
  } catch {
467
1102
  return;
468
1103
  }
@@ -479,7 +1114,13 @@ function walkAstNodes(node, visit) {
479
1114
  }
480
1115
  }
481
1116
 
482
- const CONDITION_VARS = [ {
1117
+ const ACTIVITY_STATUSES = [ "active", "done", "skipped", "failed" ], TERMINAL_ACTIVITY_STATUSES = [ "done", "skipped", "failed" ];
1118
+
1119
+ function isTerminalActivityStatus(status) {
1120
+ return TERMINAL_ACTIVITY_STATUSES.includes(status);
1121
+ }
1122
+
1123
+ 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
1124
  name: "self",
484
1125
  binding: "always",
485
1126
  label: "this workflow instance",
@@ -509,11 +1150,16 @@ const CONDITION_VARS = [ {
509
1150
  binding: "always",
510
1151
  label: "the current time",
511
1152
  description: "The ISO clock reading shared by every condition in one evaluation pass, so they agree on the time."
1153
+ }, {
1154
+ name: "context",
1155
+ binding: "always",
1156
+ label: "start-time context",
1157
+ 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
1158
  }, {
513
1159
  name: "effects",
514
1160
  binding: "always",
515
1161
  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."
1162
+ 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
1163
  }, {
518
1164
  name: "effectStatus",
519
1165
  binding: "always",
@@ -548,23 +1194,35 @@ const CONDITION_VARS = [ {
548
1194
  name: "can",
549
1195
  binding: "caller",
550
1196
  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."
1197
+ 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
1198
  }, {
553
1199
  name: "row",
554
1200
  binding: "spawn",
555
1201
  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)."
1202
+ 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)."
557
1203
  }, {
558
1204
  name: "params",
559
1205
  binding: "caller",
560
1206
  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."
1207
+ 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
1208
  }, {
563
1209
  name: "subworkflows",
564
1210
  binding: "always",
565
1211
  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 = [ {
1212
+ 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`."
1213
+ } ], 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 = [ {
1214
+ name: "tag",
1215
+ description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1216
+ }, {
1217
+ name: "definition",
1218
+ description: "The `name` of the definition under evaluation (its own start.filter binds it)."
1219
+ }, {
1220
+ name: "now",
1221
+ description: "The ISO clock reading of the evaluating engine."
1222
+ }, {
1223
+ name: "fields",
1224
+ 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."
1225
+ } ], GUARD_PREDICATE_VARS = [ {
568
1226
  name: "guard",
569
1227
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
570
1228
  }, {
@@ -787,13 +1445,7 @@ function formatIssues(issues) {
787
1445
  });
788
1446
  }
789
1447
 
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_]*$/;
1448
+ const 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_]*$/;
797
1449
 
798
1450
  function groqIdentifier(referencedAs) {
799
1451
  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 +1544,7 @@ const StoredFieldOpSchema = v.variant("type", [ ...opSchemas(StoredFieldRefSchem
892
1544
  type: v.literal("status.set"),
893
1545
  activity: NonEmpty,
894
1546
  status: picklist(ACTIVITY_STATUSES)
895
- }) ]), StoredTransitionOpSchema = StoredFieldOpSchema, AuditOpSchema = v.strictObject({
1547
+ }) ]), AuditOpSchema = v.strictObject({
896
1548
  type: v.literal("audit"),
897
1549
  target: AuthoringFieldRefSchema,
898
1550
  value: ValueExprSchema,
@@ -1020,6 +1672,15 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
1020
1672
  bindings: v.optional(v.record(v.string(), ConditionSchema)),
1021
1673
  input: v.optional(v.record(v.string(), v.unknown())),
1022
1674
  outputs: v.optional(v.array(FieldShapeSchema))
1675
+ }), DefinitionRefSchema = v.strictObject({
1676
+ name: NonEmpty,
1677
+ version: v.optional(v.union([ PositiveInt, v.literal("latest") ]))
1678
+ }), SubworkflowsSchema = v.strictObject({
1679
+ forEach: NonEmpty,
1680
+ definition: DefinitionRefSchema,
1681
+ with: v.optional(v.record(NonEmpty, ConditionSchema)),
1682
+ context: v.optional(v.record(NonEmpty, ConditionSchema)),
1683
+ onExit: v.optional(picklist([ "detach", "abort" ]))
1023
1684
  }), ActionParamSchema = v.strictObject({
1024
1685
  type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
1025
1686
  name: NonEmpty,
@@ -1034,14 +1695,19 @@ function actionFields(op, group) {
1034
1695
  title: v.optional(v.string()),
1035
1696
  description: v.optional(v.string()),
1036
1697
  group: v.optional(group),
1698
+ when: v.optional(ConditionSchema),
1037
1699
  filter: v.optional(ConditionSchema),
1038
1700
  params: v.optional(v.array(ActionParamSchema)),
1039
1701
  ops: v.optional(v.array(op)),
1040
- effects: v.optional(v.array(EffectSchema))
1702
+ effects: v.optional(v.array(EffectSchema)),
1703
+ spawn: v.optional(SubworkflowsSchema)
1041
1704
  };
1042
1705
  }
1043
1706
 
1044
- const StoredActionSchema = pinned()(v.strictObject(actionFields(StoredOpSchema, StoredGroupMembershipSchema))), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v.strictObject({
1707
+ const StoredActionSchema = pinned()(v.strictObject({
1708
+ ...actionFields(StoredOpSchema, StoredGroupMembershipSchema),
1709
+ roles: v.optional(v.array(NonEmpty))
1710
+ })), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v.strictObject({
1045
1711
  ...actionFields(AuthoringOpSchema, AuthoringGroupMembershipSchema),
1046
1712
  roles: v.optional(v.array(NonEmpty)),
1047
1713
  status: v.optional(TerminalActivityStatus)
@@ -1056,35 +1722,19 @@ const StoredActionSchema = pinned()(v.strictObject(actionFields(StoredOpSchema,
1056
1722
  filter: v.optional(ConditionSchema),
1057
1723
  params: v.optional(v.array(ActionParamSchema)),
1058
1724
  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
- });
1725
+ })), AuthoringActionSchema = pinned()(v.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema));
1069
1726
 
1070
- function activityFields({field: field, action: action, op: op, target: target, group: group}) {
1727
+ function activityFields({field: field, action: action, target: target, group: group}) {
1071
1728
  return {
1072
1729
  name: NonEmpty,
1073
1730
  title: v.optional(v.string()),
1074
1731
  description: v.optional(v.string()),
1075
1732
  groups: v.optional(v.array(GroupSchema)),
1076
1733
  group: v.optional(group),
1077
- kind: v.optional(picklist(ACTIVITY_KINDS)),
1078
1734
  target: v.optional(target),
1079
- activation: v.optional(picklist([ "auto", "manual" ])),
1080
1735
  filter: v.optional(ConditionSchema),
1081
1736
  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
1737
  actions: v.optional(v.array(action)),
1087
- subworkflows: v.optional(SubworkflowsSchema),
1088
1738
  fields: v.optional(v.array(field))
1089
1739
  };
1090
1740
  }
@@ -1092,30 +1742,26 @@ function activityFields({field: field, action: action, op: op, target: target, g
1092
1742
  const StoredActivitySchema = pinned()(v.strictObject(activityFields({
1093
1743
  field: FieldEntrySchema,
1094
1744
  action: StoredActionSchema,
1095
- op: StoredOpSchema,
1096
1745
  target: StoredManualTargetSchema,
1097
1746
  group: StoredGroupMembershipSchema
1098
1747
  }))), AuthoringActivitySchema = pinned()(v.strictObject(activityFields({
1099
1748
  field: AuthoringFieldEntrySchema,
1100
1749
  action: AuthoringActionSchema,
1101
- op: AuthoringOpSchema,
1102
1750
  target: AuthoringManualTargetSchema,
1103
1751
  group: AuthoringGroupMembershipSchema
1104
1752
  })));
1105
1753
 
1106
- function transitionFields(op, filter) {
1754
+ function transitionFields(when) {
1107
1755
  return {
1108
1756
  name: NonEmpty,
1109
1757
  title: v.optional(v.string()),
1110
1758
  description: v.optional(v.string()),
1111
1759
  to: NonEmpty,
1112
- filter: filter,
1113
- ops: v.optional(v.array(op)),
1114
- effects: v.optional(v.array(EffectSchema))
1760
+ when: when
1115
1761
  };
1116
1762
  }
1117
1763
 
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({
1764
+ 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
1765
  type: v.literal("self")
1120
1766
  }), v.strictObject({
1121
1767
  type: v.literal("now")
@@ -1177,16 +1823,25 @@ const StoredStageSchema = pinned()(v.strictObject(stageFields({
1177
1823
  transition: AuthoringTransitionSchema,
1178
1824
  guard: AuthoringGuardSchema,
1179
1825
  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" ];
1826
+ }))), 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" ];
1827
+
1828
+ function startFields(kind) {
1829
+ return {
1830
+ kind: kind,
1831
+ filter: v.optional(ConditionSchema)
1832
+ };
1833
+ }
1181
1834
 
1182
- function workflowFields(field, stage) {
1835
+ const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KINDS)))), AuthoringStartSchema = pinned()(v.strictObject(startFields(v.optional(picklist(START_KINDS)))));
1836
+
1837
+ function workflowFields({field: field, stage: stage, start: start}) {
1183
1838
  return {
1184
1839
  name: NonEmpty,
1185
1840
  title: NonEmpty,
1186
1841
  description: v.optional(v.string()),
1187
1842
  groups: v.optional(v.array(GroupSchema)),
1188
1843
  lifecycle: v.optional(picklist(WORKFLOW_LIFECYCLES)),
1189
- applicableWhen: v.optional(ConditionSchema),
1844
+ start: v.optional(start),
1190
1845
  initialStage: NonEmpty,
1191
1846
  fields: v.optional(v.array(field)),
1192
1847
  stages: v.pipe(v.array(stage), v.minLength(1, "must declare at least one stage")),
@@ -1195,7 +1850,11 @@ function workflowFields(field, stage) {
1195
1850
  };
1196
1851
  }
1197
1852
 
1198
- const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields(FieldEntrySchema, StoredStageSchema)));
1853
+ const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields({
1854
+ field: FieldEntrySchema,
1855
+ stage: StoredStageSchema,
1856
+ start: StoredStartSchema
1857
+ })));
1199
1858
 
1200
1859
  function parseStoredDefinition(input, label) {
1201
1860
  return parseOrThrow({
@@ -1205,12 +1864,28 @@ function parseStoredDefinition(input, label) {
1205
1864
  });
1206
1865
  }
1207
1866
 
1208
- const AuthoringWorkflowSchema = pinned()(v.strictObject(workflowFields(AuthoringFieldEntrySchema, AuthoringStageSchema))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
1867
+ const AuthoringWorkflowSchema = pinned()(v.strictObject(workflowFields({
1868
+ field: AuthoringFieldEntrySchema,
1869
+ stage: AuthoringStageSchema,
1870
+ start: AuthoringStartSchema
1871
+ }))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
1209
1872
 
1210
1873
  function isStartableDefinition(definition) {
1211
1874
  return definition.lifecycle !== "child";
1212
1875
  }
1213
1876
 
1877
+ function startKindOf(definition) {
1878
+ return definition.start?.kind ?? "interactive";
1879
+ }
1880
+
1881
+ function isSubjectEntry(entry) {
1882
+ return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
1883
+ }
1884
+
1885
+ function isInputSourced(entry) {
1886
+ return entry.initialValue?.type === "input";
1887
+ }
1888
+
1214
1889
  function formatValidationError(label, issues) {
1215
1890
  const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1216
1891
  return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
@@ -1313,12 +1988,6 @@ function checkActivities({def: def, i: i, activityNames: activityNames, issues:
1313
1988
  }
1314
1989
 
1315
1990
  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
1991
  for (const [a, action] of (activity.actions ?? []).entries()) checkStatusSetOps({
1323
1992
  ops: action.ops,
1324
1993
  activityNames: activityNames,
@@ -1366,25 +2035,11 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
1366
2035
 
1367
2036
  function effectNameSites(def) {
1368
2037
  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
- }
2038
+ 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({
2039
+ effects: action.effects,
2040
+ path: [ "stages", i, "activities", j, "actions", a, "effects" ],
2041
+ sites: sites
2042
+ });
1388
2043
  return sites;
1389
2044
  }
1390
2045
 
@@ -1471,16 +2126,16 @@ function checkPredicates(def, issues) {
1471
2126
  path: [ "predicates", name ],
1472
2127
  message: `predicate "${name}" shadows the built-in $${name} — pick another name`
1473
2128
  });
1474
- for (const [name, groq] of Object.entries(def.predicates ?? {})) checkPredicateBody({
2129
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkPredicateBody({
1475
2130
  name: name,
1476
- groq: groq,
2131
+ groq: groq2,
1477
2132
  names: names,
1478
2133
  issues: issues
1479
2134
  });
1480
2135
  }
1481
2136
 
1482
- function checkPredicateBody({name: name, groq: groq, names: names, issues: issues}) {
1483
- const reads = conditionParameterNames(groq);
2137
+ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issues}) {
2138
+ const reads = conditionParameterNames(groq2);
1484
2139
  for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
1485
2140
  path: [ "predicates", name ],
1486
2141
  message: `predicate "${name}" reads $${caller} — predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
@@ -1506,11 +2161,11 @@ function checkUnboundCallerVars(def, issues) {
1506
2161
  }
1507
2162
 
1508
2163
  function unboundCallerVars(policy) {
1509
- return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : [ "can" ];
2164
+ return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
1510
2165
  }
1511
2166
 
1512
2167
  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`;
2168
+ 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
2169
  }
1515
2170
 
1516
2171
  function conditionSites(def) {
@@ -1543,23 +2198,11 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
1543
2198
  names: entryNames(activity.fields)
1544
2199
  })) ];
1545
2200
  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`,
2201
+ groq: t.when,
2202
+ path: [ "stages", i, "transitions", k, "when" ],
2203
+ label: `transition "${t.name}" when`,
1549
2204
  policy: "cascade",
1550
2205
  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
2206
  });
1564
2207
  collectEditableSites({
1565
2208
  entries: stage.fields,
@@ -1593,11 +2236,14 @@ function collectEditableSites({entries: entries, path: path, labelPrefix: labelP
1593
2236
  });
1594
2237
  }
1595
2238
 
1596
- function collectOpWhereSites({ops: ops, path: path, label: label, fields: fields, sites: sites}) {
2239
+ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
1597
2240
  for (const [o, op] of (ops ?? []).entries()) op.where !== void 0 && sites.push({
1598
2241
  groq: op.where,
1599
2242
  path: [ ...path, o, "where" ],
1600
2243
  label: `${label} ${op.type} where`,
2244
+ ...policy !== void 0 ? {
2245
+ policy: policy
2246
+ } : {},
1601
2247
  fields: fields
1602
2248
  });
1603
2249
  }
@@ -1607,36 +2253,21 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
1607
2253
  scope: "activity",
1608
2254
  names: entryNames(activity.fields)
1609
2255
  }, ...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,
2256
+ activity.filter !== void 0 && sites.push({
2257
+ groq: activity.filter,
2258
+ path: [ ...path, "filter" ],
2259
+ label: `activity "${activity.name}".filter`,
2260
+ policy: "cascade",
2261
+ fields: fields
2262
+ });
2263
+ for (const [name, groq2] of Object.entries(activity.requirements ?? {})) sites.push({
2264
+ groq: groq2,
1622
2265
  path: [ ...path, "requirements", name ],
1623
2266
  label: `activity "${activity.name}" requirement "${name}"`,
1624
2267
  policy: "caller-bound",
1625
2268
  fields: fields
1626
2269
  });
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({
2270
+ collectEditableSites({
1640
2271
  entries: activity.fields,
1641
2272
  path: [ ...path, "fields" ],
1642
2273
  labelPrefix: `activity "${activity.name}"`,
@@ -1647,59 +2278,77 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
1647
2278
  path: path,
1648
2279
  fields: fields,
1649
2280
  sites: sites
1650
- }), collectSubworkflowSites({
1651
- activity: activity,
1652
- path: path,
1653
- fields: fields,
1654
- sites: sites
1655
2281
  });
1656
2282
  }
1657
2283
 
1658
2284
  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
- });
2285
+ for (const [a, action] of (activity.actions ?? []).entries()) {
2286
+ const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
2287
+ action.when !== void 0 && sites.push({
2288
+ groq: action.when,
2289
+ path: [ ...actionPath, "when" ],
2290
+ label: `action "${action.name}" when`,
2291
+ policy: "cascade",
2292
+ fields: fields
2293
+ }), action.filter !== void 0 && sites.push({
2294
+ groq: action.filter,
2295
+ path: [ ...actionPath, "filter" ],
2296
+ label: `action "${action.name}" filter`,
2297
+ policy: cascadeFired ? "cascade" : "caller-bound",
2298
+ fields: fields
2299
+ });
2300
+ const payloadPolicy = cascadeFired ? "triggered-payload" : void 0;
2301
+ collectBindingSites({
2302
+ effects: action.effects,
2303
+ path: [ ...actionPath, "effects" ],
2304
+ label: `action "${action.name}"`,
2305
+ policy: payloadPolicy,
2306
+ fields: fields,
2307
+ sites: sites
2308
+ }), collectOpWhereSites({
2309
+ ops: action.ops,
2310
+ path: [ ...actionPath, "ops" ],
2311
+ label: `action "${action.name}"`,
2312
+ policy: payloadPolicy,
2313
+ fields: fields,
2314
+ sites: sites
2315
+ }), collectSpawnSites({
2316
+ action: action,
2317
+ path: actionPath,
2318
+ fields: fields,
2319
+ sites: sites
2320
+ });
2321
+ }
1678
2322
  }
1679
2323
 
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" ];
2324
+ function collectSpawnSites({action: action, path: path, fields: fields, sites: sites}) {
2325
+ const spawn = action.spawn;
2326
+ if (spawn === void 0) return;
2327
+ const spawnPath = [ ...path, "spawn" ];
1684
2328
  sites.push({
1685
- groq: sub.forEach,
1686
- path: [ ...subPath, "forEach" ],
1687
- label: `activity "${activity.name}".subworkflows.forEach`,
2329
+ groq: spawn.forEach,
2330
+ path: [ ...spawnPath, "forEach" ],
2331
+ label: `action "${action.name}".spawn.forEach`,
2332
+ policy: "cascade",
1688
2333
  fields: fields
1689
2334
  });
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}"`,
2335
+ for (const [group, record] of [ [ "with", spawn.with ], [ "context", spawn.context ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2336
+ groq: groq2,
2337
+ path: [ ...spawnPath, group, key ],
2338
+ label: `action "${action.name}".spawn.${group} "${key}"`,
2339
+ policy: "triggered-payload",
1694
2340
  fields: fields
1695
2341
  });
1696
2342
  }
1697
2343
 
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,
2344
+ function collectBindingSites({effects: effects, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
2345
+ for (const [e, effect] of (effects ?? []).entries()) for (const [key, groq2] of Object.entries(effect.bindings ?? {})) sites.push({
2346
+ groq: groq2,
1701
2347
  path: [ ...path, e, "bindings", key ],
1702
2348
  label: `${label} effect "${effect.name}" binding "${key}"`,
2349
+ ...policy !== void 0 ? {
2350
+ policy: policy
2351
+ } : {},
1703
2352
  fields: fields
1704
2353
  });
1705
2354
  }
@@ -1711,36 +2360,76 @@ function checkAssigneesEntries(def, issues) {
1711
2360
  });
1712
2361
  }
1713
2362
 
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
- };
2363
+ function checkActivityTerminalPaths(def, issues) {
2364
+ for (const [i, stage] of def.stages.entries()) {
2365
+ const resolvable = terminallyResolvableActivities(stage);
2366
+ for (const [j, activity] of (stage.activities ?? []).entries()) resolvable.has(activity.name) || issues.push({
2367
+ path: [ "stages", i, "activities", j ],
2368
+ 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`
2369
+ });
2370
+ }
1721
2371
  }
1722
2372
 
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
- };
2373
+ function terminallyResolvableActivities(stage) {
2374
+ 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));
2375
+ return new Set(terminalSets.map(op => op.activity));
2376
+ }
1730
2377
 
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}`
2378
+ function checkTerminalStageActivities(def, issues) {
2379
+ for (const [i, stage] of def.stages.entries()) (stage.transitions ?? []).length > 0 || (stage.activities ?? []).length === 0 || issues.push({
2380
+ path: [ "stages", i, "activities" ],
2381
+ 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`
2382
+ });
2383
+ }
2384
+
2385
+ function storedRolesIssue(action) {
2386
+ if (action.roles !== void 0) {
2387
+ 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`;
2388
+ 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`;
2389
+ }
2390
+ }
2391
+
2392
+ function checkStoredRolesPlacement(def, issues) {
2393
+ for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) {
2394
+ const message = storedRolesIssue(action);
2395
+ message !== void 0 && issues.push({
2396
+ path: [ "stages", i, "activities", j, "actions", a, "roles" ],
2397
+ message: message
1740
2398
  });
1741
2399
  }
1742
2400
  }
1743
2401
 
2402
+ function checkTriggeredActionParams(def, issues) {
2403
+ 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({
2404
+ path: [ "stages", i, "activities", j, "actions", a, "params" ],
2405
+ 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`
2406
+ });
2407
+ }
2408
+
2409
+ function checkStart(def, issues) {
2410
+ if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2411
+ path: [ "start" ],
2412
+ 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'`"
2413
+ }), checkStartFilterReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
2414
+ path: [ "fields", n, "required" ],
2415
+ 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`
2416
+ });
2417
+ }
2418
+
2419
+ function checkStartFilterReads(def, issues) {
2420
+ const filter = def.start?.filter;
2421
+ if (filter === void 0) return;
2422
+ const bindable = new Set((def.fields ?? []).filter(isInputSourced).map(entry => entry.name)), declared = new Set((def.fields ?? []).map(entry => entry.name));
2423
+ for (const name of conditionFieldReadNames(filter)) bindable.has(name) || issues.push({
2424
+ path: [ "start", "filter" ],
2425
+ 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)}`
2426
+ });
2427
+ readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2428
+ path: [ "start", "filter" ],
2429
+ 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"
2430
+ });
2431
+ }
2432
+
1744
2433
  function checkGroups(def, issues) {
1745
2434
  const workflowGroups = checkGroupDeclarations({
1746
2435
  groups: def.groups,
@@ -1857,7 +2546,7 @@ function checkConditionFieldReads(def, issues) {
1857
2546
  path: site.path,
1858
2547
  message: undeclaredFieldReadMessage(site, name)
1859
2548
  });
1860
- for (const [name, groq] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq)) everywhere.has(read) || issues.push({
2549
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq2)) everywhere.has(read) || issues.push({
1861
2550
  path: [ "predicates", name ],
1862
2551
  message: noFieldAnywhereMessage(`predicate "${name}"`, read)
1863
2552
  });
@@ -1961,38 +2650,29 @@ function seedEarlierSiblingTarget(args) {
1961
2650
  });
1962
2651
  }
1963
2652
 
2653
+ function opSites(def) {
2654
+ const sites = [];
2655
+ 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({
2656
+ ops: action.ops,
2657
+ path: [ "stages", i, "activities", j, "actions", a, "ops" ],
2658
+ label: `action "${action.name}"`,
2659
+ stage: stage,
2660
+ activity: activity
2661
+ });
2662
+ return sites;
2663
+ }
2664
+
1964
2665
  function checkFieldReadOpValues(def, issues) {
1965
2666
  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
- }
2667
+ for (const site of opSites(def)) checkOpsFieldReads({
2668
+ workflowEntries: workflowEntries,
2669
+ stageEntries: site.stage.fields ?? [],
2670
+ issues: issues,
2671
+ ops: site.ops,
2672
+ path: site.path,
2673
+ label: site.label,
2674
+ activityNames: entryNames(site.activity.fields)
2675
+ });
1996
2676
  }
1997
2677
 
1998
2678
  function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
@@ -2045,6 +2725,49 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activit
2045
2725
  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
2726
  }
2047
2727
 
2728
+ function checkUpdateWhereOps(def, issues) {
2729
+ const workflow = def.fields ?? [];
2730
+ for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
2731
+ if (op.type !== "field.updateWhere") continue;
2732
+ const scopes = {
2733
+ workflow: workflow,
2734
+ stage: site.stage.fields ?? [],
2735
+ activity: site.activity.fields ?? []
2736
+ };
2737
+ checkUpdateWhereTargetKind({
2738
+ op: op,
2739
+ path: [ ...site.path, o ],
2740
+ label: site.label,
2741
+ scopes: scopes,
2742
+ issues: issues
2743
+ }), checkUpdateWhereMergeKeys({
2744
+ op: op,
2745
+ path: [ ...site.path, o ],
2746
+ label: site.label,
2747
+ issues: issues
2748
+ });
2749
+ }
2750
+ }
2751
+
2752
+ function checkUpdateWhereTargetKind({op: op, path: path, label: label, scopes: scopes, issues: issues}) {
2753
+ const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
2754
+ target === void 0 || target.type === "array" || issues.push({
2755
+ path: [ ...path, "target" ],
2756
+ 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)}`
2757
+ });
2758
+ }
2759
+
2760
+ function rowOpsHint(type) {
2761
+ return type === "doc.refs" || type === "assignees" ? `; to add or drop ${type} rows use field.append / field.removeWhere` : "";
2762
+ }
2763
+
2764
+ function checkUpdateWhereMergeKeys({op: op, path: path, label: label, issues: issues}) {
2765
+ if (op.value.type === "object") for (const key of Object.keys(op.value.fields)) key !== "_key" && key !== "_type" || issues.push({
2766
+ path: [ ...path, "value", "fields", key ],
2767
+ 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`
2768
+ });
2769
+ }
2770
+
2048
2771
  function checkGuardFieldReads(def, issues) {
2049
2772
  const reads = {
2050
2773
  workflowEntries: def.fields ?? [],
@@ -2242,10 +2965,12 @@ function checkWorkflowInvariants(def) {
2242
2965
  stageNames: stageNames,
2243
2966
  issues: issues
2244
2967
  }), 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),
2968
+ checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
2969
+ checkUnboundCallerVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
2970
+ checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
2971
+ checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
2972
+ checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
2248
2973
  checkGroups(def, issues), issues;
2249
2974
  }
2250
2975
 
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 };
2976
+ export { ACTIVITY_KINDS, ACTOR_KINDS, 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, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GroupSchema, InstanceNotFoundError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, 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, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };