@sanity/workflow-engine 0.28.0 → 0.30.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.
@@ -41,79 +41,6 @@ function andConditions(parts) {
41
41
  if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
42
42
  }
43
43
 
44
- class WorkflowError extends Error {
45
- kind;
46
- constructor(kind, message, options) {
47
- super(message, options), this.kind = kind;
48
- }
49
- }
50
-
51
- class ContractViolationError extends WorkflowError {
52
- constructor(message) {
53
- super("contract-violation", message), this.name = "ContractViolationError";
54
- }
55
- }
56
-
57
- class InstanceNotFoundError extends WorkflowError {
58
- instanceId;
59
- constructor(args) {
60
- super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
61
- this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
62
- }
63
- }
64
-
65
- class DefinitionNotFoundError extends WorkflowError {
66
- definition;
67
- version;
68
- constructor(args) {
69
- super("definition-not-found", args.version !== void 0 ? `Workflow definition ${args.definition} v${args.version} not deployed` : `Workflow definition ${args.definition} has no deployed versions`),
70
- this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
71
- }
72
- }
73
-
74
- class SpawnContractsInvalidError extends WorkflowError {
75
- issues;
76
- constructor(args) {
77
- super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
78
- this.issues = args.issues;
79
- }
80
- }
81
-
82
- class DefinitionInUseError extends WorkflowError {
83
- definition;
84
- blockedBy;
85
- constructor(args) {
86
- super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
87
- this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
88
- }
89
- }
90
-
91
- function definitionInUseMessage(definition, blockedBy) {
92
- if (blockedBy.reason === "non-terminal-instances") {
93
- const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
94
- return `Cannot delete ${definition}: ${blockedBy.instanceIds.length} non-terminal instance(s) exist (${preview}). Pass cascade to abort them first — instances are aborted in place, never deleted.`;
95
- }
96
- const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
97
- return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
98
- }
99
-
100
- class EffectNotFoundError extends WorkflowError {
101
- instanceId;
102
- effectKey;
103
- settled;
104
- constructor(args) {
105
- super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
106
- this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
107
- }
108
- }
109
-
110
- function effectNotFoundMessage(args) {
111
- const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
112
- if (args.settled === void 0) return base;
113
- const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
114
- 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}`;
115
- }
116
-
117
44
  const KNOWN_SCHEMES = /* @__PURE__ */ new Set([ "dataset", "canvas", "media-library", "dashboard" ]), KNOWN_SCHEMES_TEXT = [ ...KNOWN_SCHEMES ].join(", ");
118
45
 
119
46
  class VersionSpecificDatasetGdrError extends Error {
@@ -329,6 +256,79 @@ function isGdr(value) {
329
256
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
330
257
  }
331
258
 
259
+ class WorkflowError extends Error {
260
+ kind;
261
+ constructor(kind, message, options) {
262
+ super(message, options), this.kind = kind;
263
+ }
264
+ }
265
+
266
+ class ContractViolationError extends WorkflowError {
267
+ constructor(message) {
268
+ super("contract-violation", message), this.name = "ContractViolationError";
269
+ }
270
+ }
271
+
272
+ class InstanceNotFoundError extends WorkflowError {
273
+ instanceId;
274
+ constructor(args) {
275
+ super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
276
+ this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
277
+ }
278
+ }
279
+
280
+ class DefinitionNotFoundError extends WorkflowError {
281
+ definition;
282
+ version;
283
+ constructor(args) {
284
+ super("definition-not-found", args.version !== void 0 ? `Workflow definition ${args.definition} v${args.version} not deployed` : `Workflow definition ${args.definition} has no deployed versions`),
285
+ this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
286
+ }
287
+ }
288
+
289
+ class SpawnContractsInvalidError extends WorkflowError {
290
+ issues;
291
+ constructor(args) {
292
+ super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
293
+ this.issues = args.issues;
294
+ }
295
+ }
296
+
297
+ class DefinitionInUseError extends WorkflowError {
298
+ definition;
299
+ blockedBy;
300
+ constructor(args) {
301
+ super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
302
+ this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
303
+ }
304
+ }
305
+
306
+ function definitionInUseMessage(definition, blockedBy) {
307
+ if (blockedBy.reason === "non-terminal-instances") {
308
+ const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
309
+ return `Cannot delete ${definition}: ${blockedBy.instanceIds.length} non-terminal instance(s) exist (${preview}). Pass cascade to abort them first — instances are aborted in place, never deleted.`;
310
+ }
311
+ const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
312
+ return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
313
+ }
314
+
315
+ class EffectNotFoundError extends WorkflowError {
316
+ instanceId;
317
+ effectKey;
318
+ settled;
319
+ constructor(args) {
320
+ super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
321
+ this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
322
+ }
323
+ }
324
+
325
+ function effectNotFoundMessage(args) {
326
+ const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
327
+ if (args.settled === void 0) return base;
328
+ const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
329
+ 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}`;
330
+ }
331
+
332
332
  const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
333
333
 
