@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.
@@ -315,17 +315,17 @@ function effectNotFoundMessage(args) {
315
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
316
  }
317
317
 
318
- const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
318
+ 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
319
 
320
320
  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)`);
321
+ 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
322
  }
323
323
 
324
324
  function tagScopeFilter() {
325
325
  return "tag == $tag";
326
326
  }
327
327
 
328
- const NonEmptyString = v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty("must not be empty"));
328
+ const NonEmptyString$1 = v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty("must not be empty"));
329
329
 
330
330
  function asPredicate(validate) {
331
331
  return value => {
@@ -339,21 +339,21 @@ function asPredicate(validate) {
339
339
 
340
340
  const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v__namespace.variant("type", [ v__namespace.object({
341
341
  type: v__namespace.literal("dataset"),
342
- id: v__namespace.pipe(NonEmptyString, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
342
+ id: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
343
343
  }), v__namespace.object({
344
344
  type: v__namespace.literal("canvas"),
345
- id: NonEmptyString
345
+ id: NonEmptyString$1
346
346
  }), v__namespace.object({
347
347
  type: v__namespace.literal("media-library"),
348
- id: NonEmptyString
348
+ id: NonEmptyString$1
349
349
  }), v__namespace.object({
350
350
  type: v__namespace.literal("dashboard"),
351
- id: NonEmptyString
351
+ id: NonEmptyString$1
352
352
  }) ]), 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")),
353
+ name: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
354
354
  resource: WorkflowResourceSchema
355
355
  }), 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,
356
+ name: NonEmptyString$1,
357
357
  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
358
  workflowResource: WorkflowResourceSchema,
359
359
  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 +518,9 @@ function desugarStart(start) {
518
518
  kind: start.kind ?? "interactive",
519
519
  ...start.filter !== void 0 ? {
520
520
  filter: start.filter
521
+ } : {},
522
+ ...start.allowed !== void 0 ? {
523
+ allowed: start.allowed
521
524
  } : {}
522
525
  };
523
526
  }
@@ -1066,12 +1069,43 @@ function conditionParameterNames(groq2) {
1066
1069
  }
1067
1070
 
1068
1071
  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;
1072
+ return new Set(conditionFieldReads(groq2).map(read => read.name));
1073
+ }
1074
+
1075
+ function conditionFieldReads(groq2) {
1076
+ const reads = /* @__PURE__ */ new Map;
1077
+ return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
1078
+ }
1079
+
1080
+ function collectFieldReads(node, reads) {
1081
+ if (Array.isArray(node)) {
1082
+ for (const item of node) collectFieldReads(item, reads);
1083
+ return;
1084
+ }
1085
+ if (typeof node != "object" || node === null) return;
1086
+ const chain = fieldsAttributeChain(node);
1087
+ if (chain !== void 0) {
1088
+ const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
1089
+ reads.set(`${name}.${path ?? ""}`, {
1090
+ name: name,
1091
+ path: path
1092
+ });
1093
+ return;
1094
+ }
1095
+ for (const value of Object.values(node)) collectFieldReads(value, reads);
1096
+ }
1097
+
1098
+ function fieldsAttributeChain(node) {
1099
+ const names = [];
1100
+ let current = node;
1101
+ for (;typeof current == "object" && current !== null; ) {
1102
+ const typed = current;
1103
+ if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
1104
+ names.unshift(typed.name);
1105
+ const base = typed.base;
1106
+ if (base?.type === "Parameter" && base.name === "fields") return names;
1107
+ current = base;
1108
+ }
1075
1109
  }
1076
1110
 
1077
1111
  function conditionEffectReads(groq2) {
@@ -1145,7 +1179,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1145
1179
  name: "fields",
1146
1180
  binding: "always",
1147
1181
  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."
1182
+ 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."
1149
1183
  }, {
1150
1184
  name: "parent",
1151
1185
  binding: "always",
@@ -1215,7 +1249,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1215
1249
  name: "row",
1216
1250
  binding: "spawn",
1217
1251
  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)."
1252
+ 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
1253
  }, {
1220
1254
  name: "params",
1221
1255
  binding: "caller",
@@ -1231,20 +1265,148 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
1231
1265
  description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
1232
1266
  }, {
1233
1267
  name: "definition",
1234
- description: "The `name` of the definition under evaluation (its own start.filter binds it)."
1268
+ description: "The `name` of the definition under evaluation (its own start block binds it)."
1235
1269
  }, {
1236
1270
  name: "now",
1237
1271
  description: "The ISO clock reading of the evaluating engine."
1238
- }, {
1272
+ } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1239
1273
  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."
1274
+ 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."
1241
1275
  } ], GUARD_PREDICATE_VARS = [ {
1242
1276
  name: "guard",
1243
1277
  description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
1244
1278
  }, {
1245
1279
  name: "mutation",
1246
1280
  description: "The attempted mutation — `mutation.action` is the write kind being gated."
1247
- } ], ACTOR_KINDS = [ "person", "agent", "system" ];
1281
+ } ], NonEmptyString = v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "must be a non-empty string"));
1282
+
1283
+ function isParseableInstant(value) {
1284
+ return typeof value == "string" && !Number.isNaN(Date.parse(value));
1285
+ }
1286
+
1287
+ const IsoTimestamp = v__namespace.pipe(v__namespace.string(), v__namespace.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
1288
+
1289
+ function tolerantObject() {
1290
+ return (entries, ..._exact) => v__namespace.looseObject(entries);
1291
+ }
1292
+
1293
+ function tolerantEntries() {
1294
+ return (entries, ..._exact) => entries;
1295
+ }
1296
+
1297
+ function schemaTreeShape(schema) {
1298
+ return walkSchemaShape(schema, /* @__PURE__ */ new Set);
1299
+ }
1300
+
1301
+ function walkSchemaShape(node, path) {
1302
+ if (typeof node != "object" || node === null) return "unknown";
1303
+ if (path.has(node)) return "(circular)";
1304
+ path.add(node);
1305
+ try {
1306
+ const schema = node;
1307
+ return containerShape(schema, path) ?? leafShape(schema);
1308
+ } finally {
1309
+ path.delete(node);
1310
+ }
1311
+ }
1312
+
1313
+ const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
1314
+ union: schema.options.map(option => walkSchemaShape(option, path))
1315
+ }), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
1316
+ strict_object: objectWalker,
1317
+ loose_object: objectWalker,
1318
+ object: objectWalker,
1319
+ array: (schema, path) => ({
1320
+ array: walkSchemaShape(schema.item, path)
1321
+ }),
1322
+ record: (schema, path) => ({
1323
+ record: walkSchemaShape(schema.value, path)
1324
+ }),
1325
+ union: unionWalker,
1326
+ variant: unionWalker,
1327
+ lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
1328
+ exact_optional: unwrapWalker,
1329
+ optional: unwrapWalker,
1330
+ nullable: unwrapWalker
1331
+ };
1332
+
1333
+ function containerShape(schema, path) {
1334
+ return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
1335
+ }
1336
+
1337
+ function objectEntriesShape(entries, path) {
1338
+ const out = {};
1339
+ for (const [key, entry] of Object.entries(entries)) {
1340
+ const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
1341
+ out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
1342
+ }
1343
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
1344
+ }
1345
+
1346
+ function isOptionalEntry(entry) {
1347
+ if (typeof entry != "object" || entry === null) return !1;
1348
+ const kind = entry.type;
1349
+ return kind === "exact_optional" || kind === "optional";
1350
+ }
1351
+
1352
+ const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
1353
+
1354
+ function leafShape(schema) {
1355
+ if (schema.type === "picklist") return schema.options.join(" | ");
1356
+ if (schema.type === "literal") return `literal ${String(schema.literal)}`;
1357
+ if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
1358
+ 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`);
1359
+ return String(schema.type);
1360
+ }
1361
+
1362
+ function formatValidationError(label, issues) {
1363
+ const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
1364
+ return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
1365
+ }
1366
+
1367
+ function issuesFromValibot(issues) {
1368
+ return issues.map(issue => ({
1369
+ path: issue.path ? issue.path.map(item => item.key) : [],
1370
+ message: issue.message
1371
+ }));
1372
+ }
1373
+
1374
+ function formatIssuePath(path) {
1375
+ let out = "";
1376
+ for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
1377
+ return out;
1378
+ }
1379
+
1380
+ class PersistedDocShapeError extends WorkflowError {
1381
+ documentId;
1382
+ documentType;
1383
+ issues;
1384
+ constructor(args) {
1385
+ super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
1386
+ this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
1387
+ this.issues = args.issues;
1388
+ }
1389
+ }
1390
+
1391
+ function parsePersistedDoc(args) {
1392
+ const result = v__namespace.safeParse(args.schema, args.doc);
1393
+ if (!result.success) throw new PersistedDocShapeError({
1394
+ documentId: documentIdOf(args.doc),
1395
+ documentType: args.docType,
1396
+ issues: issuesFromValibot(result.issues)
1397
+ });
1398
+ return result.output;
1399
+ }
1400
+
1401
+ function documentIdOf(doc) {
1402
+ if (typeof doc == "object" && doc !== null) {
1403
+ const id = doc._id;
1404
+ if (typeof id == "string") return id;
1405
+ }
1406
+ return "(unknown id)";
1407
+ }
1408
+
1409
+ const ACTOR_KINDS = [ "person", "agent", "system" ];
1248
1410
 
1249
1411
  function releaseDocId(releaseName) {
1250
1412
  return `_.releases.${releaseName}`;
@@ -1291,29 +1453,28 @@ class FieldValueShapeError extends WorkflowError {
1291
1453
  }
1292
1454
  }
1293
1455
 
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")),
1456
+ const GdrUriSchema = v__namespace.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
1457
+ id: GdrUriSchema,
1458
+ type: NonEmptyString
1459
+ }), ReleaseRefShape = v__namespace.pipe(tolerantObject()({
1460
+ id: GdrUriSchema,
1299
1461
  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({
1462
+ releaseName: NonEmptyString
1463
+ }), 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
1464
  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({
1465
+ id: NonEmptyString,
1466
+ roles: v__namespace.exactOptional(v__namespace.array(v__namespace.string())),
1467
+ onBehalfOf: v__namespace.exactOptional(v__namespace.string())
1468
+ }), AssigneeShape = v__namespace.union([ tolerantObject()({
1307
1469
  type: v__namespace.literal("user"),
1308
- id: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1309
- }), v__namespace.looseObject({
1470
+ id: NonEmptyString
1471
+ }), tolerantObject()({
1310
1472
  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 = {
1473
+ role: NonEmptyString
1474
+ }) ]), 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, fieldValueSchemas = {
1313
1475
  "doc.ref": v__namespace.union([ v__namespace.null(), GdrShape ]),
1314
1476
  "doc.refs": v__namespace.array(GdrShape),
1315
1477
  "release.ref": v__namespace.union([ v__namespace.null(), ReleaseRefShape ]),
1316
- query: v__namespace.any(),
1317
1478
  string: NullableString,
1318
1479
  text: NullableString,
1319
1480
  number: NullableNumber,
@@ -1324,6 +1485,9 @@ const GdrShape = v__namespace.looseObject({
1324
1485
  actor: v__namespace.union([ v__namespace.null(), ActorShape ]),
1325
1486
  assignee: v__namespace.union([ v__namespace.null(), AssigneeShape ]),
1326
1487
  assignees: v__namespace.array(AssigneeShape)
1488
+ }, valueSchemas = {
1489
+ ...fieldValueSchemas,
1490
+ query: v__namespace.any()
1327
1491
  };
1328
1492
 
1329
1493
  function shapeValueSchema(shape, leaf) {
@@ -1410,7 +1574,7 @@ function isBareSeedId(id) {
1410
1574
 
1411
1575
  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
1576
  id: AuthoringRefId,
1413
- type: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1577
+ type: NonEmptyString
1414
1578
  }), seedValueSchemas = {
1415
1579
  ...valueSchemas,
1416
1580
  "doc.ref": v__namespace.union([ v__namespace.null(), AuthoringGdrShape ]),
@@ -1418,7 +1582,7 @@ const AuthoringRefId = v__namespace.pipe(v__namespace.string(), v__namespace.che
1418
1582
  "release.ref": v__namespace.union([ v__namespace.null(), v__namespace.looseObject({
1419
1583
  id: AuthoringRefId,
1420
1584
  type: v__namespace.literal("system.release"),
1421
- releaseName: v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1))
1585
+ releaseName: NonEmptyString
1422
1586
  }) ])
1423
1587
  };
1424
1588
 
@@ -1461,7 +1625,7 @@ function formatIssues(issues) {
1461
1625
  });
1462
1626
  }
1463
1627
 
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_]*$/;
1628
+ 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
1629
 
1466
1630
  function groqIdentifier(referencedAs) {
1467
1631
  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`));
@@ -1844,7 +2008,8 @@ const StoredStageSchema = pinned()(v__namespace.strictObject(stageFields({
1844
2008
  function startFields(kind) {
1845
2009
  return {
1846
2010
  kind: kind,
1847
- filter: v__namespace.optional(ConditionSchema)
2011
+ filter: v__namespace.optional(ConditionSchema),
2012
+ allowed: v__namespace.optional(ConditionSchema)
1848
2013
  };
1849
2014
  }
1850
2015
 
@@ -1902,24 +2067,6 @@ function isInputSourced(entry) {
1902
2067
  return entry.initialValue?.type === "input";
1903
2068
  }
1904
2069
 
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
2070
  function parseOrThrow({schema: schema, input: input, label: label}) {
1924
2071
  const result = v__namespace.safeParse(schema, input);
1925
2072
  if (!result.success) throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
@@ -1948,6 +2095,13 @@ function checkDuplicates({names: names, what: what, issues: issues}) {
1948
2095
  return seen;
1949
2096
  }
1950
2097
 
2098
+ function checkDefinitionName(def, issues) {
2099
+ LAKE_ID_SEGMENT_RE.test(def.name) || issues.push({
2100
+ path: [ "name" ],
2101
+ 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`
2102
+ });
2103
+ }
2104
+
1951
2105
  function checkStages(def, issues) {
1952
2106
  const stageNames = checkDuplicates({
1953
2107
  names: def.stages.map((stage, i) => ({
@@ -2084,6 +2238,10 @@ function checkGuardNames(def, issues) {
2084
2238
  what: "guard name (the guard lake _id derives from it — unique per definition)",
2085
2239
  issues: issues
2086
2240
  });
2241
+ for (const {name: name, path: path} of sites) LAKE_ID_SEGMENT_RE.test(name) || issues.push({
2242
+ path: path,
2243
+ 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`
2244
+ });
2087
2245
  }
2088
2246
 
2089
2247
  function fieldScopes(def) {
@@ -2154,7 +2312,11 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
2154
2312
  const reads = conditionParameterNames(groq2);
2155
2313
  for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
2156
2314
  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`
2315
+ message: `predicate "${name}" reads $${caller} — predicates pre-evaluate caller-free; compose caller gates at the condition site instead`
2316
+ });
2317
+ reads.has("row") && issues.push({
2318
+ path: [ "predicates", name ],
2319
+ 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
2320
  });
2159
2321
  for (const other of names) other === name || !reads.has(other) || issues.push({
2160
2322
  path: [ "predicates", name ],
@@ -2162,24 +2324,35 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
2162
2324
  });
2163
2325
  }
2164
2326
 
2165
- function entryNames(entries) {
2166
- return new Set((entries ?? []).map(entry => entry.name));
2327
+ function entryMap(entries) {
2328
+ return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
2167
2329
  }
2168
2330
 
2169
- function checkUnboundCallerVars(def, issues) {
2331
+ function checkUnboundConditionVars(def, issues) {
2170
2332
  for (const site of conditionSites(def)) {
2171
2333
  const reads = conditionParameterNames(site.groq);
2172
- for (const name of unboundCallerVars(site.policy)) reads.has(name) && issues.push({
2334
+ for (const name of unboundVarsAt(site)) reads.has(name) && issues.push({
2173
2335
  path: site.path,
2174
- message: callerVarMessage(site, name)
2336
+ message: name === "row" ? rowVarMessage(site) : callerVarMessage(site, name)
2175
2337
  });
2176
2338
  }
2177
2339
  }
2178
2340
 
2341
+ function unboundVarsAt(site) {
2342
+ const callerVars = unboundCallerVars(site.policy);
2343
+ return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
2344
+ }
2345
+
2179
2346
  function unboundCallerVars(policy) {
2180
2347
  return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
2181
2348
  }
2182
2349
 
2350
+ 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";
2351
+
2352
+ function rowVarMessage(site) {
2353
+ 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`;
2354
+ }
2355
+
2183
2356
  function callerVarMessage(site, name) {
2184
2357
  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
2358
  }
@@ -2187,7 +2360,7 @@ function callerVarMessage(site, name) {
2187
2360
  function conditionSites(def) {
2188
2361
  const sites = [], workflowLayer = {
2189
2362
  scope: "workflow",
2190
- names: entryNames(def.fields)
2363
+ entries: entryMap(def.fields)
2191
2364
  };
2192
2365
  collectEditableSites({
2193
2366
  entries: def.fields,
@@ -2208,10 +2381,10 @@ function conditionSites(def) {
2208
2381
  function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
2209
2382
  const stageLayer = {
2210
2383
  scope: "stage",
2211
- names: entryNames(stage.fields)
2384
+ entries: entryMap(stage.fields)
2212
2385
  }, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
2213
2386
  scope: "activity",
2214
- names: entryNames(activity.fields)
2387
+ entries: entryMap(activity.fields)
2215
2388
  })) ];
2216
2389
  for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
2217
2390
  groq: t.when,
@@ -2225,6 +2398,7 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
2225
2398
  path: [ "stages", i, "fields" ],
2226
2399
  labelPrefix: `stage "${stage.name}"`,
2227
2400
  fields: editableWindow,
2401
+ unionWindow: !0,
2228
2402
  sites: sites
2229
2403
  });
2230
2404
  for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
@@ -2232,23 +2406,28 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
2232
2406
  path: [ "stages", i, "editable", name ],
2233
2407
  label: `stage "${stage.name}" editable override "${name}"`,
2234
2408
  policy: "caller-bound",
2235
- fields: editableWindow
2409
+ fields: editableWindow,
2410
+ unionWindow: !0
2236
2411
  });
2237
2412
  for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
2238
2413
  activity: activity,
2239
2414
  path: [ "stages", i, "activities", j ],
2240
2415
  stageFields: stageFields2,
2416
+ workflowLayer: workflowLayer,
2241
2417
  sites: sites
2242
2418
  });
2243
2419
  }
2244
2420
 
2245
- function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
2421
+ function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, unionWindow: unionWindow, sites: sites}) {
2246
2422
  for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
2247
2423
  groq: entry.editable,
2248
2424
  path: [ ...path, n, "editable" ],
2249
2425
  label: `${labelPrefix} field "${entry.name}" editable`,
2250
2426
  policy: "caller-bound",
2251
- fields: fields
2427
+ fields: fields,
2428
+ ...unionWindow === !0 ? {
2429
+ unionWindow: !0
2430
+ } : {}
2252
2431
  });
2253
2432
  }
2254
2433
 
@@ -2260,14 +2439,15 @@ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy
2260
2439
  ...policy !== void 0 ? {
2261
2440
  policy: policy
2262
2441
  } : {},
2442
+ bindsRow: !0,
2263
2443
  fields: fields
2264
2444
  });
2265
2445
  }
2266
2446
 
2267
- function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
2447
+ function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, workflowLayer: workflowLayer, sites: sites}) {
2268
2448
  const fields = [ {
2269
2449
  scope: "activity",
2270
- names: entryNames(activity.fields)
2450
+ entries: entryMap(activity.fields)
2271
2451
  }, ...stageFields2 ];
2272
2452
  activity.filter !== void 0 && sites.push({
2273
2453
  groq: activity.filter,
@@ -2293,11 +2473,12 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
2293
2473
  activity: activity,
2294
2474
  path: path,
2295
2475
  fields: fields,
2476
+ workflowLayer: workflowLayer,
2296
2477
  sites: sites
2297
2478
  });
2298
2479
  }
2299
2480
 
2300
- function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
2481
+ function collectActionConditionSites({activity: activity, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2301
2482
  for (const [a, action] of (activity.actions ?? []).entries()) {
2302
2483
  const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
2303
2484
  action.when !== void 0 && sites.push({
@@ -2332,12 +2513,13 @@ function collectActionConditionSites({activity: activity, path: path, fields: fi
2332
2513
  action: action,
2333
2514
  path: actionPath,
2334
2515
  fields: fields,
2516
+ workflowLayer: workflowLayer,
2335
2517
  sites: sites
2336
2518
  });
2337
2519
  }
2338
2520
  }
2339
2521
 
2340
- function collectSpawnSites({action: action, path: path, fields: fields, sites: sites}) {
2522
+ function collectSpawnSites({action: action, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
2341
2523
  const spawn = action.spawn;
2342
2524
  if (spawn === void 0) return;
2343
2525
  const spawnPath = [ ...path, "spawn" ];
@@ -2346,13 +2528,17 @@ function collectSpawnSites({action: action, path: path, fields: fields, sites: s
2346
2528
  path: [ ...spawnPath, "forEach" ],
2347
2529
  label: `action "${action.name}".spawn.forEach`,
2348
2530
  policy: "cascade",
2349
- fields: fields
2531
+ fields: [ workflowLayer ],
2532
+ windowNote: "spawn.forEach evaluates against workflow-scope $fields only — stage/activity fields are never bound at discovery time"
2350
2533
  });
2351
- for (const [group, record] of [ [ "with", spawn.with ], [ "context", spawn.context ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
2534
+ 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
2535
  groq: groq2,
2353
2536
  path: [ ...spawnPath, group, key ],
2354
2537
  label: `action "${action.name}".spawn.${group} "${key}"`,
2355
2538
  policy: "triggered-payload",
2539
+ ...bindsRow ? {
2540
+ bindsRow: !0
2541
+ } : {},
2356
2542
  fields: fields
2357
2543
  });
2358
2544
  }
@@ -2426,7 +2612,7 @@ function checkStart(def, issues) {
2426
2612
  if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
2427
2613
  path: [ "start" ],
2428
2614
  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({
2615
+ }), 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({
2430
2616
  path: [ "fields", n, "required" ],
2431
2617
  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`
2432
2618
  });
@@ -2434,15 +2620,43 @@ function checkStart(def, issues) {
2434
2620
 
2435
2621
  function checkStartFilterReads(def, issues) {
2436
2622
  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({
2623
+ filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
2440
2624
  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({
2625
+ 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"
2626
+ }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
2444
2627
  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"
2628
+ 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"
2629
+ }));
2630
+ }
2631
+
2632
+ function checkStartAllowedReads(def, issues) {
2633
+ const allowed = def.start?.allowed;
2634
+ if (allowed === void 0) return;
2635
+ 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())}`;
2636
+ for (const read of conditionFieldReads(allowed)) {
2637
+ const entry = bindable.get(read.name);
2638
+ if (entry === void 0) {
2639
+ issues.push({
2640
+ path: [ "start", "allowed" ],
2641
+ 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}`
2642
+ });
2643
+ continue;
2644
+ }
2645
+ pushFieldReadPathIssue({
2646
+ where: "start.allowed",
2647
+ read: {
2648
+ field: read.name,
2649
+ path: read.path
2650
+ },
2651
+ target: entry,
2652
+ path: [ "start", "allowed" ],
2653
+ issues: issues,
2654
+ nodes: START_ALLOWED_VALUE_NODES
2655
+ });
2656
+ }
2657
+ readsRootDocument(allowed) && issues.push({
2658
+ path: [ "start", "allowed" ],
2659
+ 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
2660
  });
2447
2661
  }
2448
2662
 
@@ -2553,39 +2767,80 @@ function checkGroupMembership({group: group, reachable: reachable, path: path, l
2553
2767
  }
2554
2768
 
2555
2769
  function checkConditionFieldReads(def, issues) {
2556
- const everywhere = allDeclaredFieldNames(def);
2557
- for (const site of conditionSites(def)) for (const name of conditionFieldReadNames(site.groq)) fieldVisibleAtSite({
2770
+ const everywhere = allDeclaredFieldEntries(def);
2771
+ for (const site of conditionSites(def)) checkSiteFieldReads({
2558
2772
  site: site,
2559
- name: name,
2560
- everywhere: everywhere
2561
- }) || issues.push({
2562
- path: site.path,
2563
- message: undeclaredFieldReadMessage(site, name)
2773
+ everywhere: everywhere,
2774
+ issues: issues
2564
2775
  });
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)
2776
+ for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkSiteFieldReads({
2777
+ site: {
2778
+ groq: groq2,
2779
+ path: [ "predicates", name ],
2780
+ label: `predicate "${name}"`,
2781
+ fields: "context-dependent"
2782
+ },
2783
+ everywhere: everywhere,
2784
+ issues: issues
2568
2785
  });
2569
2786
  }
2570
2787
 
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;
2788
+ function checkSiteFieldReads({site: site, everywhere: everywhere, issues: issues}) {
2789
+ for (const read of conditionFieldReads(site.groq)) {
2790
+ const targets = readTargets({
2791
+ site: site,
2792
+ read: read,
2793
+ everywhere: everywhere
2794
+ });
2795
+ if (targets === "undeclared") {
2796
+ issues.push({
2797
+ path: site.path,
2798
+ message: undeclaredFieldReadMessage(site, read.name)
2799
+ });
2800
+ continue;
2801
+ }
2802
+ const path = read.path;
2803
+ path !== void 0 && (targets.some(target => fieldValuePathIssue({
2804
+ target: target,
2805
+ path: path
2806
+ }) === void 0) || pushFieldReadPathIssue({
2807
+ where: site.label,
2808
+ read: {
2809
+ field: read.name,
2810
+ path: path
2811
+ },
2812
+ target: targets[0],
2813
+ path: site.path,
2814
+ issues: issues
2815
+ }));
2816
+ }
2817
+ }
2818
+
2819
+ function readTargets({site: site, read: read, everywhere: everywhere}) {
2820
+ if (site.fields === "context-dependent") return everywhere.get(read.name) ?? "undeclared";
2821
+ const layers = site.fields.filter(layer => layer.entries.has(read.name));
2822
+ if (layers.length === 0) return "undeclared";
2823
+ const entries = layers.map(layer => layer.entries.get(read.name));
2824
+ return site.unionWindow === !0 ? entries : [ entries[0] ];
2575
2825
  }
2576
2826
 
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));
2827
+ function allDeclaredFieldEntries(def) {
2828
+ const declared = /* @__PURE__ */ new Map;
2829
+ for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) {
2830
+ const list = declared.get(entry.name);
2831
+ list === void 0 ? declared.set(entry.name, [ entry ]) : list.push(entry);
2832
+ }
2833
+ return declared;
2579
2834
  }
2580
2835
 
2581
2836
  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`;
2837
+ 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
2838
  }
2584
2839
 
2585
2840
  function undeclaredFieldReadMessage(site, name) {
2586
2841
  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)"}`;
2842
+ 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})`;
2843
+ return `${site.label} reads $fields.${name}, but ${window} — the read evaluates to GROQ null, so the condition silently misevaluates. Visible: ${visible.join(", ") || "(none)"}`;
2589
2844
  }
2590
2845
 
2591
2846
  function checkFieldReadSeeds(def, issues) {
@@ -2687,7 +2942,7 @@ function checkFieldReadOpValues(def, issues) {
2687
2942
  ops: site.ops,
2688
2943
  path: site.path,
2689
2944
  label: site.label,
2690
- activityNames: entryNames(site.activity.fields)
2945
+ activityEntries: entryMap(site.activity.fields)
2691
2946
  });
2692
2947
  }
2693
2948
 
@@ -2707,7 +2962,7 @@ function fieldReadsIn(value, path) {
2707
2962
  } ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
2708
2963
  }
2709
2964
 
2710
- function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityNames: activityNames, issues: issues}) {
2965
+ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
2711
2966
  const hosts = [ {
2712
2967
  scope: "stage",
2713
2968
  entries: stageEntries
@@ -2722,7 +2977,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2722
2977
  read: read,
2723
2978
  where: where,
2724
2979
  hosts: hosts,
2725
- activityNames: activityNames
2980
+ activityEntries: activityEntries
2726
2981
  })
2727
2982
  });
2728
2983
  return;
@@ -2736,8 +2991,8 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
2736
2991
  });
2737
2992
  }
2738
2993
 
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}`));
2994
+ function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityEntries: activityEntries}) {
2995
+ 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}`));
2741
2996
  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)"}`;
2742
2997
  }
2743
2998
 
@@ -2845,11 +3100,14 @@ function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, pa
2845
3100
  });
2846
3101
  }
2847
3102
 
2848
- function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
3103
+ function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues, nodes: nodes}) {
2849
3104
  if (read.path === void 0) return;
2850
3105
  const pathIssue = fieldValuePathIssue({
2851
3106
  target: target,
2852
- path: read.path
3107
+ path: read.path,
3108
+ ...nodes ? {
3109
+ nodes: nodes
3110
+ } : {}
2853
3111
  });
2854
3112
  pathIssue !== void 0 && issues.push({
2855
3113
  path: path,
@@ -2920,22 +3178,26 @@ const SCALAR = {
2920
3178
  kind: "rows",
2921
3179
  of: shape.of ?? []
2922
3180
  })
3181
+ }, START_ALLOWED_VALUE_NODES = {
3182
+ ...VALUE_NODES,
3183
+ "doc.ref": () => GDR_VALUE
2923
3184
  };
2924
3185
 
2925
- function valueNodeFor(shape) {
2926
- return Object.hasOwn(VALUE_NODES, shape.type) ? VALUE_NODES[shape.type](shape) : void 0;
3186
+ function valueNodeFor(shape, nodes) {
3187
+ return Object.hasOwn(nodes, shape.type) ? nodes[shape.type](shape) : void 0;
2927
3188
  }
2928
3189
 
2929
3190
  const INDEX_SEGMENT = /^\d+$/;
2930
3191
 
2931
- function stepIntoValueNode(node, segment) {
3192
+ function stepIntoValueNode(args) {
3193
+ const {node: node, segment: segment, nodes: nodes} = args;
2932
3194
  switch (node.kind) {
2933
3195
  case "scalar":
2934
3196
  return "the value is a scalar with no sub-paths";
2935
3197
 
2936
3198
  case "object":
2937
3199
  {
2938
- const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
3200
+ const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub, nodes);
2939
3201
  return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
2940
3202
  }
2941
3203
 
@@ -2956,18 +3218,24 @@ function stepIntoValueNode(node, segment) {
2956
3218
  }
2957
3219
  }
2958
3220
 
2959
- function fieldValuePathIssue({target: target, path: path}) {
2960
- let node = valueNodeFor(target);
3221
+ function fieldValuePathIssue({target: target, path: path, nodes: nodes = VALUE_NODES}) {
3222
+ let node = valueNodeFor(target, nodes);
2961
3223
  if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
2962
3224
  for (const segment of path.split(".")) {
2963
- const next = stepIntoValueNode(node, segment);
3225
+ const next = stepIntoValueNode({
3226
+ node: node,
3227
+ segment: segment,
3228
+ nodes: nodes
3229
+ });
2964
3230
  if (typeof next == "string") return `at segment "${segment}": ${next}`;
2965
3231
  node = next;
2966
3232
  }
2967
3233
  }
2968
3234
 
2969
3235
  function checkWorkflowInvariants(def) {
2970
- const issues = [], stageNames = checkStages(def, issues);
3236
+ const issues = [];
3237
+ checkDefinitionName(def, issues);
3238
+ const stageNames = checkStages(def, issues);
2971
3239
  return checkInitialStage({
2972
3240
  def: def,
2973
3241
  stageNames: stageNames,
@@ -2982,7 +3250,7 @@ function checkWorkflowInvariants(def) {
2982
3250
  issues: issues
2983
3251
  }), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
2984
3252
  checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
2985
- checkUnboundCallerVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3253
+ checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
2986
3254
  checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
2987
3255
  checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
2988
3256
  checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
@@ -2991,8 +3259,12 @@ function checkWorkflowInvariants(def) {
2991
3259
 
2992
3260
  exports.ACTIVITY_KINDS = ACTIVITY_KINDS;
2993
3261
 
3262
+ exports.ACTIVITY_STATUSES = ACTIVITY_STATUSES;
3263
+
2994
3264
  exports.ACTOR_KINDS = ACTOR_KINDS;
2995
3265
 
3266
+ exports.ActorShape = ActorShape;
3267
+
2996
3268
  exports.AuthoringActionSchema = AuthoringActionSchema;
2997
3269
 
2998
3270
  exports.AuthoringActivitySchema = AuthoringActivitySchema;
@@ -3035,6 +3307,10 @@ exports.EffectSchema = EffectSchema;
3035
3307
 
3036
3308
  exports.FIELD_READ = FIELD_READ;
3037
3309
 
3310
+ exports.FIELD_SCOPES = FIELD_SCOPES;
3311
+
3312
+ exports.FIELD_VALUE_KINDS = FIELD_VALUE_KINDS;
3313
+
3038
3314
  exports.FILTER_SCOPE_VARS = FILTER_SCOPE_VARS;
3039
3315
 
3040
3316
  exports.FieldValueShapeError = FieldValueShapeError;
@@ -3043,14 +3319,26 @@ exports.GROUP_KINDS = GROUP_KINDS;
3043
3319
 
3044
3320
  exports.GUARD_PREDICATE_VARS = GUARD_PREDICATE_VARS;
3045
3321
 
3322
+ exports.GdrShape = GdrShape;
3323
+
3046
3324
  exports.GroupSchema = GroupSchema;
3047
3325
 
3048
3326
  exports.InstanceNotFoundError = InstanceNotFoundError;
3049
3327
 
3328
+ exports.IsoTimestamp = IsoTimestamp;
3329
+
3330
+ exports.MUTATION_GUARD_ACTIONS = MUTATION_GUARD_ACTIONS;
3331
+
3332
+ exports.NonEmptyString = NonEmptyString;
3333
+
3334
+ exports.PersistedDocShapeError = PersistedDocShapeError;
3335
+
3050
3336
  exports.RESERVED_CONDITION_VARS = RESERVED_CONDITION_VARS;
3051
3337
 
3052
3338
  exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
3053
3339
 
3340
+ exports.START_ALLOWED_VARS = START_ALLOWED_VARS;
3341
+
3054
3342
  exports.START_FILTER_VARS = START_FILTER_VARS;
3055
3343
 
3056
3344
  exports.StoredFieldOpSchema = StoredFieldOpSchema;
@@ -3101,6 +3389,8 @@ exports.evaluatePredicates = evaluatePredicates;
3101
3389
 
3102
3390
  exports.extractDocumentId = extractDocumentId;
3103
3391
 
3392
+ exports.fieldValueSchemas = fieldValueSchemas;
3393
+
3104
3394
  exports.formatIssuePath = formatIssuePath;
3105
3395
 
3106
3396
  exports.formatIssues = formatIssues;
@@ -3133,6 +3423,8 @@ exports.isInputSourced = isInputSourced;
3133
3423
 
3134
3424
  exports.isNotesEntry = isNotesEntry;
3135
3425
 
3426
+ exports.isParseableInstant = isParseableInstant;
3427
+
3136
3428
  exports.isStartableDefinition = isStartableDefinition;
3137
3429
 
3138
3430
  exports.isSubjectEntry = isSubjectEntry;
@@ -3151,6 +3443,8 @@ exports.parseGdr = parseGdr;
3151
3443
 
3152
3444
  exports.parseOrThrow = parseOrThrow;
3153
3445
 
3446
+ exports.parsePersistedDoc = parsePersistedDoc;
3447
+
3154
3448
  exports.parseResourceGdr = parseResourceGdr;
3155
3449
 
3156
3450
  exports.parseStoredDefinition = parseStoredDefinition;
@@ -3187,6 +3481,8 @@ exports.runGroq = runGroq;
3187
3481
 
3188
3482
  exports.sameResource = sameResource;
3189
3483
 
3484
+ exports.schemaTreeShape = schemaTreeShape;
3485
+
3190
3486
  exports.selfGdr = selfGdr;
3191
3487
 
3192
3488
  exports.startKindOf = startKindOf;
@@ -3197,6 +3493,10 @@ exports.toBareId = toBareId;
3197
3493
 
3198
3494
  exports.toPhysicalGdr = toPhysicalGdr;
3199
3495
 
3496
+ exports.tolerantEntries = tolerantEntries;
3497
+
3498
+ exports.tolerantObject = tolerantObject;
3499
+
3200
3500
  exports.tryParseGdr = tryParseGdr;
3201
3501
 
3202
3502
  exports.validateFieldAppendItem = validateFieldAppendItem;