@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.
- package/CHANGELOG.md +99 -0
- package/DATAMODEL.md +168 -0
- package/dist/_chunks-cjs/invariants.cjs +223 -82
- package/dist/_chunks-es/invariants.js +213 -78
- package/dist/define.d.cts +42 -23
- package/dist/define.d.ts +42 -23
- package/dist/index.cjs +1717 -864
- package/dist/index.d.cts +450 -176
- package/dist/index.d.ts +450 -176
- package/dist/index.js +1698 -863
- package/package.json +2 -2
|
@@ -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 =
|
|
139
|
+
const DATA_MODEL_VERSION = 4, DATA_MODEL_MIN_READER = 4, READER_MODEL_ROLLOUT_URL = "https://www.sanity.io/docs/editorial-workflows/prerelease";
|
|
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}.
|
|
149
|
+
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}`),
|
|
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.
|
|
804
|
-
|
|
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
|
-
} ],
|
|
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
|
-
},
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
})
|
|
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
|
|
1959
|
-
if (issues
|
|
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
|
-
|
|
2026
|
-
const keys =
|
|
2027
|
-
return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${formatMessage(
|
|
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.
|
|
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
|
-
|
|
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 [
|
|
2934
|
-
groq:
|
|
2935
|
-
path: [ ...path, "requirements",
|
|
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 && (
|
|
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),
|
|
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
|
|
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
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
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
|
|
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
|
|
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
|
|
3175
|
-
const
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
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
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
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
|
-
},
|
|
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,
|
|
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 };
|