@sanity/workflow-engine 0.16.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,82 +509,17 @@ 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
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
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) {
305
- if (!TAG_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`);
515
+ if (!LAKE_ID_SEGMENT_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS})`);
306
516
  }
307
517
 
308
518
  function tagScopeFilter() {
309
519
  return "tag == $tag";
310
520
  }
311
521
 
312
- const NonEmptyString = v.pipe(v.string(), v.nonEmpty("must not be empty"));
522
+ const NonEmptyString$1 = v.pipe(v.string(), v.nonEmpty("must not be empty"));
313
523
 
314
524
  function asPredicate(validate) {
315
525
  return value => {
@@ -323,21 +533,22 @@ function asPredicate(validate) {
323
533
 
324
534
  const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v.variant("type", [ v.object({
325
535
  type: v.literal("dataset"),
326
- id: v.pipe(NonEmptyString, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
536
+ id: v.pipe(NonEmptyString$1, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
327
537
  }), v.object({
328
538
  type: v.literal("canvas"),
329
- id: NonEmptyString
539
+ id: NonEmptyString$1
330
540
  }), v.object({
331
541
  type: v.literal("media-library"),
332
- id: NonEmptyString
542
+ id: NonEmptyString$1
333
543
  }), v.object({
334
544
  type: v.literal("dashboard"),
335
- id: NonEmptyString
545
+ id: NonEmptyString$1
336
546
  }) ]), ResourceBindingSchema = v.object({
337
- name: v.pipe(NonEmptyString, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
547
+ name: v.pipe(NonEmptyString$1, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
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
- name: NonEmptyString,
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"))),
@@ -502,6 +713,9 @@ function desugarStart(start) {
502
713
  kind: start.kind ?? "interactive",
503
714
  ...start.filter !== void 0 ? {
504
715
  filter: start.filter
716
+ } : {},
717
+ ...start.allowed !== void 0 ? {
718
+ allowed: start.allowed
505
719
  } : {}
506
720
  };
507
721
  }
@@ -577,6 +791,8 @@ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
577
791
  required: entry.required,
578
792
  initialValue: entry.initialValue,
579
793
  editable: editable,
794
+ options: entry.options,
795
+ validation: entry.validation,
580
796
  types: entry.types,
581
797
  fields: entry.fields,
582
798
  of: entry.of
@@ -719,7 +935,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
719
935
  };
720
936
  }
721
937
 
722
- const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
938
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref" ];
723
939
 
724
940
  function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
725
941
  if (target === void 0 || target.type === "url") return target;
@@ -740,18 +956,15 @@ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
740
956
  };
741
957
  }
742
958
 
743
- function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
744
- if ("type" in action) return desugarClaimAction({
745
- action: action,
746
- path: path,
747
- env: env,
748
- ctx: ctx
749
- });
750
- action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
959
+ function reportEmptyActionRoles({action: action, path: path, ctx: ctx}) {
960
+ action.roles === void 0 || action.roles.length > 0 || ctx.issues.push({
751
961
  path: [ ...path, "roles" ],
752
962
  message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
753
963
  });
754
- 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({
755
968
  ops: action.ops,
756
969
  path: [ ...path, "ops" ],
757
970
  env: env,
@@ -762,9 +975,32 @@ function desugarAction({action: action, path: path, env: env, activityName: acti
762
975
  type: "status.set",
763
976
  activity: activityName,
764
977
  status: action.status
765
- }), {
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 {
766
1001
  ...stripUndefined({
767
1002
  name: action.name,
1003
+ semantics: action.semantics,
768
1004
  title: action.title,
769
1005
  description: action.description,
770
1006
  group: normalizeGroup(action.group),
@@ -1050,12 +1286,43 @@ function conditionParameterNames(groq2) {
1050
1286
  }
1051
1287
 
1052
1288
  function conditionFieldReadNames(groq2) {
1053
- const read = /* @__PURE__ */ new Set;
1054
- return walkAstNodes(tryParseGroq(groq2), node => {
1055
- if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
1056
- const base = node.base;
1057
- base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
1058
- }), read;
1289
+ return new Set(conditionFieldReads(groq2).map(read => read.name));
1290
+ }
1291
+
1292
+ function conditionFieldReads(groq2) {
1293
+ const reads = /* @__PURE__ */ new Map;
1294
+ return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
1295
+ }
1296
+
1297
+ function collectFieldReads(node, reads) {
1298
+ if (Array.isArray(node)) {
1299
+ for (const item of node) collectFieldReads(item, reads);
1300
+ return;
1301
+ }
1302
+ if (typeof node != "object" || node === null) return;
1303
+ const chain = fieldsAttributeChain(node);
1304
+ if (chain !== void 0) {
1305
+ const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
1306
+ reads.set(`${name}.${path ?? ""}`, {
1307
+ name: name,
1308
+ path: path
1309
+ });
1310
+ return;
1311
+ }
1312
+ for (const value of Object.values(node)) collectFieldReads(value, reads);
1313
+ }
1314
+
1315
+ function fieldsAttributeChain(node) {
1316
+ const names = [];
1317
+ let current = node;
1318
+ for (;typeof current == "object" && current !== null; ) {
1319
+ const typed = current;
1320
+ if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
1321
+ names.unshift(typed.name);
1322
+ const base = typed.base;
1323
+ if (base?.type === "Parameter" && base.name === "fields") return names;
1324
+ current = base;
1325
+ }
1059
1326
  }
1060
1327
 
1061
1328
  function conditionEffectReads(groq2) {
@@ -1120,7 +1387,7 @@ function isTerminalActivityStatus(status) {
1120
1387
  return TERMINAL_ACTIVITY_STATUSES.includes(status);
1121
1388
  }
1122
1389
 
1123
- const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], CONDITION_VARS = [ {
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 = [ {
1124
1391
  name: "self",
1125
1392
  binding: "always",
1126
1393
  label: "this workflow instance",
@@ -1129,7 +1396,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1129
1396
  name: "fields",
1130
1397
  binding: "always",
1131
1398
  label: "the workflow's fields",
1132
- description: "Declared field entries rendered by name (`$fields.<name>` is the value, no envelope). Stage/activity scopes overlay lexically; `doc.ref` values render the hydrated document when the snapshot holds it."
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."
1133
1400
  }, {
1134
1401
  name: "parent",
1135
1402
  binding: "always",
@@ -1199,7 +1466,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1199
1466
  name: "row",
1200
1467
  binding: "spawn",
1201
1468
  label: "the spawned row",
1202
- description: "One `spawn.forEach` result row, bound while its `with` map evaluates — and the row under test while a where-op `where` evaluates (per row)."
1469
+ 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."
1203
1470
  }, {
1204
1471
  name: "params",
1205
1472
  binding: "caller",
@@ -1210,25 +1477,161 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1210
1477
  binding: "always",
1211
1478
  label: "the spawned subworkflows",
1212
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`."
1213
- } ], RESERVED_CONDITION_VARS = CONDITION_VARS.map(v2 => v2.name), FILTER_SCOPE_VARS = CONDITION_VARS.filter(v2 => v2.binding === "always").map(v2 => v2.name), CALLER_BOUND_VARS = CONDITION_VARS.filter(v2 => v2.binding === "caller").map(v2 => v2.name), START_FILTER_VARS = [ {
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 = [ {
1214
1481
  name: "tag",
1482
+ label: "this engine tag",
1215
1483
  description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1216
1484
  }, {
1217
1485
  name: "definition",
1218
- description: "The `name` of the definition under evaluation (its own start.filter binds it)."
1486
+ label: "this workflow definition",
1487
+ description: "The `name` of the definition under evaluation (its own start block binds it)."
1219
1488
  }, {
1220
1489
  name: "now",
1490
+ label: "the current time",
1221
1491
  description: "The ISO clock reading of the evaluating engine."
1222
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."
1496
+ } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1223
1497
  name: "fields",
1224
- description: "The candidate initialFields by entry name, when the read surface has them (the Studio start control: yes; a per-doc picker: no). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble."
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."
1225
1500
  } ], GUARD_PREDICATE_VARS = [ {
1226
1501
  name: "guard",
1227
1502
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
1228
1503
  }, {
1229
1504
  name: "mutation",
1230
1505
  description: "The attempted mutation — `mutation.action` is the write kind being gated."
1231
- } ], ACTOR_KINDS = [ "person", "agent", "system" ];
1506
+ } ], NonEmptyString = v.pipe(v.string(), v.minLength(1, "must be a non-empty string"));
1507
+
1508
+ function isParseableInstant(value) {
1509
+ return typeof value == "string" && !Number.isNaN(Date.parse(value));
1510
+ }
1511
+
1512
+ const IsoTimestamp = v.pipe(v.string(), v.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
1513
+
1514
+ function tolerantObject() {
1515
+ return (entries, ..._exact) => v.looseObject(entries);
1516
+ }
1517
+
1518
+ function tolerantEntries() {
1519
+ return (entries, ..._exact) => entries;
1520
+ }
1521
+
1522
+ function schemaTreeShape(schema) {
1523
+ return walkSchemaShape(schema, /* @__PURE__ */ new Set);
1524
+ }
1525
+
1526
+ function walkSchemaShape(node, path) {
1527
+ if (typeof node != "object" || node === null) return "unknown";
1528
+ if (path.has(node)) return "(circular)";
1529
+ path.add(node);
1530
+ try {
1531
+ const schema = node;
1532
+ return containerShape(schema, path) ?? leafShape(schema);
1533
+ } finally {
1534
+ path.delete(node);
1535
+ }
1536
+ }
1537
+
1538
+ const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
1539
+ union: schema.options.map(option => walkSchemaShape(option, path))
1540
+ }), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
1541
+ strict_object: objectWalker,
1542
+ loose_object: objectWalker,
1543
+ object: objectWalker,
1544
+ array: (schema, path) => ({
1545
+ array: walkSchemaShape(schema.item, path)
1546
+ }),
1547
+ record: (schema, path) => ({
1548
+ record: walkSchemaShape(schema.value, path)
1549
+ }),
1550
+ union: unionWalker,
1551
+ variant: unionWalker,
1552
+ lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
1553
+ exact_optional: unwrapWalker,
1554
+ optional: unwrapWalker,
1555
+ nullable: unwrapWalker
1556
+ };
1557
+
1558
+ function containerShape(schema, path) {
1559
+ return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
1560
+ }
1561
+
1562
+ function objectEntriesShape(entries, path) {
1563
+ const out = {};
1564
+ for (const [key, entry] of Object.entries(entries)) {
1565
+ const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
1566
+ out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
1567
+ }
1568
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
1569
+ }
1570
+
1571
+ function isOptionalEntry(entry) {
1572
+ if (typeof entry != "object" || entry === null) return !1;
1573
+ const kind = entry.type;
1574
+ return kind === "exact_optional" || kind === "optional";
1575
+ }
1576
+
1577
+ const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
1578
+
1579
+ function leafShape(schema) {
1580
+ if (schema.type === "picklist") return schema.options.join(" | ");
1581
+ if (schema.type === "literal") return `literal ${String(schema.literal)}`;
1582
+ if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
1583
+ 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`);
1584
+ return String(schema.type);
1585
+ }
1586
+
1587
+ function formatValidationError(label, issues) {
1588
+ const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1589
+ return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1590
+ }
1591
+
1592
+ function issuesFromValibot(issues) {
1593
+ return issues.map(issue => ({
1594
+ path: issue.path ? issue.path.map(item => item.key) : [],
1595
+ message: issue.message
1596
+ }));
1597
+ }
1598
+
1599
+ function formatIssuePath(path) {
1600
+ let out = "";
1601
+ for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1602
+ return out;
1603
+ }
1604
+
1605
+ class PersistedDocShapeError extends WorkflowError {
1606
+ documentId;
1607
+ documentType;
1608
+ issues;
1609
+ constructor(args) {
1610
+ super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
1611
+ this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
1612
+ this.issues = args.issues;
1613
+ }
1614
+ }
1615
+
1616
+ function parsePersistedDoc(args) {
1617
+ const result = v.safeParse(args.schema, args.doc);
1618
+ if (!result.success) throw new PersistedDocShapeError({
1619
+ documentId: documentIdOf(args.doc),
1620
+ documentType: args.docType,
1621
+ issues: issuesFromValibot(result.issues)
1622
+ });
1623
+ return result.output;
1624
+ }
1625
+
1626
+ function documentIdOf(doc) {
1627
+ if (typeof doc == "object" && doc !== null) {
1628
+ const id = doc._id;
1629
+ if (typeof id == "string") return id;
1630
+ }
1631
+ return "(unknown id)";
1632
+ }
1633
+
1634
+ const ACTOR_KINDS = [ "person", "agent", "system" ];
1232
1635
 
1233
1636
  function releaseDocId(releaseName) {
1234
1637
  return `_.releases.${releaseName}`;
@@ -1243,6 +1646,18 @@ function releaseRef({res: res, releaseName: releaseName}) {
1243
1646
  };
1244
1647
  }
1245
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
+
1246
1661
  function isTodoListItem(row) {
1247
1662
  if (typeof row != "object" || row === null) return !1;
1248
1663
  const candidate = row, status = candidate.status;
@@ -1275,29 +1690,53 @@ class FieldValueShapeError extends WorkflowError {
1275
1690
  }
1276
1691
  }
1277
1692
 
1278
- const GdrShape = v.looseObject({
1279
- id: v.pipe(v.string(), v.check(s => isGdrUri(s), "must be a GDR URI")),
1280
- type: v.pipe(v.string(), v.minLength(1))
1281
- }), ReleaseRefShape = v.pipe(v.looseObject({
1282
- id: v.pipe(v.string(), v.check(s => isGdrUri(s), "must be a GDR URI")),
1693
+ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
1694
+ id: GdrUriSchema,
1695
+ type: NonEmptyString
1696
+ }), ReleaseRefShape = v.pipe(tolerantObject()({
1697
+ id: GdrUriSchema,
1283
1698
  type: v.literal("system.release"),
1284
- releaseName: v.pipe(v.string(), v.minLength(1))
1285
- }), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = v.looseObject({
1699
+ releaseName: NonEmptyString
1700
+ }), 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()({
1286
1701
  kind: v.picklist(ACTOR_KINDS),
1287
- id: v.pipe(v.string(), v.minLength(1)),
1288
- roles: v.optional(v.array(v.string())),
1289
- onBehalfOf: v.optional(v.string())
1290
- }), AssigneeShape = v.union([ v.looseObject({
1702
+ id: NonEmptyString,
1703
+ roles: v.exactOptional(v.array(v.string())),
1704
+ onBehalfOf: v.exactOptional(v.string())
1705
+ }), AssigneeShape = v.union([ tolerantObject()({
1291
1706
  type: v.literal("user"),
1292
- id: v.pipe(v.string(), v.minLength(1))
1293
- }), v.looseObject({
1707
+ id: NonEmptyString
1708
+ }), tolerantObject()({
1294
1709
  type: v.literal("role"),
1295
- role: v.pipe(v.string(), v.minLength(1))
1296
- }) ]), 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(), v.pipe(v.string(), v.check(s => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")) ]), 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, valueSchemas = {
1710
+ role: NonEmptyString
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 = {
1297
1736
  "doc.ref": v.union([ v.null(), GdrShape ]),
1298
1737
  "doc.refs": v.array(GdrShape),
1738
+ subject: v.union([ v.null(), GdrShape ]),
1299
1739
  "release.ref": v.union([ v.null(), ReleaseRefShape ]),
1300
- query: v.any(),
1301
1740
  string: NullableString,
1302
1741
  text: NullableString,
1303
1742
  number: NullableNumber,
@@ -1308,10 +1747,62 @@ const GdrShape = v.looseObject({
1308
1747
  actor: v.union([ v.null(), ActorShape ]),
1309
1748
  assignee: v.union([ v.null(), AssigneeShape ]),
1310
1749
  assignees: v.array(AssigneeShape)
1750
+ }, valueSchemas = {
1751
+ ...fieldValueSchemas,
1752
+ query: v.any()
1311
1753
  };
1312
1754
 
1313
1755
  function shapeValueSchema(shape, leaf) {
1314
- 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("; ")));
1315
1806
  }
1316
1807
 
1317
1808
  function objectSchema(fields, leaf) {
@@ -1334,7 +1825,7 @@ function appendItemSchema(entryType, shape) {
1334
1825
  function rejectedRefTypes(args) {
1335
1826
  const {entryType: entryType, types: types, value: value} = args;
1336
1827
  if (types === void 0 || value === null || value === void 0) return [];
1337
- if (entryType !== "doc.ref" && entryType !== "doc.refs") return [];
1828
+ if (!refKindAcceptsTypes(entryType)) return [];
1338
1829
  let items = [ value ];
1339
1830
  return entryType === "doc.refs" && (items = Array.isArray(value) ? value : []),
1340
1831
  [ ...new Set(items.map(gdrTypeOf).filter(t => t !== void 0 && !types.includes(t))) ];
@@ -1369,6 +1860,10 @@ function checkValueAgainst(args, leaf) {
1369
1860
  entryType: args.entryType,
1370
1861
  types: args.types,
1371
1862
  value: args.value
1863
+ }) ?? choiceValueIssues(args.options, args.value) ?? scalarValidationIssues({
1864
+ entryType: args.entryType,
1865
+ validation: args.validation,
1866
+ value: args.value
1372
1867
  }) : formatIssues(result.issues);
1373
1868
  }
1374
1869
 
@@ -1394,15 +1889,16 @@ function isBareSeedId(id) {
1394
1889
 
1395
1890
  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({
1396
1891
  id: AuthoringRefId,
1397
- type: v.pipe(v.string(), v.minLength(1))
1892
+ type: NonEmptyString
1398
1893
  }), seedValueSchemas = {
1399
1894
  ...valueSchemas,
1400
1895
  "doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
1401
1896
  "doc.refs": v.array(AuthoringGdrShape),
1897
+ subject: v.union([ v.null(), AuthoringGdrShape ]),
1402
1898
  "release.ref": v.union([ v.null(), v.looseObject({
1403
1899
  id: AuthoringRefId,
1404
1900
  type: v.literal("system.release"),
1405
- releaseName: v.pipe(v.string(), v.minLength(1))
1901
+ releaseName: NonEmptyString
1406
1902
  }) ])
1407
1903
  };
1408
1904
 
@@ -1438,14 +1934,14 @@ function validateFieldAppendItem(args) {
1438
1934
  });
1439
1935
  }
1440
1936
 
1441
- function formatIssues(issues) {
1937
+ function formatIssues(issues, formatMessage = issue => issue.message) {
1442
1938
  return issues.map(i => {
1443
1939
  const keys = i.path?.map(p => p.key) ?? [];
1444
- return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
1940
+ return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(i)}`;
1445
1941
  });
1446
1942
  }
1447
1943
 
1448
- const NonEmpty = v.pipe(v.string(), v.minLength(1, "must be a non-empty string")), PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1944
+ const NonEmpty = NonEmptyString, PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1449
1945
 
1450
1946
  function groqIdentifier(referencedAs) {
1451
1947
  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`));
@@ -1567,7 +2063,15 @@ function groupMembershipNames(group) {
1567
2063
  return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
1568
2064
  }
1569
2065
 
1570
- 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
+ });
1571
2075
 
1572
2076
  function asShape(input) {
1573
2077
  return typeof input == "object" && input !== null ? input : {};
@@ -1601,14 +2105,16 @@ function compositeChecked(entries) {
1601
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\``));
1602
2106
  }
1603
2107
 
1604
- const FieldShapeSchema = v.lazy(() => compositeChecked({
2108
+ const FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
1605
2109
  type: FieldValueKindSchema,
1606
2110
  name: FieldEntryName,
1607
2111
  title: v.optional(v.string()),
1608
2112
  description: v.optional(v.string()),
2113
+ options: v.optional(ChoiceOptionsSchema),
2114
+ validation: v.optional(ScalarValidationSchema),
1609
2115
  fields: v.optional(v.array(FieldShapeSchema)),
1610
2116
  of: v.optional(v.array(FieldShapeSchema))
1611
- })), 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 ]);
1612
2118
 
1613
2119
  function fieldBase(editable, group) {
1614
2120
  return {
@@ -1626,6 +2132,8 @@ function fieldEntryFields(editable, group) {
1626
2132
  return {
1627
2133
  type: FieldKindSchema,
1628
2134
  ...fieldBase(editable, group),
2135
+ options: v.optional(ChoiceOptionsSchema),
2136
+ validation: v.optional(ScalarValidationSchema),
1629
2137
  types: v.optional(v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
1630
2138
  fields: v.optional(v.array(FieldShapeSchema)),
1631
2139
  of: v.optional(v.array(FieldShapeSchema))
@@ -1638,7 +2146,9 @@ function literalSeedIssues(entry) {
1638
2146
  value: entry.initialValue.value,
1639
2147
  types: entry.types,
1640
2148
  fields: entry.fields,
1641
- of: entry.of
2149
+ of: entry.of,
2150
+ options: entry.options,
2151
+ validation: entry.validation
1642
2152
  });
1643
2153
  }
1644
2154
 
@@ -1647,10 +2157,37 @@ function literalSeedCheck() {
1647
2157
  }
1648
2158
 
1649
2159
  function refTypesCheck() {
1650
- 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}"`);
1651
2161
  }
1652
2162
 
1653
- 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({
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("; "));
2179
+ }
2180
+
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({
1654
2191
  type: v.literal("claim"),
1655
2192
  name: FieldEntryName,
1656
2193
  title: v.optional(v.string()),
@@ -1681,17 +2218,25 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
1681
2218
  with: v.optional(v.record(NonEmpty, ConditionSchema)),
1682
2219
  context: v.optional(v.record(NonEmpty, ConditionSchema)),
1683
2220
  onExit: v.optional(picklist([ "detach", "abort" ]))
1684
- }), ActionParamSchema = v.strictObject({
2221
+ }), ActionParamSchema = v.pipe(v.strictObject({
1685
2222
  type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
1686
2223
  name: NonEmpty,
1687
2224
  title: v.optional(v.string()),
1688
2225
  description: v.optional(v.string()),
1689
- required: v.optional(v.boolean())
1690
- });
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
+ }
1691
2235
 
1692
2236
  function actionFields(op, group) {
1693
2237
  return {
1694
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"))),
1695
2240
  title: v.optional(v.string()),
1696
2241
  description: v.optional(v.string()),
1697
2242
  group: v.optional(group),
@@ -1828,7 +2373,8 @@ const StoredStageSchema = pinned()(v.strictObject(stageFields({
1828
2373
  function startFields(kind) {
1829
2374
  return {
1830
2375
  kind: kind,
1831
- filter: v.optional(ConditionSchema)
2376
+ filter: v.optional(ConditionSchema),
2377
+ allowed: v.optional(ConditionSchema)
1832
2378
  };
1833
2379
  }
1834
2380
 
@@ -1879,31 +2425,13 @@ function startKindOf(definition) {
1879
2425
  }
1880
2426
 
1881
2427
  function isSubjectEntry(entry) {
1882
- return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
2428
+ return entry.type === "subject";
1883
2429
  }
1884
2430
 
1885
2431
  function isInputSourced(entry) {
1886
2432
  return entry.initialValue?.type === "input";
1887
2433
  }
1888
2434
 
1889
- function formatValidationError(label, issues) {
1890
- const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1891
- return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1892
- }
1893
-
1894
- function issuesFromValibot(issues) {
1895
- return issues.map(issue => ({
1896
- path: issue.path ? issue.path.map(item => item.key) : [],
1897
- message: issue.message
1898
- }));
1899
- }
1900
-
1901
- function formatIssuePath(path) {
1902
- let out = "";
1903
- for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1904
- return out;
1905
- }
1906
-
1907
2435
  function parseOrThrow({schema: schema, input: input, label: label}) {
1908
2436
  const result = v.safeParse(schema, input);
1909
2437
  if (!result.success) throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
@@ -1932,6 +2460,13 @@ function checkDuplicates({names: names, what: what, issues: issues}) {
1932
2460
  return seen;
1933
2461
  }
1934
2462
 
2463
+ function checkDefinitionName(def, issues) {
2464
+ LAKE_ID_SEGMENT_RE.test(def.name) || issues.push({
2465
+ path: [ "name" ],
2466
+ message: `definition name "${def.name}" does not match the definition-name grammar ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS}) — the name is interpolated into every deployed document id (\`<tag>.<name>.v<version>\`), which the engine keeps to the same segment grammar as the tag`
2467
+ });
2468
+ }
2469
+
1935
2470
  function checkStages(def, issues) {
1936
2471
  const stageNames = checkDuplicates({
1937
2472
  names: def.stages.map((stage, i) => ({
@@ -2068,6 +2603,10 @@ function checkGuardNames(def, issues) {
2068
2603
  what: "guard name (the guard lake _id derives from it — unique per definition)",
2069
2604
  issues: issues
2070
2605
  });
2606
+ for (const {name: name, path: path} of sites) LAKE_ID_SEGMENT_RE.test(name) || issues.push({
2607
+ path: path,
2608
+ message: `guard name "${name}" does not match the guard-name grammar ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS}) — the guard's lake _id derives from it at stage entry, so an invalid name would wedge the entering commit with an opaque lake error instead of failing here`
2609
+ });
2071
2610
  }
2072
2611
 
2073
2612
  function fieldScopes(def) {
@@ -2138,7 +2677,11 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
2138
2677
  const reads = conditionParameterNames(groq2);
2139
2678
  for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
2140
2679
  path: [ "predicates", name ],
2141
- message: `predicate "${name}" reads $${caller} — predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
2680
+ message: `predicate "${name}" reads $${caller} — predicates pre-evaluate caller-free; compose caller gates at the condition site instead`
2681
+ });
2682
+ reads.has("row") && issues.push({
2683
+ path: [ "predicates", name ],
2684
+ message: `predicate "${name}" reads $row — ${ROW_BINDING_CLAUSE}; predicates pre-evaluate once per instance, so the read is GROQ null and the predicate can never hold`
2142
2685
  });
2143
2686
  for (const other of names) other === name || !reads.has(other) || issues.push({
2144
2687
  path: [ "predicates", name ],
@@ -2146,24 +2689,35 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
2146
2689
  });
2147
2690
  }
2148
2691
 
2149
- function entryNames(entries) {
2150
- return new Set((entries ?? []).map(entry => entry.name));
2692
+ function entryMap(entries) {
2693
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
2151
2694
  }
2152
2695
 
2153
- function checkUnboundCallerVars(def, issues) {
2696
+ function checkUnboundConditionVars(def, issues) {
2154
2697
  for (const site of conditionSites(def)) {
2155
2698
  const reads = conditionParameterNames(site.groq);
2156
- for (const name of unboundCallerVars(site.policy)) reads.has(name) && issues.push({
2699
+ for (const name of unboundVarsAt(site)) reads.has(name) && issues.push({
2157
2700
  path: site.path,
2158
- message: callerVarMessage(site, name)
2701
+ message: name === "row" ? rowVarMessage(site) : callerVarMessage(site, name)
2159
2702
  });
2160
2703
  }
2161
2704
  }
2162
2705
 
2706
+ function unboundVarsAt(site) {
2707
+ const callerVars = unboundCallerVars(site.policy);
2708
+ return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
2709
+ }
2710
+
2163
2711
  function unboundCallerVars(policy) {
2164
2712
  return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
2165
2713
  }
2166
2714
 
2715
+ const ROW_BINDING_CLAUSE = "$row (the discovered row in a spawn projection; the stored row under test in a where-op) is bound only while a spawn `with` projection or a where-op `where` evaluates";
2716
+
2717
+ function rowVarMessage(site) {
2718
+ return `${site.label} reads $row — ${ROW_BINDING_CLAUSE}; this site never binds it, so the read is GROQ null and the condition can never resolve. Move the per-row read into \`spawn.with\` or a where-op, or gate on instance state instead`;
2719
+ }
2720
+
2167
2721
  function callerVarMessage(site, name) {
2168
2722
  return site.policy === "cascade" ? `${site.label} reads $${name} — cascade gates (transition \`when\`s, activity \`filter\`s, a cascade-fired action's \`when\`/\`filter\`) must resolve identically no matter whose token drives the cascade ($assigned is constant false, the other caller vars hold no value); gate on instance state (e.g. a field an action wrote), or pin executing identities with \`roles\`` : site.policy === "caller-bound" ? `${site.label} reads $${name} — $params (the firing action's args) is bound only while the action's effect bindings and where-op \`where\`s evaluate; this site never binds it, so the condition could never pass. Bind $params in an effect binding or a where-op instead, or gate on a field an action wrote` : site.policy === "triggered-payload" && name === "params" ? `${site.label} reads $params — a cascade-fired action has no caller to supply args, so $params never holds a value in its payload; read fields or effect outputs instead` : `${site.label} reads $can — $can (the caller's grants) is bound only in the caller-bound projection (action filters, requirements, editable predicates) and never holds a value in cascade conditions; move the check to one of those sites or drop $can`;
2169
2723
  }
@@ -2171,7 +2725,7 @@ function callerVarMessage(site, name) {
2171
2725
  function conditionSites(def) {
2172
2726
  const sites = [], workflowLayer = {
2173
2727
  scope: "workflow",
2174
- names: entryNames(def.fields)
2728
+ entries: entryMap(def.fields)
2175
2729
  };
2176
2730
  collectEditableSites({
2177
2731
  entries: def.fields,
@@ -2192,10 +2746,10 @@ function conditionSites(def) {
2192
2746
  function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
2193
2747
  const stageLayer = {
2194
2748
  scope: "stage",
2195
- names: entryNames(stage.fields)
2749
+ entries: entryMap(stage.fields)
2196
2750
  }, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
2197
2751
  scope: "activity",
2198
- names: entryNames(activity.fields)
2752
+ entries: entryMap(activity.fields)
2199
2753
  })) ];
2200
2754
  for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
2201
2755
  groq: t.when,
@@ -2209,6 +2763,7 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
2209
2763
  path: [ "stages", i, "fields" ],
2210
2764
  labelPrefix: `stage "${stage.name}"`,
2211
2765
  fields: editableWindow,
2766
+ unionWindow: !0,
2212
2767
  sites: sites
2213
2768
  });
2214
2769
  for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
@@ -2216,23 +2771,28 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
2216
2771
  path: [ "stages", i, "editable", name ],
2217
2772
  label: `stage "${stage.name}" editable override "${name}"`,
2218
2773
  policy: "caller-bound",
2219
- fields: editableWindow
2774
+ fields: editableWindow,
2775
+ unionWindow: !0
2220
2776
  });
2221
2777
  for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
2222
2778
  activity: activity,
2223
2779
  path: [ "stages", i, "activities", j ],
2224
2780
  stageFields: stageFields2,
2781
+ workflowLayer: workflowLayer,
2225
2782
  sites: sites
2226
2783
  });
2227
2784
  }
2228
2785
 
2229
- function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
2786
+ function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, unionWindow: unionWindow, sites: sites}) {
2230
2787
  for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
2231
2788
  groq: entry.editable,
2232
2789
  path: [ ...path, n, "editable" ],
2233
2790
  label: `${labelPrefix} field "${entry.name}" editable`,
2234
2791
  policy: "caller-bound",
2235
- fields: fields
2792
+ fields: fields,
2793
+ ...unionWindow === !0 ? {
2794
+ unionWindow: !0
2795
+ } : {}
2236
2796
  });
2237
2797
  }
2238
2798
 
@@ -2244,14 +2804,15 @@ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy
2244
2804
  ...policy !== void 0 ? {
2245
2805
  policy: policy
2246
2806
  } : {},
2807
+ bindsRow: !0,
2247
2808
  fields: fields
2248
2809
  });
2249
2810
  }
2250
2811
 
2251
- function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
2812
+ function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, workflowLayer: workflowLayer, sites: sites}) {
2252
2813
  const fields = [ {
2253
2814
  scope: "activity",
2254
- names: entryNames(activity.fields)
2815
+ entries: entryMap(activity.fields)
2255
2816
  }, ...stageFields2 ];
2256
2817
  activity.filter !== void 0 && sites.push({
2257
2818
  groq: activity.filter,
@@ -2277,11 +2838,12 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
2277
2838
  activity: activity,
2278
2839
  path: path,
2279
2840
  fields: fields,
2841
+ workflowLayer: workflowLayer,
2280
2842
  sites: sites
2281
2843
  });
2282
2844
  }
2283
2845
 
2284
- function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
2846
+ function collectActionConditionSites({activity: activity, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2285
2847
  for (const [a, action] of (activity.actions ?? []).entries()) {
2286
2848
  const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
2287
2849
  action.when !== void 0 && sites.push({
@@ -2316,12 +2878,13 @@ function collectActionConditionSites({activity: activity, path: path, fields: fi
2316
2878
  action: action,
2317
2879
  path: actionPath,
2318
2880
  fields: fields,
2881
+ workflowLayer: workflowLayer,
2319
2882
  sites: sites
2320
2883
  });
2321
2884
  }
2322
2885
  }
2323
2886
 
2324
- function collectSpawnSites({action: action, path: path, fields: fields, sites: sites}) {
2887
+ function collectSpawnSites({action: action, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2325
2888
  const spawn = action.spawn;
2326
2889
  if (spawn === void 0) return;
2327
2890
  const spawnPath = [ ...path, "spawn" ];
@@ -2330,13 +2893,17 @@ function collectSpawnSites({action: action, path: path, fields: fields, sites: s
2330
2893
  path: [ ...spawnPath, "forEach" ],
2331
2894
  label: `action "${action.name}".spawn.forEach`,
2332
2895
  policy: "cascade",
2333
- fields: fields
2896
+ fields: [ workflowLayer ],
2897
+ windowNote: "spawn.forEach evaluates against workflow-scope $fields only — stage/activity fields are never bound at discovery time"
2334
2898
  });
2335
- for (const [group, record] of [ [ "with", spawn.with ], [ "context", spawn.context ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2899
+ for (const [group, record, bindsRow] of [ [ "with", spawn.with, !0 ], [ "context", spawn.context, !1 ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2336
2900
  groq: groq2,
2337
2901
  path: [ ...spawnPath, group, key ],
2338
2902
  label: `action "${action.name}".spawn.${group} "${key}"`,
2339
2903
  policy: "triggered-payload",
2904
+ ...bindsRow ? {
2905
+ bindsRow: !0
2906
+ } : {},
2340
2907
  fields: fields
2341
2908
  });
2342
2909
  }
@@ -2360,6 +2927,52 @@ function checkAssigneesEntries(def, issues) {
2360
2927
  });
2361
2928
  }
2362
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
+
2363
2976
  function checkActivityTerminalPaths(def, issues) {
2364
2977
  for (const [i, stage] of def.stages.entries()) {
2365
2978
  const resolvable = terminallyResolvableActivities(stage);
@@ -2410,23 +3023,69 @@ function checkStart(def, issues) {
2410
3023
  if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2411
3024
  path: [ "start" ],
2412
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'`"
2413
- }), checkStartFilterReads(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({
2414
3028
  path: [ "fields", n, "required" ],
2415
- message: `start.kind 'autonomous' means runs are initiated by a system reacting to a document, so every required input must be derivable from that triggering document — required entry "${entry.name}" (kind "${entry.type}") is not a document reference. Make it optional, seed it another way (query/literal), or declare it doc.ref / doc.refs`
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)`
2416
3030
  });
2417
3031
  }
2418
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
+
2419
3050
  function checkStartFilterReads(def, issues) {
2420
3051
  const filter = def.start?.filter;
2421
- if (filter === void 0) return;
2422
- const bindable = new Set((def.fields ?? []).filter(isInputSourced).map(entry => entry.name)), declared = new Set((def.fields ?? []).map(entry => entry.name));
2423
- for (const name of conditionFieldReadNames(filter)) bindable.has(name) || issues.push({
3052
+ filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
2424
3053
  path: [ "start", "filter" ],
2425
- message: declared.has(name) ? `start.filter reads $fields.${name}, but "${name}" is not an \`input\`-sourced entry $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after any read-side evaluation), so the read is GROQ null and the filter silently never passes. Bindable (input) fields: ${knownList(bindable)}` : `start.filter reads $fields.${name}, but no workflow-scope field entry named "${name}" is declared — $fields binds only the caller's input entries, so the read is GROQ null and the filter silently never passes. Bindable (input) fields: ${knownList(bindable)}`
2426
- });
2427
- readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
3054
+ message: "start.filter reads $fields, but the filter is browse-time-purea 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"
3055
+ }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2428
3056
  path: [ "start", "filter" ],
2429
- message: "start.filter reads the candidate document (its root), but the definition declares no required subject entry (a required doc.ref / doc.refs input) — a read surface would never have a document to bind as root, so the filter could never pass. Declare a required subject entry, or gate on $fields / the dataset instead"
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"
3058
+ }));
3059
+ }
3060
+
3061
+ function checkStartAllowedReads(def, issues) {
3062
+ const allowed = def.start?.allowed;
3063
+ if (allowed === void 0) return;
3064
+ const bindable = new Map((def.fields ?? []).filter(isInputSourced).map(entry => [ entry.name, entry ])), declared = new Set((def.fields ?? []).map(entry => entry.name)), nullVerdict = `so the read is GROQ null and null semantics decide the verdict (refuse-all or vacuously-allow, by the expression's shape), never the caller's real input. Bindable (input) fields: ${knownList(bindable.keys())}`;
3065
+ for (const read of conditionFieldReads(allowed)) {
3066
+ const entry = bindable.get(read.name);
3067
+ if (entry === void 0) {
3068
+ issues.push({
3069
+ path: [ "start", "allowed" ],
3070
+ message: declared.has(read.name) ? `start.allowed reads $fields.${read.name}, but "${read.name}" is not an \`input\`-sourced entry — $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after the start gate), ${nullVerdict}` : `start.allowed reads $fields.${read.name}, but no workflow-scope field entry named "${read.name}" is declared — $fields binds only the caller's input entries, ${nullVerdict}`
3071
+ });
3072
+ continue;
3073
+ }
3074
+ pushFieldReadPathIssue({
3075
+ where: "start.allowed",
3076
+ read: {
3077
+ field: read.name,
3078
+ path: read.path
3079
+ },
3080
+ target: entry,
3081
+ path: [ "start", "allowed" ],
3082
+ issues: issues,
3083
+ nodes: START_ALLOWED_VALUE_NODES
3084
+ });
3085
+ }
3086
+ readsRootDocument(allowed) && issues.push({
3087
+ path: [ "start", "allowed" ],
3088
+ message: "start.allowed reads the candidate document as its root, but no root ever binds in the start-allowed context — startInstance holds inputs, not a loaded document, so the read is GROQ null and null semantics decide the verdict, never the workflow's real state. Read the subject as $fields.<entry> (its GDR URI is $fields.<entry>.id); a per-document visibility rule belongs in start.filter"
2430
3089
  });
2431
3090
  }
2432
3091
 
@@ -2537,39 +3196,80 @@ function checkGroupMembership({group: group, reachable: reachable, path: path, l
2537
3196
  }
2538
3197
 
2539
3198
  function checkConditionFieldReads(def, issues) {
2540
- const everywhere = allDeclaredFieldNames(def);
2541
- for (const site of conditionSites(def)) for (const name of conditionFieldReadNames(site.groq)) fieldVisibleAtSite({
3199
+ const everywhere = allDeclaredFieldEntries(def);
3200
+ for (const site of conditionSites(def)) checkSiteFieldReads({
2542
3201
  site: site,
2543
- name: name,
2544
- everywhere: everywhere
2545
- }) || issues.push({
2546
- path: site.path,
2547
- message: undeclaredFieldReadMessage(site, name)
3202
+ everywhere: everywhere,
3203
+ issues: issues
2548
3204
  });
2549
- for (const [name, groq2] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq2)) everywhere.has(read) || issues.push({
2550
- path: [ "predicates", name ],
2551
- message: noFieldAnywhereMessage(`predicate "${name}"`, read)
3205
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkSiteFieldReads({
3206
+ site: {
3207
+ groq: groq2,
3208
+ path: [ "predicates", name ],
3209
+ label: `predicate "${name}"`,
3210
+ fields: "context-dependent"
3211
+ },
3212
+ everywhere: everywhere,
3213
+ issues: issues
2552
3214
  });
2553
3215
  }
2554
3216
 
2555
- function allDeclaredFieldNames(def) {
2556
- const names = /* @__PURE__ */ new Set;
2557
- for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) names.add(entry.name);
2558
- return names;
3217
+ function checkSiteFieldReads({site: site, everywhere: everywhere, issues: issues}) {
3218
+ for (const read of conditionFieldReads(site.groq)) {
3219
+ const targets = readTargets({
3220
+ site: site,
3221
+ read: read,
3222
+ everywhere: everywhere
3223
+ });
3224
+ if (targets === "undeclared") {
3225
+ issues.push({
3226
+ path: site.path,
3227
+ message: undeclaredFieldReadMessage(site, read.name)
3228
+ });
3229
+ continue;
3230
+ }
3231
+ const path = read.path;
3232
+ path !== void 0 && (targets.some(target => fieldValuePathIssue({
3233
+ target: target,
3234
+ path: path
3235
+ }) === void 0) || pushFieldReadPathIssue({
3236
+ where: site.label,
3237
+ read: {
3238
+ field: read.name,
3239
+ path: path
3240
+ },
3241
+ target: targets[0],
3242
+ path: site.path,
3243
+ issues: issues
3244
+ }));
3245
+ }
3246
+ }
3247
+
3248
+ function readTargets({site: site, read: read, everywhere: everywhere}) {
3249
+ if (site.fields === "context-dependent") return everywhere.get(read.name) ?? "undeclared";
3250
+ const layers = site.fields.filter(layer => layer.entries.has(read.name));
3251
+ if (layers.length === 0) return "undeclared";
3252
+ const entries = layers.map(layer => layer.entries.get(read.name));
3253
+ return site.unionWindow === !0 ? entries : [ entries[0] ];
2559
3254
  }
2560
3255
 
2561
- function fieldVisibleAtSite({site: site, name: name, everywhere: everywhere}) {
2562
- return site.fields === "context-dependent" ? everywhere.has(name) : site.fields.some(layer => layer.names.has(name));
3256
+ function allDeclaredFieldEntries(def) {
3257
+ const declared = /* @__PURE__ */ new Map;
3258
+ for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) {
3259
+ const list = declared.get(entry.name);
3260
+ list === void 0 ? declared.set(entry.name, [ entry ]) : list.push(entry);
3261
+ }
3262
+ return declared;
2563
3263
  }
2564
3264
 
2565
3265
  function noFieldAnywhereMessage(label, name) {
2566
- return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently fails closed`;
3266
+ return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently misevaluates`;
2567
3267
  }
2568
3268
 
2569
3269
  function undeclaredFieldReadMessage(site, name) {
2570
3270
  if (site.fields === "context-dependent") return noFieldAnywhereMessage(site.label, name);
2571
- const visible = site.fields.flatMap(layer => [ ...layer.names ].map(n => `${layer.scope}:${n}`));
2572
- return `${site.label} reads $fields.${name}, but no field entry named "${name}" is lexically visible here — the read evaluates to GROQ null, so the condition silently fails closed. Visible: ${visible.join(", ") || "(none)"}`;
3271
+ const visible = site.fields.flatMap(layer => [ ...layer.entries.keys() ].map(n => `${layer.scope}:${n}`)), window = site.windowNote === void 0 ? `no field entry named "${name}" is lexically visible here` : `"${name}" is not in this site's window (${site.windowNote})`;
3272
+ return `${site.label} reads $fields.${name}, but ${window} — the read evaluates to GROQ null, so the condition silently misevaluates. Visible: ${visible.join(", ") || "(none)"}`;
2573
3273
  }
2574
3274
 
2575
3275
  function checkFieldReadSeeds(def, issues) {
@@ -2671,7 +3371,7 @@ function checkFieldReadOpValues(def, issues) {
2671
3371
  ops: site.ops,
2672
3372
  path: site.path,
2673
3373
  label: site.label,
2674
- activityNames: entryNames(site.activity.fields)
3374
+ activityEntries: site.activity.fields ?? []
2675
3375
  });
2676
3376
  }
2677
3377
 
@@ -2691,8 +3391,11 @@ function fieldReadsIn(value, path) {
2691
3391
  } ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
2692
3392
  }
2693
3393
 
2694
- function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityNames: activityNames, issues: issues}) {
3394
+ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
2695
3395
  const hosts = [ {
3396
+ scope: "activity",
3397
+ entries: activityEntries
3398
+ }, {
2696
3399
  scope: "stage",
2697
3400
  entries: stageEntries
2698
3401
  }, {
@@ -2705,8 +3408,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2705
3408
  message: opFieldReadMissMessage({
2706
3409
  read: read,
2707
3410
  where: where,
2708
- hosts: hosts,
2709
- activityNames: activityNames
3411
+ hosts: hosts
2710
3412
  })
2711
3413
  });
2712
3414
  return;
@@ -2720,9 +3422,9 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2720
3422
  });
2721
3423
  }
2722
3424
 
2723
- function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityNames: activityNames}) {
2724
- const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityNames?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
2725
- return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty.${activityHint} Known: ${known.join(", ") || "(none)"}`;
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)"}`;
2726
3428
  }
2727
3429
 
2728
3430
  function checkUpdateWhereOps(def, issues) {
@@ -2829,11 +3531,14 @@ function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, pa
2829
3531
  });
2830
3532
  }
2831
3533
 
2832
- function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
3534
+ function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues, nodes: nodes}) {
2833
3535
  if (read.path === void 0) return;
2834
3536
  const pathIssue = fieldValuePathIssue({
2835
3537
  target: target,
2836
- path: read.path
3538
+ path: read.path,
3539
+ ...nodes ? {
3540
+ nodes: nodes
3541
+ } : {}
2837
3542
  });
2838
3543
  pathIssue !== void 0 && issues.push({
2839
3544
  path: path,
@@ -2882,6 +3587,7 @@ const SCALAR = {
2882
3587
  kind: "list",
2883
3588
  item: GDR_VALUE
2884
3589
  }),
3590
+ subject: () => DOC_CONTENT,
2885
3591
  "release.ref": () => RELEASE_VALUE,
2886
3592
  string: () => SCALAR,
2887
3593
  text: () => SCALAR,
@@ -2904,22 +3610,27 @@ const SCALAR = {
2904
3610
  kind: "rows",
2905
3611
  of: shape.of ?? []
2906
3612
  })
3613
+ }, START_ALLOWED_VALUE_NODES = {
3614
+ ...VALUE_NODES,
3615
+ "doc.ref": () => GDR_VALUE,
3616
+ subject: () => GDR_VALUE
2907
3617
  };
2908
3618
 
2909
- function valueNodeFor(shape) {
2910
- return Object.hasOwn(VALUE_NODES, shape.type) ? VALUE_NODES[shape.type](shape) : void 0;
3619
+ function valueNodeFor(shape, nodes) {
3620
+ return Object.hasOwn(nodes, shape.type) ? nodes[shape.type](shape) : void 0;
2911
3621
  }
2912
3622
 
2913
3623
  const INDEX_SEGMENT = /^\d+$/;
2914
3624
 
2915
- function stepIntoValueNode(node, segment) {
3625
+ function stepIntoValueNode(args) {
3626
+ const {node: node, segment: segment, nodes: nodes} = args;
2916
3627
  switch (node.kind) {
2917
3628
  case "scalar":
2918
3629
  return "the value is a scalar with no sub-paths";
2919
3630
 
2920
3631
  case "object":
2921
3632
  {
2922
- const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
3633
+ const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub, nodes);
2923
3634
  return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
2924
3635
  }
2925
3636
 
@@ -2940,18 +3651,24 @@ function stepIntoValueNode(node, segment) {
2940
3651
  }
2941
3652
  }
2942
3653
 
2943
- function fieldValuePathIssue({target: target, path: path}) {
2944
- let node = valueNodeFor(target);
3654
+ function fieldValuePathIssue({target: target, path: path, nodes: nodes = VALUE_NODES}) {
3655
+ let node = valueNodeFor(target, nodes);
2945
3656
  if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
2946
3657
  for (const segment of path.split(".")) {
2947
- const next = stepIntoValueNode(node, segment);
3658
+ const next = stepIntoValueNode({
3659
+ node: node,
3660
+ segment: segment,
3661
+ nodes: nodes
3662
+ });
2948
3663
  if (typeof next == "string") return `at segment "${segment}": ${next}`;
2949
3664
  node = next;
2950
3665
  }
2951
3666
  }
2952
3667
 
2953
3668
  function checkWorkflowInvariants(def) {
2954
- const issues = [], stageNames = checkStages(def, issues);
3669
+ const issues = [];
3670
+ checkDefinitionName(def, issues);
3671
+ const stageNames = checkStages(def, issues);
2955
3672
  return checkInitialStage({
2956
3673
  def: def,
2957
3674
  stageNames: stageNames,
@@ -2966,11 +3683,12 @@ function checkWorkflowInvariants(def) {
2966
3683
  issues: issues
2967
3684
  }), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
2968
3685
  checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
2969
- checkUnboundCallerVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3686
+ checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
2970
3687
  checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
2971
- 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),
2972
3690
  checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
2973
3691
  checkGroups(def, issues), issues;
2974
3692
  }
2975
3693
 
2976
- export { ACTIVITY_KINDS, ACTOR_KINDS, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GroupSchema, InstanceNotFoundError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, START_FILTER_VARS, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
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 };