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