@sanity/workflow-engine 0.32.0 → 0.33.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.
@@ -1,6 +1,6 @@
1
1
  import * as v from "valibot";
2
2
 
3
- import { conditionOutcome, runGroq as runGroq$1, evaluateConditionOutcome as evaluateConditionOutcome$1, _exhaustiveOptions } from "@sanity/groq-condition-describe";
3
+ import { _exhaustiveOptions, conditionOutcome, runGroq as runGroq$1, evaluateConditionOutcome as evaluateConditionOutcome$1 } from "@sanity/groq-condition-describe";
4
4
 
5
5
  import { parse } from "groq-js";
6
6
 
@@ -370,107 +370,191 @@ function tagScopeFilter() {
370
370
  return "tag == $tag";
371
371
  }
372
372
 
373
- const NonEmptyString$1 = v.pipe(v.string(), v.nonEmpty("must not be empty"));
373
+ const NonEmptyString$1 = v.pipe(v.string(), v.minLength(1, "must be a non-empty string"));
374
374
 
375
- function asPredicate(validate) {
376
- return value => {
377
- try {
378
- return validate(value), !0;
379
- } catch {
380
- return !1;
381
- }
382
- };
375
+ function isParseableInstant(value) {
376
+ return typeof value == "string" && !Number.isNaN(Date.parse(value));
383
377
  }
384
378
 
385
- const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts);
379
+ const IsoTimestamp = v.pipe(v.string(), v.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
386
380
 
387
- function lakeSegment(label) {
388
- return v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, `invalid ${label} — ${LAKE_ID_SEGMENT_GLOSS}`));
381
+ function tolerantObject() {
382
+ return (entries, ..._exact) => v.looseObject(entries);
389
383
  }
390
384
 
391
- const WorkflowResourceSchema = v.variant("type", [ v.object({
392
- type: v.literal("dataset"),
393
- id: v.pipe(NonEmptyString$1, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
394
- }), v.object({
395
- type: v.literal("canvas"),
396
- id: NonEmptyString$1
397
- }), v.object({
398
- type: v.literal("media-library"),
399
- id: NonEmptyString$1
400
- }), v.object({
401
- type: v.literal("dashboard"),
402
- id: NonEmptyString$1
403
- }) ]), ResourceBindingSchema = v.object({
404
- name: v.pipe(NonEmptyString$1, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
405
- resource: WorkflowResourceSchema
406
- }), DefinitionSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v.object({
407
- name: lakeSegment("name"),
408
- expectedMinReaderModel: v.optional(v.custom(() => !0), void 0),
409
- tag: lakeSegment("tag"),
410
- workflowResource: WorkflowResourceSchema,
411
- resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => duplicateHandleMessage(bindings) === void 0, issue => duplicateHandleMessage(issue.input) ?? "duplicate resource handle name"))),
412
- definitions: v.pipe(v.array(DefinitionSchema), v.minLength(1, "a deployment needs at least one definition"))
413
- });
385
+ function tolerantEntries() {
386
+ return (entries, ..._exact) => entries;
387
+ }
414
388
 
415
- function firstDuplicatePair(items, keyOf) {
416
- const seen = /* @__PURE__ */ new Map;
417
- for (const item of items) {
418
- const key = keyOf(item), earlier = seen.get(key);
419
- if (earlier !== void 0) return [ earlier, item ];
420
- seen.set(key, item);
421
- }
389
+ function schemaTreeShape(schema) {
390
+ return walkSchemaShape(schema, /* @__PURE__ */ new Set);
422
391
  }
423
392
 
424
- function duplicateHandleMessage(bindings) {
425
- const pair = firstDuplicatePair(bindings, binding => binding.name);
426
- if (pair !== void 0) return `duplicate resource handle name "${pair[1].name}" — each binding name must be unique within a deployment`;
393
+ function walkSchemaShape(node, path) {
394
+ if (typeof node != "object" || node === null) return "unknown";
395
+ if (path.has(node)) return "(circular)";
396
+ path.add(node);
397
+ try {
398
+ const schema = node;
399
+ return containerShape(schema, path) ?? leafShape(schema);
400
+ } finally {
401
+ path.delete(node);
402
+ }
427
403
  }
428
404
 
429
- function duplicateNameMessage(deployments) {
430
- const pair = firstDuplicatePair(deployments, deployment => deployment.name);
431
- if (pair !== void 0) return `duplicate deployment name "${pair[1].name}" each deployment must use a unique name`;
405
+ const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
406
+ union: schema.options.map(option => walkSchemaShape(option, path))
407
+ }), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
408
+ strict_object: objectWalker,
409
+ loose_object: objectWalker,
410
+ object: objectWalker,
411
+ array: (schema, path) => ({
412
+ array: walkSchemaShape(schema.item, path)
413
+ }),
414
+ record: (schema, path) => ({
415
+ record: walkSchemaShape(schema.value, path)
416
+ }),
417
+ union: unionWalker,
418
+ variant: unionWalker,
419
+ lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
420
+ exact_optional: unwrapWalker,
421
+ optional: unwrapWalker,
422
+ nullable: unwrapWalker
423
+ };
424
+
425
+ function containerShape(schema, path) {
426
+ return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
432
427
  }
433
428
 
434
- function partitionKey(deployment) {
435
- return `${resourceGdr(deployment.workflowResource)} ${deployment.tag}`;
429
+ function objectEntriesShape(entries, path) {
430
+ const out = {};
431
+ for (const [key, entry] of Object.entries(entries)) {
432
+ const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
433
+ out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
434
+ }
435
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
436
436
  }
437
437
 
438
- function partitionCollisionMessage(deployments) {
439
- const pair = firstDuplicatePair(deployments, partitionKey);
440
- if (pair === void 0) return;
441
- const [first, second] = pair;
442
- return `deployments "${first.name}" and "${second.name}" share workflow resource + tag "${second.tag}" — both would write into the same partition; change one deployment’s tag or resource`;
438
+ function isOptionalEntry(entry) {
439
+ if (typeof entry != "object" || entry === null) return !1;
440
+ const kind = entry.type;
441
+ return kind === "exact_optional" || kind === "optional";
443
442
  }
444
443
 
445
- const TelemetryLoggerSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v.object({
446
- deployments: v.pipe(v.array(DeploymentSchema), v.minLength(1, "a config needs at least one deployment"), v.check(deployments => duplicateNameMessage(deployments) === void 0, issue => duplicateNameMessage(issue.input) ?? "duplicate deployment name"), v.check(deployments => partitionCollisionMessage(deployments) === void 0, issue => partitionCollisionMessage(issue.input) ?? "duplicate deployment partition")),
447
- telemetry: v.optional(TelemetryLoggerSchema)
448
- });
444
+ const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
449
445
 
450
- function resourceAliasesToMap(resourceAliases) {
451
- return Object.fromEntries((resourceAliases ?? []).map(binding => [ binding.name, binding.resource ]));
446
+ function leafShape(schema) {
447
+ if (schema.type === "picklist") return schema.options.join(" | ");
448
+ if (schema.type === "literal") return `literal ${String(schema.literal)}`;
449
+ if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
450
+ 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`);
451
+ return String(schema.type);
452
452
  }
453
453
 
454
- const FIELD_READ = /^\$fields\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
454
+ function formatValidationError(label, issues) {
455
+ const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
456
+ return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
457
+ }
455
458
 
456
- function isGuardReadExpr(expr) {
457
- return expr === "$self" || expr === "$now" || FIELD_READ.test(expr) || EFFECTS_READ.test(expr);
459
+ function issuesFromValibot(issues) {
460
+ return issues.map(issue => ({
461
+ path: issue.path ? issue.path.map(item => item.key) : [],
462
+ message: issue.message
463
+ }));
458
464
  }
459
465
 
460
- function printGuardRead(read) {
461
- switch (read.type) {
462
- case "self":
463
- return "$self";
466
+ function formatIssuePath(path) {
467
+ let out = "";
468
+ for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
469
+ return out;
470
+ }
464
471
 
465
- case "now":
466
- return "$now";
472
+ class PersistedDocShapeError extends WorkflowError {
473
+ documentId;
474
+ documentType;
475
+ issues;
476
+ constructor(args) {
477
+ super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
478
+ this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
479
+ this.issues = args.issues;
480
+ }
481
+ }
467
482
 
468
- case "fieldRead":
469
- return read.path === void 0 ? `$fields.${read.field}` : `$fields.${read.field}.${read.path}`;
483
+ function parsePersistedDoc(args) {
484
+ const result = v.safeParse(args.schema, args.doc);
485
+ if (!result.success) throw new PersistedDocShapeError({
486
+ documentId: documentIdOf(args.doc),
487
+ documentType: args.docType,
488
+ issues: issuesFromValibot(result.issues)
489
+ });
490
+ return result.output;
491
+ }
470
492
 
471
- case "effectsRead":
472
- return read.path === void 0 ? `$effects['${read.effect}']` : `$effects['${read.effect}'].${read.path}`;
493
+ function documentIdOf(doc) {
494
+ if (typeof doc == "object" && doc !== null) {
495
+ const id = doc._id;
496
+ if (typeof id == "string") return id;
473
497
  }
498
+ return "(unknown id)";
499
+ }
500
+
501
+ const ACTOR_KINDS = [ "person", "agent", "system" ];
502
+
503
+ function releaseDocId(releaseName) {
504
+ return `_.releases.${releaseName}`;
505
+ }
506
+
507
+ function releaseRef({res: res, releaseName: releaseName}) {
508
+ if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
509
+ return {
510
+ id: gdrFromResource(res, releaseDocId(releaseName)),
511
+ type: "system.release",
512
+ releaseName: releaseName
513
+ };
514
+ }
515
+
516
+ function isAlwaysArrayFieldKind(kind) {
517
+ return kind === "doc.refs" || kind === "assignee" || kind === "assignees" || kind === "array";
518
+ }
519
+
520
+ function isSingleDocRefKind(kind) {
521
+ return kind === "doc.ref" || kind === "subject";
522
+ }
523
+
524
+ function refKindAcceptsTypes(kind) {
525
+ return isSingleDocRefKind(kind) || kind === "doc.refs";
526
+ }
527
+
528
+ function assignmentKindAcceptsRoles(kind) {
529
+ return kind === "assignee" || kind === "assignees";
530
+ }
531
+
532
+ function isSingleDocRefEntry(entry) {
533
+ return isSingleDocRefKind(entry._type);
534
+ }
535
+
536
+ function isAssignmentFieldEntry(entry) {
537
+ return assignmentKindAcceptsRoles(entry._type);
538
+ }
539
+
540
+ function isTodoListItem(row) {
541
+ if (typeof row != "object" || row === null) return !1;
542
+ const candidate = row, status = candidate.status;
543
+ return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
544
+ }
545
+
546
+ function declaredRowColumns(entry) {
547
+ if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
548
+ }
549
+
550
+ function isTodoListEntry(entry) {
551
+ const columns = declaredRowColumns(entry);
552
+ return columns !== void 0 && columns.has("label") && columns.has("status");
553
+ }
554
+
555
+ function isNotesEntry(entry) {
556
+ const columns = declaredRowColumns(entry);
557
+ return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
474
558
  }
475
559
 
476
560
  const UNIVERSAL_ROLE_ALIAS_KEY = "$all";
@@ -501,2151 +585,2157 @@ function actorFulfillsRole({actorRoles: actorRoles, required: required, aliases:
501
585
  return actorRoles.some(role => accepted.has(role));
502
586
  }
503
587
 
504
- function groq(strings, ...values) {
505
- return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
588
+ function normalizeAssignmentMembers(value) {
589
+ return value === null ? [] : Array.isArray(value) ? value : [ value ];
506
590
  }
507
591
 
508
- function serializeGroqValue(value) {
509
- const serialized = JSON.stringify(value);
510
- if (serialized === void 0) throw new Error(`groq tag cannot serialize ${typeof value} — interpolate JSON-representable values only`);
511
- return serialized;
592
+ function assignmentMembers(entries) {
593
+ return entries.flatMap(entry => isAssignmentFieldEntry(entry) ? normalizeAssignmentMembers(entry.value) : []);
512
594
  }
513
595
 
514
- function desugarWorkflow(authoring) {
515
- const issues = [];
516
- checkReservedRoleAliasKeys(authoring.roleAliases, issues);
517
- const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
518
- issues: issues,
519
- roleAliases: roleAliases
520
- }, workflowFields2 = desugarFieldEntries({
521
- entries: authoring.fields,
522
- path: [ "fields" ],
523
- ctx: ctx
524
- }), workflowLayer = layerOf(workflowFields2), stages = authoring.stages.map((stage, i) => {
525
- const path = [ "stages", i ], stageFields2 = desugarFieldEntries({
526
- entries: stage.fields,
527
- path: [ ...path, "fields" ],
528
- ctx: ctx
529
- }), stageEnv = {
530
- layers: [ {
531
- scope: "stage",
532
- entries: layerOf(stageFields2)
533
- }, {
534
- scope: "workflow",
535
- entries: workflowLayer
536
- } ]
537
- }, activities = (stage.activities ?? []).map((activity, j) => desugarActivity({
538
- activity: activity,
539
- path: [ ...path, "activities", j ],
540
- stageEnv: stageEnv,
541
- ctx: ctx
542
- })), transitions = (stage.transitions ?? []).map(transition => desugarTransition({
543
- transition: transition
544
- })), editable = desugarStageEditable({
545
- overrides: stage.editable,
546
- inScope: editableFieldNames({
547
- workflowFields: workflowFields2,
548
- stageFields: stageFields2,
549
- activities: activities
550
- }),
551
- path: [ ...path, "editable" ],
552
- ctx: ctx
553
- });
554
- return {
555
- ...stripUndefined({
556
- name: stage.name,
557
- semantics: stage.semantics,
558
- title: stage.title,
559
- description: stage.description,
560
- groups: stage.groups,
561
- guards: stage.guards?.map(desugarGuard)
562
- }),
563
- ...stageFields2 ? {
564
- fields: stageFields2
565
- } : {},
566
- ...activities.length > 0 ? {
567
- activities: activities
568
- } : {},
569
- ...transitions.length > 0 ? {
570
- transitions: transitions
571
- } : {},
572
- ...editable ? {
573
- editable: editable
574
- } : {}
575
- };
576
- });
577
- return {
578
- definition: {
579
- ...stripUndefined({
580
- name: authoring.name,
581
- semantics: authoring.semantics,
582
- title: authoring.title,
583
- description: authoring.description,
584
- groups: authoring.groups,
585
- lifecycle: authoring.lifecycle,
586
- start: desugarStart(authoring.start),
587
- initialStage: authoring.initialStage,
588
- predicates: authoring.predicates,
589
- roleAliases: roleAliases
590
- }),
591
- ...workflowFields2 ? {
592
- fields: workflowFields2
593
- } : {},
594
- stages: stages
595
- },
596
- issues: ctx.issues
597
- };
596
+ function assignmentState(members) {
597
+ return members.length === 0 ? "unrouted" : members.some(member => member.type === "user") ? "held" : "routed";
598
598
  }
599
599
 
600
- function desugarStart(start) {
601
- if (start !== void 0) return {
602
- kind: start.kind ?? "interactive",
603
- ...start.filter !== void 0 ? {
604
- filter: start.filter
605
- } : {},
606
- ...start.requirements !== void 0 ? {
607
- requirements: start.requirements
608
- } : {}
600
+ function activeAssignmentMembers(members) {
601
+ const users = members.filter(member => member.type === "user");
602
+ return users.length > 0 ? users : members;
603
+ }
604
+
605
+ function assignmentMatch(members, identity) {
606
+ const active = activeAssignmentMembers(members);
607
+ return active.some(member => member.type === "user" && member.id === identity.userId) ? "user" : active.some(member => member.type === "role" && identity.roles.includes(member.role)) ? "role" : void 0;
608
+ }
609
+
610
+ function identityMatchesAssignment(members, identity) {
611
+ return assignmentMatch(members, identity) !== void 0;
612
+ }
613
+
614
+ function assignmentStateCounts(assignments, identity) {
615
+ const counts = {
616
+ unrouted: 0,
617
+ routed: 0,
618
+ held: 0
609
619
  };
620
+ for (const members of assignments) {
621
+ const state = assignmentState(members);
622
+ state === "unrouted" ? counts.unrouted += 1 : identityMatchesAssignment(members, identity) && (counts[state] += 1);
623
+ }
624
+ return counts;
610
625
  }
611
626
 
612
- function checkReservedRoleAliasKeys(aliases, issues) {
613
- for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
614
- path: [ "roleAliases", key ],
615
- 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.`
616
- });
627
+ function openActivityAssignments(instance) {
628
+ return (findOpenStageEntry(instance)?.activities ?? []).filter(activity => activity.status === "active").map(activity => assignmentMembers(activity.fields ?? []));
617
629
  }
618
630
 
619
- const TODOLIST_OF = [ {
620
- type: "string",
621
- name: "label",
622
- title: "Label"
623
- }, {
624
- type: "string",
625
- name: "status",
626
- title: "Status"
627
- }, {
628
- type: "assignee",
629
- name: "assignee",
630
- title: "Assignee"
631
- }, {
632
- type: "date",
633
- name: "dueDate",
634
- title: "Due date"
635
- } ], NOTES_OF = [ {
636
- type: "text",
637
- name: "body",
638
- title: "Body"
639
- }, {
640
- type: "actor",
641
- name: "actor",
642
- title: "Actor"
643
- }, {
644
- type: "datetime",
645
- name: "at",
646
- title: "At"
647
- } ];
631
+ function instanceAssignmentStateCounts(instance, identity) {
632
+ return assignmentStateCounts(openActivityAssignments(instance), identity);
633
+ }
648
634
 
649
- function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
650
- return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
651
- entry: entry,
652
- path: [ ...path, i ],
653
- ctx: ctx
635
+ function actorMatchesAssignment(args) {
636
+ const actor = args.actor;
637
+ return actor === void 0 ? !1 : activeAssignmentMembers(args.members).some(member => member.type === "user" ? member.id === actor.id : actorFulfillsRole({
638
+ actorRoles: actor.roles,
639
+ required: member.role,
640
+ aliases: args.roleAliases
654
641
  }));
655
642
  }
656
643
 
657
- function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
658
- if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
659
- entry: entry,
660
- path: path,
661
- ctx: ctx
662
- });
663
- const editable = normalizeEditable({
664
- editable: entry.editable,
665
- path: [ ...path, "editable" ],
666
- ctx: ctx
667
- });
668
- return {
669
- ...stripUndefined({
670
- type: entry.type,
671
- name: entry.name,
672
- title: entry.title,
673
- description: entry.description,
674
- group: normalizeGroup(entry.group),
675
- required: entry.required,
676
- initialValue: entry.initialValue,
677
- editable: editable,
678
- options: entry.options,
679
- validation: entry.validation,
680
- types: entry.types,
681
- roles: entry.roles,
682
- fields: entry.fields,
683
- of: entry.of
684
- })
644
+ const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
645
+
646
+ function classifyPrincipalId(id) {
647
+ if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
648
+ namespace: "sentinel"
649
+ };
650
+ if (id.startsWith("g")) return {
651
+ namespace: "global",
652
+ globalId: id
653
+ };
654
+ if (id.startsWith("p-")) return {
655
+ namespace: "robot",
656
+ globalId: id
657
+ };
658
+ const embeddedGlobal = E_PREFIXED_PROJECT_ID.exec(id)?.[1];
659
+ return embeddedGlobal !== void 0 ? embeddedGlobal.startsWith("g") ? {
660
+ namespace: "project",
661
+ globalId: embeddedGlobal
662
+ } : {
663
+ namespace: "unknown"
664
+ } : id.startsWith("p") ? {
665
+ namespace: "project"
666
+ } : {
667
+ namespace: "unknown"
685
668
  };
686
669
  }
687
670
 
688
- function desugarListField({entry: entry, path: path, ctx: ctx}) {
689
- const editable = normalizeEditable({
690
- editable: entry.editable,
691
- path: [ ...path, "editable" ],
692
- ctx: ctx
693
- });
694
- return {
695
- ...stripUndefined({
696
- name: entry.name,
697
- title: entry.title,
698
- description: entry.description,
699
- group: normalizeGroup(entry.group),
700
- required: entry.required,
701
- initialValue: entry.initialValue,
702
- editable: editable
703
- }),
704
- type: "array",
705
- of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
706
- };
671
+ function directoryBridgeId(sanityUserId) {
672
+ if (typeof sanityUserId == "string") return classifyPrincipalId(sanityUserId).namespace === "global" ? sanityUserId : void 0;
707
673
  }
708
674
 
709
- function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
710
- if (editable === void 0 || editable === !0) return editable;
711
- if (Array.isArray(editable)) {
712
- const condition = rolesCondition(editable, ctx.roleAliases);
713
- if (condition === void 0) {
714
- ctx.issues.push({
715
- path: path,
716
- message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
717
- });
718
- return;
719
- }
720
- return condition;
675
+ function firstCarriedGlobalId(candidates) {
676
+ for (const candidate of candidates) {
677
+ if (candidate === void 0) continue;
678
+ const {globalId: globalId} = classifyPrincipalId(candidate);
679
+ if (globalId !== void 0) return globalId;
721
680
  }
722
- return editable;
723
681
  }
724
682
 
725
- function editableFieldNames({workflowFields: workflowFields2, stageFields: stageFields2, activities: activities}) {
726
- const names = /* @__PURE__ */ new Set, collect = entries => {
727
- for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
728
- };
729
- collect(workflowFields2), collect(stageFields2);
730
- for (const activity of activities) collect(activity.fields);
731
- return names;
683
+ function lakePrincipalId(args) {
684
+ return args.localPrincipalId ?? args.actor.id;
732
685
  }
733
686
 
734
- function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
735
- if (overrides === void 0) return;
736
- const out = {};
737
- for (const [name, value] of Object.entries(overrides)) {
738
- if (!inScope.has(name)) {
739
- ctx.issues.push({
740
- path: [ ...path, name ],
741
- 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\``
742
- });
743
- continue;
744
- }
745
- const normalized = normalizeEditable({
746
- editable: value,
747
- path: [ ...path, name ],
748
- ctx: ctx
749
- });
750
- normalized !== void 0 && (out[name] = normalized);
751
- }
752
- return Object.keys(out).length > 0 ? out : void 0;
753
- }
754
-
755
- function layerOf(entries) {
756
- return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
757
- }
758
-
759
- function normalizeGroup(group) {
760
- return group === void 0 || Array.isArray(group) ? group : [ group ];
761
- }
762
-
763
- function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
764
- const activityFields2 = desugarFieldEntries({
765
- entries: activity.fields,
766
- path: [ ...path, "fields" ],
767
- ctx: ctx
768
- }), env = {
769
- layers: [ {
770
- scope: "activity",
771
- entries: layerOf(activityFields2)
772
- }, ...stageEnv.layers ]
773
- }, actions = (activity.actions ?? []).map((action, a) => desugarAction({
774
- action: action,
775
- path: [ ...path, "actions", a ],
776
- env: env,
777
- activityName: activity.name,
778
- ctx: ctx
779
- })), target = desugarTarget({
780
- target: activity.target,
781
- env: env,
782
- path: [ ...path, "target" ],
783
- ctx: ctx
784
- });
785
- return {
786
- ...stripUndefined({
787
- name: activity.name,
788
- semantics: activity.semantics,
789
- title: activity.title,
790
- description: activity.description,
791
- groups: activity.groups,
792
- group: normalizeGroup(activity.group),
793
- filter: activity.filter,
794
- requirements: activity.requirements
795
- }),
796
- ...target ? {
797
- target: target
798
- } : {},
799
- ...activityFields2 ? {
800
- fields: activityFields2
801
- } : {},
802
- ...actions.length > 0 ? {
803
- actions: actions
804
- } : {}
805
- };
806
- }
807
-
808
- const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref" ];
809
-
810
- function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
811
- if (target === void 0 || target.type === "url") return target;
812
- const ref = typeof target.field == "string" ? {
813
- field: target.field
814
- } : target.field, field = resolveRef({
815
- ref: ref,
816
- env: env,
817
- path: [ ...path, "field" ],
818
- ctx: ctx
819
- }) ?? fallbackRef(ref), entry = entryAt(env, field);
820
- return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
821
- path: [ ...path, "field" ],
822
- message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
823
- }), {
824
- type: "field",
825
- field: field
826
- };
827
- }
828
-
829
- function reportEmptyActionRoles({action: action, path: path, ctx: ctx}) {
830
- action.roles === void 0 || action.roles.length > 0 || ctx.issues.push({
831
- path: [ ...path, "roles" ],
832
- message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
833
- });
834
- }
835
-
836
- function desugarActionOps(args) {
837
- const {action: action, path: path, env: env, activityName: activityName, ctx: ctx} = args, ops = desugarOps({
838
- ops: action.ops,
839
- path: [ ...path, "ops" ],
840
- env: env,
841
- firingActivity: activityName,
842
- ctx: ctx
843
- }) ?? [];
844
- return action.status !== void 0 && ops.push({
845
- type: "status.set",
846
- activity: activityName,
847
- status: action.status
848
- }), ops;
849
- }
850
-
851
- function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
852
- reportEmptyActionRoles({
853
- action: action,
854
- path: path,
855
- ctx: ctx
856
- });
857
- const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarActionOps({
858
- action: action,
859
- path: path,
860
- env: env,
861
- activityName: activityName,
862
- ctx: ctx
863
- });
864
- return {
865
- ...stripUndefined({
866
- name: action.name,
867
- semantics: action.semantics,
868
- title: action.title,
869
- description: action.description,
870
- group: normalizeGroup(action.group),
871
- when: action.when,
872
- params: action.params,
873
- effects: action.effects,
874
- spawn: action.spawn
875
- }),
876
- ...cascadeFired && action.roles !== void 0 && action.roles.length > 0 ? {
877
- roles: action.roles
878
- } : {},
879
- ...filter ? {
880
- filter: filter
881
- } : {},
882
- ...ops.length > 0 ? {
883
- ops: ops
884
- } : {}
885
- };
886
- }
887
-
888
- const DEFAULT_TRANSITION_WHEN = "$allActivitiesDone";
889
-
890
- function desugarTransition({transition: transition}) {
891
- return {
892
- ...stripUndefined({
893
- name: transition.name,
894
- title: transition.title,
895
- description: transition.description,
896
- to: transition.to
897
- }),
898
- when: transition.when ?? DEFAULT_TRANSITION_WHEN
899
- };
900
- }
901
-
902
- function desugarGuard(guard) {
903
- const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
904
- return {
905
- ...rest,
906
- match: {
907
- ...matchRest,
908
- ...idRefs !== void 0 ? {
909
- idRefs: idRefs.map(printGuardRead)
910
- } : {}
911
- },
912
- ...metadata !== void 0 ? {
913
- metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
914
- } : {}
915
- };
916
- }
917
-
918
- function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
919
- if (ops) return ops.map((op, i) => desugarOp({
920
- op: op,
921
- path: [ ...path, i ],
922
- env: env,
923
- firingActivity: firingActivity,
924
- ctx: ctx
925
- }));
926
- }
927
-
928
- function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
929
- if (op.type === "status.set") return {
930
- type: "status.set",
931
- activity: op.activity ?? firingActivity,
932
- status: op.status
933
- };
934
- if (op.type === "audit") return desugarAuditOp({
935
- op: op,
936
- path: path,
937
- env: env,
938
- ctx: ctx
939
- });
940
- const target = resolveRef({
941
- ref: op.target,
942
- env: env,
943
- path: [ ...path, "target" ],
944
- ctx: ctx
945
- }) ?? fallbackRef(op.target);
946
- return {
947
- ...op,
948
- target: target
949
- };
950
- }
951
-
952
- const AUDIT_STAMPS = {
953
- actor: {
954
- type: "actor"
955
- },
956
- at: {
957
- type: "now"
958
- }
959
- };
960
-
961
- function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
962
- const target = resolveRef({
963
- ref: op.target,
964
- env: env,
965
- path: [ ...path, "target" ],
966
- ctx: ctx
967
- }) ?? fallbackRef(op.target);
968
- if (op.value.type !== "object") return ctx.issues.push({
969
- path: [ ...path, "value" ],
970
- message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
971
- }), {
972
- type: "field.append",
973
- target: target,
974
- value: op.value
975
- };
976
- const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
977
- ...op.value.fields
978
- };
979
- for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
980
- if (stamp in fields) {
981
- ctx.issues.push({
982
- path: [ ...path, "value", "fields", stamp ],
983
- message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
984
- });
985
- continue;
986
- }
987
- fields[stamp] = source;
988
- }
989
- return {
990
- type: "field.append",
991
- target: target,
992
- value: {
993
- type: "object",
994
- fields: fields
995
- }
996
- };
997
- }
998
-
999
- function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
1000
- const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
1001
- for (const layer of layers) if (layer.entries.has(ref.field)) return {
1002
- scope: layer.scope,
1003
- field: ref.field
1004
- };
1005
- const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
1006
- ctx.issues.push({
1007
- path: path,
1008
- message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
1009
- });
1010
- }
1011
-
1012
- function entryAt(env, ref) {
1013
- return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
1014
- }
1015
-
1016
- function fallbackRef(ref) {
1017
- return {
1018
- scope: ref.scope ?? "workflow",
1019
- field: ref.field
1020
- };
1021
- }
1022
-
1023
- function rolesCondition(roles, aliases) {
1024
- if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
1025
- }
1026
-
1027
- function stripUndefined(obj) {
1028
- const out = {};
1029
- for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
1030
- return out;
1031
- }
1032
-
1033
- function isUnevaluable(result) {
1034
- return result == null;
1035
- }
1036
-
1037
- async function evaluateConditionOutcome(args) {
1038
- const {condition: condition, snapshot: snapshot, params: params} = args;
1039
- return evaluateConditionOutcome$1({
1040
- condition: condition,
1041
- params: params,
1042
- dataset: snapshot.docs
1043
- });
1044
- }
1045
-
1046
- async function evaluateCondition(args) {
1047
- return await evaluateConditionOutcome(args) === "satisfied";
1048
- }
1049
-
1050
- async function evaluatePredicates(args) {
1051
- const out = {};
1052
- for (const [name, groq2] of Object.entries(args.predicates ?? {})) {
1053
- const result = await runGroq({
1054
- groq: groq2,
1055
- params: args.params,
1056
- snapshot: args.snapshot
1057
- }), outcome = conditionOutcome(result);
1058
- out[name] = outcome === "unevaluable" ? null : outcome === "satisfied";
1059
- }
1060
- return out;
1061
- }
1062
-
1063
- async function runGroq({groq: groq2, params: params, snapshot: snapshot}) {
1064
- return runGroq$1({
1065
- groq: groq2,
1066
- params: params,
1067
- dataset: snapshot.docs
1068
- });
1069
- }
1070
-
1071
- function conditionSyntaxIssues(groq2, boundVars) {
1072
- const issues = [];
1073
- let tree;
1074
- try {
1075
- tree = parse(groq2);
1076
- } catch (err) {
1077
- issues.push(errorMessage(err));
1078
- }
1079
- 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."),
1080
- 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(", "));
1081
- return issues;
1082
- }
1083
-
1084
- function conditionParameterNames(groq2) {
1085
- const read = /* @__PURE__ */ new Set;
1086
- return walkAstNodes(tryParseGroq(groq2), node => {
1087
- node.type === "Parameter" && typeof node.name == "string" && read.add(node.name);
1088
- }), read;
1089
- }
1090
-
1091
- function conditionFieldReadNames(groq2) {
1092
- return new Set(conditionFieldReads(groq2).map(read => read.name));
1093
- }
1094
-
1095
- function conditionFieldReads(groq2) {
1096
- const reads = /* @__PURE__ */ new Map;
1097
- return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
1098
- }
1099
-
1100
- function collectFieldReads(node, reads) {
1101
- if (Array.isArray(node)) {
1102
- for (const item of node) collectFieldReads(item, reads);
1103
- return;
1104
- }
1105
- if (typeof node != "object" || node === null) return;
1106
- const chain = fieldsAttributeChain(node);
1107
- if (chain !== void 0) {
1108
- const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
1109
- reads.set(`${name}.${path ?? ""}`, {
1110
- name: name,
1111
- path: path
1112
- });
1113
- return;
1114
- }
1115
- for (const value of Object.values(node)) collectFieldReads(value, reads);
1116
- }
1117
-
1118
- function fieldsAttributeChain(node) {
1119
- const names = [];
1120
- let current = node;
1121
- for (;typeof current == "object" && current !== null; ) {
1122
- const typed = current;
1123
- if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
1124
- names.unshift(typed.name);
1125
- const base = typed.base;
1126
- if (base?.type === "Parameter" && base.name === "fields") return names;
1127
- current = base;
1128
- }
1129
- }
1130
-
1131
- function conditionEffectReads(groq2) {
1132
- const reads = [];
1133
- return walkAstNodes(tryParseGroq(groq2), node => {
1134
- if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
1135
- const base = node.base;
1136
- base?.type !== "AccessAttribute" || typeof base.name != "string" || base.base?.type === "Parameter" && base.base.name === "effects" && reads.push({
1137
- effect: base.name,
1138
- key: node.name
1139
- });
1140
- }), reads;
1141
- }
1142
-
1143
- function readsRootDocument(groq2) {
1144
- return nodeReadsRoot(tryParseGroq(groq2), 0);
1145
- }
1146
-
1147
- const SCOPED_CHILD = {
1148
- Filter: "expr",
1149
- Projection: "expr",
1150
- Map: "expr",
1151
- FlatMap: "expr",
1152
- PipeFuncCall: "args"
1153
- };
1154
-
1155
- function nodeReadsRoot(node, depth) {
1156
- if (Array.isArray(node)) return node.some(item => nodeReadsRoot(item, depth));
1157
- if (typeof node != "object" || node === null) return !1;
1158
- const typed = node;
1159
- if (depth === 0 && typed.type === "This" || depth === 0 && typed.type === "AccessAttribute" && typed.base === void 0) return !0;
1160
- if (typed.type === "Parent") {
1161
- const climb = typeof typed.n == "number" ? typed.n : 1;
1162
- return depth - climb <= 0;
1163
- }
1164
- const scopedChild = typeof typed.type == "string" ? SCOPED_CHILD[typed.type] : void 0;
1165
- 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));
1166
- }
1167
-
1168
- function tryParseGroq(groq2) {
1169
- try {
1170
- return parse(groq2);
1171
- } catch {
1172
- return;
1173
- }
687
+ function isRecord(value) {
688
+ return typeof value == "object" && value !== null && !Array.isArray(value);
1174
689
  }
1175
690
 
1176
- function walkAstNodes(node, visit) {
1177
- if (Array.isArray(node)) {
1178
- for (const item of node) walkAstNodes(item, visit);
1179
- return;
1180
- }
1181
- if (!(typeof node != "object" || node === null)) {
1182
- visit(node);
1183
- for (const value of Object.values(node)) walkAstNodes(value, visit);
691
+ class FieldValueShapeError extends WorkflowError {
692
+ entryType;
693
+ entryName;
694
+ issues;
695
+ constructor(args) {
696
+ const issueText = args.issues.join("; ");
697
+ super("field-value-shape", `Field entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`),
698
+ this.name = "FieldValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName,
699
+ this.issues = args.issues;
1184
700
  }
1185
701
  }
1186
702
 
1187
- const ACTIVITY_STATUSES = _exhaustiveOptions()([ "active", "done", "skipped", "failed" ]), TERMINAL_ACTIVITY_STATUSES = _exhaustiveOptions()([ "done", "skipped", "failed" ]);
1188
-
1189
- function isTerminalActivityStatus(status) {
1190
- return TERMINAL_ACTIVITY_STATUSES.includes(status);
1191
- }
1192
-
1193
- const SIGNAL_SEMANTICS = [ "signal.positive", "signal.caution", "signal.critical" ], DECISION_SEMANTICS = [ "decision.accept", "decision.decline" ], ACTION_SEMANTICS = [ ...DECISION_SEMANTICS, ...SIGNAL_SEMANTICS ], FIELD_SCOPES = _exhaustiveOptions()([ "workflow", "stage", "activity" ]), DOCUMENT_VALUE_PERMISSIONS = [ "create", "manage", "read", "update" ], LAKE_MUTATION_GUARD_ACTIONS = _exhaustiveOptions()([ "create", "update", "delete" ]), MUTATION_GUARD_ACTIONS = _exhaustiveOptions()([ ...LAKE_MUTATION_GUARD_ACTIONS, "publish", "unpublish" ]), GUARD_ACTIONS_REQUIRED_MESSAGE = "a guard must match at least one action", MUTATION_GUARD_ID_SPACES = [ "authored", "edit", "published" ];
1194
-
1195
- function mutationGuardActionIdSpace(action) {
1196
- return action === "update" ? "edit" : action === "publish" || action === "unpublish" ? "published" : "authored";
1197
- }
1198
-
1199
- function mutationGuardRequiresSplitEmission(actions) {
1200
- return new Set(actions.map(mutationGuardActionIdSpace)).size > 1;
1201
- }
1202
-
1203
- const ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ];
1204
-
1205
- function releaseDocId(releaseName) {
1206
- return `_.releases.${releaseName}`;
1207
- }
1208
-
1209
- function releaseRef({res: res, releaseName: releaseName}) {
1210
- if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
1211
- return {
1212
- id: gdrFromResource(res, releaseDocId(releaseName)),
1213
- type: "system.release",
1214
- releaseName: releaseName
1215
- };
1216
- }
703
+ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
704
+ id: GdrUriSchema,
705
+ type: NonEmptyString$1
706
+ }), ReleaseRefShape = v.pipe(tolerantObject()({
707
+ id: GdrUriSchema,
708
+ type: v.literal("system.release"),
709
+ releaseName: NonEmptyString$1
710
+ }), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = tolerantObject()({
711
+ kind: v.picklist(ACTOR_KINDS),
712
+ id: NonEmptyString$1,
713
+ roles: v.exactOptional(v.array(v.string())),
714
+ onBehalfOf: v.exactOptional(v.string())
715
+ }), AssigneeShape = v.union([ tolerantObject()({
716
+ type: v.literal("user"),
717
+ id: NonEmptyString$1
718
+ }), tolerantObject()({
719
+ type: v.literal("role"),
720
+ role: NonEmptyString$1
721
+ }) ]), AssigneeListShape = v.pipe(v.union([ v.null(), AssigneeShape, v.array(AssigneeShape) ]), v.transform(normalizeAssignmentMembers)), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableProgress = v.union([ v.null(), v.pipe(v.number(), v.finite("progress must be a finite number"), v.minValue(0, "progress must be at least 0"), v.maxValue(100, "progress must be at most 100")) ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "dueDate", "datetime", "dueDatetime", "dateTime" ]);
1217
722
 
1218
- function isAlwaysArrayFieldKind(kind) {
1219
- return kind === "doc.refs" || kind === "assignee" || kind === "assignees" || kind === "array";
723
+ function normalizedChoiceKind(kind) {
724
+ return kind === "dateTime" ? "datetime" : kind;
1220
725
  }
1221
726
 
1222
- function isSingleDocRefKind(kind) {
1223
- return kind === "doc.ref" || kind === "subject";
727
+ function checkChoiceList(args) {
728
+ const {entryType: entryType, options: options, validation: validation} = args;
729
+ if (options === void 0) return;
730
+ if (!CHOICE_KINDS.has(entryType)) return [ `\`options\` is not valid on "${entryType}" values` ];
731
+ const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => issuesOf(checkValueAgainst({
732
+ entryType: kind,
733
+ value: option.value,
734
+ validation: validation
735
+ }, valueSchemas))?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
736
+ for (const option of options.list) seen.has(option.value) && issues.push(`duplicate option value ${JSON.stringify(option.value)}`),
737
+ seen.add(option.value);
738
+ return issues.length === 0 ? void 0 : issues;
1224
739
  }
1225
740
 
1226
- function refKindAcceptsTypes(kind) {
1227
- return isSingleDocRefKind(kind) || kind === "doc.refs";
741
+ function choiceValueIssues(options, value) {
742
+ if (!(options === void 0 || value === null || value === void 0)) return options.list.some(option => Object.is(option.value, value)) ? void 0 : [ `value ${JSON.stringify(value)} is not declared in \`options.list\`; expected one of ${options.list.map(option => JSON.stringify(option.value)).join(", ")}` ];
1228
743
  }
1229
744
 
1230
- function assignmentKindAcceptsRoles(kind) {
1231
- return kind === "assignee" || kind === "assignees";
1232
- }
745
+ const fieldValueSchemas = {
746
+ "doc.ref": v.union([ v.null(), GdrShape ]),
747
+ "doc.refs": v.array(GdrShape),
748
+ subject: v.union([ v.null(), GdrShape ]),
749
+ "release.ref": v.union([ v.null(), ReleaseRefShape ]),
750
+ string: NullableString,
751
+ text: NullableString,
752
+ number: NullableNumber,
753
+ progress: NullableProgress,
754
+ boolean: NullableBoolean,
755
+ date: NullableDate,
756
+ dueDate: NullableDate,
757
+ datetime: NullableDateTime,
758
+ dueDatetime: NullableDateTime,
759
+ url: NullableUrl,
760
+ actor: v.union([ v.null(), ActorShape ]),
761
+ assignee: AssigneeListShape,
762
+ assignees: v.array(AssigneeShape)
763
+ }, WritePrincipalId = v.pipe(NonEmptyString$1, v.rawTransform(({dataset: dataset, addIssue: addIssue}) => {
764
+ const classified = classifyPrincipalId(dataset.value);
765
+ return classified.namespace === "global" || classified.namespace === "robot" ? dataset.value : classified.namespace === "project" && classified.globalId !== void 0 ? classified.globalId : (addIssue({
766
+ message: `principal id "${dataset.value}" is not an account-global user id. Workflow user ids are the global \`sanityUserId\` (or a robot token's id). Resolve project members through your surface's member hook, or /projects/<projectId>/users/<id> → sanityUserId.`
767
+ }), dataset.value);
768
+ })), ActorWriteShape = tolerantObject()({
769
+ kind: v.picklist(ACTOR_KINDS),
770
+ id: WritePrincipalId,
771
+ roles: v.exactOptional(v.array(v.string())),
772
+ onBehalfOf: v.exactOptional(v.string())
773
+ }), AssigneePrincipalId = v.pipe(WritePrincipalId, v.check(id => classifyPrincipalId(id).namespace !== "robot", "robot principals cannot be assignment members")), AssigneeWriteShape = v.union([ tolerantObject()({
774
+ type: v.literal("user"),
775
+ id: AssigneePrincipalId
776
+ }), tolerantObject()({
777
+ type: v.literal("role"),
778
+ role: NonEmptyString$1
779
+ }) ]), SingularAssigneeWriteShape = v.pipe(v.union([ AssigneeWriteShape, v.array(AssigneeWriteShape) ]), v.transform(normalizeAssignmentMembers), v.check(members => members.filter(member => member.type === "user").length <= 1, "must contain at most one user member")), valueSchemas = {
780
+ ...fieldValueSchemas,
781
+ actor: v.nullable(ActorWriteShape),
782
+ assignee: SingularAssigneeWriteShape,
783
+ assignees: v.array(AssigneeWriteShape),
784
+ query: v.any()
785
+ };
1233
786
 
1234
- function isSingleDocRefEntry(entry) {
1235
- return isSingleDocRefKind(entry._type);
787
+ function shapeValueSchema(args) {
788
+ const {shape: shape, leaf: leaf} = args;
789
+ if (shape.type === "object") return objectSchema({
790
+ fields: shape.fields ?? [],
791
+ leaf: leaf
792
+ });
793
+ if (shape.type === "array") return v.array(objectSchema({
794
+ fields: shape.of ?? [],
795
+ leaf: leaf
796
+ }));
797
+ const schema = leaf[shape.type] ?? v.any();
798
+ return constrainedScalarSchema({
799
+ schema: schema,
800
+ entryType: shape.type,
801
+ ...shape
802
+ });
1236
803
  }
1237
804
 
1238
- function isAssignmentFieldEntry(entry) {
1239
- return assignmentKindAcceptsRoles(entry._type);
805
+ function scalarMeasurement(entryType, value) {
806
+ if ((entryType === "number" || entryType === "progress") && typeof value == "number") return value;
807
+ if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
1240
808
  }
1241
809
 
1242
- function isTodoListItem(row) {
1243
- if (typeof row != "object" || row === null) return !1;
1244
- const candidate = row, status = candidate.status;
1245
- return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
810
+ function scalarBoundIssue(args) {
811
+ const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
812
+ return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" || entryType === "progress" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
1246
813
  }
1247
814
 
1248
- function declaredRowColumns(entry) {
1249
- if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
815
+ function scalarValidationIssues(args) {
816
+ const {entryType: entryType, validation: validation, value: value} = args;
817
+ if (validation === void 0 || value === null || value === void 0) return;
818
+ const measured = scalarMeasurement(entryType, value);
819
+ if (measured === void 0) return;
820
+ const issues = [ scalarBoundIssue({
821
+ entryType: entryType,
822
+ measured: measured,
823
+ bound: validation.min,
824
+ limit: "min"
825
+ }), scalarBoundIssue({
826
+ entryType: entryType,
827
+ measured: measured,
828
+ bound: validation.max,
829
+ limit: "max"
830
+ }) ].filter(issue => issue !== void 0);
831
+ return issues.length === 0 ? void 0 : issues;
1250
832
  }
1251
833
 
1252
- function isTodoListEntry(entry) {
1253
- const columns = declaredRowColumns(entry);
1254
- return columns !== void 0 && columns.has("label") && columns.has("status");
834
+ function constrainedScalarSchema(args) {
835
+ const {schema: schema, entryType: entryType, options: options, validation: validation} = args;
836
+ return options === void 0 && validation === void 0 ? schema : v.pipe(schema, v.check(value => choiceValueIssues(options, value) === void 0 && scalarValidationIssues({
837
+ entryType: entryType,
838
+ validation: validation,
839
+ value: value
840
+ }) === void 0, issue => [ ...choiceValueIssues(options, issue.input) ?? [], ...scalarValidationIssues({
841
+ entryType: entryType,
842
+ validation: validation,
843
+ value: issue.input
844
+ }) ?? [] ].join("; ")));
1255
845
  }
1256
846
 
1257
- function isNotesEntry(entry) {
1258
- const columns = declaredRowColumns(entry);
1259
- return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
847
+ function objectSchema(args) {
848
+ const {fields: fields, leaf: leaf} = args, entries = /* @__PURE__ */ Object.create(null);
849
+ for (const f of fields) entries[f.name] = v.optional(shapeValueSchema({
850
+ shape: f,
851
+ leaf: leaf
852
+ }));
853
+ return v.looseObject(entries);
1260
854
  }
1261
855
 
1262
- const CONDITION_VARS = [ {
1263
- name: "self",
1264
- binding: "always",
1265
- label: "this workflow instance",
1266
- description: "GDR URI of the instance document itself — `*[_id == $self][0]` reads the instance in the snapshot."
1267
- }, {
1268
- name: "fields",
1269
- binding: "always",
1270
- label: "the workflow's fields",
1271
- 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` (or `subject`) 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."
1272
- }, {
1273
- name: "parent",
1274
- binding: "always",
1275
- label: "the parent workflow",
1276
- description: "The parent instance's GDR URI, `null` on a root instance."
1277
- }, {
1278
- name: "ancestors",
1279
- binding: "always",
1280
- label: "the workflow's ancestors",
1281
- description: "GDR URIs of the ancestor chain, root first."
1282
- }, {
1283
- name: "stage",
1284
- binding: "always",
1285
- label: "the current stage",
1286
- description: "The current stage's name."
1287
- }, {
1288
- name: "now",
1289
- binding: "always",
1290
- label: "the current time",
1291
- description: "The ISO clock reading shared by every condition in one evaluation pass, so they agree on the time."
1292
- }, {
1293
- name: "context",
1294
- binding: "always",
1295
- label: "start-time context",
1296
- 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."
1297
- }, {
1298
- name: "effects",
1299
- binding: "always",
1300
- label: "automation outputs",
1301
- 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."
1302
- }, {
1303
- name: "effectStatus",
1304
- binding: "always",
1305
- label: "automation status",
1306
- description: "Effect name → `'done'` | `'failed'` of the latest completed run queued during the current stage entry; absent until the effect drains for this entry. Re-entry-safe: runs queued under a prior entry never count, so `$effectStatus['<effect>'] == 'done'` (or `defined($effectStatus['<effect>'])` for settled-either-way) waits for the fresh run."
1307
- }, {
1308
- name: "activities",
1309
- binding: "always",
1310
- label: "this stage's activities",
1311
- description: "The current stage's activity rows, statuses included."
1312
- }, {
1313
- name: "allActivitiesDone",
1314
- binding: "always",
1315
- label: "all activities in this stage are finished",
1316
- description: "Every current-stage activity is `done` or `skipped` — the default transition gate."
1317
- }, {
1318
- name: "anyActivityFailed",
1319
- binding: "always",
1320
- label: "an activity in this stage has failed",
1321
- description: "Some current-stage activity is `failed`."
1322
- }, {
1323
- name: "actor",
1324
- binding: "caller",
1325
- label: "you",
1326
- description: "The acting identity (id + roles); `undefined` when no caller rides the evaluation. Never holds a value in the cascade gates (deploy-rejected there)."
1327
- }, {
1328
- name: "assigned",
1329
- binding: "caller",
1330
- label: "you are assigned to this activity",
1331
- description: "Whether the caller matches the activity's assignees-kind field entry (by user id, or by a role under the definition's `roleAliases`); `false` outside an activity context. Constant `false` in the cascade gates (deploy-rejected there)."
1332
- }, {
1333
- name: "can",
1334
- binding: "caller",
1335
- label: "your permissions",
1336
- 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."
1337
- }, {
1338
- name: "attributes",
1339
- binding: "caller",
1340
- label: "your attributes",
1341
- description: "Advisory org-level User Attributes for the caller (Enterprise — same values as lake `user::attributes()`), keyed by attribute name with each active scalar or array value. `undefined` on expected HTTP absence; unexpected fetch failures throw; empty page binds `{}`. Bound wherever grants ride the evaluation (same sites as `$can`). Soft-gate paths fetch at most 100 attributes (no further pages) and warn when the envelope reports `hasMore: true` (partial bag still binds). Not a security boundary — the Content Lake remains the only enforcement point."
1342
- }, {
1343
- name: "row",
1344
- binding: "spawn",
1345
- label: "the spawned row",
1346
- 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."
1347
- }, {
1348
- name: "params",
1349
- binding: "caller",
1350
- label: "the action's arguments",
1351
- 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."
1352
- }, {
1353
- name: "subworkflows",
1354
- binding: "always",
1355
- label: "the spawned subworkflows",
1356
- 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`."
1357
- } ], 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 = [ {
1358
- name: "tag",
1359
- label: "this engine tag",
1360
- description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1361
- }, {
1362
- name: "definition",
1363
- label: "this workflow definition",
1364
- description: "The `name` of the definition under evaluation (its own start block binds it)."
1365
- }, {
1366
- name: "now",
1367
- label: "the current time",
1368
- description: "The ISO clock reading of the evaluating engine."
1369
- } ], START_REQUIREMENT_VARS = [ ...START_FILTER_VARS, {
1370
- name: "fields",
1371
- label: "the start's input fields",
1372
- 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` or `subject` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
1373
- } ], GUARD_PREDICATE_VARS = [ {
1374
- name: "document",
1375
- description: "The attempted mutation images, exposed as `document.before` and `document.after`."
1376
- }, {
1377
- name: "guard",
1378
- description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
1379
- }, {
1380
- name: "mutation",
1381
- description: "The attempted mutation — `mutation.action` is the write kind being gated."
1382
- } ], NonEmptyString = v.pipe(v.string(), v.minLength(1, "must be a non-empty string"));
856
+ function wholeValueSchema(args) {
857
+ const {entryType: entryType, shape: shape, leaf: leaf} = args;
858
+ return entryType === "object" ? v.union([ v.null(), objectSchema({
859
+ fields: shape.fields ?? [],
860
+ leaf: leaf
861
+ }) ]) : entryType === "array" ? v.array(objectSchema({
862
+ fields: shape.of ?? [],
863
+ leaf: leaf
864
+ })) : leaf[entryType];
865
+ }
1383
866
 
1384
- function isParseableInstant(value) {
1385
- return typeof value == "string" && !Number.isNaN(Date.parse(value));
867
+ function appendItemSchema(entryType, shape) {
868
+ if (entryType === "array") return objectSchema({
869
+ fields: shape.of ?? [],
870
+ leaf: valueSchemas
871
+ });
872
+ if (entryType === "doc.refs") return GdrShape;
873
+ if (entryType === "assignee" || entryType === "assignees") return AssigneeWriteShape;
1386
874
  }
1387
875
 
1388
- const IsoTimestamp = v.pipe(v.string(), v.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
876
+ function rejectedRefTypes(args) {
877
+ const {entryType: entryType, types: types, value: value} = args;
878
+ if (types === void 0 || value === null || value === void 0) return [];
879
+ if (!refKindAcceptsTypes(entryType)) return [];
880
+ let items = [ value ];
881
+ return entryType === "doc.refs" && (items = Array.isArray(value) ? value : []),
882
+ [ ...new Set(items.map(gdrTypeOf).filter(t => t !== void 0 && !types.includes(t))) ];
883
+ }
1389
884
 
1390
- function tolerantObject() {
1391
- return (entries, ..._exact) => v.looseObject(entries);
885
+ function refTypeIssues(args) {
886
+ const rejected = rejectedRefTypes(args);
887
+ if (rejected.length === 0) return;
888
+ const accepts = (args.types ?? []).map(t => `"${t}"`).join(", ");
889
+ return rejected.map(t => `document type "${t}" is not accepted — this entry accepts ${accepts} (a GDR's \`type\` names the target document's schema type)`);
1392
890
  }
1393
891
 
1394
- function tolerantEntries() {
1395
- return (entries, ..._exact) => entries;
892
+ function gdrTypeOf(item) {
893
+ if (typeof item != "object" || item === null) return;
894
+ const t = item.type;
895
+ return typeof t == "string" ? t : void 0;
1396
896
  }
1397
897
 
1398
- function schemaTreeShape(schema) {
1399
- return walkSchemaShape(schema, /* @__PURE__ */ new Set);
898
+ function parseFieldValue(args) {
899
+ return checkValueAgainst(args, valueSchemas);
1400
900
  }
1401
901
 
1402
- function walkSchemaShape(node, path) {
1403
- if (typeof node != "object" || node === null) return "unknown";
1404
- if (path.has(node)) return "(circular)";
1405
- path.add(node);
1406
- try {
1407
- const schema = node;
1408
- return containerShape(schema, path) ?? leafShape(schema);
1409
- } finally {
1410
- path.delete(node);
1411
- }
902
+ function checkValueAgainst(args, leaf) {
903
+ const schema = wholeValueSchema({
904
+ entryType: args.entryType,
905
+ shape: args,
906
+ leaf: leaf
907
+ });
908
+ if (schema === void 0) return {
909
+ issues: [ `unknown field entry type ${args.entryType}` ]
910
+ };
911
+ const result = v.safeParse(schema, args.value);
912
+ if (!result.success) return {
913
+ issues: formatIssues(result.issues)
914
+ };
915
+ const postIssues = refTypeIssues({
916
+ entryType: args.entryType,
917
+ types: args.types,
918
+ value: args.value
919
+ }) ?? choiceValueIssues(args.options, args.value) ?? scalarValidationIssues({
920
+ entryType: args.entryType,
921
+ validation: args.validation,
922
+ value: args.value
923
+ }) ?? assignmentValueIssues(args, result.output);
924
+ return postIssues !== void 0 ? {
925
+ issues: postIssues
926
+ } : {
927
+ output: result.output
928
+ };
1412
929
  }
1413
930
 
1414
- const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
1415
- union: schema.options.map(option => walkSchemaShape(option, path))
1416
- }), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
1417
- strict_object: objectWalker,
1418
- loose_object: objectWalker,
1419
- object: objectWalker,
1420
- array: (schema, path) => ({
1421
- array: walkSchemaShape(schema.item, path)
1422
- }),
1423
- record: (schema, path) => ({
1424
- record: walkSchemaShape(schema.value, path)
1425
- }),
1426
- union: unionWalker,
1427
- variant: unionWalker,
1428
- lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
1429
- exact_optional: unwrapWalker,
1430
- optional: unwrapWalker,
1431
- nullable: unwrapWalker
1432
- };
931
+ function assignmentIdentity(assignee) {
932
+ return assignee.type === "user" ? `user:${assignee.id}` : `role:${assignee.role}`;
933
+ }
1433
934
 
1434
- function containerShape(schema, path) {
1435
- return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
935
+ function assignmentCandidate(value) {
936
+ const parsed = v.safeParse(AssigneeShape, value);
937
+ return parsed.success ? parsed.output : void 0;
1436
938
  }
1437
939
 
1438
- function objectEntriesShape(entries, path) {
1439
- const out = {};
1440
- for (const [key, entry] of Object.entries(entries)) {
1441
- const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
1442
- out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
940
+ function assignmentValueIssues(args, value) {
941
+ const retained = previousAssignmentIdentities(args), issues = [];
942
+ return visitAssignmentEntries({
943
+ entryType: args.entryType,
944
+ value: value,
945
+ roles: args.roles,
946
+ fields: args.fields,
947
+ of: args.of,
948
+ path: "$"
949
+ }, ({assignee: assignee, roles: roles, path: path}) => {
950
+ consumeRetainedAssignment({
951
+ retained: retained,
952
+ path: path,
953
+ identity: assignmentIdentity(assignee)
954
+ }) || issues.push(...assignmentCandidateIssues({
955
+ roles: roles,
956
+ memberRoles: args.memberRoles,
957
+ roleAliases: args.roleAliases
958
+ }, assignee));
959
+ }), issues.length === 0 ? void 0 : issues;
960
+ }
961
+
962
+ function consumeRetainedAssignment(args) {
963
+ const identities = args.retained.get(args.path), remaining = identities?.get(args.identity) ?? 0;
964
+ return remaining === 0 || identities === void 0 ? !1 : (remaining === 1 ? identities.delete(args.identity) : identities.set(args.identity, remaining - 1),
965
+ !0);
966
+ }
967
+
968
+ function previousAssignmentIdentities(args) {
969
+ const retained = /* @__PURE__ */ new Map;
970
+ if (args.previousValue === void 0) return retained;
971
+ const schema = wholeValueSchema({
972
+ entryType: args.entryType,
973
+ shape: args,
974
+ leaf: valueSchemas
975
+ });
976
+ if (schema === void 0) return retained;
977
+ const previous = v.safeParse(schema, args.previousValue);
978
+ return previous.success && visitAssignmentEntries({
979
+ entryType: args.entryType,
980
+ value: previous.output,
981
+ roles: args.roles,
982
+ fields: args.fields,
983
+ of: args.of,
984
+ path: "$"
985
+ }, ({assignee: assignee, path: path}) => {
986
+ const identities = retained.get(path) ?? /* @__PURE__ */ new Map, identity = assignmentIdentity(assignee);
987
+ identities.set(identity, (identities.get(identity) ?? 0) + 1), retained.set(path, identities);
988
+ }), retained;
989
+ }
990
+
991
+ function visitAssignmentEntries(args, visit) {
992
+ if (assignmentKindAcceptsRoles(args.entryType)) {
993
+ visitAssignmentCandidates(args, visit);
994
+ return;
1443
995
  }
1444
- return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
996
+ if (args.entryType === "object") {
997
+ visitAssignmentFields({
998
+ fields: args.fields,
999
+ value: args.value,
1000
+ path: args.path
1001
+ }, visit);
1002
+ return;
1003
+ }
1004
+ if (!(args.entryType !== "array" || !Array.isArray(args.value))) for (const row of args.value) visitAssignmentFields({
1005
+ fields: args.of,
1006
+ value: row,
1007
+ path: `${args.path}[]`
1008
+ }, visit);
1445
1009
  }
1446
1010
 
1447
- function isOptionalEntry(entry) {
1448
- if (typeof entry != "object" || entry === null) return !1;
1449
- const kind = entry.type;
1450
- return kind === "exact_optional" || kind === "optional";
1011
+ function visitAssignmentCandidates(args, visit) {
1012
+ if (args.roles === void 0) return;
1013
+ const indexedValues = Array.isArray(args.value) ? args.value.map(value => ({
1014
+ value: value,
1015
+ path: args.path
1016
+ })) : [ {
1017
+ value: args.value,
1018
+ path: args.path
1019
+ } ];
1020
+ for (const {value: value, path: path} of indexedValues) {
1021
+ const assignee = assignmentCandidate(value);
1022
+ assignee !== void 0 && visit({
1023
+ assignee: assignee,
1024
+ roles: args.roles,
1025
+ path: path
1026
+ });
1027
+ }
1451
1028
  }
1452
1029
 
1453
- const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
1030
+ function visitAssignmentFields(args, visit) {
1031
+ if (!(args.fields === void 0 || !isRecord(args.value))) for (const field of args.fields) visitAssignmentShape({
1032
+ shape: field,
1033
+ value: args.value[field.name],
1034
+ path: `${args.path}.${field.name}`
1035
+ }, visit);
1036
+ }
1454
1037
 
1455
- function leafShape(schema) {
1456
- if (schema.type === "picklist") return schema.options.join(" | ");
1457
- if (schema.type === "literal") return `literal ${String(schema.literal)}`;
1458
- if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
1459
- 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`);
1460
- return String(schema.type);
1038
+ function visitAssignmentShape(args, visit) {
1039
+ visitAssignmentEntries({
1040
+ entryType: args.shape.type,
1041
+ value: args.value,
1042
+ roles: args.shape.roles,
1043
+ fields: args.shape.fields,
1044
+ of: args.shape.of,
1045
+ path: args.path
1046
+ }, visit);
1047
+ }
1048
+
1049
+ function assignmentCandidateIssues(args, assignee) {
1050
+ if (assignee.type === "role") return collectiveRoleIssues(args, assignee.role);
1051
+ const held = args.memberRoles?.[assignee.id];
1052
+ return held === void 0 ? args.memberRoles === void 0 ? [] : [ `user "${assignee.id}" has no available project membership for role eligibility` ] : args.roles.some(required => actorFulfillsRole({
1053
+ actorRoles: held,
1054
+ required: required,
1055
+ aliases: args.roleAliases
1056
+ })) ? [] : [ `user "${assignee.id}" does not fulfill any eligible role: ${formatEligibleRoles(args.roles)}` ];
1057
+ }
1058
+
1059
+ function collectiveRoleIssues(args, role) {
1060
+ return args.roles.includes(role) ? [] : [ `collective role "${role}" is not eligible — this entry accepts ${formatEligibleRoles(args.roles)}` ];
1061
+ }
1062
+
1063
+ function formatEligibleRoles(roles) {
1064
+ return roles.map(role => `"${role}"`).join(", ");
1065
+ }
1066
+
1067
+ function issuesOf(check) {
1068
+ return "issues" in check ? check.issues : void 0;
1069
+ }
1070
+
1071
+ function validateFieldValue(args) {
1072
+ const check = checkValueAgainst(args, valueSchemas);
1073
+ if ("issues" in check) throw new FieldValueShapeError({
1074
+ entryType: args.entryType,
1075
+ entryName: args.entryName,
1076
+ issues: check.issues,
1077
+ mode: "value"
1078
+ });
1079
+ return check.output;
1461
1080
  }
1462
1081
 
1463
- function formatValidationError(label, issues) {
1464
- const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1465
- return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1082
+ const BARE_ID_SOURCE = "[A-Za-z0-9_][A-Za-z0-9._-]{0,127}", BARE_ID_RE = new RegExp(`^${BARE_ID_SOURCE}$`), ALIAS_REF_RE = new RegExp(`^@${RESOURCE_ALIAS_NAME_SOURCE}:${BARE_ID_SOURCE}$`);
1083
+
1084
+ function isBareDocumentId(id) {
1085
+ return BARE_ID_RE.test(id) && !id.includes("..");
1466
1086
  }
1467
1087
 
1468
- function issuesFromValibot(issues) {
1469
- return issues.map(issue => ({
1470
- path: issue.path ? issue.path.map(item => item.key) : [],
1471
- message: issue.message
1472
- }));
1088
+ function isAuthoringRefId(id) {
1089
+ if (id.startsWith("@")) {
1090
+ const separator = id.indexOf(":");
1091
+ return ALIAS_REF_RE.test(id) && isBareDocumentId(id.slice(separator + 1));
1092
+ }
1093
+ return id.includes(":") ? isGdrUri(id) : isBareDocumentId(id);
1473
1094
  }
1474
1095
 
1475
- function formatIssuePath(path) {
1476
- let out = "";
1477
- for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1478
- return out;
1096
+ function isBareSeedId(id) {
1097
+ return isBareDocumentId(id);
1479
1098
  }
1480
1099
 
1481
- class PersistedDocShapeError extends WorkflowError {
1482
- documentId;
1483
- documentType;
1484
- issues;
1485
- constructor(args) {
1486
- super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
1487
- this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
1488
- this.issues = args.issues;
1489
- }
1100
+ const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a bare document id, a GDR URI, or a portable `@<alias>:<id>` reference")), AuthoringGdrShape = v.looseObject({
1101
+ id: AuthoringRefId,
1102
+ type: NonEmptyString$1
1103
+ }), seedValueSchemas = {
1104
+ ...valueSchemas,
1105
+ "doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
1106
+ "doc.refs": v.array(AuthoringGdrShape),
1107
+ subject: v.union([ v.null(), AuthoringGdrShape ]),
1108
+ "release.ref": v.union([ v.null(), v.looseObject({
1109
+ id: AuthoringRefId,
1110
+ type: v.literal("system.release"),
1111
+ releaseName: NonEmptyString$1
1112
+ }) ])
1113
+ };
1114
+
1115
+ function checkLiteralSeed(args) {
1116
+ return args.value === null ? [ "a literal seed cannot be null — omit `initialValue` to start the field empty" ] : issuesOf(checkValueAgainst(args, seedValueSchemas));
1490
1117
  }
1491
1118
 
1492
- function parsePersistedDoc(args) {
1493
- const result = v.safeParse(args.schema, args.doc);
1494
- if (!result.success) throw new PersistedDocShapeError({
1495
- documentId: documentIdOf(args.doc),
1496
- documentType: args.docType,
1497
- issues: issuesFromValibot(result.issues)
1119
+ function validateFieldAppendItem(args) {
1120
+ const schema = appendItemSchema(args.entryType, args);
1121
+ if (schema === void 0) throw new FieldValueShapeError({
1122
+ entryType: args.entryType,
1123
+ entryName: args.entryName,
1124
+ issues: [ `field entry type ${args.entryType} does not support append` ],
1125
+ mode: "item"
1126
+ });
1127
+ const result = v.safeParse(schema, args.item);
1128
+ if (!result.success) throw new FieldValueShapeError({
1129
+ entryType: args.entryType,
1130
+ entryName: args.entryName,
1131
+ issues: formatIssues(result.issues),
1132
+ mode: "item"
1133
+ });
1134
+ const assignmentArgs = {
1135
+ entryType: appendAssignmentEntryType(args.entryType),
1136
+ value: result.output,
1137
+ roles: args.roles,
1138
+ fields: args.entryType === "array" ? args.of : void 0,
1139
+ memberRoles: args.memberRoles,
1140
+ roleAliases: args.roleAliases
1141
+ }, assignmentIssues = assignmentValueIssues(assignmentArgs, result.output);
1142
+ if (assignmentIssues !== void 0) throw new FieldValueShapeError({
1143
+ entryType: args.entryType,
1144
+ entryName: args.entryName,
1145
+ issues: assignmentIssues,
1146
+ mode: "item"
1147
+ });
1148
+ const typeIssues = refTypeIssues({
1149
+ entryType: args.entryType,
1150
+ types: args.types,
1151
+ value: [ args.item ]
1152
+ });
1153
+ if (typeIssues !== void 0) throw new FieldValueShapeError({
1154
+ entryType: args.entryType,
1155
+ entryName: args.entryName,
1156
+ issues: typeIssues,
1157
+ mode: "item"
1498
1158
  });
1499
1159
  return result.output;
1500
1160
  }
1501
1161
 
1502
- function documentIdOf(doc) {
1503
- if (typeof doc == "object" && doc !== null) {
1504
- const id = doc._id;
1505
- if (typeof id == "string") return id;
1506
- }
1507
- return "(unknown id)";
1162
+ function appendAssignmentEntryType(entryType) {
1163
+ return entryType === "assignee" || entryType === "assignees" ? "assignee" : entryType === "array" ? "object" : entryType;
1508
1164
  }
1509
1165
 
1510
- const ACTOR_KINDS = [ "person", "agent", "system" ];
1511
-
1512
- function normalizeAssignmentMembers(value) {
1513
- return value === null ? [] : Array.isArray(value) ? value : [ value ];
1166
+ function formatIssues(issues, formatMessage = issue => issue.message) {
1167
+ const formatOne = (issue, prefix) => {
1168
+ const keys = [ ...prefix, ...issue.path?.map(p => p.key) ?? [] ], sub = issue.issues?.filter(candidate => candidate.expected !== "null");
1169
+ return sub !== void 0 && sub.length > 0 ? sub.flatMap(candidate => formatOne(candidate, keys)) : [ `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(issue)}` ];
1170
+ };
1171
+ return issues.flatMap(issue => formatOne(issue, []));
1514
1172
  }
1515
1173
 
1516
- function assignmentMembers(entries) {
1517
- return entries.flatMap(entry => isAssignmentFieldEntry(entry) ? normalizeAssignmentMembers(entry.value) : []);
1518
- }
1174
+ const ACTIVITY_STATUSES = _exhaustiveOptions()([ "active", "done", "skipped", "failed" ]), TERMINAL_ACTIVITY_STATUSES = _exhaustiveOptions()([ "done", "skipped", "failed" ]);
1519
1175
 
1520
- function assignmentState(members) {
1521
- return members.length === 0 ? "unrouted" : members.some(member => member.type === "user") ? "held" : "routed";
1176
+ function isTerminalActivityStatus(status) {
1177
+ return TERMINAL_ACTIVITY_STATUSES.includes(status);
1522
1178
  }
1523
1179
 
1524
- function activeAssignmentMembers(members) {
1525
- const users = members.filter(member => member.type === "user");
1526
- return users.length > 0 ? users : members;
1180
+ const SIGNAL_SEMANTICS = [ "signal.positive", "signal.caution", "signal.critical" ], DECISION_SEMANTICS = [ "decision.accept", "decision.decline" ], ACTION_SEMANTICS = [ ...DECISION_SEMANTICS, ...SIGNAL_SEMANTICS ], FIELD_SCOPES = _exhaustiveOptions()([ "workflow", "stage", "activity" ]), DOCUMENT_VALUE_PERMISSIONS = [ "create", "manage", "read", "update" ], LAKE_MUTATION_GUARD_ACTIONS = _exhaustiveOptions()([ "create", "update", "delete" ]), MUTATION_GUARD_ACTIONS = _exhaustiveOptions()([ ...LAKE_MUTATION_GUARD_ACTIONS, "publish", "unpublish" ]), GUARD_ACTIONS_REQUIRED_MESSAGE = "a guard must match at least one action", MUTATION_GUARD_ID_SPACES = [ "authored", "edit", "published" ];
1181
+
1182
+ function mutationGuardActionIdSpace(action) {
1183
+ return action === "update" ? "edit" : action === "publish" || action === "unpublish" ? "published" : "authored";
1527
1184
  }
1528
1185
 
1529
- function assignmentMatch(members, identity) {
1530
- const active = activeAssignmentMembers(members);
1531
- return active.some(member => member.type === "user" && member.id === identity.userId) ? "user" : active.some(member => member.type === "role" && identity.roles.includes(member.role)) ? "role" : void 0;
1186
+ function mutationGuardRequiresSplitEmission(actions) {
1187
+ return new Set(actions.map(mutationGuardActionIdSpace)).size > 1;
1532
1188
  }
1533
1189
 
1534
- function identityMatchesAssignment(members, identity) {
1535
- return assignmentMatch(members, identity) !== void 0;
1190
+ const ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], NonEmpty = NonEmptyString$1, PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1191
+
1192
+ function groqIdentifier(referencedAs) {
1193
+ return v.pipe(v.string(), v.regex(GROQ_IDENTIFIER, `must be a GROQ-safe identifier (letters, digits, underscore; not starting with a digit) because it is referenced as ${referencedAs} in GROQ conditions`));
1536
1194
  }
1537
1195
 
1538
- function assignmentStateCounts(assignments, identity) {
1539
- const counts = {
1540
- unrouted: 0,
1541
- routed: 0,
1542
- held: 0
1543
- };
1544
- for (const members of assignments) {
1545
- const state = assignmentState(members);
1546
- state === "unrouted" ? counts.unrouted += 1 : identityMatchesAssignment(members, identity) && (counts[state] += 1);
1547
- }
1548
- return counts;
1196
+ function picklist(options) {
1197
+ return v.picklist(options, invalidOptionMessage(options));
1549
1198
  }
1550
1199
 
1551
- function openActivityAssignments(instance) {
1552
- return (findOpenStageEntry(instance)?.activities ?? []).filter(activity => activity.status === "active").map(activity => assignmentMembers(activity.fields ?? []));
1200
+ function invalidOptionMessage(options) {
1201
+ return `Invalid option: expected one of ${options.map(option => `"${option}"`).join("|")}`;
1553
1202
  }
1554
1203
 
1555
- function instanceAssignmentStateCounts(instance, identity) {
1556
- return assignmentStateCounts(openActivityAssignments(instance), identity);
1204
+ function pinned() {
1205
+ return (schema, ..._exact) => schema;
1557
1206
  }
1558
1207
 
1559
- function actorMatchesAssignment(args) {
1560
- const actor = args.actor;
1561
- return actor === void 0 ? !1 : activeAssignmentMembers(args.members).some(member => member.type === "user" ? member.id === actor.id : actorFulfillsRole({
1562
- actorRoles: actor.roles,
1563
- required: member.role,
1564
- aliases: args.roleAliases
1565
- }));
1208
+ const LiteralSchema = v.strictObject({
1209
+ type: v.literal("literal"),
1210
+ value: v.unknown()
1211
+ }), FieldReadSchema = v.strictObject({
1212
+ type: v.literal("fieldRead"),
1213
+ scope: v.optional(v.union([ v.literal("workflow"), v.literal("stage") ])),
1214
+ field: NonEmpty,
1215
+ path: v.optional(v.string())
1216
+ }), FieldSourceSchema = v.variant("type", [ v.strictObject({
1217
+ type: v.literal("input")
1218
+ }), v.strictObject({
1219
+ type: v.literal("query"),
1220
+ query: NonEmpty
1221
+ }), LiteralSchema, FieldReadSchema ]), ValueExprSchema = v.lazy(() => v.variant("type", [ LiteralSchema, FieldReadSchema, v.strictObject({
1222
+ type: v.literal("param"),
1223
+ param: NonEmpty
1224
+ }), v.strictObject({
1225
+ type: v.literal("actor")
1226
+ }), v.strictObject({
1227
+ type: v.literal("now")
1228
+ }), v.strictObject({
1229
+ type: v.literal("self")
1230
+ }), v.strictObject({
1231
+ type: v.literal("stage")
1232
+ }), v.strictObject({
1233
+ type: v.literal("object"),
1234
+ fields: v.record(NonEmpty, ValueExprSchema)
1235
+ }) ])), StoredFieldRefSchema = v.strictObject({
1236
+ scope: picklist(FIELD_SCOPES),
1237
+ field: NonEmpty
1238
+ }), AuthoringFieldRefSchema = v.strictObject({
1239
+ scope: v.optional(picklist(FIELD_SCOPES)),
1240
+ field: NonEmpty
1241
+ }), HREF_SCHEMES = [ "http:", "https:" ];
1242
+
1243
+ function isHttpUrl(value) {
1244
+ try {
1245
+ return HREF_SCHEMES.includes(new URL(value).protocol);
1246
+ } catch {
1247
+ return !1;
1248
+ }
1566
1249
  }
1567
1250
 
1568
- const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
1251
+ const UrlString = v.pipe(v.string(), v.url("must be a valid URL"), v.check(isHttpUrl, "must be an http(s) URL"));
1569
1252
 
1570
- function classifyPrincipalId(id) {
1571
- if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
1572
- namespace: "sentinel"
1573
- };
1574
- if (id.startsWith("g")) return {
1575
- namespace: "global",
1576
- globalId: id
1577
- };
1578
- if (id.startsWith("p-")) return {
1579
- namespace: "robot",
1580
- globalId: id
1581
- };
1582
- const embeddedGlobal = E_PREFIXED_PROJECT_ID.exec(id)?.[1];
1583
- return embeddedGlobal !== void 0 ? embeddedGlobal.startsWith("g") ? {
1584
- namespace: "project",
1585
- globalId: embeddedGlobal
1586
- } : {
1587
- namespace: "unknown"
1588
- } : id.startsWith("p") ? {
1589
- namespace: "project"
1590
- } : {
1591
- namespace: "unknown"
1592
- };
1253
+ function manualTargetSchema(ref) {
1254
+ return v.variant("type", [ v.strictObject({
1255
+ type: v.literal("url"),
1256
+ url: UrlString
1257
+ }), v.strictObject({
1258
+ type: v.literal("field"),
1259
+ field: ref
1260
+ }) ]);
1593
1261
  }
1594
1262
 
1595
- function directoryBridgeId(sanityUserId) {
1596
- if (typeof sanityUserId == "string") return classifyPrincipalId(sanityUserId).namespace === "global" ? sanityUserId : void 0;
1263
+ const StoredManualTargetSchema = pinned()(manualTargetSchema(StoredFieldRefSchema)), AuthoringManualTargetSchema = pinned()(manualTargetSchema(v.union([ NonEmpty, AuthoringFieldRefSchema ]))), ConditionSchema = NonEmpty;
1264
+
1265
+ function opSchemas(targetSchema) {
1266
+ return [ v.strictObject({
1267
+ type: v.literal("field.set"),
1268
+ target: targetSchema,
1269
+ value: ValueExprSchema
1270
+ }), v.strictObject({
1271
+ type: v.literal("field.setIfMissing"),
1272
+ target: targetSchema,
1273
+ value: ValueExprSchema
1274
+ }), v.strictObject({
1275
+ type: v.literal("field.unset"),
1276
+ target: targetSchema
1277
+ }), v.strictObject({
1278
+ type: v.literal("field.append"),
1279
+ target: targetSchema,
1280
+ value: ValueExprSchema
1281
+ }), v.strictObject({
1282
+ type: v.literal("field.inc"),
1283
+ target: targetSchema,
1284
+ value: v.optional(ValueExprSchema)
1285
+ }), v.strictObject({
1286
+ type: v.literal("field.dec"),
1287
+ target: targetSchema,
1288
+ value: v.optional(ValueExprSchema)
1289
+ }), v.strictObject({
1290
+ type: v.literal("field.updateWhere"),
1291
+ target: targetSchema,
1292
+ where: ConditionSchema,
1293
+ value: ValueExprSchema
1294
+ }), v.strictObject({
1295
+ type: v.literal("field.removeWhere"),
1296
+ target: targetSchema,
1297
+ where: ConditionSchema
1298
+ }) ];
1597
1299
  }
1598
1300
 
1599
- function firstCarriedGlobalId(candidates) {
1600
- for (const candidate of candidates) {
1601
- if (candidate === void 0) continue;
1602
- const {globalId: globalId} = classifyPrincipalId(candidate);
1603
- if (globalId !== void 0) return globalId;
1604
- }
1605
- }
1301
+ const StoredFieldOpSchema = pinned()(v.variant("type", [ ...opSchemas(StoredFieldRefSchema) ])), StoredOpSchema = pinned()(v.variant("type", [ ...opSchemas(StoredFieldRefSchema), v.strictObject({
1302
+ type: v.literal("status.set"),
1303
+ activity: NonEmpty,
1304
+ status: picklist(ACTIVITY_STATUSES)
1305
+ }) ])), AuditOpSchema = v.strictObject({
1306
+ type: v.literal("audit"),
1307
+ target: AuthoringFieldRefSchema,
1308
+ value: ValueExprSchema,
1309
+ stampFields: v.optional(v.strictObject({
1310
+ actor: v.optional(NonEmpty),
1311
+ at: v.optional(NonEmpty)
1312
+ }))
1313
+ }), AuthoringOpSchema = pinned()(v.variant("type", [ ...opSchemas(AuthoringFieldRefSchema), v.strictObject({
1314
+ type: v.literal("status.set"),
1315
+ activity: v.optional(NonEmpty),
1316
+ status: picklist(ACTIVITY_STATUSES)
1317
+ }), AuditOpSchema ])), GroupName = v.pipe(v.string(), v.regex(GROQ_IDENTIFIER, "must be an identifier (letters, digits, underscore; not starting with a digit)")), GroupSchema = pinned()(v.strictObject({
1318
+ name: GroupName,
1319
+ title: v.optional(v.string()),
1320
+ description: v.optional(v.string()),
1321
+ kind: v.optional(picklist(GROUP_KINDS))
1322
+ })), GroupNameList = v.pipe(v.array(GroupName), v.minLength(1, "name at least one group, or omit `group`"), v.check(names => new Set(names).size === names.length, "a group is listed more than once — list each group once")), StoredGroupMembershipSchema = GroupNameList, AuthoringGroupMembershipSchema = v.union([ GroupName, GroupNameList ]);
1606
1323
 
1607
- function lakePrincipalId(args) {
1608
- return args.localPrincipalId ?? args.actor.id;
1324
+ function groupMembershipNames(group) {
1325
+ return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
1609
1326
  }
1610
1327
 
1611
- function isRecord(value) {
1612
- return typeof value == "object" && value !== null && !Array.isArray(value);
1613
- }
1328
+ const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "progress", "boolean", "date", "dueDate", "datetime", "dueDatetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), AUTHORING_FIELD_SUGAR_KINDS = _exhaustiveOptions()([ "todoList", "notes" ]), AUTHORING_FIELD_KINDS = [ ...FIELD_VALUE_KINDS, ...AUTHORING_FIELD_SUGAR_KINDS ], AuthoringRawFieldKindSchema = v.picklist(FIELD_VALUE_KINDS, issue => `${invalidOptionMessage(AUTHORING_FIELD_KINDS)} but received ${JSON.stringify(issue.input)}`), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v.pipe(v.number(), v.finite("must be finite")), ScalarValidationSchema = v.pipe(v.strictObject({
1329
+ min: v.optional(FiniteNumber),
1330
+ max: v.optional(FiniteNumber)
1331
+ }), v.check(validation => validation.min !== void 0 || validation.max !== void 0, "declare at least one bound, or omit `validation`"), v.check(validation => validation.min === void 0 || validation.max === void 0 || validation.min <= validation.max, "`min` must be less than or equal to `max`")), ChoiceOptionsSchema = v.strictObject({
1332
+ list: v.pipe(v.array(v.strictObject({
1333
+ title: NonEmpty,
1334
+ value: v.union([ v.string(), v.number() ])
1335
+ })), v.minLength(1, "declare at least one choice, or omit `options`"))
1336
+ });
1614
1337
 
1615
- class FieldValueShapeError extends WorkflowError {
1616
- entryType;
1617
- entryName;
1618
- issues;
1619
- constructor(args) {
1620
- const issueText = args.issues.join("; ");
1621
- super("field-value-shape", `Field entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`),
1622
- this.name = "FieldValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName,
1623
- this.issues = args.issues;
1624
- }
1338
+ function asShape(input) {
1339
+ return typeof input == "object" && input !== null ? input : {};
1625
1340
  }
1626
1341
 
1627
- const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
1628
- id: GdrUriSchema,
1629
- type: NonEmptyString
1630
- }), ReleaseRefShape = v.pipe(tolerantObject()({
1631
- id: GdrUriSchema,
1632
- type: v.literal("system.release"),
1633
- releaseName: NonEmptyString
1634
- }), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = tolerantObject()({
1635
- kind: v.picklist(ACTOR_KINDS),
1636
- id: NonEmptyString,
1637
- roles: v.exactOptional(v.array(v.string())),
1638
- onBehalfOf: v.exactOptional(v.string())
1639
- }), AssigneeShape = v.union([ tolerantObject()({
1640
- type: v.literal("user"),
1641
- id: NonEmptyString
1642
- }), tolerantObject()({
1643
- type: v.literal("role"),
1644
- role: NonEmptyString
1645
- }) ]), AssigneeListShape = v.pipe(v.union([ v.null(), AssigneeShape, v.array(AssigneeShape) ]), v.transform(normalizeAssignmentMembers)), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableProgress = v.union([ v.null(), v.pipe(v.number(), v.finite("progress must be a finite number"), v.minValue(0, "progress must be at least 0"), v.maxValue(100, "progress must be at most 100")) ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "dueDate", "datetime", "dueDatetime", "dateTime" ]);
1646
-
1647
- function normalizedChoiceKind(kind) {
1648
- return kind === "dateTime" ? "datetime" : kind;
1342
+ function compositeShapeOk(input) {
1343
+ const shape = asShape(input);
1344
+ return shape.type === "object" ? Array.isArray(shape.fields) && shape.fields.length > 0 && shape.of === void 0 : shape.type === "array" ? Array.isArray(shape.of) && shape.of.length > 0 && shape.fields === void 0 : shape.fields === void 0 && shape.of === void 0;
1649
1345
  }
1650
1346
 
1651
- function checkChoiceList(args) {
1652
- const {entryType: entryType, options: options, validation: validation} = args;
1653
- if (options === void 0) return;
1654
- if (!CHOICE_KINDS.has(entryType)) return [ `\`options\` is not valid on "${entryType}" values` ];
1655
- const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => issuesOf(checkValueAgainst({
1656
- entryType: kind,
1657
- value: option.value,
1658
- validation: validation
1659
- }, valueSchemas))?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
1660
- for (const option of options.list) seen.has(option.value) && issues.push(`duplicate option value ${JSON.stringify(option.value)}`),
1661
- seen.add(option.value);
1662
- return issues.length === 0 ? void 0 : issues;
1347
+ function compositeShapeMessage(input) {
1348
+ const shape = asShape(input);
1349
+ return shape.type === "object" ? shape.of !== void 0 ? "an `object` kind declares its sub-fields with `fields`, not `of`" : "an `object` kind needs a non-empty `fields` list of sub-field shapes" : shape.type === "array" ? shape.fields !== void 0 ? "an `array` kind declares its item shape with `of`, not `fields`" : "an `array` kind needs a non-empty `of` list of sub-field shapes" : `\`fields\` / \`of\` are only valid on the \`object\` / \`array\` kinds, not "${String(shape.type)}"`;
1663
1350
  }
1664
1351
 
1665
- function choiceValueIssues(options, value) {
1666
- if (!(options === void 0 || value === null || value === void 0)) return options.list.some(option => Object.is(option.value, value)) ? void 0 : [ `value ${JSON.stringify(value)} is not declared in \`options.list\`; expected one of ${options.list.map(option => JSON.stringify(option.value)).join(", ")}` ];
1352
+ function duplicateSubfieldName(input) {
1353
+ const shape = asShape(input);
1354
+ let list = [];
1355
+ Array.isArray(shape.fields) ? list = shape.fields : Array.isArray(shape.of) && (list = shape.of);
1356
+ const seen = /* @__PURE__ */ new Set;
1357
+ for (const item of list) {
1358
+ const name = asShape(item).name;
1359
+ if (typeof name == "string") {
1360
+ if (seen.has(name)) return name;
1361
+ seen.add(name);
1362
+ }
1363
+ }
1667
1364
  }
1668
1365
 
1669
- const fieldValueSchemas = {
1670
- "doc.ref": v.union([ v.null(), GdrShape ]),
1671
- "doc.refs": v.array(GdrShape),
1672
- subject: v.union([ v.null(), GdrShape ]),
1673
- "release.ref": v.union([ v.null(), ReleaseRefShape ]),
1674
- string: NullableString,
1675
- text: NullableString,
1676
- number: NullableNumber,
1677
- progress: NullableProgress,
1678
- boolean: NullableBoolean,
1679
- date: NullableDate,
1680
- dueDate: NullableDate,
1681
- datetime: NullableDateTime,
1682
- dueDatetime: NullableDateTime,
1683
- url: NullableUrl,
1684
- actor: v.union([ v.null(), ActorShape ]),
1685
- assignee: AssigneeListShape,
1686
- assignees: v.array(AssigneeShape)
1687
- }, WritePrincipalId = v.pipe(NonEmptyString, v.rawTransform(({dataset: dataset, addIssue: addIssue}) => {
1688
- const classified = classifyPrincipalId(dataset.value);
1689
- return classified.namespace === "global" || classified.namespace === "robot" ? dataset.value : classified.namespace === "project" && classified.globalId !== void 0 ? classified.globalId : (addIssue({
1690
- message: `principal id "${dataset.value}" is not an account-global user id. Workflow user ids are the global \`sanityUserId\` (or a robot token's id). Resolve project members through your surface's member hook, or /projects/<projectId>/users/<id> → sanityUserId.`
1691
- }), dataset.value);
1692
- })), ActorWriteShape = tolerantObject()({
1693
- kind: v.picklist(ACTOR_KINDS),
1694
- id: WritePrincipalId,
1695
- roles: v.exactOptional(v.array(v.string())),
1696
- onBehalfOf: v.exactOptional(v.string())
1697
- }), AssigneePrincipalId = v.pipe(WritePrincipalId, v.check(id => classifyPrincipalId(id).namespace !== "robot", "robot principals cannot be assignment members")), AssigneeWriteShape = v.union([ tolerantObject()({
1698
- type: v.literal("user"),
1699
- id: AssigneePrincipalId
1700
- }), tolerantObject()({
1701
- type: v.literal("role"),
1702
- role: NonEmptyString
1703
- }) ]), SingularAssigneeWriteShape = v.pipe(v.union([ AssigneeWriteShape, v.array(AssigneeWriteShape) ]), v.transform(normalizeAssignmentMembers), v.check(members => members.filter(member => member.type === "user").length <= 1, "must contain at most one user member")), valueSchemas = {
1704
- ...fieldValueSchemas,
1705
- actor: v.nullable(ActorWriteShape),
1706
- assignee: SingularAssigneeWriteShape,
1707
- assignees: v.array(AssigneeWriteShape),
1708
- query: v.any()
1709
- };
1710
-
1711
- function shapeValueSchema(args) {
1712
- const {shape: shape, leaf: leaf} = args;
1713
- if (shape.type === "object") return objectSchema({
1714
- fields: shape.fields ?? [],
1715
- leaf: leaf
1716
- });
1717
- if (shape.type === "array") return v.array(objectSchema({
1718
- fields: shape.of ?? [],
1719
- leaf: leaf
1720
- }));
1721
- const schema = leaf[shape.type] ?? v.any();
1722
- return constrainedScalarSchema({
1723
- schema: schema,
1724
- entryType: shape.type,
1725
- ...shape
1726
- });
1366
+ function compositeChecked(entries) {
1367
+ return v.pipe(v.strictObject(entries), v.check(input => compositeShapeOk(input), issue => compositeShapeMessage(issue.input)), v.check(input => duplicateSubfieldName(input) === void 0, issue => `duplicate sub-field name "${duplicateSubfieldName(issue.input)}" — sub-field names must be unique within \`fields\` / \`of\``));
1727
1368
  }
1728
1369
 
1729
- function scalarMeasurement(entryType, value) {
1730
- if ((entryType === "number" || entryType === "progress") && typeof value == "number") return value;
1731
- if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
1732
- }
1370
+ const AssignmentRolesSchema = v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one eligible role, or omit `roles`")), FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
1371
+ type: FieldValueKindSchema,
1372
+ name: FieldEntryName,
1373
+ title: v.optional(v.string()),
1374
+ description: v.optional(v.string()),
1375
+ options: v.optional(ChoiceOptionsSchema),
1376
+ validation: v.optional(ScalarValidationSchema),
1377
+ roles: v.optional(AssignmentRolesSchema),
1378
+ fields: v.optional(v.array(FieldShapeSchema)),
1379
+ of: v.optional(v.array(FieldShapeSchema))
1380
+ }), choiceOptionsCheck(), scalarValidationCheck(), assignmentRolesCheck())), StoredEditableSchema = pinned()(v.union([ v.literal(!0), NonEmpty ])), AuthoringEditableSchema = pinned()(v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]));
1733
1381
 
1734
- function scalarBoundIssue(args) {
1735
- const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
1736
- return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" || entryType === "progress" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
1382
+ function fieldBase(editable, group) {
1383
+ return {
1384
+ name: FieldEntryName,
1385
+ title: v.optional(v.string()),
1386
+ description: v.optional(v.string()),
1387
+ group: v.optional(group),
1388
+ required: v.optional(v.boolean()),
1389
+ initialValue: v.optional(FieldSourceSchema),
1390
+ editable: v.optional(editable)
1391
+ };
1737
1392
  }
1738
1393
 
1739
- function scalarValidationIssues(args) {
1740
- const {entryType: entryType, validation: validation, value: value} = args;
1741
- if (validation === void 0 || value === null || value === void 0) return;
1742
- const measured = scalarMeasurement(entryType, value);
1743
- if (measured === void 0) return;
1744
- const issues = [ scalarBoundIssue({
1745
- entryType: entryType,
1746
- measured: measured,
1747
- bound: validation.min,
1748
- limit: "min"
1749
- }), scalarBoundIssue({
1750
- entryType: entryType,
1751
- measured: measured,
1752
- bound: validation.max,
1753
- limit: "max"
1754
- }) ].filter(issue => issue !== void 0);
1755
- return issues.length === 0 ? void 0 : issues;
1394
+ function fieldEntryFields({editable: editable, group: group, kind: kind = FieldKindSchema}) {
1395
+ return {
1396
+ type: kind,
1397
+ ...fieldBase(editable, group),
1398
+ options: v.optional(ChoiceOptionsSchema),
1399
+ validation: v.optional(ScalarValidationSchema),
1400
+ types: v.optional(v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
1401
+ roles: v.optional(AssignmentRolesSchema),
1402
+ fields: v.optional(v.array(FieldShapeSchema)),
1403
+ of: v.optional(v.array(FieldShapeSchema))
1404
+ };
1756
1405
  }
1757
1406
 
1758
- function constrainedScalarSchema(args) {
1759
- const {schema: schema, entryType: entryType, options: options, validation: validation} = args;
1760
- return options === void 0 && validation === void 0 ? schema : v.pipe(schema, v.check(value => choiceValueIssues(options, value) === void 0 && scalarValidationIssues({
1761
- entryType: entryType,
1762
- validation: validation,
1763
- value: value
1764
- }) === void 0, issue => [ ...choiceValueIssues(options, issue.input) ?? [], ...scalarValidationIssues({
1765
- entryType: entryType,
1766
- validation: validation,
1767
- value: issue.input
1768
- }) ?? [] ].join("; ")));
1407
+ function literalSeedIssues(entry) {
1408
+ if (entry.initialValue?.type === "literal") return checkLiteralSeed({
1409
+ entryType: entry.type,
1410
+ value: entry.initialValue.value,
1411
+ types: entry.types,
1412
+ fields: entry.fields,
1413
+ of: entry.of,
1414
+ options: entry.options,
1415
+ validation: entry.validation,
1416
+ roles: entry.roles
1417
+ });
1769
1418
  }
1770
1419
 
1771
- function objectSchema(args) {
1772
- const {fields: fields, leaf: leaf} = args, entries = /* @__PURE__ */ Object.create(null);
1773
- for (const f of fields) entries[f.name] = v.optional(shapeValueSchema({
1774
- shape: f,
1775
- leaf: leaf
1776
- }));
1777
- return v.looseObject(entries);
1420
+ function literalSeedCheck() {
1421
+ return v.check(entry => literalSeedIssues(entry) === void 0, issue => `initialValue literal does not fit the declared kind: ${(literalSeedIssues(issue.input) ?? []).join("; ")}`);
1778
1422
  }
1779
1423
 
1780
- function wholeValueSchema(args) {
1781
- const {entryType: entryType, shape: shape, leaf: leaf} = args;
1782
- return entryType === "object" ? v.union([ v.null(), objectSchema({
1783
- fields: shape.fields ?? [],
1784
- leaf: leaf
1785
- }) ]) : entryType === "array" ? v.array(objectSchema({
1786
- fields: shape.of ?? [],
1787
- leaf: leaf
1788
- })) : leaf[entryType];
1424
+ function refTypesCheck() {
1425
+ return v.check(entry => entry.types === void 0 || refKindAcceptsTypes(entry.type), issue => `\`types\` is only valid on \`doc.ref\` / \`doc.refs\` / \`subject\` entries, not "${issue.input.type}"`);
1789
1426
  }
1790
1427
 
1791
- function appendItemSchema(entryType, shape) {
1792
- if (entryType === "array") return objectSchema({
1793
- fields: shape.of ?? [],
1794
- leaf: valueSchemas
1795
- });
1796
- if (entryType === "doc.refs") return GdrShape;
1797
- if (entryType === "assignee" || entryType === "assignees") return AssigneeWriteShape;
1428
+ function assignmentRolesCheck() {
1429
+ return v.check(entry => entry.roles === void 0 || assignmentKindAcceptsRoles(entry.type), issue => `\`roles\` is only valid on \`assignee\` / \`assignees\` entries, not "${issue.input.type}"`);
1798
1430
  }
1799
1431
 
1800
- function rejectedRefTypes(args) {
1801
- const {entryType: entryType, types: types, value: value} = args;
1802
- if (types === void 0 || value === null || value === void 0) return [];
1803
- if (!refKindAcceptsTypes(entryType)) return [];
1804
- let items = [ value ];
1805
- return entryType === "doc.refs" && (items = Array.isArray(value) ? value : []),
1806
- [ ...new Set(items.map(gdrTypeOf).filter(t => t !== void 0 && !types.includes(t))) ];
1432
+ function choiceOptionsCheck() {
1433
+ return v.check(entry => checkChoiceList({
1434
+ entryType: entry.type,
1435
+ options: entry.options,
1436
+ validation: entry.validation
1437
+ }) === void 0, issue => (checkChoiceList({
1438
+ entryType: issue.input.type,
1439
+ options: issue.input.options,
1440
+ validation: issue.input.validation
1441
+ }) ?? []).join("; "));
1807
1442
  }
1808
1443
 
1809
- function refTypeIssues(args) {
1810
- const rejected = rejectedRefTypes(args);
1811
- if (rejected.length === 0) return;
1812
- const accepts = (args.types ?? []).map(t => `"${t}"`).join(", ");
1813
- return rejected.map(t => `document type "${t}" is not accepted — this entry accepts ${accepts} (a GDR's \`type\` names the target document's schema type)`);
1814
- }
1444
+ const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "progress" ]);
1815
1445
 
1816
- function gdrTypeOf(item) {
1817
- if (typeof item != "object" || item === null) return;
1818
- const t = item.type;
1819
- return typeof t == "string" ? t : void 0;
1446
+ function scalarValidationCheck() {
1447
+ return v.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
1820
1448
  }
1821
1449
 
1822
- function parseFieldValue(args) {
1823
- return checkValueAgainst(args, valueSchemas);
1450
+ function scalarValidationDeclarationIssues(entry) {
1451
+ const {type: type, validation: validation} = entry;
1452
+ if (validation === void 0) return;
1453
+ if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` / \`progress\` values, not "${type}"` ];
1454
+ if (type === "progress") {
1455
+ const issues2 = Object.entries(validation).flatMap(([bound, value]) => typeof value == "number" && value >= 0 && value <= 100 ? [] : [ `\`validation.${bound}\` must stay within the progress kind's 0–100 contract` ]);
1456
+ return issues2.length === 0 ? void 0 : issues2;
1457
+ }
1458
+ if (type === "number") return;
1459
+ const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
1460
+ return issues.length === 0 ? void 0 : issues;
1824
1461
  }
1825
1462
 
1826
- function checkValueAgainst(args, leaf) {
1827
- const schema = wholeValueSchema({
1828
- entryType: args.entryType,
1829
- shape: args,
1830
- leaf: leaf
1831
- });
1832
- if (schema === void 0) return {
1833
- issues: [ `unknown field entry type ${args.entryType}` ]
1463
+ const FieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
1464
+ editable: StoredEditableSchema,
1465
+ group: StoredGroupMembershipSchema
1466
+ })), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
1467
+ editable: AuthoringEditableSchema,
1468
+ group: AuthoringGroupMembershipSchema,
1469
+ kind: AuthoringRawFieldKindSchema
1470
+ })), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck()));
1471
+
1472
+ function listSugarFields(type) {
1473
+ return {
1474
+ type: v.literal(type),
1475
+ ...fieldBase(AuthoringEditableSchema, AuthoringGroupMembershipSchema)
1834
1476
  };
1835
- const result = v.safeParse(schema, args.value);
1836
- if (!result.success) return {
1837
- issues: formatIssues(result.issues)
1477
+ }
1478
+
1479
+ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))), NotesFieldSchema = pinned()(v.strictObject(listSugarFields("notes"))), AuthoringFieldEntrySchema = pinned()(v.lazy(input => {
1480
+ const type = asShape(input).type;
1481
+ return type === "todoList" ? TodoListFieldSchema : type === "notes" ? NotesFieldSchema : RawAuthoringFieldEntrySchema;
1482
+ })), RUNTIME_KINDS = _exhaustiveOptions()([ "function", "durableFunction", "selfHosted" ]), RuntimeBlockSchema = pinned()(v.strictObject({
1483
+ kind: picklist(RUNTIME_KINDS)
1484
+ })), EffectRuntimeBlockSchema = pinned()(v.variant("kind", [ v.strictObject({
1485
+ kind: v.literal("function"),
1486
+ timeout: v.optional(PositiveInt),
1487
+ memory: v.optional(PositiveInt)
1488
+ }), v.strictObject({
1489
+ kind: v.literal("durableFunction")
1490
+ }), v.strictObject({
1491
+ kind: v.literal("selfHosted")
1492
+ }) ], invalidOptionMessage(RUNTIME_KINDS))), EFFECT_RETRY_KINDS = _exhaustiveOptions()([ "engine" ]), EffectRetryBackoffSchema = pinned()(v.strictObject({
1493
+ kind: picklist([ "fixed", "exponential" ]),
1494
+ delayMs: PositiveInt
1495
+ }));
1496
+
1497
+ function effectRetryFields(kind) {
1498
+ return {
1499
+ kind: kind,
1500
+ attempts: PositiveInt,
1501
+ backoff: v.optional(EffectRetryBackoffSchema),
1502
+ expiryMs: v.optional(PositiveInt)
1838
1503
  };
1839
- const postIssues = refTypeIssues({
1840
- entryType: args.entryType,
1841
- types: args.types,
1842
- value: args.value
1843
- }) ?? choiceValueIssues(args.options, args.value) ?? scalarValidationIssues({
1844
- entryType: args.entryType,
1845
- validation: args.validation,
1846
- value: args.value
1847
- }) ?? assignmentValueIssues(args, result.output);
1848
- return postIssues !== void 0 ? {
1849
- issues: postIssues
1850
- } : {
1851
- output: result.output
1504
+ }
1505
+
1506
+ const StoredEffectRetrySchema = pinned()(v.strictObject(effectRetryFields(picklist(EFFECT_RETRY_KINDS)))), AuthoringEffectRetrySchema = pinned()(v.strictObject(effectRetryFields(v.optional(picklist(EFFECT_RETRY_KINDS)))));
1507
+
1508
+ function effectFields(retry) {
1509
+ return {
1510
+ name: NonEmpty,
1511
+ title: v.optional(v.string()),
1512
+ description: v.optional(v.string()),
1513
+ bindings: v.optional(v.record(v.string(), ConditionSchema)),
1514
+ input: v.optional(v.record(v.string(), v.unknown())),
1515
+ outputs: v.optional(v.array(FieldShapeSchema)),
1516
+ retry: v.optional(retry)
1852
1517
  };
1853
1518
  }
1854
1519
 
1855
- function assignmentIdentity(assignee) {
1856
- return assignee.type === "user" ? `user:${assignee.id}` : `role:${assignee.role}`;
1520
+ const EffectSchema = pinned()(v.strictObject(effectFields(StoredEffectRetrySchema))), AuthoringEffectSchema = pinned()(v.strictObject({
1521
+ ...effectFields(AuthoringEffectRetrySchema),
1522
+ runtime: v.optional(EffectRuntimeBlockSchema)
1523
+ })), DefinitionRefSchema = v.strictObject({
1524
+ name: NonEmpty,
1525
+ version: v.optional(v.union([ PositiveInt, v.literal("latest") ]))
1526
+ }), SubworkflowsSchema = v.strictObject({
1527
+ forEach: NonEmpty,
1528
+ definition: DefinitionRefSchema,
1529
+ with: v.optional(v.record(NonEmpty, ConditionSchema)),
1530
+ context: v.optional(v.record(NonEmpty, ConditionSchema)),
1531
+ onExit: v.optional(picklist([ "detach", "abort" ]))
1532
+ }), ActionParamSchema = v.pipe(v.strictObject({
1533
+ type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
1534
+ name: NonEmpty,
1535
+ title: v.optional(v.string()),
1536
+ description: v.optional(v.string()),
1537
+ required: v.optional(v.boolean()),
1538
+ options: v.optional(ChoiceOptionsSchema),
1539
+ validation: v.optional(ScalarValidationSchema)
1540
+ }), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
1541
+
1542
+ function semanticNamespace(semantic) {
1543
+ return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
1857
1544
  }
1858
1545
 
1859
- function assignmentCandidate(value) {
1860
- const parsed = v.safeParse(AssigneeShape, value);
1861
- return parsed.success ? parsed.output : void 0;
1546
+ function hasUniqueSemanticNamespaces(semantics) {
1547
+ const namespaces = semantics.map(semanticNamespace);
1548
+ return new Set(namespaces).size === namespaces.length;
1862
1549
  }
1863
1550
 
1864
- function assignmentValueIssues(args, value) {
1865
- const retained = previousAssignmentIdentities(args), issues = [];
1866
- return visitAssignmentEntries({
1867
- entryType: args.entryType,
1868
- value: value,
1869
- roles: args.roles,
1870
- fields: args.fields,
1871
- of: args.of,
1872
- path: "$"
1873
- }, ({assignee: assignee, roles: roles, path: path}) => {
1874
- consumeRetainedAssignment({
1875
- retained: retained,
1876
- path: path,
1877
- identity: assignmentIdentity(assignee)
1878
- }) || issues.push(...assignmentCandidateIssues({
1879
- roles: roles,
1880
- memberRoles: args.memberRoles,
1881
- roleAliases: args.roleAliases
1882
- }, assignee));
1883
- }), issues.length === 0 ? void 0 : issues;
1551
+ function semanticsFieldSchema(semantic) {
1552
+ return v.optional(v.pipe(v.array(semantic), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
1884
1553
  }
1885
1554
 
1886
- function consumeRetainedAssignment(args) {
1887
- const identities = args.retained.get(args.path), remaining = identities?.get(args.identity) ?? 0;
1888
- return remaining === 0 || identities === void 0 ? !1 : (remaining === 1 ? identities.delete(args.identity) : identities.set(args.identity, remaining - 1),
1889
- !0);
1555
+ const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
1556
+
1557
+ function actionFields(schemas) {
1558
+ const {op: op, group: group, effect: effect} = schemas;
1559
+ return {
1560
+ name: NonEmpty,
1561
+ semantics: ActionSemanticsFieldSchema,
1562
+ title: v.optional(v.string()),
1563
+ description: v.optional(v.string()),
1564
+ group: v.optional(group),
1565
+ when: v.optional(ConditionSchema),
1566
+ filter: v.optional(ConditionSchema),
1567
+ params: v.optional(v.array(ActionParamSchema)),
1568
+ ops: v.optional(v.array(op)),
1569
+ effects: v.optional(v.array(effect)),
1570
+ spawn: v.optional(SubworkflowsSchema)
1571
+ };
1890
1572
  }
1891
1573
 
1892
- function previousAssignmentIdentities(args) {
1893
- const retained = /* @__PURE__ */ new Map;
1894
- if (args.previousValue === void 0) return retained;
1895
- const schema = wholeValueSchema({
1896
- entryType: args.entryType,
1897
- shape: args,
1898
- leaf: valueSchemas
1899
- });
1900
- if (schema === void 0) return retained;
1901
- const previous = v.safeParse(schema, args.previousValue);
1902
- return previous.success && visitAssignmentEntries({
1903
- entryType: args.entryType,
1904
- value: previous.output,
1905
- roles: args.roles,
1906
- fields: args.fields,
1907
- of: args.of,
1908
- path: "$"
1909
- }, ({assignee: assignee, path: path}) => {
1910
- const identities = retained.get(path) ?? /* @__PURE__ */ new Map, identity = assignmentIdentity(assignee);
1911
- identities.set(identity, (identities.get(identity) ?? 0) + 1), retained.set(path, identities);
1912
- }), retained;
1574
+ const StoredActionSchema = pinned()(v.strictObject({
1575
+ ...actionFields({
1576
+ op: StoredOpSchema,
1577
+ group: StoredGroupMembershipSchema,
1578
+ effect: EffectSchema
1579
+ }),
1580
+ roles: v.optional(v.array(NonEmpty))
1581
+ })), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v.strictObject({
1582
+ ...actionFields({
1583
+ op: AuthoringOpSchema,
1584
+ group: AuthoringGroupMembershipSchema,
1585
+ effect: AuthoringEffectSchema
1586
+ }),
1587
+ roles: v.optional(v.array(NonEmpty)),
1588
+ status: v.optional(TerminalActivityStatus)
1589
+ })), AuthoringActionSchema = pinned()(RawAuthoringActionSchema), requirementBase = {
1590
+ name: NonEmpty,
1591
+ title: v.optional(v.string()),
1592
+ description: v.optional(v.string())
1593
+ }, GroqRequirementSchemaRaw = v.strictObject({
1594
+ ...requirementBase,
1595
+ type: v.literal("groq"),
1596
+ query: ConditionSchema
1597
+ }), SingleSubjectRequirementSchemaRaw = v.strictObject({
1598
+ ...requirementBase,
1599
+ type: v.literal("singleSubject")
1600
+ }), GroqRequirementSchema = pinned()(GroqRequirementSchemaRaw), StartRequirementSchema = pinned()(v.variant("type", [ GroqRequirementSchemaRaw, SingleSubjectRequirementSchemaRaw ]));
1601
+
1602
+ function activityFields({field: field, action: action, target: target, group: group}) {
1603
+ return {
1604
+ name: NonEmpty,
1605
+ semantics: SemanticsFieldSchema,
1606
+ title: v.optional(v.string()),
1607
+ description: v.optional(v.string()),
1608
+ groups: v.optional(v.array(GroupSchema)),
1609
+ group: v.optional(group),
1610
+ target: v.optional(target),
1611
+ filter: v.optional(ConditionSchema),
1612
+ requirements: v.optional(v.array(GroqRequirementSchema)),
1613
+ actions: v.optional(v.array(action)),
1614
+ fields: v.optional(v.array(field))
1615
+ };
1913
1616
  }
1914
1617
 
1915
- function visitAssignmentEntries(args, visit) {
1916
- if (assignmentKindAcceptsRoles(args.entryType)) {
1917
- visitAssignmentCandidates(args, visit);
1918
- return;
1919
- }
1920
- if (args.entryType === "object") {
1921
- visitAssignmentFields({
1922
- fields: args.fields,
1923
- value: args.value,
1924
- path: args.path
1925
- }, visit);
1926
- return;
1927
- }
1928
- if (!(args.entryType !== "array" || !Array.isArray(args.value))) for (const row of args.value) visitAssignmentFields({
1929
- fields: args.of,
1930
- value: row,
1931
- path: `${args.path}[]`
1932
- }, visit);
1933
- }
1618
+ const StoredActivitySchema = pinned()(v.strictObject(activityFields({
1619
+ field: FieldEntrySchema,
1620
+ action: StoredActionSchema,
1621
+ target: StoredManualTargetSchema,
1622
+ group: StoredGroupMembershipSchema
1623
+ }))), AuthoringActivitySchema = pinned()(v.strictObject(activityFields({
1624
+ field: AuthoringFieldEntrySchema,
1625
+ action: AuthoringActionSchema,
1626
+ target: AuthoringManualTargetSchema,
1627
+ group: AuthoringGroupMembershipSchema
1628
+ })));
1934
1629
 
1935
- function visitAssignmentCandidates(args, visit) {
1936
- if (args.roles === void 0) return;
1937
- const indexedValues = Array.isArray(args.value) ? args.value.map(value => ({
1938
- value: value,
1939
- path: args.path
1940
- })) : [ {
1941
- value: args.value,
1942
- path: args.path
1943
- } ];
1944
- for (const {value: value, path: path} of indexedValues) {
1945
- const assignee = assignmentCandidate(value);
1946
- assignee !== void 0 && visit({
1947
- assignee: assignee,
1948
- roles: args.roles,
1949
- path: path
1950
- });
1951
- }
1630
+ function transitionFields(when) {
1631
+ return {
1632
+ name: NonEmpty,
1633
+ title: v.optional(v.string()),
1634
+ description: v.optional(v.string()),
1635
+ to: NonEmpty,
1636
+ when: when
1637
+ };
1952
1638
  }
1953
1639
 
1954
- function visitAssignmentFields(args, visit) {
1955
- if (!(args.fields === void 0 || !isRecord(args.value))) for (const field of args.fields) visitAssignmentShape({
1956
- shape: field,
1957
- value: args.value[field.name],
1958
- path: `${args.path}.${field.name}`
1959
- }, visit);
1960
- }
1640
+ const StoredTransitionSchema = pinned()(v.strictObject(transitionFields(ConditionSchema))), AuthoringTransitionSchema = pinned()(v.strictObject(transitionFields(v.optional(ConditionSchema)))), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardActionsSchema = v.array(GuardActionSchema), NonEmptyGuardActionsSchema = v.pipe(GuardActionsSchema, v.minLength(1, GUARD_ACTIONS_REQUIRED_MESSAGE)), GuardReadPath = v.pipe(NonEmpty, v.check(path => !/[\r\n\u2028\u2029]/.test(path), "a guard read path cannot contain a line break")), GuardReadSchema = pinned()(v.variant("type", [ v.strictObject({
1641
+ type: v.literal("self")
1642
+ }), v.strictObject({
1643
+ type: v.literal("now")
1644
+ }), v.strictObject({
1645
+ type: v.literal("fieldRead"),
1646
+ field: FieldEntryName,
1647
+ path: v.optional(GuardReadPath)
1648
+ }), v.strictObject({
1649
+ type: v.literal("effectsRead"),
1650
+ effect: v.pipe(NonEmpty, v.check(name => !name.includes("'"), "an effect name cannot contain `'`")),
1651
+ path: v.optional(GuardReadPath)
1652
+ }) ]));
1961
1653
 
1962
- function visitAssignmentShape(args, visit) {
1963
- visitAssignmentEntries({
1964
- entryType: args.shape.type,
1965
- value: args.value,
1966
- roles: args.shape.roles,
1967
- fields: args.shape.fields,
1968
- of: args.shape.of,
1969
- path: args.path
1970
- }, visit);
1654
+ function guardMatchFields(read, actions) {
1655
+ return {
1656
+ types: v.optional(v.array(NonEmpty)),
1657
+ idRefs: v.optional(v.array(read)),
1658
+ idPatterns: v.optional(v.array(NonEmpty)),
1659
+ actions: actions
1660
+ };
1971
1661
  }
1972
1662
 
1973
- function assignmentCandidateIssues(args, assignee) {
1974
- if (assignee.type === "role") return collectiveRoleIssues(args, assignee.role);
1975
- const held = args.memberRoles?.[assignee.id];
1976
- return held === void 0 ? args.memberRoles === void 0 ? [] : [ `user "${assignee.id}" has no available project membership for role eligibility` ] : args.roles.some(required => actorFulfillsRole({
1977
- actorRoles: held,
1978
- required: required,
1979
- aliases: args.roleAliases
1980
- })) ? [] : [ `user "${assignee.id}" does not fulfill any eligible role: ${formatEligibleRoles(args.roles)}` ];
1663
+ function guardFields(read, actions) {
1664
+ return {
1665
+ name: NonEmpty,
1666
+ title: v.optional(v.string()),
1667
+ description: v.optional(v.string()),
1668
+ match: v.strictObject(guardMatchFields(read, actions)),
1669
+ predicate: v.optional(v.string()),
1670
+ metadata: v.optional(v.record(NonEmpty, read))
1671
+ };
1981
1672
  }
1982
1673
 
1983
- function collectiveRoleIssues(args, role) {
1984
- return args.roles.includes(role) ? [] : [ `collective role "${role}" is not eligible — this entry accepts ${formatEligibleRoles(args.roles)}` ];
1985
- }
1674
+ const GuardSchema = v.strictObject(guardFields(NonEmpty, GuardActionsSchema)), AuthoringGuardSchema = v.strictObject(guardFields(GuardReadSchema, NonEmptyGuardActionsSchema));
1986
1675
 
1987
- function formatEligibleRoles(roles) {
1988
- return roles.map(role => `"${role}"`).join(", ");
1676
+ function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
1677
+ return {
1678
+ name: NonEmpty,
1679
+ semantics: SemanticsFieldSchema,
1680
+ title: v.optional(v.string()),
1681
+ description: v.optional(v.string()),
1682
+ groups: v.optional(v.array(GroupSchema)),
1683
+ activities: v.optional(v.array(activity)),
1684
+ transitions: v.optional(v.array(transition)),
1685
+ guards: v.optional(v.array(guard)),
1686
+ fields: v.optional(v.array(field)),
1687
+ editable: v.optional(v.record(FieldEntryName, editable))
1688
+ };
1989
1689
  }
1990
1690
 
1991
- function issuesOf(check) {
1992
- return "issues" in check ? check.issues : void 0;
1993
- }
1691
+ const StoredStageSchema = pinned()(v.strictObject(stageFields({
1692
+ field: FieldEntrySchema,
1693
+ activity: StoredActivitySchema,
1694
+ transition: StoredTransitionSchema,
1695
+ guard: GuardSchema,
1696
+ editable: StoredEditableSchema
1697
+ }))), AuthoringStageSchema = pinned()(v.strictObject(stageFields({
1698
+ field: AuthoringFieldEntrySchema,
1699
+ activity: AuthoringActivitySchema,
1700
+ transition: AuthoringTransitionSchema,
1701
+ guard: AuthoringGuardSchema,
1702
+ editable: AuthoringEditableSchema
1703
+ }))), RoleAliasesSchema = pinned()(v.record(NonEmpty, v.pipe(v.array(NonEmpty), v.minLength(1, "a role alias must list at least one fulfilling role")))), WORKFLOW_LIFECYCLES = _exhaustiveOptions()([ "standalone", "child" ]), START_KINDS = _exhaustiveOptions()([ "interactive", "autonomous" ]);
1994
1704
 
1995
- function validateFieldValue(args) {
1996
- const check = checkValueAgainst(args, valueSchemas);
1997
- if ("issues" in check) throw new FieldValueShapeError({
1998
- entryType: args.entryType,
1999
- entryName: args.entryName,
2000
- issues: check.issues,
2001
- mode: "value"
2002
- });
2003
- return check.output;
1705
+ function startFields(kind) {
1706
+ return {
1707
+ kind: kind,
1708
+ filter: v.optional(ConditionSchema),
1709
+ requirements: v.optional(v.array(StartRequirementSchema))
1710
+ };
2004
1711
  }
2005
1712
 
2006
- const BARE_ID_SOURCE = "[A-Za-z0-9_][A-Za-z0-9._-]{0,127}", BARE_ID_RE = new RegExp(`^${BARE_ID_SOURCE}$`), ALIAS_REF_RE = new RegExp(`^@${RESOURCE_ALIAS_NAME_SOURCE}:${BARE_ID_SOURCE}$`);
1713
+ const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KINDS)))), AuthoringStartSchema = pinned()(v.strictObject(startFields(v.optional(picklist(START_KINDS)))));
2007
1714
 
2008
- function isBareDocumentId(id) {
2009
- return BARE_ID_RE.test(id) && !id.includes("..");
1715
+ function workflowFields({field: field, stage: stage, start: start}) {
1716
+ return {
1717
+ name: NonEmpty,
1718
+ semantics: SemanticsFieldSchema,
1719
+ title: NonEmpty,
1720
+ description: v.optional(v.string()),
1721
+ groups: v.optional(v.array(GroupSchema)),
1722
+ lifecycle: v.optional(picklist(WORKFLOW_LIFECYCLES)),
1723
+ start: v.optional(start),
1724
+ initialStage: NonEmpty,
1725
+ fields: v.optional(v.array(field)),
1726
+ stages: v.pipe(v.array(stage), v.minLength(1, "must declare at least one stage")),
1727
+ predicates: v.optional(v.record(groqIdentifier("`$<name>`"), ConditionSchema)),
1728
+ roleAliases: v.optional(RoleAliasesSchema)
1729
+ };
2010
1730
  }
2011
1731
 
2012
- function isAuthoringRefId(id) {
2013
- if (id.startsWith("@")) {
2014
- const separator = id.indexOf(":");
2015
- return ALIAS_REF_RE.test(id) && isBareDocumentId(id.slice(separator + 1));
2016
- }
2017
- return id.includes(":") ? isGdrUri(id) : isBareDocumentId(id);
2018
- }
1732
+ const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields({
1733
+ field: FieldEntrySchema,
1734
+ stage: StoredStageSchema,
1735
+ start: StoredStartSchema
1736
+ })));
2019
1737
 
2020
- function isBareSeedId(id) {
2021
- return isBareDocumentId(id);
1738
+ function parseStoredDefinition(input, label) {
1739
+ return parseOrThrow({
1740
+ schema: WorkflowDefinitionSchema,
1741
+ input: input,
1742
+ label: label
1743
+ });
2022
1744
  }
2023
1745
 
2024
- const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a bare document id, a GDR URI, or a portable `@<alias>:<id>` reference")), AuthoringGdrShape = v.looseObject({
2025
- id: AuthoringRefId,
2026
- type: NonEmptyString
2027
- }), seedValueSchemas = {
2028
- ...valueSchemas,
2029
- "doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
2030
- "doc.refs": v.array(AuthoringGdrShape),
2031
- subject: v.union([ v.null(), AuthoringGdrShape ]),
2032
- "release.ref": v.union([ v.null(), v.looseObject({
2033
- id: AuthoringRefId,
2034
- type: v.literal("system.release"),
2035
- releaseName: NonEmptyString
2036
- }) ])
2037
- };
1746
+ const AuthoringWorkflowSchema = pinned()(v.strictObject({
1747
+ ...workflowFields({
1748
+ field: AuthoringFieldEntrySchema,
1749
+ stage: AuthoringStageSchema,
1750
+ start: AuthoringStartSchema
1751
+ }),
1752
+ runtime: v.optional(RuntimeBlockSchema)
1753
+ })), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
2038
1754
 
2039
- function checkLiteralSeed(args) {
2040
- return args.value === null ? [ "a literal seed cannot be null — omit `initialValue` to start the field empty" ] : issuesOf(checkValueAgainst(args, seedValueSchemas));
1755
+ function isStartableDefinition(definition) {
1756
+ return definition.lifecycle !== "child";
2041
1757
  }
2042
1758
 
2043
- function validateFieldAppendItem(args) {
2044
- const schema = appendItemSchema(args.entryType, args);
2045
- if (schema === void 0) throw new FieldValueShapeError({
2046
- entryType: args.entryType,
2047
- entryName: args.entryName,
2048
- issues: [ `field entry type ${args.entryType} does not support append` ],
2049
- mode: "item"
2050
- });
2051
- const result = v.safeParse(schema, args.item);
2052
- if (!result.success) throw new FieldValueShapeError({
2053
- entryType: args.entryType,
2054
- entryName: args.entryName,
2055
- issues: formatIssues(result.issues),
2056
- mode: "item"
2057
- });
2058
- const assignmentArgs = {
2059
- entryType: appendAssignmentEntryType(args.entryType),
2060
- value: result.output,
2061
- roles: args.roles,
2062
- fields: args.entryType === "array" ? args.of : void 0,
2063
- memberRoles: args.memberRoles,
2064
- roleAliases: args.roleAliases
2065
- }, assignmentIssues = assignmentValueIssues(assignmentArgs, result.output);
2066
- if (assignmentIssues !== void 0) throw new FieldValueShapeError({
2067
- entryType: args.entryType,
2068
- entryName: args.entryName,
2069
- issues: assignmentIssues,
2070
- mode: "item"
2071
- });
2072
- const typeIssues = refTypeIssues({
2073
- entryType: args.entryType,
2074
- types: args.types,
2075
- value: [ args.item ]
2076
- });
2077
- if (typeIssues !== void 0) throw new FieldValueShapeError({
2078
- entryType: args.entryType,
2079
- entryName: args.entryName,
2080
- issues: typeIssues,
2081
- mode: "item"
2082
- });
1759
+ function startKindOf(definition) {
1760
+ return definition.start?.kind ?? "interactive";
1761
+ }
1762
+
1763
+ function isSubjectEntry(entry) {
1764
+ return entry.type === "subject";
1765
+ }
1766
+
1767
+ function isDueDateEntry(entry) {
1768
+ return entry.type === "dueDate" || entry.type === "dueDatetime";
1769
+ }
1770
+
1771
+ function isInputSourced(entry) {
1772
+ return entry.initialValue?.type === "input";
1773
+ }
1774
+
1775
+ function parseOrThrow({schema: schema, input: input, label: label}) {
1776
+ const result = v.safeParse(schema, input);
1777
+ if (!result.success) throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
2083
1778
  return result.output;
2084
1779
  }
2085
1780
 
2086
- function appendAssignmentEntryType(entryType) {
2087
- return entryType === "assignee" || entryType === "assignees" ? "assignee" : entryType === "array" ? "object" : entryType;
1781
+ function labelFor(fn, value) {
1782
+ if (value && typeof value == "object" && "name" in value) {
1783
+ const raw = value.name;
1784
+ if (typeof raw == "string") return `${fn}("${raw}")`;
1785
+ }
1786
+ return fn;
2088
1787
  }
2089
1788
 
2090
- function formatIssues(issues, formatMessage = issue => issue.message) {
2091
- const formatOne = (issue, prefix) => {
2092
- const keys = [ ...prefix, ...issue.path?.map(p => p.key) ?? [] ], sub = issue.issues?.filter(candidate => candidate.expected !== "null");
2093
- return sub !== void 0 && sub.length > 0 ? sub.flatMap(candidate => formatOne(candidate, keys)) : [ `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(issue)}` ];
1789
+ const NonEmptyString = v.pipe(v.string(), v.nonEmpty("must not be empty"));
1790
+
1791
+ function asPredicate(validate) {
1792
+ return value => {
1793
+ try {
1794
+ return validate(value), !0;
1795
+ } catch {
1796
+ return !1;
1797
+ }
2094
1798
  };
2095
- return issues.flatMap(issue => formatOne(issue, []));
2096
1799
  }
2097
1800
 
2098
- const NonEmpty = NonEmptyString, PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1801
+ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts);
2099
1802
 
2100
- function groqIdentifier(referencedAs) {
2101
- return v.pipe(v.string(), v.regex(GROQ_IDENTIFIER, `must be a GROQ-safe identifier (letters, digits, underscore; not starting with a digit) because it is referenced as ${referencedAs} in GROQ conditions`));
1803
+ function lakeSegment(label) {
1804
+ return v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, `invalid ${label} ${LAKE_ID_SEGMENT_GLOSS}`));
2102
1805
  }
2103
1806
 
2104
- function picklist(options) {
2105
- return v.picklist(options, invalidOptionMessage(options));
1807
+ const WorkflowResourceSchema = v.variant("type", [ v.object({
1808
+ type: v.literal("dataset"),
1809
+ id: v.pipe(NonEmptyString, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
1810
+ }), v.object({
1811
+ type: v.literal("canvas"),
1812
+ id: NonEmptyString
1813
+ }), v.object({
1814
+ type: v.literal("media-library"),
1815
+ id: NonEmptyString
1816
+ }), v.object({
1817
+ type: v.literal("dashboard"),
1818
+ id: NonEmptyString
1819
+ }) ]), ResourceBindingSchema = v.object({
1820
+ name: v.pipe(NonEmptyString, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
1821
+ resource: WorkflowResourceSchema
1822
+ }), DefinitionSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v.object({
1823
+ name: lakeSegment("name"),
1824
+ expectedMinReaderModel: v.optional(v.custom(() => !0), void 0),
1825
+ tag: lakeSegment("tag"),
1826
+ workflowResource: WorkflowResourceSchema,
1827
+ resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => duplicateHandleMessage(bindings) === void 0, issue => duplicateHandleMessage(issue.input) ?? "duplicate resource handle name"))),
1828
+ definitions: v.pipe(v.array(DefinitionSchema), v.minLength(1, "a deployment needs at least one definition")),
1829
+ runtime: v.optional(RuntimeBlockSchema)
1830
+ });
1831
+
1832
+ function firstDuplicatePair(items, keyOf) {
1833
+ const seen = /* @__PURE__ */ new Map;
1834
+ for (const item of items) {
1835
+ const key = keyOf(item), earlier = seen.get(key);
1836
+ if (earlier !== void 0) return [ earlier, item ];
1837
+ seen.set(key, item);
1838
+ }
2106
1839
  }
2107
1840
 
2108
- function invalidOptionMessage(options) {
2109
- return `Invalid option: expected one of ${options.map(option => `"${option}"`).join("|")}`;
1841
+ function duplicateHandleMessage(bindings) {
1842
+ const pair = firstDuplicatePair(bindings, binding => binding.name);
1843
+ if (pair !== void 0) return `duplicate resource handle name "${pair[1].name}" — each binding name must be unique within a deployment`;
2110
1844
  }
2111
1845
 
2112
- function pinned() {
2113
- return (schema, ..._exact) => schema;
1846
+ function duplicateNameMessage(deployments) {
1847
+ const pair = firstDuplicatePair(deployments, deployment => deployment.name);
1848
+ if (pair !== void 0) return `duplicate deployment name "${pair[1].name}" — each deployment must use a unique name`;
2114
1849
  }
2115
1850
 
2116
- const LiteralSchema = v.strictObject({
2117
- type: v.literal("literal"),
2118
- value: v.unknown()
2119
- }), FieldReadSchema = v.strictObject({
2120
- type: v.literal("fieldRead"),
2121
- scope: v.optional(v.union([ v.literal("workflow"), v.literal("stage") ])),
2122
- field: NonEmpty,
2123
- path: v.optional(v.string())
2124
- }), FieldSourceSchema = v.variant("type", [ v.strictObject({
2125
- type: v.literal("input")
2126
- }), v.strictObject({
2127
- type: v.literal("query"),
2128
- query: NonEmpty
2129
- }), LiteralSchema, FieldReadSchema ]), ValueExprSchema = v.lazy(() => v.variant("type", [ LiteralSchema, FieldReadSchema, v.strictObject({
2130
- type: v.literal("param"),
2131
- param: NonEmpty
2132
- }), v.strictObject({
2133
- type: v.literal("actor")
2134
- }), v.strictObject({
2135
- type: v.literal("now")
2136
- }), v.strictObject({
2137
- type: v.literal("self")
2138
- }), v.strictObject({
2139
- type: v.literal("stage")
2140
- }), v.strictObject({
2141
- type: v.literal("object"),
2142
- fields: v.record(NonEmpty, ValueExprSchema)
2143
- }) ])), StoredFieldRefSchema = v.strictObject({
2144
- scope: picklist(FIELD_SCOPES),
2145
- field: NonEmpty
2146
- }), AuthoringFieldRefSchema = v.strictObject({
2147
- scope: v.optional(picklist(FIELD_SCOPES)),
2148
- field: NonEmpty
2149
- }), HREF_SCHEMES = [ "http:", "https:" ];
1851
+ function partitionKey(deployment) {
1852
+ return `${resourceGdr(deployment.workflowResource)} ${deployment.tag}`;
1853
+ }
2150
1854
 
2151
- function isHttpUrl(value) {
2152
- try {
2153
- return HREF_SCHEMES.includes(new URL(value).protocol);
2154
- } catch {
2155
- return !1;
2156
- }
1855
+ function partitionCollisionMessage(deployments) {
1856
+ const pair = firstDuplicatePair(deployments, partitionKey);
1857
+ if (pair === void 0) return;
1858
+ const [first, second] = pair;
1859
+ return `deployments "${first.name}" and "${second.name}" share workflow resource + tag "${second.tag}" — both would write into the same partition; change one deployment’s tag or resource`;
2157
1860
  }
2158
1861
 
2159
- const UrlString = v.pipe(v.string(), v.url("must be a valid URL"), v.check(isHttpUrl, "must be an http(s) URL"));
1862
+ const TelemetryLoggerSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v.object({
1863
+ deployments: v.pipe(v.array(DeploymentSchema), v.minLength(1, "a config needs at least one deployment"), v.check(deployments => duplicateNameMessage(deployments) === void 0, issue => duplicateNameMessage(issue.input) ?? "duplicate deployment name"), v.check(deployments => partitionCollisionMessage(deployments) === void 0, issue => partitionCollisionMessage(issue.input) ?? "duplicate deployment partition")),
1864
+ telemetry: v.optional(TelemetryLoggerSchema)
1865
+ });
2160
1866
 
2161
- function manualTargetSchema(ref) {
2162
- return v.variant("type", [ v.strictObject({
2163
- type: v.literal("url"),
2164
- url: UrlString
2165
- }), v.strictObject({
2166
- type: v.literal("field"),
2167
- field: ref
2168
- }) ]);
1867
+ function resourceAliasesToMap(resourceAliases) {
1868
+ return Object.fromEntries((resourceAliases ?? []).map(binding => [ binding.name, binding.resource ]));
2169
1869
  }
2170
1870
 
2171
- const StoredManualTargetSchema = pinned()(manualTargetSchema(StoredFieldRefSchema)), AuthoringManualTargetSchema = pinned()(manualTargetSchema(v.union([ NonEmpty, AuthoringFieldRefSchema ]))), ConditionSchema = NonEmpty;
1871
+ const FIELD_READ = /^\$fields\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
2172
1872
 
2173
- function opSchemas(targetSchema) {
2174
- return [ v.strictObject({
2175
- type: v.literal("field.set"),
2176
- target: targetSchema,
2177
- value: ValueExprSchema
2178
- }), v.strictObject({
2179
- type: v.literal("field.setIfMissing"),
2180
- target: targetSchema,
2181
- value: ValueExprSchema
2182
- }), v.strictObject({
2183
- type: v.literal("field.unset"),
2184
- target: targetSchema
2185
- }), v.strictObject({
2186
- type: v.literal("field.append"),
2187
- target: targetSchema,
2188
- value: ValueExprSchema
2189
- }), v.strictObject({
2190
- type: v.literal("field.inc"),
2191
- target: targetSchema,
2192
- value: v.optional(ValueExprSchema)
2193
- }), v.strictObject({
2194
- type: v.literal("field.dec"),
2195
- target: targetSchema,
2196
- value: v.optional(ValueExprSchema)
2197
- }), v.strictObject({
2198
- type: v.literal("field.updateWhere"),
2199
- target: targetSchema,
2200
- where: ConditionSchema,
2201
- value: ValueExprSchema
2202
- }), v.strictObject({
2203
- type: v.literal("field.removeWhere"),
2204
- target: targetSchema,
2205
- where: ConditionSchema
2206
- }) ];
1873
+ function isGuardReadExpr(expr) {
1874
+ return expr === "$self" || expr === "$now" || FIELD_READ.test(expr) || EFFECTS_READ.test(expr);
2207
1875
  }
2208
1876
 
2209
- const StoredFieldOpSchema = pinned()(v.variant("type", [ ...opSchemas(StoredFieldRefSchema) ])), StoredOpSchema = pinned()(v.variant("type", [ ...opSchemas(StoredFieldRefSchema), v.strictObject({
2210
- type: v.literal("status.set"),
2211
- activity: NonEmpty,
2212
- status: picklist(ACTIVITY_STATUSES)
2213
- }) ])), AuditOpSchema = v.strictObject({
2214
- type: v.literal("audit"),
2215
- target: AuthoringFieldRefSchema,
2216
- value: ValueExprSchema,
2217
- stampFields: v.optional(v.strictObject({
2218
- actor: v.optional(NonEmpty),
2219
- at: v.optional(NonEmpty)
2220
- }))
2221
- }), AuthoringOpSchema = pinned()(v.variant("type", [ ...opSchemas(AuthoringFieldRefSchema), v.strictObject({
2222
- type: v.literal("status.set"),
2223
- activity: v.optional(NonEmpty),
2224
- status: picklist(ACTIVITY_STATUSES)
2225
- }), AuditOpSchema ])), GroupName = v.pipe(v.string(), v.regex(GROQ_IDENTIFIER, "must be an identifier (letters, digits, underscore; not starting with a digit)")), GroupSchema = pinned()(v.strictObject({
2226
- name: GroupName,
2227
- title: v.optional(v.string()),
2228
- description: v.optional(v.string()),
2229
- kind: v.optional(picklist(GROUP_KINDS))
2230
- })), GroupNameList = v.pipe(v.array(GroupName), v.minLength(1, "name at least one group, or omit `group`"), v.check(names => new Set(names).size === names.length, "a group is listed more than once — list each group once")), StoredGroupMembershipSchema = GroupNameList, AuthoringGroupMembershipSchema = v.union([ GroupName, GroupNameList ]);
1877
+ function printGuardRead(read) {
1878
+ switch (read.type) {
1879
+ case "self":
1880
+ return "$self";
2231
1881
 
2232
- function groupMembershipNames(group) {
2233
- return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
1882
+ case "now":
1883
+ return "$now";
1884
+
1885
+ case "fieldRead":
1886
+ return read.path === void 0 ? `$fields.${read.field}` : `$fields.${read.field}.${read.path}`;
1887
+
1888
+ case "effectsRead":
1889
+ return read.path === void 0 ? `$effects['${read.effect}']` : `$effects['${read.effect}'].${read.path}`;
1890
+ }
1891
+ }
1892
+
1893
+ function groq(strings, ...values) {
1894
+ return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
1895
+ }
1896
+
1897
+ function serializeGroqValue(value) {
1898
+ const serialized = JSON.stringify(value);
1899
+ if (serialized === void 0) throw new Error(`groq tag cannot serialize ${typeof value} — interpolate JSON-representable values only`);
1900
+ return serialized;
1901
+ }
1902
+
1903
+ function desugarWorkflow(authoring) {
1904
+ const issues = [];
1905
+ checkReservedRoleAliasKeys(authoring.roleAliases, issues);
1906
+ const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
1907
+ issues: issues,
1908
+ roleAliases: roleAliases,
1909
+ effectRuntime: /* @__PURE__ */ new Map
1910
+ }, workflowFields2 = desugarFieldEntries({
1911
+ entries: authoring.fields,
1912
+ path: [ "fields" ],
1913
+ ctx: ctx
1914
+ }), workflowLayer = layerOf(workflowFields2), stages = authoring.stages.map((stage, i) => {
1915
+ const path = [ "stages", i ], stageFields2 = desugarFieldEntries({
1916
+ entries: stage.fields,
1917
+ path: [ ...path, "fields" ],
1918
+ ctx: ctx
1919
+ }), stageEnv = {
1920
+ layers: [ {
1921
+ scope: "stage",
1922
+ entries: layerOf(stageFields2)
1923
+ }, {
1924
+ scope: "workflow",
1925
+ entries: workflowLayer
1926
+ } ]
1927
+ }, activities = (stage.activities ?? []).map((activity, j) => desugarActivity({
1928
+ activity: activity,
1929
+ path: [ ...path, "activities", j ],
1930
+ stageEnv: stageEnv,
1931
+ ctx: ctx
1932
+ })), transitions = (stage.transitions ?? []).map(transition => desugarTransition({
1933
+ transition: transition
1934
+ })), editable = desugarStageEditable({
1935
+ overrides: stage.editable,
1936
+ inScope: editableFieldNames({
1937
+ workflowFields: workflowFields2,
1938
+ stageFields: stageFields2,
1939
+ activities: activities
1940
+ }),
1941
+ path: [ ...path, "editable" ],
1942
+ ctx: ctx
1943
+ });
1944
+ return {
1945
+ ...stripUndefined({
1946
+ name: stage.name,
1947
+ semantics: stage.semantics,
1948
+ title: stage.title,
1949
+ description: stage.description,
1950
+ groups: stage.groups,
1951
+ guards: stage.guards?.map(desugarGuard)
1952
+ }),
1953
+ ...stageFields2 ? {
1954
+ fields: stageFields2
1955
+ } : {},
1956
+ ...activities.length > 0 ? {
1957
+ activities: activities
1958
+ } : {},
1959
+ ...transitions.length > 0 ? {
1960
+ transitions: transitions
1961
+ } : {},
1962
+ ...editable ? {
1963
+ editable: editable
1964
+ } : {}
1965
+ };
1966
+ });
1967
+ return {
1968
+ definition: {
1969
+ ...stripUndefined({
1970
+ name: authoring.name,
1971
+ semantics: authoring.semantics,
1972
+ title: authoring.title,
1973
+ description: authoring.description,
1974
+ groups: authoring.groups,
1975
+ lifecycle: authoring.lifecycle,
1976
+ start: desugarStart(authoring.start),
1977
+ initialStage: authoring.initialStage,
1978
+ predicates: authoring.predicates,
1979
+ roleAliases: roleAliases
1980
+ }),
1981
+ ...workflowFields2 ? {
1982
+ fields: workflowFields2
1983
+ } : {},
1984
+ stages: stages
1985
+ },
1986
+ runtime: collectRuntime({
1987
+ authoring: authoring,
1988
+ ctx: ctx
1989
+ }),
1990
+ issues: ctx.issues
1991
+ };
2234
1992
  }
2235
1993
 
2236
- const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "progress", "boolean", "date", "dueDate", "datetime", "dueDatetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), AUTHORING_FIELD_SUGAR_KINDS = _exhaustiveOptions()([ "todoList", "notes" ]), AUTHORING_FIELD_KINDS = [ ...FIELD_VALUE_KINDS, ...AUTHORING_FIELD_SUGAR_KINDS ], AuthoringRawFieldKindSchema = v.picklist(FIELD_VALUE_KINDS, issue => `${invalidOptionMessage(AUTHORING_FIELD_KINDS)} but received ${JSON.stringify(issue.input)}`), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v.pipe(v.number(), v.finite("must be finite")), ScalarValidationSchema = v.pipe(v.strictObject({
2237
- min: v.optional(FiniteNumber),
2238
- max: v.optional(FiniteNumber)
2239
- }), v.check(validation => validation.min !== void 0 || validation.max !== void 0, "declare at least one bound, or omit `validation`"), v.check(validation => validation.min === void 0 || validation.max === void 0 || validation.min <= validation.max, "`min` must be less than or equal to `max`")), ChoiceOptionsSchema = v.strictObject({
2240
- list: v.pipe(v.array(v.strictObject({
2241
- title: NonEmpty,
2242
- value: v.union([ v.string(), v.number() ])
2243
- })), v.minLength(1, "declare at least one choice, or omit `options`"))
2244
- });
2245
-
2246
- function asShape(input) {
2247
- return typeof input == "object" && input !== null ? input : {};
1994
+ function collectRuntime(args) {
1995
+ const {kind: kind} = args.authoring.runtime ?? {}, effects = Object.fromEntries(args.ctx.effectRuntime), declared = {
1996
+ ...kind === void 0 ? {} : {
1997
+ kind: kind
1998
+ },
1999
+ ...Object.keys(effects).length === 0 ? {} : {
2000
+ effects: effects
2001
+ }
2002
+ };
2003
+ return Object.keys(declared).length === 0 ? void 0 : declared;
2004
+ }
2005
+
2006
+ function desugarEffect(args) {
2007
+ const {runtime: runtime, ...effect} = args.effect;
2008
+ runtime !== void 0 && args.ctx.effectRuntime.set(effect.name, runtime);
2009
+ const {retry: retry} = effect;
2010
+ return retry === void 0 ? effect : {
2011
+ ...effect,
2012
+ retry: {
2013
+ ...retry,
2014
+ kind: retry.kind ?? "engine"
2015
+ }
2016
+ };
2248
2017
  }
2249
2018
 
2250
- function compositeShapeOk(input) {
2251
- const shape = asShape(input);
2252
- return shape.type === "object" ? Array.isArray(shape.fields) && shape.fields.length > 0 && shape.of === void 0 : shape.type === "array" ? Array.isArray(shape.of) && shape.of.length > 0 && shape.fields === void 0 : shape.fields === void 0 && shape.of === void 0;
2019
+ function desugarStart(start) {
2020
+ if (start !== void 0) return {
2021
+ kind: start.kind ?? "interactive",
2022
+ ...start.filter !== void 0 ? {
2023
+ filter: start.filter
2024
+ } : {},
2025
+ ...start.requirements !== void 0 ? {
2026
+ requirements: start.requirements
2027
+ } : {}
2028
+ };
2253
2029
  }
2254
2030
 
2255
- function compositeShapeMessage(input) {
2256
- const shape = asShape(input);
2257
- return shape.type === "object" ? shape.of !== void 0 ? "an `object` kind declares its sub-fields with `fields`, not `of`" : "an `object` kind needs a non-empty `fields` list of sub-field shapes" : shape.type === "array" ? shape.fields !== void 0 ? "an `array` kind declares its item shape with `of`, not `fields`" : "an `array` kind needs a non-empty `of` list of sub-field shapes" : `\`fields\` / \`of\` are only valid on the \`object\` / \`array\` kinds, not "${String(shape.type)}"`;
2031
+ function checkReservedRoleAliasKeys(aliases, issues) {
2032
+ for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
2033
+ path: [ "roleAliases", key ],
2034
+ 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.`
2035
+ });
2258
2036
  }
2259
2037
 
2260
- function duplicateSubfieldName(input) {
2261
- const shape = asShape(input);
2262
- let list = [];
2263
- Array.isArray(shape.fields) ? list = shape.fields : Array.isArray(shape.of) && (list = shape.of);
2264
- const seen = /* @__PURE__ */ new Set;
2265
- for (const item of list) {
2266
- const name = asShape(item).name;
2267
- if (typeof name == "string") {
2268
- if (seen.has(name)) return name;
2269
- seen.add(name);
2270
- }
2271
- }
2272
- }
2038
+ const TODOLIST_OF = [ {
2039
+ type: "string",
2040
+ name: "label",
2041
+ title: "Label"
2042
+ }, {
2043
+ type: "string",
2044
+ name: "status",
2045
+ title: "Status"
2046
+ }, {
2047
+ type: "assignee",
2048
+ name: "assignee",
2049
+ title: "Assignee"
2050
+ }, {
2051
+ type: "date",
2052
+ name: "dueDate",
2053
+ title: "Due date"
2054
+ } ], NOTES_OF = [ {
2055
+ type: "text",
2056
+ name: "body",
2057
+ title: "Body"
2058
+ }, {
2059
+ type: "actor",
2060
+ name: "actor",
2061
+ title: "Actor"
2062
+ }, {
2063
+ type: "datetime",
2064
+ name: "at",
2065
+ title: "At"
2066
+ } ];
2273
2067
 
2274
- function compositeChecked(entries) {
2275
- return v.pipe(v.strictObject(entries), v.check(input => compositeShapeOk(input), issue => compositeShapeMessage(issue.input)), v.check(input => duplicateSubfieldName(input) === void 0, issue => `duplicate sub-field name "${duplicateSubfieldName(issue.input)}" sub-field names must be unique within \`fields\` / \`of\``));
2068
+ function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
2069
+ return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
2070
+ entry: entry,
2071
+ path: [ ...path, i ],
2072
+ ctx: ctx
2073
+ }));
2276
2074
  }
2277
2075
 
2278
- const AssignmentRolesSchema = v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one eligible role, or omit `roles`")), FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
2279
- type: FieldValueKindSchema,
2280
- name: FieldEntryName,
2281
- title: v.optional(v.string()),
2282
- description: v.optional(v.string()),
2283
- options: v.optional(ChoiceOptionsSchema),
2284
- validation: v.optional(ScalarValidationSchema),
2285
- roles: v.optional(AssignmentRolesSchema),
2286
- fields: v.optional(v.array(FieldShapeSchema)),
2287
- of: v.optional(v.array(FieldShapeSchema))
2288
- }), choiceOptionsCheck(), scalarValidationCheck(), assignmentRolesCheck())), StoredEditableSchema = pinned()(v.union([ v.literal(!0), NonEmpty ])), AuthoringEditableSchema = pinned()(v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]));
2289
-
2290
- function fieldBase(editable, group) {
2076
+ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
2077
+ if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
2078
+ entry: entry,
2079
+ path: path,
2080
+ ctx: ctx
2081
+ });
2082
+ const editable = normalizeEditable({
2083
+ editable: entry.editable,
2084
+ path: [ ...path, "editable" ],
2085
+ ctx: ctx
2086
+ });
2291
2087
  return {
2292
- name: FieldEntryName,
2293
- title: v.optional(v.string()),
2294
- description: v.optional(v.string()),
2295
- group: v.optional(group),
2296
- required: v.optional(v.boolean()),
2297
- initialValue: v.optional(FieldSourceSchema),
2298
- editable: v.optional(editable)
2088
+ ...stripUndefined({
2089
+ type: entry.type,
2090
+ name: entry.name,
2091
+ title: entry.title,
2092
+ description: entry.description,
2093
+ group: normalizeGroup(entry.group),
2094
+ required: entry.required,
2095
+ initialValue: entry.initialValue,
2096
+ editable: editable,
2097
+ options: entry.options,
2098
+ validation: entry.validation,
2099
+ types: entry.types,
2100
+ roles: entry.roles,
2101
+ fields: entry.fields,
2102
+ of: entry.of
2103
+ })
2299
2104
  };
2300
2105
  }
2301
2106
 
2302
- function fieldEntryFields({editable: editable, group: group, kind: kind = FieldKindSchema}) {
2107
+ function desugarListField({entry: entry, path: path, ctx: ctx}) {
2108
+ const editable = normalizeEditable({
2109
+ editable: entry.editable,
2110
+ path: [ ...path, "editable" ],
2111
+ ctx: ctx
2112
+ });
2303
2113
  return {
2304
- type: kind,
2305
- ...fieldBase(editable, group),
2306
- options: v.optional(ChoiceOptionsSchema),
2307
- validation: v.optional(ScalarValidationSchema),
2308
- types: v.optional(v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
2309
- roles: v.optional(AssignmentRolesSchema),
2310
- fields: v.optional(v.array(FieldShapeSchema)),
2311
- of: v.optional(v.array(FieldShapeSchema))
2114
+ ...stripUndefined({
2115
+ name: entry.name,
2116
+ title: entry.title,
2117
+ description: entry.description,
2118
+ group: normalizeGroup(entry.group),
2119
+ required: entry.required,
2120
+ initialValue: entry.initialValue,
2121
+ editable: editable
2122
+ }),
2123
+ type: "array",
2124
+ of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
2312
2125
  };
2313
2126
  }
2314
2127
 
2315
- function literalSeedIssues(entry) {
2316
- if (entry.initialValue?.type === "literal") return checkLiteralSeed({
2317
- entryType: entry.type,
2318
- value: entry.initialValue.value,
2319
- types: entry.types,
2320
- fields: entry.fields,
2321
- of: entry.of,
2322
- options: entry.options,
2323
- validation: entry.validation,
2324
- roles: entry.roles
2325
- });
2128
+ function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
2129
+ if (editable === void 0 || editable === !0) return editable;
2130
+ if (Array.isArray(editable)) {
2131
+ const condition = rolesCondition(editable, ctx.roleAliases);
2132
+ if (condition === void 0) {
2133
+ ctx.issues.push({
2134
+ path: path,
2135
+ message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
2136
+ });
2137
+ return;
2138
+ }
2139
+ return condition;
2140
+ }
2141
+ return editable;
2142
+ }
2143
+
2144
+ function editableFieldNames({workflowFields: workflowFields2, stageFields: stageFields2, activities: activities}) {
2145
+ const names = /* @__PURE__ */ new Set, collect = entries => {
2146
+ for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
2147
+ };
2148
+ collect(workflowFields2), collect(stageFields2);
2149
+ for (const activity of activities) collect(activity.fields);
2150
+ return names;
2326
2151
  }
2327
2152
 
2328
- function literalSeedCheck() {
2329
- return v.check(entry => literalSeedIssues(entry) === void 0, issue => `initialValue literal does not fit the declared kind: ${(literalSeedIssues(issue.input) ?? []).join("; ")}`);
2153
+ function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
2154
+ if (overrides === void 0) return;
2155
+ const out = {};
2156
+ for (const [name, value] of Object.entries(overrides)) {
2157
+ if (!inScope.has(name)) {
2158
+ ctx.issues.push({
2159
+ path: [ ...path, name ],
2160
+ 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\``
2161
+ });
2162
+ continue;
2163
+ }
2164
+ const normalized = normalizeEditable({
2165
+ editable: value,
2166
+ path: [ ...path, name ],
2167
+ ctx: ctx
2168
+ });
2169
+ normalized !== void 0 && (out[name] = normalized);
2170
+ }
2171
+ return Object.keys(out).length > 0 ? out : void 0;
2330
2172
  }
2331
2173
 
2332
- function refTypesCheck() {
2333
- return v.check(entry => entry.types === void 0 || refKindAcceptsTypes(entry.type), issue => `\`types\` is only valid on \`doc.ref\` / \`doc.refs\` / \`subject\` entries, not "${issue.input.type}"`);
2174
+ function layerOf(entries) {
2175
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
2334
2176
  }
2335
2177
 
2336
- function assignmentRolesCheck() {
2337
- return v.check(entry => entry.roles === void 0 || assignmentKindAcceptsRoles(entry.type), issue => `\`roles\` is only valid on \`assignee\` / \`assignees\` entries, not "${issue.input.type}"`);
2178
+ function normalizeGroup(group) {
2179
+ return group === void 0 || Array.isArray(group) ? group : [ group ];
2338
2180
  }
2339
2181
 
2340
- function choiceOptionsCheck() {
2341
- return v.check(entry => checkChoiceList({
2342
- entryType: entry.type,
2343
- options: entry.options,
2344
- validation: entry.validation
2345
- }) === void 0, issue => (checkChoiceList({
2346
- entryType: issue.input.type,
2347
- options: issue.input.options,
2348
- validation: issue.input.validation
2349
- }) ?? []).join("; "));
2182
+ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
2183
+ const activityFields2 = desugarFieldEntries({
2184
+ entries: activity.fields,
2185
+ path: [ ...path, "fields" ],
2186
+ ctx: ctx
2187
+ }), env = {
2188
+ layers: [ {
2189
+ scope: "activity",
2190
+ entries: layerOf(activityFields2)
2191
+ }, ...stageEnv.layers ]
2192
+ }, actions = (activity.actions ?? []).map((action, a) => desugarAction({
2193
+ action: action,
2194
+ path: [ ...path, "actions", a ],
2195
+ env: env,
2196
+ activityName: activity.name,
2197
+ ctx: ctx
2198
+ })), target = desugarTarget({
2199
+ target: activity.target,
2200
+ env: env,
2201
+ path: [ ...path, "target" ],
2202
+ ctx: ctx
2203
+ });
2204
+ return {
2205
+ ...stripUndefined({
2206
+ name: activity.name,
2207
+ semantics: activity.semantics,
2208
+ title: activity.title,
2209
+ description: activity.description,
2210
+ groups: activity.groups,
2211
+ group: normalizeGroup(activity.group),
2212
+ filter: activity.filter,
2213
+ requirements: activity.requirements
2214
+ }),
2215
+ ...target ? {
2216
+ target: target
2217
+ } : {},
2218
+ ...activityFields2 ? {
2219
+ fields: activityFields2
2220
+ } : {},
2221
+ ...actions.length > 0 ? {
2222
+ actions: actions
2223
+ } : {}
2224
+ };
2350
2225
  }
2351
2226
 
2352
- const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "progress" ]);
2227
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref" ];
2353
2228
 
2354
- function scalarValidationCheck() {
2355
- return v.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
2229
+ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
2230
+ if (target === void 0 || target.type === "url") return target;
2231
+ const ref = typeof target.field == "string" ? {
2232
+ field: target.field
2233
+ } : target.field, field = resolveRef({
2234
+ ref: ref,
2235
+ env: env,
2236
+ path: [ ...path, "field" ],
2237
+ ctx: ctx
2238
+ }) ?? fallbackRef(ref), entry = entryAt(env, field);
2239
+ return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
2240
+ path: [ ...path, "field" ],
2241
+ message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
2242
+ }), {
2243
+ type: "field",
2244
+ field: field
2245
+ };
2356
2246
  }
2357
2247
 
2358
- function scalarValidationDeclarationIssues(entry) {
2359
- const {type: type, validation: validation} = entry;
2360
- if (validation === void 0) return;
2361
- if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` / \`progress\` values, not "${type}"` ];
2362
- if (type === "progress") {
2363
- const issues2 = Object.entries(validation).flatMap(([bound, value]) => typeof value == "number" && value >= 0 && value <= 100 ? [] : [ `\`validation.${bound}\` must stay within the progress kind's 0–100 contract` ]);
2364
- return issues2.length === 0 ? void 0 : issues2;
2365
- }
2366
- if (type === "number") return;
2367
- const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
2368
- return issues.length === 0 ? void 0 : issues;
2248
+ function reportEmptyActionRoles({action: action, path: path, ctx: ctx}) {
2249
+ action.roles === void 0 || action.roles.length > 0 || ctx.issues.push({
2250
+ path: [ ...path, "roles" ],
2251
+ message: "roles: [] names no roles omit it to allow any identity, or list at least one role"
2252
+ });
2369
2253
  }
2370
2254
 
2371
- const FieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
2372
- editable: StoredEditableSchema,
2373
- group: StoredGroupMembershipSchema
2374
- })), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
2375
- editable: AuthoringEditableSchema,
2376
- group: AuthoringGroupMembershipSchema,
2377
- kind: AuthoringRawFieldKindSchema
2378
- })), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck()));
2255
+ function desugarActionOps(args) {
2256
+ const {action: action, path: path, env: env, activityName: activityName, ctx: ctx} = args, ops = desugarOps({
2257
+ ops: action.ops,
2258
+ path: [ ...path, "ops" ],
2259
+ env: env,
2260
+ firingActivity: activityName,
2261
+ ctx: ctx
2262
+ }) ?? [];
2263
+ return action.status !== void 0 && ops.push({
2264
+ type: "status.set",
2265
+ activity: activityName,
2266
+ status: action.status
2267
+ }), ops;
2268
+ }
2379
2269
 
2380
- function listSugarFields(type) {
2270
+ function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
2271
+ reportEmptyActionRoles({
2272
+ action: action,
2273
+ path: path,
2274
+ ctx: ctx
2275
+ });
2276
+ const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarActionOps({
2277
+ action: action,
2278
+ path: path,
2279
+ env: env,
2280
+ activityName: activityName,
2281
+ ctx: ctx
2282
+ });
2381
2283
  return {
2382
- type: v.literal(type),
2383
- ...fieldBase(AuthoringEditableSchema, AuthoringGroupMembershipSchema)
2284
+ ...stripUndefined({
2285
+ name: action.name,
2286
+ semantics: action.semantics,
2287
+ title: action.title,
2288
+ description: action.description,
2289
+ group: normalizeGroup(action.group),
2290
+ when: action.when,
2291
+ params: action.params,
2292
+ effects: action.effects?.map(effect => desugarEffect({
2293
+ effect: effect,
2294
+ ctx: ctx
2295
+ })),
2296
+ spawn: action.spawn
2297
+ }),
2298
+ ...cascadeFired && action.roles !== void 0 && action.roles.length > 0 ? {
2299
+ roles: action.roles
2300
+ } : {},
2301
+ ...filter ? {
2302
+ filter: filter
2303
+ } : {},
2304
+ ...ops.length > 0 ? {
2305
+ ops: ops
2306
+ } : {}
2384
2307
  };
2385
2308
  }
2386
2309
 
2387
- const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))), NotesFieldSchema = pinned()(v.strictObject(listSugarFields("notes"))), AuthoringFieldEntrySchema = pinned()(v.lazy(input => {
2388
- const type = asShape(input).type;
2389
- return type === "todoList" ? TodoListFieldSchema : type === "notes" ? NotesFieldSchema : RawAuthoringFieldEntrySchema;
2390
- })), EffectSchema = v.strictObject({
2391
- name: NonEmpty,
2392
- title: v.optional(v.string()),
2393
- description: v.optional(v.string()),
2394
- bindings: v.optional(v.record(v.string(), ConditionSchema)),
2395
- input: v.optional(v.record(v.string(), v.unknown())),
2396
- outputs: v.optional(v.array(FieldShapeSchema))
2397
- }), DefinitionRefSchema = v.strictObject({
2398
- name: NonEmpty,
2399
- version: v.optional(v.union([ PositiveInt, v.literal("latest") ]))
2400
- }), SubworkflowsSchema = v.strictObject({
2401
- forEach: NonEmpty,
2402
- definition: DefinitionRefSchema,
2403
- with: v.optional(v.record(NonEmpty, ConditionSchema)),
2404
- context: v.optional(v.record(NonEmpty, ConditionSchema)),
2405
- onExit: v.optional(picklist([ "detach", "abort" ]))
2406
- }), ActionParamSchema = v.pipe(v.strictObject({
2407
- type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
2408
- name: NonEmpty,
2409
- title: v.optional(v.string()),
2410
- description: v.optional(v.string()),
2411
- required: v.optional(v.boolean()),
2412
- options: v.optional(ChoiceOptionsSchema),
2413
- validation: v.optional(ScalarValidationSchema)
2414
- }), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
2310
+ const DEFAULT_TRANSITION_WHEN = "$allActivitiesDone";
2415
2311
 
2416
- function semanticNamespace(semantic) {
2417
- return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
2312
+ function desugarTransition({transition: transition}) {
2313
+ return {
2314
+ ...stripUndefined({
2315
+ name: transition.name,
2316
+ title: transition.title,
2317
+ description: transition.description,
2318
+ to: transition.to
2319
+ }),
2320
+ when: transition.when ?? DEFAULT_TRANSITION_WHEN
2321
+ };
2418
2322
  }
2419
2323
 
2420
- function hasUniqueSemanticNamespaces(semantics) {
2421
- const namespaces = semantics.map(semanticNamespace);
2422
- return new Set(namespaces).size === namespaces.length;
2324
+ function desugarGuard(guard) {
2325
+ const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
2326
+ return {
2327
+ ...rest,
2328
+ match: {
2329
+ ...matchRest,
2330
+ ...idRefs !== void 0 ? {
2331
+ idRefs: idRefs.map(printGuardRead)
2332
+ } : {}
2333
+ },
2334
+ ...metadata !== void 0 ? {
2335
+ metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
2336
+ } : {}
2337
+ };
2423
2338
  }
2424
2339
 
2425
- function semanticsFieldSchema(semantic) {
2426
- return v.optional(v.pipe(v.array(semantic), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
2340
+ function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
2341
+ if (ops) return ops.map((op, i) => desugarOp({
2342
+ op: op,
2343
+ path: [ ...path, i ],
2344
+ env: env,
2345
+ firingActivity: firingActivity,
2346
+ ctx: ctx
2347
+ }));
2348
+ }
2349
+
2350
+ function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
2351
+ if (op.type === "status.set") return {
2352
+ type: "status.set",
2353
+ activity: op.activity ?? firingActivity,
2354
+ status: op.status
2355
+ };
2356
+ if (op.type === "audit") return desugarAuditOp({
2357
+ op: op,
2358
+ path: path,
2359
+ env: env,
2360
+ ctx: ctx
2361
+ });
2362
+ const target = resolveRef({
2363
+ ref: op.target,
2364
+ env: env,
2365
+ path: [ ...path, "target" ],
2366
+ ctx: ctx
2367
+ }) ?? fallbackRef(op.target);
2368
+ return {
2369
+ ...op,
2370
+ target: target
2371
+ };
2427
2372
  }
2428
2373
 
2429
- const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
2374
+ const AUDIT_STAMPS = {
2375
+ actor: {
2376
+ type: "actor"
2377
+ },
2378
+ at: {
2379
+ type: "now"
2380
+ }
2381
+ };
2430
2382
 
2431
- function actionFields(op, group) {
2383
+ function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
2384
+ const target = resolveRef({
2385
+ ref: op.target,
2386
+ env: env,
2387
+ path: [ ...path, "target" ],
2388
+ ctx: ctx
2389
+ }) ?? fallbackRef(op.target);
2390
+ if (op.value.type !== "object") return ctx.issues.push({
2391
+ path: [ ...path, "value" ],
2392
+ message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
2393
+ }), {
2394
+ type: "field.append",
2395
+ target: target,
2396
+ value: op.value
2397
+ };
2398
+ const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
2399
+ ...op.value.fields
2400
+ };
2401
+ for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
2402
+ if (stamp in fields) {
2403
+ ctx.issues.push({
2404
+ path: [ ...path, "value", "fields", stamp ],
2405
+ message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
2406
+ });
2407
+ continue;
2408
+ }
2409
+ fields[stamp] = source;
2410
+ }
2432
2411
  return {
2433
- name: NonEmpty,
2434
- semantics: ActionSemanticsFieldSchema,
2435
- title: v.optional(v.string()),
2436
- description: v.optional(v.string()),
2437
- group: v.optional(group),
2438
- when: v.optional(ConditionSchema),
2439
- filter: v.optional(ConditionSchema),
2440
- params: v.optional(v.array(ActionParamSchema)),
2441
- ops: v.optional(v.array(op)),
2442
- effects: v.optional(v.array(EffectSchema)),
2443
- spawn: v.optional(SubworkflowsSchema)
2412
+ type: "field.append",
2413
+ target: target,
2414
+ value: {
2415
+ type: "object",
2416
+ fields: fields
2417
+ }
2444
2418
  };
2445
2419
  }
2446
2420
 
2447
- const StoredActionSchema = pinned()(v.strictObject({
2448
- ...actionFields(StoredOpSchema, StoredGroupMembershipSchema),
2449
- roles: v.optional(v.array(NonEmpty))
2450
- })), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v.strictObject({
2451
- ...actionFields(AuthoringOpSchema, AuthoringGroupMembershipSchema),
2452
- roles: v.optional(v.array(NonEmpty)),
2453
- status: v.optional(TerminalActivityStatus)
2454
- })), AuthoringActionSchema = pinned()(RawAuthoringActionSchema), requirementBase = {
2455
- name: NonEmpty,
2456
- title: v.optional(v.string()),
2457
- description: v.optional(v.string())
2458
- }, GroqRequirementSchemaRaw = v.strictObject({
2459
- ...requirementBase,
2460
- type: v.literal("groq"),
2461
- query: ConditionSchema
2462
- }), SingleSubjectRequirementSchemaRaw = v.strictObject({
2463
- ...requirementBase,
2464
- type: v.literal("singleSubject")
2465
- }), GroqRequirementSchema = pinned()(GroqRequirementSchemaRaw), StartRequirementSchema = pinned()(v.variant("type", [ GroqRequirementSchemaRaw, SingleSubjectRequirementSchemaRaw ]));
2466
-
2467
- function activityFields({field: field, action: action, target: target, group: group}) {
2468
- return {
2469
- name: NonEmpty,
2470
- semantics: SemanticsFieldSchema,
2471
- title: v.optional(v.string()),
2472
- description: v.optional(v.string()),
2473
- groups: v.optional(v.array(GroupSchema)),
2474
- group: v.optional(group),
2475
- target: v.optional(target),
2476
- filter: v.optional(ConditionSchema),
2477
- requirements: v.optional(v.array(GroqRequirementSchema)),
2478
- actions: v.optional(v.array(action)),
2479
- fields: v.optional(v.array(field))
2421
+ function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
2422
+ const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
2423
+ for (const layer of layers) if (layer.entries.has(ref.field)) return {
2424
+ scope: layer.scope,
2425
+ field: ref.field
2480
2426
  };
2427
+ const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
2428
+ ctx.issues.push({
2429
+ path: path,
2430
+ message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
2431
+ });
2481
2432
  }
2482
2433
 
2483
- const StoredActivitySchema = pinned()(v.strictObject(activityFields({
2484
- field: FieldEntrySchema,
2485
- action: StoredActionSchema,
2486
- target: StoredManualTargetSchema,
2487
- group: StoredGroupMembershipSchema
2488
- }))), AuthoringActivitySchema = pinned()(v.strictObject(activityFields({
2489
- field: AuthoringFieldEntrySchema,
2490
- action: AuthoringActionSchema,
2491
- target: AuthoringManualTargetSchema,
2492
- group: AuthoringGroupMembershipSchema
2493
- })));
2434
+ function entryAt(env, ref) {
2435
+ return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
2436
+ }
2494
2437
 
2495
- function transitionFields(when) {
2438
+ function fallbackRef(ref) {
2496
2439
  return {
2497
- name: NonEmpty,
2498
- title: v.optional(v.string()),
2499
- description: v.optional(v.string()),
2500
- to: NonEmpty,
2501
- when: when
2440
+ scope: ref.scope ?? "workflow",
2441
+ field: ref.field
2502
2442
  };
2503
2443
  }
2504
2444
 
2505
- const StoredTransitionSchema = pinned()(v.strictObject(transitionFields(ConditionSchema))), AuthoringTransitionSchema = pinned()(v.strictObject(transitionFields(v.optional(ConditionSchema)))), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardActionsSchema = v.array(GuardActionSchema), NonEmptyGuardActionsSchema = v.pipe(GuardActionsSchema, v.minLength(1, GUARD_ACTIONS_REQUIRED_MESSAGE)), GuardReadPath = v.pipe(NonEmpty, v.check(path => !/[\r\n\u2028\u2029]/.test(path), "a guard read path cannot contain a line break")), GuardReadSchema = pinned()(v.variant("type", [ v.strictObject({
2506
- type: v.literal("self")
2507
- }), v.strictObject({
2508
- type: v.literal("now")
2509
- }), v.strictObject({
2510
- type: v.literal("fieldRead"),
2511
- field: FieldEntryName,
2512
- path: v.optional(GuardReadPath)
2513
- }), v.strictObject({
2514
- type: v.literal("effectsRead"),
2515
- effect: v.pipe(NonEmpty, v.check(name => !name.includes("'"), "an effect name cannot contain `'`")),
2516
- path: v.optional(GuardReadPath)
2517
- }) ]));
2445
+ function rolesCondition(roles, aliases) {
2446
+ if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
2447
+ }
2518
2448
 
2519
- function guardMatchFields(read, actions) {
2520
- return {
2521
- types: v.optional(v.array(NonEmpty)),
2522
- idRefs: v.optional(v.array(read)),
2523
- idPatterns: v.optional(v.array(NonEmpty)),
2524
- actions: actions
2525
- };
2449
+ function stripUndefined(obj) {
2450
+ const out = {};
2451
+ for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
2452
+ return out;
2526
2453
  }
2527
2454
 
2528
- function guardFields(read, actions) {
2529
- return {
2530
- name: NonEmpty,
2531
- title: v.optional(v.string()),
2532
- description: v.optional(v.string()),
2533
- match: v.strictObject(guardMatchFields(read, actions)),
2534
- predicate: v.optional(v.string()),
2535
- metadata: v.optional(v.record(NonEmpty, read))
2536
- };
2455
+ function isUnevaluable(result) {
2456
+ return result == null;
2537
2457
  }
2538
2458
 
2539
- const GuardSchema = v.strictObject(guardFields(NonEmpty, GuardActionsSchema)), AuthoringGuardSchema = v.strictObject(guardFields(GuardReadSchema, NonEmptyGuardActionsSchema));
2459
+ async function evaluateConditionOutcome(args) {
2460
+ const {condition: condition, snapshot: snapshot, params: params} = args;
2461
+ return evaluateConditionOutcome$1({
2462
+ condition: condition,
2463
+ params: params,
2464
+ dataset: snapshot.docs
2465
+ });
2466
+ }
2540
2467
 
2541
- function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
2542
- return {
2543
- name: NonEmpty,
2544
- semantics: SemanticsFieldSchema,
2545
- title: v.optional(v.string()),
2546
- description: v.optional(v.string()),
2547
- groups: v.optional(v.array(GroupSchema)),
2548
- activities: v.optional(v.array(activity)),
2549
- transitions: v.optional(v.array(transition)),
2550
- guards: v.optional(v.array(guard)),
2551
- fields: v.optional(v.array(field)),
2552
- editable: v.optional(v.record(FieldEntryName, editable))
2553
- };
2468
+ async function evaluateCondition(args) {
2469
+ return await evaluateConditionOutcome(args) === "satisfied";
2554
2470
  }
2555
2471
 
2556
- const StoredStageSchema = pinned()(v.strictObject(stageFields({
2557
- field: FieldEntrySchema,
2558
- activity: StoredActivitySchema,
2559
- transition: StoredTransitionSchema,
2560
- guard: GuardSchema,
2561
- editable: StoredEditableSchema
2562
- }))), AuthoringStageSchema = pinned()(v.strictObject(stageFields({
2563
- field: AuthoringFieldEntrySchema,
2564
- activity: AuthoringActivitySchema,
2565
- transition: AuthoringTransitionSchema,
2566
- guard: AuthoringGuardSchema,
2567
- editable: AuthoringEditableSchema
2568
- }))), RoleAliasesSchema = pinned()(v.record(NonEmpty, v.pipe(v.array(NonEmpty), v.minLength(1, "a role alias must list at least one fulfilling role")))), WORKFLOW_LIFECYCLES = _exhaustiveOptions()([ "standalone", "child" ]), START_KINDS = _exhaustiveOptions()([ "interactive", "autonomous" ]);
2472
+ async function evaluatePredicates(args) {
2473
+ const out = {};
2474
+ for (const [name, groq2] of Object.entries(args.predicates ?? {})) {
2475
+ const result = await runGroq({
2476
+ groq: groq2,
2477
+ params: args.params,
2478
+ snapshot: args.snapshot
2479
+ }), outcome = conditionOutcome(result);
2480
+ out[name] = outcome === "unevaluable" ? null : outcome === "satisfied";
2481
+ }
2482
+ return out;
2483
+ }
2569
2484
 
2570
- function startFields(kind) {
2571
- return {
2572
- kind: kind,
2573
- filter: v.optional(ConditionSchema),
2574
- requirements: v.optional(v.array(StartRequirementSchema))
2575
- };
2485
+ async function runGroq({groq: groq2, params: params, snapshot: snapshot}) {
2486
+ return runGroq$1({
2487
+ groq: groq2,
2488
+ params: params,
2489
+ dataset: snapshot.docs
2490
+ });
2576
2491
  }
2577
2492
 
2578
- const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KINDS)))), AuthoringStartSchema = pinned()(v.strictObject(startFields(v.optional(picklist(START_KINDS)))));
2493
+ function conditionSyntaxIssues(groq2, boundVars) {
2494
+ const issues = [];
2495
+ let tree;
2496
+ try {
2497
+ tree = parse(groq2);
2498
+ } catch (err) {
2499
+ issues.push(errorMessage(err));
2500
+ }
2501
+ 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."),
2502
+ 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(", "));
2503
+ return issues;
2504
+ }
2579
2505
 
2580
- function workflowFields({field: field, stage: stage, start: start}) {
2581
- return {
2582
- name: NonEmpty,
2583
- semantics: SemanticsFieldSchema,
2584
- title: NonEmpty,
2585
- description: v.optional(v.string()),
2586
- groups: v.optional(v.array(GroupSchema)),
2587
- lifecycle: v.optional(picklist(WORKFLOW_LIFECYCLES)),
2588
- start: v.optional(start),
2589
- initialStage: NonEmpty,
2590
- fields: v.optional(v.array(field)),
2591
- stages: v.pipe(v.array(stage), v.minLength(1, "must declare at least one stage")),
2592
- predicates: v.optional(v.record(groqIdentifier("`$<name>`"), ConditionSchema)),
2593
- roleAliases: v.optional(RoleAliasesSchema)
2594
- };
2506
+ function conditionParameterNames(groq2) {
2507
+ const read = /* @__PURE__ */ new Set;
2508
+ return walkAstNodes(tryParseGroq(groq2), node => {
2509
+ node.type === "Parameter" && typeof node.name == "string" && read.add(node.name);
2510
+ }), read;
2595
2511
  }
2596
2512
 
2597
- const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields({
2598
- field: FieldEntrySchema,
2599
- stage: StoredStageSchema,
2600
- start: StoredStartSchema
2601
- })));
2602
-
2603
- function parseStoredDefinition(input, label) {
2604
- return parseOrThrow({
2605
- schema: WorkflowDefinitionSchema,
2606
- input: input,
2607
- label: label
2608
- });
2513
+ function conditionFieldReadNames(groq2) {
2514
+ return new Set(conditionFieldReads(groq2).map(read => read.name));
2609
2515
  }
2610
2516
 
2611
- const AuthoringWorkflowSchema = pinned()(v.strictObject(workflowFields({
2612
- field: AuthoringFieldEntrySchema,
2613
- stage: AuthoringStageSchema,
2614
- start: AuthoringStartSchema
2615
- }))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
2517
+ function conditionFieldReads(groq2) {
2518
+ const reads = /* @__PURE__ */ new Map;
2519
+ return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
2520
+ }
2616
2521
 
2617
- function isStartableDefinition(definition) {
2618
- return definition.lifecycle !== "child";
2522
+ function collectFieldReads(node, reads) {
2523
+ if (Array.isArray(node)) {
2524
+ for (const item of node) collectFieldReads(item, reads);
2525
+ return;
2526
+ }
2527
+ if (typeof node != "object" || node === null) return;
2528
+ const chain = fieldsAttributeChain(node);
2529
+ if (chain !== void 0) {
2530
+ const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
2531
+ reads.set(`${name}.${path ?? ""}`, {
2532
+ name: name,
2533
+ path: path
2534
+ });
2535
+ return;
2536
+ }
2537
+ for (const value of Object.values(node)) collectFieldReads(value, reads);
2619
2538
  }
2620
2539
 
2621
- function startKindOf(definition) {
2622
- return definition.start?.kind ?? "interactive";
2540
+ function fieldsAttributeChain(node) {
2541
+ const names = [];
2542
+ let current = node;
2543
+ for (;typeof current == "object" && current !== null; ) {
2544
+ const typed = current;
2545
+ if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
2546
+ names.unshift(typed.name);
2547
+ const base = typed.base;
2548
+ if (base?.type === "Parameter" && base.name === "fields") return names;
2549
+ current = base;
2550
+ }
2623
2551
  }
2624
2552
 
2625
- function isSubjectEntry(entry) {
2626
- return entry.type === "subject";
2553
+ function conditionEffectReads(groq2) {
2554
+ const reads = [];
2555
+ return walkAstNodes(tryParseGroq(groq2), node => {
2556
+ if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
2557
+ const base = node.base;
2558
+ base?.type !== "AccessAttribute" || typeof base.name != "string" || base.base?.type === "Parameter" && base.base.name === "effects" && reads.push({
2559
+ effect: base.name,
2560
+ key: node.name
2561
+ });
2562
+ }), reads;
2627
2563
  }
2628
2564
 
2629
- function isDueDateEntry(entry) {
2630
- return entry.type === "dueDate" || entry.type === "dueDatetime";
2565
+ function readsRootDocument(groq2) {
2566
+ return nodeReadsRoot(tryParseGroq(groq2), 0);
2631
2567
  }
2632
2568
 
2633
- function isInputSourced(entry) {
2634
- return entry.initialValue?.type === "input";
2569
+ const SCOPED_CHILD = {
2570
+ Filter: "expr",
2571
+ Projection: "expr",
2572
+ Map: "expr",
2573
+ FlatMap: "expr",
2574
+ PipeFuncCall: "args"
2575
+ };
2576
+
2577
+ function nodeReadsRoot(node, depth) {
2578
+ if (Array.isArray(node)) return node.some(item => nodeReadsRoot(item, depth));
2579
+ if (typeof node != "object" || node === null) return !1;
2580
+ const typed = node;
2581
+ if (depth === 0 && typed.type === "This" || depth === 0 && typed.type === "AccessAttribute" && typed.base === void 0) return !0;
2582
+ if (typed.type === "Parent") {
2583
+ const climb = typeof typed.n == "number" ? typed.n : 1;
2584
+ return depth - climb <= 0;
2585
+ }
2586
+ const scopedChild = typeof typed.type == "string" ? SCOPED_CHILD[typed.type] : void 0;
2587
+ 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));
2635
2588
  }
2636
2589
 
2637
- function parseOrThrow({schema: schema, input: input, label: label}) {
2638
- const result = v.safeParse(schema, input);
2639
- if (!result.success) throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
2640
- return result.output;
2590
+ function tryParseGroq(groq2) {
2591
+ try {
2592
+ return parse(groq2);
2593
+ } catch {
2594
+ return;
2595
+ }
2641
2596
  }
2642
2597
 
2643
- function labelFor(fn, value) {
2644
- if (value && typeof value == "object" && "name" in value) {
2645
- const raw = value.name;
2646
- if (typeof raw == "string") return `${fn}("${raw}")`;
2598
+ function walkAstNodes(node, visit) {
2599
+ if (Array.isArray(node)) {
2600
+ for (const item of node) walkAstNodes(item, visit);
2601
+ return;
2647
2602
  }
2648
- return fn;
2603
+ if (!(typeof node != "object" || node === null)) {
2604
+ visit(node);
2605
+ for (const value of Object.values(node)) walkAstNodes(value, visit);
2606
+ }
2607
+ }
2608
+
2609
+ const CONDITION_VARS = [ {
2610
+ name: "self",
2611
+ binding: "always",
2612
+ label: "this workflow instance",
2613
+ description: "GDR URI of the instance document itself — `*[_id == $self][0]` reads the instance in the snapshot."
2614
+ }, {
2615
+ name: "fields",
2616
+ binding: "always",
2617
+ label: "the workflow's fields",
2618
+ 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` (or `subject`) 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."
2619
+ }, {
2620
+ name: "parent",
2621
+ binding: "always",
2622
+ label: "the parent workflow",
2623
+ description: "The parent instance's GDR URI, `null` on a root instance."
2624
+ }, {
2625
+ name: "ancestors",
2626
+ binding: "always",
2627
+ label: "the workflow's ancestors",
2628
+ description: "GDR URIs of the ancestor chain, root first."
2629
+ }, {
2630
+ name: "stage",
2631
+ binding: "always",
2632
+ label: "the current stage",
2633
+ description: "The current stage's name."
2634
+ }, {
2635
+ name: "now",
2636
+ binding: "always",
2637
+ label: "the current time",
2638
+ description: "The ISO clock reading shared by every condition in one evaluation pass, so they agree on the time."
2639
+ }, {
2640
+ name: "context",
2641
+ binding: "always",
2642
+ label: "start-time context",
2643
+ 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."
2644
+ }, {
2645
+ name: "effects",
2646
+ binding: "always",
2647
+ label: "automation outputs",
2648
+ 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."
2649
+ }, {
2650
+ name: "effectStatus",
2651
+ binding: "always",
2652
+ label: "automation status",
2653
+ description: "Effect name → `'done'` | `'failed'` of the latest completed run queued during the current stage entry; absent until the effect drains for this entry. Re-entry-safe: runs queued under a prior entry never count, so `$effectStatus['<effect>'] == 'done'` (or `defined($effectStatus['<effect>'])` for settled-either-way) waits for the fresh run."
2654
+ }, {
2655
+ name: "activities",
2656
+ binding: "always",
2657
+ label: "this stage's activities",
2658
+ description: "The current stage's activity rows, statuses included."
2659
+ }, {
2660
+ name: "allActivitiesDone",
2661
+ binding: "always",
2662
+ label: "all activities in this stage are finished",
2663
+ description: "Every current-stage activity is `done` or `skipped` — the default transition gate."
2664
+ }, {
2665
+ name: "anyActivityFailed",
2666
+ binding: "always",
2667
+ label: "an activity in this stage has failed",
2668
+ description: "Some current-stage activity is `failed`."
2669
+ }, {
2670
+ name: "actor",
2671
+ binding: "caller",
2672
+ label: "you",
2673
+ description: "The acting identity (id + roles); `undefined` when no caller rides the evaluation. Never holds a value in the cascade gates (deploy-rejected there)."
2674
+ }, {
2675
+ name: "assigned",
2676
+ binding: "caller",
2677
+ label: "you are assigned to this activity",
2678
+ description: "Whether the caller matches the activity's assignees-kind field entry (by user id, or by a role under the definition's `roleAliases`); `false` outside an activity context. Constant `false` in the cascade gates (deploy-rejected there)."
2679
+ }, {
2680
+ name: "can",
2681
+ binding: "caller",
2682
+ label: "your permissions",
2683
+ 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."
2684
+ }, {
2685
+ name: "attributes",
2686
+ binding: "caller",
2687
+ label: "your attributes",
2688
+ description: "Advisory org-level User Attributes for the caller (Enterprise — same values as lake `user::attributes()`), keyed by attribute name with each active scalar or array value. `undefined` on expected HTTP absence; unexpected fetch failures throw; empty page binds `{}`. Bound wherever grants ride the evaluation (same sites as `$can`). Soft-gate paths fetch at most 100 attributes (no further pages) and warn when the envelope reports `hasMore: true` (partial bag still binds). Not a security boundary — the Content Lake remains the only enforcement point."
2689
+ }, {
2690
+ name: "row",
2691
+ binding: "spawn",
2692
+ label: "the spawned row",
2693
+ 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."
2694
+ }, {
2695
+ name: "params",
2696
+ binding: "caller",
2697
+ label: "the action's arguments",
2698
+ 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."
2699
+ }, {
2700
+ name: "subworkflows",
2701
+ binding: "always",
2702
+ label: "the spawned subworkflows",
2703
+ 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`."
2704
+ } ], 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 = [ {
2705
+ name: "tag",
2706
+ label: "this engine tag",
2707
+ description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
2708
+ }, {
2709
+ name: "definition",
2710
+ label: "this workflow definition",
2711
+ description: "The `name` of the definition under evaluation (its own start block binds it)."
2712
+ }, {
2713
+ name: "now",
2714
+ label: "the current time",
2715
+ description: "The ISO clock reading of the evaluating engine."
2716
+ } ], START_REQUIREMENT_VARS = [ ...START_FILTER_VARS, {
2717
+ name: "fields",
2718
+ label: "the start's input fields",
2719
+ 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` or `subject` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
2720
+ } ], GUARD_PREDICATE_VARS = [ {
2721
+ name: "document",
2722
+ description: "The attempted mutation images, exposed as `document.before` and `document.after`."
2723
+ }, {
2724
+ name: "guard",
2725
+ description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
2726
+ }, {
2727
+ name: "mutation",
2728
+ description: "The attempted mutation — `mutation.action` is the write kind being gated."
2729
+ } ];
2730
+
2731
+ function effectBackoffMs(retry, endedAttempts) {
2732
+ const {backoff: backoff} = retry;
2733
+ return backoff === void 0 || endedAttempts < 1 ? 0 : backoff.kind === "fixed" ? backoff.delayMs : backoff.delayMs * 2 ** (endedAttempts - 1);
2734
+ }
2735
+
2736
+ function totalEffectBackoffMs(retry) {
2737
+ const waits = retry.attempts - 1;
2738
+ return retry.backoff === void 0 || waits < 1 ? 0 : retry.backoff.kind === "fixed" ? retry.backoff.delayMs * waits : retry.backoff.delayMs * (2 ** waits - 1);
2649
2739
  }
2650
2740
 
2651
2741
  function knownList(ids) {
@@ -3236,6 +3326,40 @@ function pushOutputIssues(args) {
3236
3326
  }
3237
3327
  }
3238
3328
 
3329
+ function checkEffectRetry(def, issues) {
3330
+ for (const {action: action, path: path} of actionSites(def)) for (const [e, effect] of (action.effects ?? []).entries()) effect.retry !== void 0 && pushRetryIssue({
3331
+ retry: effect.retry,
3332
+ name: effect.name,
3333
+ path: [ ...path, "effects", e, "retry" ],
3334
+ issues: issues
3335
+ });
3336
+ }
3337
+
3338
+ function pushRetryIssue(args) {
3339
+ const {retry: retry, name: name, path: path, issues: issues} = args;
3340
+ if (retry.expiryMs !== void 0 && retry.expiryMs > MAX_RETRY_SPAN_MS) {
3341
+ issues.push({
3342
+ path: [ ...path, "expiryMs" ],
3343
+ message: `effect "${name}" declares a ${retry.expiryMs}ms \`expiryMs\`, past the ${MAX_RETRY_SPAN_MS}ms ceiling — a policy runs inside one \`drainEffects\` invocation, and no host offers a single execution longer than a year. Lower \`expiryMs\``
3344
+ });
3345
+ return;
3346
+ }
3347
+ const wait = totalEffectBackoffMs(retry);
3348
+ if (wait > MAX_RETRY_SPAN_MS) {
3349
+ issues.push({
3350
+ path: [ ...path, "attempts" ],
3351
+ message: `effect "${name}" accumulates ${spanned(wait)} of backoff before attempt ${retry.attempts}, past the ${MAX_RETRY_SPAN_MS}ms ceiling — a policy runs inside one \`drainEffects\` invocation, and no host offers a single execution longer than a year. Lower \`attempts\` or \`backoff.delayMs\``
3352
+ });
3353
+ return;
3354
+ }
3355
+ retry.expiryMs === void 0 || wait < retry.expiryMs || issues.push({
3356
+ path: [ ...path, "expiryMs" ],
3357
+ message: `effect "${name}" waits ${wait}ms of backoff before attempt ${retry.attempts}, which its ${retry.expiryMs}ms \`expiryMs\` closes first — the effect fails before the declared attempts run. Raise \`expiryMs\`, or lower \`attempts\`/\`backoff.delayMs\``
3358
+ });
3359
+ }
3360
+
3361
+ const spanned = ms => Number.isFinite(ms) ? `${ms}ms` : "more milliseconds than a number can count", MAX_RETRY_SPAN_MS = 316224e5;
3362
+
3239
3363
  function checkSubjectEntries(def, issues) {
3240
3364
  for (const {entries: entries, scope: scope, path: path, label: label} of fieldScopes(def)) {
3241
3365
  const subjects = (entries ?? []).flatMap((entry, n) => isSubjectEntry(entry) ? [ {
@@ -4011,9 +4135,9 @@ function checkWorkflowInvariants(def) {
4011
4135
  checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
4012
4136
  checkFieldReadOpValues(def, issues), checkFieldTargetOps(def, issues), checkGuardFieldReads(def, issues),
4013
4137
  checkAssigneesEntries(def, issues), checkDueDateEntries(def, issues), checkSubjectEntries(def, issues),
4014
- checkLevelKindEffectOutputs(def, issues), checkActivityTerminalPaths(def, issues),
4138
+ checkLevelKindEffectOutputs(def, issues), checkEffectRetry(def, issues), checkActivityTerminalPaths(def, issues),
4015
4139
  checkTerminalStageActivities(def, issues), checkTriggeredActionParams(def, issues),
4016
4140
  checkStoredRolesPlacement(def, issues), checkGroups(def, issues), issues;
4017
4141
  }
4018
4142
 
4019
- export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DECISION_SEMANTICS, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_ACTIONS_REQUIRED_MESSAGE, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, LAKE_MUTATION_GUARD_ACTIONS, MUTATION_GUARD_ACTIONS, MUTATION_GUARD_ID_SPACES, NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, SIGNAL_SEMANTICS, START_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, UNIVERSAL_ROLE_ALIAS_KEY, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, activeAssignmentMembers, actorFulfillsRole, actorMatchesAssignment, andConditions, assignmentKindAcceptsRoles, assignmentMatch, assignmentMembers, assignmentState, assignmentStateCounts, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, condemnSubworkflow, condemnedSubworkflows, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, findCurrentActivityEntry, findOpenStageEntry, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, identityMatchesAssignment, instanceAssignmentStateCounts, isAlwaysArrayFieldKind, isAssignmentFieldEntry, isBareDocumentId, isBareSeedId, isCascadeFired, isFilterScopedOut, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isRecord, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, liveSubworkflows, mutationGuardActionIdSpace, mutationGuardRequiresSplitEmission, normalizeAssignmentMembers, openActivityAssignments, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resolvedChildStatus, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
4143
+ export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringEffectSchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DECISION_SEMANTICS, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, FIELD_READ, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_ACTIONS_REQUIRED_MESSAGE, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, LAKE_MUTATION_GUARD_ACTIONS, MUTATION_GUARD_ACTIONS, MUTATION_GUARD_ID_SPACES, NonEmptyString$1 as NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, SIGNAL_SEMANTICS, START_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, UNIVERSAL_ROLE_ALIAS_KEY, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, activeAssignmentMembers, actorFulfillsRole, actorMatchesAssignment, andConditions, assignmentKindAcceptsRoles, assignmentMatch, assignmentMembers, assignmentState, assignmentStateCounts, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, condemnSubworkflow, condemnedSubworkflows, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, effectBackoffMs, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, findCurrentActivityEntry, findOpenStageEntry, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, identityMatchesAssignment, instanceAssignmentStateCounts, isAlwaysArrayFieldKind, isAssignmentFieldEntry, isBareDocumentId, isBareSeedId, isCascadeFired, isFilterScopedOut, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isRecord, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, liveSubworkflows, mutationGuardActionIdSpace, mutationGuardRequiresSplitEmission, normalizeAssignmentMembers, openActivityAssignments, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resolvedChildStatus, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };