@sanity/workflow-engine 0.16.0 → 0.17.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.
@@ -299,17 +299,17 @@ function effectNotFoundMessage(args) {
299
299
  return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
300
300
  }
301
301
 
302
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
302
+ const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
303
303
 
304
304
  function validateTag(tag) {
305
- if (!TAG_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`);
305
+ if (!LAKE_ID_SEGMENT_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS})`);
306
306
  }
307
307
 
308
308
  function tagScopeFilter() {
309
309
  return "tag == $tag";
310
310
  }
311
311
 
312
- const NonEmptyString = v.pipe(v.string(), v.nonEmpty("must not be empty"));
312
+ const NonEmptyString$1 = v.pipe(v.string(), v.nonEmpty("must not be empty"));
313
313
 
314
314
  function asPredicate(validate) {
315
315
  return value => {
@@ -323,21 +323,21 @@ function asPredicate(validate) {
323
323
 
324
324
  const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v.variant("type", [ v.object({
325
325
  type: v.literal("dataset"),
326
- id: v.pipe(NonEmptyString, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
326
+ id: v.pipe(NonEmptyString$1, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
327
327
  }), v.object({
328
328
  type: v.literal("canvas"),
329
- id: NonEmptyString
329
+ id: NonEmptyString$1
330
330
  }), v.object({
331
331
  type: v.literal("media-library"),
332
- id: NonEmptyString
332
+ id: NonEmptyString$1
333
333
  }), v.object({
334
334
  type: v.literal("dashboard"),
335
- id: NonEmptyString
335
+ id: NonEmptyString$1
336
336
  }) ]), ResourceBindingSchema = v.object({
337
- name: v.pipe(NonEmptyString, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
337
+ name: v.pipe(NonEmptyString$1, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
338
338
  resource: WorkflowResourceSchema
339
339
  }), DefinitionSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v.object({
340
- name: NonEmptyString,
340
+ name: NonEmptyString$1,
341
341
  tag: v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
342
342
  workflowResource: WorkflowResourceSchema,
343
343
  resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => new Set(bindings.map(binding => binding.name)).size === bindings.length, "duplicate resource handle name — each binding name must be unique within a deployment"))),
@@ -502,6 +502,9 @@ function desugarStart(start) {
502
502
  kind: start.kind ?? "interactive",
503
503
  ...start.filter !== void 0 ? {
504
504
  filter: start.filter
505
+ } : {},
506
+ ...start.allowed !== void 0 ? {
507
+ allowed: start.allowed
505
508
  } : {}
506
509
  };
507
510
  }
@@ -1050,12 +1053,43 @@ function conditionParameterNames(groq2) {
1050
1053
  }
1051
1054
 
1052
1055
  function conditionFieldReadNames(groq2) {
1053
- const read = /* @__PURE__ */ new Set;
1054
- return walkAstNodes(tryParseGroq(groq2), node => {
1055
- if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
1056
- const base = node.base;
1057
- base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
1058
- }), read;
1056
+ return new Set(conditionFieldReads(groq2).map(read => read.name));
1057
+ }
1058
+
1059
+ function conditionFieldReads(groq2) {
1060
+ const reads = /* @__PURE__ */ new Map;
1061
+ return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
1062
+ }
1063
+
1064
+ function collectFieldReads(node, reads) {
1065
+ if (Array.isArray(node)) {
1066
+ for (const item of node) collectFieldReads(item, reads);
1067
+ return;
1068
+ }
1069
+ if (typeof node != "object" || node === null) return;
1070
+ const chain = fieldsAttributeChain(node);
1071
+ if (chain !== void 0) {
1072
+ const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
1073
+ reads.set(`${name}.${path ?? ""}`, {
1074
+ name: name,
1075
+ path: path
1076
+ });
1077
+ return;
1078
+ }
1079
+ for (const value of Object.values(node)) collectFieldReads(value, reads);
1080
+ }
1081
+
1082
+ function fieldsAttributeChain(node) {
1083
+ const names = [];
1084
+ let current = node;
1085
+ for (;typeof current == "object" && current !== null; ) {
1086
+ const typed = current;
1087
+ if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
1088
+ names.unshift(typed.name);
1089
+ const base = typed.base;
1090
+ if (base?.type === "Parameter" && base.name === "fields") return names;
1091
+ current = base;
1092
+ }
1059
1093
  }
1060
1094
 
1061
1095
  function conditionEffectReads(groq2) {
@@ -1129,7 +1163,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1129
1163
  name: "fields",
1130
1164
  binding: "always",
1131
1165
  label: "the workflow's fields",
1132
- description: "Declared field entries rendered by name (`$fields.<name>` is the value, no envelope). Stage/activity scopes overlay lexically; `doc.ref` values render the hydrated document when the snapshot holds it."
1166
+ description: "Declared field entries rendered by name (`$fields.<name>` is the value, no wrapper). Stage/activity scopes overlay lexically. What a read puts in your hand follows the declared kind: a singular `doc.ref` DEREFERENCES into the hydrated document (lake `_id`/`_type` plus content fields — which may themselves be named `id`/`type`), while `doc.refs` elements and `release.ref` stay REFERENCES — `{id, type[, releaseName]}` with `id` a GDR URI — because identity reads (membership, counting, joins) must stay total without hydrating every target, and targets may live in resources the evaluation cannot fetch from. Conditions evaluate against the in-memory snapshot only, so a dereferencing plural kind would silently hole wherever hydration lagged."
1133
1167
  }, {
1134
1168
  name: "parent",
1135
1169
  binding: "always",
@@ -1199,7 +1233,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1199
1233
  name: "row",
1200
1234
  binding: "spawn",
1201
1235
  label: "the spawned row",
1202
- description: "One `spawn.forEach` result row, bound while its `with` map evaluates — and the row under test while a where-op `where` evaluates (per row)."
1236
+ description: "One `spawn.forEach` result row, bound while its `with` map evaluates — and the row under test while a where-op `where` evaluates (per row). Deploy rejects a read at every other site (including `spawn.forEach` itself and `spawn.context`), where it is GROQ null."
1203
1237
  }, {
1204
1238
  name: "params",
1205
1239
  binding: "caller",
@@ -1215,20 +1249,148 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1215
1249
  description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1216
1250
  }, {
1217
1251
  name: "definition",
1218
- description: "The `name` of the definition under evaluation (its own start.filter binds it)."
1252
+ description: "The `name` of the definition under evaluation (its own start block binds it)."
1219
1253
  }, {
1220
1254
  name: "now",
1221
1255
  description: "The ISO clock reading of the evaluating engine."
1222
- }, {
1256
+ } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1223
1257
  name: "fields",
1224
- description: "The candidate initialFields by entry name, when the read surface has them (the Studio start control: yes; a per-doc picker: no). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble."
1258
+ description: "The caller's input entries by name (`initialFields` at `startInstance`, the values the start would seed; at a pre-flight, the values gathered so far, so a read of a not-yet-supplied entry is GROQ null). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble — a singular `doc.ref` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
1225
1259
  } ], GUARD_PREDICATE_VARS = [ {
1226
1260
  name: "guard",
1227
1261
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
1228
1262
  }, {
1229
1263
  name: "mutation",
1230
1264
  description: "The attempted mutation — `mutation.action` is the write kind being gated."
1231
- } ], ACTOR_KINDS = [ "person", "agent", "system" ];
1265
+ } ], NonEmptyString = v.pipe(v.string(), v.minLength(1, "must be a non-empty string"));
1266
+
1267
+ function isParseableInstant(value) {
1268
+ return typeof value == "string" && !Number.isNaN(Date.parse(value));
1269
+ }
1270
+
1271
+ const IsoTimestamp = v.pipe(v.string(), v.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
1272
+
1273
+ function tolerantObject() {
1274
+ return (entries, ..._exact) => v.looseObject(entries);
1275
+ }
1276
+
1277
+ function tolerantEntries() {
1278
+ return (entries, ..._exact) => entries;
1279
+ }
1280
+
1281
+ function schemaTreeShape(schema) {
1282
+ return walkSchemaShape(schema, /* @__PURE__ */ new Set);
1283
+ }
1284
+
1285
+ function walkSchemaShape(node, path) {
1286
+ if (typeof node != "object" || node === null) return "unknown";
1287
+ if (path.has(node)) return "(circular)";
1288
+ path.add(node);
1289
+ try {
1290
+ const schema = node;
1291
+ return containerShape(schema, path) ?? leafShape(schema);
1292
+ } finally {
1293
+ path.delete(node);
1294
+ }
1295
+ }
1296
+
1297
+ const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
1298
+ union: schema.options.map(option => walkSchemaShape(option, path))
1299
+ }), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
1300
+ strict_object: objectWalker,
1301
+ loose_object: objectWalker,
1302
+ object: objectWalker,
1303
+ array: (schema, path) => ({
1304
+ array: walkSchemaShape(schema.item, path)
1305
+ }),
1306
+ record: (schema, path) => ({
1307
+ record: walkSchemaShape(schema.value, path)
1308
+ }),
1309
+ union: unionWalker,
1310
+ variant: unionWalker,
1311
+ lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
1312
+ exact_optional: unwrapWalker,
1313
+ optional: unwrapWalker,
1314
+ nullable: unwrapWalker
1315
+ };
1316
+
1317
+ function containerShape(schema, path) {
1318
+ return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
1319
+ }
1320
+
1321
+ function objectEntriesShape(entries, path) {
1322
+ const out = {};
1323
+ for (const [key, entry] of Object.entries(entries)) {
1324
+ const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
1325
+ out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
1326
+ }
1327
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
1328
+ }
1329
+
1330
+ function isOptionalEntry(entry) {
1331
+ if (typeof entry != "object" || entry === null) return !1;
1332
+ const kind = entry.type;
1333
+ return kind === "exact_optional" || kind === "optional";
1334
+ }
1335
+
1336
+ const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
1337
+
1338
+ function leafShape(schema) {
1339
+ if (schema.type === "picklist") return schema.options.join(" | ");
1340
+ if (schema.type === "literal") return `literal ${String(schema.literal)}`;
1341
+ if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
1342
+ 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`);
1343
+ return String(schema.type);
1344
+ }
1345
+
1346
+ function formatValidationError(label, issues) {
1347
+ const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1348
+ return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1349
+ }
1350
+
1351
+ function issuesFromValibot(issues) {
1352
+ return issues.map(issue => ({
1353
+ path: issue.path ? issue.path.map(item => item.key) : [],
1354
+ message: issue.message
1355
+ }));
1356
+ }
1357
+
1358
+ function formatIssuePath(path) {
1359
+ let out = "";
1360
+ for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1361
+ return out;
1362
+ }
1363
+
1364
+ class PersistedDocShapeError extends WorkflowError {
1365
+ documentId;
1366
+ documentType;
1367
+ issues;
1368
+ constructor(args) {
1369
+ super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
1370
+ this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
1371
+ this.issues = args.issues;
1372
+ }
1373
+ }
1374
+
1375
+ function parsePersistedDoc(args) {
1376
+ const result = v.safeParse(args.schema, args.doc);
1377
+ if (!result.success) throw new PersistedDocShapeError({
1378
+ documentId: documentIdOf(args.doc),
1379
+ documentType: args.docType,
1380
+ issues: issuesFromValibot(result.issues)
1381
+ });
1382
+ return result.output;
1383
+ }
1384
+
1385
+ function documentIdOf(doc) {
1386
+ if (typeof doc == "object" && doc !== null) {
1387
+ const id = doc._id;
1388
+ if (typeof id == "string") return id;
1389
+ }
1390
+ return "(unknown id)";
1391
+ }
1392
+
1393
+ const ACTOR_KINDS = [ "person", "agent", "system" ];
1232
1394
 
1233
1395
  function releaseDocId(releaseName) {
1234
1396
  return `_.releases.${releaseName}`;
@@ -1275,29 +1437,28 @@ class FieldValueShapeError extends WorkflowError {
1275
1437
  }
1276
1438
  }
1277
1439
 
1278
- const GdrShape = v.looseObject({
1279
- id: v.pipe(v.string(), v.check(s => isGdrUri(s), "must be a GDR URI")),
1280
- type: v.pipe(v.string(), v.minLength(1))
1281
- }), ReleaseRefShape = v.pipe(v.looseObject({
1282
- id: v.pipe(v.string(), v.check(s => isGdrUri(s), "must be a GDR URI")),
1440
+ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
1441
+ id: GdrUriSchema,
1442
+ type: NonEmptyString
1443
+ }), ReleaseRefShape = v.pipe(tolerantObject()({
1444
+ id: GdrUriSchema,
1283
1445
  type: v.literal("system.release"),
1284
- releaseName: v.pipe(v.string(), v.minLength(1))
1285
- }), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = v.looseObject({
1446
+ releaseName: NonEmptyString
1447
+ }), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = tolerantObject()({
1286
1448
  kind: v.picklist(ACTOR_KINDS),
1287
- id: v.pipe(v.string(), v.minLength(1)),
1288
- roles: v.optional(v.array(v.string())),
1289
- onBehalfOf: v.optional(v.string())
1290
- }), AssigneeShape = v.union([ v.looseObject({
1449
+ id: NonEmptyString,
1450
+ roles: v.exactOptional(v.array(v.string())),
1451
+ onBehalfOf: v.exactOptional(v.string())
1452
+ }), AssigneeShape = v.union([ tolerantObject()({
1291
1453
  type: v.literal("user"),
1292
- id: v.pipe(v.string(), v.minLength(1))
1293
- }), v.looseObject({
1454
+ id: NonEmptyString
1455
+ }), tolerantObject()({
1294
1456
  type: v.literal("role"),
1295
- role: v.pipe(v.string(), v.minLength(1))
1296
- }) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableDateTime = v.union([ v.null(), v.pipe(v.string(), v.check(s => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")) ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, valueSchemas = {
1457
+ role: NonEmptyString
1458
+ }) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, fieldValueSchemas = {
1297
1459
  "doc.ref": v.union([ v.null(), GdrShape ]),
1298
1460
  "doc.refs": v.array(GdrShape),
1299
1461
  "release.ref": v.union([ v.null(), ReleaseRefShape ]),
1300
- query: v.any(),
1301
1462
  string: NullableString,
1302
1463
  text: NullableString,
1303
1464
  number: NullableNumber,
@@ -1308,6 +1469,9 @@ const GdrShape = v.looseObject({
1308
1469
  actor: v.union([ v.null(), ActorShape ]),
1309
1470
  assignee: v.union([ v.null(), AssigneeShape ]),
1310
1471
  assignees: v.array(AssigneeShape)
1472
+ }, valueSchemas = {
1473
+ ...fieldValueSchemas,
1474
+ query: v.any()
1311
1475
  };
1312
1476
 
1313
1477
  function shapeValueSchema(shape, leaf) {
@@ -1394,7 +1558,7 @@ function isBareSeedId(id) {
1394
1558
 
1395
1559
  const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a bare document id, a GDR URI, or a portable `@<alias>:<id>` reference")), AuthoringGdrShape = v.looseObject({
1396
1560
  id: AuthoringRefId,
1397
- type: v.pipe(v.string(), v.minLength(1))
1561
+ type: NonEmptyString
1398
1562
  }), seedValueSchemas = {
1399
1563
  ...valueSchemas,
1400
1564
  "doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
@@ -1402,7 +1566,7 @@ const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a b
1402
1566
  "release.ref": v.union([ v.null(), v.looseObject({
1403
1567
  id: AuthoringRefId,
1404
1568
  type: v.literal("system.release"),
1405
- releaseName: v.pipe(v.string(), v.minLength(1))
1569
+ releaseName: NonEmptyString
1406
1570
  }) ])
1407
1571
  };
1408
1572
 
@@ -1445,7 +1609,7 @@ function formatIssues(issues) {
1445
1609
  });
1446
1610
  }
1447
1611
 
1448
- const NonEmpty = v.pipe(v.string(), v.minLength(1, "must be a non-empty string")), PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1612
+ const NonEmpty = NonEmptyString, PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
1449
1613
 
1450
1614
  function groqIdentifier(referencedAs) {
1451
1615
  return v.pipe(v.string(), v.regex(GROQ_IDENTIFIER, `must be a GROQ-safe identifier (letters, digits, underscore; not starting with a digit) because it is referenced as ${referencedAs} in GROQ conditions`));
@@ -1828,7 +1992,8 @@ const StoredStageSchema = pinned()(v.strictObject(stageFields({
1828
1992
  function startFields(kind) {
1829
1993
  return {
1830
1994
  kind: kind,
1831
- filter: v.optional(ConditionSchema)
1995
+ filter: v.optional(ConditionSchema),
1996
+ allowed: v.optional(ConditionSchema)
1832
1997
  };
1833
1998
  }
1834
1999
 
@@ -1886,24 +2051,6 @@ function isInputSourced(entry) {
1886
2051
  return entry.initialValue?.type === "input";
1887
2052
  }
1888
2053
 
1889
- function formatValidationError(label, issues) {
1890
- const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1891
- return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1892
- }
1893
-
1894
- function issuesFromValibot(issues) {
1895
- return issues.map(issue => ({
1896
- path: issue.path ? issue.path.map(item => item.key) : [],
1897
- message: issue.message
1898
- }));
1899
- }
1900
-
1901
- function formatIssuePath(path) {
1902
- let out = "";
1903
- for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1904
- return out;
1905
- }
1906
-
1907
2054
  function parseOrThrow({schema: schema, input: input, label: label}) {
1908
2055
  const result = v.safeParse(schema, input);
1909
2056
  if (!result.success) throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
@@ -1932,6 +2079,13 @@ function checkDuplicates({names: names, what: what, issues: issues}) {
1932
2079
  return seen;
1933
2080
  }
1934
2081
 
2082
+ function checkDefinitionName(def, issues) {
2083
+ LAKE_ID_SEGMENT_RE.test(def.name) || issues.push({
2084
+ path: [ "name" ],
2085
+ 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`
2086
+ });
2087
+ }
2088
+
1935
2089
  function checkStages(def, issues) {
1936
2090
  const stageNames = checkDuplicates({
1937
2091
  names: def.stages.map((stage, i) => ({
@@ -2068,6 +2222,10 @@ function checkGuardNames(def, issues) {
2068
2222
  what: "guard name (the guard lake _id derives from it — unique per definition)",
2069
2223
  issues: issues
2070
2224
  });
2225
+ for (const {name: name, path: path} of sites) LAKE_ID_SEGMENT_RE.test(name) || issues.push({
2226
+ path: path,
2227
+ 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`
2228
+ });
2071
2229
  }
2072
2230
 
2073
2231
  function fieldScopes(def) {
@@ -2138,7 +2296,11 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
2138
2296
  const reads = conditionParameterNames(groq2);
2139
2297
  for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
2140
2298
  path: [ "predicates", name ],
2141
- message: `predicate "${name}" reads $${caller} — predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
2299
+ message: `predicate "${name}" reads $${caller} — predicates pre-evaluate caller-free; compose caller gates at the condition site instead`
2300
+ });
2301
+ reads.has("row") && issues.push({
2302
+ path: [ "predicates", name ],
2303
+ message: `predicate "${name}" reads $row — ${ROW_BINDING_CLAUSE}; predicates pre-evaluate once per instance, so the read is GROQ null and the predicate can never hold`
2142
2304
  });
2143
2305
  for (const other of names) other === name || !reads.has(other) || issues.push({
2144
2306
  path: [ "predicates", name ],
@@ -2146,24 +2308,35 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
2146
2308
  });
2147
2309
  }
2148
2310
 
2149
- function entryNames(entries) {
2150
- return new Set((entries ?? []).map(entry => entry.name));
2311
+ function entryMap(entries) {
2312
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
2151
2313
  }
2152
2314
 
2153
- function checkUnboundCallerVars(def, issues) {
2315
+ function checkUnboundConditionVars(def, issues) {
2154
2316
  for (const site of conditionSites(def)) {
2155
2317
  const reads = conditionParameterNames(site.groq);
2156
- for (const name of unboundCallerVars(site.policy)) reads.has(name) && issues.push({
2318
+ for (const name of unboundVarsAt(site)) reads.has(name) && issues.push({
2157
2319
  path: site.path,
2158
- message: callerVarMessage(site, name)
2320
+ message: name === "row" ? rowVarMessage(site) : callerVarMessage(site, name)
2159
2321
  });
2160
2322
  }
2161
2323
  }
2162
2324
 
2325
+ function unboundVarsAt(site) {
2326
+ const callerVars = unboundCallerVars(site.policy);
2327
+ return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
2328
+ }
2329
+
2163
2330
  function unboundCallerVars(policy) {
2164
2331
  return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
2165
2332
  }
2166
2333
 
2334
+ 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";
2335
+
2336
+ function rowVarMessage(site) {
2337
+ 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`;
2338
+ }
2339
+
2167
2340
  function callerVarMessage(site, name) {
2168
2341
  return site.policy === "cascade" ? `${site.label} reads $${name} — cascade gates (transition \`when\`s, activity \`filter\`s, a cascade-fired action's \`when\`/\`filter\`) must resolve identically no matter whose token drives the cascade ($assigned is constant false, the other caller vars hold no value); gate on instance state (e.g. a field an action wrote), or pin executing identities with \`roles\`` : site.policy === "caller-bound" ? `${site.label} reads $${name} — $params (the firing action's args) is bound only while the action's effect bindings and where-op \`where\`s evaluate; this site never binds it, so the condition could never pass. Bind $params in an effect binding or a where-op instead, or gate on a field an action wrote` : site.policy === "triggered-payload" && name === "params" ? `${site.label} reads $params — a cascade-fired action has no caller to supply args, so $params never holds a value in its payload; read fields or effect outputs instead` : `${site.label} reads $can — $can (the caller's grants) is bound only in the caller-bound projection (action filters, requirements, editable predicates) and never holds a value in cascade conditions; move the check to one of those sites or drop $can`;
2169
2342
  }
@@ -2171,7 +2344,7 @@ function callerVarMessage(site, name) {
2171
2344
  function conditionSites(def) {
2172
2345
  const sites = [], workflowLayer = {
2173
2346
  scope: "workflow",
2174
- names: entryNames(def.fields)
2347
+ entries: entryMap(def.fields)
2175
2348
  };
2176
2349
  collectEditableSites({
2177
2350
  entries: def.fields,
@@ -2192,10 +2365,10 @@ function conditionSites(def) {
2192
2365
  function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
2193
2366
  const stageLayer = {
2194
2367
  scope: "stage",
2195
- names: entryNames(stage.fields)
2368
+ entries: entryMap(stage.fields)
2196
2369
  }, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
2197
2370
  scope: "activity",
2198
- names: entryNames(activity.fields)
2371
+ entries: entryMap(activity.fields)
2199
2372
  })) ];
2200
2373
  for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
2201
2374
  groq: t.when,
@@ -2209,6 +2382,7 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
2209
2382
  path: [ "stages", i, "fields" ],
2210
2383
  labelPrefix: `stage "${stage.name}"`,
2211
2384
  fields: editableWindow,
2385
+ unionWindow: !0,
2212
2386
  sites: sites
2213
2387
  });
2214
2388
  for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
@@ -2216,23 +2390,28 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
2216
2390
  path: [ "stages", i, "editable", name ],
2217
2391
  label: `stage "${stage.name}" editable override "${name}"`,
2218
2392
  policy: "caller-bound",
2219
- fields: editableWindow
2393
+ fields: editableWindow,
2394
+ unionWindow: !0
2220
2395
  });
2221
2396
  for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
2222
2397
  activity: activity,
2223
2398
  path: [ "stages", i, "activities", j ],
2224
2399
  stageFields: stageFields2,
2400
+ workflowLayer: workflowLayer,
2225
2401
  sites: sites
2226
2402
  });
2227
2403
  }
2228
2404
 
2229
- function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
2405
+ function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, unionWindow: unionWindow, sites: sites}) {
2230
2406
  for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
2231
2407
  groq: entry.editable,
2232
2408
  path: [ ...path, n, "editable" ],
2233
2409
  label: `${labelPrefix} field "${entry.name}" editable`,
2234
2410
  policy: "caller-bound",
2235
- fields: fields
2411
+ fields: fields,
2412
+ ...unionWindow === !0 ? {
2413
+ unionWindow: !0
2414
+ } : {}
2236
2415
  });
2237
2416
  }
2238
2417
 
@@ -2244,14 +2423,15 @@ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy
2244
2423
  ...policy !== void 0 ? {
2245
2424
  policy: policy
2246
2425
  } : {},
2426
+ bindsRow: !0,
2247
2427
  fields: fields
2248
2428
  });
2249
2429
  }
2250
2430
 
2251
- function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
2431
+ function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, workflowLayer: workflowLayer, sites: sites}) {
2252
2432
  const fields = [ {
2253
2433
  scope: "activity",
2254
- names: entryNames(activity.fields)
2434
+ entries: entryMap(activity.fields)
2255
2435
  }, ...stageFields2 ];
2256
2436
  activity.filter !== void 0 && sites.push({
2257
2437
  groq: activity.filter,
@@ -2277,11 +2457,12 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
2277
2457
  activity: activity,
2278
2458
  path: path,
2279
2459
  fields: fields,
2460
+ workflowLayer: workflowLayer,
2280
2461
  sites: sites
2281
2462
  });
2282
2463
  }
2283
2464
 
2284
- function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
2465
+ function collectActionConditionSites({activity: activity, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2285
2466
  for (const [a, action] of (activity.actions ?? []).entries()) {
2286
2467
  const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
2287
2468
  action.when !== void 0 && sites.push({
@@ -2316,12 +2497,13 @@ function collectActionConditionSites({activity: activity, path: path, fields: fi
2316
2497
  action: action,
2317
2498
  path: actionPath,
2318
2499
  fields: fields,
2500
+ workflowLayer: workflowLayer,
2319
2501
  sites: sites
2320
2502
  });
2321
2503
  }
2322
2504
  }
2323
2505
 
2324
- function collectSpawnSites({action: action, path: path, fields: fields, sites: sites}) {
2506
+ function collectSpawnSites({action: action, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2325
2507
  const spawn = action.spawn;
2326
2508
  if (spawn === void 0) return;
2327
2509
  const spawnPath = [ ...path, "spawn" ];
@@ -2330,13 +2512,17 @@ function collectSpawnSites({action: action, path: path, fields: fields, sites: s
2330
2512
  path: [ ...spawnPath, "forEach" ],
2331
2513
  label: `action "${action.name}".spawn.forEach`,
2332
2514
  policy: "cascade",
2333
- fields: fields
2515
+ fields: [ workflowLayer ],
2516
+ windowNote: "spawn.forEach evaluates against workflow-scope $fields only — stage/activity fields are never bound at discovery time"
2334
2517
  });
2335
- for (const [group, record] of [ [ "with", spawn.with ], [ "context", spawn.context ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2518
+ for (const [group, record, bindsRow] of [ [ "with", spawn.with, !0 ], [ "context", spawn.context, !1 ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2336
2519
  groq: groq2,
2337
2520
  path: [ ...spawnPath, group, key ],
2338
2521
  label: `action "${action.name}".spawn.${group} "${key}"`,
2339
2522
  policy: "triggered-payload",
2523
+ ...bindsRow ? {
2524
+ bindsRow: !0
2525
+ } : {},
2340
2526
  fields: fields
2341
2527
  });
2342
2528
  }
@@ -2410,7 +2596,7 @@ function checkStart(def, issues) {
2410
2596
  if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2411
2597
  path: [ "start" ],
2412
2598
  message: "a spawn-only (lifecycle 'child') definition declares `start` — children are instantiated by a parent's `spawn`, never started standalone, so the block would never apply. Remove `start`, or drop `lifecycle: 'child'`"
2413
- }), checkStartFilterReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
2599
+ }), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
2414
2600
  path: [ "fields", n, "required" ],
2415
2601
  message: `start.kind 'autonomous' means runs are initiated by a system reacting to a document, so every required input must be derivable from that triggering document — required entry "${entry.name}" (kind "${entry.type}") is not a document reference. Make it optional, seed it another way (query/literal), or declare it doc.ref / doc.refs`
2416
2602
  });
@@ -2418,15 +2604,43 @@ function checkStart(def, issues) {
2418
2604
 
2419
2605
  function checkStartFilterReads(def, issues) {
2420
2606
  const filter = def.start?.filter;
2421
- if (filter === void 0) return;
2422
- const bindable = new Set((def.fields ?? []).filter(isInputSourced).map(entry => entry.name)), declared = new Set((def.fields ?? []).map(entry => entry.name));
2423
- for (const name of conditionFieldReadNames(filter)) bindable.has(name) || issues.push({
2607
+ filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
2424
2608
  path: [ "start", "filter" ],
2425
- message: declared.has(name) ? `start.filter reads $fields.${name}, but "${name}" is not an \`input\`-sourced entry $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after any read-side evaluation), so the read is GROQ null and the filter silently never passes. Bindable (input) fields: ${knownList(bindable)}` : `start.filter reads $fields.${name}, but no workflow-scope field entry named "${name}" is declared — $fields binds only the caller's input entries, so the read is GROQ null and the filter silently never passes. Bindable (input) fields: ${knownList(bindable)}`
2426
- });
2427
- readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2609
+ 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"
2610
+ }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2428
2611
  path: [ "start", "filter" ],
2429
- message: "start.filter reads the candidate document (its root), but the definition declares no required subject entry (a required doc.ref / doc.refs input) — a read surface would never have a document to bind as root, so the filter could never pass. Declare a required subject entry, or gate on $fields / the dataset instead"
2612
+ message: "start.filter reads the candidate document (its root), but the definition declares no required subject entry (a required doc.ref / doc.refs input) — a read surface would never have a document to bind as root, so every root read is GROQ null and the filter silently misevaluates. Declare a required subject entry, or gate on the dataset instead"
2613
+ }));
2614
+ }
2615
+
2616
+ function checkStartAllowedReads(def, issues) {
2617
+ const allowed = def.start?.allowed;
2618
+ if (allowed === void 0) return;
2619
+ 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())}`;
2620
+ for (const read of conditionFieldReads(allowed)) {
2621
+ const entry = bindable.get(read.name);
2622
+ if (entry === void 0) {
2623
+ issues.push({
2624
+ path: [ "start", "allowed" ],
2625
+ 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}`
2626
+ });
2627
+ continue;
2628
+ }
2629
+ pushFieldReadPathIssue({
2630
+ where: "start.allowed",
2631
+ read: {
2632
+ field: read.name,
2633
+ path: read.path
2634
+ },
2635
+ target: entry,
2636
+ path: [ "start", "allowed" ],
2637
+ issues: issues,
2638
+ nodes: START_ALLOWED_VALUE_NODES
2639
+ });
2640
+ }
2641
+ readsRootDocument(allowed) && issues.push({
2642
+ path: [ "start", "allowed" ],
2643
+ message: "start.allowed reads the candidate document as its root, but no root ever binds in the start-allowed context — startInstance holds inputs, not a loaded document, so the read is GROQ null and null semantics decide the verdict, never the workflow's real state. Read the subject as $fields.<entry> (its GDR URI is $fields.<entry>.id); a per-document visibility rule belongs in start.filter"
2430
2644
  });
2431
2645
  }
2432
2646
 
@@ -2537,39 +2751,80 @@ function checkGroupMembership({group: group, reachable: reachable, path: path, l
2537
2751
  }
2538
2752
 
2539
2753
  function checkConditionFieldReads(def, issues) {
2540
- const everywhere = allDeclaredFieldNames(def);
2541
- for (const site of conditionSites(def)) for (const name of conditionFieldReadNames(site.groq)) fieldVisibleAtSite({
2754
+ const everywhere = allDeclaredFieldEntries(def);
2755
+ for (const site of conditionSites(def)) checkSiteFieldReads({
2542
2756
  site: site,
2543
- name: name,
2544
- everywhere: everywhere
2545
- }) || issues.push({
2546
- path: site.path,
2547
- message: undeclaredFieldReadMessage(site, name)
2757
+ everywhere: everywhere,
2758
+ issues: issues
2548
2759
  });
2549
- for (const [name, groq2] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq2)) everywhere.has(read) || issues.push({
2550
- path: [ "predicates", name ],
2551
- message: noFieldAnywhereMessage(`predicate "${name}"`, read)
2760
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkSiteFieldReads({
2761
+ site: {
2762
+ groq: groq2,
2763
+ path: [ "predicates", name ],
2764
+ label: `predicate "${name}"`,
2765
+ fields: "context-dependent"
2766
+ },
2767
+ everywhere: everywhere,
2768
+ issues: issues
2552
2769
  });
2553
2770
  }
2554
2771
 
2555
- function allDeclaredFieldNames(def) {
2556
- const names = /* @__PURE__ */ new Set;
2557
- for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) names.add(entry.name);
2558
- return names;
2772
+ function checkSiteFieldReads({site: site, everywhere: everywhere, issues: issues}) {
2773
+ for (const read of conditionFieldReads(site.groq)) {
2774
+ const targets = readTargets({
2775
+ site: site,
2776
+ read: read,
2777
+ everywhere: everywhere
2778
+ });
2779
+ if (targets === "undeclared") {
2780
+ issues.push({
2781
+ path: site.path,
2782
+ message: undeclaredFieldReadMessage(site, read.name)
2783
+ });
2784
+ continue;
2785
+ }
2786
+ const path = read.path;
2787
+ path !== void 0 && (targets.some(target => fieldValuePathIssue({
2788
+ target: target,
2789
+ path: path
2790
+ }) === void 0) || pushFieldReadPathIssue({
2791
+ where: site.label,
2792
+ read: {
2793
+ field: read.name,
2794
+ path: path
2795
+ },
2796
+ target: targets[0],
2797
+ path: site.path,
2798
+ issues: issues
2799
+ }));
2800
+ }
2559
2801
  }
2560
2802
 
2561
- function fieldVisibleAtSite({site: site, name: name, everywhere: everywhere}) {
2562
- return site.fields === "context-dependent" ? everywhere.has(name) : site.fields.some(layer => layer.names.has(name));
2803
+ function readTargets({site: site, read: read, everywhere: everywhere}) {
2804
+ if (site.fields === "context-dependent") return everywhere.get(read.name) ?? "undeclared";
2805
+ const layers = site.fields.filter(layer => layer.entries.has(read.name));
2806
+ if (layers.length === 0) return "undeclared";
2807
+ const entries = layers.map(layer => layer.entries.get(read.name));
2808
+ return site.unionWindow === !0 ? entries : [ entries[0] ];
2809
+ }
2810
+
2811
+ function allDeclaredFieldEntries(def) {
2812
+ const declared = /* @__PURE__ */ new Map;
2813
+ for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) {
2814
+ const list = declared.get(entry.name);
2815
+ list === void 0 ? declared.set(entry.name, [ entry ]) : list.push(entry);
2816
+ }
2817
+ return declared;
2563
2818
  }
2564
2819
 
2565
2820
  function noFieldAnywhereMessage(label, name) {
2566
- return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently fails closed`;
2821
+ return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently misevaluates`;
2567
2822
  }
2568
2823
 
2569
2824
  function undeclaredFieldReadMessage(site, name) {
2570
2825
  if (site.fields === "context-dependent") return noFieldAnywhereMessage(site.label, name);
2571
- const visible = site.fields.flatMap(layer => [ ...layer.names ].map(n => `${layer.scope}:${n}`));
2572
- return `${site.label} reads $fields.${name}, but no field entry named "${name}" is lexically visible here — the read evaluates to GROQ null, so the condition silently fails closed. Visible: ${visible.join(", ") || "(none)"}`;
2826
+ 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})`;
2827
+ return `${site.label} reads $fields.${name}, but ${window} — the read evaluates to GROQ null, so the condition silently misevaluates. Visible: ${visible.join(", ") || "(none)"}`;
2573
2828
  }
2574
2829
 
2575
2830
  function checkFieldReadSeeds(def, issues) {
@@ -2671,7 +2926,7 @@ function checkFieldReadOpValues(def, issues) {
2671
2926
  ops: site.ops,
2672
2927
  path: site.path,
2673
2928
  label: site.label,
2674
- activityNames: entryNames(site.activity.fields)
2929
+ activityEntries: entryMap(site.activity.fields)
2675
2930
  });
2676
2931
  }
2677
2932
 
@@ -2691,7 +2946,7 @@ function fieldReadsIn(value, path) {
2691
2946
  } ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
2692
2947
  }
2693
2948
 
2694
- function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityNames: activityNames, issues: issues}) {
2949
+ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
2695
2950
  const hosts = [ {
2696
2951
  scope: "stage",
2697
2952
  entries: stageEntries
@@ -2706,7 +2961,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2706
2961
  read: read,
2707
2962
  where: where,
2708
2963
  hosts: hosts,
2709
- activityNames: activityNames
2964
+ activityEntries: activityEntries
2710
2965
  })
2711
2966
  });
2712
2967
  return;
@@ -2720,8 +2975,8 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2720
2975
  });
2721
2976
  }
2722
2977
 
2723
- function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityNames: activityNames}) {
2724
- const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityNames?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
2978
+ function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityEntries: activityEntries}) {
2979
+ const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityEntries?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
2725
2980
  return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty.${activityHint} Known: ${known.join(", ") || "(none)"}`;
2726
2981
  }
2727
2982
 
@@ -2829,11 +3084,14 @@ function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, pa
2829
3084
  });
2830
3085
  }
2831
3086
 
2832
- function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
3087
+ function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues, nodes: nodes}) {
2833
3088
  if (read.path === void 0) return;
2834
3089
  const pathIssue = fieldValuePathIssue({
2835
3090
  target: target,
2836
- path: read.path
3091
+ path: read.path,
3092
+ ...nodes ? {
3093
+ nodes: nodes
3094
+ } : {}
2837
3095
  });
2838
3096
  pathIssue !== void 0 && issues.push({
2839
3097
  path: path,
@@ -2904,22 +3162,26 @@ const SCALAR = {
2904
3162
  kind: "rows",
2905
3163
  of: shape.of ?? []
2906
3164
  })
3165
+ }, START_ALLOWED_VALUE_NODES = {
3166
+ ...VALUE_NODES,
3167
+ "doc.ref": () => GDR_VALUE
2907
3168
  };
2908
3169
 
2909
- function valueNodeFor(shape) {
2910
- return Object.hasOwn(VALUE_NODES, shape.type) ? VALUE_NODES[shape.type](shape) : void 0;
3170
+ function valueNodeFor(shape, nodes) {
3171
+ return Object.hasOwn(nodes, shape.type) ? nodes[shape.type](shape) : void 0;
2911
3172
  }
2912
3173
 
2913
3174
  const INDEX_SEGMENT = /^\d+$/;
2914
3175
 
2915
- function stepIntoValueNode(node, segment) {
3176
+ function stepIntoValueNode(args) {
3177
+ const {node: node, segment: segment, nodes: nodes} = args;
2916
3178
  switch (node.kind) {
2917
3179
  case "scalar":
2918
3180
  return "the value is a scalar with no sub-paths";
2919
3181
 
2920
3182
  case "object":
2921
3183
  {
2922
- const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
3184
+ const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub, nodes);
2923
3185
  return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
2924
3186
  }
2925
3187
 
@@ -2940,18 +3202,24 @@ function stepIntoValueNode(node, segment) {
2940
3202
  }
2941
3203
  }
2942
3204
 
2943
- function fieldValuePathIssue({target: target, path: path}) {
2944
- let node = valueNodeFor(target);
3205
+ function fieldValuePathIssue({target: target, path: path, nodes: nodes = VALUE_NODES}) {
3206
+ let node = valueNodeFor(target, nodes);
2945
3207
  if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
2946
3208
  for (const segment of path.split(".")) {
2947
- const next = stepIntoValueNode(node, segment);
3209
+ const next = stepIntoValueNode({
3210
+ node: node,
3211
+ segment: segment,
3212
+ nodes: nodes
3213
+ });
2948
3214
  if (typeof next == "string") return `at segment "${segment}": ${next}`;
2949
3215
  node = next;
2950
3216
  }
2951
3217
  }
2952
3218
 
2953
3219
  function checkWorkflowInvariants(def) {
2954
- const issues = [], stageNames = checkStages(def, issues);
3220
+ const issues = [];
3221
+ checkDefinitionName(def, issues);
3222
+ const stageNames = checkStages(def, issues);
2955
3223
  return checkInitialStage({
2956
3224
  def: def,
2957
3225
  stageNames: stageNames,
@@ -2966,11 +3234,11 @@ function checkWorkflowInvariants(def) {
2966
3234
  issues: issues
2967
3235
  }), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
2968
3236
  checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
2969
- checkUnboundCallerVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3237
+ checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
2970
3238
  checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
2971
3239
  checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
2972
3240
  checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
2973
3241
  checkGroups(def, issues), issues;
2974
3242
  }
2975
3243
 
2976
- export { ACTIVITY_KINDS, ACTOR_KINDS, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GroupSchema, InstanceNotFoundError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, START_FILTER_VARS, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
3244
+ export { ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, MUTATION_GUARD_ACTIONS, NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, START_ALLOWED_VARS, START_FILTER_VARS, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };