@sanity/workflow-engine 0.20.0 → 0.21.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.
@@ -101,12 +101,33 @@ function isUnprimed(instance) {
101
101
 
102
102
  function parseDefinitionSnapshotValue(instance) {
103
103
  try {
104
- return JSON.parse(instance.definitionSnapshot);
104
+ return normalizeLegacyActivityRequirements(JSON.parse(instance.definitionSnapshot));
105
105
  } catch (err) {
106
106
  rethrowWithContext(err, `Failed to parse definitionSnapshot on instance "${instance._id}"`);
107
107
  }
108
108
  }
109
109
 
110
+ function normalizeLegacyActivityRequirements(value) {
111
+ for (const stage of arrayMember(value, "stages")) for (const activity of arrayMember(stage, "activities")) normalizeLegacyRequirementMap(activity);
112
+ return value;
113
+ }
114
+
115
+ function arrayMember(value, key) {
116
+ if (typeof value != "object" || value === null) return [];
117
+ const member = value[key];
118
+ return Array.isArray(member) ? member : [];
119
+ }
120
+
121
+ function normalizeLegacyRequirementMap(value) {
122
+ if (typeof value != "object" || value === null) return;
123
+ const activity = value, requirements = activity.requirements;
124
+ typeof requirements != "object" || requirements === null || Array.isArray(requirements) || (activity.requirements = Object.entries(requirements).map(([name, query]) => ({
125
+ type: "groq",
126
+ name: name,
127
+ query: query
128
+ })));
129
+ }
130
+
110
131
  function parseDefinitionSnapshot(instance) {
111
132
  return parseDefinitionSnapshotValue(instance);
112
133
  }
@@ -115,7 +136,7 @@ function parentRef(instance) {
115
136
  return instance.ancestors.at(-1);
116
137
  }
117
138
 
118
- 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";
139
+ const DATA_MODEL_VERSION = 4, DATA_MODEL_MIN_READER = 4, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
119
140
 
120
141
  class ReaderModelAcknowledgementError extends WorkflowError {
121
142
  code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
@@ -125,7 +146,7 @@ class ReaderModelAcknowledgementError extends WorkflowError {
125
146
  documentationUrl=READER_MODEL_ROLLOUT_URL;
126
147
  constructor(expectedMinReaderModel, context = "Deployment") {
127
148
  const expected = expectedMinReaderModel === void 0 ? "missing" : String(expectedMinReaderModel);
128
- 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}`),
149
+ 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. Instances written before the new floor may stop matching user-identity queries, and older readers refuse documents this engine touches — a prerelease deployment with no data worth migrating can reset every engine-owned document with \`sanity-workflows nuke\` and start fresh. ${READER_MODEL_ROLLOUT_URL}`),
129
150
  this.name = "ReaderModelAcknowledgementError", this.expectedMinReaderModel = expectedMinReaderModel;
130
151
  }
131
152
  }
@@ -190,6 +211,22 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
190
211
  compatibility: "additive",
191
212
  applicability: "detectable",
192
213
  summary: "Pending-effect claims carry an exact-claim token gating mid-dispatch state reports."
214
+ }), Object.freeze({
215
+ id: "classified-principal-ids",
216
+ introducedInModel: 4,
217
+ minReaderModel: 4,
218
+ documentTypes: Object.freeze([ "instance" ]),
219
+ compatibility: "reader-floor",
220
+ applicability: "unconditional",
221
+ 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."
222
+ }), Object.freeze({
223
+ id: "readiness-requirements",
224
+ introducedInModel: 4,
225
+ minReaderModel: 4,
226
+ documentTypes: Object.freeze([ "definition" ]),
227
+ compatibility: "reader-floor",
228
+ applicability: "detectable",
229
+ summary: "Start and activity readiness use named polymorphic requirement arrays."
193
230
  }) ]);
194
231
 
195
232
  function recordOf(value) {
@@ -250,6 +287,11 @@ function hasClaimTokens(document) {
250
287
  });
251
288
  }
252
289
 
