@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.
@@ -20,6 +20,291 @@ function _interopNamespaceCompat(e) {
20
20
 
21
21
  var v__namespace = /* @__PURE__ */ _interopNamespaceCompat(v);
22
22
 
23
+ class WorkflowError extends Error {
24
+ kind;
25
+ constructor(kind, message, options) {
26
+ super(message, options), this.kind = kind;
27
+ }
28
+ }
29
+
30
+ class ContractViolationError extends WorkflowError {
31
+ constructor(message) {
32
+ super("contract-violation", message), this.name = "ContractViolationError";
33
+ }
34
+ }
35
+
36
+ class InstanceNotFoundError extends WorkflowError {
37
+ instanceId;
38
+ constructor(args) {
39
+ super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
40
+ this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
41
+ }
42
+ }
43
+
44
+ class DefinitionNotFoundError extends WorkflowError {
45
+ definition;
46
+ version;
47
+ constructor(args) {
48
+ 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`),
49
+ this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
50
+ }
51
+ }
52
+
53
+ class SpawnContractsInvalidError extends WorkflowError {
54
+ issues;
55
+ constructor(args) {
56
+ super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
57
+ this.issues = args.issues;
58
+ }
59
+ }
60
+
61
+ class DefinitionInUseError extends WorkflowError {
62
+ definition;
63
+ blockedBy;
64
+ constructor(args) {
65
+ super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
66
+ this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
67
+ }
68
+ }
69
+
70
+ function definitionInUseMessage(definition, blockedBy) {
71
+ if (blockedBy.reason === "non-terminal-instances") {
72
+ const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
73
+ 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.`;
74
+ }
75
+ const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
76
+ return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
77
+ }
78
+
79
+ class EffectNotFoundError extends WorkflowError {
80
+ instanceId;
81
+ effectKey;
82
+ settled;
83
+ constructor(args) {
84
+ super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
85
+ this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
86
+ }
87
+ }
88
+
89
+ function effectNotFoundMessage(args) {
90
+ const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
91
+ if (args.settled === void 0) return base;
92
+ const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
93
+ 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}`;
94
+ }
95
+
96
+ function errorMessage(err) {
97
+ return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
98
+ }
99
+
100
+ function rethrowWithContext(err, context) {
101
+ throw new Error(`${context}: ${errorMessage(err)}`, {
102
+ cause: err
103
+ });
104
+ }
105
+
106
+ const WORKFLOW_INSTANCE_TYPE = "sanity.workflow.instance";
107
+
108
+ function terminalState(instance) {
109
+ return instance.abortedAt !== void 0 ? "aborted" : instance.completedAt !== void 0 ? "completed" : "in-flight";
110
+ }
111
+
112
+ function isUnprimed(instance) {
113
+ return instance.stages.length === 0 && terminalState(instance) === "in-flight";
114
+ }
115
+
116
+ function parseDefinitionSnapshotValue(instance) {
117
+ try {
118
+ return JSON.parse(instance.definitionSnapshot);
119
+ } catch (err) {
120
+ rethrowWithContext(err, `Failed to parse definitionSnapshot on instance "${instance._id}"`);
121
+ }
122
+ }
123
+
124
+ function parseDefinitionSnapshot(instance) {
125
+ return parseDefinitionSnapshotValue(instance);
126
+ }
127
+
128
+ function parentRef(instance) {
129
+ return instance.ancestors.at(-1);
130
+ }
131
+
132
+ 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";
133
+
134
+ class ReaderModelAcknowledgementError extends WorkflowError {
135
+ code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
136
+ expectedMinReaderModel;
137
+ engineMinReaderModel=DATA_MODEL_MIN_READER;
138
+ engineModelVersion=DATA_MODEL_VERSION;
139
+ documentationUrl=READER_MODEL_ROLLOUT_URL;
140
+ constructor(expectedMinReaderModel, context = "Deployment") {
141
+ const expected = expectedMinReaderModel === void 0 ? "missing" : String(expectedMinReaderModel);
142
+ 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}`),
143
+ this.name = "ReaderModelAcknowledgementError", this.expectedMinReaderModel = expectedMinReaderModel;
144
+ }
145
+ }
146
+
147
+ function assertReaderModelAcknowledgement(expectedMinReaderModel, context) {
148
+ if (typeof expectedMinReaderModel != "number" || !Number.isFinite(expectedMinReaderModel) || !Number.isInteger(expectedMinReaderModel) || expectedMinReaderModel < 0 || expectedMinReaderModel !== DATA_MODEL_MIN_READER) throw new ReaderModelAcknowledgementError(expectedMinReaderModel, context);
149
+ }
150
+
151
+ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
152
+ id: "governed-model-stamps",
153
+ introducedInModel: 1,
154
+ minReaderModel: 0,
155
+ documentTypes: Object.freeze([ "definition", "instance" ]),
156
+ compatibility: "additive",
157
+ applicability: "unconditional",
158
+ summary: "Definition and instance documents carry model provenance and reader-floor stamps."
159
+ }), Object.freeze({
160
+ id: "subject-field-kind",
161
+ introducedInModel: 2,
162
+ minReaderModel: 0,
163
+ documentTypes: Object.freeze([ "definition", "instance" ]),
164
+ compatibility: "additive",
165
+ applicability: "detectable",
166
+ summary: "A workflow-level subject field identifies the document a workflow is about."
167
+ }), Object.freeze({
168
+ id: "typed-scalar-choice-lists",
169
+ introducedInModel: 2,
170
+ minReaderModel: 2,
171
+ documentTypes: Object.freeze([ "definition", "instance" ]),
172
+ compatibility: "reader-floor",
173
+ applicability: "detectable",
174
+ summary: "Scalar fields may constrain writes to a persisted typed choice list."
175
+ }), Object.freeze({
176
+ id: "action-semantics",
177
+ introducedInModel: 2,
178
+ minReaderModel: 0,
179
+ documentTypes: Object.freeze([ "definition" ]),
180
+ compatibility: "additive",
181
+ applicability: "detectable",
182
+ summary: "Ordinary actions may carry a closed bag of advisory workflow semantics."
183
+ }), Object.freeze({
184
+ id: "inclusive-scalar-bounds",
185
+ introducedInModel: 2,
186
+ minReaderModel: 2,
187
+ documentTypes: Object.freeze([ "definition", "instance" ]),
188
+ compatibility: "reader-floor",
189
+ applicability: "detectable",
190
+ summary: "String, text, and number values may carry persisted inclusive bounds."
191
+ }) ]);
192
+
193
+ function recordOf(value) {
194
+ return value !== null && typeof value == "object" && !Array.isArray(value) ? value : void 0;
195
+ }
196
+
197
+ function recordsAt(record, key) {
198
+ const value = record[key];
199
+ return Array.isArray(value) ? value.map(recordOf).filter(item => item !== void 0) : [];
200
+ }
201
+
202
+ function nestedFieldEntries(entries) {
203
+ return entries.flatMap(entry => [ entry, ...nestedFieldEntries(recordsAt(entry, "fields")), ...nestedFieldEntries(recordsAt(entry, "of")) ]);
204
+ }
205
+
206
+ function parsedDefinitionSnapshot(root) {
207
+ if (typeof root.definitionSnapshot == "string") return recordOf(parseDefinitionSnapshotValue({
208
+ _id: typeof root._id == "string" ? root._id : "<unknown instance>",
209
+ definitionSnapshot: root.definitionSnapshot
210
+ }));
211
+ }
212
+
213
+ function persistedFieldEntries(document) {
214
+ const root = recordOf(document);
215
+ if (root === void 0) return [];
216
+ 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"));
217
+ 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")) ]);
218
+ }
219
+
220
+ function hasChoiceList(document) {
221
+ return persistedFieldEntries(document).some(entry => {
222
+ const options = recordOf(entry.options);
223
+ return options !== void 0 && Array.isArray(options.list);
224
+ });
225
+ }
226
+
227
+ function hasSubjectField(document) {
228
+ return persistedFieldEntries(document).some(entry => entry.type === "subject" || entry._type === "subject");
229
+ }
230
+
231
+ function hasActionSemantics(document) {
232
+ const root = recordOf(document);
233
+ return root === void 0 ? !1 : recordsAt(root, "stages").flatMap(stage => recordsAt(stage, "activities")).flatMap(activity => recordsAt(activity, "actions")).some(action => Array.isArray(action.semantics));
234
+ }
235
+
236
+ function hasScalarValidation(document) {
237
+ return persistedFieldEntries(document).some(entry => {
238
+ const validation = recordOf(entry.validation);
239
+ return typeof validation?.min == "number" || typeof validation?.max == "number";
240
+ });
241
+ }
242
+
243
+ const featureDetectors = {
244
+ "governed-model-stamps": () => !0,
245
+ "subject-field-kind": hasSubjectField,
246
+ "typed-scalar-choice-lists": hasChoiceList,
247
+ "action-semantics": hasActionSemantics,
248
+ "inclusive-scalar-bounds": hasScalarValidation
249
+ };
250
+
251
+ function requiredModelFeatures(documentType, document) {
252
+ return DATA_MODEL_CHANGES.filter(change => change.documentTypes.some(candidate => candidate === documentType) && featureDetectors[change.id](document));
253
+ }
254
+
255
+ function requiredReaderModel(documentType, document) {
256
+ return Math.max(0, ...requiredModelFeatures(documentType, document).map(change => change.minReaderModel));
257
+ }
258
+
259
+ function modelStampFor(args) {
260
+ return {
261
+ modelVersion: DATA_MODEL_VERSION,
262
+ minReaderModel: Math.max(args.storedMinReaderModel ?? 0, requiredReaderModel(args.documentType, args.document))
263
+ };
264
+ }
265
+
266
+ function fieldTreeShape(value) {
267
+ if (Array.isArray(value)) return value.map(fieldTreeShape);
268
+ if (value === null) return "null";
269
+ if (typeof value == "object") {
270
+ const record = value;
271
+ return Object.fromEntries(Object.keys(record).toSorted().map(key => [ key, fieldTreeShape(record[key]) ]));
272
+ }
273
+ return typeof value;
274
+ }
275
+
276
+ function modelVersionOf(doc) {
277
+ const stamp = doc.modelVersion;
278
+ return typeof stamp == "number" ? stamp : 0;
279
+ }
280
+
281
+ function minReaderModelOf(doc) {
282
+ const floor = doc.minReaderModel;
283
+ return typeof floor == "number" ? floor : modelVersionOf(doc);
284
+ }
285
+
286
+ class ModelVersionAheadError extends WorkflowError {
287
+ documentId;
288
+ documentModelVersion;
289
+ requiredReaderModel;
290
+ engineModelVersion;
291
+ constructor(args) {
292
+ 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.`),
293
+ this.name = "ModelVersionAheadError", this.documentId = args.documentId, this.documentModelVersion = args.documentModelVersion,
294
+ this.requiredReaderModel = args.requiredReaderModel, this.engineModelVersion = DATA_MODEL_VERSION;
295
+ }
296
+ }
297
+
298
+ function assertReadableModel(doc) {
299
+ const documentReaderModel = minReaderModelOf(doc);
300
+ if (documentReaderModel > DATA_MODEL_VERSION) throw new ModelVersionAheadError({
301
+ documentId: doc._id,
302
+ documentModelVersion: modelVersionOf(doc),
303
+ requiredReaderModel: documentReaderModel
304
+ });
305
+ return doc;
306
+ }
307
+
23
308
  function isCascadeFired(action) {
24
309
  return action.when !== void 0;
25
310
  }
@@ -40,16 +325,6 @@ function driverKind(actor) {
40
325
  return actor.kind === "person" || actor.kind === "agent" ? actor.kind : "service";
41
326
  }
42
327
 
43
- function errorMessage(err) {
44
- return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
45
- }
46
-
47
- function rethrowWithContext(err, context) {
48
- throw new Error(`${context}: ${errorMessage(err)}`, {
49
- cause: err
50
- });
51
- }
52
-
53
328
  function andConditions(parts) {
54
329
  const present = parts.filter(p => p !== void 0);
55
330
  if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
@@ -250,82 +525,17 @@ function isGdr(value) {
250
525
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
251
526
  }
252
527
 
253
- class WorkflowError extends Error {
254
- kind;
255
- constructor(kind, message, options) {
256
- super(message, options), this.kind = kind;
257
- }
258
- }
259
-
260
- class ContractViolationError extends WorkflowError {
261
- constructor(message) {
262
- super("contract-violation", message), this.name = "ContractViolationError";
263
- }
264
- }
265
-
266
- class InstanceNotFoundError extends WorkflowError {
267
- instanceId;
268
- constructor(args) {
269
- super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
270
- this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
271
- }
272
- }
273
-
274
- class DefinitionNotFoundError extends WorkflowError {
275
- definition;
276
- version;
277
- constructor(args) {
278
- 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`),
279
- this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
280
- }
281
- }
282
-
283
- class DefinitionInUseError extends WorkflowError {
284
- definition;
285
- blockedBy;
286
- constructor(args) {
287
- super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
288
- this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
289
- }
290
- }
291
-
292
- function definitionInUseMessage(definition, blockedBy) {
293
- if (blockedBy.reason === "non-terminal-instances") {
294
- const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
295
- 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.`;
296
- }
297
- const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
298
- return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
299
- }
300
-
301
- class EffectNotFoundError extends WorkflowError {
302
- instanceId;
303
- effectKey;
304
- settled;
305
- constructor(args) {
306
- super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
307
- this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
308
- }
309
- }
310
-
311
- function effectNotFoundMessage(args) {
312
- const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
313
- if (args.settled === void 0) return base;
314
- const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
315
- return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
316
- }
317
-
318
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
528
+ const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
319
529
 
320
530
  function validateTag(tag) {
321
- 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)`);
531
+ 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})`);
322
532
  }
323
533
 
324
534
  function tagScopeFilter() {
325
535
  return "tag == $tag";
326
536
  }
327
537
 
328
- const NonEmptyString = v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty("must not be empty"));
538
+ const NonEmptyString$1 = v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty("must not be empty"));
329
539
 
330
540
  function asPredicate(validate) {
331
541
  return value => {
@@ -339,21 +549,22 @@ function asPredicate(validate) {
339
549
 
340
550
  const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v__namespace.variant("type", [ v__namespace.object({
341
551
  type: v__namespace.literal("dataset"),
342
- id: v__namespace.pipe(NonEmptyString, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
552
+ id: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
343
553
  }), v__namespace.object({
344
554
  type: v__namespace.literal("canvas"),
345
- id: NonEmptyString
555
+ id: NonEmptyString$1
346
556
  }), v__namespace.object({
347
557
  type: v__namespace.literal("media-library"),
348
- id: NonEmptyString
558
+ id: NonEmptyString$1
349
559
  }), v__namespace.object({
350
560
  type: v__namespace.literal("dashboard"),
351
- id: NonEmptyString
561
+ id: NonEmptyString$1
352
562
  }) ]), ResourceBindingSchema = v__namespace.object({
353
- name: v__namespace.pipe(NonEmptyString, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
563
+ name: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
354
564
  resource: WorkflowResourceSchema
355
565
  }), DefinitionSchema = v__namespace.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v__namespace.object({
356
- name: NonEmptyString,
566
+ name: NonEmptyString$1,
567
+ expectedMinReaderModel: v__namespace.optional(v__namespace.custom(() => !0), void 0),
357
568
  tag: v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty(), v__namespace.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
358
569
  workflowResource: WorkflowResourceSchema,
359
570
  resourceAliases: v__namespace.optional(v__namespace.pipe(v__namespace.array(ResourceBindingSchema), v__namespace.check(bindings => new Set(bindings.map(binding => binding.name)).size === bindings.length, "duplicate resource handle name — each binding name must be unique within a deployment"))),
@@ -518,6 +729,9 @@ function desugarStart(start) {
518
729
  kind: start.kind ?? "interactive",
519
730
  ...start.filter !== void 0 ? {
520
731
  filter: start.filter
732
+ } : {},
733
+ ...start.allowed !== void 0 ? {
734
+ allowed: start.allowed
521
735
  } : {}
522
736
  };
523
737
  }
@@ -593,6 +807,8 @@ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
593
807
  required: entry.required,
594
808
  initialValue: entry.initialValue,
595
809
  editable: editable,
810
+ options: entry.options,
811
+ validation: entry.validation,
596
812
  types: entry.types,
597
813
  fields: entry.fields,
598
814
  of: entry.of
@@ -735,7 +951,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
735
951
  };
736
952
  }
737
953
 
738
- const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
954
+ const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref" ];
739
955
 
740
956
  function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
741
957
  if (target === void 0 || target.type === "url") return target;
@@ -756,18 +972,15 @@ function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
756
972
  };
757
973
  }
758
974
 
759
- function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
760
- if ("type" in action) return desugarClaimAction({
761
- action: action,
762
- path: path,
763
- env: env,
764
- ctx: ctx
765
- });
766
- action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
975
+ function reportEmptyActionRoles({action: action, path: path, ctx: ctx}) {
976
+ action.roles === void 0 || action.roles.length > 0 || ctx.issues.push({
767
977
  path: [ ...path, "roles" ],
768
978
  message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
769
979
  });
770
- const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
980
+ }
981
+
982
+ function desugarActionOps(args) {
983
+ const {action: action, path: path, env: env, activityName: activityName, ctx: ctx} = args, ops = desugarOps({
771
984
  ops: action.ops,
772
985
  path: [ ...path, "ops" ],
773
986
  env: env,
@@ -778,9 +991,32 @@ function desugarAction({action: action, path: path, env: env, activityName: acti
778
991
  type: "status.set",
779
992
  activity: activityName,
780
993
  status: action.status
781
- }), {
994
+ }), ops;
995
+ }
996
+
997
+ function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
998
+ if ("type" in action) return desugarClaimAction({
999
+ action: action,
1000
+ path: path,
1001
+ env: env,
1002
+ ctx: ctx
1003
+ });
1004
+ reportEmptyActionRoles({
1005
+ action: action,
1006
+ path: path,
1007
+ ctx: ctx
1008
+ });
1009
+ const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarActionOps({
1010
+ action: action,
1011
+ path: path,
1012
+ env: env,
1013
+ activityName: activityName,
1014
+ ctx: ctx
1015
+ });
1016
+ return {
782
1017
  ...stripUndefined({
783
1018
  name: action.name,
1019
+ semantics: action.semantics,
784
1020
  title: action.title,
785
1021
  description: action.description,
786
1022
  group: normalizeGroup(action.group),
@@ -1066,12 +1302,43 @@ function conditionParameterNames(groq2) {
1066
1302
  }
1067
1303
 
1068
1304
  function conditionFieldReadNames(groq2) {
1069
- const read = /* @__PURE__ */ new Set;
1070
- return walkAstNodes(tryParseGroq(groq2), node => {
1071
- if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
1072
- const base = node.base;
1073
- base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
1074
- }), read;
1305
+ return new Set(conditionFieldReads(groq2).map(read => read.name));
1306
+ }
1307
+
1308
+ function conditionFieldReads(groq2) {
1309
+ const reads = /* @__PURE__ */ new Map;
1310
+ return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
1311
+ }
1312
+
1313
+ function collectFieldReads(node, reads) {
1314
+ if (Array.isArray(node)) {
1315
+ for (const item of node) collectFieldReads(item, reads);
1316
+ return;
1317
+ }
1318
+ if (typeof node != "object" || node === null) return;
1319
+ const chain = fieldsAttributeChain(node);
1320
+ if (chain !== void 0) {
1321
+ const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
1322
+ reads.set(`${name}.${path ?? ""}`, {
1323
+ name: name,
1324
+ path: path
1325
+ });
1326
+ return;
1327
+ }
1328
+ for (const value of Object.values(node)) collectFieldReads(value, reads);
1329
+ }
1330
+
1331
+ function fieldsAttributeChain(node) {
1332
+ const names = [];
1333
+ let current = node;
1334
+ for (;typeof current == "object" && current !== null; ) {
1335
+ const typed = current;
1336
+ if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
1337
+ names.unshift(typed.name);
1338
+ const base = typed.base;
1339
+ if (base?.type === "Parameter" && base.name === "fields") return names;
1340
+ current = base;
1341
+ }
1075
1342
  }
1076
1343
 
1077
1344
  function conditionEffectReads(groq2) {
@@ -1136,7 +1403,7 @@ function isTerminalActivityStatus(status) {
1136
1403
  return TERMINAL_ACTIVITY_STATUSES.includes(status);
1137
1404
  }
1138
1405
 
1139
- 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 = [ {
1406
+ 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 = [ {
1140
1407
  name: "self",
1141
1408
  binding: "always",
1142
1409
  label: "this workflow instance",
@@ -1145,7 +1412,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1145
1412
  name: "fields",
1146
1413
  binding: "always",
1147
1414
  label: "the workflow's fields",
1148
- 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."
1415
+ 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."
1149
1416
  }, {
1150
1417
  name: "parent",
1151
1418
  binding: "always",
@@ -1215,7 +1482,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1215
1482
  name: "row",
1216
1483
  binding: "spawn",
1217
1484
  label: "the spawned row",
1218
- 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)."
1485
+ 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."
1219
1486
  }, {
1220
1487
  name: "params",
1221
1488
  binding: "caller",
@@ -1226,25 +1493,161 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1226
1493
  binding: "always",
1227
1494
  label: "the spawned subworkflows",
1228
1495
  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`."
1229
- } ], 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 = [ {
1496
+ } ], 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 = [ {
1230
1497
  name: "tag",
1498
+ label: "this engine tag",
1231
1499
  description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1232
1500
  }, {
1233
1501
  name: "definition",
1234
- description: "The `name` of the definition under evaluation (its own start.filter binds it)."
1502
+ label: "this workflow definition",
1503
+ description: "The `name` of the definition under evaluation (its own start block binds it)."
1235
1504
  }, {
1236
1505
  name: "now",
1506
+ label: "the current time",
1237
1507
  description: "The ISO clock reading of the evaluating engine."
1238
1508
  }, {
1509
+ name: SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR,
1510
+ label: "the subject already has an in-flight workflow",
1511
+ description: "Whether any instance in this engine tag, across all definitions, has the same resource-qualified subject and no `completedAt`. Advisory under concurrent starts."
1512
+ } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1239
1513
  name: "fields",
1240
- 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."
1514
+ label: "the start's input fields",
1515
+ 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."
1241
1516
  } ], GUARD_PREDICATE_VARS = [ {
1242
1517
  name: "guard",
1243
1518
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
1244
1519
  }, {
1245
1520
  name: "mutation",
1246
1521
  description: "The attempted mutation — `mutation.action` is the write kind being gated."
1247
- } ], ACTOR_KINDS = [ "person", "agent", "system" ];
1522
+ } ], NonEmptyString = v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "must be a non-empty string"));
1523
+
1524
+ function isParseableInstant(value) {
1525
+ return typeof value == "string" && !Number.isNaN(Date.parse(value));
1526
+ }
1527
+
1528
+ const IsoTimestamp = v__namespace.pipe(v__namespace.string(), v__namespace.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
1529
+
1530
+ function tolerantObject() {
1531
+ return (entries, ..._exact) => v__namespace.looseObject(entries);
1532
+ }
1533
+
1534
+ function tolerantEntries() {
1535
+ return (entries, ..._exact) => entries;
1536
+ }
1537
+
1538
+ function schemaTreeShape(schema) {
1539
+ return walkSchemaShape(schema, /* @__PURE__ */ new Set);
1540
+ }
1541
+
1542
+ function walkSchemaShape(node, path) {
1543
+ if (typeof node != "object" || node === null) return "unknown";
1544
+ if (path.has(node)) return "(circular)";
1545
+ path.add(node);
1546
+ try {
1547
+ const schema = node;
1548
+ return containerShape(schema, path) ?? leafShape(schema);
1549
+ } finally {
1550
+ path.delete(node);
1551
+ }
1552
+ }
1553
+
1554
+ const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
1555
+ union: schema.options.map(option => walkSchemaShape(option, path))
1556
+ }), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
1557
+ strict_object: objectWalker,
1558
+ loose_object: objectWalker,
1559
+ object: objectWalker,
1560
+ array: (schema, path) => ({
1561
+ array: walkSchemaShape(schema.item, path)
1562
+ }),
1563
+ record: (schema, path) => ({
1564
+ record: walkSchemaShape(schema.value, path)
1565
+ }),
1566
+ union: unionWalker,
1567
+ variant: unionWalker,
1568
+ lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
1569
+ exact_optional: unwrapWalker,
1570
+ optional: unwrapWalker,
1571
+ nullable: unwrapWalker
1572
+ };
1573
+
1574
+ function containerShape(schema, path) {
1575
+ return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
1576
+ }
1577
+
1578
+ function objectEntriesShape(entries, path) {
1579
+ const out = {};
1580
+ for (const [key, entry] of Object.entries(entries)) {
1581
+ const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
1582
+ out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
1583
+ }
1584
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
1585
+ }
1586
+
1587
+ function isOptionalEntry(entry) {
1588
+ if (typeof entry != "object" || entry === null) return !1;
1589
+ const kind = entry.type;
1590
+ return kind === "exact_optional" || kind === "optional";
1591
+ }
1592
+
1593
+ const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
1594
+
1595
+ function leafShape(schema) {
1596
+ if (schema.type === "picklist") return schema.options.join(" | ");
1597
+ if (schema.type === "literal") return `literal ${String(schema.literal)}`;
1598
+ if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
1599
+ 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`);
1600
+ return String(schema.type);
1601
+ }
1602
+
1603
+ function formatValidationError(label, issues) {
1604
+ const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1605
+ return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1606
+ }
1607
+
1608
+ function issuesFromValibot(issues) {
1609
+ return issues.map(issue => ({
1610
+ path: issue.path ? issue.path.map(item => item.key) : [],
1611
+ message: issue.message
1612
+ }));
1613
+ }
1614
+
1615
+ function formatIssuePath(path) {
1616
+ let out = "";
1617
+ for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1618
+ return out;
1619
+ }
1620
+
1621
+ class PersistedDocShapeError extends WorkflowError {
1622
+ documentId;
1623
+ documentType;
1624
+ issues;
1625
+ constructor(args) {
1626
+ super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
1627
+ this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
1628
+ this.issues = args.issues;
1629
+ }
1630
+ }
1631
+
1632
+ function parsePersistedDoc(args) {
1633
+ const result = v__namespace.safeParse(args.schema, args.doc);
1634
+ if (!result.success) throw new PersistedDocShapeError({
1635
+ documentId: documentIdOf(args.doc),
1636
+ documentType: args.docType,
1637
+ issues: issuesFromValibot(result.issues)
1638
+ });
1639
+ return result.output;
1640
+ }
1641
+
1642
+ function documentIdOf(doc) {
1643
+ if (typeof doc == "object" && doc !== null) {
1644
+ const id = doc._id;
1645
+ if (typeof id == "string") return id;
1646
+ }
1647
+ return "(unknown id)";
1648
+ }
1649
+
1650
+ const ACTOR_KINDS = [ "person", "agent", "system" ];
1248
1651
 
1249
1652
  function releaseDocId(releaseName) {
1250
1653
  return `_.releases.${releaseName}`;
@@ -1259,6 +1662,18 @@ function releaseRef({res: res, releaseName: releaseName}) {
1259
1662
  };
1260
1663
  }
1261
1664
 
1665
+ function isSingleDocRefKind(kind) {
1666
+ return kind === "doc.ref" || kind === "subject";
1667
+ }
1668
+
1669
+ function refKindAcceptsTypes(kind) {
1670
+ return isSingleDocRefKind(kind) || kind === "doc.refs";
1671
+ }
1672
+
1673
+ function isSingleDocRefEntry(entry) {
1674
+ return isSingleDocRefKind(entry._type);
1675
+ }
1676
+
1262
1677
  function isTodoListItem(row) {
1263
1678
  if (typeof row != "object" || row === null) return !1;
1264
1679
  const candidate = row, status = candidate.status;
@@ -1291,29 +1706,53 @@ class FieldValueShapeError extends WorkflowError {
1291
1706
  }
1292
1707
  }
1293
1708
 
1294
- const GdrShape = v__namespace.looseObject({
1295
- id: v__namespace.pipe(v__namespace.string(), v__namespace.check(s => isGdrUri(s), "must be a GDR URI")),
1296
- type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1297
- }), ReleaseRefShape = v__namespace.pipe(v__namespace.looseObject({
1298
- id: v__namespace.pipe(v__namespace.string(), v__namespace.check(s => isGdrUri(s), "must be a GDR URI")),
1709
+ const GdrUriSchema = v__namespace.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
1710
+ id: GdrUriSchema,
1711
+ type: NonEmptyString
1712
+ }), ReleaseRefShape = v__namespace.pipe(tolerantObject()({
1713
+ id: GdrUriSchema,
1299
1714
  type: v__namespace.literal("system.release"),
1300
- releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1301
- }), v__namespace.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = v__namespace.looseObject({
1715
+ releaseName: NonEmptyString
1716
+ }), v__namespace.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = tolerantObject()({
1302
1717
  kind: v__namespace.picklist(ACTOR_KINDS),
1303
- id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1)),
1304
- roles: v__namespace.optional(v__namespace.array(v__namespace.string())),
1305
- onBehalfOf: v__namespace.optional(v__namespace.string())
1306
- }), AssigneeShape = v__namespace.union([ v__namespace.looseObject({
1718
+ id: NonEmptyString,
1719
+ roles: v__namespace.exactOptional(v__namespace.array(v__namespace.string())),
1720
+ onBehalfOf: v__namespace.exactOptional(v__namespace.string())
1721
+ }), AssigneeShape = v__namespace.union([ tolerantObject()({
1307
1722
  type: v__namespace.literal("user"),
1308
- id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1309
- }), v__namespace.looseObject({
1723
+ id: NonEmptyString
1724
+ }), tolerantObject()({
1310
1725
  type: v__namespace.literal("role"),
1311
- role: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1312
- }) ]), NullableString = v__namespace.union([ v__namespace.null(), v__namespace.string() ]), NullableNumber = v__namespace.union([ v__namespace.null(), v__namespace.number() ]), NullableBoolean = v__namespace.union([ v__namespace.null(), v__namespace.boolean() ]), NullableDateTime = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.check(s => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")) ]), NullableDate = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, valueSchemas = {
1726
+ role: NonEmptyString
1727
+ }) ]), NullableString = v__namespace.union([ v__namespace.null(), v__namespace.string() ]), NullableNumber = v__namespace.union([ v__namespace.null(), v__namespace.number() ]), NullableBoolean = v__namespace.union([ v__namespace.null(), v__namespace.boolean() ]), NullableDateTime = v__namespace.union([ v__namespace.null(), IsoTimestamp ]), NullableDate = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
1728
+
1729
+ function normalizedChoiceKind(kind) {
1730
+ return kind === "dateTime" ? "datetime" : kind;
1731
+ }
1732
+
1733
+ function checkChoiceList(args) {
1734
+ const {entryType: entryType, options: options, validation: validation} = args;
1735
+ if (options === void 0) return;
1736
+ if (!CHOICE_KINDS.has(entryType)) return [ `\`options\` is not valid on "${entryType}" values` ];
1737
+ const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => checkValueAgainst({
1738
+ entryType: kind,
1739
+ value: option.value,
1740
+ validation: validation
1741
+ }, valueSchemas)?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
1742
+ for (const option of options.list) seen.has(option.value) && issues.push(`duplicate option value ${JSON.stringify(option.value)}`),
1743
+ seen.add(option.value);
1744
+ return issues.length === 0 ? void 0 : issues;
1745
+ }
1746
+
1747
+ function choiceValueIssues(options, value) {
1748
+ 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(", ")}` ];
1749
+ }
1750
+
1751
+ const fieldValueSchemas = {
1313
1752
  "doc.ref": v__namespace.union([ v__namespace.null(), GdrShape ]),
1314
1753
  "doc.refs": v__namespace.array(GdrShape),
1754
+ subject: v__namespace.union([ v__namespace.null(), GdrShape ]),
1315
1755
  "release.ref": v__namespace.union([ v__namespace.null(), ReleaseRefShape ]),
1316
- query: v__namespace.any(),
1317
1756
  string: NullableString,
1318
1757
  text: NullableString,
1319
1758
  number: NullableNumber,
@@ -1324,10 +1763,62 @@ const GdrShape = v__namespace.looseObject({
1324
1763
  actor: v__namespace.union([ v__namespace.null(), ActorShape ]),
1325
1764
  assignee: v__namespace.union([ v__namespace.null(), AssigneeShape ]),
1326
1765
  assignees: v__namespace.array(AssigneeShape)
1766
+ }, valueSchemas = {
1767
+ ...fieldValueSchemas,
1768
+ query: v__namespace.any()
1327
1769
  };
1328
1770
 
1329
1771
  function shapeValueSchema(shape, leaf) {
1330
- return shape.type === "object" ? objectSchema(shape.fields ?? [], leaf) : shape.type === "array" ? v__namespace.array(objectSchema(shape.of ?? [], leaf)) : leaf[shape.type] ?? v__namespace.any();
1772
+ if (shape.type === "object") return objectSchema(shape.fields ?? [], leaf);
1773
+ if (shape.type === "array") return v__namespace.array(objectSchema(shape.of ?? [], leaf));
1774
+ const schema = leaf[shape.type] ?? v__namespace.any();
1775
+ return constrainedScalarSchema({
1776
+ schema: schema,
1777
+ entryType: shape.type,
1778
+ ...shape
1779
+ });
1780
+ }
1781
+
1782
+ function scalarMeasurement(entryType, value) {
1783
+ if (entryType === "number" && typeof value == "number") return value;
1784
+ if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
1785
+ }
1786
+
1787
+ function scalarBoundIssue(args) {
1788
+ const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
1789
+ 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}`;
1790
+ }
1791
+
1792
+ function scalarValidationIssues(args) {
1793
+ const {entryType: entryType, validation: validation, value: value} = args;
1794
+ if (validation === void 0 || value === null || value === void 0) return;
1795
+ const measured = scalarMeasurement(entryType, value);
1796
+ if (measured === void 0) return;
1797
+ const issues = [ scalarBoundIssue({
1798
+ entryType: entryType,
1799
+ measured: measured,
1800
+ bound: validation.min,
1801
+ limit: "min"
1802
+ }), scalarBoundIssue({
1803
+ entryType: entryType,
1804
+ measured: measured,
1805
+ bound: validation.max,
1806
+ limit: "max"
1807
+ }) ].filter(issue => issue !== void 0);
1808
+ return issues.length === 0 ? void 0 : issues;
1809
+ }
1810
+
1811
+ function constrainedScalarSchema(args) {
1812
+ const {schema: schema, entryType: entryType, options: options, validation: validation} = args;
1813
+ return options === void 0 && validation === void 0 ? schema : v__namespace.pipe(schema, v__namespace.check(value => choiceValueIssues(options, value) === void 0 && scalarValidationIssues({
1814
+ entryType: entryType,
1815
+ validation: validation,
1816
+ value: value
1817
+ }) === void 0, issue => [ ...choiceValueIssues(options, issue.input) ?? [], ...scalarValidationIssues({
1818
+ entryType: entryType,
1819
+ validation: validation,
1820
+ value: issue.input
1821
+ }) ?? [] ].join("; ")));
1331
1822
  }
1332
1823
 
1333
1824
  function objectSchema(fields, leaf) {
@@ -1350,7 +1841,7 @@ function appendItemSchema(entryType, shape) {
1350
1841
  function rejectedRefTypes(args) {
1351
1842
  const {entryType: entryType, types: types, value: value} = args;
1352
1843
  if (types === void 0 || value === null || value === void 0) return [];
1353
- if (entryType !== "doc.ref" && entryType !== "doc.refs") return [];
1844
+ if (!refKindAcceptsTypes(entryType)) return [];
1354
1845
  let items = [ value ];
1355
1846
  return entryType === "doc.refs" && (items = Array.isArray(value) ? value : []),
1356
1847
  [ ...new Set(items.map(gdrTypeOf).filter(t => t !== void 0 && !types.includes(t))) ];
@@ -1385,6 +1876,10 @@ function checkValueAgainst(args, leaf) {
1385
1876
  entryType: args.entryType,
1386
1877
  types: args.types,
1387
1878
  value: args.value
1879
+ }) ?? choiceValueIssues(args.options, args.value) ?? scalarValidationIssues({
1880
+ entryType: args.entryType,
1881
+ validation: args.validation,
1882
+ value: args.value
1388
1883
  }) : formatIssues(result.issues);
1389
1884
  }
1390
1885
 
@@ -1410,15 +1905,16 @@ function isBareSeedId(id) {
1410
1905
 
1411
1906
  const AuthoringRefId = v__namespace.pipe(v__namespace.string(), v__namespace.check(isAuthoringRefId, "must be a bare document id, a GDR URI, or a portable `@<alias>:<id>` reference")), AuthoringGdrShape = v__namespace.looseObject({
1412
1907
  id: AuthoringRefId,
1413
- type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1908
+ type: NonEmptyString
1414
1909
  }), seedValueSchemas = {
1415
1910
  ...valueSchemas,
1416
1911
  "doc.ref": v__namespace.union([ v__namespace.null(), AuthoringGdrShape ]),
1417
1912
  "doc.refs": v__namespace.array(AuthoringGdrShape),
1913
+ subject: v__namespace.union([ v__namespace.null(), AuthoringGdrShape ]),
1418
1914
  "release.ref": v__namespace.union([ v__namespace.null(), v__namespace.looseObject({
1419
1915
  id: AuthoringRefId,
1420
1916
  type: v__namespace.literal("system.release"),
1421
- releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1917
+ releaseName: NonEmptyString
1422
1918
  }) ])
1423
1919
  };
1424
1920
 
@@ -1454,14 +1950,14 @@ function validateFieldAppendItem(args) {
1454
1950
  });
1455
1951
  }
1456
1952
 
1457
- function formatIssues(issues) {
1953
+ function formatIssues(issues, formatMessage = issue => issue.message) {
1458
1954
  return issues.map(i => {
1459
1955
  const keys = i.path?.map(p => p.key) ?? [];
1460
- return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
1956
+ return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(i)}`;
1461
1957
  });
1462
1958
  }
1463
1959
 
1464
- const NonEmpty = v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "must be a non-empty string")), PositiveInt = v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1960
+ const NonEmpty = NonEmptyString, PositiveInt = v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1465
1961
 
1466
1962
  function groqIdentifier(referencedAs) {
1467
1963
  return v__namespace.pipe(v__namespace.string(), v__namespace.regex(GROQ_IDENTIFIER, `must be a GROQ-safe identifier (letters, digits, underscore; not starting with a digit) because it is referenced as ${referencedAs} in GROQ conditions`));
@@ -1583,7 +2079,15 @@ function groupMembershipNames(group) {
1583
2079
  return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
1584
2080
  }
1585
2081
 
1586
- 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>`");
2082
+ 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__namespace.pipe(v__namespace.number(), v__namespace.finite("must be finite")), ScalarValidationSchema = v__namespace.pipe(v__namespace.strictObject({
2083
+ min: v__namespace.optional(FiniteNumber),
2084
+ max: v__namespace.optional(FiniteNumber)
2085
+ }), v__namespace.check(validation => validation.min !== void 0 || validation.max !== void 0, "declare at least one bound, or omit `validation`"), v__namespace.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__namespace.strictObject({
2086
+ list: v__namespace.pipe(v__namespace.array(v__namespace.strictObject({
2087
+ title: NonEmpty,
2088
+ value: v__namespace.union([ v__namespace.string(), v__namespace.number() ])
2089
+ })), v__namespace.minLength(1, "declare at least one choice, or omit `options`"))
2090
+ });
1587
2091
 
1588
2092
  function asShape(input) {
1589
2093
  return typeof input == "object" && input !== null ? input : {};
@@ -1617,14 +2121,16 @@ function compositeChecked(entries) {
1617
2121
  return v__namespace.pipe(v__namespace.strictObject(entries), v__namespace.check(input => compositeShapeOk(input), issue => compositeShapeMessage(issue.input)), v__namespace.check(input => duplicateSubfieldName(input) === void 0, issue => `duplicate sub-field name "${duplicateSubfieldName(issue.input)}" — sub-field names must be unique within \`fields\` / \`of\``));
1618
2122
  }
1619
2123
 
1620
- const FieldShapeSchema = v__namespace.lazy(() => compositeChecked({
2124
+ const FieldShapeSchema = v__namespace.lazy(() => v__namespace.pipe(compositeChecked({
1621
2125
  type: FieldValueKindSchema,
1622
2126
  name: FieldEntryName,
1623
2127
  title: v__namespace.optional(v__namespace.string()),
1624
2128
  description: v__namespace.optional(v__namespace.string()),
2129
+ options: v__namespace.optional(ChoiceOptionsSchema),
2130
+ validation: v__namespace.optional(ScalarValidationSchema),
1625
2131
  fields: v__namespace.optional(v__namespace.array(FieldShapeSchema)),
1626
2132
  of: v__namespace.optional(v__namespace.array(FieldShapeSchema))
1627
- })), StoredEditableSchema = v__namespace.union([ v__namespace.literal(!0), NonEmpty ]), AuthoringEditableSchema = v__namespace.union([ v__namespace.literal(!0), v__namespace.array(NonEmpty), NonEmpty ]);
2133
+ }), choiceOptionsCheck(), scalarValidationCheck())), StoredEditableSchema = v__namespace.union([ v__namespace.literal(!0), NonEmpty ]), AuthoringEditableSchema = v__namespace.union([ v__namespace.literal(!0), v__namespace.array(NonEmpty), NonEmpty ]);
1628
2134
 
1629
2135
  function fieldBase(editable, group) {
1630
2136
  return {
@@ -1642,6 +2148,8 @@ function fieldEntryFields(editable, group) {
1642
2148
  return {
1643
2149
  type: FieldKindSchema,
1644
2150
  ...fieldBase(editable, group),
2151
+ options: v__namespace.optional(ChoiceOptionsSchema),
2152
+ validation: v__namespace.optional(ScalarValidationSchema),
1645
2153
  types: v__namespace.optional(v__namespace.pipe(v__namespace.array(NonEmpty), v__namespace.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
1646
2154
  fields: v__namespace.optional(v__namespace.array(FieldShapeSchema)),
1647
2155
  of: v__namespace.optional(v__namespace.array(FieldShapeSchema))
@@ -1654,7 +2162,9 @@ function literalSeedIssues(entry) {
1654
2162
  value: entry.initialValue.value,
1655
2163
  types: entry.types,
1656
2164
  fields: entry.fields,
1657
- of: entry.of
2165
+ of: entry.of,
2166
+ options: entry.options,
2167
+ validation: entry.validation
1658
2168
  });
1659
2169
  }
1660
2170
 
@@ -1663,10 +2173,37 @@ function literalSeedCheck() {
1663
2173
  }
1664
2174
 
1665
2175
  function refTypesCheck() {
1666
- return v__namespace.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}"`);
2176
+ return v__namespace.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}"`);
2177
+ }
2178
+
2179
+ function choiceOptionsCheck() {
2180
+ return v__namespace.check(entry => checkChoiceList({
2181
+ entryType: entry.type,
2182
+ options: entry.options,
2183
+ validation: entry.validation
2184
+ }) === void 0, issue => (checkChoiceList({
2185
+ entryType: issue.input.type,
2186
+ options: issue.input.options,
2187
+ validation: issue.input.validation
2188
+ }) ?? []).join("; "));
2189
+ }
2190
+
2191
+ const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number" ]);
2192
+
2193
+ function scalarValidationCheck() {
2194
+ return v__namespace.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
1667
2195
  }
1668
2196
 
1669
- const FieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields(StoredEditableSchema, StoredGroupMembershipSchema)), refTypesCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields(AuthoringEditableSchema, AuthoringGroupMembershipSchema)), refTypesCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v__namespace.strictObject({
2197
+ function scalarValidationDeclarationIssues(entry) {
2198
+ const {type: type, validation: validation} = entry;
2199
+ if (validation === void 0) return;
2200
+ if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` values, not "${type}"` ];
2201
+ if (type === "number") return;
2202
+ const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
2203
+ return issues.length === 0 ? void 0 : issues;
2204
+ }
2205
+
2206
+ const FieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields(StoredEditableSchema, StoredGroupMembershipSchema)), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields(AuthoringEditableSchema, AuthoringGroupMembershipSchema)), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v__namespace.strictObject({
1670
2207
  type: v__namespace.literal("claim"),
1671
2208
  name: FieldEntryName,
1672
2209
  title: v__namespace.optional(v__namespace.string()),
@@ -1697,17 +2234,25 @@ const TodoListFieldSchema = pinned()(v__namespace.strictObject(listSugarFields("
1697
2234
  with: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1698
2235
  context: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
1699
2236
  onExit: v__namespace.optional(picklist([ "detach", "abort" ]))
1700
- }), ActionParamSchema = v__namespace.strictObject({
2237
+ }), ActionParamSchema = v__namespace.pipe(v__namespace.strictObject({
1701
2238
  type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
1702
2239
  name: NonEmpty,
1703
2240
  title: v__namespace.optional(v__namespace.string()),
1704
2241
  description: v__namespace.optional(v__namespace.string()),
1705
- required: v__namespace.optional(v__namespace.boolean())
1706
- });
2242
+ required: v__namespace.optional(v__namespace.boolean()),
2243
+ options: v__namespace.optional(ChoiceOptionsSchema),
2244
+ validation: v__namespace.optional(ScalarValidationSchema)
2245
+ }), choiceOptionsCheck(), scalarValidationCheck());
2246
+
2247
+ function hasUniqueSemanticNamespaces(semantics) {
2248
+ const namespaces = semantics.map(semantic => semantic.split(".", 1)[0]);
2249
+ return new Set(namespaces).size === namespaces.length;
2250
+ }
1707
2251
 
1708
2252
  function actionFields(op, group) {
1709
2253
  return {
1710
2254
  name: NonEmpty,
2255
+ semantics: v__namespace.optional(v__namespace.pipe(v__namespace.array(picklist(ACTION_SEMANTICS)), v__namespace.minLength(1, "declare at least one semantic, or omit `semantics`"), v__namespace.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace"))),
1711
2256
  title: v__namespace.optional(v__namespace.string()),
1712
2257
  description: v__namespace.optional(v__namespace.string()),
1713
2258
  group: v__namespace.optional(group),
@@ -1844,7 +2389,8 @@ const StoredStageSchema = pinned()(v__namespace.strictObject(stageFields({
1844
2389
  function startFields(kind) {
1845
2390
  return {
1846
2391
  kind: kind,
1847
- filter: v__namespace.optional(ConditionSchema)
2392
+ filter: v__namespace.optional(ConditionSchema),
2393
+ allowed: v__namespace.optional(ConditionSchema)
1848
2394
  };
1849
2395
  }
1850
2396
 
@@ -1895,31 +2441,13 @@ function startKindOf(definition) {
1895
2441
  }
1896
2442
 
1897
2443
  function isSubjectEntry(entry) {
1898
- return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
2444
+ return entry.type === "subject";
1899
2445
  }
1900
2446
 
1901
2447
  function isInputSourced(entry) {
1902
2448
  return entry.initialValue?.type === "input";
1903
2449
  }
1904
2450
 
1905
- function formatValidationError(label, issues) {
1906
- const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1907
- return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1908
- }
1909
-
1910
- function issuesFromValibot(issues) {
1911
- return issues.map(issue => ({
1912
- path: issue.path ? issue.path.map(item => item.key) : [],
1913
- message: issue.message
1914
- }));
1915
- }
1916
-
1917
- function formatIssuePath(path) {
1918
- let out = "";
1919
- for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1920
- return out;
1921
- }
1922
-
1923
2451
  function parseOrThrow({schema: schema, input: input, label: label}) {
1924
2452
  const result = v__namespace.safeParse(schema, input);
1925
2453
  if (!result.success) throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
@@ -1948,6 +2476,13 @@ function checkDuplicates({names: names, what: what, issues: issues}) {
1948
2476
  return seen;
1949
2477
  }
1950
2478
 
2479
+ function checkDefinitionName(def, issues) {
2480
+ LAKE_ID_SEGMENT_RE.test(def.name) || issues.push({
2481
+ path: [ "name" ],
2482
+ 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`
2483
+ });
2484
+ }
2485
+
1951
2486
  function checkStages(def, issues) {
1952
2487
  const stageNames = checkDuplicates({
1953
2488
  names: def.stages.map((stage, i) => ({
@@ -2084,6 +2619,10 @@ function checkGuardNames(def, issues) {
2084
2619
  what: "guard name (the guard lake _id derives from it — unique per definition)",
2085
2620
  issues: issues
2086
2621
  });
2622
+ for (const {name: name, path: path} of sites) LAKE_ID_SEGMENT_RE.test(name) || issues.push({
2623
+ path: path,
2624
+ 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`
2625
+ });
2087
2626
  }
2088
2627
 
2089
2628
  function fieldScopes(def) {
@@ -2154,7 +2693,11 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
2154
2693
  const reads = conditionParameterNames(groq2);
2155
2694
  for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
2156
2695
  path: [ "predicates", name ],
2157
- message: `predicate "${name}" reads $${caller} — predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
2696
+ message: `predicate "${name}" reads $${caller} — predicates pre-evaluate caller-free; compose caller gates at the condition site instead`
2697
+ });
2698
+ reads.has("row") && issues.push({
2699
+ path: [ "predicates", name ],
2700
+ 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`
2158
2701
  });
2159
2702
  for (const other of names) other === name || !reads.has(other) || issues.push({
2160
2703
  path: [ "predicates", name ],
@@ -2162,24 +2705,35 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
2162
2705
  });
2163
2706
  }
2164
2707
 
2165
- function entryNames(entries) {
2166
- return new Set((entries ?? []).map(entry => entry.name));
2708
+ function entryMap(entries) {
2709
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
2167
2710
  }
2168
2711
 
2169
- function checkUnboundCallerVars(def, issues) {
2712
+ function checkUnboundConditionVars(def, issues) {
2170
2713
  for (const site of conditionSites(def)) {
2171
2714
  const reads = conditionParameterNames(site.groq);
2172
- for (const name of unboundCallerVars(site.policy)) reads.has(name) && issues.push({
2715
+ for (const name of unboundVarsAt(site)) reads.has(name) && issues.push({
2173
2716
  path: site.path,
2174
- message: callerVarMessage(site, name)
2717
+ message: name === "row" ? rowVarMessage(site) : callerVarMessage(site, name)
2175
2718
  });
2176
2719
  }
2177
2720
  }
2178
2721
 
2722
+ function unboundVarsAt(site) {
2723
+ const callerVars = unboundCallerVars(site.policy);
2724
+ return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
2725
+ }
2726
+
2179
2727
  function unboundCallerVars(policy) {
2180
2728
  return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
2181
2729
  }
2182
2730
 
2731
+ 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";
2732
+
2733
+ function rowVarMessage(site) {
2734
+ 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`;
2735
+ }
2736
+
2183
2737
  function callerVarMessage(site, name) {
2184
2738
  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`;
2185
2739
  }
@@ -2187,7 +2741,7 @@ function callerVarMessage(site, name) {
2187
2741
  function conditionSites(def) {
2188
2742
  const sites = [], workflowLayer = {
2189
2743
  scope: "workflow",
2190
- names: entryNames(def.fields)
2744
+ entries: entryMap(def.fields)
2191
2745
  };
2192
2746
  collectEditableSites({
2193
2747
  entries: def.fields,
@@ -2208,10 +2762,10 @@ function conditionSites(def) {
2208
2762
  function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
2209
2763
  const stageLayer = {
2210
2764
  scope: "stage",
2211
- names: entryNames(stage.fields)
2765
+ entries: entryMap(stage.fields)
2212
2766
  }, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
2213
2767
  scope: "activity",
2214
- names: entryNames(activity.fields)
2768
+ entries: entryMap(activity.fields)
2215
2769
  })) ];
2216
2770
  for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
2217
2771
  groq: t.when,
@@ -2225,6 +2779,7 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
2225
2779
  path: [ "stages", i, "fields" ],
2226
2780
  labelPrefix: `stage "${stage.name}"`,
2227
2781
  fields: editableWindow,
2782
+ unionWindow: !0,
2228
2783
  sites: sites
2229
2784
  });
2230
2785
  for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
@@ -2232,23 +2787,28 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
2232
2787
  path: [ "stages", i, "editable", name ],
2233
2788
  label: `stage "${stage.name}" editable override "${name}"`,
2234
2789
  policy: "caller-bound",
2235
- fields: editableWindow
2790
+ fields: editableWindow,
2791
+ unionWindow: !0
2236
2792
  });
2237
2793
  for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
2238
2794
  activity: activity,
2239
2795
  path: [ "stages", i, "activities", j ],
2240
2796
  stageFields: stageFields2,
2797
+ workflowLayer: workflowLayer,
2241
2798
  sites: sites
2242
2799
  });
2243
2800
  }
2244
2801
 
2245
- function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
2802
+ function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, unionWindow: unionWindow, sites: sites}) {
2246
2803
  for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
2247
2804
  groq: entry.editable,
2248
2805
  path: [ ...path, n, "editable" ],
2249
2806
  label: `${labelPrefix} field "${entry.name}" editable`,
2250
2807
  policy: "caller-bound",
2251
- fields: fields
2808
+ fields: fields,
2809
+ ...unionWindow === !0 ? {
2810
+ unionWindow: !0
2811
+ } : {}
2252
2812
  });
2253
2813
  }
2254
2814
 
@@ -2260,14 +2820,15 @@ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy
2260
2820
  ...policy !== void 0 ? {
2261
2821
  policy: policy
2262
2822
  } : {},
2823
+ bindsRow: !0,
2263
2824
  fields: fields
2264
2825
  });
2265
2826
  }
2266
2827
 
2267
- function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
2828
+ function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, workflowLayer: workflowLayer, sites: sites}) {
2268
2829
  const fields = [ {
2269
2830
  scope: "activity",
2270
- names: entryNames(activity.fields)
2831
+ entries: entryMap(activity.fields)
2271
2832
  }, ...stageFields2 ];
2272
2833
  activity.filter !== void 0 && sites.push({
2273
2834
  groq: activity.filter,
@@ -2293,11 +2854,12 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
2293
2854
  activity: activity,
2294
2855
  path: path,
2295
2856
  fields: fields,
2857
+ workflowLayer: workflowLayer,
2296
2858
  sites: sites
2297
2859
  });
2298
2860
  }
2299
2861
 
2300
- function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
2862
+ function collectActionConditionSites({activity: activity, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2301
2863
  for (const [a, action] of (activity.actions ?? []).entries()) {
2302
2864
  const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
2303
2865
  action.when !== void 0 && sites.push({
@@ -2332,12 +2894,13 @@ function collectActionConditionSites({activity: activity, path: path, fields: fi
2332
2894
  action: action,
2333
2895
  path: actionPath,
2334
2896
  fields: fields,
2897
+ workflowLayer: workflowLayer,
2335
2898
  sites: sites
2336
2899
  });
2337
2900
  }
2338
2901
  }
2339
2902
 
2340
- function collectSpawnSites({action: action, path: path, fields: fields, sites: sites}) {
2903
+ function collectSpawnSites({action: action, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2341
2904
  const spawn = action.spawn;
2342
2905
  if (spawn === void 0) return;
2343
2906
  const spawnPath = [ ...path, "spawn" ];
@@ -2346,13 +2909,17 @@ function collectSpawnSites({action: action, path: path, fields: fields, sites: s
2346
2909
  path: [ ...spawnPath, "forEach" ],
2347
2910
  label: `action "${action.name}".spawn.forEach`,
2348
2911
  policy: "cascade",
2349
- fields: fields
2912
+ fields: [ workflowLayer ],
2913
+ windowNote: "spawn.forEach evaluates against workflow-scope $fields only — stage/activity fields are never bound at discovery time"
2350
2914
  });
2351
- for (const [group, record] of [ [ "with", spawn.with ], [ "context", spawn.context ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2915
+ for (const [group, record, bindsRow] of [ [ "with", spawn.with, !0 ], [ "context", spawn.context, !1 ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2352
2916
  groq: groq2,
2353
2917
  path: [ ...spawnPath, group, key ],
2354
2918
  label: `action "${action.name}".spawn.${group} "${key}"`,
2355
2919
  policy: "triggered-payload",
2920
+ ...bindsRow ? {
2921
+ bindsRow: !0
2922
+ } : {},
2356
2923
  fields: fields
2357
2924
  });
2358
2925
  }
@@ -2376,6 +2943,52 @@ function checkAssigneesEntries(def, issues) {
2376
2943
  });
2377
2944
  }
2378
2945
 
2946
+ function nestedSubjectPaths(shapes, base) {
2947
+ return (shapes ?? []).flatMap((shape, i) => isSubjectEntry(shape) ? [ [ ...base, i, "type" ] ] : [ ...nestedSubjectPaths(shape.fields, [ ...base, i, "fields" ]), ...nestedSubjectPaths(shape.of, [ ...base, i, "of" ]) ]);
2948
+ }
2949
+
2950
+ function checkSubjectEffectOutputs(def, issues) {
2951
+ 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({
2952
+ action: action,
2953
+ path: [ "stages", i, "activities", j, "actions", a ],
2954
+ issues: issues
2955
+ });
2956
+ }
2957
+
2958
+ function pushSubjectOutputIssues(args) {
2959
+ const {action: action, path: path, issues: issues} = args;
2960
+ for (const [e, effect] of (action.effects ?? []).entries()) {
2961
+ const base = [ ...path, "effects", e, "outputs" ];
2962
+ for (const nestedPath of nestedSubjectPaths(effect.outputs, base)) issues.push({
2963
+ path: nestedPath,
2964
+ 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`
2965
+ });
2966
+ }
2967
+ }
2968
+
2969
+ function checkSubjectEntries(def, issues) {
2970
+ for (const {entries: entries, scope: scope, path: path, label: label} of fieldScopes(def)) {
2971
+ const subjects = (entries ?? []).flatMap((entry, n) => isSubjectEntry(entry) ? [ {
2972
+ entry: entry,
2973
+ n: n
2974
+ } ] : []);
2975
+ if (scope !== "workflow") for (const {entry: entry, n: n} of subjects) issues.push({
2976
+ path: [ ...path, n, "type" ],
2977
+ 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`
2978
+ }); else for (const {n: n} of subjects.slice(1)) issues.push({
2979
+ path: [ ...path, n ],
2980
+ message: "at most one subject-kind field entry per workflow — the runtime identifies THE subject by kind, and a second one makes it ambiguous"
2981
+ });
2982
+ for (const [n, entry] of (entries ?? []).entries()) {
2983
+ const nested = [ ...nestedSubjectPaths(entry.fields, [ ...path, n, "fields" ]), ...nestedSubjectPaths(entry.of, [ ...path, n, "of" ]) ];
2984
+ for (const nestedPath of nested) issues.push({
2985
+ path: nestedPath,
2986
+ 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`
2987
+ });
2988
+ }
2989
+ }
2990
+ }
2991
+
2379
2992
  function checkActivityTerminalPaths(def, issues) {
2380
2993
  for (const [i, stage] of def.stages.entries()) {
2381
2994
  const resolvable = terminallyResolvableActivities(stage);
@@ -2426,23 +3039,69 @@ function checkStart(def, issues) {
2426
3039
  if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2427
3040
  path: [ "start" ],
2428
3041
  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'`"
2429
- }), checkStartFilterReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
3042
+ }), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), checkStartSubjectVariable(def, issues),
3043
+ def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
2430
3044
  path: [ "fields", n, "required" ],
2431
- 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`
3045
+ 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)`
2432
3046
  });
2433
3047
  }
2434
3048
 
3049
+ function checkStartSubjectVariable(def, issues) {
3050
+ const subject = (def.fields ?? []).find(isSubjectEntry);
3051
+ 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)) {
3052
+ if (subject === void 0) {
3053
+ issues.push({
3054
+ path: [ "start", key ],
3055
+ 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`
3056
+ });
3057
+ continue;
3058
+ }
3059
+ key === "allowed" && !isInputSourced(subject) && issues.push({
3060
+ path: [ "start", key ],
3061
+ 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`
3062
+ });
3063
+ }
3064
+ }
3065
+
2435
3066
  function checkStartFilterReads(def, issues) {
2436
3067
  const filter = def.start?.filter;
2437
- if (filter === void 0) return;
2438
- const bindable = new Set((def.fields ?? []).filter(isInputSourced).map(entry => entry.name)), declared = new Set((def.fields ?? []).map(entry => entry.name));
2439
- for (const name of conditionFieldReadNames(filter)) bindable.has(name) || issues.push({
3068
+ filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
2440
3069
  path: [ "start", "filter" ],
2441
- 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)}`
2442
- });
2443
- readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
3070
+ 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"
3071
+ }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2444
3072
  path: [ "start", "filter" ],
2445
- 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"
3073
+ 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"
3074
+ }));
3075
+ }
3076
+
3077
+ function checkStartAllowedReads(def, issues) {
3078
+ const allowed = def.start?.allowed;
3079
+ if (allowed === void 0) return;
3080
+ 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())}`;
3081
+ for (const read of conditionFieldReads(allowed)) {
3082
+ const entry = bindable.get(read.name);
3083
+ if (entry === void 0) {
3084
+ issues.push({
3085
+ path: [ "start", "allowed" ],
3086
+ 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}`
3087
+ });
3088
+ continue;
3089
+ }
3090
+ pushFieldReadPathIssue({
3091
+ where: "start.allowed",
3092
+ read: {
3093
+ field: read.name,
3094
+ path: read.path
3095
+ },
3096
+ target: entry,
3097
+ path: [ "start", "allowed" ],
3098
+ issues: issues,
3099
+ nodes: START_ALLOWED_VALUE_NODES
3100
+ });
3101
+ }
3102
+ readsRootDocument(allowed) && issues.push({
3103
+ path: [ "start", "allowed" ],
3104
+ 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"
2446
3105
  });
2447
3106
  }
2448
3107
 
@@ -2553,39 +3212,80 @@ function checkGroupMembership({group: group, reachable: reachable, path: path, l
2553
3212
  }
2554
3213
 
2555
3214
  function checkConditionFieldReads(def, issues) {
2556
- const everywhere = allDeclaredFieldNames(def);
2557
- for (const site of conditionSites(def)) for (const name of conditionFieldReadNames(site.groq)) fieldVisibleAtSite({
3215
+ const everywhere = allDeclaredFieldEntries(def);
3216
+ for (const site of conditionSites(def)) checkSiteFieldReads({
2558
3217
  site: site,
2559
- name: name,
2560
- everywhere: everywhere
2561
- }) || issues.push({
2562
- path: site.path,
2563
- message: undeclaredFieldReadMessage(site, name)
3218
+ everywhere: everywhere,
3219
+ issues: issues
2564
3220
  });
2565
- for (const [name, groq2] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq2)) everywhere.has(read) || issues.push({
2566
- path: [ "predicates", name ],
2567
- message: noFieldAnywhereMessage(`predicate "${name}"`, read)
3221
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkSiteFieldReads({
3222
+ site: {
3223
+ groq: groq2,
3224
+ path: [ "predicates", name ],
3225
+ label: `predicate "${name}"`,
3226
+ fields: "context-dependent"
3227
+ },
3228
+ everywhere: everywhere,
3229
+ issues: issues
2568
3230
  });
2569
3231
  }
2570
3232
 
2571
- function allDeclaredFieldNames(def) {
2572
- const names = /* @__PURE__ */ new Set;
2573
- for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) names.add(entry.name);
2574
- return names;
3233
+ function checkSiteFieldReads({site: site, everywhere: everywhere, issues: issues}) {
3234
+ for (const read of conditionFieldReads(site.groq)) {
3235
+ const targets = readTargets({
3236
+ site: site,
3237
+ read: read,
3238
+ everywhere: everywhere
3239
+ });
3240
+ if (targets === "undeclared") {
3241
+ issues.push({
3242
+ path: site.path,
3243
+ message: undeclaredFieldReadMessage(site, read.name)
3244
+ });
3245
+ continue;
3246
+ }
3247
+ const path = read.path;
3248
+ path !== void 0 && (targets.some(target => fieldValuePathIssue({
3249
+ target: target,
3250
+ path: path
3251
+ }) === void 0) || pushFieldReadPathIssue({
3252
+ where: site.label,
3253
+ read: {
3254
+ field: read.name,
3255
+ path: path
3256
+ },
3257
+ target: targets[0],
3258
+ path: site.path,
3259
+ issues: issues
3260
+ }));
3261
+ }
3262
+ }
3263
+
3264
+ function readTargets({site: site, read: read, everywhere: everywhere}) {
3265
+ if (site.fields === "context-dependent") return everywhere.get(read.name) ?? "undeclared";
3266
+ const layers = site.fields.filter(layer => layer.entries.has(read.name));
3267
+ if (layers.length === 0) return "undeclared";
3268
+ const entries = layers.map(layer => layer.entries.get(read.name));
3269
+ return site.unionWindow === !0 ? entries : [ entries[0] ];
2575
3270
  }
2576
3271
 
2577
- function fieldVisibleAtSite({site: site, name: name, everywhere: everywhere}) {
2578
- return site.fields === "context-dependent" ? everywhere.has(name) : site.fields.some(layer => layer.names.has(name));
3272
+ function allDeclaredFieldEntries(def) {
3273
+ const declared = /* @__PURE__ */ new Map;
3274
+ for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) {
3275
+ const list = declared.get(entry.name);
3276
+ list === void 0 ? declared.set(entry.name, [ entry ]) : list.push(entry);
3277
+ }
3278
+ return declared;
2579
3279
  }
2580
3280
 
2581
3281
  function noFieldAnywhereMessage(label, name) {
2582
- 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`;
3282
+ 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`;
2583
3283
  }
2584
3284
 
2585
3285
  function undeclaredFieldReadMessage(site, name) {
2586
3286
  if (site.fields === "context-dependent") return noFieldAnywhereMessage(site.label, name);
2587
- const visible = site.fields.flatMap(layer => [ ...layer.names ].map(n => `${layer.scope}:${n}`));
2588
- 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)"}`;
3287
+ 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})`;
3288
+ return `${site.label} reads $fields.${name}, but ${window} — the read evaluates to GROQ null, so the condition silently misevaluates. Visible: ${visible.join(", ") || "(none)"}`;
2589
3289
  }
2590
3290
 
2591
3291
  function checkFieldReadSeeds(def, issues) {
@@ -2687,7 +3387,7 @@ function checkFieldReadOpValues(def, issues) {
2687
3387
  ops: site.ops,
2688
3388
  path: site.path,
2689
3389
  label: site.label,
2690
- activityNames: entryNames(site.activity.fields)
3390
+ activityEntries: site.activity.fields ?? []
2691
3391
  });
2692
3392
  }
2693
3393
 
@@ -2707,8 +3407,11 @@ function fieldReadsIn(value, path) {
2707
3407
  } ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
2708
3408
  }
2709
3409
 
2710
- function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityNames: activityNames, issues: issues}) {
3410
+ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
2711
3411
  const hosts = [ {
3412
+ scope: "activity",
3413
+ entries: activityEntries
3414
+ }, {
2712
3415
  scope: "stage",
2713
3416
  entries: stageEntries
2714
3417
  }, {
@@ -2721,8 +3424,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2721
3424
  message: opFieldReadMissMessage({
2722
3425
  read: read,
2723
3426
  where: where,
2724
- hosts: hosts,
2725
- activityNames: activityNames
3427
+ hosts: hosts
2726
3428
  })
2727
3429
  });
2728
3430
  return;
@@ -2736,9 +3438,9 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2736
3438
  });
2737
3439
  }
2738
3440
 
2739
- function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityNames: activityNames}) {
2740
- 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}`));
2741
- 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)"}`;
3441
+ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
3442
+ 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}`));
3443
+ 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)"}`;
2742
3444
  }
2743
3445
 
2744
3446
  function checkUpdateWhereOps(def, issues) {
@@ -2845,11 +3547,14 @@ function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, pa
2845
3547
  });
2846
3548
  }
2847
3549
 
2848
- function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
3550
+ function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues, nodes: nodes}) {
2849
3551
  if (read.path === void 0) return;
2850
3552
  const pathIssue = fieldValuePathIssue({
2851
3553
  target: target,
2852
- path: read.path
3554
+ path: read.path,
3555
+ ...nodes ? {
3556
+ nodes: nodes
3557
+ } : {}
2853
3558
  });
2854
3559
  pathIssue !== void 0 && issues.push({
2855
3560
  path: path,
@@ -2898,6 +3603,7 @@ const SCALAR = {
2898
3603
  kind: "list",
2899
3604
  item: GDR_VALUE
2900
3605
  }),
3606
+ subject: () => DOC_CONTENT,
2901
3607
  "release.ref": () => RELEASE_VALUE,
2902
3608
  string: () => SCALAR,
2903
3609
  text: () => SCALAR,
@@ -2920,22 +3626,27 @@ const SCALAR = {
2920
3626
  kind: "rows",
2921
3627
  of: shape.of ?? []
2922
3628
  })
3629
+ }, START_ALLOWED_VALUE_NODES = {
3630
+ ...VALUE_NODES,
3631
+ "doc.ref": () => GDR_VALUE,
3632
+ subject: () => GDR_VALUE
2923
3633
  };
2924
3634
 
2925
- function valueNodeFor(shape) {
2926
- return Object.hasOwn(VALUE_NODES, shape.type) ? VALUE_NODES[shape.type](shape) : void 0;
3635
+ function valueNodeFor(shape, nodes) {
3636
+ return Object.hasOwn(nodes, shape.type) ? nodes[shape.type](shape) : void 0;
2927
3637
  }
2928
3638
 
2929
3639
  const INDEX_SEGMENT = /^\d+$/;
2930
3640
 
2931
- function stepIntoValueNode(node, segment) {
3641
+ function stepIntoValueNode(args) {
3642
+ const {node: node, segment: segment, nodes: nodes} = args;
2932
3643
  switch (node.kind) {
2933
3644
  case "scalar":
2934
3645
  return "the value is a scalar with no sub-paths";
2935
3646
 
2936
3647
  case "object":
2937
3648
  {
2938
- const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
3649
+ const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub, nodes);
2939
3650
  return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
2940
3651
  }
2941
3652
 
@@ -2956,18 +3667,24 @@ function stepIntoValueNode(node, segment) {
2956
3667
  }
2957
3668
  }
2958
3669
 
2959
- function fieldValuePathIssue({target: target, path: path}) {
2960
- let node = valueNodeFor(target);
3670
+ function fieldValuePathIssue({target: target, path: path, nodes: nodes = VALUE_NODES}) {
3671
+ let node = valueNodeFor(target, nodes);
2961
3672
  if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
2962
3673
  for (const segment of path.split(".")) {
2963
- const next = stepIntoValueNode(node, segment);
3674
+ const next = stepIntoValueNode({
3675
+ node: node,
3676
+ segment: segment,
3677
+ nodes: nodes
3678
+ });
2964
3679
  if (typeof next == "string") return `at segment "${segment}": ${next}`;
2965
3680
  node = next;
2966
3681
  }
2967
3682
  }
2968
3683
 
2969
3684
  function checkWorkflowInvariants(def) {
2970
- const issues = [], stageNames = checkStages(def, issues);
3685
+ const issues = [];
3686
+ checkDefinitionName(def, issues);
3687
+ const stageNames = checkStages(def, issues);
2971
3688
  return checkInitialStage({
2972
3689
  def: def,
2973
3690
  stageNames: stageNames,
@@ -2982,17 +3699,24 @@ function checkWorkflowInvariants(def) {
2982
3699
  issues: issues
2983
3700
  }), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
2984
3701
  checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
2985
- checkUnboundCallerVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3702
+ checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
2986
3703
  checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
2987
- checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
3704
+ checkAssigneesEntries(def, issues), checkSubjectEntries(def, issues), checkSubjectEffectOutputs(def, issues),
3705
+ checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
2988
3706
  checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
2989
3707
  checkGroups(def, issues), issues;
2990
3708
  }
2991
3709
 
3710
+ exports.ACTION_SEMANTICS = ACTION_SEMANTICS;
3711
+
2992
3712
  exports.ACTIVITY_KINDS = ACTIVITY_KINDS;
2993
3713
 
3714
+ exports.ACTIVITY_STATUSES = ACTIVITY_STATUSES;
3715
+
2994
3716
  exports.ACTOR_KINDS = ACTOR_KINDS;
2995
3717
 
3718
+ exports.ActorShape = ActorShape;
3719
+
2996
3720
  exports.AuthoringActionSchema = AuthoringActionSchema;
2997
3721
 
2998
3722
  exports.AuthoringActivitySchema = AuthoringActivitySchema;
@@ -3015,6 +3739,12 @@ exports.CONDITION_VARS = CONDITION_VARS;
3015
3739
 
3016
3740
  exports.ContractViolationError = ContractViolationError;
3017
3741
 
3742
+ exports.DATA_MODEL_CHANGES = DATA_MODEL_CHANGES;
3743
+
3744
+ exports.DATA_MODEL_MIN_READER = DATA_MODEL_MIN_READER;
3745
+
3746
+ exports.DATA_MODEL_VERSION = DATA_MODEL_VERSION;
3747
+
3018
3748
  exports.DEFAULT_TRANSITION_WHEN = DEFAULT_TRANSITION_WHEN;
3019
3749
 
3020
3750
  exports.DOCUMENT_VALUE_PERMISSIONS = DOCUMENT_VALUE_PERMISSIONS;
@@ -3035,6 +3765,10 @@ exports.EffectSchema = EffectSchema;
3035
3765
 
3036
3766
  exports.FIELD_READ = FIELD_READ;
3037
3767
 
3768
+ exports.FIELD_SCOPES = FIELD_SCOPES;
3769
+
3770
+ exports.FIELD_VALUE_KINDS = FIELD_VALUE_KINDS;
3771
+
3038
3772
  exports.FILTER_SCOPE_VARS = FILTER_SCOPE_VARS;
3039
3773
 
3040
3774
  exports.FieldValueShapeError = FieldValueShapeError;
@@ -3043,20 +3777,44 @@ exports.GROUP_KINDS = GROUP_KINDS;
3043
3777
 
3044
3778
  exports.GUARD_PREDICATE_VARS = GUARD_PREDICATE_VARS;
3045
3779
 
3780
+ exports.GdrShape = GdrShape;
3781
+
3046
3782
  exports.GroupSchema = GroupSchema;
3047
3783
 
3048
3784
  exports.InstanceNotFoundError = InstanceNotFoundError;
3049
3785
 
3786
+ exports.IsoTimestamp = IsoTimestamp;
3787
+
3788
+ exports.MUTATION_GUARD_ACTIONS = MUTATION_GUARD_ACTIONS;
3789
+
3790
+ exports.ModelVersionAheadError = ModelVersionAheadError;
3791
+
3792
+ exports.NonEmptyString = NonEmptyString;
3793
+
3794
+ exports.PersistedDocShapeError = PersistedDocShapeError;
3795
+
3796
+ exports.READER_MODEL_ROLLOUT_URL = READER_MODEL_ROLLOUT_URL;
3797
+
3050
3798
  exports.RESERVED_CONDITION_VARS = RESERVED_CONDITION_VARS;
3051
3799
 
3052
3800
  exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
3053
3801
 
3802
+ exports.ReaderModelAcknowledgementError = ReaderModelAcknowledgementError;
3803
+
3804
+ exports.START_ALLOWED_VARS = START_ALLOWED_VARS;
3805
+
3054
3806
  exports.START_FILTER_VARS = START_FILTER_VARS;
3055
3807
 
3808
+ exports.SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR = SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR;
3809
+
3810
+ exports.SpawnContractsInvalidError = SpawnContractsInvalidError;
3811
+
3056
3812
  exports.StoredFieldOpSchema = StoredFieldOpSchema;
3057
3813
 
3058
3814
  exports.WORKFLOW_DEFINITION_TYPE = WORKFLOW_DEFINITION_TYPE;
3059
3815
 
3816
+ exports.WORKFLOW_INSTANCE_TYPE = WORKFLOW_INSTANCE_TYPE;
3817
+
3060
3818
  exports.WorkflowConfigSchema = WorkflowConfigSchema;
3061
3819
 
3062
3820
  exports.WorkflowError = WorkflowError;
@@ -3065,10 +3823,16 @@ exports.actorFulfillsRole = actorFulfillsRole;
3065
3823
 
3066
3824
  exports.andConditions = andConditions;
3067
3825
 
3826
+ exports.assertReadableModel = assertReadableModel;
3827
+
3828
+ exports.assertReaderModelAcknowledgement = assertReaderModelAcknowledgement;
3829
+
3068
3830
  exports.checkFieldValue = checkFieldValue;
3069
3831
 
3070
3832
  exports.checkWorkflowInvariants = checkWorkflowInvariants;
3071
3833
 
3834
+ exports.choiceValueIssues = choiceValueIssues;
3835
+
3072
3836
  exports.clientConfigFromResource = clientConfigFromResource;
3073
3837
 
3074
3838
  exports.conditionEffectReads = conditionEffectReads;
@@ -3101,6 +3865,10 @@ exports.evaluatePredicates = evaluatePredicates;
3101
3865
 
3102
3866
  exports.extractDocumentId = extractDocumentId;
3103
3867
 
3868
+ exports.fieldTreeShape = fieldTreeShape;
3869
+
3870
+ exports.fieldValueSchemas = fieldValueSchemas;
3871
+
3104
3872
  exports.formatIssuePath = formatIssuePath;
3105
3873
 
3106
3874
  exports.formatIssues = formatIssues;
@@ -3133,6 +3901,12 @@ exports.isInputSourced = isInputSourced;
3133
3901
 
3134
3902
  exports.isNotesEntry = isNotesEntry;
3135
3903
 
3904
+ exports.isParseableInstant = isParseableInstant;
3905
+
3906
+ exports.isSingleDocRefEntry = isSingleDocRefEntry;
3907
+
3908
+ exports.isSingleDocRefKind = isSingleDocRefKind;
3909
+
3136
3910
  exports.isStartableDefinition = isStartableDefinition;
3137
3911
 
3138
3912
  exports.isSubjectEntry = isSubjectEntry;
@@ -3145,12 +3919,26 @@ exports.isTodoListItem = isTodoListItem;
3145
3919
 
3146
3920
  exports.isUnevaluable = isUnevaluable;
3147
3921
 
3922
+ exports.isUnprimed = isUnprimed;
3923
+
3148
3924
  exports.labelFor = labelFor;
3149
3925
 
3926
+ exports.minReaderModelOf = minReaderModelOf;
3927
+
3928
+ exports.modelStampFor = modelStampFor;
3929
+
3930
+ exports.modelVersionOf = modelVersionOf;
3931
+
3932
+ exports.parentRef = parentRef;
3933
+
3934
+ exports.parseDefinitionSnapshot = parseDefinitionSnapshot;
3935
+
3150
3936
  exports.parseGdr = parseGdr;
3151
3937
 
3152
3938
  exports.parseOrThrow = parseOrThrow;
3153
3939
 
3940
+ exports.parsePersistedDoc = parsePersistedDoc;
3941
+
3154
3942
  exports.parseResourceGdr = parseResourceGdr;
3155
3943
 
3156
3944
  exports.parseStoredDefinition = parseStoredDefinition;
@@ -3163,6 +3951,8 @@ exports.refDashboard = refDashboard;
3163
3951
 
3164
3952
  exports.refDataset = refDataset;
3165
3953
 
3954
+ exports.refKindAcceptsTypes = refKindAcceptsTypes;
3955
+
3166
3956
  exports.refMediaLibrary = refMediaLibrary;
3167
3957
 
3168
3958
  exports.refTypeIssues = refTypeIssues;
@@ -3173,6 +3963,10 @@ exports.releaseDocId = releaseDocId;
3173
3963
 
3174
3964
  exports.releaseRef = releaseRef;
3175
3965
 
3966
+ exports.requiredModelFeatures = requiredModelFeatures;
3967
+
3968
+ exports.requiredReaderModel = requiredReaderModel;
3969
+
3176
3970
  exports.resourceAliasesToMap = resourceAliasesToMap;
3177
3971
 
3178
3972
  exports.resourceFromGdrUri = resourceFromGdrUri;
@@ -3187,16 +3981,26 @@ exports.runGroq = runGroq;
3187
3981
 
3188
3982
  exports.sameResource = sameResource;
3189
3983
 
3984
+ exports.scalarValidationIssues = scalarValidationIssues;
3985
+
3986
+ exports.schemaTreeShape = schemaTreeShape;
3987
+
3190
3988
  exports.selfGdr = selfGdr;
3191
3989
 
3192
3990
  exports.startKindOf = startKindOf;
3193
3991
 
3194
3992
  exports.tagScopeFilter = tagScopeFilter;
3195
3993
 
3994
+ exports.terminalState = terminalState;
3995
+
3196
3996
  exports.toBareId = toBareId;
3197
3997
 
3198
3998
  exports.toPhysicalGdr = toPhysicalGdr;
3199
3999
 
4000
+ exports.tolerantEntries = tolerantEntries;
4001
+
4002
+ exports.tolerantObject = tolerantObject;
4003
+
3200
4004
  exports.tryParseGdr = tryParseGdr;
3201
4005
 
3202
4006
  exports.validateFieldAppendItem = validateFieldAppendItem;