@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.
@@ -55,79 +55,6 @@ function andConditions(parts) {
55
55
  if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
56
56
  }
57
57
 
58
- class WorkflowError extends Error {
59
- kind;
60
- constructor(kind, message, options) {
61
- super(message, options), this.kind = kind;
62
- }
63
- }
64
-
65
- class ContractViolationError extends WorkflowError {
66
- constructor(message) {
67
- super("contract-violation", message), this.name = "ContractViolationError";
68
- }
69
- }
70
-
71
- class InstanceNotFoundError extends WorkflowError {
72
- instanceId;
73
- constructor(args) {
74
- super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
75
- this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
76
- }
77
- }
78
-
79
- class DefinitionNotFoundError extends WorkflowError {
80
- definition;
81
- version;
82
- constructor(args) {
83
- 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`),
84
- this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
85
- }
86
- }
87
-
88
- class SpawnContractsInvalidError extends WorkflowError {
89
- issues;
90
- constructor(args) {
91
- super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
92
- this.issues = args.issues;
93
- }
94
- }
95
-
96
- class DefinitionInUseError extends WorkflowError {
97
- definition;
98
- blockedBy;
99
- constructor(args) {
100
- super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
101
- this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
102
- }
103
- }
104
-
105
- function definitionInUseMessage(definition, blockedBy) {
106
- if (blockedBy.reason === "non-terminal-instances") {
107
- const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
108
- 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.`;
109
- }
110
- const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
111
- return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
112
- }
113
-
114
- class EffectNotFoundError extends WorkflowError {
115
- instanceId;
116
- effectKey;
117
- settled;
118
- constructor(args) {
119
- super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
120
- this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
121
- }
122
- }
123
-
124
- function effectNotFoundMessage(args) {
125
- const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
126
- if (args.settled === void 0) return base;
127
- const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
128
- 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}`;
129
- }
130
-
131
58
  const KNOWN_SCHEMES = /* @__PURE__ */ new Set([ "dataset", "canvas", "media-library", "dashboard" ]), KNOWN_SCHEMES_TEXT = [ ...KNOWN_SCHEMES ].join(", ");
132
59
 
133
60
  class VersionSpecificDatasetGdrError extends Error {
@@ -343,6 +270,79 @@ function isGdr(value) {
343
270
  return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
344
271
  }
345
272
 
273
+ class WorkflowError extends Error {
274
+ kind;
275
+ constructor(kind, message, options) {
276
+ super(message, options), this.kind = kind;
277
+ }
278
+ }
279
+
280
+ class ContractViolationError extends WorkflowError {
281
+ constructor(message) {
282
+ super("contract-violation", message), this.name = "ContractViolationError";
283
+ }
284
+ }
285
+
286
+ class InstanceNotFoundError extends WorkflowError {
287
+ instanceId;
288
+ constructor(args) {
289
+ super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
290
+ this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
291
+ }
292
+ }
293
+
294
+ class DefinitionNotFoundError extends WorkflowError {
295
+ definition;
296
+ version;
297
+ constructor(args) {
298
+ 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`),
299
+ this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
300
+ }
301
+ }
302
+
303
+ class SpawnContractsInvalidError extends WorkflowError {
304
+ issues;
305
+ constructor(args) {
306
+ super("spawn-contracts-invalid", args.message), this.name = "SpawnContractsInvalidError",
307
+ this.issues = args.issues;
308
+ }
309
+ }
310
+
311
+ class DefinitionInUseError extends WorkflowError {
312
+ definition;
313
+ blockedBy;
314
+ constructor(args) {
315
+ super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
316
+ this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
317
+ }
318
+ }
319
+
320
+ function definitionInUseMessage(definition, blockedBy) {
321
+ if (blockedBy.reason === "non-terminal-instances") {
322
+ const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
323
+ 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.`;
324
+ }
325
+ const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
326
+ return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
327
+ }
328
+
329
+ class EffectNotFoundError extends WorkflowError {
330
+ instanceId;
331
+ effectKey;
332
+ settled;
333
+ constructor(args) {
334
+ super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
335
+ this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
336
+ }
337
+ }
338
+
339
+ function effectNotFoundMessage(args) {
340
+ const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
341
+ if (args.settled === void 0) return base;
342
+ const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
343
+ 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}`;
344
+ }
345
+
346
346
  const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
347
347
 
348
348
  function validateTag(tag) {
@@ -539,6 +539,7 @@ function desugarWorkflow(authoring) {
539
539
  return {
540
540
  ...stripUndefined({
541
541
  name: stage.name,
542
+ semantics: stage.semantics,
542
543
  title: stage.title,
543
544
  description: stage.description,
544
545
  groups: stage.groups,
@@ -562,6 +563,7 @@ function desugarWorkflow(authoring) {
562
563
  definition: {
563
564
  ...stripUndefined({
564
565
  name: authoring.name,
566
+ semantics: authoring.semantics,
565
567
  title: authoring.title,
566
568
  description: authoring.description,
567
569
  groups: authoring.groups,
@@ -666,6 +668,7 @@ function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
666
668
  options: entry.options,
667
669
  validation: entry.validation,
668
670
  types: entry.types,
671
+ roles: entry.roles,
669
672
  fields: entry.fields,
670
673
  of: entry.of
671
674
  })
@@ -788,6 +791,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
788
791
  return {
789
792
  ...stripUndefined({
790
793
  name: activity.name,
794
+ semantics: activity.semantics,
791
795
  title: activity.title,
792
796
  description: activity.description,
793
797
  groups: activity.groups,
@@ -1259,7 +1263,66 @@ function isTerminalActivityStatus(status) {
1259
1263
  return TERMINAL_ACTIVITY_STATUSES.includes(status);
1260
1264
  }
1261
1265
 
1262
- 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 = [ {
1266
+ 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" ];
1267
+
1268
+ function releaseDocId(releaseName) {
1269
+ return `_.releases.${releaseName}`;
1270
+ }
1271
+
1272
+ function releaseRef({res: res, releaseName: releaseName}) {
1273
+ if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
1274
+ return {
1275
+ id: gdrFromResource(res, releaseDocId(releaseName)),
1276
+ type: "system.release",
1277
+ releaseName: releaseName
1278
+ };
1279
+ }
1280
+
1281
+ function isAlwaysArrayFieldKind(kind) {
1282
+ return kind === "doc.refs" || kind === "assignees" || kind === "array";
1283
+ }
1284
+
1285
+ function isSingleDocRefKind(kind) {
1286
+ return kind === "doc.ref" || kind === "subject";
1287
+ }
1288
+
1289
+ function refKindAcceptsTypes(kind) {
1290
+ return isSingleDocRefKind(kind) || kind === "doc.refs";
1291
+ }
1292
+
1293
+ function assignmentKindAcceptsRoles(kind) {
1294
+ return kind === "assignee" || kind === "assignees";
1295
+ }
1296
+
1297
+ function isSingleDocRefEntry(entry) {
1298
+ return isSingleDocRefKind(entry._type);
1299
+ }
1300
+
1301
+ function isAssignmentFieldEntry(entry) {
1302
+ return assignmentKindAcceptsRoles(entry._type);
1303
+ }
1304
+
1305
+ function isTodoListItem(row) {
1306
+ if (typeof row != "object" || row === null) return !1;
1307
+ const candidate = row, status = candidate.status;
1308
+ return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
1309
+ }
1310
+
1311
+ function declaredRowColumns(entry) {
1312
+ if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
1313
+ }
1314
+
1315
+ function isTodoListEntry(entry) {
1316
+ const columns = declaredRowColumns(entry);
1317
+ return columns !== void 0 && columns.has("label") && columns.has("status");
1318
+ }
1319
+
1320
+ function isNotesEntry(entry) {
1321
+ const columns = declaredRowColumns(entry);
1322
+ return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
1323
+ }
1324
+
1325
+ const CONDITION_VARS = [ {
1263
1326
  name: "self",
1264
1327
  binding: "always",
1265
1328
  label: "this workflow instance",
@@ -1334,6 +1397,11 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
1334
1397
  binding: "caller",
1335
1398
  label: "your permissions",
1336
1399
  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."
1400
+ }, {
1401
+ name: "attributes",
1402
+ binding: "caller",
1403
+ label: "your attributes",
1404
+ 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."
1337
1405
  }, {
1338
1406
  name: "row",
1339
1407
  binding: "spawn",
@@ -1499,54 +1567,7 @@ function documentIdOf(doc) {
1499
1567
  return "(unknown id)";
1500
1568
  }
1501
1569
 
1502
- const ACTOR_KINDS = [ "person", "agent", "system" ];
1503
-
1504
- function releaseDocId(releaseName) {
1505
- return `_.releases.${releaseName}`;
1506
- }
1507
-
1508
- function releaseRef({res: res, releaseName: releaseName}) {
1509
- if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
1510
- return {
1511
- id: gdrFromResource(res, releaseDocId(releaseName)),
1512
- type: "system.release",
1513
- releaseName: releaseName
1514
- };
1515
- }
1516
-
1517
- function isSingleDocRefKind(kind) {
1518
- return kind === "doc.ref" || kind === "subject";
1519
- }
1520
-
1521
- function refKindAcceptsTypes(kind) {
1522
- return isSingleDocRefKind(kind) || kind === "doc.refs";
1523
- }
1524
-
1525
- function isSingleDocRefEntry(entry) {
1526
- return isSingleDocRefKind(entry._type);
1527
- }
1528
-
1529
- function isTodoListItem(row) {
1530
- if (typeof row != "object" || row === null) return !1;
1531
- const candidate = row, status = candidate.status;
1532
- return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
1533
- }
1534
-
1535
- function declaredRowColumns(entry) {
1536
- if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
1537
- }
1538
-
1539
- function isTodoListEntry(entry) {
1540
- const columns = declaredRowColumns(entry);
1541
- return columns !== void 0 && columns.has("label") && columns.has("status");
1542
- }
1543
-
1544
- function isNotesEntry(entry) {
1545
- const columns = declaredRowColumns(entry);
1546
- return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
1547
- }
1548
-
1549
- const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
1570
+ const ACTOR_KINDS = [ "person", "agent", "system" ], ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
1550
1571
 
1551
1572
  function classifyPrincipalId(id) {
1552
1573
  if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
@@ -1589,6 +1610,10 @@ function lakePrincipalId(args) {
1589
1610
  return args.localPrincipalId ?? args.actor.id;
1590
1611
  }
1591
1612
 
1613
+ function isRecord(value) {
1614
+ return typeof value == "object" && value !== null && !Array.isArray(value);
1615
+ }
1616
+
1592
1617
  class FieldValueShapeError extends WorkflowError {
1593
1618
  entryType;
1594
1619
  entryName;
@@ -1685,9 +1710,16 @@ const fieldValueSchemas = {
1685
1710
  query: v__namespace.any()
1686
1711
  };
1687
1712
 
1688
- function shapeValueSchema(shape, leaf) {
1689
- if (shape.type === "object") return objectSchema(shape.fields ?? [], leaf);
1690
- if (shape.type === "array") return v__namespace.array(objectSchema(shape.of ?? [], leaf));
1713
+ function shapeValueSchema(args) {
1714
+ const {shape: shape, leaf: leaf} = args;
1715
+ if (shape.type === "object") return objectSchema({
1716
+ fields: shape.fields ?? [],
1717
+ leaf: leaf
1718
+ });
1719
+ if (shape.type === "array") return v__namespace.array(objectSchema({
1720
+ fields: shape.of ?? [],
1721
+ leaf: leaf
1722
+ }));
1691
1723
  const schema = leaf[shape.type] ?? v__namespace.any();
1692
1724
  return constrainedScalarSchema({
1693
1725
  schema: schema,
@@ -1738,19 +1770,31 @@ function constrainedScalarSchema(args) {
1738
1770
  }) ?? [] ].join("; ")));
1739
1771
  }
1740
1772
 
1741
- function objectSchema(fields, leaf) {
1742
- const entries = /* @__PURE__ */ Object.create(null);
1743
- for (const f of fields) entries[f.name] = v__namespace.optional(shapeValueSchema(f, leaf));
1773
+ function objectSchema(args) {
1774
+ const {fields: fields, leaf: leaf} = args, entries = /* @__PURE__ */ Object.create(null);
1775
+ for (const f of fields) entries[f.name] = v__namespace.optional(shapeValueSchema({
1776
+ shape: f,
1777
+ leaf: leaf
1778
+ }));
1744
1779
  return v__namespace.looseObject(entries);
1745
1780
  }
1746
1781
 
1747
1782
  function wholeValueSchema(args) {
1748
1783
  const {entryType: entryType, shape: shape, leaf: leaf} = args;
1749
- return entryType === "object" ? v__namespace.union([ v__namespace.null(), objectSchema(shape.fields ?? [], leaf) ]) : entryType === "array" ? v__namespace.array(objectSchema(shape.of ?? [], leaf)) : leaf[entryType];
1784
+ return entryType === "object" ? v__namespace.union([ v__namespace.null(), objectSchema({
1785
+ fields: shape.fields ?? [],
1786
+ leaf: leaf
1787
+ }) ]) : entryType === "array" ? v__namespace.array(objectSchema({
1788
+ fields: shape.of ?? [],
1789
+ leaf: leaf
1790
+ })) : leaf[entryType];
1750
1791
  }
1751
1792
 
1752
1793
  function appendItemSchema(entryType, shape) {
1753
- if (entryType === "array") return objectSchema(shape.of ?? [], valueSchemas);
1794
+ if (entryType === "array") return objectSchema({
1795
+ fields: shape.of ?? [],
1796
+ leaf: valueSchemas
1797
+ });
1754
1798
  if (entryType === "doc.refs") return GdrShape;
1755
1799
  if (entryType === "assignees") return AssigneeWriteShape;
1756
1800
  }
@@ -1802,7 +1846,7 @@ function checkValueAgainst(args, leaf) {
1802
1846
  entryType: args.entryType,
1803
1847
  validation: args.validation,
1804
1848
  value: args.value
1805
- });
1849
+ }) ?? assignmentValueIssues(args, result.output);
1806
1850
  return postIssues !== void 0 ? {
1807
1851
  issues: postIssues
1808
1852
  } : {
@@ -1810,6 +1854,142 @@ function checkValueAgainst(args, leaf) {
1810
1854
  };
1811
1855
  }
1812
1856
 
1857
+ function assignmentIdentity(assignee) {
1858
+ return assignee.type === "user" ? `user:${assignee.id}` : `role:${assignee.role}`;
1859
+ }
1860
+
1861
+ function assignmentCandidate(value) {
1862
+ const parsed = v__namespace.safeParse(AssigneeShape, value);
1863
+ return parsed.success ? parsed.output : void 0;
1864
+ }
1865
+
1866
+ function assignmentValueIssues(args, value) {
1867
+ const retained = previousAssignmentIdentities(args), issues = [];
1868
+ return visitAssignmentEntries({
1869
+ entryType: args.entryType,
1870
+ value: value,
1871
+ roles: args.roles,
1872
+ fields: args.fields,
1873
+ of: args.of,
1874
+ path: "$"
1875
+ }, ({assignee: assignee, roles: roles, path: path}) => {
1876
+ consumeRetainedAssignment({
1877
+ retained: retained,
1878
+ path: path,
1879
+ identity: assignmentIdentity(assignee)
1880
+ }) || issues.push(...assignmentCandidateIssues({
1881
+ roles: roles,
1882
+ memberRoles: args.memberRoles,
1883
+ roleAliases: args.roleAliases
1884
+ }, assignee));
1885
+ }), issues.length === 0 ? void 0 : issues;
1886
+ }
1887
+
1888
+ function consumeRetainedAssignment(args) {
1889
+ const identities = args.retained.get(args.path), remaining = identities?.get(args.identity) ?? 0;
1890
+ return remaining === 0 || identities === void 0 ? !1 : (remaining === 1 ? identities.delete(args.identity) : identities.set(args.identity, remaining - 1),
1891
+ !0);
1892
+ }
1893
+
1894
+ function previousAssignmentIdentities(args) {
1895
+ const retained = /* @__PURE__ */ new Map;
1896
+ if (args.previousValue === void 0 || args.entryType === "assignee") return retained;
1897
+ const schema = wholeValueSchema({
1898
+ entryType: args.entryType,
1899
+ shape: args,
1900
+ leaf: valueSchemas
1901
+ });
1902
+ if (schema === void 0) return retained;
1903
+ const previous = v__namespace.safeParse(schema, args.previousValue);
1904
+ return previous.success && visitAssignmentEntries({
1905
+ entryType: args.entryType,
1906
+ value: previous.output,
1907
+ roles: args.roles,
1908
+ fields: args.fields,
1909
+ of: args.of,
1910
+ path: "$"
1911
+ }, ({assignee: assignee, path: path}) => {
1912
+ const identities = retained.get(path) ?? /* @__PURE__ */ new Map, identity = assignmentIdentity(assignee);
1913
+ identities.set(identity, (identities.get(identity) ?? 0) + 1), retained.set(path, identities);
1914
+ }), retained;
1915
+ }
1916
+
1917
+ function visitAssignmentEntries(args, visit) {
1918
+ if (assignmentKindAcceptsRoles(args.entryType)) {
1919
+ visitAssignmentCandidates(args, visit);
1920
+ return;
1921
+ }
1922
+ if (args.entryType === "object") {
1923
+ visitAssignmentFields({
1924
+ fields: args.fields,
1925
+ value: args.value,
1926
+ path: args.path
1927
+ }, visit);
1928
+ return;
1929
+ }
1930
+ if (!(args.entryType !== "array" || !Array.isArray(args.value))) for (const row of args.value) visitAssignmentFields({
1931
+ fields: args.of,
1932
+ value: row,
1933
+ path: `${args.path}[]`
1934
+ }, visit);
1935
+ }
1936
+
1937
+ function visitAssignmentCandidates(args, visit) {
1938
+ if (args.roles === void 0) return;
1939
+ const indexedValues = args.entryType === "assignees" && Array.isArray(args.value) ? args.value.map(value => ({
1940
+ value: value,
1941
+ path: args.path
1942
+ })) : [ {
1943
+ value: args.value,
1944
+ path: args.path
1945
+ } ];
1946
+ for (const {value: value, path: path} of indexedValues) {
1947
+ const assignee = assignmentCandidate(value);
1948
+ assignee !== void 0 && visit({
1949
+ assignee: assignee,
1950
+ roles: args.roles,
1951
+ path: path
1952
+ });
1953
+ }
1954
+ }
1955
+
1956
+ function visitAssignmentFields(args, visit) {
1957
+ if (!(args.fields === void 0 || !isRecord(args.value))) for (const field of args.fields) visitAssignmentShape({
1958
+ shape: field,
1959
+ value: args.value[field.name],
1960
+ path: `${args.path}.${field.name}`
1961
+ }, visit);
1962
+ }
1963
+
1964
+ function visitAssignmentShape(args, visit) {
1965
+ visitAssignmentEntries({
1966
+ entryType: args.shape.type,
1967
+ value: args.value,
1968
+ roles: args.shape.roles,
1969
+ fields: args.shape.fields,
1970
+ of: args.shape.of,
1971
+ path: args.path
1972
+ }, visit);
1973
+ }
1974
+
1975
+ function assignmentCandidateIssues(args, assignee) {
1976
+ if (assignee.type === "role") return collectiveRoleIssues(args, assignee.role);
1977
+ const held = args.memberRoles?.[assignee.id];
1978
+ return held === void 0 ? args.memberRoles === void 0 ? [] : [ `user "${assignee.id}" has no available project membership for role eligibility` ] : args.roles.some(required => actorFulfillsRole({
1979
+ actorRoles: held,
1980
+ required: required,
1981
+ aliases: args.roleAliases
1982
+ })) ? [] : [ `user "${assignee.id}" does not fulfill any eligible role: ${formatEligibleRoles(args.roles)}` ];
1983
+ }
1984
+
1985
+ function collectiveRoleIssues(args, role) {
1986
+ return args.roles.includes(role) ? [] : [ `collective role "${role}" is not eligible — this entry accepts ${formatEligibleRoles(args.roles)}` ];
1987
+ }
1988
+
1989
+ function formatEligibleRoles(roles) {
1990
+ return roles.map(role => `"${role}"`).join(", ");
1991
+ }
1992
+
1813
1993
  function issuesOf(check) {
1814
1994
  return "issues" in check ? check.issues : void 0;
1815
1995
  }
@@ -1869,6 +2049,20 @@ function validateFieldAppendItem(args) {
1869
2049
  issues: formatIssues(result.issues),
1870
2050
  mode: "item"
1871
2051
  });
2052
+ const assignmentArgs = {
2053
+ entryType: appendAssignmentEntryType(args.entryType),
2054
+ value: result.output,
2055
+ roles: args.roles,
2056
+ fields: args.entryType === "array" ? args.of : void 0,
2057
+ memberRoles: args.memberRoles,
2058
+ roleAliases: args.roleAliases
2059
+ }, assignmentIssues = assignmentValueIssues(assignmentArgs, result.output);
2060
+ if (assignmentIssues !== void 0) throw new FieldValueShapeError({
2061
+ entryType: args.entryType,
2062
+ entryName: args.entryName,
2063
+ issues: assignmentIssues,
2064
+ mode: "item"
2065
+ });
1872
2066
  const typeIssues = refTypeIssues({
1873
2067
  entryType: args.entryType,
1874
2068
  types: args.types,
@@ -1883,6 +2077,10 @@ function validateFieldAppendItem(args) {
1883
2077
  return result.output;
1884
2078
  }
1885
2079
 
2080
+ function appendAssignmentEntryType(entryType) {
2081
+ return entryType === "assignees" ? "assignee" : entryType === "array" ? "object" : entryType;
2082
+ }
2083
+
1886
2084
  function formatIssues(issues, formatMessage = issue => issue.message) {
1887
2085
  const formatOne = (issue, prefix) => {
1888
2086
  const keys = [ ...prefix, ...issue.path?.map(p => p.key) ?? [] ], sub = issue.issues?.filter(candidate => candidate.expected !== "null");
@@ -1975,6 +2173,10 @@ function opSchemas(targetSchema) {
1975
2173
  type: v__namespace.literal("field.set"),
1976
2174
  target: targetSchema,
1977
2175
  value: ValueExprSchema
2176
+ }), v__namespace.strictObject({
2177
+ type: v__namespace.literal("field.setIfMissing"),
2178
+ target: targetSchema,
2179
+ value: ValueExprSchema
1978
2180
  }), v__namespace.strictObject({
1979
2181
  type: v__namespace.literal("field.unset"),
1980
2182
  target: targetSchema
@@ -1982,6 +2184,14 @@ function opSchemas(targetSchema) {
1982
2184
  type: v__namespace.literal("field.append"),
1983
2185
  target: targetSchema,
1984
2186
  value: ValueExprSchema
2187
+ }), v__namespace.strictObject({
2188
+ type: v__namespace.literal("field.inc"),
2189
+ target: targetSchema,
2190
+ value: v__namespace.optional(ValueExprSchema)
2191
+ }), v__namespace.strictObject({
2192
+ type: v__namespace.literal("field.dec"),
2193
+ target: targetSchema,
2194
+ value: v__namespace.optional(ValueExprSchema)
1985
2195
  }), v__namespace.strictObject({
1986
2196
  type: v__namespace.literal("field.updateWhere"),
1987
2197
  target: targetSchema,
@@ -2063,16 +2273,17 @@ function compositeChecked(entries) {
2063
2273
  return v__namespace.pipe(v__namespace.strictObject(entries), v__namespace.check(input => compositeShapeOk(input), issue => compositeShapeMessage(issue.input)), v__namespace.check(input => duplicateSubfieldName(input) === void 0, issue => `duplicate sub-field name "${duplicateSubfieldName(issue.input)}" — sub-field names must be unique within \`fields\` / \`of\``));
2064
2274
  }
2065
2275
 
2066
- const FieldShapeSchema = v__namespace.lazy(() => v__namespace.pipe(compositeChecked({
2276
+ const AssignmentRolesSchema = v__namespace.pipe(v__namespace.array(NonEmpty), v__namespace.minLength(1, "declare at least one eligible role, or omit `roles`")), FieldShapeSchema = v__namespace.lazy(() => v__namespace.pipe(compositeChecked({
2067
2277
  type: FieldValueKindSchema,
2068
2278
  name: FieldEntryName,
2069
2279
  title: v__namespace.optional(v__namespace.string()),
2070
2280
  description: v__namespace.optional(v__namespace.string()),
2071
2281
  options: v__namespace.optional(ChoiceOptionsSchema),
2072
2282
  validation: v__namespace.optional(ScalarValidationSchema),
2283
+ roles: v__namespace.optional(AssignmentRolesSchema),
2073
2284
  fields: v__namespace.optional(v__namespace.array(FieldShapeSchema)),
2074
2285
  of: v__namespace.optional(v__namespace.array(FieldShapeSchema))
2075
- }), choiceOptionsCheck(), scalarValidationCheck())), StoredEditableSchema = v__namespace.union([ v__namespace.literal(!0), NonEmpty ]), AuthoringEditableSchema = v__namespace.union([ v__namespace.literal(!0), v__namespace.array(NonEmpty), NonEmpty ]);
2286
+ }), choiceOptionsCheck(), scalarValidationCheck(), assignmentRolesCheck())), StoredEditableSchema = v__namespace.union([ v__namespace.literal(!0), NonEmpty ]), AuthoringEditableSchema = v__namespace.union([ v__namespace.literal(!0), v__namespace.array(NonEmpty), NonEmpty ]);
2076
2287
 
2077
2288
  function fieldBase(editable, group) {
2078
2289
  return {
@@ -2093,6 +2304,7 @@ function fieldEntryFields({editable: editable, group: group, kind: kind = FieldK
2093
2304
  options: v__namespace.optional(ChoiceOptionsSchema),
2094
2305
  validation: v__namespace.optional(ScalarValidationSchema),
2095
2306
  types: v__namespace.optional(v__namespace.pipe(v__namespace.array(NonEmpty), v__namespace.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
2307
+ roles: v__namespace.optional(AssignmentRolesSchema),
2096
2308
  fields: v__namespace.optional(v__namespace.array(FieldShapeSchema)),
2097
2309
  of: v__namespace.optional(v__namespace.array(FieldShapeSchema))
2098
2310
  };
@@ -2106,7 +2318,8 @@ function literalSeedIssues(entry) {
2106
2318
  fields: entry.fields,
2107
2319
  of: entry.of,
2108
2320
  options: entry.options,
2109
- validation: entry.validation
2321
+ validation: entry.validation,
2322
+ roles: entry.roles
2110
2323
  });
2111
2324
  }
2112
2325
 
@@ -2118,6 +2331,10 @@ function refTypesCheck() {
2118
2331
  return v__namespace.check(entry => entry.types === void 0 || refKindAcceptsTypes(entry.type), issue => `\`types\` is only valid on \`doc.ref\` / \`doc.refs\` / \`subject\` entries, not "${issue.input.type}"`);
2119
2332
  }
2120
2333
 
2334
+ function assignmentRolesCheck() {
2335
+ return v__namespace.check(entry => entry.roles === void 0 || assignmentKindAcceptsRoles(entry.type), issue => `\`roles\` is only valid on \`assignee\` / \`assignees\` entries, not "${issue.input.type}"`);
2336
+ }
2337
+
2121
2338
  function choiceOptionsCheck() {
2122
2339
  return v__namespace.check(entry => checkChoiceList({
2123
2340
  entryType: entry.type,
@@ -2152,11 +2369,11 @@ function scalarValidationDeclarationIssues(entry) {
2152
2369
  const FieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields({
2153
2370
  editable: StoredEditableSchema,
2154
2371
  group: StoredGroupMembershipSchema
2155
- })), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields({
2372
+ })), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v__namespace.pipe(compositeChecked(fieldEntryFields({
2156
2373
  editable: AuthoringEditableSchema,
2157
2374
  group: AuthoringGroupMembershipSchema,
2158
2375
  kind: AuthoringRawFieldKindSchema
2159
- })), refTypesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v__namespace.strictObject({
2376
+ })), refTypesCheck(), assignmentRolesCheck(), choiceOptionsCheck(), scalarValidationCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v__namespace.strictObject({
2160
2377
  type: v__namespace.literal("claim"),
2161
2378
  name: FieldEntryName,
2162
2379
  title: v__namespace.optional(v__namespace.string()),
@@ -2198,17 +2415,27 @@ const TodoListFieldSchema = pinned()(v__namespace.strictObject(listSugarFields("
2198
2415
  required: v__namespace.optional(v__namespace.boolean()),
2199
2416
  options: v__namespace.optional(ChoiceOptionsSchema),
2200
2417
  validation: v__namespace.optional(ScalarValidationSchema)
2201
- }), choiceOptionsCheck(), scalarValidationCheck());
2418
+ }), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v__namespace.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v__namespace.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v__namespace.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
2419
+
2420
+ function semanticNamespace(semantic) {
2421
+ return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
2422
+ }
2202
2423
 
2203
2424
  function hasUniqueSemanticNamespaces(semantics) {
2204
- const namespaces = semantics.map(semantic => semantic.split(".", 1)[0]);
2425
+ const namespaces = semantics.map(semanticNamespace);
2205
2426
  return new Set(namespaces).size === namespaces.length;
2206
2427
  }
2207
2428
 
2429
+ function semanticsFieldSchema(semantic) {
2430
+ return v__namespace.optional(v__namespace.pipe(v__namespace.array(semantic), v__namespace.minLength(1, "declare at least one semantic, or omit `semantics`"), v__namespace.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
2431
+ }
2432
+
2433
+ const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
2434
+
2208
2435
  function actionFields(op, group) {
2209
2436
  return {
2210
2437
  name: NonEmpty,
2211
- semantics: v__namespace.optional(v__namespace.pipe(v__namespace.array(picklist(ACTION_SEMANTICS)), v__namespace.minLength(1, "declare at least one semantic, or omit `semantics`"), v__namespace.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace"))),
2438
+ semantics: ActionSemanticsFieldSchema,
2212
2439
  title: v__namespace.optional(v__namespace.string()),
2213
2440
  description: v__namespace.optional(v__namespace.string()),
2214
2441
  group: v__namespace.optional(group),
@@ -2255,6 +2482,7 @@ const StoredActionSchema = pinned()(v__namespace.strictObject({
2255
2482
  function activityFields({field: field, action: action, target: target, group: group}) {
2256
2483
  return {
2257
2484
  name: NonEmpty,
2485
+ semantics: SemanticsFieldSchema,
2258
2486
  title: v__namespace.optional(v__namespace.string()),
2259
2487
  description: v__namespace.optional(v__namespace.string()),
2260
2488
  groups: v__namespace.optional(v__namespace.array(GroupSchema)),
@@ -2328,6 +2556,7 @@ const GuardSchema = v__namespace.strictObject(guardFields(NonEmpty)), AuthoringG
2328
2556
  function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
2329
2557
  return {
2330
2558
  name: NonEmpty,
2559
+ semantics: SemanticsFieldSchema,
2331
2560
  title: v__namespace.optional(v__namespace.string()),
2332
2561
  description: v__namespace.optional(v__namespace.string()),
2333
2562
  groups: v__namespace.optional(v__namespace.array(GroupSchema)),
@@ -2366,6 +2595,7 @@ const StoredStartSchema = pinned()(v__namespace.strictObject(startFields(picklis
2366
2595
  function workflowFields({field: field, stage: stage, start: start}) {
2367
2596
  return {
2368
2597
  name: NonEmpty,
2598
+ semantics: SemanticsFieldSchema,
2369
2599
  title: NonEmpty,
2370
2600
  description: v__namespace.optional(v__namespace.string()),
2371
2601
  groups: v__namespace.optional(v__namespace.array(GroupSchema)),
@@ -2562,11 +2792,20 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
2562
2792
  });
2563
2793
  }
2564
2794
 
2795
+ function actionSites(def) {
2796
+ return def.stages.flatMap((stage, i) => (stage.activities ?? []).flatMap((activity, j) => (activity.actions ?? []).map((action, a) => ({
2797
+ action: action,
2798
+ activity: activity,
2799
+ stage: stage,
2800
+ path: [ "stages", i, "activities", j, "actions", a ]
2801
+ }))));
2802
+ }
2803
+
2565
2804
  function effectNameSites(def) {
2566
2805
  const sites = [];
2567
- 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({
2806
+ for (const {action: action, path: path} of actionSites(def)) collectEffects({
2568
2807
  effects: action.effects,
2569
- path: [ "stages", i, "activities", j, "actions", a, "effects" ],
2808
+ path: [ ...path, "effects" ],
2570
2809
  sites: sites
2571
2810
  });
2572
2811
  return sites;
@@ -2697,13 +2936,22 @@ function checkUnboundConditionVars(def, issues) {
2697
2936
  }
2698
2937
  }
2699
2938
 
2939
+ const SOFT_GATE_VARS = {
2940
+ can: {
2941
+ noun: "the caller's grants"
2942
+ },
2943
+ attributes: {
2944
+ noun: "the caller's org-level user attributes"
2945
+ }
2946
+ }, SOFT_GATE_VAR_NAMES = Object.keys(SOFT_GATE_VARS);
2947
+
2700
2948
  function unboundVarsAt(site) {
2701
2949
  const callerVars = unboundCallerVars(site.policy);
2702
2950
  return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
2703
2951
  }
2704
2952
 
2705
2953
  function unboundCallerVars(policy) {
2706
- return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
2954
+ return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ ...SOFT_GATE_VAR_NAMES, "params" ] : SOFT_GATE_VAR_NAMES;
2707
2955
  }
2708
2956
 
2709
2957
  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";
@@ -2713,7 +2961,12 @@ function rowVarMessage(site) {
2713
2961
  }
2714
2962
 
2715
2963
  function callerVarMessage(site, name) {
2716
- 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`;
2964
+ 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\``;
2965
+ 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`;
2966
+ 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`;
2967
+ const softGate = SOFT_GATE_VARS[name];
2968
+ 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}`;
2969
+ throw new Error(`callerVarMessage: unreachable for $${name} at ${site.label} (policy ${site.policy})`);
2717
2970
  }
2718
2971
 
2719
2972
  function conditionSites(def) {
@@ -2968,9 +3221,9 @@ function belowScopeNestedSites(args) {
2968
3221
  }
2969
3222
 
2970
3223
  function checkLevelKindEffectOutputs(def, issues) {
2971
- 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({
3224
+ for (const {action: action, path: path} of actionSites(def)) pushOutputIssues({
2972
3225
  action: action,
2973
- path: [ "stages", i, "activities", j, "actions", a ],
3226
+ path: path,
2974
3227
  issues: issues
2975
3228
  });
2976
3229
  }
@@ -3052,18 +3305,18 @@ function storedRolesIssue(action) {
3052
3305
  }
3053
3306
 
3054
3307
  function checkStoredRolesPlacement(def, issues) {
3055
- for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) {
3308
+ for (const {action: action, path: path} of actionSites(def)) {
3056
3309
  const message = storedRolesIssue(action);
3057
3310
  message !== void 0 && issues.push({
3058
- path: [ "stages", i, "activities", j, "actions", a, "roles" ],
3311
+ path: [ ...path, "roles" ],
3059
3312
  message: message
3060
3313
  });
3061
3314
  }
3062
3315
  }
3063
3316
 
3064
3317
  function checkTriggeredActionParams(def, issues) {
3065
- 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({
3066
- path: [ "stages", i, "activities", j, "actions", a, "params" ],
3318
+ for (const {action: action, path: path} of actionSites(def)) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
3319
+ path: [ ...path, "params" ],
3067
3320
  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`
3068
3321
  });
3069
3322
  }
@@ -3108,13 +3361,20 @@ function checkSingleSubjectRequirements(def, issues) {
3108
3361
 
3109
3362
  function checkStartFilterReads(def, issues) {
3110
3363
  const filter = def.start?.filter;
3111
- filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
3364
+ if (filter === void 0) return;
3365
+ const params = conditionParameterNames(filter);
3366
+ params.has("fields") && issues.push({
3112
3367
  path: [ "start", "filter" ],
3113
3368
  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"
3114
- }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
3369
+ });
3370
+ for (const name of CALLER_BOUND_VARS) params.has(name) && issues.push({
3371
+ path: [ "start", "filter" ],
3372
+ 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`
3373
+ });
3374
+ readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
3115
3375
  path: [ "start", "filter" ],
3116
3376
  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"
3117
- }));
3377
+ });
3118
3378
  }
3119
3379
 
3120
3380
  function checkStartRequirementReads(def, issues) {
@@ -3410,15 +3670,13 @@ function seedEarlierSiblingTarget(args) {
3410
3670
  }
3411
3671
 
3412
3672
  function opSites(def) {
3413
- const sites = [];
3414
- 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({
3673
+ return actionSites(def).map(({action: action, activity: activity, stage: stage, path: path}) => ({
3415
3674
  ops: action.ops,
3416
- path: [ "stages", i, "activities", j, "actions", a, "ops" ],
3675
+ path: [ ...path, "ops" ],
3417
3676
  label: `action "${action.name}"`,
3418
3677
  stage: stage,
3419
3678
  activity: activity
3420
- });
3421
- return sites;
3679
+ }));
3422
3680
  }
3423
3681
 
3424
3682
  function checkFieldReadOpValues(def, issues) {
@@ -3435,7 +3693,7 @@ function checkFieldReadOpValues(def, issues) {
3435
3693
  }
3436
3694
 
3437
3695
  function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
3438
- 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({
3696
+ 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({
3439
3697
  ...ctx,
3440
3698
  read: read,
3441
3699
  path: readPath,
@@ -3486,35 +3744,55 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
3486
3744
  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)"}`;
3487
3745
  }
3488
3746
 
3489
- function checkUpdateWhereOps(def, issues) {
3747
+ function checkFieldTargetOps(def, issues) {
3490
3748
  const workflow = def.fields ?? [];
3491
3749
  for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
3492
- if (op.type !== "field.updateWhere") continue;
3493
- const scopes = {
3494
- workflow: workflow,
3495
- stage: site.stage.fields ?? [],
3496
- activity: site.activity.fields ?? []
3497
- };
3498
- checkUpdateWhereTargetKind({
3750
+ const path = [ ...site.path, o ], rule = TARGET_KIND_RULES[op.type];
3751
+ rule !== void 0 && "target" in op && checkTargetKind({
3499
3752
  op: op,
3500
- path: [ ...site.path, o ],
3753
+ path: [ ...path, "target" ],
3501
3754
  label: site.label,
3502
- scopes: scopes,
3755
+ scopes: opTargetScopes(workflow, site),
3756
+ rule: rule,
3503
3757
  issues: issues
3504
- }), checkUpdateWhereMergeKeys({
3758
+ }), op.type === "field.updateWhere" && checkUpdateWhereMergeKeys({
3505
3759
  op: op,
3506
- path: [ ...site.path, o ],
3760
+ path: path,
3507
3761
  label: site.label,
3508
3762
  issues: issues
3509
3763
  });
3510
3764
  }
3511
3765
  }
3512
3766
 
3513
- function checkUpdateWhereTargetKind({op: op, path: path, label: label, scopes: scopes, issues: issues}) {
3767
+ const arithmeticTargetRule = {
3768
+ accepts: target => target.type === "number",
3769
+ issue: () => "arithmetic ops target `number` entries only"
3770
+ }, TARGET_KIND_RULES = {
3771
+ "field.inc": arithmeticTargetRule,
3772
+ "field.dec": arithmeticTargetRule,
3773
+ "field.setIfMissing": {
3774
+ accepts: target => !isAlwaysArrayFieldKind(target.type),
3775
+ issue: () => "setIfMissing applies to nullable entries only; an empty array entry already holds []"
3776
+ },
3777
+ "field.updateWhere": {
3778
+ accepts: target => target.type === "array",
3779
+ issue: target => "updateWhere merges declared row sub-fields, so its target must be an `array` entry" + rowOpsHint(target.type)
3780
+ }
3781
+ };
3782
+
3783
+ function opTargetScopes(workflow, site) {
3784
+ return {
3785
+ workflow: workflow,
3786
+ stage: site.stage.fields ?? [],
3787
+ activity: site.activity.fields ?? []
3788
+ };
3789
+ }
3790
+
3791
+ function checkTargetKind({op: op, path: path, label: label, scopes: scopes, rule: rule, issues: issues}) {
3514
3792
  const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
3515
- target === void 0 || target.type === "array" || issues.push({
3516
- path: [ ...path, "target" ],
3517
- 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)}`
3793
+ target === void 0 || rule.accepts(target) || issues.push({
3794
+ path: path,
3795
+ message: `${label} ${op.type} targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — ${rule.issue(target)}`
3518
3796
  });
3519
3797
  }
3520
3798
 
@@ -3746,7 +4024,7 @@ function checkWorkflowInvariants(def) {
3746
4024
  }), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
3747
4025
  checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
3748
4026
  checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
3749
- checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
4027
+ checkFieldReadOpValues(def, issues), checkFieldTargetOps(def, issues), checkGuardFieldReads(def, issues),
3750
4028
  checkAssigneesEntries(def, issues), checkDueDateEntries(def, issues), checkSubjectEntries(def, issues),
3751
4029
  checkLevelKindEffectOutputs(def, issues), checkActivityTerminalPaths(def, issues),
3752
4030
  checkTerminalStageActivities(def, issues), checkTriggeredActionParams(def, issues),
@@ -3787,6 +4065,8 @@ exports.CONDITION_VARS = CONDITION_VARS;
3787
4065
 
3788
4066
  exports.ContractViolationError = ContractViolationError;
3789
4067
 
4068
+ exports.DECISION_SEMANTICS = DECISION_SEMANTICS;
4069
+
3790
4070
  exports.DEFAULT_TRANSITION_WHEN = DEFAULT_TRANSITION_WHEN;
3791
4071
 
3792
4072
  exports.DOCUMENT_VALUE_PERMISSIONS = DOCUMENT_VALUE_PERMISSIONS;
@@ -3837,6 +4117,8 @@ exports.RESERVED_CONDITION_VARS = RESERVED_CONDITION_VARS;
3837
4117
 
3838
4118
  exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
3839
4119
 
4120
+ exports.SIGNAL_SEMANTICS = SIGNAL_SEMANTICS;
4121
+
3840
4122
  exports.START_FILTER_VARS = START_FILTER_VARS;
3841
4123
 
3842
4124
  exports.START_REQUIREMENT_VARS = START_REQUIREMENT_VARS;
@@ -3859,6 +4141,8 @@ exports.actorFulfillsRole = actorFulfillsRole;
3859
4141
 
3860
4142
  exports.andConditions = andConditions;
3861
4143
 
4144
+ exports.assignmentKindAcceptsRoles = assignmentKindAcceptsRoles;
4145
+
3862
4146
  exports.checkWorkflowInvariants = checkWorkflowInvariants;
3863
4147
 
3864
4148
  exports.choiceValueIssues = choiceValueIssues;
@@ -3921,6 +4205,10 @@ exports.groq = groq;
3921
4205
 
3922
4206
  exports.groupMembershipNames = groupMembershipNames;
3923
4207
 
4208
+ exports.isAlwaysArrayFieldKind = isAlwaysArrayFieldKind;
4209
+
4210
+ exports.isAssignmentFieldEntry = isAssignmentFieldEntry;
4211
+
3924
4212
  exports.isBareSeedId = isBareSeedId;
3925
4213
 
3926
4214
  exports.isCascadeFired = isCascadeFired;
@@ -3937,6 +4225,8 @@ exports.isNotesEntry = isNotesEntry;
3937
4225
 
3938
4226
  exports.isParseableInstant = isParseableInstant;
3939
4227
 
4228
+ exports.isRecord = isRecord;
4229
+
3940
4230
  exports.isSingleDocRefEntry = isSingleDocRefEntry;
3941
4231
 
3942
4232
  exports.isSingleDocRefKind = isSingleDocRefKind;