334
334
  function validateTag(tag) {
@@ -525,6 +525,7 @@ function desugarWorkflow(authoring) {
525
525
  return {
526
526
  ...stripUndefined({
527
527
  name: stage.name,
528
+ semantics: stage.semantics,
528
529
  title: stage.title,
529
530
  description: stage.description,
530
531
  groups: stage.groups,
@@ -548,6 +549,7 @@ function desugarWorkflow(authoring) {
548
549
  definition: {
549
550
  ...stripUndefined({
550
551
  name: authoring.name,
552
+ semantics: authoring.semantics,
551
553
  title: authoring.title,
552
554
  description: authoring.description,
553
555
  groups: authoring.groups,
@@ -652,6 +654,7 @@ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
652
654
  options: entry.options,
653
655
  validation: entry.validation,
654
656
  types: entry.types,
657
+ roles: entry.roles,
655
658
  fields: entry.fields,
656
659
  of: entry.of
657
660
  })
@@ -774,6 +777,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
774
777
  return {
775
778
  ...stripUndefined({
776
779
  name: activity.name,
780
+ semantics: activity.semantics,
777
781
  title: activity.title,
778
782
  description: activity.description,
779
783
  groups: activity.groups,
@@ -1245,7 +1249,66 @@ function isTerminalActivityStatus(status) {
1245
1249
  return TERMINAL_ACTIVITY_STATUSES.includes(status);
1246
1250
  }
1247
1251
 
1248
- const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], CONDITION_VARS = [ {
1252
+ const SIGNAL_SEMANTICS = [ "signal.positive", "signal.caution", "signal.critical" ], DECISION_SEMANTICS = [ "decision.accept", "decision.decline" ], ACTION_SEMANTICS = [ ...DECISION_SEMANTICS, ...SIGNAL_SEMANTICS ], FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ];
1253
+
1254
+ function releaseDocId(releaseName) {
1255
+ return `_.releases.${releaseName}`;
1256
+ }
1257
+
1258
+ function releaseRef({res: res, releaseName: releaseName}) {
1259
+ if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
1260
+ return {
1261
+ id: gdrFromResource(res, releaseDocId(releaseName)),
1262
+ type: "system.release",
1263
+ releaseName: releaseName
1264
+ };
1265
+ }
1266
+
1267
+ function isAlwaysArrayFieldKind(kind) {
1268
+ return kind === "doc.refs" || kind === "assignees" || kind === "array";
1269
+ }
1270
+
1271
+ function isSingleDocRefKind(kind) {
1272
+ return kind === "doc.ref" || kind === "subject";
1273
+ }
1274
+
1275
+ function refKindAcceptsTypes(kind) {
1276
+ return isSingleDocRefKind(kind) || kind === "doc.refs";
1277
+ }
1278
+
1279
+ function assignmentKindAcceptsRoles(kind) {
1280
+ return kind === "assignee" || kind === "assignees";
1281
+ }
1282
+
1283
+ function isSingleDocRefEntry(entry) {
1284
+ return isSingleDocRefKind(entry._type);
1285
+ }
1286
+
1287
+ function isAssignmentFieldEntry(entry) {
1288
+ return assignmentKindAcceptsRoles(entry._type);
1289
+ }
1290
+
1291
+ function isTodoListItem(row) {
1292
+ if (typeof row != "object" || row === null) return !1;
1293
+ const candidate = row, status = candidate.status;
1294
+ return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
1295
+ }
1296
+
1297
+ function declaredRowColumns(entry) {
1298
+ if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
1299
+ }
1300
+
1301
+ function isTodoListEntry(entry) {
1302
+ const columns = declaredRowColumns(entry);
1303
+ return columns !== void 0 && columns.has("label") && columns.has("status");
1304
+ }
1305
+
1306
+ function isNotesEntry(entry) {
1307
+ const columns = declaredRowColumns(entry);
1308
+ return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
1309
+ }
1310
+
1311
+ const CONDITION_VARS = [ {
1249
1312
  name: "self",
1250
1313
  binding: "always",
1251
1314
  label: "this workflow instance",
@@ -1320,6 +1383,11 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
1320
1383
  binding: "caller",
1321
1384
  label: "your permissions",
1322
1385
  description: "Advisory per-permission booleans computed from the caller's grants; `undefined` without grants. Bound wherever grants ride the evaluation: the projection's rendered scope (fireAction-action filters, activity requirements, editability predicates) and the fireAction/editField commit gates. Deploy rejects it at every site that evaluates without grants: transition `when`s, activity filters, cascade-fired actions' `when`/`filter`, effect bindings, where-op `where`s, and the spawn `forEach`/`with`/`context` sites."
1386
+ }, {
1387
+ name: "attributes",
1388
+ binding: "caller",
1389
+ label: "your attributes",
1390
+ description: "Advisory org-level User Attributes for the caller (Enterprise — same values as lake `user::attributes()`), keyed by attribute name with each active scalar or array value. `undefined` on expected HTTP absence; unexpected fetch failures throw; empty page binds `{}`. Bound wherever grants ride the evaluation (same sites as `$can`). Soft-gate paths fetch at most 100 attributes (no further pages) and warn when the envelope reports `hasMore: true` (partial bag still binds). Not a security boundary — the Content Lake remains the only enforcement point."
1323
1391
  }, {
1324
1392
  name: "row",
1325
1393
  binding: "spawn",
@@ -1485,54 +1553,7 @@ function documentIdOf(doc) {
1485
1553
  return "(unknown id)";
1486
1554
  }
1487
1555
 
1488
- const ACTOR_KINDS = [ "person", "agent", "system" ];
1489
-
1490
- function releaseDocId(releaseName) {
1491
- return `_.releases.${releaseName}`;
1492
- }
1493
-
1494
- function releaseRef({res: res, releaseName: releaseName}) {
1495
- if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
1496
- return {
1497
- id: gdrFromResource(res, releaseDocId(releaseName)),
1498
- type: "system.release",
1499
- releaseName: releaseName
1500
- };
1501
- }
1502
-
1503
- function isSingleDocRefKind(kind) {
1504
- return kind === "doc.ref" || kind === "subject";
1505
- }
1506
-
1507
- function refKindAcceptsTypes(kind) {
1508
- return isSingleDocRefKind(kind) || kind === "doc.refs";
1509
- }
1510
-
1511
- function isSingleDocRefEntry(entry) {
1512
- return isSingleDocRefKind(entry._type);
1513
- }
1514
-
1515
- function isTodoListItem(row) {
1516
- if (typeof row != "object" || row === null) return !1;
1517
- const candidate = row, status = candidate.status;
1518
- return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
1519
- }
1520
-
1521
- function declaredRowColumns(entry) {
1522
- if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
1523
- }
1524
-
1525
- function isTodoListEntry(entry) {
1526
- const columns = declaredRowColumns(entry);
1527
- return columns !== void 0 && columns.has("label") && columns.has("status");
1528
- }
1529
-
1530
- function isNotesEntry(entry) {
1531
- const columns = declaredRowColumns(entry);
1532
- return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
1533
- }
1534
-
1535
- const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
1556
+ const ACTOR_KINDS = [ "person", "agent", "system" ], ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
1536
1557
 
1537
1558
  function classifyPrincipalId(id) {
1538
1559
  if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
@@ -1575,6 +1596,10 @@ function lakePrincipalId(args) {
1575
1596
  return args.localPrincipalId ?? args.actor.id;
1576
1597
  }
1577
1598
 
1599
+ function isRecord(value) {
1600
+ return typeof value == "object" && value !== null && !Array.isArray(value);
1601
+ }
1602
+
1578
1603
  class FieldValueShapeError extends WorkflowError {
1579
1604
  entryType;
1580
1605
  entryName;
@@ -1671,9 +1696,16 @@ const fieldValueSchemas = {
1671
1696
  query: v.any()
1672
1697
  };
1673
1698
 
1674
- function shapeValueSchema(shape, leaf) {
1675
- if (shape.type === "object") return objectSchema(shape.fields ?? [], leaf);
1676
- if (shape.type === "array") return v.array(objectSchema(shape.of ?? [], leaf));
1699
+ function shapeValueSchema(args) {
1700
+ const {shape: shape, leaf: leaf} = args;
1701
+ if (shape.type === "object") return objectSchema({
1702
+ fields: shape.fields ?? [],
1703
+ leaf: leaf
1704
+ });
1705
+ if (shape.type === "array") return v.array(objectSchema({
1706
+ fields: shape.of ?? [],
1707
+ leaf: leaf
1708
+ }));
1677
1709
  const schema = leaf[shape.type] ?? v.any();
1678
1710
  return constrainedScalarSchema({
1679
1711
  schema: schema,
@@ -1724,19 +1756,31 @@ function constrainedScalarSchema(args) {
1724
1756
  }) ?? [] ].join("; ")));
1725
1757
  }
1726
1758
 
1727
- function objectSchema(fields, leaf) {
1728
- const entries = /* @__PURE__ */ Object.create(null);
1729
- for (const f of fields) entries[f.name] = v.optional(shapeValueSchema(f, leaf));
1759
+ function objectSchema(args) {
1760
+ const {fields: fields, leaf: leaf} = args, entries = /* @__PURE__ */ Object.create(null);
1761
+ for (const f of fields) entries[f.name] = v.optional(shapeValueSchema({
1762
+ shape: f,
1763
+ leaf: leaf
1764
+ }));
1730
1765
  return v.looseObject(entries);
1731
1766
  }
1732
1767
 
1733
1768
  function wholeValueSchema(args) {
1734
1769
  const {entryType: entryType, shape: shape, leaf: leaf} = args;
1735
- return entryType === "object" ? v.union([ v.null(), objectSchema(shape.fields ?? [], leaf) ]) : entryType === "array" ? v.array(objectSchema(shape.of ?? [], leaf)) : leaf[entryType];
1770
+ return entryType === "object" ? v.union([ v.null(), objectSchema({
1771
+ fields: shape.fields ?? [],
1772
+ leaf: leaf
1773
+ }) ]) : entryType === "array" ? v.array(objectSchema({
1774
+ fields: shape.of ?? [],
1775
+ leaf: leaf
1776
+ })) : leaf[entryType];
1736
1777
  }
1737
1778
 
1738
1779
  function appendItemSchema(entryType, shape) {
1739
- if (entryType === "array") return objectSchema(shape.of ?? [], valueSchemas);
1780
+ if (entryType === "array") return objectSchema({
1781
+ fields: shape.of ?? [],
1782
+ leaf: valueSchemas
1783
+ });
1740
1784
  if (entryType === "doc.refs") return GdrShape;
1741
1785
  if (entryType === "assignees") return AssigneeWriteShape;
1742
1786
  }
@@ -1788,7 +1832,7 @@ function checkValueAgainst(args, leaf) {
1788
1832
  entryType: args.entryType,
1789
1833
  validation: args.validation,
1790
1834
  value: args.value
1791
- });
1835
+ }) ?? assignmentValueIssues(args, result.output);
1792
1836
  return postIssues !== void 0 ? {
1793
1837
  issues: postIssues
1794
1838
  } : {
@@ -1796,6 +1840,142 @@ function checkValueAgainst(args, leaf) {
1796
1840
  };
1797
1841
  }
1798
1842
 
1843
+ function assignmentIdentity(assignee) {
1844
+ return assignee.type === "user" ? `user:${assignee.id}` : `role:${assignee.role}`;
1845
+ }
1846
+
1847
+ function assignmentCandidate(value) {
1848
+ const parsed = v.safeParse(AssigneeShape, value);
1849
+ return parsed.success ? parsed.output : void 0;
1850
+ }
1851
+
1852
+ function assignmentValueIssues(args, value) {
1853
+ const retained = previousAssignmentIdentities(args), issues = [];
1854
+ return visitAssignmentEntries({
1855
+ entryType: args.entryType,
1856
+ value: value,
1857
+ roles: args.roles,
1858
+ fields: args.fields,
1859
+ of: args.of,
1860
+ path: "$"
1861
+ }, ({assignee: assignee, roles: roles, path: path}) => {
1862
+ consumeRetainedAssignment({
1863
+ retained: retained,
1864
+ path: path,
1865
+ identity: assignmentIdentity(assignee)
1866
+ }) || issues.push(...assignmentCandidateIssues({
1867
+ roles: roles,
1868
+ memberRoles: args.memberRoles,
1869
+ roleAliases: args.roleAliases
1870
+ }, assignee));
1871
+ }), issues.length === 0 ? void 0 : issues;
1872
+ }
1873
+
1874
+ function consumeRetainedAssignment(args) {
1875
+ const identities = args.retained.get(args.path), remaining = identities?.get(args.identity) ?? 0;
1876
+ return remaining === 0 || identities === void 0 ? !1 : (remaining === 1 ? identities.delete(args.identity) : identities.set(args.identity, remaining - 1),
1877
+ !0);
1878
+ }
1879
+
1880
+ function previousAssignmentIdentities(args) {
1881
+ const retained = /* @__PURE__ */ new Map;
1882
+ if (args.previousValue === void 0 || args.entryType === "assignee") return retained;
1883
+ const schema = wholeValueSchema({
1884
+ entryType: args.entryType,
1885
+ shape: args,
1886
+ leaf: valueSchemas
1887
+ });
1888
+ if (schema === void 0) return retained;
1889
+ const previous = v.safeParse(schema, args.previousValue);
1890
+ return previous.success && visitAssignmentEntries({
1891
+ entryType: args.entryType,
1892
+ value: previous.output,
1893
+ roles: args.roles,
1894
+ fields: args.fields,
1895
+ of: args.of,
1896
+ path: "$"
1897
+ }, ({assignee: assignee, path: path}) => {
1898
+ const identities = retained.get(path) ?? /* @__PURE__ */ new Map, identity = assignmentIdentity(assignee);
1899
+ identities.set(identity, (identities.get(identity) ?? 0) + 1), retained.set(path, identities);
1900
+ }), retained;
1901
+ }
1902
+
1903
+ function visitAssignmentEntries(args, visit) {
1904
+ if (assignmentKindAcceptsRoles(args.entryType)) {
1905
+ visitAssignmentCandidates(args, visit);
1906
+ return;
1907
+ }
1908
+ if (args.entryType === "object") {
1909
+ visitAssignmentFields({
1910
+ fields: args.fields,
1911
+ value: args.value,
1912
+ path: args.path
1913
+ }, visit);
1914
+ return;
1915
+ }
1916
+ if (!(args.entryType !== "array" || !Array.isArray(args.value))) for (const row of args.value) visitAssignmentFields({
1917
+ fields: args.of,
1918
+ value: row,
1919
+ path: `${args.path}[]`
1920
+ }, visit);
1921
+ }
1922
+
1923
+ function visitAssignmentCandidates(args, visit) {
1924
+ if (args.roles === void 0) return;
1925
+ const indexedValues = args.entryType === "assignees" && Array.isArray(args.value) ? args.value.map(value => ({
1926
+ value: value,
1927
+ path: args.path
1928
+ })) : [ {
1929
+ value: args.value,
1930
+ path: args.path
1931
+ } ];
1932
+ for (const {value: value, path: path} of indexedValues) {
1933
+ const assignee = assignmentCandidate(value);
1934
+ assignee !== void 0 && visit({
1935
+ assignee: assignee,
1936
+ roles: args.roles,
1937
+ path: path
1938
+ });
1939
+ }
1940
+ }
1941
+
1942
+ function visitAssignmentFields(args, visit) {
1943
+ if (!(args.fields === void 0 || !isRecord(args.value))) for (const field of args.fields) visitAssignmentShape({
1944
+ shape: field,
1945
+ value: args.value[field.name],
1946
+ path: `${args.path}.${field.name}`
1947
+ }, visit);
1948
+ }
1949
+
1950
+ function visitAssignmentShape(args, visit) {
1951
+ visitAssignmentEntries({
1952
+ entryType: args.shape.type,
1953
+ value: args.value,
1954
+ roles: args.shape.roles,
1955
+ fields: args.shape.fields,
1956
+ of: args.shape.of,
1957
+ path: args.path
1958
+ }, visit);
1959
+ }
1960
+
1961
+ function assignmentCandidateIssues(args, assignee) {
1962
+ if (assignee.type === "role") return collectiveRoleIssues(args, assignee.role);
1963
+ const held = args.memberRoles?.[assignee.id];
1964
+ return held === void 0 ? args.memberRoles === void 0 ? [] : [ `user "${assignee.id}" has no available project membership for role eligibility` ] : args.roles.some(required => actorFulfillsRole({
1965
+ actorRoles: held,
1966
+ required: required,
1967
+ aliases: args.roleAliases
1968
+ })) ? [] : [ `user "${assignee.id}" does not fulfill any eligible role: ${formatEligibleRoles(args.roles)}` ];
1969
+ }
1970
+
1971
+ function collectiveRoleIssues(args, role) {
1972
+ return args.roles.includes(role) ? [] : [ `collective role "${role}" is not eligible — this entry accepts ${formatEligibleRoles(args.roles)}` ];
1973
+ }
1974
+
1975
+ function formatEligibleRoles(roles) {
1976
+ return roles.map(role => `"${role}"`).join(", ");
1977
+ }
1978
+
1799
1979
  function issuesOf(check) {
1800
1980
  return "issues" in check ? check.issues : void 0;
1801
1981
  }
@@ -1855,6 +2035,20 @@ function validateFieldAppendItem(args) {
1855
2035
  issues: formatIssues(result.issues),
1856
2036
  mode: "item"
1857
2037
  });
2038
+ const assignmentArgs = {
2039
+ entryType: appendAssignmentEntryType(args.entryType),
2040
+ value: result.output,
2041
+ roles: args.roles,
2042
+ fields: args.entryType === "array" ? args.of : void 0,
2043
+ memberRoles: args.memberRoles,
2044
+ roleAliases: args.roleAliases
2045
+ }, assignmentIssues = assignmentValueIssues(assignmentArgs, result.output);
2046
+ if (assignmentIssues !== void 0) throw new FieldValueShapeError({
2047
+ entryType: args.entryType,
2048
+ entryName: args.entryName,
2049
+ issues: assignmentIssues,
2050
+ mode: "item"
2051
+ });
1858
2052
  const typeIssues = refTypeIssues({
1859
2053
  entryType: args.entryType,
1860
2054
  types: args.types,
@@ -1869,6 +2063,10 @@ function validateFieldAppendItem(args) {
1869
2063
  return result.output;
1870
2064
  }
1871
2065
 
2066
+ function appendAssignmentEntryType(entryType) {
2067
+ return entryType === "assignees" ? "assignee" : entryType === "array" ? "object" : entryType;
2068
+ }
2069
+
1872
2070
  function formatIssues(issues, formatMessage = issue => issue.message) {
1873
2071
  const formatOne = (issue, prefix) => {
1874
2072
  const keys = [ ...prefix, ...issue.path?.map(p => p.key) ?? [] ], sub = issue.issues?.filter(candidate => candidate.expected !== "null");
@@ -1961,6 +2159,10 @@ function opSchemas(targetSchema) {
1961
2159
  type: v.literal("field.set"),
1962
2160
  target: targetSchema,
1963
2161
  value: ValueExprSchema
2162
+ }), v.strictObject({
2163
+ type: v.literal("field.setIfMissing"),
2164
+ target: targetSchema,
2165
+ value: ValueExprSchema
1964
2166
  }), v.strictObject({
1965
2167
  type: v.literal("field.unset"),
1966
2168
  target: targetSchema
@@ -1968,6 +2170,14 @@ function opSchemas(targetSchema) {
1968
2170
  type: v.literal("field.append"),
1969
2171
  target: targetSchema,
1970
2172
  value: ValueExprSchema
2173
+ }), v.strictObject({
2174
+ type: v.literal("field.inc"),
2175
+ target: targetSchema,
2176
+ value: v.optional(ValueExprSchema)
2177
+ }), v.strictObject({
2178
+ type: v.literal("field.dec"),
2179
+ target: targetSchema,
2180
+ value: v.optional(ValueExprSchema)
1971
2181
  }), v.strictObject({
1972
2182
  type: v.literal("field.updateWhere"),
1973
2183
  target: targetSchema,
@@ -2049,16 +2259,17 @@ function compositeChecked(entries) {
2049
2259
  return v.pipe(v.strictObject(entries), v.check(input => compositeShapeOk(input), issue => compositeShapeMessage(issue.input)), v.check(input => duplicateSubfieldName(input) === void 0, issue => `duplicate sub-field name "${duplicateSubfieldName(issue.input)}" — sub-field names must be unique within \`fields\` / \`of\``));
2050
2260
  }
2051
2261
 
2052
- const FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
2262
+ const AssignmentRolesSchema = v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one eligible role, or omit `roles`")), FieldShapeSchema = v.lazy(() => v.pipe(compositeChecked({
2053
2263
  type: FieldValueKindSchema,
2054
2264
  name: FieldEntryName,
2055
2265
  title: v.optional(v.string()),
2056
2266
  description: v.optional(v.string()),
2057
2267
  options: v.optional(ChoiceOptionsSchema),
2058
2268
  validation: v.optional(ScalarValidationSchema),
2269
+ roles: v.optional(AssignmentRolesSchema),
2059
2270
  fields: v.optional(v.array(FieldShapeSchema)),
2060
2271
  of: v.optional(v.array(FieldShapeSchema))
2061
- }), choiceOptionsCheck(), scalarValidationCheck())), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
2272
+ }), choiceOptionsCheck(), scalarValidationCheck(), assignmentRolesCheck())), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
2062
2273
 
2063
2274
  function fieldBase(editable, group) {
2064
2275
  return {
@@ -2079,6 +2290,7 @@ function fieldEntryFields({editable: editable, group: group, kind: kind = FieldK
2079
2290
  options: v.optional(ChoiceOptionsSchema),
2080
2291
  validation: v.optional(ScalarValidationSchema),
2081
2292
  types: v.optional(v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
2293
+ roles: v.optional(AssignmentRolesSchema),
2082
2294
  fields: v.optional(v.array(FieldShapeSchema)),
2083
2295
  of: v.optional(v.array(FieldShapeSchema))
2084
2296
  };
@@ -2092,7 +2304,8 @@ function literalSeedIssues(entry) {
2092
2304
  fields: entry.fields,
2093
2305
  of: entry.of,
2094
2306
  options: entry.options,
2095
- validation: entry.validation
2307
+ validation: entry.validation,
2308
+ roles: entry.roles
2096
2309
  });
2097
2310
  }
2098
2311
 
@@ -2104,6 +2317,10 @@ function refTypesCheck() {
2104
2317
  return v.check(entry => entry.types === void 0 || refKindAcceptsTypes(entry.type), issue => `\`types\` is only valid on \`doc.ref\` / \`doc.refs\` / \`subject\` entries, not "${issue.input.type}"`);
2105
2318
  }
2106
2319
 
2320
+ function assignmentRolesCheck() {
2321
+ return v.check(entry => entry.roles === void 0 || assignmentKindAcceptsRoles(entry.type), issue => `\`roles\` is only valid on \`assignee\` / \`assignees\` entries, not "${issue.input.type}"`);
2322
+ }
2323
+
2107
2324
  function choiceOptionsCheck() {
2108
2325
  return v.check(entry => checkChoiceList({
2109
2326
  entryType: entry.type,
@@ -2138,11 +2355,11 @@ function scalarValidationDeclarationIssues(entry) {
2138
2355
  const FieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
2139
2356
  editable: StoredEditableSchema,
2140
2357
  group: StoredGroupMembershipSchema
2141
- })), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
2358
+ })), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields({
2142
2359
  editable: AuthoringEditableSchema,
2143
2360
  group: AuthoringGroupMembershipSchema,
2144
2361
  kind: AuthoringRawFieldKindSchema
2145
- })), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v.strictObject({
2362
+ })), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v.strictObject({
2146
2363
  type: v.literal("claim"),
2147
2364
  name: FieldEntryName,
2148
2365
  title: v.optional(v.string()),
@@ -2184,17 +2401,27 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
2184
2401
  required: v.optional(v.boolean()),
2185
2402
  options: v.optional(ChoiceOptionsSchema),
2186
2403
  validation: v.optional(ScalarValidationSchema)
2187
- }), choiceOptionsCheck(), scalarValidationCheck());
2404
+ }), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
2405
+
2406
+ function semanticNamespace(semantic) {
2407
+ return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
2408
+ }
2188
2409
 
2189
2410
  function hasUniqueSemanticNamespaces(semantics) {
2190
- const namespaces = semantics.map(semantic => semantic.split(".", 1)[0]);
2411
+ const namespaces = semantics.map(semanticNamespace);
2191
2412
  return new Set(namespaces).size === namespaces.length;
2192
2413
  }
2193
2414
 
2415
+ function semanticsFieldSchema(semantic) {
2416
+ return v.optional(v.pipe(v.array(semantic), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
2417
+ }
2418
+
2419
+ const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
2420
+
2194
2421
  function actionFields(op, group) {
2195
2422
  return {
2196
2423
  name: NonEmpty,
2197
- semantics: v.optional(v.pipe(v.array(picklist(ACTION_SEMANTICS)), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace"))),
2424
+ semantics: ActionSemanticsFieldSchema,
2198
2425
  title: v.optional(v.string()),
2199
2426
  description: v.optional(v.string()),
2200
2427
  group: v.optional(group),
@@ -2241,6 +2468,7 @@ const StoredActionSchema = pinned()(v.strictObject({
2241
2468
  function activityFields({field: field, action: action, target: target, group: group}) {
2242
2469
  return {
2243
2470
  name: NonEmpty,
2471
+ semantics: SemanticsFieldSchema,
2244
2472
  title: v.optional(v.string()),
2245
2473
  description: v.optional(v.string()),
2246
2474
  groups: v.optional(v.array(GroupSchema)),
@@ -2314,6 +2542,7 @@ const GuardSchema = v.strictObject(guardFields(NonEmpty)), AuthoringGuardSchema
2314
2542
  function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
2315
2543
  return {
2316
2544
  name: NonEmpty,
2545
+ semantics: SemanticsFieldSchema,
2317
2546
  title: v.optional(v.string()),
2318
2547
  description: v.optional(v.string()),
2319
2548
  groups: v.optional(v.array(GroupSchema)),
@@ -2352,6 +2581,7 @@ const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KIN
2352
2581
  function workflowFields({field: field, stage: stage, start: start}) {
2353
2582
  return {
2354
2583
  name: NonEmpty,
2584
+ semantics: SemanticsFieldSchema,
2355
2585
  title: NonEmpty,
2356
2586
  description: v.optional(v.string()),
2357
2587
  groups: v.optional(v.array(GroupSchema)),
@@ -2548,11 +2778,20 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
2548
2778
  });
2549
2779
  }
2550
2780
 
2781
+ function actionSites(def) {
2782
+ return def.stages.flatMap((stage, i) => (stage.activities ?? []).flatMap((activity, j) => (activity.actions ?? []).map((action, a) => ({
2783
+ action: action,
2784
+ activity: activity,
2785
+ stage: stage,
2786
+ path: [ "stages", i, "activities", j, "actions", a ]
2787
+ }))));
2788
+ }
2789
+
2551
2790
  function effectNameSites(def) {
2552
2791
  const sites = [];
2553
- for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) collectEffects({
2792
+ for (const {action: action, path: path} of actionSites(def)) collectEffects({
2554
2793
  effects: action.effects,
2555
- path: [ "stages", i, "activities", j, "actions", a, "effects" ],
2794
+ path: [ ...path, "effects" ],
2556
2795
  sites: sites
2557
2796
  });
2558
2797
  return sites;
@@ -2683,13 +2922,22 @@ function checkUnboundConditionVars(def, issues) {
2683
2922
  }
2684
2923
  }
2685
2924
 
2925
+ const SOFT_GATE_VARS = {
2926
+ can: {
2927
+ noun: "the caller's grants"
2928
+ },
2929
+ attributes: {
2930
+ noun: "the caller's org-level user attributes"
2931
+ }
2932
+ }, SOFT_GATE_VAR_NAMES = Object.keys(SOFT_GATE_VARS);
2933
+
2686
2934
  function unboundVarsAt(site) {
2687
2935
  const callerVars = unboundCallerVars(site.policy);
2688
2936
  return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
2689
2937
  }
2690
2938
 
2691
2939
  function unboundCallerVars(policy) {
2692
- return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
2940
+ return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ ...SOFT_GATE_VAR_NAMES, "params" ] : SOFT_GATE_VAR_NAMES;
2693
2941
  }
2694
2942
 
2695
2943
  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";
@@ -2699,7 +2947,12 @@ function rowVarMessage(site) {
2699
2947
  }
2700
2948
 
2701
2949
  function callerVarMessage(site, name) {
2702
- 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`;
2950
+ if (site.policy === "cascade") return `${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\``;
2951
+ if (site.policy === "caller-bound") return `${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`;
2952
+ if (site.policy === "triggered-payload" && name === "params") return `${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`;
2953
+ const softGate = SOFT_GATE_VARS[name];
2954
+ if (softGate !== void 0) return `${site.label} reads $${name} — $${name} (${softGate.noun}) is bound only in the caller-bound projection (action filters, requirements, editable predicates); this site never binds it. Move the check to one of those sites or drop $${name}`;
2955
+ throw new Error(`callerVarMessage: unreachable for $${name} at ${site.label} (policy ${site.policy})`);
2703
2956
  }
2704
2957
 
2705
2958
  function conditionSites(def) {
@@ -2954,9 +3207,9 @@ function belowScopeNestedSites(args) {
2954
3207
  }
2955
3208
 
2956
3209
  function checkLevelKindEffectOutputs(def, issues) {
2957
- for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) pushOutputIssues({
3210
+ for (const {action: action, path: path} of actionSites(def)) pushOutputIssues({
2958
3211
  action: action,
2959
- path: [ "stages", i, "activities", j, "actions", a ],
3212
+ path: path,
2960
3213
  issues: issues
2961
3214
  });
2962
3215
  }
@@ -3038,18 +3291,18 @@ function storedRolesIssue(action) {
3038
3291
  }
3039
3292
 
3040
3293
  function checkStoredRolesPlacement(def, issues) {
3041
- for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) {
3294
+ for (const {action: action, path: path} of actionSites(def)) {
3042
3295
  const message = storedRolesIssue(action);
3043
3296
  message !== void 0 && issues.push({
3044
- path: [ "stages", i, "activities", j, "actions", a, "roles" ],
3297
+ path: [ ...path, "roles" ],
3045
3298
  message: message
3046
3299
  });
3047
3300
  }
3048
3301
  }
3049
3302
 
3050
3303
  function checkTriggeredActionParams(def, issues) {
3051
- for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
3052
- path: [ "stages", i, "activities", j, "actions", a, "params" ],
3304
+ for (const {action: action, path: path} of actionSites(def)) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
3305
+ path: [ ...path, "params" ],
3053
3306
  message: `action "${action.name}" declares params but is cascade-fired (\`when\`) — no caller ever supplies args to a trigger. Drop the params, or drop \`when\` to make it a fireAction-fired action`
3054
3307
  });
3055
3308
  }
@@ -3094,13 +3347,20 @@ function checkSingleSubjectRequirements(def, issues) {
3094
3347
 
3095
3348
  function checkStartFilterReads(def, issues) {
3096
3349
  const filter = def.start?.filter;
3097
- filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
3350
+ if (filter === void 0) return;
3351
+ const params = conditionParameterNames(filter);
3352
+ params.has("fields") && issues.push({
3098
3353
  path: [ "start", "filter" ],
3099
3354
  message: "start.filter reads $fields, but the filter is browse-time-pure — a start surface evaluates it per document, before any inputs exist, so $fields cannot be bound. Move the input-dependent rule to a start requirement, the start-time readiness predicate that binds $fields and is enforced by startInstance"
3100
- }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
3355
+ });
3356
+ for (const name of CALLER_BOUND_VARS) params.has(name) && issues.push({
3357
+ path: [ "start", "filter" ],
3358
+ message: `start.filter reads $${name} — start.filter is a read-side document-visibility rule (definitionsForDocument / start controls); startInstance never evaluates it, and no caller bag binds there, so the read is GROQ null and the filter can never match. Drop $${name} from start.filter. A "who may start" rule belongs in a start requirement, not in the browse-time filter`
3359
+ });
3360
+ readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
3101
3361
  path: [ "start", "filter" ],
3102
3362
  message: "start.filter reads the candidate document (its root), but the definition declares no `subject` entry — a read surface would never have a document to bind as root, so every root read is GROQ null and the filter silently misevaluates. Declare a `subject` entry (the document the workflow is about), or gate on the dataset instead"
3103
- }));
3363
+ });
3104
3364
  }
3105
3365
 
3106
3366
  function checkStartRequirementReads(def, issues) {
@@ -3396,15 +3656,13 @@ function seedEarlierSiblingTarget(args) {
3396
3656
  }
3397
3657
 
3398
3658
  function opSites(def) {
3399
- const sites = [];
3400
- for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) sites.push({
3659
+ return actionSites(def).map(({action: action, activity: activity, stage: stage, path: path}) => ({
3401
3660
  ops: action.ops,
3402
- path: [ "stages", i, "activities", j, "actions", a, "ops" ],
3661
+ path: [ ...path, "ops" ],
3403
3662
  label: `action "${action.name}"`,
3404
3663
  stage: stage,
3405
3664
  activity: activity
3406
- });
3407
- return sites;
3665
+ }));
3408
3666
  }
3409
3667
 
3410
3668
  function checkFieldReadOpValues(def, issues) {
@@ -3421,7 +3679,7 @@ function checkFieldReadOpValues(def, issues) {
3421
3679
  }
3422
3680
 
3423
3681
  function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
3424
- for (const [o, op] of (ops ?? []).entries()) if ("value" in op) for (const {read: read, path: readPath} of fieldReadsIn(op.value, [ ...path, o, "value" ])) checkOpFieldRead({
3682
+ for (const [o, op] of (ops ?? []).entries()) if (!(!("value" in op) || op.value === void 0)) for (const {read: read, path: readPath} of fieldReadsIn(op.value, [ ...path, o, "value" ])) checkOpFieldRead({
3425
3683
  ...ctx,
3426
3684
  read: read,
3427
3685
  path: readPath,
@@ -3472,35 +3730,55 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
3472
3730
  return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty. Known: ${known.join(", ") || "(none)"}`;
3473
3731
  }
3474
3732
 
3475
- function checkUpdateWhereOps(def, issues) {
3733
+ function checkFieldTargetOps(def, issues) {
3476
3734
  const workflow = def.fields ?? [];
3477
3735
  for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
3478
- if (op.type !== "field.updateWhere") continue;
3479
- const scopes = {
3480
- workflow: workflow,
3481
- stage: site.stage.fields ?? [],
3482
- activity: site.activity.fields ?? []
3483
- };
3484
- checkUpdateWhereTargetKind({
3736
+ const path = [ ...site.path, o ], rule = TARGET_KIND_RULES[op.type];
3737
+ rule !== void 0 && "target" in op && checkTargetKind({
3485
3738
  op: op,
3486
- path: [ ...site.path, o ],
3739
+ path: [ ...path, "target" ],
3487
3740
  label: site.label,
3488
- scopes: scopes,
3741
+ scopes: opTargetScopes(workflow, site),
3742
+ rule: rule,
3489
3743
  issues: issues
3490
- }), checkUpdateWhereMergeKeys({
3744
+ }), op.type === "field.updateWhere" && checkUpdateWhereMergeKeys({
3491
3745
  op: op,
3492
- path: [ ...site.path, o ],
3746
+ path: path,
3493
3747
  label: site.label,
3494
3748
  issues: issues
3495
3749
  });
3496
3750
  }
3497
3751
  }
3498
3752
 
3499
- function checkUpdateWhereTargetKind({op: op, path: path, label: label, scopes: scopes, issues: issues}) {
3753
+ const arithmeticTargetRule = {
3754
+ accepts: target => target.type === "number",
3755
+ issue: () => "arithmetic ops target `number` entries only"
3756
+ }, TARGET_KIND_RULES = {
3757
+ "field.inc": arithmeticTargetRule,
3758
+ "field.dec": arithmeticTargetRule,
3759
+ "field.setIfMissing": {
3760
+ accepts: target => !isAlwaysArrayFieldKind(target.type),
3761
+ issue: () => "setIfMissing applies to nullable entries only; an empty array entry already holds []"
3762
+ },
3763
+ "field.updateWhere": {
3764
+ accepts: target => target.type === "array",
3765
+ issue: target => "updateWhere merges declared row sub-fields, so its target must be an `array` entry" + rowOpsHint(target.type)
3766
+ }
3767
+ };
3768
+
3769
+ function opTargetScopes(workflow, site) {
3770
+ return {
3771
+ workflow: workflow,
3772
+ stage: site.stage.fields ?? [],
3773
+ activity: site.activity.fields ?? []
3774
+ };
3775
+ }
3776
+
3777
+ function checkTargetKind({op: op, path: path, label: label, scopes: scopes, rule: rule, issues: issues}) {
3500
3778
  const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
3501
- target === void 0 || target.type === "array" || issues.push({
3502
- path: [ ...path, "target" ],
3503
- message: `${label} field.updateWhere targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — updateWhere merges declared row sub-fields, so its target must be an \`array\` entry${rowOpsHint(target.type)}`
3779
+ target === void 0 || rule.accepts(target) || issues.push({
3780
+ path: path,
3781
+ message: `${label} ${op.type} targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — ${rule.issue(target)}`
3504
3782
  });
3505
3783
  }
3506
3784
 
@@ -3732,11 +4010,11 @@ function checkWorkflowInvariants(def) {
3732
4010
  }), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
3733
4011
  checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
3734
4012
  checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3735
- checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
4013
+ checkFieldReadOpValues(def, issues), checkFieldTargetOps(def, issues), checkGuardFieldReads(def, issues),
3736
4014
  checkAssigneesEntries(def, issues), checkDueDateEntries(def, issues), checkSubjectEntries(def, issues),
3737
4015
  checkLevelKindEffectOutputs(def, issues), checkActivityTerminalPaths(def, issues),
3738
4016
  checkTerminalStageActivities(def, issues), checkTriggeredActionParams(def, issues),
3739
4017
  checkStoredRolesPlacement(def, issues), checkGroups(def, issues), issues;
3740
4018
  }
3741
4019
 
3742
- export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, 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_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
4020
+ export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DECISION_SEMANTICS, 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, SIGNAL_SEMANTICS, START_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, assignmentKindAcceptsRoles, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isAlwaysArrayFieldKind, isAssignmentFieldEntry, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isRecord, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };