@sanity/workflow-engine 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -295,17 +315,17 @@ function effectNotFoundMessage(args) {
295
315
  return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
296
316
  }
297
317
 
298
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
318
+ const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
299
319
 
300
320
  function validateTag(tag) {
301
- if (!TAG_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`);
321
+ if (!LAKE_ID_SEGMENT_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS})`);
302
322
  }
303
323
 
304
324
  function tagScopeFilter() {
305
325
  return "tag == $tag";
306
326
  }
307
327
 
308
- const NonEmptyString = v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty("must not be empty"));
328
+ const NonEmptyString$1 = v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty("must not be empty"));
309
329
 
310
330
  function asPredicate(validate) {
311
331
  return value => {
@@ -319,21 +339,21 @@ function asPredicate(validate) {
319
339
 
320
340
  const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v__namespace.variant("type", [ v__namespace.object({
321
341
  type: v__namespace.literal("dataset"),
322
- id: v__namespace.pipe(NonEmptyString, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
342
+ id: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
323
343
  }), v__namespace.object({
324
344
  type: v__namespace.literal("canvas"),
325
- id: NonEmptyString
345
+ id: NonEmptyString$1
326
346
  }), v__namespace.object({
327
347
  type: v__namespace.literal("media-library"),
328
- id: NonEmptyString
348
+ id: NonEmptyString$1
329
349
  }), v__namespace.object({
330
350
  type: v__namespace.literal("dashboard"),
331
- id: NonEmptyString
351
+ id: NonEmptyString$1
332
352
  }) ]), ResourceBindingSchema = v__namespace.object({
333
- name: v__namespace.pipe(NonEmptyString, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
353
+ name: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
334
354
  resource: WorkflowResourceSchema
335
355
  }), DefinitionSchema = v__namespace.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v__namespace.object({
336
- name: NonEmptyString,
356
+ name: NonEmptyString$1,
337
357
  tag: v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty(), v__namespace.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
338
358
  workflowResource: WorkflowResourceSchema,
339
359
  resourceAliases: v__namespace.optional(v__namespace.pipe(v__namespace.array(ResourceBindingSchema), v__namespace.check(bindings => new Set(bindings.map(binding => binding.name)).size === bindings.length, "duplicate resource handle name — each binding name must be unique within a deployment"))),
@@ -397,6 +417,599 @@ 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
+ ...start.allowed !== void 0 ? {
523
+ allowed: start.allowed
524
+ } : {}
525
+ };
526
+ }
527
+
528
+ function checkReservedRoleAliasKeys(aliases, issues) {
529
+ for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
530
+ path: [ "roleAliases", key ],
531
+ 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.`
532
+ });
533
+ }
534
+
535
+ const TODOLIST_OF = [ {
536
+ type: "string",
537
+ name: "label",
538
+ title: "Label"
539
+ }, {
540
+ type: "string",
541
+ name: "status",
542
+ title: "Status"
543
+ }, {
544
+ type: "assignee",
545
+ name: "assignee",
546
+ title: "Assignee"
547
+ }, {
548
+ type: "date",
549
+ name: "dueDate",
550
+ title: "Due date"
551
+ } ], NOTES_OF = [ {
552
+ type: "text",
553
+ name: "body",
554
+ title: "Body"
555
+ }, {
556
+ type: "actor",
557
+ name: "actor",
558
+ title: "Actor"
559
+ }, {
560
+ type: "datetime",
561
+ name: "at",
562
+ title: "At"
563
+ } ];
564
+
565
+ function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
566
+ return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
567
+ entry: entry,
568
+ path: [ ...path, i ],
569
+ ctx: ctx
570
+ }));
571
+ }
572
+
573
+ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
574
+ if (entry.type === "claim") return desugarClaimField({
575
+ entry: entry,
576
+ path: path,
577
+ ctx: ctx
578
+ });
579
+ if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
580
+ entry: entry,
581
+ path: path,
582
+ ctx: ctx
583
+ });
584
+ const editable = normalizeEditable({
585
+ editable: entry.editable,
586
+ path: [ ...path, "editable" ],
587
+ ctx: ctx
588
+ });
589
+ return {
590
+ ...stripUndefined({
591
+ type: entry.type,
592
+ name: entry.name,
593
+ title: entry.title,
594
+ description: entry.description,
595
+ group: normalizeGroup(entry.group),
596
+ required: entry.required,
597
+ initialValue: entry.initialValue,
598
+ editable: editable,
599
+ types: entry.types,
600
+ fields: entry.fields,
601
+ of: entry.of
602
+ })
603
+ };
604
+ }
605
+
606
+ function desugarClaimField({entry: entry, path: path, ctx: ctx}) {
607
+ const desugared = {
608
+ ...stripUndefined({
609
+ name: entry.name,
610
+ title: entry.title,
611
+ description: entry.description,
612
+ group: normalizeGroup(entry.group)
613
+ }),
614
+ type: "actor"
615
+ };
616
+ return ctx.claimFields.set(desugared, {
617
+ name: entry.name,
618
+ path: path
619
+ }), desugared;
620
+ }
621
+
622
+ function desugarListField({entry: entry, path: path, ctx: ctx}) {
623
+ const editable = normalizeEditable({
624
+ editable: entry.editable,
625
+ path: [ ...path, "editable" ],
626
+ ctx: ctx
627
+ });
628
+ return {
629
+ ...stripUndefined({
630
+ name: entry.name,
631
+ title: entry.title,
632
+ description: entry.description,
633
+ group: normalizeGroup(entry.group),
634
+ required: entry.required,
635
+ initialValue: entry.initialValue,
636
+ editable: editable
637
+ }),
638
+ type: "array",
639
+ of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
640
+ };
641
+ }
642
+
643
+ function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
644
+ if (editable === void 0 || editable === !0) return editable;
645
+ if (Array.isArray(editable)) {
646
+ const condition = rolesCondition(editable, ctx.roleAliases);
647
+ if (condition === void 0) {
648
+ ctx.issues.push({
649
+ path: path,
650
+ message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
651
+ });
652
+ return;
653
+ }
654
+ return condition;
655
+ }
656
+ return editable;
657
+ }
658
+
659
+ function editableFieldNames({workflowFields: workflowFields2, stageFields: stageFields2, activities: activities}) {
660
+ const names = /* @__PURE__ */ new Set, collect = entries => {
661
+ for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
662
+ };
663
+ collect(workflowFields2), collect(stageFields2);
664
+ for (const activity of activities) collect(activity.fields);
665
+ return names;
666
+ }
667
+
668
+ function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
669
+ if (overrides === void 0) return;
670
+ const out = {};
671
+ for (const [name, value] of Object.entries(overrides)) {
672
+ if (!inScope.has(name)) {
673
+ ctx.issues.push({
674
+ path: [ ...path, name ],
675
+ 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\``
676
+ });
677
+ continue;
678
+ }
679
+ const normalized = normalizeEditable({
680
+ editable: value,
681
+ path: [ ...path, name ],
682
+ ctx: ctx
683
+ });
684
+ normalized !== void 0 && (out[name] = normalized);
685
+ }
686
+ return Object.keys(out).length > 0 ? out : void 0;
687
+ }
688
+
689
+ function layerOf(entries) {
690
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
691
+ }
692
+
693
+ function normalizeGroup(group) {
694
+ return group === void 0 || Array.isArray(group) ? group : [ group ];
695
+ }
696
+
697
+ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
698
+ const activityFields2 = desugarFieldEntries({
699
+ entries: activity.fields,
700
+ path: [ ...path, "fields" ],
701
+ ctx: ctx
702
+ }), env = {
703
+ layers: [ {
704
+ scope: "activity",
705
+ entries: layerOf(activityFields2)
706
+ }, ...stageEnv.layers ]
707
+ }, actions = (activity.actions ?? []).map((action, a) => desugarAction({
708
+ action: action,
709
+ path: [ ...path, "actions", a ],
710
+ env: env,
711
+ activityName: activity.name,
712
+ ctx: ctx
713
+ })), target = desugarTarget({
714
+ target: activity.target,
715
+ env: env,
716
+ path: [ ...path, "target" ],
717
+ ctx: ctx
718
+ });
719
+ return {
720
+ ...stripUndefined({
721
+ name: activity.name,
722
+ title: activity.title,
723
+ description: activity.description,
724
+ groups: activity.groups,
725
+ group: normalizeGroup(activity.group),
726
+ filter: activity.filter,
727
+ requirements: activity.requirements
728
+ }),
729
+ ...target ? {
730
+ target: target
731
+ } : {},
732
+ ...activityFields2 ? {
733
+ fields: activityFields2
734
+ } : {},
735
+ ...actions.length > 0 ? {
736
+ actions: actions
737
+ } : {}
738
+ };
739
+ }
740
+
741
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
742
+
743
+ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
744
+ if (target === void 0 || target.type === "url") return target;
745
+ const ref = typeof target.field == "string" ? {
746
+ field: target.field
747
+ } : target.field, field = resolveRef({
748
+ ref: ref,
749
+ env: env,
750
+ path: [ ...path, "field" ],
751
+ ctx: ctx
752
+ }) ?? fallbackRef(ref), entry = entryAt(env, field);
753
+ return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
754
+ path: [ ...path, "field" ],
755
+ message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
756
+ }), {
757
+ type: "field",
758
+ field: field
759
+ };
760
+ }
761
+
762
+ function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
763
+ if ("type" in action) return desugarClaimAction({
764
+ action: action,
765
+ path: path,
766
+ env: env,
767
+ ctx: ctx
768
+ });
769
+ action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
770
+ path: [ ...path, "roles" ],
771
+ message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
772
+ });
773
+ const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
774
+ ops: action.ops,
775
+ path: [ ...path, "ops" ],
776
+ env: env,
777
+ firingActivity: activityName,
778
+ ctx: ctx
779
+ }) ?? [];
780
+ return action.status !== void 0 && ops.push({
781
+ type: "status.set",
782
+ activity: activityName,
783
+ status: action.status
784
+ }), {
785
+ ...stripUndefined({
786
+ name: action.name,
787
+ title: action.title,
788
+ description: action.description,
789
+ group: normalizeGroup(action.group),
790
+ when: action.when,
791
+ params: action.params,
792
+ effects: action.effects,
793
+ spawn: action.spawn
794
+ }),
795
+ ...cascadeFired && action.roles !== void 0 && action.roles.length > 0 ? {
796
+ roles: action.roles
797
+ } : {},
798
+ ...filter ? {
799
+ filter: filter
800
+ } : {},
801
+ ...ops.length > 0 ? {
802
+ ops: ops
803
+ } : {}
804
+ };
805
+ }
806
+
807
+ function desugarClaimAction({action: action, path: path, env: env, ctx: ctx}) {
808
+ const ref = typeof action.field == "string" ? {
809
+ field: action.field
810
+ } : action.field, resolved = resolveRef({
811
+ ref: ref,
812
+ env: env,
813
+ path: [ ...path, "field" ],
814
+ ctx: ctx
815
+ });
816
+ if (resolved) {
817
+ const entry = entryAt(env, resolved);
818
+ entry && entry.type !== "actor" && ctx.issues.push({
819
+ path: [ ...path, "field" ],
820
+ 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)`
821
+ }), checkShadowedClaimTarget({
822
+ actionName: action.name,
823
+ field: ref.field,
824
+ resolved: resolved,
825
+ env: env,
826
+ path: [ ...path, "field" ],
827
+ ctx: ctx
828
+ }), entry && ctx.claimedFields.add(entry);
829
+ }
830
+ const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([ noSteal, rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = resolved ? [ {
831
+ type: "field.set",
832
+ target: resolved,
833
+ value: {
834
+ type: "actor"
835
+ }
836
+ } ] : [];
837
+ return {
838
+ ...stripUndefined({
839
+ name: action.name,
840
+ title: action.title,
841
+ description: action.description,
842
+ group: normalizeGroup(action.group),
843
+ params: action.params,
844
+ effects: action.effects
845
+ }),
846
+ filter: filter,
847
+ ...ops.length > 0 ? {
848
+ ops: ops
849
+ } : {}
850
+ };
851
+ }
852
+
853
+ function checkShadowedClaimTarget({actionName: actionName, field: field, resolved: resolved, env: env, path: path, ctx: ctx}) {
854
+ const nearest = env.layers.find(l => l.entries.has(field));
855
+ nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
856
+ path: path,
857
+ 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`
858
+ });
859
+ }
860
+
861
+ function checkUnclaimedClaimFields(ctx) {
862
+ for (const [entry, {name: name, path: path}] of ctx.claimFields) ctx.claimedFields.has(entry) || ctx.issues.push({
863
+ path: path,
864
+ 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`
865
+ });
866
+ }
867
+
868
+ const DEFAULT_TRANSITION_WHEN = "$allActivitiesDone";
869
+
870
+ function desugarTransition({transition: transition}) {
871
+ return {
872
+ ...stripUndefined({
873
+ name: transition.name,
874
+ title: transition.title,
875
+ description: transition.description,
876
+ to: transition.to
877
+ }),
878
+ when: transition.when ?? DEFAULT_TRANSITION_WHEN
879
+ };
880
+ }
881
+
882
+ function desugarGuard(guard) {
883
+ const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
884
+ return {
885
+ ...rest,
886
+ match: {
887
+ ...matchRest,
888
+ ...idRefs !== void 0 ? {
889
+ idRefs: idRefs.map(printGuardRead)
890
+ } : {}
891
+ },
892
+ ...metadata !== void 0 ? {
893
+ metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
894
+ } : {}
895
+ };
896
+ }
897
+
898
+ function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
899
+ if (ops) return ops.map((op, i) => desugarOp({
900
+ op: op,
901
+ path: [ ...path, i ],
902
+ env: env,
903
+ firingActivity: firingActivity,
904
+ ctx: ctx
905
+ }));
906
+ }
907
+
908
+ function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
909
+ if (op.type === "status.set") return {
910
+ type: "status.set",
911
+ activity: op.activity ?? firingActivity,
912
+ status: op.status
913
+ };
914
+ if (op.type === "audit") return desugarAuditOp({
915
+ op: op,
916
+ path: path,
917
+ env: env,
918
+ ctx: ctx
919
+ });
920
+ const target = resolveRef({
921
+ ref: op.target,
922
+ env: env,
923
+ path: [ ...path, "target" ],
924
+ ctx: ctx
925
+ }) ?? fallbackRef(op.target);
926
+ return {
927
+ ...op,
928
+ target: target
929
+ };
930
+ }
931
+
932
+ const AUDIT_STAMPS = {
933
+ actor: {
934
+ type: "actor"
935
+ },
936
+ at: {
937
+ type: "now"
938
+ }
939
+ };
940
+
941
+ function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
942
+ const target = resolveRef({
943
+ ref: op.target,
944
+ env: env,
945
+ path: [ ...path, "target" ],
946
+ ctx: ctx
947
+ }) ?? fallbackRef(op.target);
948
+ if (op.value.type !== "object") return ctx.issues.push({
949
+ path: [ ...path, "value" ],
950
+ message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
951
+ }), {
952
+ type: "field.append",
953
+ target: target,
954
+ value: op.value
955
+ };
956
+ const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
957
+ ...op.value.fields
958
+ };
959
+ for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
960
+ if (stamp in fields) {
961
+ ctx.issues.push({
962
+ path: [ ...path, "value", "fields", stamp ],
963
+ message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
964
+ });
965
+ continue;
966
+ }
967
+ fields[stamp] = source;
968
+ }
969
+ return {
970
+ type: "field.append",
971
+ target: target,
972
+ value: {
973
+ type: "object",
974
+ fields: fields
975
+ }
976
+ };
977
+ }
978
+
979
+ function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
980
+ const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
981
+ for (const layer of layers) if (layer.entries.has(ref.field)) return {
982
+ scope: layer.scope,
983
+ field: ref.field
984
+ };
985
+ const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
986
+ ctx.issues.push({
987
+ path: path,
988
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
989
+ });
990
+ }
991
+
992
+ function entryAt(env, ref) {
993
+ return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
994
+ }
995
+
996
+ function fallbackRef(ref) {
997
+ return {
998
+ scope: ref.scope ?? "workflow",
999
+ field: ref.field
1000
+ };
1001
+ }
1002
+
1003
+ function rolesCondition(roles, aliases) {
1004
+ if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
1005
+ }
1006
+
1007
+ function stripUndefined(obj) {
1008
+ const out = {};
1009
+ for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
1010
+ return out;
1011
+ }
1012
+
400
1013
  function isUnevaluable(result) {
401
1014
  return result == null;
402
1015
  }
@@ -416,9 +1029,9 @@ async function evaluateCondition(args) {
416
1029
 
417
1030
  async function evaluatePredicates(args) {
418
1031
  const out = {};
419
- for (const [name, groq] of Object.entries(args.predicates ?? {})) {
1032
+ for (const [name, groq2] of Object.entries(args.predicates ?? {})) {
420
1033
  const result = await runGroq({
421
- groq: groq,
1034
+ groq: groq2,
422
1035
  params: args.params,
423
1036
  snapshot: args.snapshot
424
1037
  }), outcome = groqConditionDescribe.conditionOutcome(result);
@@ -427,46 +1040,77 @@ async function evaluatePredicates(args) {
427
1040
  return out;
428
1041
  }
429
1042
 
430
- async function runGroq({groq: groq, params: params, snapshot: snapshot}) {
1043
+ async function runGroq({groq: groq2, params: params, snapshot: snapshot}) {
431
1044
  return groqConditionDescribe.runGroq({
432
- groq: groq,
1045
+ groq: groq2,
433
1046
  params: params,
434
1047
  dataset: snapshot.docs
435
1048
  });
436
1049
  }
437
1050
 
438
- function conditionSyntaxIssues(groq, boundVars) {
1051
+ function conditionSyntaxIssues(groq2, boundVars) {
439
1052
  const issues = [];
440
1053
  let tree;
441
1054
  try {
442
- tree = groqJs.parse(groq);
1055
+ tree = groqJs.parse(groq2);
443
1056
  } catch (err) {
444
1057
  issues.push(errorMessage(err));
445
1058
  }
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(", "));
1059
+ 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."),
1060
+ 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
1061
  return issues;
449
1062
  }
450
1063
 
451
- function conditionParameterNames(groq) {
1064
+ function conditionParameterNames(groq2) {
452
1065
  const read = /* @__PURE__ */ new Set;
453
- return walkAstNodes(tryParseGroq(groq), node => {
1066
+ return walkAstNodes(tryParseGroq(groq2), node => {
454
1067
  node.type === "Parameter" && typeof node.name == "string" && read.add(node.name);
455
1068
  }), read;
456
1069
  }
457
1070
 
458
- function conditionFieldReadNames(groq) {
459
- const read = /* @__PURE__ */ new Set;
460
- return walkAstNodes(tryParseGroq(groq), node => {
461
- if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
462
- const base = node.base;
463
- base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
464
- }), read;
1071
+ function conditionFieldReadNames(groq2) {
1072
+ return new Set(conditionFieldReads(groq2).map(read => read.name));
1073
+ }
1074
+
1075
+ function conditionFieldReads(groq2) {
1076
+ const reads = /* @__PURE__ */ new Map;
1077
+ return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
1078
+ }
1079
+
1080
+ function collectFieldReads(node, reads) {
1081
+ if (Array.isArray(node)) {
1082
+ for (const item of node) collectFieldReads(item, reads);
1083
+ return;
1084
+ }
1085
+ if (typeof node != "object" || node === null) return;
1086
+ const chain = fieldsAttributeChain(node);
1087
+ if (chain !== void 0) {
1088
+ const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
1089
+ reads.set(`${name}.${path ?? ""}`, {
1090
+ name: name,
1091
+ path: path
1092
+ });
1093
+ return;
1094
+ }
1095
+ for (const value of Object.values(node)) collectFieldReads(value, reads);
1096
+ }
1097
+
1098
+ function fieldsAttributeChain(node) {
1099
+ const names = [];
1100
+ let current = node;
1101
+ for (;typeof current == "object" && current !== null; ) {
1102
+ const typed = current;
1103
+ if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
1104
+ names.unshift(typed.name);
1105
+ const base = typed.base;
1106
+ if (base?.type === "Parameter" && base.name === "fields") return names;
1107
+ current = base;
1108
+ }
465
1109
  }
466
1110
 
467
- function conditionEffectReads(groq) {
1111
+ function conditionEffectReads(groq2) {
468
1112
  const reads = [];
469
- return walkAstNodes(tryParseGroq(groq), node => {
1113
+ return walkAstNodes(tryParseGroq(groq2), node => {
470
1114
  if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
471
1115
  const base = node.base;
472
1116
  base?.type !== "AccessAttribute" || typeof base.name != "string" || base.base?.type === "Parameter" && base.base.name === "effects" && reads.push({
@@ -476,9 +1120,34 @@ function conditionEffectReads(groq) {
476
1120
  }), reads;
477
1121
  }
478
1122
 
479
- function tryParseGroq(groq) {
1123
+ function readsRootDocument(groq2) {
1124
+ return nodeReadsRoot(tryParseGroq(groq2), 0);
1125
+ }
1126
+
1127
+ const SCOPED_CHILD = {
1128
+ Filter: "expr",
1129
+ Projection: "expr",
1130
+ Map: "expr",
1131
+ FlatMap: "expr",
1132
+ PipeFuncCall: "args"
1133
+ };
1134
+
1135
+ function nodeReadsRoot(node, depth) {
1136
+ if (Array.isArray(node)) return node.some(item => nodeReadsRoot(item, depth));
1137
+ if (typeof node != "object" || node === null) return !1;
1138
+ const typed = node;
1139
+ if (depth === 0 && typed.type === "This" || depth === 0 && typed.type === "AccessAttribute" && typed.base === void 0) return !0;
1140
+ if (typed.type === "Parent") {
1141
+ const climb = typeof typed.n == "number" ? typed.n : 1;
1142
+ return depth - climb <= 0;
1143
+ }
1144
+ const scopedChild = typeof typed.type == "string" ? SCOPED_CHILD[typed.type] : void 0;
1145
+ 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));
1146
+ }
1147
+
1148
+ function tryParseGroq(groq2) {
480
1149
  try {
481
- return groqJs.parse(groq);
1150
+ return groqJs.parse(groq2);
482
1151
  } catch {
483
1152
  return;
484
1153
  }
@@ -495,7 +1164,13 @@ function walkAstNodes(node, visit) {
495
1164
  }
496
1165
  }
497
1166
 
498
- const CONDITION_VARS = [ {
1167
+ const ACTIVITY_STATUSES = [ "active", "done", "skipped", "failed" ], TERMINAL_ACTIVITY_STATUSES = [ "done", "skipped", "failed" ];
1168
+
1169
+ function isTerminalActivityStatus(status) {
1170
+ return TERMINAL_ACTIVITY_STATUSES.includes(status);
1171
+ }
1172
+
1173
+ 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
1174
  name: "self",
500
1175
  binding: "always",
501
1176
  label: "this workflow instance",
@@ -504,7 +1179,7 @@ const CONDITION_VARS = [ {
504
1179
  name: "fields",
505
1180
  binding: "always",
506
1181
  label: "the workflow's fields",
507
- description: "Declared field entries rendered by name (`$fields.<name>` is the value, no envelope). Stage/activity scopes overlay lexically; `doc.ref` values render the hydrated document when the snapshot holds it."
1182
+ description: "Declared field entries rendered by name (`$fields.<name>` is the value, no wrapper). Stage/activity scopes overlay lexically. What a read puts in your hand follows the declared kind: a singular `doc.ref` DEREFERENCES into the hydrated document (lake `_id`/`_type` plus content fields — which may themselves be named `id`/`type`), while `doc.refs` elements and `release.ref` stay REFERENCES — `{id, type[, releaseName]}` with `id` a GDR URI — because identity reads (membership, counting, joins) must stay total without hydrating every target, and targets may live in resources the evaluation cannot fetch from. Conditions evaluate against the in-memory snapshot only, so a dereferencing plural kind would silently hole wherever hydration lagged."
508
1183
  }, {
509
1184
  name: "parent",
510
1185
  binding: "always",
@@ -525,11 +1200,16 @@ const CONDITION_VARS = [ {
525
1200
  binding: "always",
526
1201
  label: "the current time",
527
1202
  description: "The ISO clock reading shared by every condition in one evaluation pass, so they agree on the time."
1203
+ }, {
1204
+ name: "context",
1205
+ binding: "always",
1206
+ label: "start-time context",
1207
+ 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
1208
  }, {
529
1209
  name: "effects",
530
1210
  binding: "always",
531
1211
  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."
1212
+ 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
1213
  }, {
534
1214
  name: "effectStatus",
535
1215
  binding: "always",
@@ -564,29 +1244,169 @@ const CONDITION_VARS = [ {
564
1244
  name: "can",
565
1245
  binding: "caller",
566
1246
  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."
1247
+ 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
1248
  }, {
569
1249
  name: "row",
570
1250
  binding: "spawn",
571
1251
  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)."
1252
+ description: "One `spawn.forEach` result row, bound while its `with` map evaluates — and the row under test while a where-op `where` evaluates (per row). Deploy rejects a read at every other site (including `spawn.forEach` itself and `spawn.context`), where it is GROQ null."
573
1253
  }, {
574
1254
  name: "params",
575
1255
  binding: "caller",
576
1256
  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."
1257
+ 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
1258
  }, {
579
1259
  name: "subworkflows",
580
1260
  binding: "always",
581
1261
  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 = [ {
1262
+ 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`."
1263
+ } ], 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 = [ {
1264
+ name: "tag",
1265
+ description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1266
+ }, {
1267
+ name: "definition",
1268
+ description: "The `name` of the definition under evaluation (its own start block binds it)."
1269
+ }, {
1270
+ name: "now",
1271
+ description: "The ISO clock reading of the evaluating engine."
1272
+ } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1273
+ name: "fields",
1274
+ description: "The caller's input entries by name (`initialFields` — at `startInstance`, the values the start would seed; at a pre-flight, the values gathered so far, so a read of a not-yet-supplied entry is GROQ null). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble — a singular `doc.ref` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
1275
+ } ], GUARD_PREDICATE_VARS = [ {
584
1276
  name: "guard",
585
1277
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
586
1278
  }, {
587
1279
  name: "mutation",
588
1280
  description: "The attempted mutation — `mutation.action` is the write kind being gated."
589
- } ], ACTOR_KINDS = [ "person", "agent", "system" ];
1281
+ } ], NonEmptyString = v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "must be a non-empty string"));
1282
+
1283
+ function isParseableInstant(value) {
1284
+ return typeof value == "string" && !Number.isNaN(Date.parse(value));
1285
+ }
1286
+
1287
+ const IsoTimestamp = v__namespace.pipe(v__namespace.string(), v__namespace.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
1288
+
1289
+ function tolerantObject() {
1290
+ return (entries, ..._exact) => v__namespace.looseObject(entries);
1291
+ }
1292
+
1293
+ function tolerantEntries() {
1294
+ return (entries, ..._exact) => entries;
1295
+ }
1296
+
1297
+ function schemaTreeShape(schema) {
1298
+ return walkSchemaShape(schema, /* @__PURE__ */ new Set);
1299
+ }
1300
+
1301
+ function walkSchemaShape(node, path) {
1302
+ if (typeof node != "object" || node === null) return "unknown";
1303
+ if (path.has(node)) return "(circular)";
1304
+ path.add(node);
1305
+ try {
1306
+ const schema = node;
1307
+ return containerShape(schema, path) ?? leafShape(schema);
1308
+ } finally {
1309
+ path.delete(node);
1310
+ }
1311
+ }
1312
+
1313
+ const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
1314
+ union: schema.options.map(option => walkSchemaShape(option, path))
1315
+ }), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
1316
+ strict_object: objectWalker,
1317
+ loose_object: objectWalker,
1318
+ object: objectWalker,
1319
+ array: (schema, path) => ({
1320
+ array: walkSchemaShape(schema.item, path)
1321
+ }),
1322
+ record: (schema, path) => ({
1323
+ record: walkSchemaShape(schema.value, path)
1324
+ }),
1325
+ union: unionWalker,
1326
+ variant: unionWalker,
1327
+ lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
1328
+ exact_optional: unwrapWalker,
1329
+ optional: unwrapWalker,
1330
+ nullable: unwrapWalker
1331
+ };
1332
+
1333
+ function containerShape(schema, path) {
1334
+ return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
1335
+ }
1336
+
1337
+ function objectEntriesShape(entries, path) {
1338
+ const out = {};
1339
+ for (const [key, entry] of Object.entries(entries)) {
1340
+ const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
1341
+ out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
1342
+ }
1343
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
1344
+ }
1345
+
1346
+ function isOptionalEntry(entry) {
1347
+ if (typeof entry != "object" || entry === null) return !1;
1348
+ const kind = entry.type;
1349
+ return kind === "exact_optional" || kind === "optional";
1350
+ }
1351
+
1352
+ const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
1353
+
1354
+ function leafShape(schema) {
1355
+ if (schema.type === "picklist") return schema.options.join(" | ");
1356
+ if (schema.type === "literal") return `literal ${String(schema.literal)}`;
1357
+ if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
1358
+ if (!LEAF_KINDS.has(String(schema.type))) throw new Error(`schemaTreeShape: unhandled schema kind "${String(schema.type)}" — extend the walker before regenerating the model ledger`);
1359
+ return String(schema.type);
1360
+ }
1361
+
1362
+ function formatValidationError(label, issues) {
1363
+ const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1364
+ return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1365
+ }
1366
+
1367
+ function issuesFromValibot(issues) {
1368
+ return issues.map(issue => ({
1369
+ path: issue.path ? issue.path.map(item => item.key) : [],
1370
+ message: issue.message
1371
+ }));
1372
+ }
1373
+
1374
+ function formatIssuePath(path) {
1375
+ let out = "";
1376
+ for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1377
+ return out;
1378
+ }
1379
+
1380
+ class PersistedDocShapeError extends WorkflowError {
1381
+ documentId;
1382
+ documentType;
1383
+ issues;
1384
+ constructor(args) {
1385
+ super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
1386
+ this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
1387
+ this.issues = args.issues;
1388
+ }
1389
+ }
1390
+
1391
+ function parsePersistedDoc(args) {
1392
+ const result = v__namespace.safeParse(args.schema, args.doc);
1393
+ if (!result.success) throw new PersistedDocShapeError({
1394
+ documentId: documentIdOf(args.doc),
1395
+ documentType: args.docType,
1396
+ issues: issuesFromValibot(result.issues)
1397
+ });
1398
+ return result.output;
1399
+ }
1400
+
1401
+ function documentIdOf(doc) {
1402
+ if (typeof doc == "object" && doc !== null) {
1403
+ const id = doc._id;
1404
+ if (typeof id == "string") return id;
1405
+ }
1406
+ return "(unknown id)";
1407
+ }
1408
+
1409
+ const ACTOR_KINDS = [ "person", "agent", "system" ];
590
1410
 
591
1411
  function releaseDocId(releaseName) {
592
1412
  return `_.releases.${releaseName}`;
@@ -633,29 +1453,28 @@ class FieldValueShapeError extends WorkflowError {
633
1453
  }
634
1454
  }
635
1455
 
636
- const GdrShape = v__namespace.looseObject({
637
- id: v__namespace.pipe(v__namespace.string(), v__namespace.check(s => isGdrUri(s), "must be a GDR URI")),
638
- type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
639
- }), ReleaseRefShape = v__namespace.pipe(v__namespace.looseObject({
640
- id: v__namespace.pipe(v__namespace.string(), v__namespace.check(s => isGdrUri(s), "must be a GDR URI")),
1456
+ const GdrUriSchema = v__namespace.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
1457
+ id: GdrUriSchema,
1458
+ type: NonEmptyString
1459
+ }), ReleaseRefShape = v__namespace.pipe(tolerantObject()({
1460
+ id: GdrUriSchema,
641
1461
  type: v__namespace.literal("system.release"),
642
- releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
643
- }), v__namespace.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = v__namespace.looseObject({
1462
+ releaseName: NonEmptyString
1463
+ }), v__namespace.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = tolerantObject()({
644
1464
  kind: v__namespace.picklist(ACTOR_KINDS),
645
- id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
646
- roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
647
- onBehalfOf: v__namespace.optional(v__namespace.string())
648
- }), AssigneeShape = v__namespace.union([ v__namespace.looseObject({
1465
+ id: NonEmptyString,
1466
+ roles: v__namespace.exactOptional(v__namespace.array(v__namespace.string())),
1467
+ onBehalfOf: v__namespace.exactOptional(v__namespace.string())
1468
+ }), AssigneeShape = v__namespace.union([ tolerantObject()({
649
1469
  type: v__namespace.literal("user"),
650
- id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
651
- }), v__namespace.looseObject({
1470
+ id: NonEmptyString
1471
+ }), tolerantObject()({
652
1472
  type: v__namespace.literal("role"),
653
- role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
654
- }) ]), NullableString = v__namespace.union([ v__namespace.null(), v__namespace.string() ]), NullableNumber = v__namespace.union([ v__namespace.null(), v__namespace.number() ]), NullableBoolean = v__namespace.union([ v__namespace.null(), v__namespace.boolean() ]), NullableDateTime = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.check(s => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")) ]), NullableDate = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, valueSchemas = {
1473
+ role: NonEmptyString
1474
+ }) ]), NullableString = v__namespace.union([ v__namespace.null(), v__namespace.string() ]), NullableNumber = v__namespace.union([ v__namespace.null(), v__namespace.number() ]), NullableBoolean = v__namespace.union([ v__namespace.null(), v__namespace.boolean() ]), NullableDateTime = v__namespace.union([ v__namespace.null(), IsoTimestamp ]), NullableDate = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, fieldValueSchemas = {
655
1475
  "doc.ref": v__namespace.union([ v__namespace.null(), GdrShape ]),
656
1476
  "doc.refs": v__namespace.array(GdrShape),
657
1477
  "release.ref": v__namespace.union([ v__namespace.null(), ReleaseRefShape ]),
658
- query: v__namespace.any(),
659
1478
  string: NullableString,
660
1479
  text: NullableString,
661
1480
  number: NullableNumber,
@@ -666,6 +1485,9 @@ const GdrShape = v__namespace.looseObject({
666
1485
  actor: v__namespace.union([ v__namespace.null(), ActorShape ]),
667
1486
  assignee: v__namespace.union([ v__namespace.null(), AssigneeShape ]),
668
1487
  assignees: v__namespace.array(AssigneeShape)
1488
+ }, valueSchemas = {
1489
+ ...fieldValueSchemas,
1490
+ query: v__namespace.any()
669
1491
  };
670
1492
 
671
1493
  function shapeValueSchema(shape, leaf) {
@@ -752,7 +1574,7 @@ function isBareSeedId(id) {
752
1574
 
753
1575
  const AuthoringRefId = v__namespace.pipe(v__namespace.string(), v__namespace.check(isAuthoringRefId, "must be a bare document id, a GDR URI, or a portable `@<alias>:<id>` reference")), AuthoringGdrShape = v__namespace.looseObject({
754
1576
  id: AuthoringRefId,
755
- type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1577
+ type: NonEmptyString
756
1578
  }), seedValueSchemas = {
757
1579
  ...valueSchemas,
758
1580
  "doc.ref": v__namespace.union([ v__namespace.null(), AuthoringGdrShape ]),
@@ -760,7 +1582,7 @@ const AuthoringRefId = v__namespace.pipe(v__namespace.string(), v__namespace.che
760
1582
  "release.ref": v__namespace.union([ v__namespace.null(), v__namespace.looseObject({
761
1583
  id: AuthoringRefId,
762
1584
  type: v__namespace.literal("system.release"),
763
- releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1585
+ releaseName: NonEmptyString
764
1586
  }) ])
765
1587
  };
766
1588
 
@@ -803,13 +1625,7 @@ function formatIssues(issues) {
803
1625
  });
804
1626
  }
805
1627
 
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_]*$/;
1628
+ const NonEmpty = NonEmptyString, PositiveInt = v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
813
1629
 
814
1630
  function groqIdentifier(referencedAs) {
815
1631
  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 +1724,7 @@ const StoredFieldOpSchema = v__namespace.variant("type", [ ...opSchemas(StoredFi
908
1724
  type: v__namespace.literal("status.set"),
909
1725
  activity: NonEmpty,
910
1726
  status: picklist(ACTIVITY_STATUSES)
911
- }) ]), StoredTransitionOpSchema = StoredFieldOpSchema, AuditOpSchema = v__namespace.strictObject({
1727
+ }) ]), AuditOpSchema = v__namespace.strictObject({
912
1728
  type: v__namespace.literal("audit"),
913
1729
  target: AuthoringFieldRefSchema,
914
1730
  value: ValueExprSchema,
@@ -1036,6 +1852,15 @@ const TodoListFieldSchema = pinned()(v__namespace.strictObject(listSugarFields("
1036
1852
  bindings: v__namespace.optional(v__namespace.record(v__namespace.string(), ConditionSchema)),
1037
1853
  input: v__namespace.optional(v__namespace.record(v__namespace.string(), v__namespace.unknown())),
1038
1854
  outputs: v__namespace.optional(v__namespace.array(FieldShapeSchema))
1855
+ }), DefinitionRefSchema = v__namespace.strictObject({
1856
+ name: NonEmpty,
1857
+ version: v__namespace.optional(v__namespace.union([ PositiveInt, v__namespace.literal("latest") ]))
1858
+ }), SubworkflowsSchema = v__namespace.strictObject({
1859
+ forEach: NonEmpty,
1860
+ definition: DefinitionRefSchema,
1861
+ with: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1862
+ context: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1863
+ onExit: v__namespace.optional(picklist([ "detach", "abort" ]))
1039
1864
  }), ActionParamSchema = v__namespace.strictObject({
1040
1865
  type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
1041
1866
  name: NonEmpty,
@@ -1050,14 +1875,19 @@ function actionFields(op, group) {
1050
1875
  title: v__namespace.optional(v__namespace.string()),
1051
1876
  description: v__namespace.optional(v__namespace.string()),
1052
1877
  group: v__namespace.optional(group),
1878
+ when: v__namespace.optional(ConditionSchema),
1053
1879
  filter: v__namespace.optional(ConditionSchema),
1054
1880
  params: v__namespace.optional(v__namespace.array(ActionParamSchema)),
1055
1881
  ops: v__namespace.optional(v__namespace.array(op)),
1056
- effects: v__namespace.optional(v__namespace.array(EffectSchema))
1882
+ effects: v__namespace.optional(v__namespace.array(EffectSchema)),
1883
+ spawn: v__namespace.optional(SubworkflowsSchema)
1057
1884
  };
1058
1885
  }
1059
1886
 
1060
- const StoredActionSchema = pinned()(v__namespace.strictObject(actionFields(StoredOpSchema, StoredGroupMembershipSchema))), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v__namespace.strictObject({
1887
+ const StoredActionSchema = pinned()(v__namespace.strictObject({
1888
+ ...actionFields(StoredOpSchema, StoredGroupMembershipSchema),
1889
+ roles: v__namespace.optional(v__namespace.array(NonEmpty))
1890
+ })), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v__namespace.strictObject({
1061
1891
  ...actionFields(AuthoringOpSchema, AuthoringGroupMembershipSchema),
1062
1892
  roles: v__namespace.optional(v__namespace.array(NonEmpty)),
1063
1893
  status: v__namespace.optional(TerminalActivityStatus)
@@ -1072,35 +1902,19 @@ const StoredActionSchema = pinned()(v__namespace.strictObject(actionFields(Store
1072
1902
  filter: v__namespace.optional(ConditionSchema),
1073
1903
  params: v__namespace.optional(v__namespace.array(ActionParamSchema)),
1074
1904
  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
- });
1905
+ })), AuthoringActionSchema = pinned()(v__namespace.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema));
1085
1906
 
1086
- function activityFields({field: field, action: action, op: op, target: target, group: group}) {
1907
+ function activityFields({field: field, action: action, target: target, group: group}) {
1087
1908
  return {
1088
1909
  name: NonEmpty,
1089
1910
  title: v__namespace.optional(v__namespace.string()),
1090
1911
  description: v__namespace.optional(v__namespace.string()),
1091
1912
  groups: v__namespace.optional(v__namespace.array(GroupSchema)),
1092
1913
  group: v__namespace.optional(group),
1093
- kind: v__namespace.optional(picklist(ACTIVITY_KINDS)),
1094
1914
  target: v__namespace.optional(target),
1095
- activation: v__namespace.optional(picklist([ "auto", "manual" ])),
1096
1915
  filter: v__namespace.optional(ConditionSchema),
1097
1916
  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
1917
  actions: v__namespace.optional(v__namespace.array(action)),
1103
- subworkflows: v__namespace.optional(SubworkflowsSchema),
1104
1918
  fields: v__namespace.optional(v__namespace.array(field))
1105
1919
  };
1106
1920
  }
@@ -1108,30 +1922,26 @@ function activityFields({field: field, action: action, op: op, target: target, g
1108
1922
  const StoredActivitySchema = pinned()(v__namespace.strictObject(activityFields({
1109
1923
  field: FieldEntrySchema,
1110
1924
  action: StoredActionSchema,
1111
- op: StoredOpSchema,
1112
1925
  target: StoredManualTargetSchema,
1113
1926
  group: StoredGroupMembershipSchema
1114
1927
  }))), AuthoringActivitySchema = pinned()(v__namespace.strictObject(activityFields({
1115
1928
  field: AuthoringFieldEntrySchema,
1116
1929
  action: AuthoringActionSchema,
1117
- op: AuthoringOpSchema,
1118
1930
  target: AuthoringManualTargetSchema,
1119
1931
  group: AuthoringGroupMembershipSchema
1120
1932
  })));
1121
1933
 
1122
- function transitionFields(op, filter) {
1934
+ function transitionFields(when) {
1123
1935
  return {
1124
1936
  name: NonEmpty,
1125
1937
  title: v__namespace.optional(v__namespace.string()),
1126
1938
  description: v__namespace.optional(v__namespace.string()),
1127
1939
  to: NonEmpty,
1128
- filter: filter,
1129
- ops: v__namespace.optional(v__namespace.array(op)),
1130
- effects: v__namespace.optional(v__namespace.array(EffectSchema))
1940
+ when: when
1131
1941
  };
1132
1942
  }
1133
1943
 
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({
1944
+ 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
1945
  type: v__namespace.literal("self")
1136
1946
  }), v__namespace.strictObject({
1137
1947
  type: v__namespace.literal("now")
@@ -1193,16 +2003,26 @@ const StoredStageSchema = pinned()(v__namespace.strictObject(stageFields({
1193
2003
  transition: AuthoringTransitionSchema,
1194
2004
  guard: AuthoringGuardSchema,
1195
2005
  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" ];
2006
+ }))), 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" ];
2007
+
2008
+ function startFields(kind) {
2009
+ return {
2010
+ kind: kind,
2011
+ filter: v__namespace.optional(ConditionSchema),
2012
+ allowed: v__namespace.optional(ConditionSchema)
2013
+ };
2014
+ }
1197
2015
 
1198
- function workflowFields(field, stage) {
2016
+ const StoredStartSchema = pinned()(v__namespace.strictObject(startFields(picklist(START_KINDS)))), AuthoringStartSchema = pinned()(v__namespace.strictObject(startFields(v__namespace.optional(picklist(START_KINDS)))));
2017
+
2018
+ function workflowFields({field: field, stage: stage, start: start}) {
1199
2019
  return {
1200
2020
  name: NonEmpty,
1201
2021
  title: NonEmpty,
1202
2022
  description: v__namespace.optional(v__namespace.string()),
1203
2023
  groups: v__namespace.optional(v__namespace.array(GroupSchema)),
1204
2024
  lifecycle: v__namespace.optional(picklist(WORKFLOW_LIFECYCLES)),
1205
- applicableWhen: v__namespace.optional(ConditionSchema),
2025
+ start: v__namespace.optional(start),
1206
2026
  initialStage: NonEmpty,
1207
2027
  fields: v__namespace.optional(v__namespace.array(field)),
1208
2028
  stages: v__namespace.pipe(v__namespace.array(stage), v__namespace.minLength(1, "must declare at least one stage")),
@@ -1211,7 +2031,11 @@ function workflowFields(field, stage) {
1211
2031
  };
1212
2032
  }
1213
2033
 
1214
- const WorkflowDefinitionSchema = pinned()(v__namespace.strictObject(workflowFields(FieldEntrySchema, StoredStageSchema)));
2034
+ const WorkflowDefinitionSchema = pinned()(v__namespace.strictObject(workflowFields({
2035
+ field: FieldEntrySchema,
2036
+ stage: StoredStageSchema,
2037
+ start: StoredStartSchema
2038
+ })));
1215
2039
 
1216
2040
  function parseStoredDefinition(input, label) {
1217
2041
  return parseOrThrow({
@@ -1221,28 +2045,26 @@ function parseStoredDefinition(input, label) {
1221
2045
  });
1222
2046
  }
1223
2047
 
1224
- const AuthoringWorkflowSchema = pinned()(v__namespace.strictObject(workflowFields(AuthoringFieldEntrySchema, AuthoringStageSchema))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
2048
+ const AuthoringWorkflowSchema = pinned()(v__namespace.strictObject(workflowFields({
2049
+ field: AuthoringFieldEntrySchema,
2050
+ stage: AuthoringStageSchema,
2051
+ start: AuthoringStartSchema
2052
+ }))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
1225
2053
 
1226
2054
  function isStartableDefinition(definition) {
1227
2055
  return definition.lifecycle !== "child";
1228
2056
  }
1229
2057
 
1230
- function formatValidationError(label, issues) {
1231
- const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1232
- return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
2058
+ function startKindOf(definition) {
2059
+ return definition.start?.kind ?? "interactive";
1233
2060
  }
1234
2061
 
1235
- function issuesFromValibot(issues) {
1236
- return issues.map(issue => ({
1237
- path: issue.path ? issue.path.map(item => item.key) : [],
1238
- message: issue.message
1239
- }));
2062
+ function isSubjectEntry(entry) {
2063
+ return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
1240
2064
  }
1241
2065
 
1242
- function formatIssuePath(path) {
1243
- let out = "";
1244
- for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1245
- return out;
2066
+ function isInputSourced(entry) {
2067
+ return entry.initialValue?.type === "input";
1246
2068
  }
1247
2069
 
1248
2070
  function parseOrThrow({schema: schema, input: input, label: label}) {
@@ -1273,6 +2095,13 @@ function checkDuplicates({names: names, what: what, issues: issues}) {
1273
2095
  return seen;
1274
2096
  }
1275
2097
 
2098
+ function checkDefinitionName(def, issues) {
2099
+ LAKE_ID_SEGMENT_RE.test(def.name) || issues.push({
2100
+ path: [ "name" ],
2101
+ message: `definition name "${def.name}" does not match the definition-name grammar ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS}) — the name is interpolated into every deployed document id (\`<tag>.<name>.v<version>\`), which the engine keeps to the same segment grammar as the tag`
2102
+ });
2103
+ }
2104
+
1276
2105
  function checkStages(def, issues) {
1277
2106
  const stageNames = checkDuplicates({
1278
2107
  names: def.stages.map((stage, i) => ({
@@ -1329,12 +2158,6 @@ function checkActivities({def: def, i: i, activityNames: activityNames, issues:
1329
2158
  }
1330
2159
 
1331
2160
  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
2161
  for (const [a, action] of (activity.actions ?? []).entries()) checkStatusSetOps({
1339
2162
  ops: action.ops,
1340
2163
  activityNames: activityNames,
@@ -1382,25 +2205,11 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
1382
2205
 
1383
2206
  function effectNameSites(def) {
1384
2207
  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
- }
2208
+ 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({
2209
+ effects: action.effects,
2210
+ path: [ "stages", i, "activities", j, "actions", a, "effects" ],
2211
+ sites: sites
2212
+ });
1404
2213
  return sites;
1405
2214
  }
1406
2215
 
@@ -1429,6 +2238,10 @@ function checkGuardNames(def, issues) {
1429
2238
  what: "guard name (the guard lake _id derives from it — unique per definition)",
1430
2239
  issues: issues
1431
2240
  });
2241
+ for (const {name: name, path: path} of sites) LAKE_ID_SEGMENT_RE.test(name) || issues.push({
2242
+ path: path,
2243
+ message: `guard name "${name}" does not match the guard-name grammar ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS}) — the guard's lake _id derives from it at stage entry, so an invalid name would wedge the entering commit with an opaque lake error instead of failing here`
2244
+ });
1432
2245
  }
1433
2246
 
1434
2247
  function fieldScopes(def) {
@@ -1487,19 +2300,23 @@ function checkPredicates(def, issues) {
1487
2300
  path: [ "predicates", name ],
1488
2301
  message: `predicate "${name}" shadows the built-in $${name} — pick another name`
1489
2302
  });
1490
- for (const [name, groq] of Object.entries(def.predicates ?? {})) checkPredicateBody({
2303
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkPredicateBody({
1491
2304
  name: name,
1492
- groq: groq,
2305
+ groq: groq2,
1493
2306
  names: names,
1494
2307
  issues: issues
1495
2308
  });
1496
2309
  }
1497
2310
 
1498
- function checkPredicateBody({name: name, groq: groq, names: names, issues: issues}) {
1499
- const reads = conditionParameterNames(groq);
2311
+ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issues}) {
2312
+ const reads = conditionParameterNames(groq2);
1500
2313
  for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
1501
2314
  path: [ "predicates", name ],
1502
- message: `predicate "${name}" reads $${caller} — predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
2315
+ message: `predicate "${name}" reads $${caller} — predicates pre-evaluate caller-free; compose caller gates at the condition site instead`
2316
+ });
2317
+ reads.has("row") && issues.push({
2318
+ path: [ "predicates", name ],
2319
+ message: `predicate "${name}" reads $row — ${ROW_BINDING_CLAUSE}; predicates pre-evaluate once per instance, so the read is GROQ null and the predicate can never hold`
1503
2320
  });
1504
2321
  for (const other of names) other === name || !reads.has(other) || issues.push({
1505
2322
  path: [ "predicates", name ],
@@ -1507,32 +2324,43 @@ function checkPredicateBody({name: name, groq: groq, names: names, issues: issue
1507
2324
  });
1508
2325
  }
1509
2326
 
1510
- function entryNames(entries) {
1511
- return new Set((entries ?? []).map(entry => entry.name));
2327
+ function entryMap(entries) {
2328
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
1512
2329
  }
1513
2330
 
1514
- function checkUnboundCallerVars(def, issues) {
2331
+ function checkUnboundConditionVars(def, issues) {
1515
2332
  for (const site of conditionSites(def)) {
1516
2333
  const reads = conditionParameterNames(site.groq);
1517
- for (const name of unboundCallerVars(site.policy)) reads.has(name) && issues.push({
2334
+ for (const name of unboundVarsAt(site)) reads.has(name) && issues.push({
1518
2335
  path: site.path,
1519
- message: callerVarMessage(site, name)
2336
+ message: name === "row" ? rowVarMessage(site) : callerVarMessage(site, name)
1520
2337
  });
1521
2338
  }
1522
2339
  }
1523
2340
 
2341
+ function unboundVarsAt(site) {
2342
+ const callerVars = unboundCallerVars(site.policy);
2343
+ return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
2344
+ }
2345
+
1524
2346
  function unboundCallerVars(policy) {
1525
- return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : [ "can" ];
2347
+ return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
2348
+ }
2349
+
2350
+ const ROW_BINDING_CLAUSE = "$row (the discovered row in a spawn projection; the stored row under test in a where-op) is bound only while a spawn `with` projection or a where-op `where` evaluates";
2351
+
2352
+ function rowVarMessage(site) {
2353
+ return `${site.label} reads $row — ${ROW_BINDING_CLAUSE}; this site never binds it, so the read is GROQ null and the condition can never resolve. Move the per-row read into \`spawn.with\` or a where-op, or gate on instance state instead`;
1526
2354
  }
1527
2355
 
1528
2356
  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`;
2357
+ 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
2358
  }
1531
2359
 
1532
2360
  function conditionSites(def) {
1533
2361
  const sites = [], workflowLayer = {
1534
2362
  scope: "workflow",
1535
- names: entryNames(def.fields)
2363
+ entries: entryMap(def.fields)
1536
2364
  };
1537
2365
  collectEditableSites({
1538
2366
  entries: def.fields,
@@ -1553,35 +2381,24 @@ function conditionSites(def) {
1553
2381
  function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
1554
2382
  const stageLayer = {
1555
2383
  scope: "stage",
1556
- names: entryNames(stage.fields)
2384
+ entries: entryMap(stage.fields)
1557
2385
  }, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
1558
2386
  scope: "activity",
1559
- names: entryNames(activity.fields)
2387
+ entries: entryMap(activity.fields)
1560
2388
  })) ];
1561
2389
  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`,
2390
+ groq: t.when,
2391
+ path: [ "stages", i, "transitions", k, "when" ],
2392
+ label: `transition "${t.name}" when`,
1565
2393
  policy: "cascade",
1566
2394
  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
2395
  });
1580
2396
  collectEditableSites({
1581
2397
  entries: stage.fields,
1582
2398
  path: [ "stages", i, "fields" ],
1583
2399
  labelPrefix: `stage "${stage.name}"`,
1584
2400
  fields: editableWindow,
2401
+ unionWindow: !0,
1585
2402
  sites: sites
1586
2403
  });
1587
2404
  for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
@@ -1589,70 +2406,64 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
1589
2406
  path: [ "stages", i, "editable", name ],
1590
2407
  label: `stage "${stage.name}" editable override "${name}"`,
1591
2408
  policy: "caller-bound",
1592
- fields: editableWindow
2409
+ fields: editableWindow,
2410
+ unionWindow: !0
1593
2411
  });
1594
2412
  for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
1595
2413
  activity: activity,
1596
2414
  path: [ "stages", i, "activities", j ],
1597
2415
  stageFields: stageFields2,
2416
+ workflowLayer: workflowLayer,
1598
2417
  sites: sites
1599
2418
  });
1600
2419
  }
1601
2420
 
1602
- function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
2421
+ function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, unionWindow: unionWindow, sites: sites}) {
1603
2422
  for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
1604
2423
  groq: entry.editable,
1605
2424
  path: [ ...path, n, "editable" ],
1606
2425
  label: `${labelPrefix} field "${entry.name}" editable`,
1607
2426
  policy: "caller-bound",
1608
- fields: fields
2427
+ fields: fields,
2428
+ ...unionWindow === !0 ? {
2429
+ unionWindow: !0
2430
+ } : {}
1609
2431
  });
1610
2432
  }
1611
2433
 
1612
- function collectOpWhereSites({ops: ops, path: path, label: label, fields: fields, sites: sites}) {
2434
+ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
1613
2435
  for (const [o, op] of (ops ?? []).entries()) op.where !== void 0 && sites.push({
1614
2436
  groq: op.where,
1615
2437
  path: [ ...path, o, "where" ],
1616
2438
  label: `${label} ${op.type} where`,
2439
+ ...policy !== void 0 ? {
2440
+ policy: policy
2441
+ } : {},
2442
+ bindsRow: !0,
1617
2443
  fields: fields
1618
2444
  });
1619
2445
  }
1620
2446
 
1621
- function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
2447
+ function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, workflowLayer: workflowLayer, sites: sites}) {
1622
2448
  const fields = [ {
1623
2449
  scope: "activity",
1624
- names: entryNames(activity.fields)
2450
+ entries: entryMap(activity.fields)
1625
2451
  }, ...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,
2452
+ activity.filter !== void 0 && sites.push({
2453
+ groq: activity.filter,
2454
+ path: [ ...path, "filter" ],
2455
+ label: `activity "${activity.name}".filter`,
2456
+ policy: "cascade",
2457
+ fields: fields
2458
+ });
2459
+ for (const [name, groq2] of Object.entries(activity.requirements ?? {})) sites.push({
2460
+ groq: groq2,
1638
2461
  path: [ ...path, "requirements", name ],
1639
2462
  label: `activity "${activity.name}" requirement "${name}"`,
1640
2463
  policy: "caller-bound",
1641
2464
  fields: fields
1642
2465
  });
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({
2466
+ collectEditableSites({
1656
2467
  entries: activity.fields,
1657
2468
  path: [ ...path, "fields" ],
1658
2469
  labelPrefix: `activity "${activity.name}"`,
@@ -1662,60 +2473,84 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
1662
2473
  activity: activity,
1663
2474
  path: path,
1664
2475
  fields: fields,
1665
- sites: sites
1666
- }), collectSubworkflowSites({
1667
- activity: activity,
1668
- path: path,
1669
- fields: fields,
2476
+ workflowLayer: workflowLayer,
1670
2477
  sites: sites
1671
2478
  });
1672
2479
  }
1673
2480
 
1674
- 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
- });
2481
+ function collectActionConditionSites({activity: activity, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2482
+ for (const [a, action] of (activity.actions ?? []).entries()) {
2483
+ const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
2484
+ action.when !== void 0 && sites.push({
2485
+ groq: action.when,
2486
+ path: [ ...actionPath, "when" ],
2487
+ label: `action "${action.name}" when`,
2488
+ policy: "cascade",
2489
+ fields: fields
2490
+ }), action.filter !== void 0 && sites.push({
2491
+ groq: action.filter,
2492
+ path: [ ...actionPath, "filter" ],
2493
+ label: `action "${action.name}" filter`,
2494
+ policy: cascadeFired ? "cascade" : "caller-bound",
2495
+ fields: fields
2496
+ });
2497
+ const payloadPolicy = cascadeFired ? "triggered-payload" : void 0;
2498
+ collectBindingSites({
2499
+ effects: action.effects,
2500
+ path: [ ...actionPath, "effects" ],
2501
+ label: `action "${action.name}"`,
2502
+ policy: payloadPolicy,
2503
+ fields: fields,
2504
+ sites: sites
2505
+ }), collectOpWhereSites({
2506
+ ops: action.ops,
2507
+ path: [ ...actionPath, "ops" ],
2508
+ label: `action "${action.name}"`,
2509
+ policy: payloadPolicy,
2510
+ fields: fields,
2511
+ sites: sites
2512
+ }), collectSpawnSites({
2513
+ action: action,
2514
+ path: actionPath,
2515
+ fields: fields,
2516
+ workflowLayer: workflowLayer,
2517
+ sites: sites
2518
+ });
2519
+ }
1694
2520
  }
1695
2521
 
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" ];
2522
+ function collectSpawnSites({action: action, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2523
+ const spawn = action.spawn;
2524
+ if (spawn === void 0) return;
2525
+ const spawnPath = [ ...path, "spawn" ];
1700
2526
  sites.push({
1701
- groq: sub.forEach,
1702
- path: [ ...subPath, "forEach" ],
1703
- label: `activity "${activity.name}".subworkflows.forEach`,
1704
- fields: fields
1705
- });
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}"`,
2527
+ groq: spawn.forEach,
2528
+ path: [ ...spawnPath, "forEach" ],
2529
+ label: `action "${action.name}".spawn.forEach`,
2530
+ policy: "cascade",
2531
+ fields: [ workflowLayer ],
2532
+ windowNote: "spawn.forEach evaluates against workflow-scope $fields only stage/activity fields are never bound at discovery time"
2533
+ });
2534
+ for (const [group, record, bindsRow] of [ [ "with", spawn.with, !0 ], [ "context", spawn.context, !1 ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2535
+ groq: groq2,
2536
+ path: [ ...spawnPath, group, key ],
2537
+ label: `action "${action.name}".spawn.${group} "${key}"`,
2538
+ policy: "triggered-payload",
2539
+ ...bindsRow ? {
2540
+ bindsRow: !0
2541
+ } : {},
1710
2542
  fields: fields
1711
2543
  });
1712
2544
  }
1713
2545
 
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,
2546
+ function collectBindingSites({effects: effects, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
2547
+ for (const [e, effect] of (effects ?? []).entries()) for (const [key, groq2] of Object.entries(effect.bindings ?? {})) sites.push({
2548
+ groq: groq2,
1717
2549
  path: [ ...path, e, "bindings", key ],
1718
2550
  label: `${label} effect "${effect.name}" binding "${key}"`,
2551
+ ...policy !== void 0 ? {
2552
+ policy: policy
2553
+ } : {},
1719
2554
  fields: fields
1720
2555
  });
1721
2556
  }
@@ -1727,34 +2562,102 @@ function checkAssigneesEntries(def, issues) {
1727
2562
  });
1728
2563
  }
1729
2564
 
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
- };
2565
+ function checkActivityTerminalPaths(def, issues) {
2566
+ for (const [i, stage] of def.stages.entries()) {
2567
+ const resolvable = terminallyResolvableActivities(stage);
2568
+ for (const [j, activity] of (stage.activities ?? []).entries()) resolvable.has(activity.name) || issues.push({
2569
+ path: [ "stages", i, "activities", j ],
2570
+ 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`
2571
+ });
2572
+ }
1737
2573
  }
1738
2574
 
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
- };
2575
+ function terminallyResolvableActivities(stage) {
2576
+ 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));
2577
+ return new Set(terminalSets.map(op => op.activity));
2578
+ }
1746
2579
 
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}`
2580
+ function checkTerminalStageActivities(def, issues) {
2581
+ for (const [i, stage] of def.stages.entries()) (stage.transitions ?? []).length > 0 || (stage.activities ?? []).length === 0 || issues.push({
2582
+ path: [ "stages", i, "activities" ],
2583
+ 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`
2584
+ });
2585
+ }
2586
+
2587
+ function storedRolesIssue(action) {
2588
+ if (action.roles !== void 0) {
2589
+ 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`;
2590
+ 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`;
2591
+ }
2592
+ }
2593
+
2594
+ function checkStoredRolesPlacement(def, issues) {
2595
+ for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) {
2596
+ const message = storedRolesIssue(action);
2597
+ message !== void 0 && issues.push({
2598
+ path: [ "stages", i, "activities", j, "actions", a, "roles" ],
2599
+ message: message
2600
+ });
2601
+ }
2602
+ }
2603
+
2604
+ function checkTriggeredActionParams(def, issues) {
2605
+ 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({
2606
+ path: [ "stages", i, "activities", j, "actions", a, "params" ],
2607
+ 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`
2608
+ });
2609
+ }
2610
+
2611
+ function checkStart(def, issues) {
2612
+ if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2613
+ path: [ "start" ],
2614
+ 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'`"
2615
+ }), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
2616
+ path: [ "fields", n, "required" ],
2617
+ 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`
2618
+ });
2619
+ }
2620
+
2621
+ function checkStartFilterReads(def, issues) {
2622
+ const filter = def.start?.filter;
2623
+ filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
2624
+ path: [ "start", "filter" ],
2625
+ message: "start.filter reads $fields, but the filter is browse-time-pure — a start surface evaluates it per document, before any inputs exist, so $fields cannot be bound. Move the input-dependent rule to start.allowed, the start-time permission predicate that binds $fields and is enforced by startInstance"
2626
+ }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2627
+ path: [ "start", "filter" ],
2628
+ message: "start.filter reads the candidate document (its root), but the definition declares no required subject entry (a required doc.ref / doc.refs input) — a read surface would never have a document to bind as root, so every root read is GROQ null and the filter silently misevaluates. Declare a required subject entry, or gate on the dataset instead"
2629
+ }));
2630
+ }
2631
+
2632
+ function checkStartAllowedReads(def, issues) {
2633
+ const allowed = def.start?.allowed;
2634
+ if (allowed === void 0) return;
2635
+ const bindable = new Map((def.fields ?? []).filter(isInputSourced).map(entry => [ entry.name, entry ])), declared = new Set((def.fields ?? []).map(entry => entry.name)), nullVerdict = `so the read is GROQ null and null semantics decide the verdict (refuse-all or vacuously-allow, by the expression's shape), never the caller's real input. Bindable (input) fields: ${knownList(bindable.keys())}`;
2636
+ for (const read of conditionFieldReads(allowed)) {
2637
+ const entry = bindable.get(read.name);
2638
+ if (entry === void 0) {
2639
+ issues.push({
2640
+ path: [ "start", "allowed" ],
2641
+ message: declared.has(read.name) ? `start.allowed reads $fields.${read.name}, but "${read.name}" is not an \`input\`-sourced entry — $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after the start gate), ${nullVerdict}` : `start.allowed reads $fields.${read.name}, but no workflow-scope field entry named "${read.name}" is declared — $fields binds only the caller's input entries, ${nullVerdict}`
2642
+ });
2643
+ continue;
2644
+ }
2645
+ pushFieldReadPathIssue({
2646
+ where: "start.allowed",
2647
+ read: {
2648
+ field: read.name,
2649
+ path: read.path
2650
+ },
2651
+ target: entry,
2652
+ path: [ "start", "allowed" ],
2653
+ issues: issues,
2654
+ nodes: START_ALLOWED_VALUE_NODES
1756
2655
  });
1757
2656
  }
2657
+ readsRootDocument(allowed) && issues.push({
2658
+ path: [ "start", "allowed" ],
2659
+ message: "start.allowed reads the candidate document as its root, but no root ever binds in the start-allowed context — startInstance holds inputs, not a loaded document, so the read is GROQ null and null semantics decide the verdict, never the workflow's real state. Read the subject as $fields.<entry> (its GDR URI is $fields.<entry>.id); a per-document visibility rule belongs in start.filter"
2660
+ });
1758
2661
  }
1759
2662
 
1760
2663
  function checkGroups(def, issues) {
@@ -1864,39 +2767,80 @@ function checkGroupMembership({group: group, reachable: reachable, path: path, l
1864
2767
  }
1865
2768
 
1866
2769
  function checkConditionFieldReads(def, issues) {
1867
- const everywhere = allDeclaredFieldNames(def);
1868
- for (const site of conditionSites(def)) for (const name of conditionFieldReadNames(site.groq)) fieldVisibleAtSite({
2770
+ const everywhere = allDeclaredFieldEntries(def);
2771
+ for (const site of conditionSites(def)) checkSiteFieldReads({
1869
2772
  site: site,
1870
- name: name,
1871
- everywhere: everywhere
1872
- }) || issues.push({
1873
- path: site.path,
1874
- message: undeclaredFieldReadMessage(site, name)
2773
+ everywhere: everywhere,
2774
+ issues: issues
1875
2775
  });
1876
- for (const [name, groq] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq)) everywhere.has(read) || issues.push({
1877
- path: [ "predicates", name ],
1878
- message: noFieldAnywhereMessage(`predicate "${name}"`, read)
2776
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkSiteFieldReads({
2777
+ site: {
2778
+ groq: groq2,
2779
+ path: [ "predicates", name ],
2780
+ label: `predicate "${name}"`,
2781
+ fields: "context-dependent"
2782
+ },
2783
+ everywhere: everywhere,
2784
+ issues: issues
1879
2785
  });
1880
2786
  }
1881
2787
 
1882
- function allDeclaredFieldNames(def) {
1883
- const names = /* @__PURE__ */ new Set;
1884
- for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) names.add(entry.name);
1885
- return names;
2788
+ function checkSiteFieldReads({site: site, everywhere: everywhere, issues: issues}) {
2789
+ for (const read of conditionFieldReads(site.groq)) {
2790
+ const targets = readTargets({
2791
+ site: site,
2792
+ read: read,
2793
+ everywhere: everywhere
2794
+ });
2795
+ if (targets === "undeclared") {
2796
+ issues.push({
2797
+ path: site.path,
2798
+ message: undeclaredFieldReadMessage(site, read.name)
2799
+ });
2800
+ continue;
2801
+ }
2802
+ const path = read.path;
2803
+ path !== void 0 && (targets.some(target => fieldValuePathIssue({
2804
+ target: target,
2805
+ path: path
2806
+ }) === void 0) || pushFieldReadPathIssue({
2807
+ where: site.label,
2808
+ read: {
2809
+ field: read.name,
2810
+ path: path
2811
+ },
2812
+ target: targets[0],
2813
+ path: site.path,
2814
+ issues: issues
2815
+ }));
2816
+ }
1886
2817
  }
1887
2818
 
1888
- function fieldVisibleAtSite({site: site, name: name, everywhere: everywhere}) {
1889
- return site.fields === "context-dependent" ? everywhere.has(name) : site.fields.some(layer => layer.names.has(name));
2819
+ function readTargets({site: site, read: read, everywhere: everywhere}) {
2820
+ if (site.fields === "context-dependent") return everywhere.get(read.name) ?? "undeclared";
2821
+ const layers = site.fields.filter(layer => layer.entries.has(read.name));
2822
+ if (layers.length === 0) return "undeclared";
2823
+ const entries = layers.map(layer => layer.entries.get(read.name));
2824
+ return site.unionWindow === !0 ? entries : [ entries[0] ];
2825
+ }
2826
+
2827
+ function allDeclaredFieldEntries(def) {
2828
+ const declared = /* @__PURE__ */ new Map;
2829
+ for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) {
2830
+ const list = declared.get(entry.name);
2831
+ list === void 0 ? declared.set(entry.name, [ entry ]) : list.push(entry);
2832
+ }
2833
+ return declared;
1890
2834
  }
1891
2835
 
1892
2836
  function noFieldAnywhereMessage(label, name) {
1893
- return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently fails closed`;
2837
+ return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently misevaluates`;
1894
2838
  }
1895
2839
 
1896
2840
  function undeclaredFieldReadMessage(site, name) {
1897
2841
  if (site.fields === "context-dependent") return noFieldAnywhereMessage(site.label, name);
1898
- const visible = site.fields.flatMap(layer => [ ...layer.names ].map(n => `${layer.scope}:${n}`));
1899
- return `${site.label} reads $fields.${name}, but no field entry named "${name}" is lexically visible here — the read evaluates to GROQ null, so the condition silently fails closed. Visible: ${visible.join(", ") || "(none)"}`;
2842
+ const visible = site.fields.flatMap(layer => [ ...layer.entries.keys() ].map(n => `${layer.scope}:${n}`)), window = site.windowNote === void 0 ? `no field entry named "${name}" is lexically visible here` : `"${name}" is not in this site's window (${site.windowNote})`;
2843
+ return `${site.label} reads $fields.${name}, but ${window} — the read evaluates to GROQ null, so the condition silently misevaluates. Visible: ${visible.join(", ") || "(none)"}`;
1900
2844
  }
1901
2845
 
1902
2846
  function checkFieldReadSeeds(def, issues) {
@@ -1977,38 +2921,29 @@ function seedEarlierSiblingTarget(args) {
1977
2921
  });
1978
2922
  }
1979
2923
 
2924
+ function opSites(def) {
2925
+ const sites = [];
2926
+ 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({
2927
+ ops: action.ops,
2928
+ path: [ "stages", i, "activities", j, "actions", a, "ops" ],
2929
+ label: `action "${action.name}"`,
2930
+ stage: stage,
2931
+ activity: activity
2932
+ });
2933
+ return sites;
2934
+ }
2935
+
1980
2936
  function checkFieldReadOpValues(def, issues) {
1981
2937
  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
- }
2938
+ for (const site of opSites(def)) checkOpsFieldReads({
2939
+ workflowEntries: workflowEntries,
2940
+ stageEntries: site.stage.fields ?? [],
2941
+ issues: issues,
2942
+ ops: site.ops,
2943
+ path: site.path,
2944
+ label: site.label,
2945
+ activityEntries: entryMap(site.activity.fields)
2946
+ });
2012
2947
  }
2013
2948
 
2014
2949
  function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
@@ -2027,7 +2962,7 @@ function fieldReadsIn(value, path) {
2027
2962
  } ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
2028
2963
  }
2029
2964
 
2030
- function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityNames: activityNames, issues: issues}) {
2965
+ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
2031
2966
  const hosts = [ {
2032
2967
  scope: "stage",
2033
2968
  entries: stageEntries
@@ -2042,7 +2977,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2042
2977
  read: read,
2043
2978
  where: where,
2044
2979
  hosts: hosts,
2045
- activityNames: activityNames
2980
+ activityEntries: activityEntries
2046
2981
  })
2047
2982
  });
2048
2983
  return;
@@ -2056,11 +2991,54 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2056
2991
  });
2057
2992
  }
2058
2993
 
2059
- function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityNames: activityNames}) {
2060
- const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityNames?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
2994
+ function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityEntries: activityEntries}) {
2995
+ const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityEntries?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
2061
2996
  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
2997
  }
2063
2998
 
2999
+ function checkUpdateWhereOps(def, issues) {
3000
+ const workflow = def.fields ?? [];
3001
+ for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
3002
+ if (op.type !== "field.updateWhere") continue;
3003
+ const scopes = {
3004
+ workflow: workflow,
3005
+ stage: site.stage.fields ?? [],
3006
+ activity: site.activity.fields ?? []
3007
+ };
3008
+ checkUpdateWhereTargetKind({
3009
+ op: op,
3010
+ path: [ ...site.path, o ],
3011
+ label: site.label,
3012
+ scopes: scopes,
3013
+ issues: issues
3014
+ }), checkUpdateWhereMergeKeys({
3015
+ op: op,
3016
+ path: [ ...site.path, o ],
3017
+ label: site.label,
3018
+ issues: issues
3019
+ });
3020
+ }
3021
+ }
3022
+
3023
+ function checkUpdateWhereTargetKind({op: op, path: path, label: label, scopes: scopes, issues: issues}) {
3024
+ const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
3025
+ target === void 0 || target.type === "array" || issues.push({
3026
+ path: [ ...path, "target" ],
3027
+ 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)}`
3028
+ });
3029
+ }
3030
+
3031
+ function rowOpsHint(type) {
3032
+ return type === "doc.refs" || type === "assignees" ? `; to add or drop ${type} rows use field.append / field.removeWhere` : "";
3033
+ }
3034
+
3035
+ function checkUpdateWhereMergeKeys({op: op, path: path, label: label, issues: issues}) {
3036
+ if (op.value.type === "object") for (const key of Object.keys(op.value.fields)) key !== "_key" && key !== "_type" || issues.push({
3037
+ path: [ ...path, "value", "fields", key ],
3038
+ 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`
3039
+ });
3040
+ }
3041
+
2064
3042
  function checkGuardFieldReads(def, issues) {
2065
3043
  const reads = {
2066
3044
  workflowEntries: def.fields ?? [],
@@ -2122,11 +3100,14 @@ function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, pa
2122
3100
  });
2123
3101
  }
2124
3102
 
2125
- function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
3103
+ function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues, nodes: nodes}) {
2126
3104
  if (read.path === void 0) return;
2127
3105
  const pathIssue = fieldValuePathIssue({
2128
3106
  target: target,
2129
- path: read.path
3107
+ path: read.path,
3108
+ ...nodes ? {
3109
+ nodes: nodes
3110
+ } : {}
2130
3111
  });
2131
3112
  pathIssue !== void 0 && issues.push({
2132
3113
  path: path,
@@ -2197,22 +3178,26 @@ const SCALAR = {
2197
3178
  kind: "rows",
2198
3179
  of: shape.of ?? []
2199
3180
  })
3181
+ }, START_ALLOWED_VALUE_NODES = {
3182
+ ...VALUE_NODES,
3183
+ "doc.ref": () => GDR_VALUE
2200
3184
  };
2201
3185
 
2202
- function valueNodeFor(shape) {
2203
- return Object.hasOwn(VALUE_NODES, shape.type) ? VALUE_NODES[shape.type](shape) : void 0;
3186
+ function valueNodeFor(shape, nodes) {
3187
+ return Object.hasOwn(nodes, shape.type) ? nodes[shape.type](shape) : void 0;
2204
3188
  }
2205
3189
 
2206
3190
  const INDEX_SEGMENT = /^\d+$/;
2207
3191
 
2208
- function stepIntoValueNode(node, segment) {
3192
+ function stepIntoValueNode(args) {
3193
+ const {node: node, segment: segment, nodes: nodes} = args;
2209
3194
  switch (node.kind) {
2210
3195
  case "scalar":
2211
3196
  return "the value is a scalar with no sub-paths";
2212
3197
 
2213
3198
  case "object":
2214
3199
  {
2215
- const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
3200
+ const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub, nodes);
2216
3201
  return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
2217
3202
  }
2218
3203
 
@@ -2233,18 +3218,24 @@ function stepIntoValueNode(node, segment) {
2233
3218
  }
2234
3219
  }
2235
3220
 
2236
- function fieldValuePathIssue({target: target, path: path}) {
2237
- let node = valueNodeFor(target);
3221
+ function fieldValuePathIssue({target: target, path: path, nodes: nodes = VALUE_NODES}) {
3222
+ let node = valueNodeFor(target, nodes);
2238
3223
  if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
2239
3224
  for (const segment of path.split(".")) {
2240
- const next = stepIntoValueNode(node, segment);
3225
+ const next = stepIntoValueNode({
3226
+ node: node,
3227
+ segment: segment,
3228
+ nodes: nodes
3229
+ });
2241
3230
  if (typeof next == "string") return `at segment "${segment}": ${next}`;
2242
3231
  node = next;
2243
3232
  }
2244
3233
  }
2245
3234
 
2246
3235
  function checkWorkflowInvariants(def) {
2247
- const issues = [], stageNames = checkStages(def, issues);
3236
+ const issues = [];
3237
+ checkDefinitionName(def, issues);
3238
+ const stageNames = checkStages(def, issues);
2248
3239
  return checkInitialStage({
2249
3240
  def: def,
2250
3241
  stageNames: stageNames,
@@ -2258,16 +3249,22 @@ function checkWorkflowInvariants(def) {
2258
3249
  stageNames: stageNames,
2259
3250
  issues: issues
2260
3251
  }), 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),
3252
+ checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
3253
+ checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3254
+ checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
3255
+ checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
3256
+ checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
2264
3257
  checkGroups(def, issues), issues;
2265
3258
  }
2266
3259
 
2267
3260
  exports.ACTIVITY_KINDS = ACTIVITY_KINDS;
2268
3261
 
3262
+ exports.ACTIVITY_STATUSES = ACTIVITY_STATUSES;
3263
+
2269
3264
  exports.ACTOR_KINDS = ACTOR_KINDS;
2270
3265
 
3266
+ exports.ActorShape = ActorShape;
3267
+
2271
3268
  exports.AuthoringActionSchema = AuthoringActionSchema;
2272
3269
 
2273
3270
  exports.AuthoringActivitySchema = AuthoringActivitySchema;
@@ -2290,6 +3287,8 @@ exports.CONDITION_VARS = CONDITION_VARS;
2290
3287
 
2291
3288
  exports.ContractViolationError = ContractViolationError;
2292
3289
 
3290
+ exports.DEFAULT_TRANSITION_WHEN = DEFAULT_TRANSITION_WHEN;
3291
+
2293
3292
  exports.DOCUMENT_VALUE_PERMISSIONS = DOCUMENT_VALUE_PERMISSIONS;
2294
3293
 
2295
3294
  exports.DRIVER_KINDS = DRIVER_KINDS;
@@ -2300,12 +3299,18 @@ exports.DefinitionNotFoundError = DefinitionNotFoundError;
2300
3299
 
2301
3300
  exports.EFFECTS_READ = EFFECTS_READ;
2302
3301
 
3302
+ exports.EXECUTOR_CLASSIFICATIONS = EXECUTOR_CLASSIFICATIONS;
3303
+
2303
3304
  exports.EffectNotFoundError = EffectNotFoundError;
2304
3305
 
2305
3306
  exports.EffectSchema = EffectSchema;
2306
3307
 
2307
3308
  exports.FIELD_READ = FIELD_READ;
2308
3309
 
3310
+ exports.FIELD_SCOPES = FIELD_SCOPES;
3311
+
3312
+ exports.FIELD_VALUE_KINDS = FIELD_VALUE_KINDS;
3313
+
2309
3314
  exports.FILTER_SCOPE_VARS = FILTER_SCOPE_VARS;
2310
3315
 
2311
3316
  exports.FieldValueShapeError = FieldValueShapeError;
@@ -2314,14 +3319,28 @@ exports.GROUP_KINDS = GROUP_KINDS;
2314
3319
 
2315
3320
  exports.GUARD_PREDICATE_VARS = GUARD_PREDICATE_VARS;
2316
3321
 
3322
+ exports.GdrShape = GdrShape;
3323
+
2317
3324
  exports.GroupSchema = GroupSchema;
2318
3325
 
2319
3326
  exports.InstanceNotFoundError = InstanceNotFoundError;
2320
3327
 
3328
+ exports.IsoTimestamp = IsoTimestamp;
3329
+
3330
+ exports.MUTATION_GUARD_ACTIONS = MUTATION_GUARD_ACTIONS;
3331
+
3332
+ exports.NonEmptyString = NonEmptyString;
3333
+
3334
+ exports.PersistedDocShapeError = PersistedDocShapeError;
3335
+
2321
3336
  exports.RESERVED_CONDITION_VARS = RESERVED_CONDITION_VARS;
2322
3337
 
2323
3338
  exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
2324
3339
 
3340
+ exports.START_ALLOWED_VARS = START_ALLOWED_VARS;
3341
+
3342
+ exports.START_FILTER_VARS = START_FILTER_VARS;
3343
+
2325
3344
  exports.StoredFieldOpSchema = StoredFieldOpSchema;
2326
3345
 
2327
3346
  exports.WORKFLOW_DEFINITION_TYPE = WORKFLOW_DEFINITION_TYPE;
@@ -2342,6 +3361,8 @@ exports.clientConfigFromResource = clientConfigFromResource;
2342
3361
 
2343
3362
  exports.conditionEffectReads = conditionEffectReads;
2344
3363
 
3364
+ exports.conditionFieldReadNames = conditionFieldReadNames;
3365
+
2345
3366
  exports.conditionParameterNames = conditionParameterNames;
2346
3367
 
2347
3368
  exports.conditionSyntaxIssues = conditionSyntaxIssues;
@@ -2350,6 +3371,14 @@ exports.datasetResourceParts = datasetResourceParts;
2350
3371
 
2351
3372
  exports.definitionDocId = definitionDocId;
2352
3373
 
3374
+ exports.deriveActivityKind = deriveActivityKind;
3375
+
3376
+ exports.deriveExecutorClassification = deriveExecutorClassification;
3377
+
3378
+ exports.desugarWorkflow = desugarWorkflow;
3379
+
3380
+ exports.driverKind = driverKind;
3381
+
2353
3382
  exports.errorMessage = errorMessage;
2354
3383
 
2355
3384
  exports.evaluateCondition = evaluateCondition;
@@ -2358,10 +3387,10 @@ exports.evaluateConditionOutcome = evaluateConditionOutcome;
2358
3387
 
2359
3388
  exports.evaluatePredicates = evaluatePredicates;
2360
3389
 
2361
- exports.expandRequiredRoles = expandRequiredRoles;
2362
-
2363
3390
  exports.extractDocumentId = extractDocumentId;
2364
3391
 
3392
+ exports.fieldValueSchemas = fieldValueSchemas;
3393
+
2365
3394
  exports.formatIssuePath = formatIssuePath;
2366
3395
 
2367
3396
  exports.formatIssues = formatIssues;
@@ -2376,20 +3405,30 @@ exports.gdrResourcePrefix = gdrResourcePrefix;
2376
3405
 
2377
3406
  exports.gdrUri = gdrUri;
2378
3407
 
3408
+ exports.groq = groq;
3409
+
2379
3410
  exports.groupMembershipNames = groupMembershipNames;
2380
3411
 
2381
3412
  exports.isBareSeedId = isBareSeedId;
2382
3413
 
3414
+ exports.isCascadeFired = isCascadeFired;
3415
+
2383
3416
  exports.isGdr = isGdr;
2384
3417
 
2385
3418
  exports.isGdrUri = isGdrUri;
2386
3419
 
2387
3420
  exports.isGuardReadExpr = isGuardReadExpr;
2388
3421
 
3422
+ exports.isInputSourced = isInputSourced;
3423
+
2389
3424
  exports.isNotesEntry = isNotesEntry;
2390
3425
 
3426
+ exports.isParseableInstant = isParseableInstant;
3427
+
2391
3428
  exports.isStartableDefinition = isStartableDefinition;
2392
3429
 
3430
+ exports.isSubjectEntry = isSubjectEntry;
3431
+
2393
3432
  exports.isTerminalActivityStatus = isTerminalActivityStatus;
2394
3433
 
2395
3434
  exports.isTodoListEntry = isTodoListEntry;
@@ -2400,17 +3439,17 @@ exports.isUnevaluable = isUnevaluable;
2400
3439
 
2401
3440
  exports.labelFor = labelFor;
2402
3441
 
2403
- exports.normalizeRoleAliases = normalizeRoleAliases;
2404
-
2405
3442
  exports.parseGdr = parseGdr;
2406
3443
 
2407
3444
  exports.parseOrThrow = parseOrThrow;
2408
3445
 
3446
+ exports.parsePersistedDoc = parsePersistedDoc;
3447
+
2409
3448
  exports.parseResourceGdr = parseResourceGdr;
2410
3449
 
2411
3450
  exports.parseStoredDefinition = parseStoredDefinition;
2412
3451
 
2413
- exports.printGuardRead = printGuardRead;
3452
+ exports.readsRootDocument = readsRootDocument;
2414
3453
 
2415
3454
  exports.refCanvas = refCanvas;
2416
3455
 
@@ -2442,14 +3481,22 @@ exports.runGroq = runGroq;
2442
3481
 
2443
3482
  exports.sameResource = sameResource;
2444
3483
 
3484
+ exports.schemaTreeShape = schemaTreeShape;
3485
+
2445
3486
  exports.selfGdr = selfGdr;
2446
3487
 
3488
+ exports.startKindOf = startKindOf;
3489
+
2447
3490
  exports.tagScopeFilter = tagScopeFilter;
2448
3491
 
2449
3492
  exports.toBareId = toBareId;
2450
3493
 
2451
3494
  exports.toPhysicalGdr = toPhysicalGdr;
2452
3495
 
3496
+ exports.tolerantEntries = tolerantEntries;
3497
+
3498
+ exports.tolerantObject = tolerantObject;
3499
+
2453
3500
  exports.tryParseGdr = tryParseGdr;
2454
3501
 
2455
3502
  exports.validateFieldAppendItem = validateFieldAppendItem;