@sanity/workflow-engine 0.20.0 → 0.22.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.
@@ -115,12 +115,33 @@ function isUnprimed(instance) {
115
115
 
116
116
  function parseDefinitionSnapshotValue(instance) {
117
117
  try {
118
- return JSON.parse(instance.definitionSnapshot);
118
+ return normalizeLegacyActivityRequirements(JSON.parse(instance.definitionSnapshot));
119
119
  } catch (err) {
120
120
  rethrowWithContext(err, `Failed to parse definitionSnapshot on instance "${instance._id}"`);
121
121
  }
122
122
  }
123
123
 
124
+ function normalizeLegacyActivityRequirements(value) {
125
+ for (const stage of arrayMember(value, "stages")) for (const activity of arrayMember(stage, "activities")) normalizeLegacyRequirementMap(activity);
126
+ return value;
127
+ }
128
+
129
+ function arrayMember(value, key) {
130
+ if (typeof value != "object" || value === null) return [];
131
+ const member = value[key];
132
+ return Array.isArray(member) ? member : [];
133
+ }
134
+
135
+ function normalizeLegacyRequirementMap(value) {
136
+ if (typeof value != "object" || value === null) return;
137
+ const activity = value, requirements = activity.requirements;
138
+ typeof requirements != "object" || requirements === null || Array.isArray(requirements) || (activity.requirements = Object.entries(requirements).map(([name, query]) => ({
139
+ type: "groq",
140
+ name: name,
141
+ query: query
142
+ })));
143
+ }
144
+
124
145
  function parseDefinitionSnapshot(instance) {
125
146
  return parseDefinitionSnapshotValue(instance);
126
147
  }
@@ -129,7 +150,7 @@ function parentRef(instance) {
129
150
  return instance.ancestors.at(-1);
130
151
  }
131
152
 
132
- const DATA_MODEL_VERSION = 3, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
153
+ const DATA_MODEL_VERSION = 4, DATA_MODEL_MIN_READER = 4, READER_MODEL_ROLLOUT_URL = "https://www.sanity.io/docs/editorial-workflows/prerelease";
133
154
 
134
155
  class ReaderModelAcknowledgementError extends WorkflowError {
135
156
  code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
@@ -139,7 +160,7 @@ class ReaderModelAcknowledgementError extends WorkflowError {
139
160
  documentationUrl=READER_MODEL_ROLLOUT_URL;
140
161
  constructor(expectedMinReaderModel, context = "Deployment") {
141
162
  const expected = expectedMinReaderModel === void 0 ? "missing" : String(expectedMinReaderModel);
142
- super("reader-model-acknowledgement", `${context} expected reader floor ${expected}; the installed engine requires acknowledgement ${DATA_MODEL_MIN_READER}. Dependency upgrades can change writer compatibility. Upgrade all readers and Functions before accepting a higher floor; after verifying the rollout, change the literal in deployment configuration. ${READER_MODEL_ROLLOUT_URL}`),
163
+ super("reader-model-acknowledgement", `${context} expected reader floor ${expected}; the installed engine requires acknowledgement ${DATA_MODEL_MIN_READER}. Do not change the acknowledgement yet: accepting ${DATA_MODEL_MIN_READER} authorizes this engine to write documents that older readers will refuse. Upgrade every Studio, CLI, MCP server, Function, and other runtime that reads engine-owned documents; verify that rollout in every environment sharing the workflow resource; then change the literal in deployment configuration and deploy the writer. Rollout guide: ${READER_MODEL_ROLLOUT_URL}`),
143
164
  this.name = "ReaderModelAcknowledgementError", this.expectedMinReaderModel = expectedMinReaderModel;
144
165
  }
145
166
  }
@@ -204,6 +225,22 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
204
225
  compatibility: "additive",
205
226
  applicability: "detectable",
206
227
  summary: "Pending-effect claims carry an exact-claim token gating mid-dispatch state reports."
228
+ }), Object.freeze({
229
+ id: "classified-principal-ids",
230
+ introducedInModel: 4,
231
+ minReaderModel: 4,
232
+ documentTypes: Object.freeze([ "instance" ]),
233
+ compatibility: "reader-floor",
234
+ applicability: "unconditional",
235
+ summary: "Principal ids are namespace-classified: actor and assignee writes carry the account-global user id only, and readers resolve legacy project-scoped ids through the prefix classifier at the instance read funnel."
236
+ }), Object.freeze({
237
+ id: "readiness-requirements",
238
+ introducedInModel: 4,
239
+ minReaderModel: 4,
240
+ documentTypes: Object.freeze([ "definition" ]),
241
+ compatibility: "reader-floor",
242
+ applicability: "detectable",
243
+ summary: "Start and activity readiness use named polymorphic requirement arrays."
207
244
  }) ]);
208
245
 
209
246
  function recordOf(value) {
@@ -264,6 +301,11 @@ function hasClaimTokens(document) {
264
301
  });
265
302
  }
266
303
 
304
+ function hasReadinessRequirements(document) {
305
+ const root = recordOf(document);
306
+ return root === void 0 ? !1 : Array.isArray(recordOf(root.start)?.requirements) ? !0 : recordsAt(root, "stages").some(stage => recordsAt(stage, "activities").some(activity => Array.isArray(activity.requirements)));
307
+ }
308
+
267
309
  const featureDetectors = {
268
310
  "governed-model-stamps": () => !0,
269
311
  "subject-field-kind": document => hasFieldKind(document, "subject"),
@@ -271,7 +313,9 @@ const featureDetectors = {
271
313
  "action-semantics": hasActionSemantics,
272
314
  "inclusive-scalar-bounds": hasScalarValidation,
273
315
  "progress-field-kind": document => hasFieldKind(document, "progress"),
274
- "effect-claim-tokens": hasClaimTokens
316
+ "effect-claim-tokens": hasClaimTokens,
317
+ "classified-principal-ids": () => !0,
318
+ "readiness-requirements": hasReadinessRequirements
275
319
  };
276
320
 
277
321
  function requiredModelFeatures(documentType, document) {
@@ -285,7 +329,7 @@ function requiredReaderModel(documentType, document) {
285
329
  function modelStampFor(args) {
286
330
  return {
287
331
  modelVersion: DATA_MODEL_VERSION,
288
- minReaderModel: Math.max(args.storedMinReaderModel ?? 0, requiredReaderModel(args.documentType, args.document))
332
+ minReaderModel: Math.max(DATA_MODEL_MIN_READER, args.storedMinReaderModel ?? 0, requiredReaderModel(args.documentType, args.document))
289
333
  };
290
334
  }
291
335
 
@@ -814,8 +858,8 @@ function desugarStart(start) {
814
858
  ...start.filter !== void 0 ? {
815
859
  filter: start.filter
816
860
  } : {},
817
- ...start.allowed !== void 0 ? {
818
- allowed: start.allowed
861
+ ...start.requirements !== void 0 ? {
862
+ requirements: start.requirements
819
863
  } : {}
820
864
  };
821
865
  }
@@ -1577,7 +1621,7 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
1577
1621
  binding: "always",
1578
1622
  label: "the spawned subworkflows",
1579
1623
  description: "Every row of the instance's subworkflow registry, faceted by `activity`/`action`/`definition`/`rowKey`/`status` (`'active'|'done'|'aborted'`) with `current` marking the open stage entry's cohort and `stage` the child's current stage. Usable anywhere — transition `when`s, requirements, any stage's gates; the settled gate is `count($subworkflows[activity == <name> && current && status == 'active']) == 0`."
1580
- } ], SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR = "subjectHasInFlightInstance", RESERVED_CONDITION_VARS = [ ...CONDITION_VARS.map(v2 => v2.name), SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR ], FILTER_SCOPE_VARS = CONDITION_VARS.filter(v2 => v2.binding === "always").map(v2 => v2.name), CALLER_BOUND_VARS = CONDITION_VARS.filter(v2 => v2.binding === "caller").map(v2 => v2.name), START_FILTER_VARS = [ {
1624
+ } ], RESERVED_CONDITION_VARS = CONDITION_VARS.map(v2 => v2.name), FILTER_SCOPE_VARS = CONDITION_VARS.filter(v2 => v2.binding === "always").map(v2 => v2.name), CALLER_BOUND_VARS = CONDITION_VARS.filter(v2 => v2.binding === "caller").map(v2 => v2.name), START_FILTER_VARS = [ {
1581
1625
  name: "tag",
1582
1626
  label: "this engine tag",
1583
1627
  description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
@@ -1589,11 +1633,7 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
1589
1633
  name: "now",
1590
1634
  label: "the current time",
1591
1635
  description: "The ISO clock reading of the evaluating engine."
1592
- }, {
1593
- name: SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR,
1594
- label: "the subject already has an in-flight workflow",
1595
- description: "Whether any instance in this engine tag, across all definitions, has the same resource-qualified subject and no `completedAt`. Advisory under concurrent starts."
1596
- } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1636
+ } ], START_REQUIREMENT_VARS = [ ...START_FILTER_VARS, {
1597
1637
  name: "fields",
1598
1638
  label: "the start's input fields",
1599
1639
  description: "The caller's input entries by name (`initialFields` — at `startInstance`, the values the start would seed; at a pre-flight, the values gathered so far, so a read of a not-yet-supplied entry is GROQ null). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble — a singular `doc.ref` or `subject` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
@@ -1778,6 +1818,37 @@ function isNotesEntry(entry) {
1778
1818
  return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
1779
1819
  }
1780
1820
 
1821
+ const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
1822
+
1823
+ function classifyPrincipalId(id) {
1824
+ if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
1825
+ namespace: "sentinel"
1826
+ };
1827
+ if (id.startsWith("g")) return {
1828
+ namespace: "global",
1829
+ globalId: id
1830
+ };
1831
+ if (id.startsWith("p-")) return {
1832
+ namespace: "robot",
1833
+ globalId: id
1834
+ };
1835
+ const embeddedGlobal = E_PREFIXED_PROJECT_ID.exec(id)?.[1];
1836
+ return embeddedGlobal !== void 0 ? embeddedGlobal.startsWith("g") ? {
1837
+ namespace: "project",
1838
+ globalId: embeddedGlobal
1839
+ } : {
1840
+ namespace: "unknown"
1841
+ } : id.startsWith("p") ? {
1842
+ namespace: "project"
1843
+ } : {
1844
+ namespace: "unknown"
1845
+ };
1846
+ }
1847
+
1848
+ function lakePrincipalId(args) {
1849
+ return args.localPrincipalId ?? args.actor.id;
1850
+ }
1851
+
1781
1852
  class FieldValueShapeError extends WorkflowError {
1782
1853
  entryType;
1783
1854
  entryName;
@@ -1818,11 +1889,11 @@ function checkChoiceList(args) {
1818
1889
  const {entryType: entryType, options: options, validation: validation} = args;
1819
1890
  if (options === void 0) return;
1820
1891
  if (!CHOICE_KINDS.has(entryType)) return [ `\`options\` is not valid on "${entryType}" values` ];
1821
- const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => checkValueAgainst({
1892
+ const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => issuesOf(checkValueAgainst({
1822
1893
  entryType: kind,
1823
1894
  value: option.value,
1824
1895
  validation: validation
1825
- }, valueSchemas)?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
1896
+ }, valueSchemas))?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
1826
1897
  for (const option of options.list) seen.has(option.value) && issues.push(`duplicate option value ${JSON.stringify(option.value)}`),
1827
1898
  seen.add(option.value);
1828
1899
  return issues.length === 0 ? void 0 : issues;
@@ -1848,8 +1919,27 @@ const fieldValueSchemas = {
1848
1919
  actor: v__namespace.union([ v__namespace.null(), ActorShape ]),
1849
1920
  assignee: v__namespace.union([ v__namespace.null(), AssigneeShape ]),
1850
1921
  assignees: v__namespace.array(AssigneeShape)
1851
- }, valueSchemas = {
1922
+ }, WritePrincipalId = v__namespace.pipe(NonEmptyString, v__namespace.rawTransform(({dataset: dataset, addIssue: addIssue}) => {
1923
+ const classified = classifyPrincipalId(dataset.value);
1924
+ return classified.namespace === "global" || classified.namespace === "robot" ? dataset.value : classified.namespace === "project" && classified.globalId !== void 0 ? classified.globalId : (addIssue({
1925
+ message: `principal id "${dataset.value}" is not an account-global user id. Workflow user ids are the global \`sanityUserId\` (or a robot token's id). Resolve project members through your surface's member hook, or /projects/<projectId>/users/<id> → sanityUserId.`
1926
+ }), dataset.value);
1927
+ })), ActorWriteShape = tolerantObject()({
1928
+ kind: v__namespace.picklist(ACTOR_KINDS),
1929
+ id: WritePrincipalId,
1930
+ roles: v__namespace.exactOptional(v__namespace.array(v__namespace.string())),
1931
+ onBehalfOf: v__namespace.exactOptional(v__namespace.string())
1932
+ }), AssigneeWriteShape = v__namespace.union([ tolerantObject()({
1933
+ type: v__namespace.literal("user"),
1934
+ id: WritePrincipalId
1935
+ }), tolerantObject()({
1936
+ type: v__namespace.literal("role"),
1937
+ role: NonEmptyString
1938
+ }) ]), valueSchemas = {
1852
1939
  ...fieldValueSchemas,
1940
+ actor: v__namespace.nullable(ActorWriteShape),
1941
+ assignee: v__namespace.nullable(AssigneeWriteShape),
1942
+ assignees: v__namespace.array(AssigneeWriteShape),
1853
1943
  query: v__namespace.any()
1854
1944
  };
1855
1945
 
@@ -1920,7 +2010,7 @@ function wholeValueSchema(args) {
1920
2010
  function appendItemSchema(entryType, shape) {
1921
2011
  if (entryType === "array") return objectSchema(shape.of ?? [], valueSchemas);
1922
2012
  if (entryType === "doc.refs") return GdrShape;
1923
- if (entryType === "assignees") return AssigneeShape;
2013
+ if (entryType === "assignees") return AssigneeWriteShape;
1924
2014
  }
1925
2015
 
1926
2016
  function rejectedRefTypes(args) {
@@ -1945,7 +2035,7 @@ function gdrTypeOf(item) {
1945
2035
  return typeof t == "string" ? t : void 0;
1946
2036
  }
1947
2037
 
1948
- function checkFieldValue(args) {
2038
+ function parseFieldValue(args) {
1949
2039
  return checkValueAgainst(args, valueSchemas);
1950
2040
  }
1951
2041
 
@@ -1955,9 +2045,14 @@ function checkValueAgainst(args, leaf) {
1955
2045
  shape: args,
1956
2046
  leaf: leaf
1957
2047
  });
1958
- if (schema === void 0) return [ `unknown field entry type ${args.entryType}` ];
2048
+ if (schema === void 0) return {
2049
+ issues: [ `unknown field entry type ${args.entryType}` ]
2050
+ };
1959
2051
  const result = v__namespace.safeParse(schema, args.value);
1960
- return result.success ? refTypeIssues({
2052
+ if (!result.success) return {
2053
+ issues: formatIssues(result.issues)
2054
+ };
2055
+ const postIssues = refTypeIssues({
1961
2056
  entryType: args.entryType,
1962
2057
  types: args.types,
1963
2058
  value: args.value
@@ -1965,17 +2060,27 @@ function checkValueAgainst(args, leaf) {
1965
2060
  entryType: args.entryType,
1966
2061
  validation: args.validation,
1967
2062
  value: args.value
1968
- }) : formatIssues(result.issues);
2063
+ });
2064
+ return postIssues !== void 0 ? {
2065
+ issues: postIssues
2066
+ } : {
2067
+ output: result.output
2068
+ };
2069
+ }
2070
+
2071
+ function issuesOf(check) {
2072
+ return "issues" in check ? check.issues : void 0;
1969
2073
  }
1970
2074
 
1971
2075
  function validateFieldValue(args) {
1972
- const issues = checkFieldValue(args);
1973
- if (issues !== void 0) throw new FieldValueShapeError({
2076
+ const check = checkValueAgainst(args, valueSchemas);
2077
+ if ("issues" in check) throw new FieldValueShapeError({
1974
2078
  entryType: args.entryType,
1975
2079
  entryName: args.entryName,
1976
- issues: issues,
2080
+ issues: check.issues,
1977
2081
  mode: "value"
1978
2082
  });
2083
+ return check.output;
1979
2084
  }
1980
2085
 
1981
2086
  const BARE_ID_SOURCE = "[A-Za-z0-9_][A-Za-z0-9._-]*", BARE_ID_RE = new RegExp(`^${BARE_ID_SOURCE}$`), ALIAS_REF_RE = new RegExp(`^@${RESOURCE_ALIAS_NAME_SOURCE}:${BARE_ID_SOURCE}$`);
@@ -2004,7 +2109,7 @@ const AuthoringRefId = v__namespace.pipe(v__namespace.string(), v__namespace.che
2004
2109
  };
2005
2110
 
2006
2111
  function checkLiteralSeed(args) {
2007
- return args.value === null ? [ "a literal seed cannot be null — omit `initialValue` to start the field empty" ] : checkValueAgainst(args, seedValueSchemas);
2112
+ return args.value === null ? [ "a literal seed cannot be null — omit `initialValue` to start the field empty" ] : issuesOf(checkValueAgainst(args, seedValueSchemas));
2008
2113
  }
2009
2114
 
2010
2115
  function validateFieldAppendItem(args) {
@@ -2033,13 +2138,15 @@ function validateFieldAppendItem(args) {
2033
2138
  issues: typeIssues,
2034
2139
  mode: "item"
2035
2140
  });
2141
+ return result.output;
2036
2142
  }
2037
2143
 
2038
2144
  function formatIssues(issues, formatMessage = issue => issue.message) {
2039
- return issues.map(i => {
2040
- const keys = i.path?.map(p => p.key) ?? [];
2041
- return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(i)}`;
2042
- });
2145
+ const formatOne = (issue, prefix) => {
2146
+ const keys = [ ...prefix, ...issue.path?.map(p => p.key) ?? [] ], sub = issue.issues?.filter(candidate => candidate.expected !== "null");
2147
+ return sub !== void 0 && sub.length > 0 ? sub.flatMap(candidate => formatOne(candidate, keys)) : [ `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(issue)}` ];
2148
+ };
2149
+ return issues.flatMap(issue => formatOne(issue, []));
2043
2150
  }
2044
2151
 
2045
2152
  const NonEmpty = NonEmptyString, PositiveInt = v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -2390,7 +2497,18 @@ const StoredActionSchema = pinned()(v__namespace.strictObject({
2390
2497
  filter: v__namespace.optional(ConditionSchema),
2391
2498
  params: v__namespace.optional(v__namespace.array(ActionParamSchema)),
2392
2499
  effects: v__namespace.optional(v__namespace.array(EffectSchema))
2393
- })), AuthoringActionSchema = pinned()(v__namespace.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema));
2500
+ })), AuthoringActionSchema = pinned()(v__namespace.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema)), requirementBase = {
2501
+ name: NonEmpty,
2502
+ title: v__namespace.optional(v__namespace.string()),
2503
+ description: v__namespace.optional(v__namespace.string())
2504
+ }, GroqRequirementSchemaRaw = v__namespace.strictObject({
2505
+ ...requirementBase,
2506
+ type: v__namespace.literal("groq"),
2507
+ query: ConditionSchema
2508
+ }), SingleSubjectRequirementSchemaRaw = v__namespace.strictObject({
2509
+ ...requirementBase,
2510
+ type: v__namespace.literal("singleSubject")
2511
+ }), GroqRequirementSchema = pinned()(GroqRequirementSchemaRaw), StartRequirementSchema = pinned()(v__namespace.variant("type", [ GroqRequirementSchemaRaw, SingleSubjectRequirementSchemaRaw ]));
2394
2512
 
2395
2513
  function activityFields({field: field, action: action, target: target, group: group}) {
2396
2514
  return {
@@ -2401,7 +2519,7 @@ function activityFields({field: field, action: action, target: target, group: gr
2401
2519
  group: v__namespace.optional(group),
2402
2520
  target: v__namespace.optional(target),
2403
2521
  filter: v__namespace.optional(ConditionSchema),
2404
- requirements: v__namespace.optional(v__namespace.record(NonEmpty, ConditionSchema)),
2522
+ requirements: v__namespace.optional(v__namespace.array(GroqRequirementSchema)),
2405
2523
  actions: v__namespace.optional(v__namespace.array(action)),
2406
2524
  fields: v__namespace.optional(v__namespace.array(field))
2407
2525
  };
@@ -2497,7 +2615,7 @@ function startFields(kind) {
2497
2615
  return {
2498
2616
  kind: kind,
2499
2617
  filter: v__namespace.optional(ConditionSchema),
2500
- allowed: v__namespace.optional(ConditionSchema)
2618
+ requirements: v__namespace.optional(v__namespace.array(StartRequirementSchema))
2501
2619
  };
2502
2620
  }
2503
2621
 
@@ -2636,6 +2754,13 @@ function checkActivities({def: def, i: i, activityNames: activityNames, issues:
2636
2754
  })),
2637
2755
  what: `action name in activity "${activity.name}"`,
2638
2756
  issues: issues
2757
+ }), checkDuplicates({
2758
+ names: (activity.requirements ?? []).map((requirement, r) => ({
2759
+ name: requirement.name,
2760
+ path: [ ...path, "requirements", r, "name" ]
2761
+ })),
2762
+ what: `requirement name in activity "${activity.name}"`,
2763
+ issues: issues
2639
2764
  }), checkStatusSetTargets({
2640
2765
  activity: activity,
2641
2766
  activityNames: activityNames,
@@ -2944,10 +3069,10 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
2944
3069
  policy: "cascade",
2945
3070
  fields: fields
2946
3071
  });
2947
- for (const [name, groq2] of Object.entries(activity.requirements ?? {})) sites.push({
2948
- groq: groq2,
2949
- path: [ ...path, "requirements", name ],
2950
- label: `activity "${activity.name}" requirement "${name}"`,
3072
+ for (const [index, requirement] of (activity.requirements ?? []).entries()) sites.push({
3073
+ groq: requirement.query,
3074
+ path: [ ...path, "requirements", index, "query" ],
3075
+ label: `activity "${activity.name}" requirement "${requirement.name}"`,
2951
3076
  policy: "caller-bound",
2952
3077
  fields: fields
2953
3078
  });
@@ -3143,73 +3268,83 @@ function checkTriggeredActionParams(def, issues) {
3143
3268
  }
3144
3269
 
3145
3270
  function checkStart(def, issues) {
3146
- if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
3271
+ if (def.start !== void 0 && (checkDuplicates({
3272
+ names: (def.start.requirements ?? []).map((requirement, index) => ({
3273
+ name: requirement.name,
3274
+ path: [ "start", "requirements", index, "name" ]
3275
+ })),
3276
+ what: "start requirement name",
3277
+ issues: issues
3278
+ }), def.lifecycle === "child" && issues.push({
3147
3279
  path: [ "start" ],
3148
3280
  message: "a spawn-only (lifecycle 'child') definition declares `start` — children are instantiated by a parent's `spawn`, never started standalone, so the block would never apply. Remove `start`, or drop `lifecycle: 'child'`"
3149
- }), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), checkStartSubjectVariable(def, issues),
3150
- def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
3281
+ }), checkStartFilterReads(def, issues), checkStartRequirementReads(def, issues),
3282
+ checkSingleSubjectRequirements(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
3151
3283
  path: [ "fields", n, "required" ],
3152
3284
  message: `start.kind 'autonomous' means runs are initiated by a system reacting to a document, so every required input must be derivable from that triggering document — required entry "${entry.name}" (kind "${entry.type}") is not the workflow's subject. Make it optional, seed it another way (query/literal), or declare it the \`subject\` entry (the document the run is about)`
3153
3285
  });
3154
3286
  }
3155
3287
 
3156
- function checkStartSubjectVariable(def, issues) {
3288
+ function checkSingleSubjectRequirements(def, issues) {
3289
+ const indexes = (def.start?.requirements ?? []).map((requirement, index) => ({
3290
+ requirement: requirement,
3291
+ index: index
3292
+ })).filter(({requirement: requirement}) => requirement.type === "singleSubject"), [first] = indexes;
3293
+ if (first === void 0) return;
3157
3294
  const subject = (def.fields ?? []).find(isSubjectEntry);
3158
- for (const [key, condition] of [ [ "filter", def.start?.filter ], [ "allowed", def.start?.allowed ] ]) if (condition !== void 0 && conditionParameterNames(condition).has(SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR)) {
3159
- if (subject === void 0) {
3160
- issues.push({
3161
- path: [ "start", key ],
3162
- message: `start.${key} reads $${SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR}, but the definition declares no \`subject\` entry — the engine has no prospective subject identity to match. Declare a \`subject\` entry (the document the workflow is about), or remove the variable`
3163
- });
3164
- continue;
3165
- }
3166
- key === "allowed" && !isInputSourced(subject) && issues.push({
3167
- path: [ "start", key ],
3168
- message: `start.allowed reads $${SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR}, but the definition's subject entry "${subject.name}" is not \`input\`-sourced — the start gate cannot bind its prospective subject. Make the subject input-sourced, or remove the variable`
3295
+ if (subject === void 0) {
3296
+ issues.push({
3297
+ path: [ "start", "requirements", first.index ],
3298
+ message: "a singleSubject start requirement caps the definition at one in-flight run per subject, but the definition declares no `subject` entry — there is no subject identity to dedupe on, so the rule could never apply. Declare a `subject` entry (the document the workflow is about), or remove the requirement"
3169
3299
  });
3300
+ return;
3170
3301
  }
3302
+ isInputSourced(subject) || issues.push({
3303
+ path: [ "start", "requirements", first.index ],
3304
+ message: `a singleSubject start requirement matches the prospective subject against in-flight runs, but the definition's subject entry "${subject.name}" is not \`input\`-sourced — the start gate cannot bind a prospective subject. Make the subject input-sourced, or remove the requirement`
3305
+ });
3171
3306
  }
3172
3307
 
3173
3308
  function checkStartFilterReads(def, issues) {
3174
3309
  const filter = def.start?.filter;
3175
3310
  filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
3176
3311
  path: [ "start", "filter" ],
3177
- 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 start.allowed, the start-time permission predicate that binds $fields and is enforced by startInstance"
3312
+ 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"
3178
3313
  }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
3179
3314
  path: [ "start", "filter" ],
3180
3315
  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"
3181
3316
  }));
3182
3317
  }
3183
3318
 
3184
- function checkStartAllowedReads(def, issues) {
3185
- const allowed = def.start?.allowed;
3186
- if (allowed === void 0) return;
3319
+ function checkStartRequirementReads(def, issues) {
3187
3320
  const bindable = new Map((def.fields ?? []).filter(isInputSourced).map(entry => [ entry.name, entry ])), declared = new Set((def.fields ?? []).map(entry => entry.name)), nullVerdict = `so the read is GROQ null and null semantics decide the verdict (refuse-all or vacuously-allow, by the expression's shape), never the caller's real input. Bindable (input) fields: ${knownList(bindable.keys())}`;
3188
- for (const read of conditionFieldReads(allowed)) {
3189
- const entry = bindable.get(read.name);
3190
- if (entry === void 0) {
3191
- issues.push({
3192
- path: [ "start", "allowed" ],
3193
- message: declared.has(read.name) ? `start.allowed reads $fields.${read.name}, but "${read.name}" is not an \`input\`-sourced entry — $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after the start gate), ${nullVerdict}` : `start.allowed reads $fields.${read.name}, but no workflow-scope field entry named "${read.name}" is declared — $fields binds only the caller's input entries, ${nullVerdict}`
3321
+ for (const [index, requirement] of (def.start?.requirements ?? []).entries()) if (requirement.type === "groq") {
3322
+ for (const read of conditionFieldReads(requirement.query)) {
3323
+ const entry = bindable.get(read.name);
3324
+ if (entry === void 0) {
3325
+ issues.push({
3326
+ path: [ "start", "requirements", index, "query" ],
3327
+ message: declared.has(read.name) ? `start requirement reads $fields.${read.name}, but "${read.name}" is not an \`input\`-sourced entry — $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after the start gate), ${nullVerdict}` : `start requirement reads $fields.${read.name}, but no workflow-scope field entry named "${read.name}" is declared — $fields binds only the caller's input entries, ${nullVerdict}`
3328
+ });
3329
+ continue;
3330
+ }
3331
+ pushFieldReadPathIssue({
3332
+ where: "start requirement",
3333
+ read: {
3334
+ field: read.name,
3335
+ path: read.path
3336
+ },
3337
+ target: entry,
3338
+ path: [ "start", "requirements", index, "query" ],
3339
+ issues: issues,
3340
+ nodes: START_REQUIREMENT_VALUE_NODES
3194
3341
  });
3195
- continue;
3196
3342
  }
3197
- pushFieldReadPathIssue({
3198
- where: "start.allowed",
3199
- read: {
3200
- field: read.name,
3201
- path: read.path
3202
- },
3203
- target: entry,
3204
- path: [ "start", "allowed" ],
3205
- issues: issues,
3206
- nodes: START_ALLOWED_VALUE_NODES
3343
+ readsRootDocument(requirement.query) && issues.push({
3344
+ path: [ "start", "requirements", index, "query" ],
3345
+ message: "start requirement reads the candidate document as its root, but no root ever binds in the start-requirement context — startInstance holds inputs, not a loaded document, so the read is GROQ null and null semantics decide the verdict, never the workflow's real state. Read the subject as $fields.<entry> (its GDR URI is $fields.<entry>.id); a per-document visibility rule belongs in start.filter"
3207
3346
  });
3208
3347
  }
3209
- readsRootDocument(allowed) && issues.push({
3210
- path: [ "start", "allowed" ],
3211
- message: "start.allowed reads the candidate document as its root, but no root ever binds in the start-allowed context — startInstance holds inputs, not a loaded document, so the read is GROQ null and null semantics decide the verdict, never the workflow's real state. Read the subject as $fields.<entry> (its GDR URI is $fields.<entry>.id); a per-document visibility rule belongs in start.filter"
3212
- });
3213
3348
  }
3214
3349
 
3215
3350
  function checkGroups(def, issues) {
@@ -3734,7 +3869,7 @@ const SCALAR = {
3734
3869
  kind: "rows",
3735
3870
  of: shape.of ?? []
3736
3871
  })
3737
- }, START_ALLOWED_VALUE_NODES = {
3872
+ }, START_REQUIREMENT_VALUE_NODES = {
3738
3873
  ...VALUE_NODES,
3739
3874
  "doc.ref": () => GDR_VALUE,
3740
3875
  subject: () => GDR_VALUE
@@ -3823,6 +3958,8 @@ exports.ACTIVITY_STATUSES = ACTIVITY_STATUSES;
3823
3958
 
3824
3959
  exports.ACTOR_KINDS = ACTOR_KINDS;
3825
3960
 
3961
+ exports.ANONYMOUS_IDENTITY = ANONYMOUS_IDENTITY;
3962
+
3826
3963
  exports.ActorShape = ActorShape;
3827
3964
 
3828
3965
  exports.AuthoringActionSchema = AuthoringActionSchema;
@@ -3909,11 +4046,11 @@ exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
3909
4046
 
3910
4047
  exports.ReaderModelAcknowledgementError = ReaderModelAcknowledgementError;
3911
4048
 
3912
- exports.START_ALLOWED_VARS = START_ALLOWED_VARS;
3913
-
3914
4049
  exports.START_FILTER_VARS = START_FILTER_VARS;
3915
4050
 
3916
- exports.SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR = SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR;
4051
+ exports.START_REQUIREMENT_VARS = START_REQUIREMENT_VARS;
4052
+
4053
+ exports.SYSTEM_IDENTITY = SYSTEM_IDENTITY;
3917
4054
 
3918
4055
  exports.SpawnContractsInvalidError = SpawnContractsInvalidError;
3919
4056
 
@@ -3937,12 +4074,12 @@ exports.assertReadableModel = assertReadableModel;
3937
4074
 
3938
4075
  exports.assertReaderModelAcknowledgement = assertReaderModelAcknowledgement;
3939
4076
 
3940
- exports.checkFieldValue = checkFieldValue;
3941
-
3942
4077
  exports.checkWorkflowInvariants = checkWorkflowInvariants;
3943
4078
 
3944
4079
  exports.choiceValueIssues = choiceValueIssues;
3945
4080
 
4081
+ exports.classifyPrincipalId = classifyPrincipalId;
4082
+
3946
4083
  exports.clientConfigFromResource = clientConfigFromResource;
3947
4084
 
3948
4085
  exports.conditionEffectReads = conditionEffectReads;
@@ -4033,6 +4170,8 @@ exports.isUnprimed = isUnprimed;
4033
4170
 
4034
4171
  exports.labelFor = labelFor;
4035
4172
 
4173
+ exports.lakePrincipalId = lakePrincipalId;
4174
+
4036
4175
  exports.minReaderModelOf = minReaderModelOf;
4037
4176
 
4038
4177
  exports.modelStampFor = modelStampFor;
@@ -4043,6 +4182,8 @@ exports.parentRef = parentRef;
4043
4182
 
4044
4183
  exports.parseDefinitionSnapshot = parseDefinitionSnapshot;
4045
4184
 
4185
+ exports.parseFieldValue = parseFieldValue;
4186
+
4046
4187
  exports.parseGdr = parseGdr;
4047
4188
 
4048
4189
  exports.parseOrThrow = parseOrThrow;