290
+ function hasReadinessRequirements(document) {
291
+ const root = recordOf(document);
292
+ 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)));
293
+ }
294
+
253
295
  const featureDetectors = {
254
296
  "governed-model-stamps": () => !0,
255
297
  "subject-field-kind": document => hasFieldKind(document, "subject"),
@@ -257,7 +299,9 @@ const featureDetectors = {
257
299
  "action-semantics": hasActionSemantics,
258
300
  "inclusive-scalar-bounds": hasScalarValidation,
259
301
  "progress-field-kind": document => hasFieldKind(document, "progress"),
260
- "effect-claim-tokens": hasClaimTokens
302
+ "effect-claim-tokens": hasClaimTokens,
303
+ "classified-principal-ids": () => !0,
304
+ "readiness-requirements": hasReadinessRequirements
261
305
  };
262
306
 
263
307
  function requiredModelFeatures(documentType, document) {
@@ -271,7 +315,7 @@ function requiredReaderModel(documentType, document) {
271
315
  function modelStampFor(args) {
272
316
  return {
273
317
  modelVersion: DATA_MODEL_VERSION,
274
- minReaderModel: Math.max(args.storedMinReaderModel ?? 0, requiredReaderModel(args.documentType, args.document))
318
+ minReaderModel: Math.max(DATA_MODEL_MIN_READER, args.storedMinReaderModel ?? 0, requiredReaderModel(args.documentType, args.document))
275
319
  };
276
320
  }
277
321
 
@@ -800,8 +844,8 @@ function desugarStart(start) {
800
844
  ...start.filter !== void 0 ? {
801
845
  filter: start.filter
802
846
  } : {},
803
- ...start.allowed !== void 0 ? {
804
- allowed: start.allowed
847
+ ...start.requirements !== void 0 ? {
848
+ requirements: start.requirements
805
849
  } : {}
806
850
  };
807
851
  }
@@ -1563,7 +1607,7 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
1563
1607
  binding: "always",
1564
1608
  label: "the spawned subworkflows",
1565
1609
  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`."
1566
- } ], 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 = [ {
1610
+ } ], 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 = [ {
1567
1611
  name: "tag",
1568
1612
  label: "this engine tag",
1569
1613
  description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
@@ -1575,11 +1619,7 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
1575
1619
  name: "now",
1576
1620
  label: "the current time",
1577
1621
  description: "The ISO clock reading of the evaluating engine."
1578
- }, {
1579
- name: SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR,
1580
- label: "the subject already has an in-flight workflow",
1581
- description: "Whether any instance in this engine tag, across all definitions, has the same resource-qualified subject and no `completedAt`. Advisory under concurrent starts."
1582
- } ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
1622
+ } ], START_REQUIREMENT_VARS = [ ...START_FILTER_VARS, {
1583
1623
  name: "fields",
1584
1624
  label: "the start's input fields",
1585
1625
  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."
@@ -1764,6 +1804,37 @@ function isNotesEntry(entry) {
1764
1804
  return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
1765
1805
  }
1766
1806
 
1807
+ const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
1808
+
1809
+ function classifyPrincipalId(id) {
1810
+ if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
1811
+ namespace: "sentinel"
1812
+ };
1813
+ if (id.startsWith("g")) return {
1814
+ namespace: "global",
1815
+ globalId: id
1816
+ };
1817
+ if (id.startsWith("p-")) return {
1818
+ namespace: "robot",
1819
+ globalId: id
1820
+ };
1821
+ const embeddedGlobal = E_PREFIXED_PROJECT_ID.exec(id)?.[1];
1822
+ return embeddedGlobal !== void 0 ? embeddedGlobal.startsWith("g") ? {
1823
+ namespace: "project",
1824
+ globalId: embeddedGlobal
1825
+ } : {
1826
+ namespace: "unknown"
1827
+ } : id.startsWith("p") ? {
1828
+ namespace: "project"
1829
+ } : {
1830
+ namespace: "unknown"
1831
+ };
1832
+ }
1833
+
1834
+ function lakePrincipalId(args) {
1835
+ return args.localPrincipalId ?? args.actor.id;
1836
+ }
1837
+
1767
1838
  class FieldValueShapeError extends WorkflowError {
1768
1839
  entryType;
1769
1840
  entryName;
@@ -1804,11 +1875,11 @@ function checkChoiceList(args) {
1804
1875
  const {entryType: entryType, options: options, validation: validation} = args;
1805
1876
  if (options === void 0) return;
1806
1877
  if (!CHOICE_KINDS.has(entryType)) return [ `\`options\` is not valid on "${entryType}" values` ];
1807
- const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => checkValueAgainst({
1878
+ const kind = normalizedChoiceKind(entryType), issues = options.list.flatMap((option, index) => issuesOf(checkValueAgainst({
1808
1879
  entryType: kind,
1809
1880
  value: option.value,
1810
1881
  validation: validation
1811
- }, valueSchemas)?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
1882
+ }, valueSchemas))?.map(issue => `at options.list.${index}.value: ${issue}`) ?? []), seen = /* @__PURE__ */ new Set;
1812
1883
  for (const option of options.list) seen.has(option.value) && issues.push(`duplicate option value ${JSON.stringify(option.value)}`),
1813
1884
  seen.add(option.value);
1814
1885
  return issues.length === 0 ? void 0 : issues;
@@ -1834,8 +1905,27 @@ const fieldValueSchemas = {
1834
1905
  actor: v.union([ v.null(), ActorShape ]),
1835
1906
  assignee: v.union([ v.null(), AssigneeShape ]),
1836
1907
  assignees: v.array(AssigneeShape)
1837
- }, valueSchemas = {
1908
+ }, WritePrincipalId = v.pipe(NonEmptyString, v.rawTransform(({dataset: dataset, addIssue: addIssue}) => {
1909
+ const classified = classifyPrincipalId(dataset.value);
1910
+ return classified.namespace === "global" || classified.namespace === "robot" ? dataset.value : classified.namespace === "project" && classified.globalId !== void 0 ? classified.globalId : (addIssue({
1911
+ 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.`
1912
+ }), dataset.value);
1913
+ })), ActorWriteShape = tolerantObject()({
1914
+ kind: v.picklist(ACTOR_KINDS),
1915
+ id: WritePrincipalId,
1916
+ roles: v.exactOptional(v.array(v.string())),
1917
+ onBehalfOf: v.exactOptional(v.string())
1918
+ }), AssigneeWriteShape = v.union([ tolerantObject()({
1919
+ type: v.literal("user"),
1920
+ id: WritePrincipalId
1921
+ }), tolerantObject()({
1922
+ type: v.literal("role"),
1923
+ role: NonEmptyString
1924
+ }) ]), valueSchemas = {
1838
1925
  ...fieldValueSchemas,
1926
+ actor: v.nullable(ActorWriteShape),
1927
+ assignee: v.nullable(AssigneeWriteShape),
1928
+ assignees: v.array(AssigneeWriteShape),
1839
1929
  query: v.any()
1840
1930
  };
1841
1931
 
@@ -1906,7 +1996,7 @@ function wholeValueSchema(args) {
1906
1996
  function appendItemSchema(entryType, shape) {
1907
1997
  if (entryType === "array") return objectSchema(shape.of ?? [], valueSchemas);
1908
1998
  if (entryType === "doc.refs") return GdrShape;
1909
- if (entryType === "assignees") return AssigneeShape;
1999
+ if (entryType === "assignees") return AssigneeWriteShape;
1910
2000
  }
1911
2001
 
1912
2002
  function rejectedRefTypes(args) {
@@ -1931,7 +2021,7 @@ function gdrTypeOf(item) {
1931
2021
  return typeof t == "string" ? t : void 0;
1932
2022
  }
1933
2023
 
1934
- function checkFieldValue(args) {
2024
+ function parseFieldValue(args) {
1935
2025
  return checkValueAgainst(args, valueSchemas);
1936
2026
  }
1937
2027
 
@@ -1941,9 +2031,14 @@ function checkValueAgainst(args, leaf) {
1941
2031
  shape: args,
1942
2032
  leaf: leaf
1943
2033
  });
1944
- if (schema === void 0) return [ `unknown field entry type ${args.entryType}` ];
2034
+ if (schema === void 0) return {
2035
+ issues: [ `unknown field entry type ${args.entryType}` ]
2036
+ };
1945
2037
  const result = v.safeParse(schema, args.value);
1946
- return result.success ? refTypeIssues({
2038
+ if (!result.success) return {
2039
+ issues: formatIssues(result.issues)
2040
+ };
2041
+ const postIssues = refTypeIssues({
1947
2042
  entryType: args.entryType,
1948
2043
  types: args.types,
1949
2044
  value: args.value
@@ -1951,17 +2046,27 @@ function checkValueAgainst(args, leaf) {
1951
2046
  entryType: args.entryType,
1952
2047
  validation: args.validation,
1953
2048
  value: args.value
1954
- }) : formatIssues(result.issues);
2049
+ });
2050
+ return postIssues !== void 0 ? {
2051
+ issues: postIssues
2052
+ } : {
2053
+ output: result.output
2054
+ };
2055
+ }
2056
+
2057
+ function issuesOf(check) {
2058
+ return "issues" in check ? check.issues : void 0;
1955
2059
  }
1956
2060
 
1957
2061
  function validateFieldValue(args) {
1958
- const issues = checkFieldValue(args);
1959
- if (issues !== void 0) throw new FieldValueShapeError({
2062
+ const check = checkValueAgainst(args, valueSchemas);
2063
+ if ("issues" in check) throw new FieldValueShapeError({
1960
2064
  entryType: args.entryType,
1961
2065
  entryName: args.entryName,
1962
- issues: issues,
2066
+ issues: check.issues,
1963
2067
  mode: "value"
1964
2068
  });
2069
+ return check.output;
1965
2070
  }
1966
2071
 
1967
2072
  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}$`);
@@ -1990,7 +2095,7 @@ const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a b
1990
2095
  };
1991
2096
 
1992
2097
  function checkLiteralSeed(args) {
1993
- return args.value === null ? [ "a literal seed cannot be null — omit `initialValue` to start the field empty" ] : checkValueAgainst(args, seedValueSchemas);
2098
+ return args.value === null ? [ "a literal seed cannot be null — omit `initialValue` to start the field empty" ] : issuesOf(checkValueAgainst(args, seedValueSchemas));
1994
2099
  }
1995
2100
 
1996
2101
  function validateFieldAppendItem(args) {
@@ -2019,13 +2124,15 @@ function validateFieldAppendItem(args) {
2019
2124
  issues: typeIssues,
2020
2125
  mode: "item"
2021
2126
  });
2127
+ return result.output;
2022
2128
  }
2023
2129
 
2024
2130
  function formatIssues(issues, formatMessage = issue => issue.message) {
2025
- return issues.map(i => {
2026
- const keys = i.path?.map(p => p.key) ?? [];
2027
- return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(i)}`;
2028
- });
2131
+ const formatOne = (issue, prefix) => {
2132
+ const keys = [ ...prefix, ...issue.path?.map(p => p.key) ?? [] ], sub = issue.issues?.filter(candidate => candidate.expected !== "null");
2133
+ return sub !== void 0 && sub.length > 0 ? sub.flatMap(candidate => formatOne(candidate, keys)) : [ `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(issue)}` ];
2134
+ };
2135
+ return issues.flatMap(issue => formatOne(issue, []));
2029
2136
  }
2030
2137
 
2031
2138
  const NonEmpty = NonEmptyString, PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -2376,7 +2483,18 @@ const StoredActionSchema = pinned()(v.strictObject({
2376
2483
  filter: v.optional(ConditionSchema),
2377
2484
  params: v.optional(v.array(ActionParamSchema)),
2378
2485
  effects: v.optional(v.array(EffectSchema))
2379
- })), AuthoringActionSchema = pinned()(v.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema));
2486
+ })), AuthoringActionSchema = pinned()(v.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema)), requirementBase = {
2487
+ name: NonEmpty,
2488
+ title: v.optional(v.string()),
2489
+ description: v.optional(v.string())
2490
+ }, GroqRequirementSchemaRaw = v.strictObject({
2491
+ ...requirementBase,
2492
+ type: v.literal("groq"),
2493
+ query: ConditionSchema
2494
+ }), SingleSubjectRequirementSchemaRaw = v.strictObject({
2495
+ ...requirementBase,
2496
+ type: v.literal("singleSubject")
2497
+ }), GroqRequirementSchema = pinned()(GroqRequirementSchemaRaw), StartRequirementSchema = pinned()(v.variant("type", [ GroqRequirementSchemaRaw, SingleSubjectRequirementSchemaRaw ]));
2380
2498
 
2381
2499
  function activityFields({field: field, action: action, target: target, group: group}) {
2382
2500
  return {
@@ -2387,7 +2505,7 @@ function activityFields({field: field, action: action, target: target, group: gr
2387
2505
  group: v.optional(group),
2388
2506
  target: v.optional(target),
2389
2507
  filter: v.optional(ConditionSchema),
2390
- requirements: v.optional(v.record(NonEmpty, ConditionSchema)),
2508
+ requirements: v.optional(v.array(GroqRequirementSchema)),
2391
2509
  actions: v.optional(v.array(action)),
2392
2510
  fields: v.optional(v.array(field))
2393
2511
  };
@@ -2483,7 +2601,7 @@ function startFields(kind) {
2483
2601
  return {
2484
2602
  kind: kind,
2485
2603
  filter: v.optional(ConditionSchema),
2486
- allowed: v.optional(ConditionSchema)
2604
+ requirements: v.optional(v.array(StartRequirementSchema))
2487
2605
  };
2488
2606
  }
2489
2607
 
@@ -2622,6 +2740,13 @@ function checkActivities({def: def, i: i, activityNames: activityNames, issues:
2622
2740
  })),
2623
2741
  what: `action name in activity "${activity.name}"`,
2624
2742
  issues: issues
2743
+ }), checkDuplicates({
2744
+ names: (activity.requirements ?? []).map((requirement, r) => ({
2745
+ name: requirement.name,
2746
+ path: [ ...path, "requirements", r, "name" ]
2747
+ })),
2748
+ what: `requirement name in activity "${activity.name}"`,
2749
+ issues: issues
2625
2750
  }), checkStatusSetTargets({
2626
2751
  activity: activity,
2627
2752
  activityNames: activityNames,
@@ -2930,10 +3055,10 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
2930
3055
  policy: "cascade",
2931
3056
  fields: fields
2932
3057
  });
2933
- for (const [name, groq2] of Object.entries(activity.requirements ?? {})) sites.push({
2934
- groq: groq2,
2935
- path: [ ...path, "requirements", name ],
2936
- label: `activity "${activity.name}" requirement "${name}"`,
3058
+ for (const [index, requirement] of (activity.requirements ?? []).entries()) sites.push({
3059
+ groq: requirement.query,
3060
+ path: [ ...path, "requirements", index, "query" ],
3061
+ label: `activity "${activity.name}" requirement "${requirement.name}"`,
2937
3062
  policy: "caller-bound",
2938
3063
  fields: fields
2939
3064
  });
@@ -3129,73 +3254,83 @@ function checkTriggeredActionParams(def, issues) {
3129
3254
  }
3130
3255
 
3131
3256
  function checkStart(def, issues) {
3132
- if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
3257
+ if (def.start !== void 0 && (checkDuplicates({
3258
+ names: (def.start.requirements ?? []).map((requirement, index) => ({
3259
+ name: requirement.name,
3260
+ path: [ "start", "requirements", index, "name" ]
3261
+ })),
3262
+ what: "start requirement name",
3263
+ issues: issues
3264
+ }), def.lifecycle === "child" && issues.push({
3133
3265
  path: [ "start" ],
3134
3266
  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'`"
3135
- }), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), checkStartSubjectVariable(def, issues),
3136
- def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
3267
+ }), checkStartFilterReads(def, issues), checkStartRequirementReads(def, issues),
3268
+ checkSingleSubjectRequirements(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
3137
3269
  path: [ "fields", n, "required" ],
3138
3270
  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)`
3139
3271
  });
3140
3272
  }
3141
3273
 
3142
- function checkStartSubjectVariable(def, issues) {
3274
+ function checkSingleSubjectRequirements(def, issues) {
3275
+ const indexes = (def.start?.requirements ?? []).map((requirement, index) => ({
3276
+ requirement: requirement,
3277
+ index: index
3278
+ })).filter(({requirement: requirement}) => requirement.type === "singleSubject"), [first] = indexes;
3279
+ if (first === void 0) return;
3143
3280
  const subject = (def.fields ?? []).find(isSubjectEntry);
3144
- 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)) {
3145
- if (subject === void 0) {
3146
- issues.push({
3147
- path: [ "start", key ],
3148
- 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`
3149
- });
3150
- continue;
3151
- }
3152
- key === "allowed" && !isInputSourced(subject) && issues.push({
3153
- path: [ "start", key ],
3154
- 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`
3281
+ if (subject === void 0) {
3282
+ issues.push({
3283
+ path: [ "start", "requirements", first.index ],
3284
+ 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"
3155
3285
  });
3286
+ return;
3156
3287
  }
3288
+ isInputSourced(subject) || issues.push({
3289
+ path: [ "start", "requirements", first.index ],
3290
+ 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`
3291
+ });
3157
3292
  }
3158
3293
 
3159
3294
  function checkStartFilterReads(def, issues) {
3160
3295
  const filter = def.start?.filter;
3161
3296
  filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
3162
3297
  path: [ "start", "filter" ],
3163
- 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"
3298
+ 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"
3164
3299
  }), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
3165
3300
  path: [ "start", "filter" ],
3166
3301
  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"
3167
3302
  }));
3168
3303
  }
3169
3304
 
3170
- function checkStartAllowedReads(def, issues) {
3171
- const allowed = def.start?.allowed;
3172
- if (allowed === void 0) return;
3305
+ function checkStartRequirementReads(def, issues) {
3173
3306
  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())}`;
3174
- for (const read of conditionFieldReads(allowed)) {
3175
- const entry = bindable.get(read.name);
3176
- if (entry === void 0) {
3177
- issues.push({
3178
- path: [ "start", "allowed" ],
3179
- 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}`
3307
+ for (const [index, requirement] of (def.start?.requirements ?? []).entries()) if (requirement.type === "groq") {
3308
+ for (const read of conditionFieldReads(requirement.query)) {
3309
+ const entry = bindable.get(read.name);
3310
+ if (entry === void 0) {
3311
+ issues.push({
3312
+ path: [ "start", "requirements", index, "query" ],
3313
+ 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}`
3314
+ });
3315
+ continue;
3316
+ }
3317
+ pushFieldReadPathIssue({
3318
+ where: "start requirement",
3319
+ read: {
3320
+ field: read.name,
3321
+ path: read.path
3322
+ },
3323
+ target: entry,
3324
+ path: [ "start", "requirements", index, "query" ],
3325
+ issues: issues,
3326
+ nodes: START_REQUIREMENT_VALUE_NODES
3180
3327
  });
3181
- continue;
3182
3328
  }
3183
- pushFieldReadPathIssue({
3184
- where: "start.allowed",
3185
- read: {
3186
- field: read.name,
3187
- path: read.path
3188
- },
3189
- target: entry,
3190
- path: [ "start", "allowed" ],
3191
- issues: issues,
3192
- nodes: START_ALLOWED_VALUE_NODES
3329
+ readsRootDocument(requirement.query) && issues.push({
3330
+ path: [ "start", "requirements", index, "query" ],
3331
+ 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"
3193
3332
  });
3194
3333
  }
3195
- readsRootDocument(allowed) && issues.push({
3196
- path: [ "start", "allowed" ],
3197
- 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"
3198
- });
3199
3334
  }
3200
3335
 
3201
3336
  function checkGroups(def, issues) {
@@ -3720,7 +3855,7 @@ const SCALAR = {
3720
3855
  kind: "rows",
3721
3856
  of: shape.of ?? []
3722
3857
  })
3723
- }, START_ALLOWED_VALUE_NODES = {
3858
+ }, START_REQUIREMENT_VALUE_NODES = {
3724
3859
  ...VALUE_NODES,
3725
3860
  "doc.ref": () => GDR_VALUE,
3726
3861
  subject: () => GDR_VALUE
@@ -3801,4 +3936,4 @@ function checkWorkflowInvariants(def) {
3801
3936
  checkGroups(def, issues), issues;
3802
3937
  }
3803
3938
 
3804
- export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DATA_MODEL_CHANGES, DATA_MODEL_MIN_READER, DATA_MODEL_VERSION, 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, ModelVersionAheadError, NonEmptyString, PersistedDocShapeError, READER_MODEL_ROLLOUT_URL, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, ReaderModelAcknowledgementError, START_ALLOWED_VARS, START_FILTER_VARS, SUBJECT_HAS_IN_FLIGHT_INSTANCE_VAR, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, assertReadableModel, assertReaderModelAcknowledgement, checkFieldValue, checkWorkflowInvariants, choiceValueIssues, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldTreeShape, fieldValueSchemas, 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, isUnprimed, labelFor, minReaderModelOf, modelStampFor, modelVersionOf, parentRef, parseDefinitionSnapshot, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, requiredModelFeatures, requiredReaderModel, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, terminalState, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
3939
+ 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, DATA_MODEL_CHANGES, DATA_MODEL_MIN_READER, DATA_MODEL_VERSION, 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, ModelVersionAheadError, NonEmptyString, PersistedDocShapeError, READER_MODEL_ROLLOUT_URL, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, ReaderModelAcknowledgementError, START_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WORKFLOW_INSTANCE_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, assertReadableModel, assertReaderModelAcknowledgement, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldTreeShape, fieldValueSchemas, 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, isUnprimed, labelFor, lakePrincipalId, minReaderModelOf, modelStampFor, modelVersionOf, parentRef, parseDefinitionSnapshot, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, requiredModelFeatures, requiredReaderModel, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, terminalState, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };