@sanity/workflow-engine 0.27.0 → 0.29.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 +197 -0
- package/DATAMODEL.md +107 -0
- package/dist/_chunks-cjs/invariants.cjs +212 -442
- package/dist/_chunks-es/invariants.js +209 -405
- package/dist/define.cjs +1 -6
- package/dist/define.d.cts +232 -24
- package/dist/define.d.ts +232 -24
- package/dist/define.js +2 -7
- package/dist/index.cjs +1216 -510
- package/dist/index.d.cts +452 -34
- package/dist/index.d.ts +452 -34
- package/dist/index.js +1106 -415
- package/package.json +1 -1
|
@@ -6,6 +6,41 @@ import { parse } from "groq-js";
|
|
|
6
6
|
|
|
7
7
|
import { isDraftId, isVersionId, getPublishedId } from "@sanity/id-utils";
|
|
8
8
|
|
|
9
|
+
function isCascadeFired(action) {
|
|
10
|
+
return action.when !== void 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function deriveActivityKind(activity) {
|
|
14
|
+
if (activity.target !== void 0) return "manual";
|
|
15
|
+
const actions = activity.actions ?? [];
|
|
16
|
+
return actions.some(a => !isCascadeFired(a)) ? "user" : actions.some(a => (a.effects ?? []).length > 0 || a.spawn !== void 0) ? "service" : actions.length > 0 ? "receive" : "script";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function deriveExecutorClassification(activity) {
|
|
20
|
+
if (activity.target !== void 0) return "off-system";
|
|
21
|
+
const actions = activity.actions ?? [], cascadeFired = actions.filter(isCascadeFired).length;
|
|
22
|
+
return cascadeFired === actions.length ? "autonomous" : cascadeFired === 0 ? "interactive" : "hybrid";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function driverKind(actor) {
|
|
26
|
+
return actor.kind === "person" || actor.kind === "agent" ? actor.kind : "service";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function errorMessage(err) {
|
|
30
|
+
return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rethrowWithContext(err, context) {
|
|
34
|
+
throw new Error(`${context}: ${errorMessage(err)}`, {
|
|
35
|
+
cause: err
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function andConditions(parts) {
|
|
40
|
+
const present = parts.filter(p => p !== void 0);
|
|
41
|
+
if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
|
|
42
|
+
}
|
|
43
|
+
|
|
9
44
|
class WorkflowError extends Error {
|
|
10
45
|
kind;
|
|
11
46
|
constructor(kind, message, options) {
|
|
@@ -79,322 +114,6 @@ function effectNotFoundMessage(args) {
|
|
|
79
114
|
return args.settled.status === "cancelled" ? `${base} — it was cancelled at ${args.settled.ranAt}${cause}` : `${base} — it already settled "${args.settled.status}" at ${args.settled.ranAt}${cause}`;
|
|
80
115
|
}
|
|
81
116
|
|
|
82
|
-
function errorMessage(err) {
|
|
83
|
-
return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function rethrowWithContext(err, context) {
|
|
87
|
-
throw new Error(`${context}: ${errorMessage(err)}`, {
|
|
88
|
-
cause: err
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const WORKFLOW_INSTANCE_TYPE = "sanity.workflow.instance";
|
|
93
|
-
|
|
94
|
-
function terminalState(instance) {
|
|
95
|
-
return instance.abortedAt !== void 0 ? "aborted" : instance.completedAt !== void 0 ? "completed" : "in-flight";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isUnprimed(instance) {
|
|
99
|
-
return instance.stages.length === 0 && terminalState(instance) === "in-flight";
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function parseDefinitionSnapshotValue(instance) {
|
|
103
|
-
try {
|
|
104
|
-
return normalizeLegacyActivityRequirements(JSON.parse(instance.definitionSnapshot));
|
|
105
|
-
} catch (err) {
|
|
106
|
-
rethrowWithContext(err, `Failed to parse definitionSnapshot on instance "${instance._id}"`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
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
|
-
|
|
131
|
-
function parseDefinitionSnapshot(instance) {
|
|
132
|
-
return parseDefinitionSnapshotValue(instance);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function parentRef(instance) {
|
|
136
|
-
return instance.ancestors.at(-1);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const DATA_MODEL_VERSION = 5, DATA_MODEL_MIN_READER = 4, READER_MODEL_ROLLOUT_URL = "https://www.sanity.io/docs/editorial-workflows/prerelease";
|
|
140
|
-
|
|
141
|
-
class ReaderModelAcknowledgementError extends WorkflowError {
|
|
142
|
-
code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
|
|
143
|
-
expectedMinReaderModel;
|
|
144
|
-
engineMinReaderModel=DATA_MODEL_MIN_READER;
|
|
145
|
-
engineModelVersion=DATA_MODEL_VERSION;
|
|
146
|
-
documentationUrl=READER_MODEL_ROLLOUT_URL;
|
|
147
|
-
constructor(expectedMinReaderModel, context = "Deployment") {
|
|
148
|
-
const expected = expectedMinReaderModel === void 0 ? "missing" : String(expectedMinReaderModel);
|
|
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}`),
|
|
150
|
-
this.name = "ReaderModelAcknowledgementError", this.expectedMinReaderModel = expectedMinReaderModel;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function assertReaderModelAcknowledgement(expectedMinReaderModel, context) {
|
|
155
|
-
if (typeof expectedMinReaderModel != "number" || !Number.isFinite(expectedMinReaderModel) || !Number.isInteger(expectedMinReaderModel) || expectedMinReaderModel < 0 || expectedMinReaderModel !== DATA_MODEL_MIN_READER) throw new ReaderModelAcknowledgementError(expectedMinReaderModel, context);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
|
|
159
|
-
id: "governed-model-stamps",
|
|
160
|
-
introducedInModel: 1,
|
|
161
|
-
minReaderModel: 0,
|
|
162
|
-
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
163
|
-
compatibility: "additive",
|
|
164
|
-
applicability: "unconditional",
|
|
165
|
-
summary: "Definition and instance documents carry model provenance and reader-floor stamps."
|
|
166
|
-
}), Object.freeze({
|
|
167
|
-
id: "subject-field-kind",
|
|
168
|
-
introducedInModel: 2,
|
|
169
|
-
minReaderModel: 0,
|
|
170
|
-
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
171
|
-
compatibility: "additive",
|
|
172
|
-
applicability: "detectable",
|
|
173
|
-
summary: "A workflow-level subject field identifies the document a workflow is about."
|
|
174
|
-
}), Object.freeze({
|
|
175
|
-
id: "typed-scalar-choice-lists",
|
|
176
|
-
introducedInModel: 2,
|
|
177
|
-
minReaderModel: 2,
|
|
178
|
-
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
179
|
-
compatibility: "reader-floor",
|
|
180
|
-
applicability: "detectable",
|
|
181
|
-
summary: "Scalar fields may constrain writes to a persisted typed choice list."
|
|
182
|
-
}), Object.freeze({
|
|
183
|
-
id: "action-semantics",
|
|
184
|
-
introducedInModel: 2,
|
|
185
|
-
minReaderModel: 0,
|
|
186
|
-
documentTypes: Object.freeze([ "definition" ]),
|
|
187
|
-
compatibility: "additive",
|
|
188
|
-
applicability: "detectable",
|
|
189
|
-
summary: "Ordinary actions may carry a closed bag of advisory workflow semantics."
|
|
190
|
-
}), Object.freeze({
|
|
191
|
-
id: "inclusive-scalar-bounds",
|
|
192
|
-
introducedInModel: 2,
|
|
193
|
-
minReaderModel: 2,
|
|
194
|
-
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
195
|
-
compatibility: "reader-floor",
|
|
196
|
-
applicability: "detectable",
|
|
197
|
-
summary: "String, text, and number values may carry persisted inclusive bounds."
|
|
198
|
-
}), Object.freeze({
|
|
199
|
-
id: "progress-field-kind",
|
|
200
|
-
introducedInModel: 3,
|
|
201
|
-
minReaderModel: 0,
|
|
202
|
-
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
203
|
-
compatibility: "additive",
|
|
204
|
-
applicability: "detectable",
|
|
205
|
-
summary: "A progress field kind carries application-defined 0–100 completion."
|
|
206
|
-
}), Object.freeze({
|
|
207
|
-
id: "effect-claim-tokens",
|
|
208
|
-
introducedInModel: 3,
|
|
209
|
-
minReaderModel: 0,
|
|
210
|
-
documentTypes: Object.freeze([ "instance" ]),
|
|
211
|
-
compatibility: "additive",
|
|
212
|
-
applicability: "detectable",
|
|
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."
|
|
230
|
-
}), Object.freeze({
|
|
231
|
-
id: "due-date-field-kinds",
|
|
232
|
-
introducedInModel: 5,
|
|
233
|
-
minReaderModel: 0,
|
|
234
|
-
documentTypes: Object.freeze([ "definition", "instance" ]),
|
|
235
|
-
compatibility: "additive",
|
|
236
|
-
applicability: "detectable",
|
|
237
|
-
summary: "Due-date field kinds (dueDate, dueDatetime) mark a level deadline, elevated aliases of date/datetime carrying the same stored value."
|
|
238
|
-
}) ]);
|
|
239
|
-
|
|
240
|
-
function recordOf(value) {
|
|
241
|
-
return value !== null && typeof value == "object" && !Array.isArray(value) ? value : void 0;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function recordsAt(record, key) {
|
|
245
|
-
const value = record[key];
|
|
246
|
-
return Array.isArray(value) ? value.map(recordOf).filter(item => item !== void 0) : [];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function nestedFieldEntries(entries) {
|
|
250
|
-
return entries.flatMap(entry => [ entry, ...nestedFieldEntries(recordsAt(entry, "fields")), ...nestedFieldEntries(recordsAt(entry, "of")) ]);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function parsedDefinitionSnapshot(root) {
|
|
254
|
-
if (typeof root.definitionSnapshot == "string") return recordOf(parseDefinitionSnapshotValue({
|
|
255
|
-
_id: typeof root._id == "string" ? root._id : "<unknown instance>",
|
|
256
|
-
definitionSnapshot: root.definitionSnapshot
|
|
257
|
-
}));
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function persistedFieldEntries(document) {
|
|
261
|
-
const root = recordOf(document);
|
|
262
|
-
if (root === void 0) return [];
|
|
263
|
-
const snapshot = parsedDefinitionSnapshot(root), roots = snapshot === void 0 ? [ root ] : [ root, snapshot ], stages = roots.flatMap(candidate => recordsAt(candidate, "stages")), activities = stages.flatMap(stage => recordsAt(stage, "activities")), actions = activities.flatMap(activity => recordsAt(activity, "actions")), effects = actions.flatMap(action => recordsAt(action, "effects"));
|
|
264
|
-
return nestedFieldEntries([ ...roots.flatMap(candidate => recordsAt(candidate, "fields")), ...stages.flatMap(stage => recordsAt(stage, "fields")), ...activities.flatMap(activity => recordsAt(activity, "fields")), ...actions.flatMap(action => recordsAt(action, "params")), ...effects.flatMap(effect => recordsAt(effect, "outputs")) ]);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function hasChoiceList(document) {
|
|
268
|
-
return persistedFieldEntries(document).some(entry => {
|
|
269
|
-
const options = recordOf(entry.options);
|
|
270
|
-
return options !== void 0 && Array.isArray(options.list);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function hasFieldKind(document, kind) {
|
|
275
|
-
return persistedFieldEntries(document).some(entry => entry.type === kind || entry._type === kind);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function hasActionSemantics(document) {
|
|
279
|
-
const root = recordOf(document);
|
|
280
|
-
return root === void 0 ? !1 : recordsAt(root, "stages").flatMap(stage => recordsAt(stage, "activities")).flatMap(activity => recordsAt(activity, "actions")).some(action => Array.isArray(action.semantics));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function hasScalarValidation(document) {
|
|
284
|
-
return persistedFieldEntries(document).some(entry => {
|
|
285
|
-
const validation = recordOf(entry.validation);
|
|
286
|
-
return typeof validation?.min == "number" || typeof validation?.max == "number";
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function hasClaimTokens(document) {
|
|
291
|
-
const root = recordOf(document);
|
|
292
|
-
return root === void 0 ? !1 : recordsAt(root, "pendingEffects").some(entry => {
|
|
293
|
-
const claim = recordOf(entry.claim);
|
|
294
|
-
return claim !== void 0 && typeof claim.claimToken == "string";
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function hasReadinessRequirements(document) {
|
|
299
|
-
const root = recordOf(document);
|
|
300
|
-
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)));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const featureDetectors = {
|
|
304
|
-
"governed-model-stamps": () => !0,
|
|
305
|
-
"subject-field-kind": document => hasFieldKind(document, "subject"),
|
|
306
|
-
"typed-scalar-choice-lists": hasChoiceList,
|
|
307
|
-
"action-semantics": hasActionSemantics,
|
|
308
|
-
"inclusive-scalar-bounds": hasScalarValidation,
|
|
309
|
-
"progress-field-kind": document => hasFieldKind(document, "progress"),
|
|
310
|
-
"effect-claim-tokens": hasClaimTokens,
|
|
311
|
-
"classified-principal-ids": () => !0,
|
|
312
|
-
"readiness-requirements": hasReadinessRequirements,
|
|
313
|
-
"due-date-field-kinds": document => hasFieldKind(document, "dueDate") || hasFieldKind(document, "dueDatetime")
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
function requiredModelFeatures(documentType, document) {
|
|
317
|
-
return DATA_MODEL_CHANGES.filter(change => change.documentTypes.some(candidate => candidate === documentType) && featureDetectors[change.id](document));
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function requiredReaderModel(documentType, document) {
|
|
321
|
-
return Math.max(0, ...requiredModelFeatures(documentType, document).map(change => change.minReaderModel));
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function modelStampFor(args) {
|
|
325
|
-
return {
|
|
326
|
-
modelVersion: DATA_MODEL_VERSION,
|
|
327
|
-
minReaderModel: Math.max(DATA_MODEL_MIN_READER, args.storedMinReaderModel ?? 0, requiredReaderModel(args.documentType, args.document))
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function fieldTreeShape(value) {
|
|
332
|
-
if (Array.isArray(value)) return value.map(fieldTreeShape);
|
|
333
|
-
if (value === null) return "null";
|
|
334
|
-
if (typeof value == "object") {
|
|
335
|
-
const record = value;
|
|
336
|
-
return Object.fromEntries(Object.keys(record).toSorted().map(key => [ key, fieldTreeShape(record[key]) ]));
|
|
337
|
-
}
|
|
338
|
-
return typeof value;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
function modelVersionOf(doc) {
|
|
342
|
-
const stamp = doc.modelVersion;
|
|
343
|
-
return typeof stamp == "number" ? stamp : 0;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function minReaderModelOf(doc) {
|
|
347
|
-
const floor = doc.minReaderModel;
|
|
348
|
-
return typeof floor == "number" ? floor : modelVersionOf(doc);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
class ModelVersionAheadError extends WorkflowError {
|
|
352
|
-
documentId;
|
|
353
|
-
documentModelVersion;
|
|
354
|
-
requiredReaderModel;
|
|
355
|
-
engineModelVersion;
|
|
356
|
-
constructor(args) {
|
|
357
|
-
super("model-version-ahead", `Document "${args.documentId}" was written by engine data model ${args.documentModelVersion} and requires a reader at model ${args.requiredReaderModel} or newer; this engine reads up to model ${DATA_MODEL_VERSION}. Upgrade @sanity/workflow-engine to a version that understands it.`),
|
|
358
|
-
this.name = "ModelVersionAheadError", this.documentId = args.documentId, this.documentModelVersion = args.documentModelVersion,
|
|
359
|
-
this.requiredReaderModel = args.requiredReaderModel, this.engineModelVersion = DATA_MODEL_VERSION;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function assertReadableModel(doc) {
|
|
364
|
-
const documentReaderModel = minReaderModelOf(doc);
|
|
365
|
-
if (documentReaderModel > DATA_MODEL_VERSION) throw new ModelVersionAheadError({
|
|
366
|
-
documentId: doc._id,
|
|
367
|
-
documentModelVersion: modelVersionOf(doc),
|
|
368
|
-
requiredReaderModel: documentReaderModel
|
|
369
|
-
});
|
|
370
|
-
return doc;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function isCascadeFired(action) {
|
|
374
|
-
return action.when !== void 0;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function deriveActivityKind(activity) {
|
|
378
|
-
if (activity.target !== void 0) return "manual";
|
|
379
|
-
const actions = activity.actions ?? [];
|
|
380
|
-
return actions.some(a => !isCascadeFired(a)) ? "user" : actions.some(a => (a.effects ?? []).length > 0 || a.spawn !== void 0) ? "service" : actions.length > 0 ? "receive" : "script";
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function deriveExecutorClassification(activity) {
|
|
384
|
-
if (activity.target !== void 0) return "off-system";
|
|
385
|
-
const actions = activity.actions ?? [], cascadeFired = actions.filter(isCascadeFired).length;
|
|
386
|
-
return cascadeFired === actions.length ? "autonomous" : cascadeFired === 0 ? "interactive" : "hybrid";
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function driverKind(actor) {
|
|
390
|
-
return actor.kind === "person" || actor.kind === "agent" ? actor.kind : "service";
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function andConditions(parts) {
|
|
394
|
-
const present = parts.filter(p => p !== void 0);
|
|
395
|
-
if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
|
|
396
|
-
}
|
|
397
|
-
|
|
398
117
|
const KNOWN_SCHEMES = /* @__PURE__ */ new Set([ "dataset", "canvas", "media-library", "dashboard" ]), KNOWN_SCHEMES_TEXT = [ ...KNOWN_SCHEMES ].join(", ");
|
|
399
118
|
|
|
400
119
|
class VersionSpecificDatasetGdrError extends Error {
|
|
@@ -806,6 +525,7 @@ function desugarWorkflow(authoring) {
|
|
|
806
525
|
return {
|
|
807
526
|
...stripUndefined({
|
|
808
527
|
name: stage.name,
|
|
528
|
+
semantics: stage.semantics,
|
|
809
529
|
title: stage.title,
|
|
810
530
|
description: stage.description,
|
|
811
531
|
groups: stage.groups,
|
|
@@ -829,6 +549,7 @@ function desugarWorkflow(authoring) {
|
|
|
829
549
|
definition: {
|
|
830
550
|
...stripUndefined({
|
|
831
551
|
name: authoring.name,
|
|
552
|
+
semantics: authoring.semantics,
|
|
832
553
|
title: authoring.title,
|
|
833
554
|
description: authoring.description,
|
|
834
555
|
groups: authoring.groups,
|
|
@@ -1055,6 +776,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
|
|
|
1055
776
|
return {
|
|
1056
777
|
...stripUndefined({
|
|
1057
778
|
name: activity.name,
|
|
779
|
+
semantics: activity.semantics,
|
|
1058
780
|
title: activity.title,
|
|
1059
781
|
description: activity.description,
|
|
1060
782
|
groups: activity.groups,
|
|
@@ -1526,7 +1248,58 @@ function isTerminalActivityStatus(status) {
|
|
|
1526
1248
|
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
1527
1249
|
}
|
|
1528
1250
|
|
|
1529
|
-
const
|
|
1251
|
+
const SIGNAL_SEMANTICS = [ "signal.positive", "signal.caution", "signal.critical" ], DECISION_SEMANTICS = [ "decision.accept", "decision.decline" ], ACTION_SEMANTICS = [ ...DECISION_SEMANTICS, ...SIGNAL_SEMANTICS ], FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ];
|
|
1252
|
+
|
|
1253
|
+
function releaseDocId(releaseName) {
|
|
1254
|
+
return `_.releases.${releaseName}`;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1258
|
+
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1259
|
+
return {
|
|
1260
|
+
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1261
|
+
type: "system.release",
|
|
1262
|
+
releaseName: releaseName
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function isAlwaysArrayFieldKind(kind) {
|
|
1267
|
+
return kind === "doc.refs" || kind === "assignees" || kind === "array";
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function isSingleDocRefKind(kind) {
|
|
1271
|
+
return kind === "doc.ref" || kind === "subject";
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function refKindAcceptsTypes(kind) {
|
|
1275
|
+
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function isSingleDocRefEntry(entry) {
|
|
1279
|
+
return isSingleDocRefKind(entry._type);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function isTodoListItem(row) {
|
|
1283
|
+
if (typeof row != "object" || row === null) return !1;
|
|
1284
|
+
const candidate = row, status = candidate.status;
|
|
1285
|
+
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function declaredRowColumns(entry) {
|
|
1289
|
+
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function isTodoListEntry(entry) {
|
|
1293
|
+
const columns = declaredRowColumns(entry);
|
|
1294
|
+
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function isNotesEntry(entry) {
|
|
1298
|
+
const columns = declaredRowColumns(entry);
|
|
1299
|
+
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const CONDITION_VARS = [ {
|
|
1530
1303
|
name: "self",
|
|
1531
1304
|
binding: "always",
|
|
1532
1305
|
label: "this workflow instance",
|
|
@@ -1601,6 +1374,11 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
|
|
|
1601
1374
|
binding: "caller",
|
|
1602
1375
|
label: "your permissions",
|
|
1603
1376
|
description: "Advisory per-permission booleans computed from the caller's grants; `undefined` without grants. Bound wherever grants ride the evaluation: the projection's rendered scope (fireAction-action filters, activity requirements, editability predicates) and the fireAction/editField commit gates. Deploy rejects it at every site that evaluates without grants: transition `when`s, activity filters, cascade-fired actions' `when`/`filter`, effect bindings, where-op `where`s, and the spawn `forEach`/`with`/`context` sites."
|
|
1377
|
+
}, {
|
|
1378
|
+
name: "attributes",
|
|
1379
|
+
binding: "caller",
|
|
1380
|
+
label: "your attributes",
|
|
1381
|
+
description: "Advisory org-level User Attributes for the caller (Enterprise — same values as lake `user::attributes()`), keyed by attribute name with each active scalar or array value. `undefined` on expected HTTP absence; unexpected fetch failures throw; empty page binds `{}`. Bound wherever grants ride the evaluation (same sites as `$can`). Soft-gate paths fetch at most 100 attributes (no further pages) and warn when the envelope reports `hasMore: true` (partial bag still binds). Not a security boundary — the Content Lake remains the only enforcement point."
|
|
1604
1382
|
}, {
|
|
1605
1383
|
name: "row",
|
|
1606
1384
|
binding: "spawn",
|
|
@@ -1766,54 +1544,7 @@ function documentIdOf(doc) {
|
|
|
1766
1544
|
return "(unknown id)";
|
|
1767
1545
|
}
|
|
1768
1546
|
|
|
1769
|
-
const ACTOR_KINDS = [ "person", "agent", "system" ]
|
|
1770
|
-
|
|
1771
|
-
function releaseDocId(releaseName) {
|
|
1772
|
-
return `_.releases.${releaseName}`;
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1776
|
-
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1777
|
-
return {
|
|
1778
|
-
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1779
|
-
type: "system.release",
|
|
1780
|
-
releaseName: releaseName
|
|
1781
|
-
};
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
function isSingleDocRefKind(kind) {
|
|
1785
|
-
return kind === "doc.ref" || kind === "subject";
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
function refKindAcceptsTypes(kind) {
|
|
1789
|
-
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
function isSingleDocRefEntry(entry) {
|
|
1793
|
-
return isSingleDocRefKind(entry._type);
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
function isTodoListItem(row) {
|
|
1797
|
-
if (typeof row != "object" || row === null) return !1;
|
|
1798
|
-
const candidate = row, status = candidate.status;
|
|
1799
|
-
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
function declaredRowColumns(entry) {
|
|
1803
|
-
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
function isTodoListEntry(entry) {
|
|
1807
|
-
const columns = declaredRowColumns(entry);
|
|
1808
|
-
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
function isNotesEntry(entry) {
|
|
1812
|
-
const columns = declaredRowColumns(entry);
|
|
1813
|
-
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1547
|
+
const ACTOR_KINDS = [ "person", "agent", "system" ], ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1817
1548
|
|
|
1818
1549
|
function classifyPrincipalId(id) {
|
|
1819
1550
|
if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
|
|
@@ -2242,6 +1973,10 @@ function opSchemas(targetSchema) {
|
|
|
2242
1973
|
type: v.literal("field.set"),
|
|
2243
1974
|
target: targetSchema,
|
|
2244
1975
|
value: ValueExprSchema
|
|
1976
|
+
}), v.strictObject({
|
|
1977
|
+
type: v.literal("field.setIfMissing"),
|
|
1978
|
+
target: targetSchema,
|
|
1979
|
+
value: ValueExprSchema
|
|
2245
1980
|
}), v.strictObject({
|
|
2246
1981
|
type: v.literal("field.unset"),
|
|
2247
1982
|
target: targetSchema
|
|
@@ -2249,6 +1984,14 @@ function opSchemas(targetSchema) {
|
|
|
2249
1984
|
type: v.literal("field.append"),
|
|
2250
1985
|
target: targetSchema,
|
|
2251
1986
|
value: ValueExprSchema
|
|
1987
|
+
}), v.strictObject({
|
|
1988
|
+
type: v.literal("field.inc"),
|
|
1989
|
+
target: targetSchema,
|
|
1990
|
+
value: v.optional(ValueExprSchema)
|
|
1991
|
+
}), v.strictObject({
|
|
1992
|
+
type: v.literal("field.dec"),
|
|
1993
|
+
target: targetSchema,
|
|
1994
|
+
value: v.optional(ValueExprSchema)
|
|
2252
1995
|
}), v.strictObject({
|
|
2253
1996
|
type: v.literal("field.updateWhere"),
|
|
2254
1997
|
target: targetSchema,
|
|
@@ -2465,17 +2208,27 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
|
|
|
2465
2208
|
required: v.optional(v.boolean()),
|
|
2466
2209
|
options: v.optional(ChoiceOptionsSchema),
|
|
2467
2210
|
validation: v.optional(ScalarValidationSchema)
|
|
2468
|
-
}), choiceOptionsCheck(), scalarValidationCheck());
|
|
2211
|
+
}), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
|
|
2212
|
+
|
|
2213
|
+
function semanticNamespace(semantic) {
|
|
2214
|
+
return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
|
|
2215
|
+
}
|
|
2469
2216
|
|
|
2470
2217
|
function hasUniqueSemanticNamespaces(semantics) {
|
|
2471
|
-
const namespaces = semantics.map(
|
|
2218
|
+
const namespaces = semantics.map(semanticNamespace);
|
|
2472
2219
|
return new Set(namespaces).size === namespaces.length;
|
|
2473
2220
|
}
|
|
2474
2221
|
|
|
2222
|
+
function semanticsFieldSchema(semantic) {
|
|
2223
|
+
return v.optional(v.pipe(v.array(semantic), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
|
|
2227
|
+
|
|
2475
2228
|
function actionFields(op, group) {
|
|
2476
2229
|
return {
|
|
2477
2230
|
name: NonEmpty,
|
|
2478
|
-
semantics:
|
|
2231
|
+
semantics: ActionSemanticsFieldSchema,
|
|
2479
2232
|
title: v.optional(v.string()),
|
|
2480
2233
|
description: v.optional(v.string()),
|
|
2481
2234
|
group: v.optional(group),
|
|
@@ -2522,6 +2275,7 @@ const StoredActionSchema = pinned()(v.strictObject({
|
|
|
2522
2275
|
function activityFields({field: field, action: action, target: target, group: group}) {
|
|
2523
2276
|
return {
|
|
2524
2277
|
name: NonEmpty,
|
|
2278
|
+
semantics: SemanticsFieldSchema,
|
|
2525
2279
|
title: v.optional(v.string()),
|
|
2526
2280
|
description: v.optional(v.string()),
|
|
2527
2281
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2595,6 +2349,7 @@ const GuardSchema = v.strictObject(guardFields(NonEmpty)), AuthoringGuardSchema
|
|
|
2595
2349
|
function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
|
|
2596
2350
|
return {
|
|
2597
2351
|
name: NonEmpty,
|
|
2352
|
+
semantics: SemanticsFieldSchema,
|
|
2598
2353
|
title: v.optional(v.string()),
|
|
2599
2354
|
description: v.optional(v.string()),
|
|
2600
2355
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2633,6 +2388,7 @@ const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KIN
|
|
|
2633
2388
|
function workflowFields({field: field, stage: stage, start: start}) {
|
|
2634
2389
|
return {
|
|
2635
2390
|
name: NonEmpty,
|
|
2391
|
+
semantics: SemanticsFieldSchema,
|
|
2636
2392
|
title: NonEmpty,
|
|
2637
2393
|
description: v.optional(v.string()),
|
|
2638
2394
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2829,11 +2585,20 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
|
|
|
2829
2585
|
});
|
|
2830
2586
|
}
|
|
2831
2587
|
|
|
2588
|
+
function actionSites(def) {
|
|
2589
|
+
return def.stages.flatMap((stage, i) => (stage.activities ?? []).flatMap((activity, j) => (activity.actions ?? []).map((action, a) => ({
|
|
2590
|
+
action: action,
|
|
2591
|
+
activity: activity,
|
|
2592
|
+
stage: stage,
|
|
2593
|
+
path: [ "stages", i, "activities", j, "actions", a ]
|
|
2594
|
+
}))));
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2832
2597
|
function effectNameSites(def) {
|
|
2833
2598
|
const sites = [];
|
|
2834
|
-
for (const
|
|
2599
|
+
for (const {action: action, path: path} of actionSites(def)) collectEffects({
|
|
2835
2600
|
effects: action.effects,
|
|
2836
|
-
path: [
|
|
2601
|
+
path: [ ...path, "effects" ],
|
|
2837
2602
|
sites: sites
|
|
2838
2603
|
});
|
|
2839
2604
|
return sites;
|
|
@@ -2964,13 +2729,22 @@ function checkUnboundConditionVars(def, issues) {
|
|
|
2964
2729
|
}
|
|
2965
2730
|
}
|
|
2966
2731
|
|
|
2732
|
+
const SOFT_GATE_VARS = {
|
|
2733
|
+
can: {
|
|
2734
|
+
noun: "the caller's grants"
|
|
2735
|
+
},
|
|
2736
|
+
attributes: {
|
|
2737
|
+
noun: "the caller's org-level user attributes"
|
|
2738
|
+
}
|
|
2739
|
+
}, SOFT_GATE_VAR_NAMES = Object.keys(SOFT_GATE_VARS);
|
|
2740
|
+
|
|
2967
2741
|
function unboundVarsAt(site) {
|
|
2968
2742
|
const callerVars = unboundCallerVars(site.policy);
|
|
2969
2743
|
return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
|
|
2970
2744
|
}
|
|
2971
2745
|
|
|
2972
2746
|
function unboundCallerVars(policy) {
|
|
2973
|
-
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [
|
|
2747
|
+
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ ...SOFT_GATE_VAR_NAMES, "params" ] : SOFT_GATE_VAR_NAMES;
|
|
2974
2748
|
}
|
|
2975
2749
|
|
|
2976
2750
|
const ROW_BINDING_CLAUSE = "$row (the discovered row in a spawn projection; the stored row under test in a where-op) is bound only while a spawn `with` projection or a where-op `where` evaluates";
|
|
@@ -2980,7 +2754,12 @@ function rowVarMessage(site) {
|
|
|
2980
2754
|
}
|
|
2981
2755
|
|
|
2982
2756
|
function callerVarMessage(site, name) {
|
|
2983
|
-
|
|
2757
|
+
if (site.policy === "cascade") return `${site.label} reads $${name} — cascade gates (transition \`when\`s, activity \`filter\`s, a cascade-fired action's \`when\`/\`filter\`) must resolve identically no matter whose token drives the cascade ($assigned is constant false, the other caller vars hold no value); gate on instance state (e.g. a field an action wrote), or pin executing identities with \`roles\``;
|
|
2758
|
+
if (site.policy === "caller-bound") return `${site.label} reads $${name} — $params (the firing action's args) is bound only while the action's effect bindings and where-op \`where\`s evaluate; this site never binds it, so the condition could never pass. Bind $params in an effect binding or a where-op instead, or gate on a field an action wrote`;
|
|
2759
|
+
if (site.policy === "triggered-payload" && name === "params") return `${site.label} reads $params — a cascade-fired action has no caller to supply args, so $params never holds a value in its payload; read fields or effect outputs instead`;
|
|
2760
|
+
const softGate = SOFT_GATE_VARS[name];
|
|
2761
|
+
if (softGate !== void 0) return `${site.label} reads $${name} — $${name} (${softGate.noun}) is bound only in the caller-bound projection (action filters, requirements, editable predicates); this site never binds it. Move the check to one of those sites or drop $${name}`;
|
|
2762
|
+
throw new Error(`callerVarMessage: unreachable for $${name} at ${site.label} (policy ${site.policy})`);
|
|
2984
2763
|
}
|
|
2985
2764
|
|
|
2986
2765
|
function conditionSites(def) {
|
|
@@ -3235,9 +3014,9 @@ function belowScopeNestedSites(args) {
|
|
|
3235
3014
|
}
|
|
3236
3015
|
|
|
3237
3016
|
function checkLevelKindEffectOutputs(def, issues) {
|
|
3238
|
-
for (const
|
|
3017
|
+
for (const {action: action, path: path} of actionSites(def)) pushOutputIssues({
|
|
3239
3018
|
action: action,
|
|
3240
|
-
path:
|
|
3019
|
+
path: path,
|
|
3241
3020
|
issues: issues
|
|
3242
3021
|
});
|
|
3243
3022
|
}
|
|
@@ -3319,18 +3098,18 @@ function storedRolesIssue(action) {
|
|
|
3319
3098
|
}
|
|
3320
3099
|
|
|
3321
3100
|
function checkStoredRolesPlacement(def, issues) {
|
|
3322
|
-
for (const
|
|
3101
|
+
for (const {action: action, path: path} of actionSites(def)) {
|
|
3323
3102
|
const message = storedRolesIssue(action);
|
|
3324
3103
|
message !== void 0 && issues.push({
|
|
3325
|
-
path: [
|
|
3104
|
+
path: [ ...path, "roles" ],
|
|
3326
3105
|
message: message
|
|
3327
3106
|
});
|
|
3328
3107
|
}
|
|
3329
3108
|
}
|
|
3330
3109
|
|
|
3331
3110
|
function checkTriggeredActionParams(def, issues) {
|
|
3332
|
-
for (const
|
|
3333
|
-
path: [
|
|
3111
|
+
for (const {action: action, path: path} of actionSites(def)) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
|
|
3112
|
+
path: [ ...path, "params" ],
|
|
3334
3113
|
message: `action "${action.name}" declares params but is cascade-fired (\`when\`) — no caller ever supplies args to a trigger. Drop the params, or drop \`when\` to make it a fireAction-fired action`
|
|
3335
3114
|
});
|
|
3336
3115
|
}
|
|
@@ -3375,13 +3154,20 @@ function checkSingleSubjectRequirements(def, issues) {
|
|
|
3375
3154
|
|
|
3376
3155
|
function checkStartFilterReads(def, issues) {
|
|
3377
3156
|
const filter = def.start?.filter;
|
|
3378
|
-
filter
|
|
3157
|
+
if (filter === void 0) return;
|
|
3158
|
+
const params = conditionParameterNames(filter);
|
|
3159
|
+
params.has("fields") && issues.push({
|
|
3379
3160
|
path: [ "start", "filter" ],
|
|
3380
3161
|
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"
|
|
3381
|
-
})
|
|
3162
|
+
});
|
|
3163
|
+
for (const name of CALLER_BOUND_VARS) params.has(name) && issues.push({
|
|
3164
|
+
path: [ "start", "filter" ],
|
|
3165
|
+
message: `start.filter reads $${name} — start.filter is a read-side document-visibility rule (definitionsForDocument / start controls); startInstance never evaluates it, and no caller bag binds there, so the read is GROQ null and the filter can never match. Drop $${name} from start.filter. A "who may start" rule belongs in a start requirement, not in the browse-time filter`
|
|
3166
|
+
});
|
|
3167
|
+
readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
3382
3168
|
path: [ "start", "filter" ],
|
|
3383
3169
|
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"
|
|
3384
|
-
})
|
|
3170
|
+
});
|
|
3385
3171
|
}
|
|
3386
3172
|
|
|
3387
3173
|
function checkStartRequirementReads(def, issues) {
|
|
@@ -3677,15 +3463,13 @@ function seedEarlierSiblingTarget(args) {
|
|
|
3677
3463
|
}
|
|
3678
3464
|
|
|
3679
3465
|
function opSites(def) {
|
|
3680
|
-
|
|
3681
|
-
for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) sites.push({
|
|
3466
|
+
return actionSites(def).map(({action: action, activity: activity, stage: stage, path: path}) => ({
|
|
3682
3467
|
ops: action.ops,
|
|
3683
|
-
path: [
|
|
3468
|
+
path: [ ...path, "ops" ],
|
|
3684
3469
|
label: `action "${action.name}"`,
|
|
3685
3470
|
stage: stage,
|
|
3686
3471
|
activity: activity
|
|
3687
|
-
});
|
|
3688
|
-
return sites;
|
|
3472
|
+
}));
|
|
3689
3473
|
}
|
|
3690
3474
|
|
|
3691
3475
|
function checkFieldReadOpValues(def, issues) {
|
|
@@ -3702,7 +3486,7 @@ function checkFieldReadOpValues(def, issues) {
|
|
|
3702
3486
|
}
|
|
3703
3487
|
|
|
3704
3488
|
function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
|
|
3705
|
-
for (const [o, op] of (ops ?? []).entries()) if ("value" in op) for (const {read: read, path: readPath} of fieldReadsIn(op.value, [ ...path, o, "value" ])) checkOpFieldRead({
|
|
3489
|
+
for (const [o, op] of (ops ?? []).entries()) if (!(!("value" in op) || op.value === void 0)) for (const {read: read, path: readPath} of fieldReadsIn(op.value, [ ...path, o, "value" ])) checkOpFieldRead({
|
|
3706
3490
|
...ctx,
|
|
3707
3491
|
read: read,
|
|
3708
3492
|
path: readPath,
|
|
@@ -3753,35 +3537,55 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
|
|
|
3753
3537
|
return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty. Known: ${known.join(", ") || "(none)"}`;
|
|
3754
3538
|
}
|
|
3755
3539
|
|
|
3756
|
-
function
|
|
3540
|
+
function checkFieldTargetOps(def, issues) {
|
|
3757
3541
|
const workflow = def.fields ?? [];
|
|
3758
3542
|
for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
workflow: workflow,
|
|
3762
|
-
stage: site.stage.fields ?? [],
|
|
3763
|
-
activity: site.activity.fields ?? []
|
|
3764
|
-
};
|
|
3765
|
-
checkUpdateWhereTargetKind({
|
|
3543
|
+
const path = [ ...site.path, o ], rule = TARGET_KIND_RULES[op.type];
|
|
3544
|
+
rule !== void 0 && "target" in op && checkTargetKind({
|
|
3766
3545
|
op: op,
|
|
3767
|
-
path: [ ...
|
|
3546
|
+
path: [ ...path, "target" ],
|
|
3768
3547
|
label: site.label,
|
|
3769
|
-
scopes:
|
|
3548
|
+
scopes: opTargetScopes(workflow, site),
|
|
3549
|
+
rule: rule,
|
|
3770
3550
|
issues: issues
|
|
3771
|
-
}), checkUpdateWhereMergeKeys({
|
|
3551
|
+
}), op.type === "field.updateWhere" && checkUpdateWhereMergeKeys({
|
|
3772
3552
|
op: op,
|
|
3773
|
-
path:
|
|
3553
|
+
path: path,
|
|
3774
3554
|
label: site.label,
|
|
3775
3555
|
issues: issues
|
|
3776
3556
|
});
|
|
3777
3557
|
}
|
|
3778
3558
|
}
|
|
3779
3559
|
|
|
3780
|
-
|
|
3560
|
+
const arithmeticTargetRule = {
|
|
3561
|
+
accepts: target => target.type === "number",
|
|
3562
|
+
issue: () => "arithmetic ops target `number` entries only"
|
|
3563
|
+
}, TARGET_KIND_RULES = {
|
|
3564
|
+
"field.inc": arithmeticTargetRule,
|
|
3565
|
+
"field.dec": arithmeticTargetRule,
|
|
3566
|
+
"field.setIfMissing": {
|
|
3567
|
+
accepts: target => !isAlwaysArrayFieldKind(target.type),
|
|
3568
|
+
issue: () => "setIfMissing applies to nullable entries only; an empty array entry already holds []"
|
|
3569
|
+
},
|
|
3570
|
+
"field.updateWhere": {
|
|
3571
|
+
accepts: target => target.type === "array",
|
|
3572
|
+
issue: target => "updateWhere merges declared row sub-fields, so its target must be an `array` entry" + rowOpsHint(target.type)
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
|
|
3576
|
+
function opTargetScopes(workflow, site) {
|
|
3577
|
+
return {
|
|
3578
|
+
workflow: workflow,
|
|
3579
|
+
stage: site.stage.fields ?? [],
|
|
3580
|
+
activity: site.activity.fields ?? []
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
function checkTargetKind({op: op, path: path, label: label, scopes: scopes, rule: rule, issues: issues}) {
|
|
3781
3585
|
const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
|
|
3782
|
-
target === void 0 || target
|
|
3783
|
-
path:
|
|
3784
|
-
message: `${label}
|
|
3586
|
+
target === void 0 || rule.accepts(target) || issues.push({
|
|
3587
|
+
path: path,
|
|
3588
|
+
message: `${label} ${op.type} targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — ${rule.issue(target)}`
|
|
3785
3589
|
});
|
|
3786
3590
|
}
|
|
3787
3591
|
|
|
@@ -4013,11 +3817,11 @@ function checkWorkflowInvariants(def) {
|
|
|
4013
3817
|
}), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
|
|
4014
3818
|
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
4015
3819
|
checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
4016
|
-
checkFieldReadOpValues(def, issues),
|
|
3820
|
+
checkFieldReadOpValues(def, issues), checkFieldTargetOps(def, issues), checkGuardFieldReads(def, issues),
|
|
4017
3821
|
checkAssigneesEntries(def, issues), checkDueDateEntries(def, issues), checkSubjectEntries(def, issues),
|
|
4018
3822
|
checkLevelKindEffectOutputs(def, issues), checkActivityTerminalPaths(def, issues),
|
|
4019
3823
|
checkTerminalStageActivities(def, issues), checkTriggeredActionParams(def, issues),
|
|
4020
3824
|
checkStoredRolesPlacement(def, issues), checkGroups(def, issues), issues;
|
|
4021
3825
|
}
|
|
4022
3826
|
|
|
4023
|
-
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,
|
|
3827
|
+
export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DECISION_SEMANTICS, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, MUTATION_GUARD_ACTIONS, NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, SIGNAL_SEMANTICS, START_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isAlwaysArrayFieldKind, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|