@sanity/workflow-engine 0.14.0 → 0.16.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 +779 -0
- package/DATAMODEL.md +252 -0
- package/README.md +2 -2
- package/dist/_chunks-cjs/invariants.cjs +3208 -0
- package/dist/_chunks-es/invariants.js +2976 -0
- package/dist/define.cjs +106 -944
- package/dist/define.d.cts +263 -411
- package/dist/define.d.ts +263 -411
- package/dist/define.js +81 -941
- package/dist/index.cjs +9882 -5220
- package/dist/index.d.cts +3099 -724
- package/dist/index.d.ts +3099 -724
- package/dist/index.js +9468 -5284
- package/package.json +9 -5
- package/dist/_chunks-cjs/schema.cjs +0 -1289
- package/dist/_chunks-cjs/schema.cjs.map +0 -1
- package/dist/_chunks-es/schema.js +0 -1274
- package/dist/_chunks-es/schema.js.map +0 -1
- package/dist/define.cjs.map +0 -1
- package/dist/define.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,2976 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
|
|
3
|
+
import { conditionOutcome, runGroq as runGroq$1, evaluateConditionOutcome as evaluateConditionOutcome$1 } from "@sanity/groq-condition-describe";
|
|
4
|
+
|
|
5
|
+
import { parse } from "groq-js";
|
|
6
|
+
|
|
7
|
+
function isCascadeFired(action) {
|
|
8
|
+
return action.when !== void 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function deriveActivityKind(activity) {
|
|
12
|
+
if (activity.target !== void 0) return "manual";
|
|
13
|
+
const actions = activity.actions ?? [];
|
|
14
|
+
return actions.some(a => !isCascadeFired(a)) ? "user" : actions.some(a => (a.effects ?? []).length > 0 || a.spawn !== void 0) ? "service" : actions.length > 0 ? "receive" : "script";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function deriveExecutorClassification(activity) {
|
|
18
|
+
if (activity.target !== void 0) return "off-system";
|
|
19
|
+
const actions = activity.actions ?? [], cascadeFired = actions.filter(isCascadeFired).length;
|
|
20
|
+
return cascadeFired === actions.length ? "autonomous" : cascadeFired === 0 ? "interactive" : "hybrid";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function driverKind(actor) {
|
|
24
|
+
return actor.kind === "person" || actor.kind === "agent" ? actor.kind : "service";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function errorMessage(err) {
|
|
28
|
+
return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function rethrowWithContext(err, context) {
|
|
32
|
+
throw new Error(`${context}: ${errorMessage(err)}`, {
|
|
33
|
+
cause: err
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function andConditions(parts) {
|
|
38
|
+
const present = parts.filter(p => p !== void 0);
|
|
39
|
+
if (present.length !== 0) return present.length === 1 ? present[0] : present.map(p => `(${p})`).join(" && ");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const KNOWN_SCHEMES = /* @__PURE__ */ new Set([ "dataset", "canvas", "media-library", "dashboard" ]), KNOWN_SCHEMES_TEXT = [ ...KNOWN_SCHEMES ].join(", ");
|
|
43
|
+
|
|
44
|
+
function parseGdr(uri) {
|
|
45
|
+
const colon = uri.indexOf(":");
|
|
46
|
+
if (colon < 0) throw new Error(`Invalid GDR "${uri}": must be a URI of form "<scheme>:<...id-parts>". Known schemes: ${KNOWN_SCHEMES_TEXT}.`);
|
|
47
|
+
const scheme = uri.slice(0, colon), rest = uri.slice(colon + 1);
|
|
48
|
+
if (!KNOWN_SCHEMES.has(scheme)) throw new Error(`Invalid GDR "${uri}": unknown scheme "${scheme}". Known: ${KNOWN_SCHEMES_TEXT}.`);
|
|
49
|
+
const parts = rest.split(":");
|
|
50
|
+
if (parts.some(part => part.length === 0)) throw new Error(`Invalid GDR "${uri}": id parts must be non-empty (no leading, trailing, or doubled ":").`);
|
|
51
|
+
if (scheme === "dataset") {
|
|
52
|
+
if (parts.length !== 3) throw new Error(`Invalid GDR "${uri}": dataset scheme requires <projectId>:<dataset>:<documentId> (3 parts after scheme); got ${parts.length}.`);
|
|
53
|
+
return {
|
|
54
|
+
scheme: "dataset",
|
|
55
|
+
projectId: parts[0],
|
|
56
|
+
dataset: parts[1],
|
|
57
|
+
documentId: parts[2]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (parts.length !== 2) throw new Error(`Invalid GDR "${uri}": ${scheme} scheme requires <resourceId>:<documentId> (2 parts after scheme); got ${parts.length}.`);
|
|
61
|
+
return {
|
|
62
|
+
scheme: scheme,
|
|
63
|
+
resourceId: parts[0],
|
|
64
|
+
documentId: parts[1]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function tryParseGdr(uri) {
|
|
69
|
+
try {
|
|
70
|
+
return parseGdr(uri);
|
|
71
|
+
} catch {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function gdrUri(parts) {
|
|
77
|
+
return parts.scheme === "dataset" ? `dataset:${parts.projectId}:${parts.dataset}:${parts.documentId}` : `${parts.scheme}:${parts.resourceId}:${parts.documentId}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function extractDocumentId(gdrUriString) {
|
|
81
|
+
return parseGdr(gdrUriString).documentId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const RESOURCE_ALIAS_NAME_SOURCE = "[a-z0-9][a-z0-9-]*", RESOURCE_ALIAS_NAME_RE = new RegExp(`^${RESOURCE_ALIAS_NAME_SOURCE}$`);
|
|
85
|
+
|
|
86
|
+
function validateResourceAliasName(name) {
|
|
87
|
+
if (!RESOURCE_ALIAS_NAME_RE.test(name)) throw new Error(`Invalid resource alias name "${name}": expected lowercase letters, digits, and dashes (no leading dash).`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function toPhysicalGdr(id, home) {
|
|
91
|
+
return isGdrUri(id) ? id : gdrFromResource(home, id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function refDataset({projectId: projectId, dataset: dataset, documentId: documentId, type: type}) {
|
|
95
|
+
return {
|
|
96
|
+
id: gdrUri({
|
|
97
|
+
scheme: "dataset",
|
|
98
|
+
projectId: projectId,
|
|
99
|
+
dataset: dataset,
|
|
100
|
+
documentId: documentId
|
|
101
|
+
}),
|
|
102
|
+
type: type
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function refCanvas({resourceId: resourceId, documentId: documentId, type: type}) {
|
|
107
|
+
return {
|
|
108
|
+
id: gdrUri({
|
|
109
|
+
scheme: "canvas",
|
|
110
|
+
resourceId: resourceId,
|
|
111
|
+
documentId: documentId
|
|
112
|
+
}),
|
|
113
|
+
type: type
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function refMediaLibrary({resourceId: resourceId, documentId: documentId, type: type}) {
|
|
118
|
+
return {
|
|
119
|
+
id: gdrUri({
|
|
120
|
+
scheme: "media-library",
|
|
121
|
+
resourceId: resourceId,
|
|
122
|
+
documentId: documentId
|
|
123
|
+
}),
|
|
124
|
+
type: type
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function refDashboard({resourceId: resourceId, documentId: documentId, type: type}) {
|
|
129
|
+
return {
|
|
130
|
+
id: gdrUri({
|
|
131
|
+
scheme: "dashboard",
|
|
132
|
+
resourceId: resourceId,
|
|
133
|
+
documentId: documentId
|
|
134
|
+
}),
|
|
135
|
+
type: type
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function datasetResourceParts(id) {
|
|
140
|
+
const dot = id.indexOf(".");
|
|
141
|
+
if (dot <= 0 || dot === id.length - 1) throw new Error(`Invalid dataset resource id "${id}": expected "<projectId>.<dataset>".`);
|
|
142
|
+
return {
|
|
143
|
+
projectId: id.slice(0, dot),
|
|
144
|
+
dataset: id.slice(dot + 1)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function clientConfigFromResource(res) {
|
|
149
|
+
return res.type === "dataset" ? datasetResourceParts(res.id) : {
|
|
150
|
+
resource: res
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function gdrFromResource(res, documentId) {
|
|
155
|
+
if (res.type === "dataset") {
|
|
156
|
+
const {projectId: projectId, dataset: dataset} = datasetResourceParts(res.id);
|
|
157
|
+
return gdrUri({
|
|
158
|
+
scheme: "dataset",
|
|
159
|
+
projectId: projectId,
|
|
160
|
+
dataset: dataset,
|
|
161
|
+
documentId: documentId
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return gdrUri({
|
|
165
|
+
scheme: res.type,
|
|
166
|
+
resourceId: res.id,
|
|
167
|
+
documentId: documentId
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resourceGdr(res) {
|
|
172
|
+
return `${res.type}:${res.id}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseResourceGdr(uri) {
|
|
176
|
+
const expected = 'expected "<type>:<id>" with no document part, e.g. "dataset:<projectId>.<dataset>" or "media-library:<resourceId>"', parts = uri.split(":");
|
|
177
|
+
if (parts.length !== 2 || parts.some(part => part.length === 0)) throw new Error(`Invalid resource GDR "${uri}": ${expected}.`);
|
|
178
|
+
const [type, id] = parts;
|
|
179
|
+
if (!KNOWN_SCHEMES.has(type)) throw new Error(`Invalid resource GDR "${uri}": unknown resource type "${type}". Known: ${KNOWN_SCHEMES_TEXT}.`);
|
|
180
|
+
return type === "dataset" && datasetResourceParts(id), {
|
|
181
|
+
type: type,
|
|
182
|
+
id: id
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function gdrResourcePrefix(res) {
|
|
187
|
+
const full = gdrFromResource(res, "x");
|
|
188
|
+
return full.slice(0, full.length - 1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resourceFromParsed(parsed) {
|
|
192
|
+
return parsed.scheme === "dataset" ? {
|
|
193
|
+
type: "dataset",
|
|
194
|
+
id: `${parsed.projectId}.${parsed.dataset}`
|
|
195
|
+
} : {
|
|
196
|
+
type: parsed.scheme,
|
|
197
|
+
id: parsed.resourceId ?? ""
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function resourceFromGdrUri(uri) {
|
|
202
|
+
const parsed = tryParseGdr(uri);
|
|
203
|
+
return parsed === void 0 ? void 0 : resourceFromParsed(parsed);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function sameResource(a, b) {
|
|
207
|
+
return a.type === b.type && a.id === b.id;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function selfGdr(doc) {
|
|
211
|
+
return gdrFromResource(doc.workflowResource, doc._id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function definitionDocId({tag: tag, definition: definition, version: version}) {
|
|
215
|
+
return `${tag}.${definition}.v${version}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isGdrUri(value) {
|
|
219
|
+
return typeof value == "string" && tryParseGdr(value) !== void 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function toBareId(id) {
|
|
223
|
+
return isGdrUri(id) ? extractDocumentId(id) : id;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function gdrRef({res: res, documentId: documentId, type: type}) {
|
|
227
|
+
return {
|
|
228
|
+
id: gdrFromResource(res, documentId),
|
|
229
|
+
type: type
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isGdr(value) {
|
|
234
|
+
return typeof value == "object" && value !== null && typeof value.id == "string" && typeof value.type == "string";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
class WorkflowError extends Error {
|
|
238
|
+
kind;
|
|
239
|
+
constructor(kind, message, options) {
|
|
240
|
+
super(message, options), this.kind = kind;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
class ContractViolationError extends WorkflowError {
|
|
245
|
+
constructor(message) {
|
|
246
|
+
super("contract-violation", message), this.name = "ContractViolationError";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
class InstanceNotFoundError extends WorkflowError {
|
|
251
|
+
instanceId;
|
|
252
|
+
constructor(args) {
|
|
253
|
+
super("instance-not-found", `Workflow instance ${args.instanceId} not found${args.detail ? ` (${args.detail})` : ""}`),
|
|
254
|
+
this.name = "InstanceNotFoundError", this.instanceId = args.instanceId;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
class DefinitionNotFoundError extends WorkflowError {
|
|
259
|
+
definition;
|
|
260
|
+
version;
|
|
261
|
+
constructor(args) {
|
|
262
|
+
super("definition-not-found", args.version !== void 0 ? `Workflow definition ${args.definition} v${args.version} not deployed` : `Workflow definition ${args.definition} has no deployed versions`),
|
|
263
|
+
this.name = "DefinitionNotFoundError", this.definition = args.definition, args.version !== void 0 && (this.version = args.version);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
class DefinitionInUseError extends WorkflowError {
|
|
268
|
+
definition;
|
|
269
|
+
blockedBy;
|
|
270
|
+
constructor(args) {
|
|
271
|
+
super("definition-in-use", definitionInUseMessage(args.definition, args.blockedBy)),
|
|
272
|
+
this.name = "DefinitionInUseError", this.definition = args.definition, this.blockedBy = args.blockedBy;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function definitionInUseMessage(definition, blockedBy) {
|
|
277
|
+
if (blockedBy.reason === "non-terminal-instances") {
|
|
278
|
+
const head = blockedBy.instanceIds.slice(0, 3).join(", "), preview = blockedBy.instanceIds.length > 3 ? `${head}, …` : head;
|
|
279
|
+
return `Cannot delete ${definition}: ${blockedBy.instanceIds.length} non-terminal instance(s) exist (${preview}). Pass cascade to abort them first — instances are aborted in place, never deleted.`;
|
|
280
|
+
}
|
|
281
|
+
const names = blockedBy.referrers.map(r => `${r.definition} v${r.version}`).join(", ");
|
|
282
|
+
return `Cannot delete ${definition}: still spawn-referenced by deployed definition(s) ${names}. Delete or redeploy the referrer(s) first.`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
class EffectNotFoundError extends WorkflowError {
|
|
286
|
+
instanceId;
|
|
287
|
+
effectKey;
|
|
288
|
+
settled;
|
|
289
|
+
constructor(args) {
|
|
290
|
+
super("effect-not-found", effectNotFoundMessage(args)), this.name = "EffectNotFoundError",
|
|
291
|
+
this.instanceId = args.instanceId, this.effectKey = args.effectKey, args.settled !== void 0 && (this.settled = args.settled);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function effectNotFoundMessage(args) {
|
|
296
|
+
const base = `Pending effect "${args.effectKey}" not found on instance ${args.instanceId}`;
|
|
297
|
+
if (args.settled === void 0) return base;
|
|
298
|
+
const cause = args.settled.detail !== void 0 ? ` (${args.settled.detail})` : "";
|
|
299
|
+
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}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const TAG_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
303
|
+
|
|
304
|
+
function validateTag(tag) {
|
|
305
|
+
if (!TAG_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${TAG_RE.source} (ASCII lowercase + digits + dashes, no leading dash, no dots)`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function tagScopeFilter() {
|
|
309
|
+
return "tag == $tag";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const NonEmptyString = v.pipe(v.string(), v.nonEmpty("must not be empty"));
|
|
313
|
+
|
|
314
|
+
function asPredicate(validate) {
|
|
315
|
+
return value => {
|
|
316
|
+
try {
|
|
317
|
+
return validate(value), !0;
|
|
318
|
+
} catch {
|
|
319
|
+
return !1;
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v.variant("type", [ v.object({
|
|
325
|
+
type: v.literal("dataset"),
|
|
326
|
+
id: v.pipe(NonEmptyString, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
|
|
327
|
+
}), v.object({
|
|
328
|
+
type: v.literal("canvas"),
|
|
329
|
+
id: NonEmptyString
|
|
330
|
+
}), v.object({
|
|
331
|
+
type: v.literal("media-library"),
|
|
332
|
+
id: NonEmptyString
|
|
333
|
+
}), v.object({
|
|
334
|
+
type: v.literal("dashboard"),
|
|
335
|
+
id: NonEmptyString
|
|
336
|
+
}) ]), ResourceBindingSchema = v.object({
|
|
337
|
+
name: v.pipe(NonEmptyString, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
|
|
338
|
+
resource: WorkflowResourceSchema
|
|
339
|
+
}), DefinitionSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v.object({
|
|
340
|
+
name: NonEmptyString,
|
|
341
|
+
tag: v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
|
|
342
|
+
workflowResource: WorkflowResourceSchema,
|
|
343
|
+
resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => new Set(bindings.map(binding => binding.name)).size === bindings.length, "duplicate resource handle name — each binding name must be unique within a deployment"))),
|
|
344
|
+
definitions: v.pipe(v.array(DefinitionSchema), v.minLength(1, "a deployment needs at least one definition"))
|
|
345
|
+
}), TelemetryLoggerSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v.object({
|
|
346
|
+
deployments: v.pipe(v.array(DeploymentSchema), v.minLength(1, "a config needs at least one deployment"), v.check(deployments => new Set(deployments.map(deployment => deployment.tag)).size === deployments.length, "duplicate deployment tag — each deployment must use a unique tag")),
|
|
347
|
+
telemetry: v.optional(TelemetryLoggerSchema)
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
function resourceAliasesToMap(resourceAliases) {
|
|
351
|
+
return Object.fromEntries((resourceAliases ?? []).map(binding => [ binding.name, binding.resource ]));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const FIELD_READ = /^\$fields\.(\w+)(?:\.(.+))?$/, EFFECTS_READ = /^\$effects\['([^']+)'\](?:\.(.+))?$/;
|
|
355
|
+
|
|
356
|
+
function isGuardReadExpr(expr) {
|
|
357
|
+
return expr === "$self" || expr === "$now" || FIELD_READ.test(expr) || EFFECTS_READ.test(expr);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function printGuardRead(read) {
|
|
361
|
+
switch (read.type) {
|
|
362
|
+
case "self":
|
|
363
|
+
return "$self";
|
|
364
|
+
|
|
365
|
+
case "now":
|
|
366
|
+
return "$now";
|
|
367
|
+
|
|
368
|
+
case "fieldRead":
|
|
369
|
+
return read.path === void 0 ? `$fields.${read.field}` : `$fields.${read.field}.${read.path}`;
|
|
370
|
+
|
|
371
|
+
case "effectsRead":
|
|
372
|
+
return read.path === void 0 ? `$effects['${read.effect}']` : `$effects['${read.effect}'].${read.path}`;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const UNIVERSAL_ROLE_ALIAS_KEY = "$all";
|
|
377
|
+
|
|
378
|
+
function normalizeRoleAliases(aliases) {
|
|
379
|
+
if (aliases === void 0 || !("*" in aliases)) return aliases;
|
|
380
|
+
const {["*"]: universal, ...perRole} = aliases;
|
|
381
|
+
return {
|
|
382
|
+
...perRole,
|
|
383
|
+
[UNIVERSAL_ROLE_ALIAS_KEY]: universal
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function expandRequiredRoles(required, aliases) {
|
|
388
|
+
if (aliases === void 0) return [ ...required ];
|
|
389
|
+
const out = /* @__PURE__ */ new Set;
|
|
390
|
+
for (const role of required) {
|
|
391
|
+
out.add(role);
|
|
392
|
+
for (const fulfiller of aliases[role] ?? []) out.add(fulfiller);
|
|
393
|
+
}
|
|
394
|
+
for (const fulfiller of aliases[UNIVERSAL_ROLE_ALIAS_KEY] ?? []) out.add(fulfiller);
|
|
395
|
+
return [ ...out ];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function actorFulfillsRole({actorRoles: actorRoles, required: required, aliases: aliases}) {
|
|
399
|
+
if (actorRoles === void 0 || actorRoles.length === 0) return !1;
|
|
400
|
+
const accepted = new Set(expandRequiredRoles([ required ], aliases));
|
|
401
|
+
return actorRoles.some(role => accepted.has(role));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function groq(strings, ...values) {
|
|
405
|
+
return strings.reduce((out, part, i) => i === 0 ? part : `${out}${serializeGroqValue(values[i - 1])}${part}`, "");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function serializeGroqValue(value) {
|
|
409
|
+
const serialized = JSON.stringify(value);
|
|
410
|
+
if (serialized === void 0) throw new Error(`groq tag cannot serialize ${typeof value} — interpolate JSON-representable values only`);
|
|
411
|
+
return serialized;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function desugarWorkflow(authoring) {
|
|
415
|
+
const issues = [];
|
|
416
|
+
checkReservedRoleAliasKeys(authoring.roleAliases, issues);
|
|
417
|
+
const roleAliases = normalizeRoleAliases(authoring.roleAliases), ctx = {
|
|
418
|
+
issues: issues,
|
|
419
|
+
claimFields: /* @__PURE__ */ new Map,
|
|
420
|
+
claimedFields: /* @__PURE__ */ new Set,
|
|
421
|
+
roleAliases: roleAliases
|
|
422
|
+
}, workflowFields2 = desugarFieldEntries({
|
|
423
|
+
entries: authoring.fields,
|
|
424
|
+
path: [ "fields" ],
|
|
425
|
+
ctx: ctx
|
|
426
|
+
}), workflowLayer = layerOf(workflowFields2), stages = authoring.stages.map((stage, i) => {
|
|
427
|
+
const path = [ "stages", i ], stageFields2 = desugarFieldEntries({
|
|
428
|
+
entries: stage.fields,
|
|
429
|
+
path: [ ...path, "fields" ],
|
|
430
|
+
ctx: ctx
|
|
431
|
+
}), stageEnv = {
|
|
432
|
+
layers: [ {
|
|
433
|
+
scope: "stage",
|
|
434
|
+
entries: layerOf(stageFields2)
|
|
435
|
+
}, {
|
|
436
|
+
scope: "workflow",
|
|
437
|
+
entries: workflowLayer
|
|
438
|
+
} ]
|
|
439
|
+
}, activities = (stage.activities ?? []).map((activity, j) => desugarActivity({
|
|
440
|
+
activity: activity,
|
|
441
|
+
path: [ ...path, "activities", j ],
|
|
442
|
+
stageEnv: stageEnv,
|
|
443
|
+
ctx: ctx
|
|
444
|
+
})), transitions = (stage.transitions ?? []).map(transition => desugarTransition({
|
|
445
|
+
transition: transition
|
|
446
|
+
})), editable = desugarStageEditable({
|
|
447
|
+
overrides: stage.editable,
|
|
448
|
+
inScope: editableFieldNames({
|
|
449
|
+
workflowFields: workflowFields2,
|
|
450
|
+
stageFields: stageFields2,
|
|
451
|
+
activities: activities
|
|
452
|
+
}),
|
|
453
|
+
path: [ ...path, "editable" ],
|
|
454
|
+
ctx: ctx
|
|
455
|
+
});
|
|
456
|
+
return {
|
|
457
|
+
...stripUndefined({
|
|
458
|
+
name: stage.name,
|
|
459
|
+
title: stage.title,
|
|
460
|
+
description: stage.description,
|
|
461
|
+
groups: stage.groups,
|
|
462
|
+
guards: stage.guards?.map(desugarGuard)
|
|
463
|
+
}),
|
|
464
|
+
...stageFields2 ? {
|
|
465
|
+
fields: stageFields2
|
|
466
|
+
} : {},
|
|
467
|
+
...activities.length > 0 ? {
|
|
468
|
+
activities: activities
|
|
469
|
+
} : {},
|
|
470
|
+
...transitions.length > 0 ? {
|
|
471
|
+
transitions: transitions
|
|
472
|
+
} : {},
|
|
473
|
+
...editable ? {
|
|
474
|
+
editable: editable
|
|
475
|
+
} : {}
|
|
476
|
+
};
|
|
477
|
+
});
|
|
478
|
+
return checkUnclaimedClaimFields(ctx), {
|
|
479
|
+
definition: {
|
|
480
|
+
...stripUndefined({
|
|
481
|
+
name: authoring.name,
|
|
482
|
+
title: authoring.title,
|
|
483
|
+
description: authoring.description,
|
|
484
|
+
groups: authoring.groups,
|
|
485
|
+
lifecycle: authoring.lifecycle,
|
|
486
|
+
start: desugarStart(authoring.start),
|
|
487
|
+
initialStage: authoring.initialStage,
|
|
488
|
+
predicates: authoring.predicates,
|
|
489
|
+
roleAliases: roleAliases
|
|
490
|
+
}),
|
|
491
|
+
...workflowFields2 ? {
|
|
492
|
+
fields: workflowFields2
|
|
493
|
+
} : {},
|
|
494
|
+
stages: stages
|
|
495
|
+
},
|
|
496
|
+
issues: ctx.issues
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function desugarStart(start) {
|
|
501
|
+
if (start !== void 0) return {
|
|
502
|
+
kind: start.kind ?? "interactive",
|
|
503
|
+
...start.filter !== void 0 ? {
|
|
504
|
+
filter: start.filter
|
|
505
|
+
} : {}
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function checkReservedRoleAliasKeys(aliases, issues) {
|
|
510
|
+
for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
|
|
511
|
+
path: [ "roleAliases", key ],
|
|
512
|
+
message: `role alias key "${key}" uses the reserved "$" prefix — that namespace is the engine's stored spelling for the universal fulfiller. Use "*" to mean "fulfills any gate", or rename the role.`
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const TODOLIST_OF = [ {
|
|
517
|
+
type: "string",
|
|
518
|
+
name: "label",
|
|
519
|
+
title: "Label"
|
|
520
|
+
}, {
|
|
521
|
+
type: "string",
|
|
522
|
+
name: "status",
|
|
523
|
+
title: "Status"
|
|
524
|
+
}, {
|
|
525
|
+
type: "assignee",
|
|
526
|
+
name: "assignee",
|
|
527
|
+
title: "Assignee"
|
|
528
|
+
}, {
|
|
529
|
+
type: "date",
|
|
530
|
+
name: "dueDate",
|
|
531
|
+
title: "Due date"
|
|
532
|
+
} ], NOTES_OF = [ {
|
|
533
|
+
type: "text",
|
|
534
|
+
name: "body",
|
|
535
|
+
title: "Body"
|
|
536
|
+
}, {
|
|
537
|
+
type: "actor",
|
|
538
|
+
name: "actor",
|
|
539
|
+
title: "Actor"
|
|
540
|
+
}, {
|
|
541
|
+
type: "datetime",
|
|
542
|
+
name: "at",
|
|
543
|
+
title: "At"
|
|
544
|
+
} ];
|
|
545
|
+
|
|
546
|
+
function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
|
|
547
|
+
return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
|
|
548
|
+
entry: entry,
|
|
549
|
+
path: [ ...path, i ],
|
|
550
|
+
ctx: ctx
|
|
551
|
+
}));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
|
|
555
|
+
if (entry.type === "claim") return desugarClaimField({
|
|
556
|
+
entry: entry,
|
|
557
|
+
path: path,
|
|
558
|
+
ctx: ctx
|
|
559
|
+
});
|
|
560
|
+
if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
|
|
561
|
+
entry: entry,
|
|
562
|
+
path: path,
|
|
563
|
+
ctx: ctx
|
|
564
|
+
});
|
|
565
|
+
const editable = normalizeEditable({
|
|
566
|
+
editable: entry.editable,
|
|
567
|
+
path: [ ...path, "editable" ],
|
|
568
|
+
ctx: ctx
|
|
569
|
+
});
|
|
570
|
+
return {
|
|
571
|
+
...stripUndefined({
|
|
572
|
+
type: entry.type,
|
|
573
|
+
name: entry.name,
|
|
574
|
+
title: entry.title,
|
|
575
|
+
description: entry.description,
|
|
576
|
+
group: normalizeGroup(entry.group),
|
|
577
|
+
required: entry.required,
|
|
578
|
+
initialValue: entry.initialValue,
|
|
579
|
+
editable: editable,
|
|
580
|
+
types: entry.types,
|
|
581
|
+
fields: entry.fields,
|
|
582
|
+
of: entry.of
|
|
583
|
+
})
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function desugarClaimField({entry: entry, path: path, ctx: ctx}) {
|
|
588
|
+
const desugared = {
|
|
589
|
+
...stripUndefined({
|
|
590
|
+
name: entry.name,
|
|
591
|
+
title: entry.title,
|
|
592
|
+
description: entry.description,
|
|
593
|
+
group: normalizeGroup(entry.group)
|
|
594
|
+
}),
|
|
595
|
+
type: "actor"
|
|
596
|
+
};
|
|
597
|
+
return ctx.claimFields.set(desugared, {
|
|
598
|
+
name: entry.name,
|
|
599
|
+
path: path
|
|
600
|
+
}), desugared;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function desugarListField({entry: entry, path: path, ctx: ctx}) {
|
|
604
|
+
const editable = normalizeEditable({
|
|
605
|
+
editable: entry.editable,
|
|
606
|
+
path: [ ...path, "editable" ],
|
|
607
|
+
ctx: ctx
|
|
608
|
+
});
|
|
609
|
+
return {
|
|
610
|
+
...stripUndefined({
|
|
611
|
+
name: entry.name,
|
|
612
|
+
title: entry.title,
|
|
613
|
+
description: entry.description,
|
|
614
|
+
group: normalizeGroup(entry.group),
|
|
615
|
+
required: entry.required,
|
|
616
|
+
initialValue: entry.initialValue,
|
|
617
|
+
editable: editable
|
|
618
|
+
}),
|
|
619
|
+
type: "array",
|
|
620
|
+
of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
|
|
625
|
+
if (editable === void 0 || editable === !0) return editable;
|
|
626
|
+
if (Array.isArray(editable)) {
|
|
627
|
+
const condition = rolesCondition(editable, ctx.roleAliases);
|
|
628
|
+
if (condition === void 0) {
|
|
629
|
+
ctx.issues.push({
|
|
630
|
+
path: path,
|
|
631
|
+
message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
return condition;
|
|
636
|
+
}
|
|
637
|
+
return editable;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function editableFieldNames({workflowFields: workflowFields2, stageFields: stageFields2, activities: activities}) {
|
|
641
|
+
const names = /* @__PURE__ */ new Set, collect = entries => {
|
|
642
|
+
for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
|
|
643
|
+
};
|
|
644
|
+
collect(workflowFields2), collect(stageFields2);
|
|
645
|
+
for (const activity of activities) collect(activity.fields);
|
|
646
|
+
return names;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
|
|
650
|
+
if (overrides === void 0) return;
|
|
651
|
+
const out = {};
|
|
652
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
653
|
+
if (!inScope.has(name)) {
|
|
654
|
+
ctx.issues.push({
|
|
655
|
+
path: [ ...path, name ],
|
|
656
|
+
message: `stage editable override "${name}" does not narrow an editable field in scope — name a workflow/stage/activity field of this stage that declares \`editable\``
|
|
657
|
+
});
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const normalized = normalizeEditable({
|
|
661
|
+
editable: value,
|
|
662
|
+
path: [ ...path, name ],
|
|
663
|
+
ctx: ctx
|
|
664
|
+
});
|
|
665
|
+
normalized !== void 0 && (out[name] = normalized);
|
|
666
|
+
}
|
|
667
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function layerOf(entries) {
|
|
671
|
+
return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function normalizeGroup(group) {
|
|
675
|
+
return group === void 0 || Array.isArray(group) ? group : [ group ];
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
|
|
679
|
+
const activityFields2 = desugarFieldEntries({
|
|
680
|
+
entries: activity.fields,
|
|
681
|
+
path: [ ...path, "fields" ],
|
|
682
|
+
ctx: ctx
|
|
683
|
+
}), env = {
|
|
684
|
+
layers: [ {
|
|
685
|
+
scope: "activity",
|
|
686
|
+
entries: layerOf(activityFields2)
|
|
687
|
+
}, ...stageEnv.layers ]
|
|
688
|
+
}, actions = (activity.actions ?? []).map((action, a) => desugarAction({
|
|
689
|
+
action: action,
|
|
690
|
+
path: [ ...path, "actions", a ],
|
|
691
|
+
env: env,
|
|
692
|
+
activityName: activity.name,
|
|
693
|
+
ctx: ctx
|
|
694
|
+
})), target = desugarTarget({
|
|
695
|
+
target: activity.target,
|
|
696
|
+
env: env,
|
|
697
|
+
path: [ ...path, "target" ],
|
|
698
|
+
ctx: ctx
|
|
699
|
+
});
|
|
700
|
+
return {
|
|
701
|
+
...stripUndefined({
|
|
702
|
+
name: activity.name,
|
|
703
|
+
title: activity.title,
|
|
704
|
+
description: activity.description,
|
|
705
|
+
groups: activity.groups,
|
|
706
|
+
group: normalizeGroup(activity.group),
|
|
707
|
+
filter: activity.filter,
|
|
708
|
+
requirements: activity.requirements
|
|
709
|
+
}),
|
|
710
|
+
...target ? {
|
|
711
|
+
target: target
|
|
712
|
+
} : {},
|
|
713
|
+
...activityFields2 ? {
|
|
714
|
+
fields: activityFields2
|
|
715
|
+
} : {},
|
|
716
|
+
...actions.length > 0 ? {
|
|
717
|
+
actions: actions
|
|
718
|
+
} : {}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
|
|
723
|
+
|
|
724
|
+
function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
|
|
725
|
+
if (target === void 0 || target.type === "url") return target;
|
|
726
|
+
const ref = typeof target.field == "string" ? {
|
|
727
|
+
field: target.field
|
|
728
|
+
} : target.field, field = resolveRef({
|
|
729
|
+
ref: ref,
|
|
730
|
+
env: env,
|
|
731
|
+
path: [ ...path, "field" ],
|
|
732
|
+
ctx: ctx
|
|
733
|
+
}) ?? fallbackRef(ref), entry = entryAt(env, field);
|
|
734
|
+
return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
|
|
735
|
+
path: [ ...path, "field" ],
|
|
736
|
+
message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
|
|
737
|
+
}), {
|
|
738
|
+
type: "field",
|
|
739
|
+
field: field
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
|
|
744
|
+
if ("type" in action) return desugarClaimAction({
|
|
745
|
+
action: action,
|
|
746
|
+
path: path,
|
|
747
|
+
env: env,
|
|
748
|
+
ctx: ctx
|
|
749
|
+
});
|
|
750
|
+
action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
|
|
751
|
+
path: [ ...path, "roles" ],
|
|
752
|
+
message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
|
|
753
|
+
});
|
|
754
|
+
const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
|
|
755
|
+
ops: action.ops,
|
|
756
|
+
path: [ ...path, "ops" ],
|
|
757
|
+
env: env,
|
|
758
|
+
firingActivity: activityName,
|
|
759
|
+
ctx: ctx
|
|
760
|
+
}) ?? [];
|
|
761
|
+
return action.status !== void 0 && ops.push({
|
|
762
|
+
type: "status.set",
|
|
763
|
+
activity: activityName,
|
|
764
|
+
status: action.status
|
|
765
|
+
}), {
|
|
766
|
+
...stripUndefined({
|
|
767
|
+
name: action.name,
|
|
768
|
+
title: action.title,
|
|
769
|
+
description: action.description,
|
|
770
|
+
group: normalizeGroup(action.group),
|
|
771
|
+
when: action.when,
|
|
772
|
+
params: action.params,
|
|
773
|
+
effects: action.effects,
|
|
774
|
+
spawn: action.spawn
|
|
775
|
+
}),
|
|
776
|
+
...cascadeFired && action.roles !== void 0 && action.roles.length > 0 ? {
|
|
777
|
+
roles: action.roles
|
|
778
|
+
} : {},
|
|
779
|
+
...filter ? {
|
|
780
|
+
filter: filter
|
|
781
|
+
} : {},
|
|
782
|
+
...ops.length > 0 ? {
|
|
783
|
+
ops: ops
|
|
784
|
+
} : {}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function desugarClaimAction({action: action, path: path, env: env, ctx: ctx}) {
|
|
789
|
+
const ref = typeof action.field == "string" ? {
|
|
790
|
+
field: action.field
|
|
791
|
+
} : action.field, resolved = resolveRef({
|
|
792
|
+
ref: ref,
|
|
793
|
+
env: env,
|
|
794
|
+
path: [ ...path, "field" ],
|
|
795
|
+
ctx: ctx
|
|
796
|
+
});
|
|
797
|
+
if (resolved) {
|
|
798
|
+
const entry = entryAt(env, resolved);
|
|
799
|
+
entry && entry.type !== "actor" && ctx.issues.push({
|
|
800
|
+
path: [ ...path, "field" ],
|
|
801
|
+
message: `claim action "${action.name}" references "${resolved.field}" of kind "${entry.type}" — a claim pair needs an actor-valued entry (a "claim" or "actor" field declaration)`
|
|
802
|
+
}), checkShadowedClaimTarget({
|
|
803
|
+
actionName: action.name,
|
|
804
|
+
field: ref.field,
|
|
805
|
+
resolved: resolved,
|
|
806
|
+
env: env,
|
|
807
|
+
path: [ ...path, "field" ],
|
|
808
|
+
ctx: ctx
|
|
809
|
+
}), entry && ctx.claimedFields.add(entry);
|
|
810
|
+
}
|
|
811
|
+
const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([ noSteal, rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = resolved ? [ {
|
|
812
|
+
type: "field.set",
|
|
813
|
+
target: resolved,
|
|
814
|
+
value: {
|
|
815
|
+
type: "actor"
|
|
816
|
+
}
|
|
817
|
+
} ] : [];
|
|
818
|
+
return {
|
|
819
|
+
...stripUndefined({
|
|
820
|
+
name: action.name,
|
|
821
|
+
title: action.title,
|
|
822
|
+
description: action.description,
|
|
823
|
+
group: normalizeGroup(action.group),
|
|
824
|
+
params: action.params,
|
|
825
|
+
effects: action.effects
|
|
826
|
+
}),
|
|
827
|
+
filter: filter,
|
|
828
|
+
...ops.length > 0 ? {
|
|
829
|
+
ops: ops
|
|
830
|
+
} : {}
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function checkShadowedClaimTarget({actionName: actionName, field: field, resolved: resolved, env: env, path: path, ctx: ctx}) {
|
|
835
|
+
const nearest = env.layers.find(l => l.entries.has(field));
|
|
836
|
+
nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
|
|
837
|
+
path: path,
|
|
838
|
+
message: `claim action "${actionName}" targets "${field}" at scope "${resolved.scope}", but a nearer ${nearest.scope}-scope entry of the same name shadows it in $fields — the no-steal filter would read the shadowing entry while the claim writes the "${resolved.scope}" one. Rename one of the entries or claim the nearer one`
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function checkUnclaimedClaimFields(ctx) {
|
|
843
|
+
for (const [entry, {name: name, path: path}] of ctx.claimFields) ctx.claimedFields.has(entry) || ctx.issues.push({
|
|
844
|
+
path: path,
|
|
845
|
+
message: `claim field "${name}" is never referenced by a claim action — you announced the pattern and wrote half of it. Declare a defineAction({ type: "claim", field: "${name}" }) or use a raw actor entry`
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const DEFAULT_TRANSITION_WHEN = "$allActivitiesDone";
|
|
850
|
+
|
|
851
|
+
function desugarTransition({transition: transition}) {
|
|
852
|
+
return {
|
|
853
|
+
...stripUndefined({
|
|
854
|
+
name: transition.name,
|
|
855
|
+
title: transition.title,
|
|
856
|
+
description: transition.description,
|
|
857
|
+
to: transition.to
|
|
858
|
+
}),
|
|
859
|
+
when: transition.when ?? DEFAULT_TRANSITION_WHEN
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function desugarGuard(guard) {
|
|
864
|
+
const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
|
|
865
|
+
return {
|
|
866
|
+
...rest,
|
|
867
|
+
match: {
|
|
868
|
+
...matchRest,
|
|
869
|
+
...idRefs !== void 0 ? {
|
|
870
|
+
idRefs: idRefs.map(printGuardRead)
|
|
871
|
+
} : {}
|
|
872
|
+
},
|
|
873
|
+
...metadata !== void 0 ? {
|
|
874
|
+
metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
|
|
875
|
+
} : {}
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
|
|
880
|
+
if (ops) return ops.map((op, i) => desugarOp({
|
|
881
|
+
op: op,
|
|
882
|
+
path: [ ...path, i ],
|
|
883
|
+
env: env,
|
|
884
|
+
firingActivity: firingActivity,
|
|
885
|
+
ctx: ctx
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
|
|
890
|
+
if (op.type === "status.set") return {
|
|
891
|
+
type: "status.set",
|
|
892
|
+
activity: op.activity ?? firingActivity,
|
|
893
|
+
status: op.status
|
|
894
|
+
};
|
|
895
|
+
if (op.type === "audit") return desugarAuditOp({
|
|
896
|
+
op: op,
|
|
897
|
+
path: path,
|
|
898
|
+
env: env,
|
|
899
|
+
ctx: ctx
|
|
900
|
+
});
|
|
901
|
+
const target = resolveRef({
|
|
902
|
+
ref: op.target,
|
|
903
|
+
env: env,
|
|
904
|
+
path: [ ...path, "target" ],
|
|
905
|
+
ctx: ctx
|
|
906
|
+
}) ?? fallbackRef(op.target);
|
|
907
|
+
return {
|
|
908
|
+
...op,
|
|
909
|
+
target: target
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const AUDIT_STAMPS = {
|
|
914
|
+
actor: {
|
|
915
|
+
type: "actor"
|
|
916
|
+
},
|
|
917
|
+
at: {
|
|
918
|
+
type: "now"
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
|
|
923
|
+
const target = resolveRef({
|
|
924
|
+
ref: op.target,
|
|
925
|
+
env: env,
|
|
926
|
+
path: [ ...path, "target" ],
|
|
927
|
+
ctx: ctx
|
|
928
|
+
}) ?? fallbackRef(op.target);
|
|
929
|
+
if (op.value.type !== "object") return ctx.issues.push({
|
|
930
|
+
path: [ ...path, "value" ],
|
|
931
|
+
message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
|
|
932
|
+
}), {
|
|
933
|
+
type: "field.append",
|
|
934
|
+
target: target,
|
|
935
|
+
value: op.value
|
|
936
|
+
};
|
|
937
|
+
const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
|
|
938
|
+
...op.value.fields
|
|
939
|
+
};
|
|
940
|
+
for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
|
|
941
|
+
if (stamp in fields) {
|
|
942
|
+
ctx.issues.push({
|
|
943
|
+
path: [ ...path, "value", "fields", stamp ],
|
|
944
|
+
message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
|
|
945
|
+
});
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
fields[stamp] = source;
|
|
949
|
+
}
|
|
950
|
+
return {
|
|
951
|
+
type: "field.append",
|
|
952
|
+
target: target,
|
|
953
|
+
value: {
|
|
954
|
+
type: "object",
|
|
955
|
+
fields: fields
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
|
|
961
|
+
const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
|
|
962
|
+
for (const layer of layers) if (layer.entries.has(ref.field)) return {
|
|
963
|
+
scope: layer.scope,
|
|
964
|
+
field: ref.field
|
|
965
|
+
};
|
|
966
|
+
const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
|
|
967
|
+
ctx.issues.push({
|
|
968
|
+
path: path,
|
|
969
|
+
message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function entryAt(env, ref) {
|
|
974
|
+
return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function fallbackRef(ref) {
|
|
978
|
+
return {
|
|
979
|
+
scope: ref.scope ?? "workflow",
|
|
980
|
+
field: ref.field
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function rolesCondition(roles, aliases) {
|
|
985
|
+
if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function stripUndefined(obj) {
|
|
989
|
+
const out = {};
|
|
990
|
+
for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
|
|
991
|
+
return out;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function isUnevaluable(result) {
|
|
995
|
+
return result == null;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async function evaluateConditionOutcome(args) {
|
|
999
|
+
const {condition: condition, snapshot: snapshot, params: params} = args;
|
|
1000
|
+
return evaluateConditionOutcome$1({
|
|
1001
|
+
condition: condition,
|
|
1002
|
+
params: params,
|
|
1003
|
+
dataset: snapshot.docs
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function evaluateCondition(args) {
|
|
1008
|
+
return await evaluateConditionOutcome(args) === "satisfied";
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function evaluatePredicates(args) {
|
|
1012
|
+
const out = {};
|
|
1013
|
+
for (const [name, groq2] of Object.entries(args.predicates ?? {})) {
|
|
1014
|
+
const result = await runGroq({
|
|
1015
|
+
groq: groq2,
|
|
1016
|
+
params: args.params,
|
|
1017
|
+
snapshot: args.snapshot
|
|
1018
|
+
}), outcome = conditionOutcome(result);
|
|
1019
|
+
out[name] = outcome === "unevaluable" ? null : outcome === "satisfied";
|
|
1020
|
+
}
|
|
1021
|
+
return out;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function runGroq({groq: groq2, params: params, snapshot: snapshot}) {
|
|
1025
|
+
return runGroq$1({
|
|
1026
|
+
groq: groq2,
|
|
1027
|
+
params: params,
|
|
1028
|
+
dataset: snapshot.docs
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function conditionSyntaxIssues(groq2, boundVars) {
|
|
1033
|
+
const issues = [];
|
|
1034
|
+
let tree;
|
|
1035
|
+
try {
|
|
1036
|
+
tree = parse(groq2);
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
issues.push(errorMessage(err));
|
|
1039
|
+
}
|
|
1040
|
+
if (/\*\s*\[\s*_type\b/.test(groq2) && issues.push("condition scans by `_type` — that's a discovery query, not a predicate. Conditions evaluate against the in-memory snapshot (instance + ancestors + field-declared docs). To bring extra docs in scope, declare a `doc.ref` (or `doc.refs`) field entry on the workflow or this stage. For lake scans like \"all articles in this release\", use a spawn action's `forEach` instead."),
|
|
1041
|
+
boundVars !== void 0 && tree !== void 0) for (const name of conditionParameterNames(groq2)) boundVars.includes(name) || issues.push(`reads $${name}, which this scope does not bind — an unbound variable evaluates to GROQ null, so the condition silently never matches. Bound here: ` + boundVars.map(n => `$${n}`).join(", "));
|
|
1042
|
+
return issues;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function conditionParameterNames(groq2) {
|
|
1046
|
+
const read = /* @__PURE__ */ new Set;
|
|
1047
|
+
return walkAstNodes(tryParseGroq(groq2), node => {
|
|
1048
|
+
node.type === "Parameter" && typeof node.name == "string" && read.add(node.name);
|
|
1049
|
+
}), read;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function conditionFieldReadNames(groq2) {
|
|
1053
|
+
const read = /* @__PURE__ */ new Set;
|
|
1054
|
+
return walkAstNodes(tryParseGroq(groq2), node => {
|
|
1055
|
+
if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
|
|
1056
|
+
const base = node.base;
|
|
1057
|
+
base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
|
|
1058
|
+
}), read;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function conditionEffectReads(groq2) {
|
|
1062
|
+
const reads = [];
|
|
1063
|
+
return walkAstNodes(tryParseGroq(groq2), node => {
|
|
1064
|
+
if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
|
|
1065
|
+
const base = node.base;
|
|
1066
|
+
base?.type !== "AccessAttribute" || typeof base.name != "string" || base.base?.type === "Parameter" && base.base.name === "effects" && reads.push({
|
|
1067
|
+
effect: base.name,
|
|
1068
|
+
key: node.name
|
|
1069
|
+
});
|
|
1070
|
+
}), reads;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function readsRootDocument(groq2) {
|
|
1074
|
+
return nodeReadsRoot(tryParseGroq(groq2), 0);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const SCOPED_CHILD = {
|
|
1078
|
+
Filter: "expr",
|
|
1079
|
+
Projection: "expr",
|
|
1080
|
+
Map: "expr",
|
|
1081
|
+
FlatMap: "expr",
|
|
1082
|
+
PipeFuncCall: "args"
|
|
1083
|
+
};
|
|
1084
|
+
|
|
1085
|
+
function nodeReadsRoot(node, depth) {
|
|
1086
|
+
if (Array.isArray(node)) return node.some(item => nodeReadsRoot(item, depth));
|
|
1087
|
+
if (typeof node != "object" || node === null) return !1;
|
|
1088
|
+
const typed = node;
|
|
1089
|
+
if (depth === 0 && typed.type === "This" || depth === 0 && typed.type === "AccessAttribute" && typed.base === void 0) return !0;
|
|
1090
|
+
if (typed.type === "Parent") {
|
|
1091
|
+
const climb = typeof typed.n == "number" ? typed.n : 1;
|
|
1092
|
+
return depth - climb <= 0;
|
|
1093
|
+
}
|
|
1094
|
+
const scopedChild = typeof typed.type == "string" ? SCOPED_CHILD[typed.type] : void 0;
|
|
1095
|
+
return scopedChild !== void 0 ? Object.entries(node).some(([key, value]) => nodeReadsRoot(value, key === scopedChild ? depth + 1 : depth)) : Object.values(node).some(value => nodeReadsRoot(value, depth));
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function tryParseGroq(groq2) {
|
|
1099
|
+
try {
|
|
1100
|
+
return parse(groq2);
|
|
1101
|
+
} catch {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function walkAstNodes(node, visit) {
|
|
1107
|
+
if (Array.isArray(node)) {
|
|
1108
|
+
for (const item of node) walkAstNodes(item, visit);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (!(typeof node != "object" || node === null)) {
|
|
1112
|
+
visit(node);
|
|
1113
|
+
for (const value of Object.values(node)) walkAstNodes(value, visit);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const ACTIVITY_STATUSES = [ "active", "done", "skipped", "failed" ], TERMINAL_ACTIVITY_STATUSES = [ "done", "skipped", "failed" ];
|
|
1118
|
+
|
|
1119
|
+
function isTerminalActivityStatus(status) {
|
|
1120
|
+
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], CONDITION_VARS = [ {
|
|
1124
|
+
name: "self",
|
|
1125
|
+
binding: "always",
|
|
1126
|
+
label: "this workflow instance",
|
|
1127
|
+
description: "GDR URI of the instance document itself — `*[_id == $self][0]` reads the instance in the snapshot."
|
|
1128
|
+
}, {
|
|
1129
|
+
name: "fields",
|
|
1130
|
+
binding: "always",
|
|
1131
|
+
label: "the workflow's fields",
|
|
1132
|
+
description: "Declared field entries rendered by name (`$fields.<name>` is the value, no envelope). Stage/activity scopes overlay lexically; `doc.ref` values render the hydrated document when the snapshot holds it."
|
|
1133
|
+
}, {
|
|
1134
|
+
name: "parent",
|
|
1135
|
+
binding: "always",
|
|
1136
|
+
label: "the parent workflow",
|
|
1137
|
+
description: "The parent instance's GDR URI, `null` on a root instance."
|
|
1138
|
+
}, {
|
|
1139
|
+
name: "ancestors",
|
|
1140
|
+
binding: "always",
|
|
1141
|
+
label: "the workflow's ancestors",
|
|
1142
|
+
description: "GDR URIs of the ancestor chain, root first."
|
|
1143
|
+
}, {
|
|
1144
|
+
name: "stage",
|
|
1145
|
+
binding: "always",
|
|
1146
|
+
label: "the current stage",
|
|
1147
|
+
description: "The current stage's name."
|
|
1148
|
+
}, {
|
|
1149
|
+
name: "now",
|
|
1150
|
+
binding: "always",
|
|
1151
|
+
label: "the current time",
|
|
1152
|
+
description: "The ISO clock reading shared by every condition in one evaluation pass, so they agree on the time."
|
|
1153
|
+
}, {
|
|
1154
|
+
name: "context",
|
|
1155
|
+
binding: "always",
|
|
1156
|
+
label: "start-time context",
|
|
1157
|
+
description: "The instance's `context` bag: values seeded at `startInstance` plus a parent's `spawn.context` handoff, by entry name. Written only at start — never mutated afterwards."
|
|
1158
|
+
}, {
|
|
1159
|
+
name: "effects",
|
|
1160
|
+
binding: "always",
|
|
1161
|
+
label: "automation outputs",
|
|
1162
|
+
description: "Completed effects' outputs, namespaced by effect name (`$effects['<effect>'].<output>`) — each effect's latest completed run wins. Handler fuel — transition triggers should read instance/lake state (or $effectStatus) instead."
|
|
1163
|
+
}, {
|
|
1164
|
+
name: "effectStatus",
|
|
1165
|
+
binding: "always",
|
|
1166
|
+
label: "automation status",
|
|
1167
|
+
description: "Effect name → `'done'` | `'failed'` of the latest completed run queued during the current stage entry; absent until the effect drains for this entry. Re-entry-safe: runs queued under a prior entry never count, so `$effectStatus['<effect>'] == 'done'` (or `defined($effectStatus['<effect>'])` for settled-either-way) waits for the fresh run."
|
|
1168
|
+
}, {
|
|
1169
|
+
name: "activities",
|
|
1170
|
+
binding: "always",
|
|
1171
|
+
label: "this stage's activities",
|
|
1172
|
+
description: "The current stage's activity rows, statuses included."
|
|
1173
|
+
}, {
|
|
1174
|
+
name: "allActivitiesDone",
|
|
1175
|
+
binding: "always",
|
|
1176
|
+
label: "all activities in this stage are finished",
|
|
1177
|
+
description: "Every current-stage activity is `done` or `skipped` — the default transition gate."
|
|
1178
|
+
}, {
|
|
1179
|
+
name: "anyActivityFailed",
|
|
1180
|
+
binding: "always",
|
|
1181
|
+
label: "an activity in this stage has failed",
|
|
1182
|
+
description: "Some current-stage activity is `failed`."
|
|
1183
|
+
}, {
|
|
1184
|
+
name: "actor",
|
|
1185
|
+
binding: "caller",
|
|
1186
|
+
label: "you",
|
|
1187
|
+
description: "The acting identity (id + roles); `undefined` when no caller rides the evaluation. Never holds a value in the cascade gates (deploy-rejected there)."
|
|
1188
|
+
}, {
|
|
1189
|
+
name: "assigned",
|
|
1190
|
+
binding: "caller",
|
|
1191
|
+
label: "you are assigned to this activity",
|
|
1192
|
+
description: "Whether the caller matches the activity's assignees-kind field entry (by user id, or by a role under the definition's `roleAliases`); `false` outside an activity context. Constant `false` in the cascade gates (deploy-rejected there)."
|
|
1193
|
+
}, {
|
|
1194
|
+
name: "can",
|
|
1195
|
+
binding: "caller",
|
|
1196
|
+
label: "your permissions",
|
|
1197
|
+
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."
|
|
1198
|
+
}, {
|
|
1199
|
+
name: "row",
|
|
1200
|
+
binding: "spawn",
|
|
1201
|
+
label: "the spawned row",
|
|
1202
|
+
description: "One `spawn.forEach` result row, bound while its `with` map evaluates — and the row under test while a where-op `where` evaluates (per row)."
|
|
1203
|
+
}, {
|
|
1204
|
+
name: "params",
|
|
1205
|
+
binding: "caller",
|
|
1206
|
+
label: "the action's arguments",
|
|
1207
|
+
description: "The firing action's args — they hold values only while a fireAction-fired action's effect bindings and where-op `where`s evaluate (spawn sites don't bind it at all, and a cascade-fired action has no caller to supply args). Deploy rejects a read in the cascade gates, in a cascade-fired action's payload, and in the caller-bound projection (action filters, requirements, editable predicates): args exist only once the caller fires the action, after those sites have evaluated."
|
|
1208
|
+
}, {
|
|
1209
|
+
name: "subworkflows",
|
|
1210
|
+
binding: "always",
|
|
1211
|
+
label: "the spawned subworkflows",
|
|
1212
|
+
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`."
|
|
1213
|
+
} ], 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 = [ {
|
|
1214
|
+
name: "tag",
|
|
1215
|
+
description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
|
|
1216
|
+
}, {
|
|
1217
|
+
name: "definition",
|
|
1218
|
+
description: "The `name` of the definition under evaluation (its own start.filter binds it)."
|
|
1219
|
+
}, {
|
|
1220
|
+
name: "now",
|
|
1221
|
+
description: "The ISO clock reading of the evaluating engine."
|
|
1222
|
+
}, {
|
|
1223
|
+
name: "fields",
|
|
1224
|
+
description: "The candidate initialFields by entry name, when the read surface has them (the Studio start control: yes; a per-doc picker: no). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble."
|
|
1225
|
+
} ], GUARD_PREDICATE_VARS = [ {
|
|
1226
|
+
name: "guard",
|
|
1227
|
+
description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
|
|
1228
|
+
}, {
|
|
1229
|
+
name: "mutation",
|
|
1230
|
+
description: "The attempted mutation — `mutation.action` is the write kind being gated."
|
|
1231
|
+
} ], ACTOR_KINDS = [ "person", "agent", "system" ];
|
|
1232
|
+
|
|
1233
|
+
function releaseDocId(releaseName) {
|
|
1234
|
+
return `_.releases.${releaseName}`;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1238
|
+
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1239
|
+
return {
|
|
1240
|
+
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1241
|
+
type: "system.release",
|
|
1242
|
+
releaseName: releaseName
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function isTodoListItem(row) {
|
|
1247
|
+
if (typeof row != "object" || row === null) return !1;
|
|
1248
|
+
const candidate = row, status = candidate.status;
|
|
1249
|
+
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function declaredRowColumns(entry) {
|
|
1253
|
+
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function isTodoListEntry(entry) {
|
|
1257
|
+
const columns = declaredRowColumns(entry);
|
|
1258
|
+
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function isNotesEntry(entry) {
|
|
1262
|
+
const columns = declaredRowColumns(entry);
|
|
1263
|
+
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
class FieldValueShapeError extends WorkflowError {
|
|
1267
|
+
entryType;
|
|
1268
|
+
entryName;
|
|
1269
|
+
issues;
|
|
1270
|
+
constructor(args) {
|
|
1271
|
+
const issueText = args.issues.join("; ");
|
|
1272
|
+
super("field-value-shape", `Field entry ${args.mode} shape invalid for "${args.entryName}" (${args.entryType}): ${issueText}`),
|
|
1273
|
+
this.name = "FieldValueShapeError", this.entryType = args.entryType, this.entryName = args.entryName,
|
|
1274
|
+
this.issues = args.issues;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const GdrShape = v.looseObject({
|
|
1279
|
+
id: v.pipe(v.string(), v.check(s => isGdrUri(s), "must be a GDR URI")),
|
|
1280
|
+
type: v.pipe(v.string(), v.minLength(1))
|
|
1281
|
+
}), ReleaseRefShape = v.pipe(v.looseObject({
|
|
1282
|
+
id: v.pipe(v.string(), v.check(s => isGdrUri(s), "must be a GDR URI")),
|
|
1283
|
+
type: v.literal("system.release"),
|
|
1284
|
+
releaseName: v.pipe(v.string(), v.minLength(1))
|
|
1285
|
+
}), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = v.looseObject({
|
|
1286
|
+
kind: v.picklist(ACTOR_KINDS),
|
|
1287
|
+
id: v.pipe(v.string(), v.minLength(1)),
|
|
1288
|
+
roles: v.optional(v.array(v.string())),
|
|
1289
|
+
onBehalfOf: v.optional(v.string())
|
|
1290
|
+
}), AssigneeShape = v.union([ v.looseObject({
|
|
1291
|
+
type: v.literal("user"),
|
|
1292
|
+
id: v.pipe(v.string(), v.minLength(1))
|
|
1293
|
+
}), v.looseObject({
|
|
1294
|
+
type: v.literal("role"),
|
|
1295
|
+
role: v.pipe(v.string(), v.minLength(1))
|
|
1296
|
+
}) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableDateTime = v.union([ v.null(), v.pipe(v.string(), v.check(s => !Number.isNaN(Date.parse(s)), "must be an ISO-8601 datetime string")) ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, valueSchemas = {
|
|
1297
|
+
"doc.ref": v.union([ v.null(), GdrShape ]),
|
|
1298
|
+
"doc.refs": v.array(GdrShape),
|
|
1299
|
+
"release.ref": v.union([ v.null(), ReleaseRefShape ]),
|
|
1300
|
+
query: v.any(),
|
|
1301
|
+
string: NullableString,
|
|
1302
|
+
text: NullableString,
|
|
1303
|
+
number: NullableNumber,
|
|
1304
|
+
boolean: NullableBoolean,
|
|
1305
|
+
date: NullableDate,
|
|
1306
|
+
datetime: NullableDateTime,
|
|
1307
|
+
url: NullableUrl,
|
|
1308
|
+
actor: v.union([ v.null(), ActorShape ]),
|
|
1309
|
+
assignee: v.union([ v.null(), AssigneeShape ]),
|
|
1310
|
+
assignees: v.array(AssigneeShape)
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
function shapeValueSchema(shape, leaf) {
|
|
1314
|
+
return shape.type === "object" ? objectSchema(shape.fields ?? [], leaf) : shape.type === "array" ? v.array(objectSchema(shape.of ?? [], leaf)) : leaf[shape.type] ?? v.any();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function objectSchema(fields, leaf) {
|
|
1318
|
+
const entries = /* @__PURE__ */ Object.create(null);
|
|
1319
|
+
for (const f of fields) entries[f.name] = v.optional(shapeValueSchema(f, leaf));
|
|
1320
|
+
return v.looseObject(entries);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function wholeValueSchema(args) {
|
|
1324
|
+
const {entryType: entryType, shape: shape, leaf: leaf} = args;
|
|
1325
|
+
return entryType === "object" ? v.union([ v.null(), objectSchema(shape.fields ?? [], leaf) ]) : entryType === "array" ? v.array(objectSchema(shape.of ?? [], leaf)) : leaf[entryType];
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function appendItemSchema(entryType, shape) {
|
|
1329
|
+
if (entryType === "array") return objectSchema(shape.of ?? [], valueSchemas);
|
|
1330
|
+
if (entryType === "doc.refs") return GdrShape;
|
|
1331
|
+
if (entryType === "assignees") return AssigneeShape;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function rejectedRefTypes(args) {
|
|
1335
|
+
const {entryType: entryType, types: types, value: value} = args;
|
|
1336
|
+
if (types === void 0 || value === null || value === void 0) return [];
|
|
1337
|
+
if (entryType !== "doc.ref" && entryType !== "doc.refs") return [];
|
|
1338
|
+
let items = [ value ];
|
|
1339
|
+
return entryType === "doc.refs" && (items = Array.isArray(value) ? value : []),
|
|
1340
|
+
[ ...new Set(items.map(gdrTypeOf).filter(t => t !== void 0 && !types.includes(t))) ];
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function refTypeIssues(args) {
|
|
1344
|
+
const rejected = rejectedRefTypes(args);
|
|
1345
|
+
if (rejected.length === 0) return;
|
|
1346
|
+
const accepts = (args.types ?? []).map(t => `"${t}"`).join(", ");
|
|
1347
|
+
return rejected.map(t => `document type "${t}" is not accepted — this entry accepts ${accepts} (a GDR's \`type\` names the target document's schema type)`);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function gdrTypeOf(item) {
|
|
1351
|
+
if (typeof item != "object" || item === null) return;
|
|
1352
|
+
const t = item.type;
|
|
1353
|
+
return typeof t == "string" ? t : void 0;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function checkFieldValue(args) {
|
|
1357
|
+
return checkValueAgainst(args, valueSchemas);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function checkValueAgainst(args, leaf) {
|
|
1361
|
+
const schema = wholeValueSchema({
|
|
1362
|
+
entryType: args.entryType,
|
|
1363
|
+
shape: args,
|
|
1364
|
+
leaf: leaf
|
|
1365
|
+
});
|
|
1366
|
+
if (schema === void 0) return [ `unknown field entry type ${args.entryType}` ];
|
|
1367
|
+
const result = v.safeParse(schema, args.value);
|
|
1368
|
+
return result.success ? refTypeIssues({
|
|
1369
|
+
entryType: args.entryType,
|
|
1370
|
+
types: args.types,
|
|
1371
|
+
value: args.value
|
|
1372
|
+
}) : formatIssues(result.issues);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function validateFieldValue(args) {
|
|
1376
|
+
const issues = checkFieldValue(args);
|
|
1377
|
+
if (issues !== void 0) throw new FieldValueShapeError({
|
|
1378
|
+
entryType: args.entryType,
|
|
1379
|
+
entryName: args.entryName,
|
|
1380
|
+
issues: issues,
|
|
1381
|
+
mode: "value"
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
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}$`);
|
|
1386
|
+
|
|
1387
|
+
function isAuthoringRefId(id) {
|
|
1388
|
+
return id.startsWith("@") ? ALIAS_REF_RE.test(id) : id.includes(":") ? isGdrUri(id) : BARE_ID_RE.test(id);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function isBareSeedId(id) {
|
|
1392
|
+
return BARE_ID_RE.test(id);
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a bare document id, a GDR URI, or a portable `@<alias>:<id>` reference")), AuthoringGdrShape = v.looseObject({
|
|
1396
|
+
id: AuthoringRefId,
|
|
1397
|
+
type: v.pipe(v.string(), v.minLength(1))
|
|
1398
|
+
}), seedValueSchemas = {
|
|
1399
|
+
...valueSchemas,
|
|
1400
|
+
"doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
|
|
1401
|
+
"doc.refs": v.array(AuthoringGdrShape),
|
|
1402
|
+
"release.ref": v.union([ v.null(), v.looseObject({
|
|
1403
|
+
id: AuthoringRefId,
|
|
1404
|
+
type: v.literal("system.release"),
|
|
1405
|
+
releaseName: v.pipe(v.string(), v.minLength(1))
|
|
1406
|
+
}) ])
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
function checkLiteralSeed(args) {
|
|
1410
|
+
return args.value === null ? [ "a literal seed cannot be null — omit `initialValue` to start the field empty" ] : checkValueAgainst(args, seedValueSchemas);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function validateFieldAppendItem(args) {
|
|
1414
|
+
const schema = appendItemSchema(args.entryType, args);
|
|
1415
|
+
if (schema === void 0) throw new FieldValueShapeError({
|
|
1416
|
+
entryType: args.entryType,
|
|
1417
|
+
entryName: args.entryName,
|
|
1418
|
+
issues: [ `field entry type ${args.entryType} does not support append` ],
|
|
1419
|
+
mode: "item"
|
|
1420
|
+
});
|
|
1421
|
+
const result = v.safeParse(schema, args.item);
|
|
1422
|
+
if (!result.success) throw new FieldValueShapeError({
|
|
1423
|
+
entryType: args.entryType,
|
|
1424
|
+
entryName: args.entryName,
|
|
1425
|
+
issues: formatIssues(result.issues),
|
|
1426
|
+
mode: "item"
|
|
1427
|
+
});
|
|
1428
|
+
const typeIssues = refTypeIssues({
|
|
1429
|
+
entryType: args.entryType,
|
|
1430
|
+
types: args.types,
|
|
1431
|
+
value: [ args.item ]
|
|
1432
|
+
});
|
|
1433
|
+
if (typeIssues !== void 0) throw new FieldValueShapeError({
|
|
1434
|
+
entryType: args.entryType,
|
|
1435
|
+
entryName: args.entryName,
|
|
1436
|
+
issues: typeIssues,
|
|
1437
|
+
mode: "item"
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function formatIssues(issues) {
|
|
1442
|
+
return issues.map(i => {
|
|
1443
|
+
const keys = i.path?.map(p => p.key) ?? [];
|
|
1444
|
+
return `${keys.length > 0 ? `at ${keys.join(".")}: ` : ""}${i.message}`;
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const NonEmpty = v.pipe(v.string(), v.minLength(1, "must be a non-empty string")), PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1449
|
+
|
|
1450
|
+
function groqIdentifier(referencedAs) {
|
|
1451
|
+
return v.pipe(v.string(), v.regex(GROQ_IDENTIFIER, `must be a GROQ-safe identifier (letters, digits, underscore; not starting with a digit) because it is referenced as ${referencedAs} in GROQ conditions`));
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
function picklist(options) {
|
|
1455
|
+
return v.picklist(options, `Invalid option: expected one of ${options.map(o => `"${o}"`).join("|")}`);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function pinned() {
|
|
1459
|
+
return (schema, ..._exact) => schema;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
const LiteralSchema = v.strictObject({
|
|
1463
|
+
type: v.literal("literal"),
|
|
1464
|
+
value: v.unknown()
|
|
1465
|
+
}), FieldReadSchema = v.strictObject({
|
|
1466
|
+
type: v.literal("fieldRead"),
|
|
1467
|
+
scope: v.optional(v.union([ v.literal("workflow"), v.literal("stage") ])),
|
|
1468
|
+
field: NonEmpty,
|
|
1469
|
+
path: v.optional(v.string())
|
|
1470
|
+
}), FieldSourceSchema = v.union([ v.strictObject({
|
|
1471
|
+
type: v.literal("input")
|
|
1472
|
+
}), v.strictObject({
|
|
1473
|
+
type: v.literal("query"),
|
|
1474
|
+
query: NonEmpty
|
|
1475
|
+
}), LiteralSchema, FieldReadSchema ]), ValueExprSchema = v.lazy(() => v.union([ LiteralSchema, FieldReadSchema, v.strictObject({
|
|
1476
|
+
type: v.literal("param"),
|
|
1477
|
+
param: NonEmpty
|
|
1478
|
+
}), v.strictObject({
|
|
1479
|
+
type: v.literal("actor")
|
|
1480
|
+
}), v.strictObject({
|
|
1481
|
+
type: v.literal("now")
|
|
1482
|
+
}), v.strictObject({
|
|
1483
|
+
type: v.literal("self")
|
|
1484
|
+
}), v.strictObject({
|
|
1485
|
+
type: v.literal("stage")
|
|
1486
|
+
}), v.strictObject({
|
|
1487
|
+
type: v.literal("object"),
|
|
1488
|
+
fields: v.record(NonEmpty, ValueExprSchema)
|
|
1489
|
+
}) ])), StoredFieldRefSchema = v.strictObject({
|
|
1490
|
+
scope: picklist(FIELD_SCOPES),
|
|
1491
|
+
field: NonEmpty
|
|
1492
|
+
}), AuthoringFieldRefSchema = v.strictObject({
|
|
1493
|
+
scope: v.optional(picklist(FIELD_SCOPES)),
|
|
1494
|
+
field: NonEmpty
|
|
1495
|
+
}), HREF_SCHEMES = [ "http:", "https:" ];
|
|
1496
|
+
|
|
1497
|
+
function isHttpUrl(value) {
|
|
1498
|
+
try {
|
|
1499
|
+
return HREF_SCHEMES.includes(new URL(value).protocol);
|
|
1500
|
+
} catch {
|
|
1501
|
+
return !1;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
const UrlString = v.pipe(v.string(), v.url("must be a valid URL"), v.check(isHttpUrl, "must be an http(s) URL"));
|
|
1506
|
+
|
|
1507
|
+
function manualTargetSchema(ref) {
|
|
1508
|
+
return v.variant("type", [ v.strictObject({
|
|
1509
|
+
type: v.literal("url"),
|
|
1510
|
+
url: UrlString
|
|
1511
|
+
}), v.strictObject({
|
|
1512
|
+
type: v.literal("field"),
|
|
1513
|
+
field: ref
|
|
1514
|
+
}) ]);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
const StoredManualTargetSchema = manualTargetSchema(StoredFieldRefSchema), AuthoringManualTargetSchema = manualTargetSchema(v.union([ NonEmpty, AuthoringFieldRefSchema ])), ConditionSchema = NonEmpty;
|
|
1518
|
+
|
|
1519
|
+
function opSchemas(targetSchema) {
|
|
1520
|
+
return [ v.strictObject({
|
|
1521
|
+
type: v.literal("field.set"),
|
|
1522
|
+
target: targetSchema,
|
|
1523
|
+
value: ValueExprSchema
|
|
1524
|
+
}), v.strictObject({
|
|
1525
|
+
type: v.literal("field.unset"),
|
|
1526
|
+
target: targetSchema
|
|
1527
|
+
}), v.strictObject({
|
|
1528
|
+
type: v.literal("field.append"),
|
|
1529
|
+
target: targetSchema,
|
|
1530
|
+
value: ValueExprSchema
|
|
1531
|
+
}), v.strictObject({
|
|
1532
|
+
type: v.literal("field.updateWhere"),
|
|
1533
|
+
target: targetSchema,
|
|
1534
|
+
where: ConditionSchema,
|
|
1535
|
+
value: ValueExprSchema
|
|
1536
|
+
}), v.strictObject({
|
|
1537
|
+
type: v.literal("field.removeWhere"),
|
|
1538
|
+
target: targetSchema,
|
|
1539
|
+
where: ConditionSchema
|
|
1540
|
+
}) ];
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
const StoredFieldOpSchema = v.variant("type", [ ...opSchemas(StoredFieldRefSchema) ]), StoredOpSchema = v.variant("type", [ ...opSchemas(StoredFieldRefSchema), v.strictObject({
|
|
1544
|
+
type: v.literal("status.set"),
|
|
1545
|
+
activity: NonEmpty,
|
|
1546
|
+
status: picklist(ACTIVITY_STATUSES)
|
|
1547
|
+
}) ]), AuditOpSchema = v.strictObject({
|
|
1548
|
+
type: v.literal("audit"),
|
|
1549
|
+
target: AuthoringFieldRefSchema,
|
|
1550
|
+
value: ValueExprSchema,
|
|
1551
|
+
stampFields: v.optional(v.strictObject({
|
|
1552
|
+
actor: v.optional(NonEmpty),
|
|
1553
|
+
at: v.optional(NonEmpty)
|
|
1554
|
+
}))
|
|
1555
|
+
}), AuthoringOpSchema = v.variant("type", [ ...opSchemas(AuthoringFieldRefSchema), v.strictObject({
|
|
1556
|
+
type: v.literal("status.set"),
|
|
1557
|
+
activity: v.optional(NonEmpty),
|
|
1558
|
+
status: picklist(ACTIVITY_STATUSES)
|
|
1559
|
+
}), AuditOpSchema ]), GroupName = v.pipe(v.string(), v.regex(GROQ_IDENTIFIER, "must be an identifier (letters, digits, underscore; not starting with a digit)")), GroupSchema = pinned()(v.strictObject({
|
|
1560
|
+
name: GroupName,
|
|
1561
|
+
title: v.optional(v.string()),
|
|
1562
|
+
description: v.optional(v.string()),
|
|
1563
|
+
kind: v.optional(picklist(GROUP_KINDS))
|
|
1564
|
+
})), GroupNameList = v.pipe(v.array(GroupName), v.minLength(1, "name at least one group, or omit `group`"), v.check(names => new Set(names).size === names.length, "a group is listed more than once — list each group once")), StoredGroupMembershipSchema = GroupNameList, AuthoringGroupMembershipSchema = v.union([ GroupName, GroupNameList ]);
|
|
1565
|
+
|
|
1566
|
+
function groupMembershipNames(group) {
|
|
1567
|
+
return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "release.ref", "string", "text", "number", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`");
|
|
1571
|
+
|
|
1572
|
+
function asShape(input) {
|
|
1573
|
+
return typeof input == "object" && input !== null ? input : {};
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function compositeShapeOk(input) {
|
|
1577
|
+
const shape = asShape(input);
|
|
1578
|
+
return shape.type === "object" ? Array.isArray(shape.fields) && shape.fields.length > 0 && shape.of === void 0 : shape.type === "array" ? Array.isArray(shape.of) && shape.of.length > 0 && shape.fields === void 0 : shape.fields === void 0 && shape.of === void 0;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function compositeShapeMessage(input) {
|
|
1582
|
+
const shape = asShape(input);
|
|
1583
|
+
return shape.type === "object" ? shape.of !== void 0 ? "an `object` kind declares its sub-fields with `fields`, not `of`" : "an `object` kind needs a non-empty `fields` list of sub-field shapes" : shape.type === "array" ? shape.fields !== void 0 ? "an `array` kind declares its item shape with `of`, not `fields`" : "an `array` kind needs a non-empty `of` list of sub-field shapes" : `\`fields\` / \`of\` are only valid on the \`object\` / \`array\` kinds, not "${String(shape.type)}"`;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function duplicateSubfieldName(input) {
|
|
1587
|
+
const shape = asShape(input);
|
|
1588
|
+
let list = [];
|
|
1589
|
+
Array.isArray(shape.fields) ? list = shape.fields : Array.isArray(shape.of) && (list = shape.of);
|
|
1590
|
+
const seen = /* @__PURE__ */ new Set;
|
|
1591
|
+
for (const item of list) {
|
|
1592
|
+
const name = asShape(item).name;
|
|
1593
|
+
if (typeof name == "string") {
|
|
1594
|
+
if (seen.has(name)) return name;
|
|
1595
|
+
seen.add(name);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function compositeChecked(entries) {
|
|
1601
|
+
return v.pipe(v.strictObject(entries), v.check(input => compositeShapeOk(input), issue => compositeShapeMessage(issue.input)), v.check(input => duplicateSubfieldName(input) === void 0, issue => `duplicate sub-field name "${duplicateSubfieldName(issue.input)}" — sub-field names must be unique within \`fields\` / \`of\``));
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
const FieldShapeSchema = v.lazy(() => compositeChecked({
|
|
1605
|
+
type: FieldValueKindSchema,
|
|
1606
|
+
name: FieldEntryName,
|
|
1607
|
+
title: v.optional(v.string()),
|
|
1608
|
+
description: v.optional(v.string()),
|
|
1609
|
+
fields: v.optional(v.array(FieldShapeSchema)),
|
|
1610
|
+
of: v.optional(v.array(FieldShapeSchema))
|
|
1611
|
+
})), StoredEditableSchema = v.union([ v.literal(!0), NonEmpty ]), AuthoringEditableSchema = v.union([ v.literal(!0), v.array(NonEmpty), NonEmpty ]);
|
|
1612
|
+
|
|
1613
|
+
function fieldBase(editable, group) {
|
|
1614
|
+
return {
|
|
1615
|
+
name: FieldEntryName,
|
|
1616
|
+
title: v.optional(v.string()),
|
|
1617
|
+
description: v.optional(v.string()),
|
|
1618
|
+
group: v.optional(group),
|
|
1619
|
+
required: v.optional(v.boolean()),
|
|
1620
|
+
initialValue: v.optional(FieldSourceSchema),
|
|
1621
|
+
editable: v.optional(editable)
|
|
1622
|
+
};
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function fieldEntryFields(editable, group) {
|
|
1626
|
+
return {
|
|
1627
|
+
type: FieldKindSchema,
|
|
1628
|
+
...fieldBase(editable, group),
|
|
1629
|
+
types: v.optional(v.pipe(v.array(NonEmpty), v.minLength(1, "declare at least one accepted type, or omit `types` to accept any"))),
|
|
1630
|
+
fields: v.optional(v.array(FieldShapeSchema)),
|
|
1631
|
+
of: v.optional(v.array(FieldShapeSchema))
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function literalSeedIssues(entry) {
|
|
1636
|
+
if (entry.initialValue?.type === "literal") return checkLiteralSeed({
|
|
1637
|
+
entryType: entry.type,
|
|
1638
|
+
value: entry.initialValue.value,
|
|
1639
|
+
types: entry.types,
|
|
1640
|
+
fields: entry.fields,
|
|
1641
|
+
of: entry.of
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function literalSeedCheck() {
|
|
1646
|
+
return v.check(entry => literalSeedIssues(entry) === void 0, issue => `initialValue literal does not fit the declared kind: ${(literalSeedIssues(issue.input) ?? []).join("; ")}`);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function refTypesCheck() {
|
|
1650
|
+
return v.check(entry => entry.types === void 0 || entry.type === "doc.ref" || entry.type === "doc.refs", issue => `\`types\` is only valid on \`doc.ref\` / \`doc.refs\` entries, not "${issue.input.type}"`);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
const FieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields(StoredEditableSchema, StoredGroupMembershipSchema)), refTypesCheck(), literalSeedCheck())), RawAuthoringFieldEntrySchema = pinned()(v.pipe(compositeChecked(fieldEntryFields(AuthoringEditableSchema, AuthoringGroupMembershipSchema)), refTypesCheck(), literalSeedCheck())), ClaimFieldSchema = pinned()(v.strictObject({
|
|
1654
|
+
type: v.literal("claim"),
|
|
1655
|
+
name: FieldEntryName,
|
|
1656
|
+
title: v.optional(v.string()),
|
|
1657
|
+
description: v.optional(v.string()),
|
|
1658
|
+
group: v.optional(AuthoringGroupMembershipSchema)
|
|
1659
|
+
}));
|
|
1660
|
+
|
|
1661
|
+
function listSugarFields(type) {
|
|
1662
|
+
return {
|
|
1663
|
+
type: v.literal(type),
|
|
1664
|
+
...fieldBase(AuthoringEditableSchema, AuthoringGroupMembershipSchema)
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))), NotesFieldSchema = pinned()(v.strictObject(listSugarFields("notes"))), AuthoringFieldEntrySchema = pinned()(v.union([ RawAuthoringFieldEntrySchema, ClaimFieldSchema, TodoListFieldSchema, NotesFieldSchema ])), EffectSchema = v.strictObject({
|
|
1669
|
+
name: NonEmpty,
|
|
1670
|
+
title: v.optional(v.string()),
|
|
1671
|
+
description: v.optional(v.string()),
|
|
1672
|
+
bindings: v.optional(v.record(v.string(), ConditionSchema)),
|
|
1673
|
+
input: v.optional(v.record(v.string(), v.unknown())),
|
|
1674
|
+
outputs: v.optional(v.array(FieldShapeSchema))
|
|
1675
|
+
}), DefinitionRefSchema = v.strictObject({
|
|
1676
|
+
name: NonEmpty,
|
|
1677
|
+
version: v.optional(v.union([ PositiveInt, v.literal("latest") ]))
|
|
1678
|
+
}), SubworkflowsSchema = v.strictObject({
|
|
1679
|
+
forEach: NonEmpty,
|
|
1680
|
+
definition: DefinitionRefSchema,
|
|
1681
|
+
with: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1682
|
+
context: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1683
|
+
onExit: v.optional(picklist([ "detach", "abort" ]))
|
|
1684
|
+
}), ActionParamSchema = v.strictObject({
|
|
1685
|
+
type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
|
|
1686
|
+
name: NonEmpty,
|
|
1687
|
+
title: v.optional(v.string()),
|
|
1688
|
+
description: v.optional(v.string()),
|
|
1689
|
+
required: v.optional(v.boolean())
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
function actionFields(op, group) {
|
|
1693
|
+
return {
|
|
1694
|
+
name: NonEmpty,
|
|
1695
|
+
title: v.optional(v.string()),
|
|
1696
|
+
description: v.optional(v.string()),
|
|
1697
|
+
group: v.optional(group),
|
|
1698
|
+
when: v.optional(ConditionSchema),
|
|
1699
|
+
filter: v.optional(ConditionSchema),
|
|
1700
|
+
params: v.optional(v.array(ActionParamSchema)),
|
|
1701
|
+
ops: v.optional(v.array(op)),
|
|
1702
|
+
effects: v.optional(v.array(EffectSchema)),
|
|
1703
|
+
spawn: v.optional(SubworkflowsSchema)
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
const StoredActionSchema = pinned()(v.strictObject({
|
|
1708
|
+
...actionFields(StoredOpSchema, StoredGroupMembershipSchema),
|
|
1709
|
+
roles: v.optional(v.array(NonEmpty))
|
|
1710
|
+
})), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v.strictObject({
|
|
1711
|
+
...actionFields(AuthoringOpSchema, AuthoringGroupMembershipSchema),
|
|
1712
|
+
roles: v.optional(v.array(NonEmpty)),
|
|
1713
|
+
status: v.optional(TerminalActivityStatus)
|
|
1714
|
+
})), ClaimActionSchema = pinned()(v.strictObject({
|
|
1715
|
+
type: v.literal("claim"),
|
|
1716
|
+
name: NonEmpty,
|
|
1717
|
+
title: v.optional(v.string()),
|
|
1718
|
+
description: v.optional(v.string()),
|
|
1719
|
+
group: v.optional(AuthoringGroupMembershipSchema),
|
|
1720
|
+
field: v.union([ NonEmpty, AuthoringFieldRefSchema ]),
|
|
1721
|
+
roles: v.optional(v.array(NonEmpty)),
|
|
1722
|
+
filter: v.optional(ConditionSchema),
|
|
1723
|
+
params: v.optional(v.array(ActionParamSchema)),
|
|
1724
|
+
effects: v.optional(v.array(EffectSchema))
|
|
1725
|
+
})), AuthoringActionSchema = pinned()(v.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema));
|
|
1726
|
+
|
|
1727
|
+
function activityFields({field: field, action: action, target: target, group: group}) {
|
|
1728
|
+
return {
|
|
1729
|
+
name: NonEmpty,
|
|
1730
|
+
title: v.optional(v.string()),
|
|
1731
|
+
description: v.optional(v.string()),
|
|
1732
|
+
groups: v.optional(v.array(GroupSchema)),
|
|
1733
|
+
group: v.optional(group),
|
|
1734
|
+
target: v.optional(target),
|
|
1735
|
+
filter: v.optional(ConditionSchema),
|
|
1736
|
+
requirements: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1737
|
+
actions: v.optional(v.array(action)),
|
|
1738
|
+
fields: v.optional(v.array(field))
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const StoredActivitySchema = pinned()(v.strictObject(activityFields({
|
|
1743
|
+
field: FieldEntrySchema,
|
|
1744
|
+
action: StoredActionSchema,
|
|
1745
|
+
target: StoredManualTargetSchema,
|
|
1746
|
+
group: StoredGroupMembershipSchema
|
|
1747
|
+
}))), AuthoringActivitySchema = pinned()(v.strictObject(activityFields({
|
|
1748
|
+
field: AuthoringFieldEntrySchema,
|
|
1749
|
+
action: AuthoringActionSchema,
|
|
1750
|
+
target: AuthoringManualTargetSchema,
|
|
1751
|
+
group: AuthoringGroupMembershipSchema
|
|
1752
|
+
})));
|
|
1753
|
+
|
|
1754
|
+
function transitionFields(when) {
|
|
1755
|
+
return {
|
|
1756
|
+
name: NonEmpty,
|
|
1757
|
+
title: v.optional(v.string()),
|
|
1758
|
+
description: v.optional(v.string()),
|
|
1759
|
+
to: NonEmpty,
|
|
1760
|
+
when: when
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
const StoredTransitionSchema = pinned()(v.strictObject(transitionFields(ConditionSchema))), AuthoringTransitionSchema = pinned()(v.strictObject(transitionFields(v.optional(ConditionSchema)))), GuardActionSchema = picklist(MUTATION_GUARD_ACTIONS), GuardReadPath = v.pipe(NonEmpty, v.check(path => !/[\r\n\u2028\u2029]/.test(path), "a guard read path cannot contain a line break")), GuardReadSchema = v.variant("type", [ v.strictObject({
|
|
1765
|
+
type: v.literal("self")
|
|
1766
|
+
}), v.strictObject({
|
|
1767
|
+
type: v.literal("now")
|
|
1768
|
+
}), v.strictObject({
|
|
1769
|
+
type: v.literal("fieldRead"),
|
|
1770
|
+
field: FieldEntryName,
|
|
1771
|
+
path: v.optional(GuardReadPath)
|
|
1772
|
+
}), v.strictObject({
|
|
1773
|
+
type: v.literal("effectsRead"),
|
|
1774
|
+
effect: v.pipe(NonEmpty, v.check(name => !name.includes("'"), "an effect name cannot contain `'`")),
|
|
1775
|
+
path: v.optional(GuardReadPath)
|
|
1776
|
+
}) ]);
|
|
1777
|
+
|
|
1778
|
+
function guardMatchFields(read) {
|
|
1779
|
+
return {
|
|
1780
|
+
types: v.optional(v.array(NonEmpty)),
|
|
1781
|
+
idRefs: v.optional(v.array(read)),
|
|
1782
|
+
idPatterns: v.optional(v.array(NonEmpty)),
|
|
1783
|
+
actions: v.pipe(v.array(GuardActionSchema), v.minLength(1, "a guard must match at least one action"))
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
function guardFields(read) {
|
|
1788
|
+
return {
|
|
1789
|
+
name: NonEmpty,
|
|
1790
|
+
title: v.optional(v.string()),
|
|
1791
|
+
description: v.optional(v.string()),
|
|
1792
|
+
match: v.strictObject(guardMatchFields(read)),
|
|
1793
|
+
predicate: v.optional(v.string()),
|
|
1794
|
+
metadata: v.optional(v.record(NonEmpty, read))
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
const GuardSchema = v.strictObject(guardFields(NonEmpty)), AuthoringGuardSchema = v.strictObject(guardFields(GuardReadSchema));
|
|
1799
|
+
|
|
1800
|
+
function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
|
|
1801
|
+
return {
|
|
1802
|
+
name: NonEmpty,
|
|
1803
|
+
title: v.optional(v.string()),
|
|
1804
|
+
description: v.optional(v.string()),
|
|
1805
|
+
groups: v.optional(v.array(GroupSchema)),
|
|
1806
|
+
activities: v.optional(v.array(activity)),
|
|
1807
|
+
transitions: v.optional(v.array(transition)),
|
|
1808
|
+
guards: v.optional(v.array(guard)),
|
|
1809
|
+
fields: v.optional(v.array(field)),
|
|
1810
|
+
editable: v.optional(v.record(FieldEntryName, editable))
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const StoredStageSchema = pinned()(v.strictObject(stageFields({
|
|
1815
|
+
field: FieldEntrySchema,
|
|
1816
|
+
activity: StoredActivitySchema,
|
|
1817
|
+
transition: StoredTransitionSchema,
|
|
1818
|
+
guard: GuardSchema,
|
|
1819
|
+
editable: StoredEditableSchema
|
|
1820
|
+
}))), AuthoringStageSchema = pinned()(v.strictObject(stageFields({
|
|
1821
|
+
field: AuthoringFieldEntrySchema,
|
|
1822
|
+
activity: AuthoringActivitySchema,
|
|
1823
|
+
transition: AuthoringTransitionSchema,
|
|
1824
|
+
guard: AuthoringGuardSchema,
|
|
1825
|
+
editable: AuthoringEditableSchema
|
|
1826
|
+
}))), RoleAliasesSchema = v.record(NonEmpty, v.pipe(v.array(NonEmpty), v.minLength(1, "a role alias must list at least one fulfilling role"))), WORKFLOW_LIFECYCLES = [ "standalone", "child" ], START_KINDS = [ "interactive", "autonomous" ];
|
|
1827
|
+
|
|
1828
|
+
function startFields(kind) {
|
|
1829
|
+
return {
|
|
1830
|
+
kind: kind,
|
|
1831
|
+
filter: v.optional(ConditionSchema)
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KINDS)))), AuthoringStartSchema = pinned()(v.strictObject(startFields(v.optional(picklist(START_KINDS)))));
|
|
1836
|
+
|
|
1837
|
+
function workflowFields({field: field, stage: stage, start: start}) {
|
|
1838
|
+
return {
|
|
1839
|
+
name: NonEmpty,
|
|
1840
|
+
title: NonEmpty,
|
|
1841
|
+
description: v.optional(v.string()),
|
|
1842
|
+
groups: v.optional(v.array(GroupSchema)),
|
|
1843
|
+
lifecycle: v.optional(picklist(WORKFLOW_LIFECYCLES)),
|
|
1844
|
+
start: v.optional(start),
|
|
1845
|
+
initialStage: NonEmpty,
|
|
1846
|
+
fields: v.optional(v.array(field)),
|
|
1847
|
+
stages: v.pipe(v.array(stage), v.minLength(1, "must declare at least one stage")),
|
|
1848
|
+
predicates: v.optional(v.record(groqIdentifier("`$<name>`"), ConditionSchema)),
|
|
1849
|
+
roleAliases: v.optional(RoleAliasesSchema)
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields({
|
|
1854
|
+
field: FieldEntrySchema,
|
|
1855
|
+
stage: StoredStageSchema,
|
|
1856
|
+
start: StoredStartSchema
|
|
1857
|
+
})));
|
|
1858
|
+
|
|
1859
|
+
function parseStoredDefinition(input, label) {
|
|
1860
|
+
return parseOrThrow({
|
|
1861
|
+
schema: WorkflowDefinitionSchema,
|
|
1862
|
+
input: input,
|
|
1863
|
+
label: label
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
const AuthoringWorkflowSchema = pinned()(v.strictObject(workflowFields({
|
|
1868
|
+
field: AuthoringFieldEntrySchema,
|
|
1869
|
+
stage: AuthoringStageSchema,
|
|
1870
|
+
start: AuthoringStartSchema
|
|
1871
|
+
}))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
|
|
1872
|
+
|
|
1873
|
+
function isStartableDefinition(definition) {
|
|
1874
|
+
return definition.lifecycle !== "child";
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function startKindOf(definition) {
|
|
1878
|
+
return definition.start?.kind ?? "interactive";
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function isSubjectEntry(entry) {
|
|
1882
|
+
return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
function isInputSourced(entry) {
|
|
1886
|
+
return entry.initialValue?.type === "input";
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function formatValidationError(label, issues) {
|
|
1890
|
+
const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
|
|
1891
|
+
return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function issuesFromValibot(issues) {
|
|
1895
|
+
return issues.map(issue => ({
|
|
1896
|
+
path: issue.path ? issue.path.map(item => item.key) : [],
|
|
1897
|
+
message: issue.message
|
|
1898
|
+
}));
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
function formatIssuePath(path) {
|
|
1902
|
+
let out = "";
|
|
1903
|
+
for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
|
|
1904
|
+
return out;
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
function parseOrThrow({schema: schema, input: input, label: label}) {
|
|
1908
|
+
const result = v.safeParse(schema, input);
|
|
1909
|
+
if (!result.success) throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
|
|
1910
|
+
return result.output;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function labelFor(fn, value) {
|
|
1914
|
+
if (value && typeof value == "object" && "name" in value) {
|
|
1915
|
+
const raw = value.name;
|
|
1916
|
+
if (typeof raw == "string") return `${fn}("${raw}")`;
|
|
1917
|
+
}
|
|
1918
|
+
return fn;
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
function knownList(ids) {
|
|
1922
|
+
const quoted = [ ...ids ].map(s => `"${s}"`);
|
|
1923
|
+
return quoted.length > 0 ? quoted.join(", ") : "(none)";
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function checkDuplicates({names: names, what: what, issues: issues}) {
|
|
1927
|
+
const seen = /* @__PURE__ */ new Set;
|
|
1928
|
+
for (const {name: name, path: path} of names) seen.has(name) && issues.push({
|
|
1929
|
+
path: path,
|
|
1930
|
+
message: `duplicate ${what} "${name}" (already declared earlier)`
|
|
1931
|
+
}), seen.add(name);
|
|
1932
|
+
return seen;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function checkStages(def, issues) {
|
|
1936
|
+
const stageNames = checkDuplicates({
|
|
1937
|
+
names: def.stages.map((stage, i) => ({
|
|
1938
|
+
name: stage.name,
|
|
1939
|
+
path: [ "stages", i, "name" ]
|
|
1940
|
+
})),
|
|
1941
|
+
what: "stage name",
|
|
1942
|
+
issues: issues
|
|
1943
|
+
});
|
|
1944
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
1945
|
+
const activityNames = checkDuplicates({
|
|
1946
|
+
names: (stage.activities ?? []).map((activity, j) => ({
|
|
1947
|
+
name: activity.name,
|
|
1948
|
+
path: [ "stages", i, "activities", j, "name" ]
|
|
1949
|
+
})),
|
|
1950
|
+
what: `activity name in stage "${stage.name}"`,
|
|
1951
|
+
issues: issues
|
|
1952
|
+
});
|
|
1953
|
+
checkDuplicates({
|
|
1954
|
+
names: (stage.transitions ?? []).map((t, k) => ({
|
|
1955
|
+
name: t.name,
|
|
1956
|
+
path: [ "stages", i, "transitions", k, "name" ]
|
|
1957
|
+
})),
|
|
1958
|
+
what: `transition name in stage "${stage.name}"`,
|
|
1959
|
+
issues: issues
|
|
1960
|
+
}), checkActivities({
|
|
1961
|
+
def: def,
|
|
1962
|
+
i: i,
|
|
1963
|
+
activityNames: activityNames,
|
|
1964
|
+
issues: issues
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
return stageNames;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function checkActivities({def: def, i: i, activityNames: activityNames, issues: issues}) {
|
|
1971
|
+
const stage = def.stages[i];
|
|
1972
|
+
for (const [j, activity] of (stage.activities ?? []).entries()) {
|
|
1973
|
+
const path = [ "stages", i, "activities", j ];
|
|
1974
|
+
checkDuplicates({
|
|
1975
|
+
names: (activity.actions ?? []).map((action, a) => ({
|
|
1976
|
+
name: action.name,
|
|
1977
|
+
path: [ ...path, "actions", a, "name" ]
|
|
1978
|
+
})),
|
|
1979
|
+
what: `action name in activity "${activity.name}"`,
|
|
1980
|
+
issues: issues
|
|
1981
|
+
}), checkStatusSetTargets({
|
|
1982
|
+
activity: activity,
|
|
1983
|
+
activityNames: activityNames,
|
|
1984
|
+
path: path,
|
|
1985
|
+
issues: issues
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function checkStatusSetTargets({activity: activity, activityNames: activityNames, path: path, issues: issues}) {
|
|
1991
|
+
for (const [a, action] of (activity.actions ?? []).entries()) checkStatusSetOps({
|
|
1992
|
+
ops: action.ops,
|
|
1993
|
+
activityNames: activityNames,
|
|
1994
|
+
path: [ ...path, "actions", a, "ops" ],
|
|
1995
|
+
issues: issues
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function checkStatusSetOps({ops: ops, activityNames: activityNames, path: path, issues: issues}) {
|
|
2000
|
+
for (const [o, op] of (ops ?? []).entries()) op.type !== "status.set" || activityNames.has(op.activity) || issues.push({
|
|
2001
|
+
path: [ ...path, o, "activity" ],
|
|
2002
|
+
message: `status.set targets activity "${op.activity}" which is not declared in this stage. Known activities: ${knownList(activityNames)}`
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
function checkInitialStage({def: def, stageNames: stageNames, issues: issues}) {
|
|
2007
|
+
stageNames.has(def.initialStage) || issues.push({
|
|
2008
|
+
path: [ "initialStage" ],
|
|
2009
|
+
message: `initialStage "${def.initialStage}" is not a declared stage. Known stages: ${knownList(stageNames)}`
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function checkTransitionTargets({def: def, stageNames: stageNames, issues: issues}) {
|
|
2014
|
+
for (const [i, stage] of def.stages.entries()) for (const [k, t] of (stage.transitions ?? []).entries()) stageNames.has(t.to) || issues.push({
|
|
2015
|
+
path: [ "stages", i, "transitions", k, "to" ],
|
|
2016
|
+
message: `transition target "${t.to}" is not a declared stage. Known stages: ${knownList(stageNames)}`
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function checkStageReachability({def: def, stageNames: stageNames, issues: issues}) {
|
|
2021
|
+
if (!stageNames.has(def.initialStage)) return;
|
|
2022
|
+
const stagesByName = /* @__PURE__ */ new Map;
|
|
2023
|
+
for (const stage of def.stages) stagesByName.has(stage.name) || stagesByName.set(stage.name, stage);
|
|
2024
|
+
const reached = /* @__PURE__ */ new Set([ def.initialStage ]), frontier = [ def.initialStage ];
|
|
2025
|
+
for (;frontier.length > 0; ) {
|
|
2026
|
+
const stage = stagesByName.get(frontier.pop());
|
|
2027
|
+
for (const t of stage?.transitions ?? []) reached.has(t.to) || !stageNames.has(t.to) || (reached.add(t.to),
|
|
2028
|
+
frontier.push(t.to));
|
|
2029
|
+
}
|
|
2030
|
+
for (const [i, stage] of def.stages.entries()) reached.has(stage.name) || issues.push({
|
|
2031
|
+
path: [ "stages", i, "name" ],
|
|
2032
|
+
message: `stage "${stage.name}" is unreachable — no transition path leads from initialStage "${def.initialStage}" to it, so no instance can ever enter it. Add a transition targeting it, or remove the stage`
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function effectNameSites(def) {
|
|
2037
|
+
const sites = [];
|
|
2038
|
+
for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) collectEffects({
|
|
2039
|
+
effects: action.effects,
|
|
2040
|
+
path: [ "stages", i, "activities", j, "actions", a, "effects" ],
|
|
2041
|
+
sites: sites
|
|
2042
|
+
});
|
|
2043
|
+
return sites;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
function checkEffectNames(def, issues) {
|
|
2047
|
+
checkDuplicates({
|
|
2048
|
+
names: effectNameSites(def),
|
|
2049
|
+
what: "effect name (registry key — unique per definition)",
|
|
2050
|
+
issues: issues
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function collectEffects({effects: effects, path: path, sites: sites}) {
|
|
2055
|
+
for (const [e, effect] of (effects ?? []).entries()) sites.push({
|
|
2056
|
+
name: effect.name,
|
|
2057
|
+
path: [ ...path, e, "name" ]
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function checkGuardNames(def, issues) {
|
|
2062
|
+
const sites = def.stages.flatMap((stage, i) => (stage.guards ?? []).map((guard, g) => ({
|
|
2063
|
+
name: guard.name,
|
|
2064
|
+
path: [ "stages", i, "guards", g, "name" ]
|
|
2065
|
+
})));
|
|
2066
|
+
checkDuplicates({
|
|
2067
|
+
names: sites,
|
|
2068
|
+
what: "guard name (the guard lake _id derives from it — unique per definition)",
|
|
2069
|
+
issues: issues
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
function fieldScopes(def) {
|
|
2074
|
+
const scopes = [ {
|
|
2075
|
+
entries: def.fields,
|
|
2076
|
+
scope: "workflow",
|
|
2077
|
+
path: [ "fields" ],
|
|
2078
|
+
label: "workflow fields"
|
|
2079
|
+
} ];
|
|
2080
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
2081
|
+
scopes.push({
|
|
2082
|
+
entries: stage.fields,
|
|
2083
|
+
scope: "stage",
|
|
2084
|
+
path: [ "stages", i, "fields" ],
|
|
2085
|
+
label: `stage "${stage.name}" fields`
|
|
2086
|
+
});
|
|
2087
|
+
for (const [j, activity] of (stage.activities ?? []).entries()) scopes.push({
|
|
2088
|
+
entries: activity.fields,
|
|
2089
|
+
scope: "activity",
|
|
2090
|
+
path: [ "stages", i, "activities", j, "fields" ],
|
|
2091
|
+
label: `activity "${activity.name}" fields`
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
return scopes;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function checkRequiredField(def, issues) {
|
|
2098
|
+
for (const {entries: entries, scope: scope, path: path, label: label} of fieldScopes(def)) for (const [n, entry] of (entries ?? []).entries()) if (entry.required === !0) {
|
|
2099
|
+
if (scope !== "workflow") issues.push({
|
|
2100
|
+
path: [ ...path, n, "required" ],
|
|
2101
|
+
message: `${label} entry "${entry.name}" is \`required\`, but \`required\` applies only to workflow-scope \`input\` entries — only those are caller-supplied at start/spawn`
|
|
2102
|
+
}); else if (entry.initialValue?.type !== "input") {
|
|
2103
|
+
const seed = entry.initialValue?.type ?? "working memory (no initialValue)";
|
|
2104
|
+
issues.push({
|
|
2105
|
+
path: [ ...path, n, "required" ],
|
|
2106
|
+
message: `field entry "${entry.name}" has initialValue "${seed}" — \`required\` applies only to \`input\`-sourced entries (the caller fills them at start/spawn)`
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function checkFieldEntryNames(def, issues) {
|
|
2113
|
+
for (const {entries: entries, path: path, label: label} of fieldScopes(def)) checkDuplicates({
|
|
2114
|
+
names: (entries ?? []).map((entry, n) => ({
|
|
2115
|
+
name: entry.name,
|
|
2116
|
+
path: [ ...path, n, "name" ]
|
|
2117
|
+
})),
|
|
2118
|
+
what: `field entry name in ${label}`,
|
|
2119
|
+
issues: issues
|
|
2120
|
+
});
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
function checkPredicates(def, issues) {
|
|
2124
|
+
const reserved = new Set(RESERVED_CONDITION_VARS), names = Object.keys(def.predicates ?? {});
|
|
2125
|
+
for (const name of names) reserved.has(name) && issues.push({
|
|
2126
|
+
path: [ "predicates", name ],
|
|
2127
|
+
message: `predicate "${name}" shadows the built-in $${name} — pick another name`
|
|
2128
|
+
});
|
|
2129
|
+
for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkPredicateBody({
|
|
2130
|
+
name: name,
|
|
2131
|
+
groq: groq2,
|
|
2132
|
+
names: names,
|
|
2133
|
+
issues: issues
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function checkPredicateBody({name: name, groq: groq2, names: names, issues: issues}) {
|
|
2138
|
+
const reads = conditionParameterNames(groq2);
|
|
2139
|
+
for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
|
|
2140
|
+
path: [ "predicates", name ],
|
|
2141
|
+
message: `predicate "${name}" reads $${caller} — predicates are instance-level (pre-evaluated once, without a caller); compose caller gates at the condition site instead`
|
|
2142
|
+
});
|
|
2143
|
+
for (const other of names) other === name || !reads.has(other) || issues.push({
|
|
2144
|
+
path: [ "predicates", name ],
|
|
2145
|
+
message: `predicate "${name}" references predicate $${other} — predicates may read built-in vars only (no cross-references; compose at the condition site instead)`
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function entryNames(entries) {
|
|
2150
|
+
return new Set((entries ?? []).map(entry => entry.name));
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
function checkUnboundCallerVars(def, issues) {
|
|
2154
|
+
for (const site of conditionSites(def)) {
|
|
2155
|
+
const reads = conditionParameterNames(site.groq);
|
|
2156
|
+
for (const name of unboundCallerVars(site.policy)) reads.has(name) && issues.push({
|
|
2157
|
+
path: site.path,
|
|
2158
|
+
message: callerVarMessage(site, name)
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function unboundCallerVars(policy) {
|
|
2164
|
+
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function callerVarMessage(site, name) {
|
|
2168
|
+
return site.policy === "cascade" ? `${site.label} reads $${name} — cascade gates (transition \`when\`s, activity \`filter\`s, a cascade-fired action's \`when\`/\`filter\`) must resolve identically no matter whose token drives the cascade ($assigned is constant false, the other caller vars hold no value); gate on instance state (e.g. a field an action wrote), or pin executing identities with \`roles\`` : site.policy === "caller-bound" ? `${site.label} reads $${name} — $params (the firing action's args) is bound only while the action's effect bindings and where-op \`where\`s evaluate; this site never binds it, so the condition could never pass. Bind $params in an effect binding or a where-op instead, or gate on a field an action wrote` : site.policy === "triggered-payload" && name === "params" ? `${site.label} reads $params — a cascade-fired action has no caller to supply args, so $params never holds a value in its payload; read fields or effect outputs instead` : `${site.label} reads $can — $can (the caller's grants) is bound only in the caller-bound projection (action filters, requirements, editable predicates) and never holds a value in cascade conditions; move the check to one of those sites or drop $can`;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
function conditionSites(def) {
|
|
2172
|
+
const sites = [], workflowLayer = {
|
|
2173
|
+
scope: "workflow",
|
|
2174
|
+
names: entryNames(def.fields)
|
|
2175
|
+
};
|
|
2176
|
+
collectEditableSites({
|
|
2177
|
+
entries: def.fields,
|
|
2178
|
+
path: [ "fields" ],
|
|
2179
|
+
labelPrefix: "workflow",
|
|
2180
|
+
fields: "context-dependent",
|
|
2181
|
+
sites: sites
|
|
2182
|
+
});
|
|
2183
|
+
for (const [i, stage] of def.stages.entries()) collectStageConditionSites({
|
|
2184
|
+
stage: stage,
|
|
2185
|
+
i: i,
|
|
2186
|
+
workflowLayer: workflowLayer,
|
|
2187
|
+
sites: sites
|
|
2188
|
+
});
|
|
2189
|
+
return sites;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
|
|
2193
|
+
const stageLayer = {
|
|
2194
|
+
scope: "stage",
|
|
2195
|
+
names: entryNames(stage.fields)
|
|
2196
|
+
}, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
|
|
2197
|
+
scope: "activity",
|
|
2198
|
+
names: entryNames(activity.fields)
|
|
2199
|
+
})) ];
|
|
2200
|
+
for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
|
|
2201
|
+
groq: t.when,
|
|
2202
|
+
path: [ "stages", i, "transitions", k, "when" ],
|
|
2203
|
+
label: `transition "${t.name}" when`,
|
|
2204
|
+
policy: "cascade",
|
|
2205
|
+
fields: stageFields2
|
|
2206
|
+
});
|
|
2207
|
+
collectEditableSites({
|
|
2208
|
+
entries: stage.fields,
|
|
2209
|
+
path: [ "stages", i, "fields" ],
|
|
2210
|
+
labelPrefix: `stage "${stage.name}"`,
|
|
2211
|
+
fields: editableWindow,
|
|
2212
|
+
sites: sites
|
|
2213
|
+
});
|
|
2214
|
+
for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
|
|
2215
|
+
groq: editable,
|
|
2216
|
+
path: [ "stages", i, "editable", name ],
|
|
2217
|
+
label: `stage "${stage.name}" editable override "${name}"`,
|
|
2218
|
+
policy: "caller-bound",
|
|
2219
|
+
fields: editableWindow
|
|
2220
|
+
});
|
|
2221
|
+
for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
|
|
2222
|
+
activity: activity,
|
|
2223
|
+
path: [ "stages", i, "activities", j ],
|
|
2224
|
+
stageFields: stageFields2,
|
|
2225
|
+
sites: sites
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
|
|
2230
|
+
for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
|
|
2231
|
+
groq: entry.editable,
|
|
2232
|
+
path: [ ...path, n, "editable" ],
|
|
2233
|
+
label: `${labelPrefix} field "${entry.name}" editable`,
|
|
2234
|
+
policy: "caller-bound",
|
|
2235
|
+
fields: fields
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
|
|
2240
|
+
for (const [o, op] of (ops ?? []).entries()) op.where !== void 0 && sites.push({
|
|
2241
|
+
groq: op.where,
|
|
2242
|
+
path: [ ...path, o, "where" ],
|
|
2243
|
+
label: `${label} ${op.type} where`,
|
|
2244
|
+
...policy !== void 0 ? {
|
|
2245
|
+
policy: policy
|
|
2246
|
+
} : {},
|
|
2247
|
+
fields: fields
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
|
|
2252
|
+
const fields = [ {
|
|
2253
|
+
scope: "activity",
|
|
2254
|
+
names: entryNames(activity.fields)
|
|
2255
|
+
}, ...stageFields2 ];
|
|
2256
|
+
activity.filter !== void 0 && sites.push({
|
|
2257
|
+
groq: activity.filter,
|
|
2258
|
+
path: [ ...path, "filter" ],
|
|
2259
|
+
label: `activity "${activity.name}".filter`,
|
|
2260
|
+
policy: "cascade",
|
|
2261
|
+
fields: fields
|
|
2262
|
+
});
|
|
2263
|
+
for (const [name, groq2] of Object.entries(activity.requirements ?? {})) sites.push({
|
|
2264
|
+
groq: groq2,
|
|
2265
|
+
path: [ ...path, "requirements", name ],
|
|
2266
|
+
label: `activity "${activity.name}" requirement "${name}"`,
|
|
2267
|
+
policy: "caller-bound",
|
|
2268
|
+
fields: fields
|
|
2269
|
+
});
|
|
2270
|
+
collectEditableSites({
|
|
2271
|
+
entries: activity.fields,
|
|
2272
|
+
path: [ ...path, "fields" ],
|
|
2273
|
+
labelPrefix: `activity "${activity.name}"`,
|
|
2274
|
+
fields: fields,
|
|
2275
|
+
sites: sites
|
|
2276
|
+
}), collectActionConditionSites({
|
|
2277
|
+
activity: activity,
|
|
2278
|
+
path: path,
|
|
2279
|
+
fields: fields,
|
|
2280
|
+
sites: sites
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
|
|
2285
|
+
for (const [a, action] of (activity.actions ?? []).entries()) {
|
|
2286
|
+
const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
|
|
2287
|
+
action.when !== void 0 && sites.push({
|
|
2288
|
+
groq: action.when,
|
|
2289
|
+
path: [ ...actionPath, "when" ],
|
|
2290
|
+
label: `action "${action.name}" when`,
|
|
2291
|
+
policy: "cascade",
|
|
2292
|
+
fields: fields
|
|
2293
|
+
}), action.filter !== void 0 && sites.push({
|
|
2294
|
+
groq: action.filter,
|
|
2295
|
+
path: [ ...actionPath, "filter" ],
|
|
2296
|
+
label: `action "${action.name}" filter`,
|
|
2297
|
+
policy: cascadeFired ? "cascade" : "caller-bound",
|
|
2298
|
+
fields: fields
|
|
2299
|
+
});
|
|
2300
|
+
const payloadPolicy = cascadeFired ? "triggered-payload" : void 0;
|
|
2301
|
+
collectBindingSites({
|
|
2302
|
+
effects: action.effects,
|
|
2303
|
+
path: [ ...actionPath, "effects" ],
|
|
2304
|
+
label: `action "${action.name}"`,
|
|
2305
|
+
policy: payloadPolicy,
|
|
2306
|
+
fields: fields,
|
|
2307
|
+
sites: sites
|
|
2308
|
+
}), collectOpWhereSites({
|
|
2309
|
+
ops: action.ops,
|
|
2310
|
+
path: [ ...actionPath, "ops" ],
|
|
2311
|
+
label: `action "${action.name}"`,
|
|
2312
|
+
policy: payloadPolicy,
|
|
2313
|
+
fields: fields,
|
|
2314
|
+
sites: sites
|
|
2315
|
+
}), collectSpawnSites({
|
|
2316
|
+
action: action,
|
|
2317
|
+
path: actionPath,
|
|
2318
|
+
fields: fields,
|
|
2319
|
+
sites: sites
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
function collectSpawnSites({action: action, path: path, fields: fields, sites: sites}) {
|
|
2325
|
+
const spawn = action.spawn;
|
|
2326
|
+
if (spawn === void 0) return;
|
|
2327
|
+
const spawnPath = [ ...path, "spawn" ];
|
|
2328
|
+
sites.push({
|
|
2329
|
+
groq: spawn.forEach,
|
|
2330
|
+
path: [ ...spawnPath, "forEach" ],
|
|
2331
|
+
label: `action "${action.name}".spawn.forEach`,
|
|
2332
|
+
policy: "cascade",
|
|
2333
|
+
fields: fields
|
|
2334
|
+
});
|
|
2335
|
+
for (const [group, record] of [ [ "with", spawn.with ], [ "context", spawn.context ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
|
|
2336
|
+
groq: groq2,
|
|
2337
|
+
path: [ ...spawnPath, group, key ],
|
|
2338
|
+
label: `action "${action.name}".spawn.${group} "${key}"`,
|
|
2339
|
+
policy: "triggered-payload",
|
|
2340
|
+
fields: fields
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
function collectBindingSites({effects: effects, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
|
|
2345
|
+
for (const [e, effect] of (effects ?? []).entries()) for (const [key, groq2] of Object.entries(effect.bindings ?? {})) sites.push({
|
|
2346
|
+
groq: groq2,
|
|
2347
|
+
path: [ ...path, e, "bindings", key ],
|
|
2348
|
+
label: `${label} effect "${effect.name}" binding "${key}"`,
|
|
2349
|
+
...policy !== void 0 ? {
|
|
2350
|
+
policy: policy
|
|
2351
|
+
} : {},
|
|
2352
|
+
fields: fields
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
function checkAssigneesEntries(def, issues) {
|
|
2357
|
+
for (const {entries: entries, path: path} of fieldScopes(def)) (entries ?? []).filter(e => e.type === "assignees").length <= 1 || issues.push({
|
|
2358
|
+
path: path,
|
|
2359
|
+
message: "at most one assignees-kind field entry per scope — the inbox reads it by kind"
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
function checkActivityTerminalPaths(def, issues) {
|
|
2364
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
2365
|
+
const resolvable = terminallyResolvableActivities(stage);
|
|
2366
|
+
for (const [j, activity] of (stage.activities ?? []).entries()) resolvable.has(activity.name) || issues.push({
|
|
2367
|
+
path: [ "stages", i, "activities", j ],
|
|
2368
|
+
message: `activity "${activity.name}" has no path to a terminal status — no action in stage "${stage.name}" resolves it (\`status: 'done'\` on one of its actions, a \`{when: <condition>, status: 'done'}\` trigger, or a sibling's \`status.set\`), so it can never finish and the stage's \`$allActivitiesDone\` gate would wedge`
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function terminallyResolvableActivities(stage) {
|
|
2374
|
+
const terminalSets = (stage.activities ?? []).flatMap(activity => activity.actions ?? []).flatMap(action => action.ops ?? []).filter(op => op.type === "status.set").filter(op => TERMINAL_ACTIVITY_STATUSES.includes(op.status));
|
|
2375
|
+
return new Set(terminalSets.map(op => op.activity));
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
function checkTerminalStageActivities(def, issues) {
|
|
2379
|
+
for (const [i, stage] of def.stages.entries()) (stage.transitions ?? []).length > 0 || (stage.activities ?? []).length === 0 || issues.push({
|
|
2380
|
+
path: [ "stages", i, "activities" ],
|
|
2381
|
+
message: `stage "${stage.name}" is terminal (no transitions) but declares activities — entering a terminal stage completes the instance, so they can never run. Move the work to a \`when\` action in the source stage (it commits in the hop that moves here), or give the stage a transition out`
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
function storedRolesIssue(action) {
|
|
2386
|
+
if (action.roles !== void 0) {
|
|
2387
|
+
if (action.roles.length === 0) return `action "${action.name}" stores \`roles: []\`, which names no roles — the runtime reads an empty pin as "anyone may execute", the opposite of a pin. Omit \`roles\` to allow any identity, or list at least one role`;
|
|
2388
|
+
if (action.when === void 0) return `action "${action.name}" stores \`roles\` but is fireAction-fired (no \`when\`) — the stored pin is only read on cascade fires. Author \`roles\` normally (it desugars into the action's \`filter\`), or add \`when\` to make it a trigger`;
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
function checkStoredRolesPlacement(def, issues) {
|
|
2393
|
+
for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) {
|
|
2394
|
+
const message = storedRolesIssue(action);
|
|
2395
|
+
message !== void 0 && issues.push({
|
|
2396
|
+
path: [ "stages", i, "activities", j, "actions", a, "roles" ],
|
|
2397
|
+
message: message
|
|
2398
|
+
});
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function checkTriggeredActionParams(def, issues) {
|
|
2403
|
+
for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
|
|
2404
|
+
path: [ "stages", i, "activities", j, "actions", a, "params" ],
|
|
2405
|
+
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`
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
function checkStart(def, issues) {
|
|
2410
|
+
if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
|
|
2411
|
+
path: [ "start" ],
|
|
2412
|
+
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'`"
|
|
2413
|
+
}), checkStartFilterReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
|
|
2414
|
+
path: [ "fields", n, "required" ],
|
|
2415
|
+
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 a document reference. Make it optional, seed it another way (query/literal), or declare it doc.ref / doc.refs`
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
function checkStartFilterReads(def, issues) {
|
|
2420
|
+
const filter = def.start?.filter;
|
|
2421
|
+
if (filter === void 0) return;
|
|
2422
|
+
const bindable = new Set((def.fields ?? []).filter(isInputSourced).map(entry => entry.name)), declared = new Set((def.fields ?? []).map(entry => entry.name));
|
|
2423
|
+
for (const name of conditionFieldReadNames(filter)) bindable.has(name) || issues.push({
|
|
2424
|
+
path: [ "start", "filter" ],
|
|
2425
|
+
message: declared.has(name) ? `start.filter reads $fields.${name}, but "${name}" is not an \`input\`-sourced entry — $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after any read-side evaluation), so the read is GROQ null and the filter silently never passes. Bindable (input) fields: ${knownList(bindable)}` : `start.filter reads $fields.${name}, but no workflow-scope field entry named "${name}" is declared — $fields binds only the caller's input entries, so the read is GROQ null and the filter silently never passes. Bindable (input) fields: ${knownList(bindable)}`
|
|
2426
|
+
});
|
|
2427
|
+
readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
2428
|
+
path: [ "start", "filter" ],
|
|
2429
|
+
message: "start.filter reads the candidate document (its root), but the definition declares no required subject entry (a required doc.ref / doc.refs input) — a read surface would never have a document to bind as root, so the filter could never pass. Declare a required subject entry, or gate on $fields / the dataset instead"
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
function checkGroups(def, issues) {
|
|
2434
|
+
const workflowGroups = checkGroupDeclarations({
|
|
2435
|
+
groups: def.groups,
|
|
2436
|
+
path: [ "groups" ],
|
|
2437
|
+
what: "group name",
|
|
2438
|
+
issues: issues
|
|
2439
|
+
});
|
|
2440
|
+
checkFieldMemberships({
|
|
2441
|
+
entries: def.fields,
|
|
2442
|
+
reachable: [ workflowGroups ],
|
|
2443
|
+
path: [ "fields" ],
|
|
2444
|
+
label: "workflow fields",
|
|
2445
|
+
issues: issues
|
|
2446
|
+
});
|
|
2447
|
+
for (const [i, stage] of def.stages.entries()) checkStageGroups({
|
|
2448
|
+
stage: stage,
|
|
2449
|
+
i: i,
|
|
2450
|
+
workflowGroups: workflowGroups,
|
|
2451
|
+
issues: issues
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function checkGroupDeclarations({groups: groups, path: path, what: what, issues: issues}) {
|
|
2456
|
+
return checkDuplicates({
|
|
2457
|
+
names: (groups ?? []).map((group, n) => ({
|
|
2458
|
+
name: group.name,
|
|
2459
|
+
path: [ ...path, n, "name" ]
|
|
2460
|
+
})),
|
|
2461
|
+
what: what,
|
|
2462
|
+
issues: issues
|
|
2463
|
+
});
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
function checkStageGroups({stage: stage, i: i, workflowGroups: workflowGroups, issues: issues}) {
|
|
2467
|
+
const path = [ "stages", i ], enclosing = [ checkGroupDeclarations({
|
|
2468
|
+
groups: stage.groups,
|
|
2469
|
+
path: [ ...path, "groups" ],
|
|
2470
|
+
what: `group name in stage "${stage.name}"`,
|
|
2471
|
+
issues: issues
|
|
2472
|
+
}), workflowGroups ];
|
|
2473
|
+
checkFieldMemberships({
|
|
2474
|
+
entries: stage.fields,
|
|
2475
|
+
reachable: enclosing,
|
|
2476
|
+
path: [ ...path, "fields" ],
|
|
2477
|
+
label: `stage "${stage.name}" fields`,
|
|
2478
|
+
issues: issues
|
|
2479
|
+
});
|
|
2480
|
+
for (const [j, activity] of (stage.activities ?? []).entries()) checkActivityGroups({
|
|
2481
|
+
activity: activity,
|
|
2482
|
+
path: [ ...path, "activities", j ],
|
|
2483
|
+
enclosing: enclosing,
|
|
2484
|
+
issues: issues
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
function checkActivityGroups({activity: activity, path: path, enclosing: enclosing, issues: issues}) {
|
|
2489
|
+
checkGroupMembership({
|
|
2490
|
+
group: activity.group,
|
|
2491
|
+
reachable: enclosing,
|
|
2492
|
+
path: [ ...path, "group" ],
|
|
2493
|
+
label: `activity "${activity.name}"`,
|
|
2494
|
+
issues: issues
|
|
2495
|
+
});
|
|
2496
|
+
const inner = [ checkGroupDeclarations({
|
|
2497
|
+
groups: activity.groups,
|
|
2498
|
+
path: [ ...path, "groups" ],
|
|
2499
|
+
what: `group name in activity "${activity.name}"`,
|
|
2500
|
+
issues: issues
|
|
2501
|
+
}), ...enclosing ];
|
|
2502
|
+
checkFieldMemberships({
|
|
2503
|
+
entries: activity.fields,
|
|
2504
|
+
reachable: inner,
|
|
2505
|
+
path: [ ...path, "fields" ],
|
|
2506
|
+
label: `activity "${activity.name}" fields`,
|
|
2507
|
+
issues: issues
|
|
2508
|
+
});
|
|
2509
|
+
for (const [a, action] of (activity.actions ?? []).entries()) checkGroupMembership({
|
|
2510
|
+
group: action.group,
|
|
2511
|
+
reachable: inner,
|
|
2512
|
+
path: [ ...path, "actions", a, "group" ],
|
|
2513
|
+
label: `action "${action.name}"`,
|
|
2514
|
+
issues: issues
|
|
2515
|
+
});
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
function checkFieldMemberships({entries: entries, reachable: reachable, path: path, label: label, issues: issues}) {
|
|
2519
|
+
for (const [n, entry] of (entries ?? []).entries()) checkGroupMembership({
|
|
2520
|
+
group: entry.group,
|
|
2521
|
+
reachable: reachable,
|
|
2522
|
+
path: [ ...path, n, "group" ],
|
|
2523
|
+
label: `${label} entry "${entry.name}"`,
|
|
2524
|
+
issues: issues
|
|
2525
|
+
});
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
function checkGroupMembership({group: group, reachable: reachable, path: path, label: label, issues: issues}) {
|
|
2529
|
+
for (const name of groupMembershipNames(group)) {
|
|
2530
|
+
if (reachable.some(names => names.has(name))) continue;
|
|
2531
|
+
const declared = new Set(reachable.flatMap(names => [ ...names ]));
|
|
2532
|
+
issues.push({
|
|
2533
|
+
path: path,
|
|
2534
|
+
message: `${label} names group "${name}", which is not declared on its enclosing chain. Declared: ${knownList(declared)}`
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
function checkConditionFieldReads(def, issues) {
|
|
2540
|
+
const everywhere = allDeclaredFieldNames(def);
|
|
2541
|
+
for (const site of conditionSites(def)) for (const name of conditionFieldReadNames(site.groq)) fieldVisibleAtSite({
|
|
2542
|
+
site: site,
|
|
2543
|
+
name: name,
|
|
2544
|
+
everywhere: everywhere
|
|
2545
|
+
}) || issues.push({
|
|
2546
|
+
path: site.path,
|
|
2547
|
+
message: undeclaredFieldReadMessage(site, name)
|
|
2548
|
+
});
|
|
2549
|
+
for (const [name, groq2] of Object.entries(def.predicates ?? {})) for (const read of conditionFieldReadNames(groq2)) everywhere.has(read) || issues.push({
|
|
2550
|
+
path: [ "predicates", name ],
|
|
2551
|
+
message: noFieldAnywhereMessage(`predicate "${name}"`, read)
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
function allDeclaredFieldNames(def) {
|
|
2556
|
+
const names = /* @__PURE__ */ new Set;
|
|
2557
|
+
for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) names.add(entry.name);
|
|
2558
|
+
return names;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
function fieldVisibleAtSite({site: site, name: name, everywhere: everywhere}) {
|
|
2562
|
+
return site.fields === "context-dependent" ? everywhere.has(name) : site.fields.some(layer => layer.names.has(name));
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
function noFieldAnywhereMessage(label, name) {
|
|
2566
|
+
return `${label} reads $fields.${name}, but no field entry named "${name}" is declared anywhere in the definition — the read evaluates to GROQ null, so the condition silently fails closed`;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function undeclaredFieldReadMessage(site, name) {
|
|
2570
|
+
if (site.fields === "context-dependent") return noFieldAnywhereMessage(site.label, name);
|
|
2571
|
+
const visible = site.fields.flatMap(layer => [ ...layer.names ].map(n => `${layer.scope}:${n}`));
|
|
2572
|
+
return `${site.label} reads $fields.${name}, but no field entry named "${name}" is lexically visible here — the read evaluates to GROQ null, so the condition silently fails closed. Visible: ${visible.join(", ") || "(none)"}`;
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
function checkFieldReadSeeds(def, issues) {
|
|
2576
|
+
const workflowEntries = def.fields ?? [];
|
|
2577
|
+
for (const {entries: entries, scope: scope, path: path, label: label} of fieldScopes(def)) for (const [n, entry] of (entries ?? []).entries()) {
|
|
2578
|
+
const read = entry.initialValue;
|
|
2579
|
+
read?.type === "fieldRead" && checkSeedRead({
|
|
2580
|
+
read: read,
|
|
2581
|
+
where: `${label} entry "${entry.name}"`,
|
|
2582
|
+
entryName: entry.name,
|
|
2583
|
+
scope: scope,
|
|
2584
|
+
siblings: entries ?? [],
|
|
2585
|
+
index: n,
|
|
2586
|
+
workflowEntries: workflowEntries,
|
|
2587
|
+
path: [ ...path, n, "initialValue" ],
|
|
2588
|
+
issues: issues
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
function checkSeedRead(args) {
|
|
2594
|
+
const {read: read, where: where, path: path, issues: issues} = args, misspelled = seedScopeSpellingIssue(args);
|
|
2595
|
+
if (misspelled !== void 0) {
|
|
2596
|
+
issues.push({
|
|
2597
|
+
path: path,
|
|
2598
|
+
message: `${where} ${misspelled}`
|
|
2599
|
+
});
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const target = read.scope === "workflow" ? seedWorkflowTarget(args) : seedEarlierSiblingTarget(args);
|
|
2603
|
+
target !== void 0 && pushFieldReadPathIssue({
|
|
2604
|
+
where: where,
|
|
2605
|
+
read: read,
|
|
2606
|
+
target: target,
|
|
2607
|
+
path: path,
|
|
2608
|
+
issues: issues
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
function seedScopeSpellingIssue({read: read, scope: scope}) {
|
|
2613
|
+
if (read.scope === "workflow" && scope === "workflow") return 'seeds from a fieldRead with scope "workflow", but workflow-scope seeds resolve against earlier same-scope siblings only (no workflow layer exists yet at start/spawn) — omit `scope`';
|
|
2614
|
+
if (read.scope === "stage") {
|
|
2615
|
+
if (scope === "workflow") return 'seeds from a fieldRead with scope "stage", but a workflow-scope entry has no stage in scope — omit `scope` to read an earlier workflow-scope sibling';
|
|
2616
|
+
if (scope === "activity") return 'seeds from a fieldRead with scope "stage", but a seed resolves non-workflow reads against the scope being seeded, so an activity-scope entry cannot read stage fields — omit `scope` to read an earlier activity-scope sibling, or use scope "workflow"';
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
function seedWorkflowTarget({read: read, where: where, workflowEntries: workflowEntries, path: path, issues: issues}) {
|
|
2621
|
+
const target = workflowEntries.find(entry => entry.name === read.field);
|
|
2622
|
+
if (target !== void 0) return target;
|
|
2623
|
+
issues.push({
|
|
2624
|
+
path: path,
|
|
2625
|
+
message: `${where} seeds from workflow-scope field "${read.field}", which is not declared — the read would resolve to the default value. Known workflow fields: ${knownList(workflowEntries.map(entry => entry.name))}`
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
function seedEarlierSiblingTarget(args) {
|
|
2630
|
+
const {read: read, where: where, entryName: entryName, scope: scope, siblings: siblings, index: index, workflowEntries: workflowEntries, path: path, issues: issues} = args, targetIndex = siblings.findIndex(entry => entry.name === read.field);
|
|
2631
|
+
if (targetIndex !== -1 && targetIndex < index) return siblings[targetIndex];
|
|
2632
|
+
if (targetIndex === index) {
|
|
2633
|
+
issues.push({
|
|
2634
|
+
path: path,
|
|
2635
|
+
message: `${where} seeds from itself — a fieldRead cannot read the entry it seeds`
|
|
2636
|
+
});
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
if (targetIndex > index) {
|
|
2640
|
+
issues.push({
|
|
2641
|
+
path: path,
|
|
2642
|
+
message: `${where} seeds from "${read.field}", which is declared later in the same scope — entries resolve in declaration order, so the read would see its default. Move "${read.field}" before "${entryName}"`
|
|
2643
|
+
});
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
const workflowHint = scope !== "workflow" && workflowEntries.some(entry => entry.name === read.field) ? ' — a workflow-scope entry of that name exists; add `scope: "workflow"` to read it (an omitted scope reads same-scope siblings only)' : "";
|
|
2647
|
+
issues.push({
|
|
2648
|
+
path: path,
|
|
2649
|
+
message: `${where} seeds from "${read.field}", which is not an earlier ${scope}-scope sibling — the read would resolve to the default value${workflowHint}. Earlier siblings: ${knownList(siblings.slice(0, index).map(entry => entry.name))}`
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
|
|
2653
|
+
function opSites(def) {
|
|
2654
|
+
const sites = [];
|
|
2655
|
+
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({
|
|
2656
|
+
ops: action.ops,
|
|
2657
|
+
path: [ "stages", i, "activities", j, "actions", a, "ops" ],
|
|
2658
|
+
label: `action "${action.name}"`,
|
|
2659
|
+
stage: stage,
|
|
2660
|
+
activity: activity
|
|
2661
|
+
});
|
|
2662
|
+
return sites;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
function checkFieldReadOpValues(def, issues) {
|
|
2666
|
+
const workflowEntries = def.fields ?? [];
|
|
2667
|
+
for (const site of opSites(def)) checkOpsFieldReads({
|
|
2668
|
+
workflowEntries: workflowEntries,
|
|
2669
|
+
stageEntries: site.stage.fields ?? [],
|
|
2670
|
+
issues: issues,
|
|
2671
|
+
ops: site.ops,
|
|
2672
|
+
path: site.path,
|
|
2673
|
+
label: site.label,
|
|
2674
|
+
activityNames: entryNames(site.activity.fields)
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
|
|
2679
|
+
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({
|
|
2680
|
+
...ctx,
|
|
2681
|
+
read: read,
|
|
2682
|
+
path: readPath,
|
|
2683
|
+
where: `${label} ${op.type} value`
|
|
2684
|
+
});
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
function fieldReadsIn(value, path) {
|
|
2688
|
+
return value.type === "fieldRead" ? [ {
|
|
2689
|
+
read: value,
|
|
2690
|
+
path: path
|
|
2691
|
+
} ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityNames: activityNames, issues: issues}) {
|
|
2695
|
+
const hosts = [ {
|
|
2696
|
+
scope: "stage",
|
|
2697
|
+
entries: stageEntries
|
|
2698
|
+
}, {
|
|
2699
|
+
scope: "workflow",
|
|
2700
|
+
entries: workflowEntries
|
|
2701
|
+
} ].filter(host => read.scope === void 0 || host.scope === read.scope), target = hosts.flatMap(host => host.entries).find(entry => entry.name === read.field);
|
|
2702
|
+
if (target === void 0) {
|
|
2703
|
+
issues.push({
|
|
2704
|
+
path: path,
|
|
2705
|
+
message: opFieldReadMissMessage({
|
|
2706
|
+
read: read,
|
|
2707
|
+
where: where,
|
|
2708
|
+
hosts: hosts,
|
|
2709
|
+
activityNames: activityNames
|
|
2710
|
+
})
|
|
2711
|
+
});
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
pushFieldReadPathIssue({
|
|
2715
|
+
where: where,
|
|
2716
|
+
read: read,
|
|
2717
|
+
target: target,
|
|
2718
|
+
path: path,
|
|
2719
|
+
issues: issues
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityNames: activityNames}) {
|
|
2724
|
+
const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityNames?.has(read.field) ? ` "${read.field}" is an activity-scope field, and op-time field reads resolve against stage and workflow scopes only — declare it at stage scope to read it from an op.` : "", known = hosts.flatMap(host => host.entries.map(entry => `${host.scope}:${entry.name}`));
|
|
2725
|
+
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.${activityHint} Known: ${known.join(", ") || "(none)"}`;
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
function checkUpdateWhereOps(def, issues) {
|
|
2729
|
+
const workflow = def.fields ?? [];
|
|
2730
|
+
for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
|
|
2731
|
+
if (op.type !== "field.updateWhere") continue;
|
|
2732
|
+
const scopes = {
|
|
2733
|
+
workflow: workflow,
|
|
2734
|
+
stage: site.stage.fields ?? [],
|
|
2735
|
+
activity: site.activity.fields ?? []
|
|
2736
|
+
};
|
|
2737
|
+
checkUpdateWhereTargetKind({
|
|
2738
|
+
op: op,
|
|
2739
|
+
path: [ ...site.path, o ],
|
|
2740
|
+
label: site.label,
|
|
2741
|
+
scopes: scopes,
|
|
2742
|
+
issues: issues
|
|
2743
|
+
}), checkUpdateWhereMergeKeys({
|
|
2744
|
+
op: op,
|
|
2745
|
+
path: [ ...site.path, o ],
|
|
2746
|
+
label: site.label,
|
|
2747
|
+
issues: issues
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
function checkUpdateWhereTargetKind({op: op, path: path, label: label, scopes: scopes, issues: issues}) {
|
|
2753
|
+
const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
|
|
2754
|
+
target === void 0 || target.type === "array" || issues.push({
|
|
2755
|
+
path: [ ...path, "target" ],
|
|
2756
|
+
message: `${label} field.updateWhere targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — updateWhere merges declared row sub-fields, so its target must be an \`array\` entry${rowOpsHint(target.type)}`
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
function rowOpsHint(type) {
|
|
2761
|
+
return type === "doc.refs" || type === "assignees" ? `; to add or drop ${type} rows use field.append / field.removeWhere` : "";
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
function checkUpdateWhereMergeKeys({op: op, path: path, label: label, issues: issues}) {
|
|
2765
|
+
if (op.value.type === "object") for (const key of Object.keys(op.value.fields)) key !== "_key" && key !== "_type" || issues.push({
|
|
2766
|
+
path: [ ...path, "value", "fields", key ],
|
|
2767
|
+
message: `${label} field.updateWhere merge writes the reserved row key "${key}" — row identity and bookkeeping are engine-stamped, and a merge would restamp every matched row with the same literal`
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
function checkGuardFieldReads(def, issues) {
|
|
2772
|
+
const reads = {
|
|
2773
|
+
workflowEntries: def.fields ?? [],
|
|
2774
|
+
effectNames: new Set(effectNameSites(def).map(site => site.name)),
|
|
2775
|
+
issues: issues
|
|
2776
|
+
};
|
|
2777
|
+
for (const [i, stage] of def.stages.entries()) for (const [g, guard] of (stage.guards ?? []).entries()) {
|
|
2778
|
+
const where = `stage "${stage.name}" guard "${guard.name}"`, guardPath = [ "stages", i, "guards", g ];
|
|
2779
|
+
for (const [k, expr] of (guard.match.idRefs ?? []).entries()) checkGuardRead({
|
|
2780
|
+
...reads,
|
|
2781
|
+
expr: expr,
|
|
2782
|
+
where: `${where} match.idRefs`,
|
|
2783
|
+
path: [ ...guardPath, "match", "idRefs", k ]
|
|
2784
|
+
});
|
|
2785
|
+
for (const [key, expr] of Object.entries(guard.metadata ?? {})) checkGuardRead({
|
|
2786
|
+
...reads,
|
|
2787
|
+
expr: expr,
|
|
2788
|
+
where: `${where} metadata "${key}"`,
|
|
2789
|
+
path: [ ...guardPath, "metadata", key ]
|
|
2790
|
+
});
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
function checkGuardRead(args) {
|
|
2795
|
+
const fieldRead = FIELD_READ.exec(args.expr);
|
|
2796
|
+
if (fieldRead !== null) {
|
|
2797
|
+
checkGuardFieldRead({
|
|
2798
|
+
...args,
|
|
2799
|
+
name: fieldRead[1],
|
|
2800
|
+
valuePath: fieldRead[2]
|
|
2801
|
+
});
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const effectName = EFFECTS_READ.exec(args.expr)?.[1];
|
|
2805
|
+
effectName === void 0 || args.effectNames.has(effectName) || args.issues.push({
|
|
2806
|
+
path: args.path,
|
|
2807
|
+
message: `${args.where} reads "${args.expr}", but no effect named "${effectName}" is declared — the read resolves to undefined and the guard deploys with a hole. Known effects: ${knownList(args.effectNames)}`
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, path: path, workflowEntries: workflowEntries, issues: issues}) {
|
|
2812
|
+
const target = workflowEntries.find(entry => entry.name === name);
|
|
2813
|
+
if (target === void 0) {
|
|
2814
|
+
issues.push({
|
|
2815
|
+
path: path,
|
|
2816
|
+
message: `${where} reads "$fields.${name}", but "${name}" is not a workflow-scope field entry — guard reads resolve against workflow-scope fields only. Known workflow fields: ${knownList(workflowEntries.map(entry => entry.name))}`
|
|
2817
|
+
});
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
pushFieldReadPathIssue({
|
|
2821
|
+
where: where,
|
|
2822
|
+
read: {
|
|
2823
|
+
field: name,
|
|
2824
|
+
path: valuePath
|
|
2825
|
+
},
|
|
2826
|
+
target: target,
|
|
2827
|
+
path: path,
|
|
2828
|
+
issues: issues
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
|
|
2833
|
+
if (read.path === void 0) return;
|
|
2834
|
+
const pathIssue = fieldValuePathIssue({
|
|
2835
|
+
target: target,
|
|
2836
|
+
path: read.path
|
|
2837
|
+
});
|
|
2838
|
+
pathIssue !== void 0 && issues.push({
|
|
2839
|
+
path: path,
|
|
2840
|
+
message: `${where} reads "${read.field}" with path "${read.path}", which does not fit the declared shape of "${target.name ?? read.field}" (${target.type}) — ${pathIssue}`
|
|
2841
|
+
});
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
const SCALAR = {
|
|
2845
|
+
kind: "scalar"
|
|
2846
|
+
}, DOC_CONTENT = {
|
|
2847
|
+
kind: "opaque"
|
|
2848
|
+
}, GDR_VALUE = {
|
|
2849
|
+
kind: "record",
|
|
2850
|
+
keys: {
|
|
2851
|
+
id: SCALAR,
|
|
2852
|
+
type: SCALAR
|
|
2853
|
+
}
|
|
2854
|
+
}, RELEASE_VALUE = {
|
|
2855
|
+
kind: "record",
|
|
2856
|
+
keys: {
|
|
2857
|
+
id: SCALAR,
|
|
2858
|
+
type: SCALAR,
|
|
2859
|
+
releaseName: SCALAR
|
|
2860
|
+
}
|
|
2861
|
+
}, ACTOR_VALUE = {
|
|
2862
|
+
kind: "record",
|
|
2863
|
+
keys: {
|
|
2864
|
+
kind: SCALAR,
|
|
2865
|
+
id: SCALAR,
|
|
2866
|
+
roles: {
|
|
2867
|
+
kind: "list",
|
|
2868
|
+
item: SCALAR
|
|
2869
|
+
},
|
|
2870
|
+
onBehalfOf: SCALAR
|
|
2871
|
+
}
|
|
2872
|
+
}, ASSIGNEE_VALUE = {
|
|
2873
|
+
kind: "record",
|
|
2874
|
+
keys: {
|
|
2875
|
+
type: SCALAR,
|
|
2876
|
+
id: SCALAR,
|
|
2877
|
+
role: SCALAR
|
|
2878
|
+
}
|
|
2879
|
+
}, VALUE_NODES = {
|
|
2880
|
+
"doc.ref": () => DOC_CONTENT,
|
|
2881
|
+
"doc.refs": () => ({
|
|
2882
|
+
kind: "list",
|
|
2883
|
+
item: GDR_VALUE
|
|
2884
|
+
}),
|
|
2885
|
+
"release.ref": () => RELEASE_VALUE,
|
|
2886
|
+
string: () => SCALAR,
|
|
2887
|
+
text: () => SCALAR,
|
|
2888
|
+
number: () => SCALAR,
|
|
2889
|
+
boolean: () => SCALAR,
|
|
2890
|
+
date: () => SCALAR,
|
|
2891
|
+
datetime: () => SCALAR,
|
|
2892
|
+
url: () => SCALAR,
|
|
2893
|
+
actor: () => ACTOR_VALUE,
|
|
2894
|
+
assignee: () => ASSIGNEE_VALUE,
|
|
2895
|
+
assignees: () => ({
|
|
2896
|
+
kind: "list",
|
|
2897
|
+
item: ASSIGNEE_VALUE
|
|
2898
|
+
}),
|
|
2899
|
+
object: shape => ({
|
|
2900
|
+
kind: "object",
|
|
2901
|
+
fields: shape.fields ?? []
|
|
2902
|
+
}),
|
|
2903
|
+
array: shape => ({
|
|
2904
|
+
kind: "rows",
|
|
2905
|
+
of: shape.of ?? []
|
|
2906
|
+
})
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
function valueNodeFor(shape) {
|
|
2910
|
+
return Object.hasOwn(VALUE_NODES, shape.type) ? VALUE_NODES[shape.type](shape) : void 0;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
const INDEX_SEGMENT = /^\d+$/;
|
|
2914
|
+
|
|
2915
|
+
function stepIntoValueNode(node, segment) {
|
|
2916
|
+
switch (node.kind) {
|
|
2917
|
+
case "scalar":
|
|
2918
|
+
return "the value is a scalar with no sub-paths";
|
|
2919
|
+
|
|
2920
|
+
case "object":
|
|
2921
|
+
{
|
|
2922
|
+
const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
|
|
2923
|
+
return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
case "rows":
|
|
2927
|
+
return INDEX_SEGMENT.test(segment) ? {
|
|
2928
|
+
kind: "object",
|
|
2929
|
+
fields: node.of
|
|
2930
|
+
} : `an array value is addressed by numeric row index first (e.g. "0.<subField>") — "${segment}" is not an index`;
|
|
2931
|
+
|
|
2932
|
+
case "list":
|
|
2933
|
+
return INDEX_SEGMENT.test(segment) ? node.item : `an array value is addressed by numeric item index (e.g. "0") — "${segment}" is not an index`;
|
|
2934
|
+
|
|
2935
|
+
case "record":
|
|
2936
|
+
return Object.hasOwn(node.keys, segment) ? node.keys[segment] : `the value carries ${knownList(Object.keys(node.keys))} — "${segment}" is not one of them`;
|
|
2937
|
+
|
|
2938
|
+
case "opaque":
|
|
2939
|
+
return node;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
function fieldValuePathIssue({target: target, path: path}) {
|
|
2944
|
+
let node = valueNodeFor(target);
|
|
2945
|
+
if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
|
|
2946
|
+
for (const segment of path.split(".")) {
|
|
2947
|
+
const next = stepIntoValueNode(node, segment);
|
|
2948
|
+
if (typeof next == "string") return `at segment "${segment}": ${next}`;
|
|
2949
|
+
node = next;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
function checkWorkflowInvariants(def) {
|
|
2954
|
+
const issues = [], stageNames = checkStages(def, issues);
|
|
2955
|
+
return checkInitialStage({
|
|
2956
|
+
def: def,
|
|
2957
|
+
stageNames: stageNames,
|
|
2958
|
+
issues: issues
|
|
2959
|
+
}), checkTransitionTargets({
|
|
2960
|
+
def: def,
|
|
2961
|
+
stageNames: stageNames,
|
|
2962
|
+
issues: issues
|
|
2963
|
+
}), checkStageReachability({
|
|
2964
|
+
def: def,
|
|
2965
|
+
stageNames: stageNames,
|
|
2966
|
+
issues: issues
|
|
2967
|
+
}), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
|
|
2968
|
+
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
2969
|
+
checkUnboundCallerVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
2970
|
+
checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
|
|
2971
|
+
checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
|
|
2972
|
+
checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
|
|
2973
|
+
checkGroups(def, issues), issues;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
export { ACTIVITY_KINDS, ACTOR_KINDS, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GroupSchema, InstanceNotFoundError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, START_FILTER_VARS, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|