@sanity/workflow-engine 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,291 @@ import { conditionOutcome, runGroq as runGroq$1, evaluateConditionOutcome as eva
4
4
 
5
5
  import { parse } from "groq-js";
6
6
 
7
+ class WorkflowError extends Error {
8
+ kind;
9
+ constructor(kind, message, options) {
10
+ super(message, options), this.kind = kind;
11
+ }
12
+ }
13
+
14
+ class ContractViolationError extends WorkflowError {
15
+ constructor(message) {
16
+ super("contract-violation", message), this.name = "ContractViolationError";
17
+ }
18
+ }
19
+
20
+ class InstanceNotFoundError extends WorkflowError {
21
+ instanceId;
22
+ constructor(args) {
23
+ super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
24
+ this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
25
+ }
26
+ }
27
+
28
+ class DefinitionNotFoundError extends WorkflowError {
29
+ definition;
30
+ version;
31
+ constructor(args) {
32
+ super("definition-not-found", args.version !== void 0 ? `Workflow definition ${args.definition} v${args.version} not deployed` : `Workflow definition ${args.definition} has no deployed versions`),
33
+ this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
34
+ }
35
+ }
36
+
37
+ class SpawnContractsInvalidError extends WorkflowError {
38
+ issues;
39
+ constructor(args) {
40
+ super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
41
+ this.issues = args.issues;
42
+ }
43
+ }
44
+
45
+ class DefinitionInUseError extends WorkflowError {
46
+ definition;
47
+ blockedBy;
48
+ constructor(args) {
49
+ super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
50
+ this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
51
+ }
52
+ }
53
+
54
+ function definitionInUseMessage(definition, blockedBy) {
55
+ if (blockedBy.reason === "non-terminal-instances") {
56
+ const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
57
+ return `Cannot delete ${definition}: ${blockedBy.instanceIds.length} non-terminal instance(s) exist (${preview}). Pass cascade to abort them first — instances are aborted in place, never deleted.`;
58
+ }
59
+ const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
60
+ return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
61
+ }
62
+
63
+ class EffectNotFoundError extends WorkflowError {
64
+ instanceId;
65
+ effectKey;
66
+ settled;
67
+ constructor(args) {
68
+ super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
69
+ this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
70
+ }
71
+ }
72
+
73
+ function effectNotFoundMessage(args) {
74
+ const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
75
+ if (args.settled === void 0) return base;
76
+ const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
77
+ return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
78
+ }
79
+
80
+ function errorMessage(err) {
81
+ return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
82
+ }
83
+
84
+ function rethrowWithContext(err, context) {
85
+ throw new Error(`${context}: ${errorMessage(err)}`, {
86
+ cause: err
87
+ });
88
+ }
89
+
90
+ const WORKFLOW_INSTANCE_TYPE = "sanity.workflow.instance";
91
+
92
+ function terminalState(instance) {
93
+ return instance.abortedAt !== void 0 ? "aborted" : instance.completedAt !== void 0 ? "completed" : "in-flight";
94
+ }
95
+
96
+ function isUnprimed(instance) {
97
+ return instance.stages.length === 0 && terminalState(instance) === "in-flight";
98
+ }
99
+
100
+ function parseDefinitionSnapshotValue(instance) {
101
+ try {
102
+ return JSON.parse(instance.definitionSnapshot);
103
+ } catch (err) {
104
+ rethrowWithContext(err, `Failed to parse definitionSnapshot on instance "${instance._id}"`);
105
+ }
106
+ }
107
+
108
+ function parseDefinitionSnapshot(instance) {
109
+ return parseDefinitionSnapshotValue(instance);
110
+ }
111
+
112
+ function parentRef(instance) {
113
+ return instance.ancestors.at(-1);
114
+ }
115
+
116
+ const DATA_MODEL_VERSION = 2, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
117
+
118
+ class ReaderModelAcknowledgementError extends WorkflowError {
119
+ code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
120
+ expectedMinReaderModel;
121
+ engineMinReaderModel=DATA_MODEL_MIN_READER;
122
+ engineModelVersion=DATA_MODEL_VERSION;
123
+ documentationUrl=READER_MODEL_ROLLOUT_URL;
124
+ constructor(expectedMinReaderModel, context = "Deployment") {
125
+ const expected = expectedMinReaderModel === void 0 ? "missing" : String(expectedMinReaderModel);
126
+ super("reader-model-acknowledgement", `${context} expected reader floor ${expected}; the installed engine requires acknowledgement ${DATA_MODEL_MIN_READER}. Dependency upgrades can change writer compatibility. Upgrade all readers and Functions before accepting a higher floor; after verifying the rollout, change the literal in deployment configuration. ${READER_MODEL_ROLLOUT_URL}`),
127
+ this.name = "ReaderModelAcknowledgementError", this.expectedMinReaderModel = expectedMinReaderModel;
128
+ }
129
+ }
130
+
131
+ function assertReaderModelAcknowledgement(expectedMinReaderModel, context) {
132
+ if (typeof expectedMinReaderModel != "number" || !Number.isFinite(expectedMinReaderModel) || !Number.isInteger(expectedMinReaderModel) || expectedMinReaderModel < 0 || expectedMinReaderModel !== DATA_MODEL_MIN_READER) throw new ReaderModelAcknowledgementError(expectedMinReaderModel, context);
133
+ }
134
+
135
+ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
136
+ id: "governed-model-stamps",
137
+ introducedInModel: 1,
138
+ minReaderModel: 0,
139
+ documentTypes: Object.freeze([ "definition", "instance" ]),
140
+ compatibility: "additive",
141
+ applicability: "unconditional",
142
+ summary: "Definition and instance documents carry model provenance and reader-floor stamps."
143
+ }), Object.freeze({
144
+ id: "subject-field-kind",
145
+ introducedInModel: 2,
146
+ minReaderModel: 0,
147
+ documentTypes: Object.freeze([ "definition", "instance" ]),
148
+ compatibility: "additive",
149
+ applicability: "detectable",
150
+ summary: "A workflow-level subject field identifies the document a workflow is about."
151
+ }), Object.freeze({
152
+ id: "typed-scalar-choice-lists",
153
+ introducedInModel: 2,
154
+ minReaderModel: 2,
155
+ documentTypes: Object.freeze([ "definition", "instance" ]),
156
+ compatibility: "reader-floor",
157
+ applicability: "detectable",
158
+ summary: "Scalar fields may constrain writes to a persisted typed choice list."
159
+ }), Object.freeze({
160
+ id: "action-semantics",
161
+ introducedInModel: 2,
162
+ minReaderModel: 0,
163
+ documentTypes: Object.freeze([ "definition" ]),
164
+ compatibility: "additive",
165
+ applicability: "detectable",
166
+ summary: "Ordinary actions may carry a closed bag of advisory workflow semantics."
167
+ }), Object.freeze({
168
+ id: "inclusive-scalar-bounds",
169
+ introducedInModel: 2,
170
+ minReaderModel: 2,
171
+ documentTypes: Object.freeze([ "definition", "instance" ]),
172
+ compatibility: "reader-floor",
173
+ applicability: "detectable",
174
+ summary: "String, text, and number values may carry persisted inclusive bounds."
175
+ }) ]);
176
+
177
+ function recordOf(value) {
178
+ return value !== null && typeof value == "object" && !Array.isArray(value) ? value : void 0;
179
+ }
180
+
181
+ function recordsAt(record, key) {
182
+ const value = record[key];
183
+ return Array.isArray(value) ? value.map(recordOf).filter(item => item !== void 0) : [];
184
+ }
185
+
186
+ function nestedFieldEntries(entries) {
187
+ return entries.flatMap(entry => [ entry, ...nestedFieldEntries(recordsAt(entry, "fields")), ...nestedFieldEntries(recordsAt(entry, "of")) ]);
188
+ }
189
+
190
+ function parsedDefinitionSnapshot(root) {
191
+ if (typeof root.definitionSnapshot == "string") return recordOf(parseDefinitionSnapshotValue({
192
+ _id: typeof root._id == "string" ? root._id : "<unknown instance>",
193
+ definitionSnapshot: root.definitionSnapshot
194
+ }));
195
+ }
196
+
197
+ function persistedFieldEntries(document) {
198
+ const root = recordOf(document);
199
+ if (root === void 0) return [];
200
+ const snapshot = parsedDefinitionSnapshot(root), roots = snapshot === void 0 ? [ root ] : [ root, snapshot ], stages = roots.flatMap(candidate => recordsAt(candidate, "stages")), activities = stages.flatMap(stage => recordsAt(stage, "activities")), actions = activities.flatMap(activity => recordsAt(activity, "actions")), effects = actions.flatMap(action => recordsAt(action, "effects"));
201
+ return nestedFieldEntries([ ...roots.flatMap(candidate => recordsAt(candidate, "fields")), ...stages.flatMap(stage => recordsAt(stage, "fields")), ...activities.flatMap(activity => recordsAt(activity, "fields")), ...actions.flatMap(action => recordsAt(action, "params")), ...effects.flatMap(effect => recordsAt(effect, "outputs")) ]);
202
+ }
203
+
204
+ function hasChoiceList(document) {
205
+ return persistedFieldEntries(document).some(entry => {
206
+ const options = recordOf(entry.options);
207
+ return options !== void 0 && Array.isArray(options.list);
208
+ });
209
+ }
210
+
211
+ function hasSubjectField(document) {
212
+ return persistedFieldEntries(document).some(entry => entry.type === "subject" || entry._type === "subject");
213
+ }
214
+
215
+ function hasActionSemantics(document) {
216
+ const root = recordOf(document);
217
+ return root === void 0 ? !1 : recordsAt(root, "stages").flatMap(stage => recordsAt(stage, "activities")).flatMap(activity => recordsAt(activity, "actions")).some(action => Array.isArray(action.semantics));
218
+ }
219
+
220
+ function hasScalarValidation(document) {
221
+ return persistedFieldEntries(document).some(entry => {
222
+ const validation = recordOf(entry.validation);
223
+ return typeof validation?.min == "number" || typeof validation?.max == "number";
224
+ });
225
+ }
226
+
227
+ const featureDetectors = {
228
+ "governed-model-stamps": () => !0,
229
+ "subject-field-kind": hasSubjectField,
230
+ "typed-scalar-choice-lists": hasChoiceList,
231
+ "action-semantics": hasActionSemantics,
232
+ "inclusive-scalar-bounds": hasScalarValidation
233
+ };
234
+
235
+ function requiredModelFeatures(documentType, document) {
236
+ return DATA_MODEL_CHANGES.filter(change => change.documentTypes.some(candidate => candidate === documentType) && featureDetectors[change.id](document));
237
+ }
238
+
239
+ function requiredReaderModel(documentType, document) {
240
+ return Math.max(0, ...requiredModelFeatures(documentType, document).map(change => change.minReaderModel));
241
+ }
242
+
243
+ function modelStampFor(args) {
244
+ return {
245
+ modelVersion: DATA_MODEL_VERSION,
246
+ minReaderModel: Math.max(args.storedMinReaderModel ?? 0, requiredReaderModel(args.documentType, args.document))
247
+ };
248
+ }
249
+
250
+ function fieldTreeShape(value) {
251
+ if (Array.isArray(value)) return value.map(fieldTreeShape);
252
+ if (value === null) return "null";
253
+ if (typeof value == "object") {
254
+ const record = value;
255
+ return Object.fromEntries(Object.keys(record).toSorted().map(key => [ key, fieldTreeShape(record[key]) ]));
256
+ }
257
+ return typeof value;
258
+ }
259
+
260
+ function modelVersionOf(doc) {
261
+ const stamp = doc.modelVersion;
262
+ return typeof stamp == "number" ? stamp : 0;
263
+ }
264
+
265
+ function minReaderModelOf(doc) {
266
+ const floor = doc.minReaderModel;
267
+ return typeof floor == "number" ? floor : modelVersionOf(doc);
268
+ }
269
+
270
+ class ModelVersionAheadError extends WorkflowError {
271
+ documentId;
272
+ documentModelVersion;
273
+ requiredReaderModel;
274
+ engineModelVersion;
275
+ constructor(args) {
276
+ super("model-version-ahead", `Document "${args.documentId}" was written by engine data model ${args.documentModelVersion} and requires a reader at model ${args.requiredReaderModel} or newer; this engine reads up to model ${DATA_MODEL_VERSION}. Upgrade @sanity/workflow-engine to a version that understands it.`),
277
+ this.name = "ModelVersionAheadError", this.documentId = args.documentId, this.documentModelVersion = args.documentModelVersion,
278
+ this.requiredReaderModel = args.requiredReaderModel, this.engineModelVersion = DATA_MODEL_VERSION;
279
+ }
280
+ }
281
+
282
+ function assertReadableModel(doc) {
283
+ const documentReaderModel = minReaderModelOf(doc);
284
+ if (documentReaderModel > DATA_MODEL_VERSION) throw new ModelVersionAheadError({
285
+ documentId: doc._id,
286
+ documentModelVersion: modelVersionOf(doc),
287
+ requiredReaderModel: documentReaderModel
288
+ });
289
+ return doc;
290
+ }
291
+
7
292
  function isCascadeFired(action) {
8
293
  return action.when !== void 0;
9
294
  }
@@ -24,16 +309,6 @@ function driverKind(actor) {
24
309
  return actor.kind === "person" || actor.kind === "agent" ? actor.kind : "service";
25
310
  }
26
311
 
27
- function errorMessage(err) {
28
- return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
29
- }
30
-
31
- function rethrowWithContext(err, context) {
32
- throw new Error(`${context}: ${errorMessage(err)}`, {
33
- cause: err
34
- });
35
- }
36
-
37
312
  function andConditions(parts) {
38
313
  const present = parts.filter(p => p !== void 0);
39
314
  if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
@@ -234,71 +509,6 @@ function isGdr(value) {
234
509
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
235
510
  }
236
511
 
237
- class WorkflowError extends Error {
238
- kind;
239
- constructor(kind, message, options) {
240
- super(message, options), this.kind = kind;
241
- }
242
- }
243
-
244
- class ContractViolationError extends WorkflowError {
245
- constructor(message) {
246
- super("contract-violation", message), this.name = "ContractViolationError";
247
- }
248
- }
249
-
250
- class InstanceNotFoundError extends WorkflowError {
251
- instanceId;
252
- constructor(args) {
253
- super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
254
- this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
255
- }
256
- }
257
-
258
- class DefinitionNotFoundError extends WorkflowError {
259
- definition;
260
- version;
261
- constructor(args) {
262
- super("definition-not-found", args.version !== void 0 ? `Workflow definition ${args.definition} v${args.version} not deployed` : `Workflow definition ${args.definition} has no deployed versions`),
263
- this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
264
- }
265
- }
266
-
267
- class DefinitionInUseError extends WorkflowError {
268
- definition;
269
- blockedBy;
270
- constructor(args) {
271
- super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
272
- this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
273
- }
274
- }
275
-
276
- function definitionInUseMessage(definition, blockedBy) {
277
- if (blockedBy.reason === "non-terminal-instances") {
278
- const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
279
- return `Cannot delete ${definition}: ${blockedBy.instanceIds.length} non-terminal instance(s) exist (${preview}). Pass cascade to abort them first — instances are aborted in place, never deleted.`;
280
- }
281
- const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
282
- return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
283
- }
284
-
285
- class EffectNotFoundError extends WorkflowError {
286
- instanceId;
287
- effectKey;
288
- settled;
289
- constructor(args) {
290
- super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
291
- this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
292
- }
293
- }
294
-
295
- function effectNotFoundMessage(args) {
296
- const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
297
- if (args.settled === void 0) return base;
298
- const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
299
- return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
300
- }
301
-
302
512
  const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
303
513
 
304
514
  function validateTag(tag) {
@@ -338,6 +548,7 @@ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(vali
338
548
  resource: WorkflowResourceSchema
339
549
  }), 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({
340
550
  name: NonEmptyString$1,
551
+ expectedMinReaderModel: v.optional(v.custom(() => !0), void 0),
341
552
  tag: v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
342
553
  workflowResource: WorkflowResourceSchema,
343
554
  resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => new Set(bindings.map(binding => binding.name)).size === bindings.length, "duplicate resource handle name — each binding name must be unique within a deployment"))),
@@ -580,6 +791,8 @@ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
580
791
  required: entry.required,
581
792
  initialValue: entry.initialValue,
582
793
  editable: editable,
794
+ options: entry.options,
795
+ validation: entry.validation,
583
796
  types: entry.types,
584
797
  fields: entry.fields,
585
798
  of: entry.of
@@ -722,7 +935,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
722
935
  };
723
936
  }
724
937
 
725
- const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
938
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref" ];
726
939
 
727
940
  function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
728
941
  if (target === void 0 || target.type === "url") return target;
@@ -743,18 +956,15 @@ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
743
956
  };
744
957
  }
745
958
 
746
- function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
747
- if ("type" in action) return desugarClaimAction({
748
- action: action,
749
- path: path,
750
- env: env,
751
- ctx: ctx
752
- });
753
- action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
959
+ function reportEmptyActionRoles({action: action, path: path, ctx: ctx}) {
960
+ action.roles === void 0 || action.roles.length > 0 || ctx.issues.push({
754
961
  path: [ ...path, "roles" ],
755
962
  message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
756
963
  });
757
- const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
964
+ }
965
+
966
+ function desugarActionOps(args) {
967
+ const {action: action, path: path, env: env, activityName: activityName, ctx: ctx} = args, ops = desugarOps({
758
968
  ops: action.ops,
759
969
  path: [ ...path, "ops" ],
760
970
  env: env,
@@ -765,9 +975,32 @@ function desugarAction({action: action, path: path, env: env, activityName: acti
765
975
  type: "status.set",
766
976
  activity: activityName,
767
977
  status: action.status
768
- }), {
978
+ }), ops;
979
+ }
980
+
981
+ function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
982
+ if ("type" in action) return desugarClaimAction({
983
+ action: action,
984
+ path: path,
985
+ env: env,
986
+ ctx: ctx
987
+ });
988
+ reportEmptyActionRoles({
989
+ action: action,
990
+ path: path,
991
+ ctx: ctx
992
+ });
993
+ const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarActionOps({
994
+ action: action,
995
+ path: path,
996
+ env: env,
997
+ activityName: activityName,
998
+ ctx: ctx
999
+ });
1000
+ return {
769
1001
  ...stripUndefined({
770
1002
  name: action.name,
1003
+ semantics: action.semantics,
771
1004
  title: action.title,
772
1005
  description: action.description,
773
1006
  group: normalizeGroup(action.group),
@@ -1154,7 +1387,7 @@ function isTerminalActivityStatus(status) {
1154
1387
  return TERMINAL_ACTIVITY_STATUSES.includes(status);
1155
1388
  }
1156
1389
 
1157
- const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], CONDITION_VARS = [ {
1390
+ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], CONDITION_VARS = [ {
1158
1391
  name: "self",
1159
1392
  binding: "always",
1160
1393
  label: "this workflow instance",
@@ -1163,7 +1396,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1163
1396
  name: "fields",
1164
1397
  binding: "always",
1165
1398
  label: "the workflow's fields",
1166
- description: "Declared field entries rendered by name (`$fields.<name>` is the value, no wrapper). Stage/activity scopes overlay lexically. What a read puts in your hand follows the declared kind: a singular `doc.ref` DEREFERENCES into the hydrated document (lake `_id`/`_type` plus content fields — which may themselves be named `id`/`type`), while `doc.refs` elements and `release.ref` stay REFERENCES — `{id, type[, releaseName]}` with `id` a GDR URI — because identity reads (membership, counting, joins) must stay total without hydrating every target, and targets may live in resources the evaluation cannot fetch from. Conditions evaluate against the in-memory snapshot only, so a dereferencing plural kind would silently hole wherever hydration lagged."
1399
+ 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."
1167
1400
  }, {
1168
1401
  name: "parent",
1169
1402
  binding: "always",
@@ -1244,18 +1477,26 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1244
1477
  binding: "always",
1245
1478
  label: "the spawned subworkflows",
1246
1479
  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`."
1247
- } ], 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 = [ {
1480
+ } ], SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR = "subjectHasInFlightInstance", RESERVED_CONDITION_VARS = [ ...CONDITION_VARS.map(v2 => v2.name), SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR ], 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 = [ {
1248
1481
  name: "tag",
1482
+ label: "this engine tag",
1249
1483
  description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1250
1484
  }, {
1251
1485
  name: "definition",
1486
+ label: "this workflow definition",
1252
1487
  description: "The `name` of the definition under evaluation (its own start block binds it)."
1253
1488
  }, {
1254
1489
  name: "now",
1490
+ label: "the current time",
1255
1491
  description: "The ISO clock reading of the evaluating engine."
1492
+ }, {
1493
+ name: SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR,
1494
+ label: "the subject already has an in-flight workflow",
1495
+ description: "Whether any instance in this engine tag, across all definitions, has the same resource-qualified subject and no `completedAt`. Advisory under concurrent starts."
1256
1496
  } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1257
1497
  name: "fields",
1258
- description: "The caller's input entries by name (`initialFields` — at `startInstance`, the values the start would seed; at a pre-flight, the values gathered so far, so a read of a not-yet-supplied entry is GROQ null). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble — a singular `doc.ref` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
1498
+ label: "the start's input fields",
1499
+ 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."
1259
1500
  } ], GUARD_PREDICATE_VARS = [ {
1260
1501
  name: "guard",
1261
1502
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
@@ -1405,6 +1646,18 @@ function releaseRef({res: res, releaseName: releaseName}) {
1405
1646
  };
1406
1647
  }
1407
1648
 
1649
+ function isSingleDocRefKind(kind) {
1650
+ return kind === "doc.ref" || kind === "subject";
1651
+ }
1652
+
1653
+ function refKindAcceptsTypes(kind) {
1654
+ return isSingleDocRefKind(kind) || kind === "doc.refs";
1655
+ }
1656
+
1657
+ function isSingleDocRefEntry(entry) {
1658
+ return isSingleDocRefKind(entry._type);
1659
+ }
1660
+
1408
1661
  function isTodoListItem(row) {
1409
1662
  if (typeof row != "object" || row === null) return !1;
1410
1663
  const candidate = row, status = candidate.status;
@@ -1455,9 +1708,34 @@ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be
1455
1708
  }), tolerantObject()({
1456
1709
  type: v.literal("role"),
1457
1710
  role: NonEmptyString
1458
- }) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), 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, fieldValueSchemas = {
1711
+ }) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), 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", "datetime", "dateTime" ]);
1712
+
1713
+ function normalizedChoiceKind(kind) {
1714
+ return kind === "dateTime" ? "datetime" : kind;
1715
+ }
1716
+
1717
+ function checkChoiceList(args) {
1718
+ const {entryType: entryType, options: options, validation: validation} = args;
1719
+ if (options === void 0) return;
1720
+ if (!CHOICE_KINDS.has(entryType)) return [ `\`options\` is not valid on "${entryType}" values` ];
1721
+ const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => checkValueAgainst({
1722
+ entryType: kind,
1723
+ value: option.value,
1724
+ validation: validation
1725
+ }, valueSchemas)?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
1726
+ for (const option of options.list) seen.has(option.value) && issues.push(`duplicate option value ${JSON.stringify(option.value)}`),
1727
+ seen.add(option.value);
1728
+ return issues.length === 0 ? void 0 : issues;
1729
+ }
1730
+
1731
+ function choiceValueIssues(options, value) {
1732
+ 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(", ")}` ];
1733
+ }
1734
+
1735
+ const fieldValueSchemas = {
1459
1736
  "doc.ref": v.union([ v.null(), GdrShape ]),
1460
1737
  "doc.refs": v.array(GdrShape),
1738
+ subject: v.union([ v.null(), GdrShape ]),
1461
1739
  "release.ref": v.union([ v.null(), ReleaseRefShape ]),
1462
1740
  string: NullableString,
1463
1741
  text: NullableString,
@@ -1475,7 +1753,56 @@ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be
1475
1753
  };
1476
1754
 
1477
1755
  function shapeValueSchema(shape, leaf) {
1478
- return shape.type === "object" ? objectSchema(shape.fields ?? [], leaf) : shape.type === "array" ? v.array(objectSchema(shape.of ?? [], leaf)) : leaf[shape.type] ?? v.any();
1756
+ if (shape.type === "object") return objectSchema(shape.fields ?? [], leaf);
1757
+ if (shape.type === "array") return v.array(objectSchema(shape.of ?? [], leaf));
1758
+ const schema = leaf[shape.type] ?? v.any();
1759
+ return constrainedScalarSchema({
1760
+ schema: schema,
1761
+ entryType: shape.type,
1762
+ ...shape
1763
+ });
1764
+ }
1765
+
1766
+ function scalarMeasurement(entryType, value) {
1767
+ if (entryType === "number" && typeof value == "number") return value;
1768
+ if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
1769
+ }
1770
+
1771
+ function scalarBoundIssue(args) {
1772
+ const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
1773
+ return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
1774
+ }
1775
+
1776
+ function scalarValidationIssues(args) {
1777
+ const {entryType: entryType, validation: validation, value: value} = args;
1778
+ if (validation === void 0 || value === null || value === void 0) return;
1779
+ const measured = scalarMeasurement(entryType, value);
1780
+ if (measured === void 0) return;
1781
+ const issues = [ scalarBoundIssue({
1782
+ entryType: entryType,
1783
+ measured: measured,
1784
+ bound: validation.min,
1785
+ limit: "min"
1786
+ }), scalarBoundIssue({
1787
+ entryType: entryType,
1788
+ measured: measured,
1789
+ bound: validation.max,
1790
+ limit: "max"
1791
+ }) ].filter(issue => issue !== void 0);
1792
+ return issues.length === 0 ? void 0 : issues;
1793
+ }
1794
+
1795
+ function constrainedScalarSchema(args) {
1796
+ const {schema: schema, entryType: entryType, options: options, validation: validation} = args;
1797
+ return options === void 0 && validation === void 0 ? schema : v.pipe(schema, v.check(value => choiceValueIssues(options, value) === void 0 && scalarValidationIssues({
1798
+ entryType: entryType,
1799
+ validation: validation,
1800
+ value: value
1801
+ }) === void 0, issue => [ ...choiceValueIssues(options, issue.input) ?? [], ...scalarValidationIssues({
1802
+ entryType: entryType,
1803
+ validation: validation,
1804
+ value: issue.input
1805
+ }) ?? [] ].join("; ")));
1479
1806
  }
1480
1807
 
1481
1808
  function objectSchema(fields, leaf) {
@@ -1498,7 +1825,7 @@ function appendItemSchema(entryType, shape) {
1498
1825
  function rejectedRefTypes(args) {
1499
1826
  const {entryType: entryType, types: types, value: value} = args;
1500
1827
  if (types === void 0 || value === null || value === void 0) return [];
1501
- if (entryType !== "doc.ref" && entryType !== "doc.refs") return [];
1828
+ if (!refKindAcceptsTypes(entryType)) return [];
1502
1829
  let items = [ value ];
1503
1830
  return entryType === "doc.refs" && (items = Array.isArray(value) ? value : []),
1504
1831
  [ ...new Set(items.map(gdrTypeOf).filter(t => t !== void 0 && !types.includes(t))) ];
@@ -1533,6 +1860,10 @@ function checkValueAgainst(args, leaf) {
1533
1860
  entryType: args.entryType,
1534
1861
  types: args.types,
1535
1862
  value: args.value
1863
+ }) ?? choiceValueIssues(args.options, args.value) ?? scalarValidationIssues({
1864
+ entryType: args.entryType,
1865
+ validation: args.validation,
1866
+ value: args.value
1536
1867
  }) : formatIssues(result.issues);
1537
1868
  }
1538
1869
 
@@ -1563,6 +1894,7 @@ const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a b
1563
1894
  ...valueSchemas,
1564
1895
  "doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
1565
1896
  "doc.refs": v.array(AuthoringGdrShape),
1897
+ subject: v.union([ v.null(), AuthoringGdrShape ]),
1566
1898
  "release.ref": v.union([ v.null(), v.looseObject({
1567
1899
  id: AuthoringRefId,
1568
1900
  type: v.literal("system.release"),
@@ -1602,10 +1934,10 @@ function validateFieldAppendItem(args) {
1602
1934
  });
1603
1935
  }
1604
1936
 
1605
- function formatIssues(issues) {
1937
+ function formatIssues(issues, formatMessage = issue => issue.message) {
1606
1938
  return issues.map(i => {
1607
1939
  const keys = i.path?.map(p => p.key) ?? [];
1608
- return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
1940
+ return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(i)}`;
1609
1941
  });
1610
1942
  }
1611
1943
 
@@ -1731,7 +2063,15 @@ function groupMembershipNames(group) {
1731
2063
  return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
1732
2064
  }
1733
2065
 
1734
- const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "release.ref", "string", "text", "number", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`");
2066
+ const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v.pipe(v.number(), v.finite("must be finite")), ScalarValidationSchema = v.pipe(v.strictObject({
2067
+ min: v.optional(FiniteNumber),
2068
+ max: v.optional(FiniteNumber)
2069
+ }), 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({
2070
+ list: v.pipe(v.array(v.strictObject({
2071
+ title: NonEmpty,
2072
+ value: v.union([ v.string(), v.number() ])
2073
+ })), v.minLength(1, "declare at least one choice, or omit `options`"))
2074
+ });
1735
2075
 
1736
2076
  function asShape(input) {
1737
2077
  return typeof input == "object" && input !== null ? input : {};
@@ -1765,14 +2105,16 @@ function compositeChecked(entries) {
1765
2105
  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\``));
1766
2106
  }
1767
2107
 
1768
- const FieldShapeSchema = v.lazy(() => compositeChecked({
2108
+ const FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
1769
2109
  type: FieldValueKindSchema,
1770
2110
  name: FieldEntryName,
1771
2111
  title: v.optional(v.string()),
1772
2112
  description: v.optional(v.string()),
2113
+ options: v.optional(ChoiceOptionsSchema),
2114
+ validation: v.optional(ScalarValidationSchema),
1773
2115
  fields: v.optional(v.array(FieldShapeSchema)),
1774
2116
  of: v.optional(v.array(FieldShapeSchema))
1775
- })), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
2117
+ }), choiceOptionsCheck(), scalarValidationCheck())), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
1776
2118
 
1777
2119
  function fieldBase(editable, group) {
1778
2120
  return {
@@ -1790,6 +2132,8 @@ function fieldEntryFields(editable, group) {
1790
2132
  return {
1791
2133
  type: FieldKindSchema,
1792
2134
  ...fieldBase(editable, group),
2135
+ options: v.optional(ChoiceOptionsSchema),
2136
+ validation: v.optional(ScalarValidationSchema),
1793
2137
  types: v.optional(v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
1794
2138
  fields: v.optional(v.array(FieldShapeSchema)),
1795
2139
  of: v.optional(v.array(FieldShapeSchema))
@@ -1802,7 +2146,9 @@ function literalSeedIssues(entry) {
1802
2146
  value: entry.initialValue.value,
1803
2147
  types: entry.types,
1804
2148
  fields: entry.fields,
1805
- of: entry.of
2149
+ of: entry.of,
2150
+ options: entry.options,
2151
+ validation: entry.validation
1806
2152
  });
1807
2153
  }
1808
2154
 
@@ -1811,10 +2157,37 @@ function literalSeedCheck() {
1811
2157
  }
1812
2158
 
1813
2159
  function refTypesCheck() {
1814
- return v.check(entry => entry.types === void 0 || entry.type === "doc.ref" || entry.type === "doc.refs", issue => `\`types\` is only valid on \`doc.ref\` / \`doc.refs\` entries, not "${issue.input.type}"`);
2160
+ 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}"`);
2161
+ }
2162
+
2163
+ function choiceOptionsCheck() {
2164
+ return v.check(entry => checkChoiceList({
2165
+ entryType: entry.type,
2166
+ options: entry.options,
2167
+ validation: entry.validation
2168
+ }) === void 0, issue => (checkChoiceList({
2169
+ entryType: issue.input.type,
2170
+ options: issue.input.options,
2171
+ validation: issue.input.validation
2172
+ }) ?? []).join("; "));
2173
+ }
2174
+
2175
+ const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number" ]);
2176
+
2177
+ function scalarValidationCheck() {
2178
+ return v.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
1815
2179
  }
1816
2180
 
1817
- const FieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields(StoredEditableSchema, StoredGroupMembershipSchema)), refTypesCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields(AuthoringEditableSchema, AuthoringGroupMembershipSchema)), refTypesCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v.strictObject({
2181
+ function scalarValidationDeclarationIssues(entry) {
2182
+ const {type: type, validation: validation} = entry;
2183
+ if (validation === void 0) return;
2184
+ if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` values, not "${type}"` ];
2185
+ if (type === "number") return;
2186
+ const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
2187
+ return issues.length === 0 ? void 0 : issues;
2188
+ }
2189
+
2190
+ const FieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields(StoredEditableSchema, StoredGroupMembershipSchema)), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields(AuthoringEditableSchema, AuthoringGroupMembershipSchema)), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v.strictObject({
1818
2191
  type: v.literal("claim"),
1819
2192
  name: FieldEntryName,
1820
2193
  title: v.optional(v.string()),
@@ -1845,17 +2218,25 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
1845
2218
  with: v.optional(v.record(NonEmpty, ConditionSchema)),
1846
2219
  context: v.optional(v.record(NonEmpty, ConditionSchema)),
1847
2220
  onExit: v.optional(picklist([ "detach", "abort" ]))
1848
- }), ActionParamSchema = v.strictObject({
2221
+ }), ActionParamSchema = v.pipe(v.strictObject({
1849
2222
  type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
1850
2223
  name: NonEmpty,
1851
2224
  title: v.optional(v.string()),
1852
2225
  description: v.optional(v.string()),
1853
- required: v.optional(v.boolean())
1854
- });
2226
+ required: v.optional(v.boolean()),
2227
+ options: v.optional(ChoiceOptionsSchema),
2228
+ validation: v.optional(ScalarValidationSchema)
2229
+ }), choiceOptionsCheck(), scalarValidationCheck());
2230
+
2231
+ function hasUniqueSemanticNamespaces(semantics) {
2232
+ const namespaces = semantics.map(semantic => semantic.split(".", 1)[0]);
2233
+ return new Set(namespaces).size === namespaces.length;
2234
+ }
1855
2235
 
1856
2236
  function actionFields(op, group) {
1857
2237
  return {
1858
2238
  name: NonEmpty,
2239
+ semantics: v.optional(v.pipe(v.array(picklist(ACTION_SEMANTICS)), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace"))),
1859
2240
  title: v.optional(v.string()),
1860
2241
  description: v.optional(v.string()),
1861
2242
  group: v.optional(group),
@@ -2044,7 +2425,7 @@ function startKindOf(definition) {
2044
2425
  }
2045
2426
 
2046
2427
  function isSubjectEntry(entry) {
2047
- return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
2428
+ return entry.type === "subject";
2048
2429
  }
2049
2430
 
2050
2431
  function isInputSourced(entry) {
@@ -2546,6 +2927,52 @@ function checkAssigneesEntries(def, issues) {
2546
2927
  });
2547
2928
  }
2548
2929
 
2930
+ function nestedSubjectPaths(shapes, base) {
2931
+ return (shapes ?? []).flatMap((shape, i) => isSubjectEntry(shape) ? [ [ ...base, i, "type" ] ] : [ ...nestedSubjectPaths(shape.fields, [ ...base, i, "fields" ]), ...nestedSubjectPaths(shape.of, [ ...base, i, "of" ]) ]);
2932
+ }
2933
+
2934
+ function checkSubjectEffectOutputs(def, issues) {
2935
+ for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) pushSubjectOutputIssues({
2936
+ action: action,
2937
+ path: [ "stages", i, "activities", j, "actions", a ],
2938
+ issues: issues
2939
+ });
2940
+ }
2941
+
2942
+ function pushSubjectOutputIssues(args) {
2943
+ const {action: action, path: path, issues: issues} = args;
2944
+ for (const [e, effect] of (action.effects ?? []).entries()) {
2945
+ const base = [ ...path, "effects", e, "outputs" ];
2946
+ for (const nestedPath of nestedSubjectPaths(effect.outputs, base)) issues.push({
2947
+ path: nestedPath,
2948
+ message: `effect "${effect.name}" declares a \`subject\` output shape — no subject reader ever looks at effect outputs. Declare \`doc.ref\` for a document-valued output`
2949
+ });
2950
+ }
2951
+ }
2952
+
2953
+ function checkSubjectEntries(def, issues) {
2954
+ for (const {entries: entries, scope: scope, path: path, label: label} of fieldScopes(def)) {
2955
+ const subjects = (entries ?? []).flatMap((entry, n) => isSubjectEntry(entry) ? [ {
2956
+ entry: entry,
2957
+ n: n
2958
+ } ] : []);
2959
+ if (scope !== "workflow") for (const {entry: entry, n: n} of subjects) issues.push({
2960
+ path: [ ...path, n, "type" ],
2961
+ message: `${label} entry "${entry.name}" is a \`subject\` — the subject names the document the WORKFLOW is about, so it is valid only on a workflow-scope entry. Declare \`doc.ref\` for a plain reference here`
2962
+ }); else for (const {n: n} of subjects.slice(1)) issues.push({
2963
+ path: [ ...path, n ],
2964
+ message: "at most one subject-kind field entry per workflow — the runtime identifies THE subject by kind, and a second one makes it ambiguous"
2965
+ });
2966
+ for (const [n, entry] of (entries ?? []).entries()) {
2967
+ const nested = [ ...nestedSubjectPaths(entry.fields, [ ...path, n, "fields" ]), ...nestedSubjectPaths(entry.of, [ ...path, n, "of" ]) ];
2968
+ for (const nestedPath of nested) issues.push({
2969
+ path: nestedPath,
2970
+ message: `${label} entry "${entry.name}" declares a \`subject\` sub-field — a nested subject can never be read as the workflow's subject. Declare \`doc.ref\` for a nested reference`
2971
+ });
2972
+ }
2973
+ }
2974
+ }
2975
+
2549
2976
  function checkActivityTerminalPaths(def, issues) {
2550
2977
  for (const [i, stage] of def.stages.entries()) {
2551
2978
  const resolvable = terminallyResolvableActivities(stage);
@@ -2596,12 +3023,30 @@ function checkStart(def, issues) {
2596
3023
  if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2597
3024
  path: [ "start" ],
2598
3025
  message: "a spawn-only (lifecycle 'child') definition declares `start` — children are instantiated by a parent's `spawn`, never started standalone, so the block would never apply. Remove `start`, or drop `lifecycle: 'child'`"
2599
- }), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
3026
+ }), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), checkStartSubjectVariable(def, issues),
3027
+ def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
2600
3028
  path: [ "fields", n, "required" ],
2601
- message: `start.kind 'autonomous' means runs are initiated by a system reacting to a document, so every required input must be derivable from that triggering document — required entry "${entry.name}" (kind "${entry.type}") is not a document reference. Make it optional, seed it another way (query/literal), or declare it doc.ref / doc.refs`
3029
+ message: `start.kind 'autonomous' means runs are initiated by a system reacting to a document, so every required input must be derivable from that triggering document — required entry "${entry.name}" (kind "${entry.type}") is not the workflow's subject. Make it optional, seed it another way (query/literal), or declare it the \`subject\` entry (the document the run is about)`
2602
3030
  });
2603
3031
  }
2604
3032
 
3033
+ function checkStartSubjectVariable(def, issues) {
3034
+ const subject = (def.fields ?? []).find(isSubjectEntry);
3035
+ for (const [key, condition] of [ [ "filter", def.start?.filter ], [ "allowed", def.start?.allowed ] ]) if (condition !== void 0 && conditionParameterNames(condition).has(SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR)) {
3036
+ if (subject === void 0) {
3037
+ issues.push({
3038
+ path: [ "start", key ],
3039
+ message: `start.${key} reads $${SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR}, but the definition declares no \`subject\` entry — the engine has no prospective subject identity to match. Declare a \`subject\` entry (the document the workflow is about), or remove the variable`
3040
+ });
3041
+ continue;
3042
+ }
3043
+ key === "allowed" && !isInputSourced(subject) && issues.push({
3044
+ path: [ "start", key ],
3045
+ message: `start.allowed reads $${SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR}, but the definition's subject entry "${subject.name}" is not \`input\`-sourced — the start gate cannot bind its prospective subject. Make the subject input-sourced, or remove the variable`
3046
+ });
3047
+ }
3048
+ }
3049
+
2605
3050
  function checkStartFilterReads(def, issues) {
2606
3051
  const filter = def.start?.filter;
2607
3052
  filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
@@ -2609,7 +3054,7 @@ function checkStartFilterReads(def, issues) {
2609
3054
  message: "start.filter reads $fields, but the filter is browse-time-pure — a start surface evaluates it per document, before any inputs exist, so $fields cannot be bound. Move the input-dependent rule to start.allowed, the start-time permission predicate that binds $fields and is enforced by startInstance"
2610
3055
  }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2611
3056
  path: [ "start", "filter" ],
2612
- message: "start.filter reads the candidate document (its root), but the definition declares no required subject entry (a required doc.ref / doc.refs input) — a read surface would never have a document to bind as root, so every root read is GROQ null and the filter silently misevaluates. Declare a required subject entry, or gate on the dataset instead"
3057
+ message: "start.filter reads the candidate document (its root), but the definition declares no `subject` entry — a read surface would never have a document to bind as root, so every root read is GROQ null and the filter silently misevaluates. Declare a `subject` entry (the document the workflow is about), or gate on the dataset instead"
2613
3058
  }));
2614
3059
  }
2615
3060
 
@@ -2926,7 +3371,7 @@ function checkFieldReadOpValues(def, issues) {
2926
3371
  ops: site.ops,
2927
3372
  path: site.path,
2928
3373
  label: site.label,
2929
- activityEntries: entryMap(site.activity.fields)
3374
+ activityEntries: site.activity.fields ?? []
2930
3375
  });
2931
3376
  }
2932
3377
 
@@ -2948,6 +3393,9 @@ function fieldReadsIn(value, path) {
2948
3393
 
2949
3394
  function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
2950
3395
  const hosts = [ {
3396
+ scope: "activity",
3397
+ entries: activityEntries
3398
+ }, {
2951
3399
  scope: "stage",
2952
3400
  entries: stageEntries
2953
3401
  }, {
@@ -2960,8 +3408,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2960
3408
  message: opFieldReadMissMessage({
2961
3409
  read: read,
2962
3410
  where: where,
2963
- hosts: hosts,
2964
- activityEntries: activityEntries
3411
+ hosts: hosts
2965
3412
  })
2966
3413
  });
2967
3414
  return;
@@ -2975,9 +3422,9 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2975
3422
  });
2976
3423
  }
2977
3424
 
2978
- function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityEntries: activityEntries}) {
2979
- const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityEntries?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
2980
- return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty.${activityHint} Known: ${known.join(", ") || "(none)"}`;
3425
+ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
3426
+ const searched = read.scope === void 0 ? "activity, stage, or workflow scope" : `${read.scope} scope`, known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
3427
+ return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty. Known: ${known.join(", ") || "(none)"}`;
2981
3428
  }
2982
3429
 
2983
3430
  function checkUpdateWhereOps(def, issues) {
@@ -3140,6 +3587,7 @@ const SCALAR = {
3140
3587
  kind: "list",
3141
3588
  item: GDR_VALUE
3142
3589
  }),
3590
+ subject: () => DOC_CONTENT,
3143
3591
  "release.ref": () => RELEASE_VALUE,
3144
3592
  string: () => SCALAR,
3145
3593
  text: () => SCALAR,
@@ -3164,7 +3612,8 @@ const SCALAR = {
3164
3612
  })
3165
3613
  }, START_ALLOWED_VALUE_NODES = {
3166
3614
  ...VALUE_NODES,
3167
- "doc.ref": () => GDR_VALUE
3615
+ "doc.ref": () => GDR_VALUE,
3616
+ subject: () => GDR_VALUE
3168
3617
  };
3169
3618
 
3170
3619
  function valueNodeFor(shape, nodes) {
@@ -3236,9 +3685,10 @@ function checkWorkflowInvariants(def) {
3236
3685
  checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
3237
3686
  checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3238
3687
  checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
3239
- checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
3688
+ checkAssigneesEntries(def, issues), checkSubjectEntries(def, issues), checkSubjectEffectOutputs(def, issues),
3689
+ checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
3240
3690
  checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
3241
3691
  checkGroups(def, issues), issues;
3242
3692
  }
3243
3693
 
3244
- export { ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, 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, START_ALLOWED_VARS, START_FILTER_VARS, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
3694
+ export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DATA_MODEL_CHANGES, DATA_MODEL_MIN_READER, DATA_MODEL_VERSION, 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, ModelVersionAheadError, NonEmptyString, PersistedDocShapeError, READER_MODEL_ROLLOUT_URL, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, ReaderModelAcknowledgementError, START_ALLOWED_VARS, START_FILTER_VARS, SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR, SpawnContractsInvalidError, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, assertReadableModel, assertReaderModelAcknowledgement, checkFieldValue, checkWorkflowInvariants, choiceValueIssues, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldTreeShape, fieldValueSchemas, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, isUnprimed, labelFor, minReaderModelOf, modelStampFor, modelVersionOf, parentRef, parseDefinitionSnapshot, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, requiredModelFeatures, requiredReaderModel, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, terminalState, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };