@sanity/workflow-engine 0.15.0 → 0.17.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 +79 -0
- package/DATAMODEL.md +177 -33
- package/dist/_chunks-cjs/invariants.cjs +1377 -330
- package/dist/_chunks-es/invariants.js +1319 -326
- package/dist/define.cjs +3 -608
- package/dist/define.d.cts +125 -416
- package/dist/define.d.ts +125 -416
- package/dist/define.js +2 -607
- package/dist/index.cjs +5370 -3906
- package/dist/index.d.cts +1439 -597
- package/dist/index.d.ts +1439 -597
- package/dist/index.js +5191 -3829
- package/package.json +3 -3
|
@@ -4,6 +4,26 @@ import { conditionOutcome, runGroq as runGroq$1, evaluateConditionOutcome as eva
|
|
|
4
4
|
|
|
5
5
|
import { parse } from "groq-js";
|
|
6
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
|
+
|
|
7
27
|
function errorMessage(err) {
|
|
8
28
|
return (err instanceof Error ? err.message : String(err)).replace(/[^\P{Cc}\n\t]/gu, "");
|
|
9
29
|
}
|
|
@@ -279,17 +299,17 @@ function effectNotFoundMessage(args) {
|
|
|
279
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}`;
|
|
280
300
|
}
|
|
281
301
|
|
|
282
|
-
const
|
|
302
|
+
const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
|
|
283
303
|
|
|
284
304
|
function validateTag(tag) {
|
|
285
|
-
if (!
|
|
305
|
+
if (!LAKE_ID_SEGMENT_RE.test(tag)) throw new ContractViolationError(`tag: invalid tag "${tag}" — must match ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS})`);
|
|
286
306
|
}
|
|
287
307
|
|
|
288
308
|
function tagScopeFilter() {
|
|
289
309
|
return "tag == $tag";
|
|
290
310
|
}
|
|
291
311
|
|
|
292
|
-
const NonEmptyString = v.pipe(v.string(), v.nonEmpty("must not be empty"));
|
|
312
|
+
const NonEmptyString$1 = v.pipe(v.string(), v.nonEmpty("must not be empty"));
|
|
293
313
|
|
|
294
314
|
function asPredicate(validate) {
|
|
295
315
|
return value => {
|
|
@@ -303,21 +323,21 @@ function asPredicate(validate) {
|
|
|
303
323
|
|
|
304
324
|
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v.variant("type", [ v.object({
|
|
305
325
|
type: v.literal("dataset"),
|
|
306
|
-
id: v.pipe(NonEmptyString, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
|
|
326
|
+
id: v.pipe(NonEmptyString$1, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
|
|
307
327
|
}), v.object({
|
|
308
328
|
type: v.literal("canvas"),
|
|
309
|
-
id: NonEmptyString
|
|
329
|
+
id: NonEmptyString$1
|
|
310
330
|
}), v.object({
|
|
311
331
|
type: v.literal("media-library"),
|
|
312
|
-
id: NonEmptyString
|
|
332
|
+
id: NonEmptyString$1
|
|
313
333
|
}), v.object({
|
|
314
334
|
type: v.literal("dashboard"),
|
|
315
|
-
id: NonEmptyString
|
|
335
|
+
id: NonEmptyString$1
|
|
316
336
|
}) ]), ResourceBindingSchema = v.object({
|
|
317
|
-
name: v.pipe(NonEmptyString, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
|
|
337
|
+
name: v.pipe(NonEmptyString$1, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
|
|
318
338
|
resource: WorkflowResourceSchema
|
|
319
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({
|
|
320
|
-
name: NonEmptyString,
|
|
340
|
+
name: NonEmptyString$1,
|
|
321
341
|
tag: v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
|
|
322
342
|
workflowResource: WorkflowResourceSchema,
|
|
323
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"))),
|
|
@@ -381,6 +401,599 @@ function actorFulfillsRole({actorRoles: actorRoles, required: required, aliases:
|
|
|
381
401
|
return actorRoles.some(role => accepted.has(role));
|
|
382
402
|
}
|
|
383
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
|
+
...start.allowed !== void 0 ? {
|
|
507
|
+
allowed: start.allowed
|
|
508
|
+
} : {}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function checkReservedRoleAliasKeys(aliases, issues) {
|
|
513
|
+
for (const key of Object.keys(aliases ?? {})) key.startsWith("$") && issues.push({
|
|
514
|
+
path: [ "roleAliases", key ],
|
|
515
|
+
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.`
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const TODOLIST_OF = [ {
|
|
520
|
+
type: "string",
|
|
521
|
+
name: "label",
|
|
522
|
+
title: "Label"
|
|
523
|
+
}, {
|
|
524
|
+
type: "string",
|
|
525
|
+
name: "status",
|
|
526
|
+
title: "Status"
|
|
527
|
+
}, {
|
|
528
|
+
type: "assignee",
|
|
529
|
+
name: "assignee",
|
|
530
|
+
title: "Assignee"
|
|
531
|
+
}, {
|
|
532
|
+
type: "date",
|
|
533
|
+
name: "dueDate",
|
|
534
|
+
title: "Due date"
|
|
535
|
+
} ], NOTES_OF = [ {
|
|
536
|
+
type: "text",
|
|
537
|
+
name: "body",
|
|
538
|
+
title: "Body"
|
|
539
|
+
}, {
|
|
540
|
+
type: "actor",
|
|
541
|
+
name: "actor",
|
|
542
|
+
title: "Actor"
|
|
543
|
+
}, {
|
|
544
|
+
type: "datetime",
|
|
545
|
+
name: "at",
|
|
546
|
+
title: "At"
|
|
547
|
+
} ];
|
|
548
|
+
|
|
549
|
+
function desugarFieldEntries({entries: entries, path: path, ctx: ctx}) {
|
|
550
|
+
return !entries || entries.length === 0 ? entries === void 0 ? void 0 : [] : entries.map((entry, i) => desugarFieldEntry({
|
|
551
|
+
entry: entry,
|
|
552
|
+
path: [ ...path, i ],
|
|
553
|
+
ctx: ctx
|
|
554
|
+
}));
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function desugarFieldEntry({entry: entry, path: path, ctx: ctx}) {
|
|
558
|
+
if (entry.type === "claim") return desugarClaimField({
|
|
559
|
+
entry: entry,
|
|
560
|
+
path: path,
|
|
561
|
+
ctx: ctx
|
|
562
|
+
});
|
|
563
|
+
if (entry.type === "todoList" || entry.type === "notes") return desugarListField({
|
|
564
|
+
entry: entry,
|
|
565
|
+
path: path,
|
|
566
|
+
ctx: ctx
|
|
567
|
+
});
|
|
568
|
+
const editable = normalizeEditable({
|
|
569
|
+
editable: entry.editable,
|
|
570
|
+
path: [ ...path, "editable" ],
|
|
571
|
+
ctx: ctx
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
...stripUndefined({
|
|
575
|
+
type: entry.type,
|
|
576
|
+
name: entry.name,
|
|
577
|
+
title: entry.title,
|
|
578
|
+
description: entry.description,
|
|
579
|
+
group: normalizeGroup(entry.group),
|
|
580
|
+
required: entry.required,
|
|
581
|
+
initialValue: entry.initialValue,
|
|
582
|
+
editable: editable,
|
|
583
|
+
types: entry.types,
|
|
584
|
+
fields: entry.fields,
|
|
585
|
+
of: entry.of
|
|
586
|
+
})
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function desugarClaimField({entry: entry, path: path, ctx: ctx}) {
|
|
591
|
+
const desugared = {
|
|
592
|
+
...stripUndefined({
|
|
593
|
+
name: entry.name,
|
|
594
|
+
title: entry.title,
|
|
595
|
+
description: entry.description,
|
|
596
|
+
group: normalizeGroup(entry.group)
|
|
597
|
+
}),
|
|
598
|
+
type: "actor"
|
|
599
|
+
};
|
|
600
|
+
return ctx.claimFields.set(desugared, {
|
|
601
|
+
name: entry.name,
|
|
602
|
+
path: path
|
|
603
|
+
}), desugared;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function desugarListField({entry: entry, path: path, ctx: ctx}) {
|
|
607
|
+
const editable = normalizeEditable({
|
|
608
|
+
editable: entry.editable,
|
|
609
|
+
path: [ ...path, "editable" ],
|
|
610
|
+
ctx: ctx
|
|
611
|
+
});
|
|
612
|
+
return {
|
|
613
|
+
...stripUndefined({
|
|
614
|
+
name: entry.name,
|
|
615
|
+
title: entry.title,
|
|
616
|
+
description: entry.description,
|
|
617
|
+
group: normalizeGroup(entry.group),
|
|
618
|
+
required: entry.required,
|
|
619
|
+
initialValue: entry.initialValue,
|
|
620
|
+
editable: editable
|
|
621
|
+
}),
|
|
622
|
+
type: "array",
|
|
623
|
+
of: entry.type === "todoList" ? TODOLIST_OF : NOTES_OF
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function normalizeEditable({editable: editable, path: path, ctx: ctx}) {
|
|
628
|
+
if (editable === void 0 || editable === !0) return editable;
|
|
629
|
+
if (Array.isArray(editable)) {
|
|
630
|
+
const condition = rolesCondition(editable, ctx.roleAliases);
|
|
631
|
+
if (condition === void 0) {
|
|
632
|
+
ctx.issues.push({
|
|
633
|
+
path: path,
|
|
634
|
+
message: "editable: [] names no roles — use `true` to open the field to anyone in its scope, or list at least one role"
|
|
635
|
+
});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
return condition;
|
|
639
|
+
}
|
|
640
|
+
return editable;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function editableFieldNames({workflowFields: workflowFields2, stageFields: stageFields2, activities: activities}) {
|
|
644
|
+
const names = /* @__PURE__ */ new Set, collect = entries => {
|
|
645
|
+
for (const entry of entries ?? []) entry.editable !== void 0 && names.add(entry.name);
|
|
646
|
+
};
|
|
647
|
+
collect(workflowFields2), collect(stageFields2);
|
|
648
|
+
for (const activity of activities) collect(activity.fields);
|
|
649
|
+
return names;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function desugarStageEditable({overrides: overrides, inScope: inScope, path: path, ctx: ctx}) {
|
|
653
|
+
if (overrides === void 0) return;
|
|
654
|
+
const out = {};
|
|
655
|
+
for (const [name, value] of Object.entries(overrides)) {
|
|
656
|
+
if (!inScope.has(name)) {
|
|
657
|
+
ctx.issues.push({
|
|
658
|
+
path: [ ...path, name ],
|
|
659
|
+
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\``
|
|
660
|
+
});
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const normalized = normalizeEditable({
|
|
664
|
+
editable: value,
|
|
665
|
+
path: [ ...path, name ],
|
|
666
|
+
ctx: ctx
|
|
667
|
+
});
|
|
668
|
+
normalized !== void 0 && (out[name] = normalized);
|
|
669
|
+
}
|
|
670
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function layerOf(entries) {
|
|
674
|
+
return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function normalizeGroup(group) {
|
|
678
|
+
return group === void 0 || Array.isArray(group) ? group : [ group ];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ctx: ctx}) {
|
|
682
|
+
const activityFields2 = desugarFieldEntries({
|
|
683
|
+
entries: activity.fields,
|
|
684
|
+
path: [ ...path, "fields" ],
|
|
685
|
+
ctx: ctx
|
|
686
|
+
}), env = {
|
|
687
|
+
layers: [ {
|
|
688
|
+
scope: "activity",
|
|
689
|
+
entries: layerOf(activityFields2)
|
|
690
|
+
}, ...stageEnv.layers ]
|
|
691
|
+
}, actions = (activity.actions ?? []).map((action, a) => desugarAction({
|
|
692
|
+
action: action,
|
|
693
|
+
path: [ ...path, "actions", a ],
|
|
694
|
+
env: env,
|
|
695
|
+
activityName: activity.name,
|
|
696
|
+
ctx: ctx
|
|
697
|
+
})), target = desugarTarget({
|
|
698
|
+
target: activity.target,
|
|
699
|
+
env: env,
|
|
700
|
+
path: [ ...path, "target" ],
|
|
701
|
+
ctx: ctx
|
|
702
|
+
});
|
|
703
|
+
return {
|
|
704
|
+
...stripUndefined({
|
|
705
|
+
name: activity.name,
|
|
706
|
+
title: activity.title,
|
|
707
|
+
description: activity.description,
|
|
708
|
+
groups: activity.groups,
|
|
709
|
+
group: normalizeGroup(activity.group),
|
|
710
|
+
filter: activity.filter,
|
|
711
|
+
requirements: activity.requirements
|
|
712
|
+
}),
|
|
713
|
+
...target ? {
|
|
714
|
+
target: target
|
|
715
|
+
} : {},
|
|
716
|
+
...activityFields2 ? {
|
|
717
|
+
fields: activityFields2
|
|
718
|
+
} : {},
|
|
719
|
+
...actions.length > 0 ? {
|
|
720
|
+
actions: actions
|
|
721
|
+
} : {}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const TARGET_DOC_KINDS = [ "doc.ref", "doc.refs", "release.ref" ];
|
|
726
|
+
|
|
727
|
+
function desugarTarget({target: target, env: env, path: path, ctx: ctx}) {
|
|
728
|
+
if (target === void 0 || target.type === "url") return target;
|
|
729
|
+
const ref = typeof target.field == "string" ? {
|
|
730
|
+
field: target.field
|
|
731
|
+
} : target.field, field = resolveRef({
|
|
732
|
+
ref: ref,
|
|
733
|
+
env: env,
|
|
734
|
+
path: [ ...path, "field" ],
|
|
735
|
+
ctx: ctx
|
|
736
|
+
}) ?? fallbackRef(ref), entry = entryAt(env, field);
|
|
737
|
+
return entry && !TARGET_DOC_KINDS.includes(entry.type) && ctx.issues.push({
|
|
738
|
+
path: [ ...path, "field" ],
|
|
739
|
+
message: `manual activity target references "${field.field}" of kind "${entry.type}" — a deep-link target needs a document-valued entry (${TARGET_DOC_KINDS.join(", ")})`
|
|
740
|
+
}), {
|
|
741
|
+
type: "field",
|
|
742
|
+
field: field
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function desugarAction({action: action, path: path, env: env, activityName: activityName, ctx: ctx}) {
|
|
747
|
+
if ("type" in action) return desugarClaimAction({
|
|
748
|
+
action: action,
|
|
749
|
+
path: path,
|
|
750
|
+
env: env,
|
|
751
|
+
ctx: ctx
|
|
752
|
+
});
|
|
753
|
+
action.roles !== void 0 && action.roles.length === 0 && ctx.issues.push({
|
|
754
|
+
path: [ ...path, "roles" ],
|
|
755
|
+
message: "roles: [] names no roles — omit it to allow any identity, or list at least one role"
|
|
756
|
+
});
|
|
757
|
+
const cascadeFired = isCascadeFired(action), filter = cascadeFired ? action.filter : andConditions([ rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = desugarOps({
|
|
758
|
+
ops: action.ops,
|
|
759
|
+
path: [ ...path, "ops" ],
|
|
760
|
+
env: env,
|
|
761
|
+
firingActivity: activityName,
|
|
762
|
+
ctx: ctx
|
|
763
|
+
}) ?? [];
|
|
764
|
+
return action.status !== void 0 && ops.push({
|
|
765
|
+
type: "status.set",
|
|
766
|
+
activity: activityName,
|
|
767
|
+
status: action.status
|
|
768
|
+
}), {
|
|
769
|
+
...stripUndefined({
|
|
770
|
+
name: action.name,
|
|
771
|
+
title: action.title,
|
|
772
|
+
description: action.description,
|
|
773
|
+
group: normalizeGroup(action.group),
|
|
774
|
+
when: action.when,
|
|
775
|
+
params: action.params,
|
|
776
|
+
effects: action.effects,
|
|
777
|
+
spawn: action.spawn
|
|
778
|
+
}),
|
|
779
|
+
...cascadeFired && action.roles !== void 0 && action.roles.length > 0 ? {
|
|
780
|
+
roles: action.roles
|
|
781
|
+
} : {},
|
|
782
|
+
...filter ? {
|
|
783
|
+
filter: filter
|
|
784
|
+
} : {},
|
|
785
|
+
...ops.length > 0 ? {
|
|
786
|
+
ops: ops
|
|
787
|
+
} : {}
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function desugarClaimAction({action: action, path: path, env: env, ctx: ctx}) {
|
|
792
|
+
const ref = typeof action.field == "string" ? {
|
|
793
|
+
field: action.field
|
|
794
|
+
} : action.field, resolved = resolveRef({
|
|
795
|
+
ref: ref,
|
|
796
|
+
env: env,
|
|
797
|
+
path: [ ...path, "field" ],
|
|
798
|
+
ctx: ctx
|
|
799
|
+
});
|
|
800
|
+
if (resolved) {
|
|
801
|
+
const entry = entryAt(env, resolved);
|
|
802
|
+
entry && entry.type !== "actor" && ctx.issues.push({
|
|
803
|
+
path: [ ...path, "field" ],
|
|
804
|
+
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)`
|
|
805
|
+
}), checkShadowedClaimTarget({
|
|
806
|
+
actionName: action.name,
|
|
807
|
+
field: ref.field,
|
|
808
|
+
resolved: resolved,
|
|
809
|
+
env: env,
|
|
810
|
+
path: [ ...path, "field" ],
|
|
811
|
+
ctx: ctx
|
|
812
|
+
}), entry && ctx.claimedFields.add(entry);
|
|
813
|
+
}
|
|
814
|
+
const noSteal = `!defined($fields.${ref.field})`, filter = andConditions([ noSteal, rolesCondition(action.roles, ctx.roleAliases), action.filter ]), ops = resolved ? [ {
|
|
815
|
+
type: "field.set",
|
|
816
|
+
target: resolved,
|
|
817
|
+
value: {
|
|
818
|
+
type: "actor"
|
|
819
|
+
}
|
|
820
|
+
} ] : [];
|
|
821
|
+
return {
|
|
822
|
+
...stripUndefined({
|
|
823
|
+
name: action.name,
|
|
824
|
+
title: action.title,
|
|
825
|
+
description: action.description,
|
|
826
|
+
group: normalizeGroup(action.group),
|
|
827
|
+
params: action.params,
|
|
828
|
+
effects: action.effects
|
|
829
|
+
}),
|
|
830
|
+
filter: filter,
|
|
831
|
+
...ops.length > 0 ? {
|
|
832
|
+
ops: ops
|
|
833
|
+
} : {}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function checkShadowedClaimTarget({actionName: actionName, field: field, resolved: resolved, env: env, path: path, ctx: ctx}) {
|
|
838
|
+
const nearest = env.layers.find(l => l.entries.has(field));
|
|
839
|
+
nearest === void 0 || nearest.scope === resolved.scope || ctx.issues.push({
|
|
840
|
+
path: path,
|
|
841
|
+
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`
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function checkUnclaimedClaimFields(ctx) {
|
|
846
|
+
for (const [entry, {name: name, path: path}] of ctx.claimFields) ctx.claimedFields.has(entry) || ctx.issues.push({
|
|
847
|
+
path: path,
|
|
848
|
+
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`
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const DEFAULT_TRANSITION_WHEN = "$allActivitiesDone";
|
|
853
|
+
|
|
854
|
+
function desugarTransition({transition: transition}) {
|
|
855
|
+
return {
|
|
856
|
+
...stripUndefined({
|
|
857
|
+
name: transition.name,
|
|
858
|
+
title: transition.title,
|
|
859
|
+
description: transition.description,
|
|
860
|
+
to: transition.to
|
|
861
|
+
}),
|
|
862
|
+
when: transition.when ?? DEFAULT_TRANSITION_WHEN
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function desugarGuard(guard) {
|
|
867
|
+
const {match: match, metadata: metadata, ...rest} = guard, {idRefs: idRefs, ...matchRest} = match;
|
|
868
|
+
return {
|
|
869
|
+
...rest,
|
|
870
|
+
match: {
|
|
871
|
+
...matchRest,
|
|
872
|
+
...idRefs !== void 0 ? {
|
|
873
|
+
idRefs: idRefs.map(printGuardRead)
|
|
874
|
+
} : {}
|
|
875
|
+
},
|
|
876
|
+
...metadata !== void 0 ? {
|
|
877
|
+
metadata: Object.fromEntries(Object.entries(metadata).map(([key, read]) => [ key, printGuardRead(read) ]))
|
|
878
|
+
} : {}
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
function desugarOps({ops: ops, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
|
|
883
|
+
if (ops) return ops.map((op, i) => desugarOp({
|
|
884
|
+
op: op,
|
|
885
|
+
path: [ ...path, i ],
|
|
886
|
+
env: env,
|
|
887
|
+
firingActivity: firingActivity,
|
|
888
|
+
ctx: ctx
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function desugarOp({op: op, path: path, env: env, firingActivity: firingActivity, ctx: ctx}) {
|
|
893
|
+
if (op.type === "status.set") return {
|
|
894
|
+
type: "status.set",
|
|
895
|
+
activity: op.activity ?? firingActivity,
|
|
896
|
+
status: op.status
|
|
897
|
+
};
|
|
898
|
+
if (op.type === "audit") return desugarAuditOp({
|
|
899
|
+
op: op,
|
|
900
|
+
path: path,
|
|
901
|
+
env: env,
|
|
902
|
+
ctx: ctx
|
|
903
|
+
});
|
|
904
|
+
const target = resolveRef({
|
|
905
|
+
ref: op.target,
|
|
906
|
+
env: env,
|
|
907
|
+
path: [ ...path, "target" ],
|
|
908
|
+
ctx: ctx
|
|
909
|
+
}) ?? fallbackRef(op.target);
|
|
910
|
+
return {
|
|
911
|
+
...op,
|
|
912
|
+
target: target
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const AUDIT_STAMPS = {
|
|
917
|
+
actor: {
|
|
918
|
+
type: "actor"
|
|
919
|
+
},
|
|
920
|
+
at: {
|
|
921
|
+
type: "now"
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
function desugarAuditOp({op: op, path: path, env: env, ctx: ctx}) {
|
|
926
|
+
const target = resolveRef({
|
|
927
|
+
ref: op.target,
|
|
928
|
+
env: env,
|
|
929
|
+
path: [ ...path, "target" ],
|
|
930
|
+
ctx: ctx
|
|
931
|
+
}) ?? fallbackRef(op.target);
|
|
932
|
+
if (op.value.type !== "object") return ctx.issues.push({
|
|
933
|
+
path: [ ...path, "value" ],
|
|
934
|
+
message: `audit value must be an object source carrying the domain fields (got "${op.value.type}")`
|
|
935
|
+
}), {
|
|
936
|
+
type: "field.append",
|
|
937
|
+
target: target,
|
|
938
|
+
value: op.value
|
|
939
|
+
};
|
|
940
|
+
const actorField = op.stampFields?.actor ?? "actor", atField = op.stampFields?.at ?? "at", fields = {
|
|
941
|
+
...op.value.fields
|
|
942
|
+
};
|
|
943
|
+
for (const [stamp, source] of [ [ actorField, AUDIT_STAMPS.actor ], [ atField, AUDIT_STAMPS.at ] ]) {
|
|
944
|
+
if (stamp in fields) {
|
|
945
|
+
ctx.issues.push({
|
|
946
|
+
path: [ ...path, "value", "fields", stamp ],
|
|
947
|
+
message: `audit stamp field "${stamp}" collides with an authored domain field — rename yours or remap the stamp via stampFields`
|
|
948
|
+
});
|
|
949
|
+
continue;
|
|
950
|
+
}
|
|
951
|
+
fields[stamp] = source;
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
type: "field.append",
|
|
955
|
+
target: target,
|
|
956
|
+
value: {
|
|
957
|
+
type: "object",
|
|
958
|
+
fields: fields
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function resolveRef({ref: ref, env: env, path: path, ctx: ctx}) {
|
|
964
|
+
const layers = ref.scope === void 0 ? env.layers : env.layers.filter(l => l.scope === ref.scope);
|
|
965
|
+
for (const layer of layers) if (layer.entries.has(ref.field)) return {
|
|
966
|
+
scope: layer.scope,
|
|
967
|
+
field: ref.field
|
|
968
|
+
};
|
|
969
|
+
const reachable = env.layers.flatMap(l => [ ...l.entries.keys() ].map(n => `${l.scope}:${n}`));
|
|
970
|
+
ctx.issues.push({
|
|
971
|
+
path: path,
|
|
972
|
+
message: `field reference "${ref.field}"${ref.scope ? ` (scope "${ref.scope}")` : ""} does not resolve to a declared entry. Reachable: ${reachable.join(", ") || "(none)"}`
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function entryAt(env, ref) {
|
|
977
|
+
return env.layers.find(l => l.scope === ref.scope)?.entries.get(ref.field);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function fallbackRef(ref) {
|
|
981
|
+
return {
|
|
982
|
+
scope: ref.scope ?? "workflow",
|
|
983
|
+
field: ref.field
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function rolesCondition(roles, aliases) {
|
|
988
|
+
if (!(!roles || roles.length === 0)) return groq`count($actor.roles[@ in ${expandRequiredRoles(roles, aliases)}]) > 0`;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function stripUndefined(obj) {
|
|
992
|
+
const out = {};
|
|
993
|
+
for (const [k, value] of Object.entries(obj)) value !== void 0 && (out[k] = value);
|
|
994
|
+
return out;
|
|
995
|
+
}
|
|
996
|
+
|
|
384
997
|
function isUnevaluable(result) {
|
|
385
998
|
return result == null;
|
|
386
999
|
}
|
|
@@ -400,9 +1013,9 @@ async function evaluateCondition(args) {
|
|
|
400
1013
|
|
|
401
1014
|
async function evaluatePredicates(args) {
|
|
402
1015
|
const out = {};
|
|
403
|
-
for (const [name,
|
|
1016
|
+
for (const [name, groq2] of Object.entries(args.predicates ?? {})) {
|
|
404
1017
|
const result = await runGroq({
|
|
405
|
-
groq:
|
|
1018
|
+
groq: groq2,
|
|
406
1019
|
params: args.params,
|
|
407
1020
|
snapshot: args.snapshot
|
|
408
1021
|
}), outcome = conditionOutcome(result);
|
|
@@ -411,46 +1024,77 @@ async function evaluatePredicates(args) {
|
|
|
411
1024
|
return out;
|
|
412
1025
|
}
|
|
413
1026
|
|
|
414
|
-
async function runGroq({groq:
|
|
1027
|
+
async function runGroq({groq: groq2, params: params, snapshot: snapshot}) {
|
|
415
1028
|
return runGroq$1({
|
|
416
|
-
groq:
|
|
1029
|
+
groq: groq2,
|
|
417
1030
|
params: params,
|
|
418
1031
|
dataset: snapshot.docs
|
|
419
1032
|
});
|
|
420
1033
|
}
|
|
421
1034
|
|
|
422
|
-
function conditionSyntaxIssues(
|
|
1035
|
+
function conditionSyntaxIssues(groq2, boundVars) {
|
|
423
1036
|
const issues = [];
|
|
424
1037
|
let tree;
|
|
425
1038
|
try {
|
|
426
|
-
tree = parse(
|
|
1039
|
+
tree = parse(groq2);
|
|
427
1040
|
} catch (err) {
|
|
428
1041
|
issues.push(errorMessage(err));
|
|
429
1042
|
}
|
|
430
|
-
if (/\*\s*\[\s*_type\b/.test(
|
|
431
|
-
boundVars !== void 0 && tree !== void 0) for (const name of conditionParameterNames(
|
|
1043
|
+
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."),
|
|
1044
|
+
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(", "));
|
|
432
1045
|
return issues;
|
|
433
1046
|
}
|
|
434
1047
|
|
|
435
|
-
function conditionParameterNames(
|
|
1048
|
+
function conditionParameterNames(groq2) {
|
|
436
1049
|
const read = /* @__PURE__ */ new Set;
|
|
437
|
-
return walkAstNodes(tryParseGroq(
|
|
1050
|
+
return walkAstNodes(tryParseGroq(groq2), node => {
|
|
438
1051
|
node.type === "Parameter" && typeof node.name == "string" && read.add(node.name);
|
|
439
1052
|
}), read;
|
|
440
1053
|
}
|
|
441
1054
|
|
|
442
|
-
function conditionFieldReadNames(
|
|
443
|
-
|
|
444
|
-
return walkAstNodes(tryParseGroq(groq), node => {
|
|
445
|
-
if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
|
|
446
|
-
const base = node.base;
|
|
447
|
-
base?.type === "Parameter" && base.name === "fields" && read.add(node.name);
|
|
448
|
-
}), read;
|
|
1055
|
+
function conditionFieldReadNames(groq2) {
|
|
1056
|
+
return new Set(conditionFieldReads(groq2).map(read => read.name));
|
|
449
1057
|
}
|
|
450
1058
|
|
|
451
|
-
function
|
|
1059
|
+
function conditionFieldReads(groq2) {
|
|
1060
|
+
const reads = /* @__PURE__ */ new Map;
|
|
1061
|
+
return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function collectFieldReads(node, reads) {
|
|
1065
|
+
if (Array.isArray(node)) {
|
|
1066
|
+
for (const item of node) collectFieldReads(item, reads);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof node != "object" || node === null) return;
|
|
1070
|
+
const chain = fieldsAttributeChain(node);
|
|
1071
|
+
if (chain !== void 0) {
|
|
1072
|
+
const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
|
|
1073
|
+
reads.set(`${name}.${path ?? ""}`, {
|
|
1074
|
+
name: name,
|
|
1075
|
+
path: path
|
|
1076
|
+
});
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
for (const value of Object.values(node)) collectFieldReads(value, reads);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function fieldsAttributeChain(node) {
|
|
1083
|
+
const names = [];
|
|
1084
|
+
let current = node;
|
|
1085
|
+
for (;typeof current == "object" && current !== null; ) {
|
|
1086
|
+
const typed = current;
|
|
1087
|
+
if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
|
|
1088
|
+
names.unshift(typed.name);
|
|
1089
|
+
const base = typed.base;
|
|
1090
|
+
if (base?.type === "Parameter" && base.name === "fields") return names;
|
|
1091
|
+
current = base;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function conditionEffectReads(groq2) {
|
|
452
1096
|
const reads = [];
|
|
453
|
-
return walkAstNodes(tryParseGroq(
|
|
1097
|
+
return walkAstNodes(tryParseGroq(groq2), node => {
|
|
454
1098
|
if (node.type !== "AccessAttribute" || typeof node.name != "string") return;
|
|
455
1099
|
const base = node.base;
|
|
456
1100
|
base?.type !== "AccessAttribute" || typeof base.name != "string" || base.base?.type === "Parameter" && base.base.name === "effects" && reads.push({
|
|
@@ -460,9 +1104,34 @@ function conditionEffectReads(groq) {
|
|
|
460
1104
|
}), reads;
|
|
461
1105
|
}
|
|
462
1106
|
|
|
463
|
-
function
|
|
1107
|
+
function readsRootDocument(groq2) {
|
|
1108
|
+
return nodeReadsRoot(tryParseGroq(groq2), 0);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const SCOPED_CHILD = {
|
|
1112
|
+
Filter: "expr",
|
|
1113
|
+
Projection: "expr",
|
|
1114
|
+
Map: "expr",
|
|
1115
|
+
FlatMap: "expr",
|
|
1116
|
+
PipeFuncCall: "args"
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
function nodeReadsRoot(node, depth) {
|
|
1120
|
+
if (Array.isArray(node)) return node.some(item => nodeReadsRoot(item, depth));
|
|
1121
|
+
if (typeof node != "object" || node === null) return !1;
|
|
1122
|
+
const typed = node;
|
|
1123
|
+
if (depth === 0 && typed.type === "This" || depth === 0 && typed.type === "AccessAttribute" && typed.base === void 0) return !0;
|
|
1124
|
+
if (typed.type === "Parent") {
|
|
1125
|
+
const climb = typeof typed.n == "number" ? typed.n : 1;
|
|
1126
|
+
return depth - climb <= 0;
|
|
1127
|
+
}
|
|
1128
|
+
const scopedChild = typeof typed.type == "string" ? SCOPED_CHILD[typed.type] : void 0;
|
|
1129
|
+
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));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function tryParseGroq(groq2) {
|
|
464
1133
|
try {
|
|
465
|
-
return parse(
|
|
1134
|
+
return parse(groq2);
|
|
466
1135
|
} catch {
|
|
467
1136
|
return;
|
|
468
1137
|
}
|
|
@@ -479,7 +1148,13 @@ function walkAstNodes(node, visit) {
|
|
|
479
1148
|
}
|
|
480
1149
|
}
|
|
481
1150
|
|
|
482
|
-
const
|
|
1151
|
+
const ACTIVITY_STATUSES = [ "active", "done", "skipped", "failed" ], TERMINAL_ACTIVITY_STATUSES = [ "done", "skipped", "failed" ];
|
|
1152
|
+
|
|
1153
|
+
function isTerminalActivityStatus(status) {
|
|
1154
|
+
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
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 = [ {
|
|
483
1158
|
name: "self",
|
|
484
1159
|
binding: "always",
|
|
485
1160
|
label: "this workflow instance",
|
|
@@ -488,7 +1163,7 @@ const CONDITION_VARS = [ {
|
|
|
488
1163
|
name: "fields",
|
|
489
1164
|
binding: "always",
|
|
490
1165
|
label: "the workflow's fields",
|
|
491
|
-
description: "Declared field entries rendered by name (`$fields.<name>` is the value, no
|
|
1166
|
+
description: "Declared field entries rendered by name (`$fields.<name>` is the value, no wrapper). Stage/activity scopes overlay lexically. What a read puts in your hand follows the declared kind: a singular `doc.ref` DEREFERENCES into the hydrated document (lake `_id`/`_type` plus content fields — which may themselves be named `id`/`type`), while `doc.refs` elements and `release.ref` stay REFERENCES — `{id, type[, releaseName]}` with `id` a GDR URI — because identity reads (membership, counting, joins) must stay total without hydrating every target, and targets may live in resources the evaluation cannot fetch from. Conditions evaluate against the in-memory snapshot only, so a dereferencing plural kind would silently hole wherever hydration lagged."
|
|
492
1167
|
}, {
|
|
493
1168
|
name: "parent",
|
|
494
1169
|
binding: "always",
|
|
@@ -509,11 +1184,16 @@ const CONDITION_VARS = [ {
|
|
|
509
1184
|
binding: "always",
|
|
510
1185
|
label: "the current time",
|
|
511
1186
|
description: "The ISO clock reading shared by every condition in one evaluation pass, so they agree on the time."
|
|
1187
|
+
}, {
|
|
1188
|
+
name: "context",
|
|
1189
|
+
binding: "always",
|
|
1190
|
+
label: "start-time context",
|
|
1191
|
+
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."
|
|
512
1192
|
}, {
|
|
513
1193
|
name: "effects",
|
|
514
1194
|
binding: "always",
|
|
515
1195
|
label: "automation outputs",
|
|
516
|
-
description: "
|
|
1196
|
+
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."
|
|
517
1197
|
}, {
|
|
518
1198
|
name: "effectStatus",
|
|
519
1199
|
binding: "always",
|
|
@@ -548,29 +1228,169 @@ const CONDITION_VARS = [ {
|
|
|
548
1228
|
name: "can",
|
|
549
1229
|
binding: "caller",
|
|
550
1230
|
label: "your permissions",
|
|
551
|
-
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 (action filters, activity requirements, editability predicates) and the fireAction/editField commit gates. Deploy rejects it at every site that evaluates without grants: transition
|
|
1231
|
+
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."
|
|
552
1232
|
}, {
|
|
553
1233
|
name: "row",
|
|
554
1234
|
binding: "spawn",
|
|
555
1235
|
label: "the spawned row",
|
|
556
|
-
description: "One `
|
|
1236
|
+
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). Deploy rejects a read at every other site (including `spawn.forEach` itself and `spawn.context`), where it is GROQ null."
|
|
557
1237
|
}, {
|
|
558
1238
|
name: "params",
|
|
559
1239
|
binding: "caller",
|
|
560
1240
|
label: "the action's arguments",
|
|
561
|
-
description: "The firing action's args — they hold values only while
|
|
1241
|
+
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."
|
|
562
1242
|
}, {
|
|
563
1243
|
name: "subworkflows",
|
|
564
1244
|
binding: "always",
|
|
565
1245
|
label: "the spawned subworkflows",
|
|
566
|
-
description: "Every row of the instance's subworkflow registry, faceted by `activity`/`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
|
|
567
|
-
} ], 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),
|
|
1246
|
+
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`."
|
|
1247
|
+
} ], 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 = [ {
|
|
1248
|
+
name: "tag",
|
|
1249
|
+
description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
|
|
1250
|
+
}, {
|
|
1251
|
+
name: "definition",
|
|
1252
|
+
description: "The `name` of the definition under evaluation (its own start block binds it)."
|
|
1253
|
+
}, {
|
|
1254
|
+
name: "now",
|
|
1255
|
+
description: "The ISO clock reading of the evaluating engine."
|
|
1256
|
+
} ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
|
|
1257
|
+
name: "fields",
|
|
1258
|
+
description: "The caller's input entries by name (`initialFields` — at `startInstance`, the values the start would seed; at a pre-flight, the values gathered so far, so a read of a not-yet-supplied entry is GROQ null). Document references bind as GDR envelopes — `$fields.<entry>.id` is the GDR URI, never a string authors assemble — a singular `doc.ref` included (nothing hydrates at the gate or the pre-flight). Pathed reads are deploy-checked against these envelope shapes."
|
|
1259
|
+
} ], GUARD_PREDICATE_VARS = [ {
|
|
568
1260
|
name: "guard",
|
|
569
1261
|
description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
|
|
570
1262
|
}, {
|
|
571
1263
|
name: "mutation",
|
|
572
1264
|
description: "The attempted mutation — `mutation.action` is the write kind being gated."
|
|
573
|
-
} ],
|
|
1265
|
+
} ], NonEmptyString = v.pipe(v.string(), v.minLength(1, "must be a non-empty string"));
|
|
1266
|
+
|
|
1267
|
+
function isParseableInstant(value) {
|
|
1268
|
+
return typeof value == "string" && !Number.isNaN(Date.parse(value));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const IsoTimestamp = v.pipe(v.string(), v.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
|
|
1272
|
+
|
|
1273
|
+
function tolerantObject() {
|
|
1274
|
+
return (entries, ..._exact) => v.looseObject(entries);
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function tolerantEntries() {
|
|
1278
|
+
return (entries, ..._exact) => entries;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function schemaTreeShape(schema) {
|
|
1282
|
+
return walkSchemaShape(schema, /* @__PURE__ */ new Set);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function walkSchemaShape(node, path) {
|
|
1286
|
+
if (typeof node != "object" || node === null) return "unknown";
|
|
1287
|
+
if (path.has(node)) return "(circular)";
|
|
1288
|
+
path.add(node);
|
|
1289
|
+
try {
|
|
1290
|
+
const schema = node;
|
|
1291
|
+
return containerShape(schema, path) ?? leafShape(schema);
|
|
1292
|
+
} finally {
|
|
1293
|
+
path.delete(node);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
|
|
1298
|
+
union: schema.options.map(option => walkSchemaShape(option, path))
|
|
1299
|
+
}), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
|
|
1300
|
+
strict_object: objectWalker,
|
|
1301
|
+
loose_object: objectWalker,
|
|
1302
|
+
object: objectWalker,
|
|
1303
|
+
array: (schema, path) => ({
|
|
1304
|
+
array: walkSchemaShape(schema.item, path)
|
|
1305
|
+
}),
|
|
1306
|
+
record: (schema, path) => ({
|
|
1307
|
+
record: walkSchemaShape(schema.value, path)
|
|
1308
|
+
}),
|
|
1309
|
+
union: unionWalker,
|
|
1310
|
+
variant: unionWalker,
|
|
1311
|
+
lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
|
|
1312
|
+
exact_optional: unwrapWalker,
|
|
1313
|
+
optional: unwrapWalker,
|
|
1314
|
+
nullable: unwrapWalker
|
|
1315
|
+
};
|
|
1316
|
+
|
|
1317
|
+
function containerShape(schema, path) {
|
|
1318
|
+
return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function objectEntriesShape(entries, path) {
|
|
1322
|
+
const out = {};
|
|
1323
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
1324
|
+
const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
|
|
1325
|
+
out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
|
|
1326
|
+
}
|
|
1327
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
function isOptionalEntry(entry) {
|
|
1331
|
+
if (typeof entry != "object" || entry === null) return !1;
|
|
1332
|
+
const kind = entry.type;
|
|
1333
|
+
return kind === "exact_optional" || kind === "optional";
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
|
|
1337
|
+
|
|
1338
|
+
function leafShape(schema) {
|
|
1339
|
+
if (schema.type === "picklist") return schema.options.join(" | ");
|
|
1340
|
+
if (schema.type === "literal") return `literal ${String(schema.literal)}`;
|
|
1341
|
+
if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
|
|
1342
|
+
if (!LEAF_KINDS.has(String(schema.type))) throw new Error(`schemaTreeShape: unhandled schema kind "${String(schema.type)}" — extend the walker before regenerating the model ledger`);
|
|
1343
|
+
return String(schema.type);
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function formatValidationError(label, issues) {
|
|
1347
|
+
const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
|
|
1348
|
+
return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function issuesFromValibot(issues) {
|
|
1352
|
+
return issues.map(issue => ({
|
|
1353
|
+
path: issue.path ? issue.path.map(item => item.key) : [],
|
|
1354
|
+
message: issue.message
|
|
1355
|
+
}));
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function formatIssuePath(path) {
|
|
1359
|
+
let out = "";
|
|
1360
|
+
for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
|
|
1361
|
+
return out;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
class PersistedDocShapeError extends WorkflowError {
|
|
1365
|
+
documentId;
|
|
1366
|
+
documentType;
|
|
1367
|
+
issues;
|
|
1368
|
+
constructor(args) {
|
|
1369
|
+
super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
|
|
1370
|
+
this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
|
|
1371
|
+
this.issues = args.issues;
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function parsePersistedDoc(args) {
|
|
1376
|
+
const result = v.safeParse(args.schema, args.doc);
|
|
1377
|
+
if (!result.success) throw new PersistedDocShapeError({
|
|
1378
|
+
documentId: documentIdOf(args.doc),
|
|
1379
|
+
documentType: args.docType,
|
|
1380
|
+
issues: issuesFromValibot(result.issues)
|
|
1381
|
+
});
|
|
1382
|
+
return result.output;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
function documentIdOf(doc) {
|
|
1386
|
+
if (typeof doc == "object" && doc !== null) {
|
|
1387
|
+
const id = doc._id;
|
|
1388
|
+
if (typeof id == "string") return id;
|
|
1389
|
+
}
|
|
1390
|
+
return "(unknown id)";
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const ACTOR_KINDS = [ "person", "agent", "system" ];
|
|
574
1394
|
|
|
575
1395
|
function releaseDocId(releaseName) {
|
|
576
1396
|
return `_.releases.${releaseName}`;
|
|
@@ -617,29 +1437,28 @@ class FieldValueShapeError extends WorkflowError {
|
|
|
617
1437
|
}
|
|
618
1438
|
}
|
|
619
1439
|
|
|
620
|
-
const
|
|
621
|
-
id:
|
|
622
|
-
type:
|
|
623
|
-
}), ReleaseRefShape = v.pipe(
|
|
624
|
-
id:
|
|
1440
|
+
const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
|
|
1441
|
+
id: GdrUriSchema,
|
|
1442
|
+
type: NonEmptyString
|
|
1443
|
+
}), ReleaseRefShape = v.pipe(tolerantObject()({
|
|
1444
|
+
id: GdrUriSchema,
|
|
625
1445
|
type: v.literal("system.release"),
|
|
626
|
-
releaseName:
|
|
627
|
-
}), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape =
|
|
1446
|
+
releaseName: NonEmptyString
|
|
1447
|
+
}), v.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = tolerantObject()({
|
|
628
1448
|
kind: v.picklist(ACTOR_KINDS),
|
|
629
|
-
id:
|
|
630
|
-
roles: v.
|
|
631
|
-
onBehalfOf: v.
|
|
632
|
-
}), AssigneeShape = v.union([
|
|
1449
|
+
id: NonEmptyString,
|
|
1450
|
+
roles: v.exactOptional(v.array(v.string())),
|
|
1451
|
+
onBehalfOf: v.exactOptional(v.string())
|
|
1452
|
+
}), AssigneeShape = v.union([ tolerantObject()({
|
|
633
1453
|
type: v.literal("user"),
|
|
634
|
-
id:
|
|
635
|
-
}),
|
|
1454
|
+
id: NonEmptyString
|
|
1455
|
+
}), tolerantObject()({
|
|
636
1456
|
type: v.literal("role"),
|
|
637
|
-
role:
|
|
638
|
-
}) ]), 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(),
|
|
1457
|
+
role: NonEmptyString
|
|
1458
|
+
}) ]), 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(), IsoTimestamp ]), 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, fieldValueSchemas = {
|
|
639
1459
|
"doc.ref": v.union([ v.null(), GdrShape ]),
|
|
640
1460
|
"doc.refs": v.array(GdrShape),
|
|
641
1461
|
"release.ref": v.union([ v.null(), ReleaseRefShape ]),
|
|
642
|
-
query: v.any(),
|
|
643
1462
|
string: NullableString,
|
|
644
1463
|
text: NullableString,
|
|
645
1464
|
number: NullableNumber,
|
|
@@ -650,6 +1469,9 @@ const GdrShape = v.looseObject({
|
|
|
650
1469
|
actor: v.union([ v.null(), ActorShape ]),
|
|
651
1470
|
assignee: v.union([ v.null(), AssigneeShape ]),
|
|
652
1471
|
assignees: v.array(AssigneeShape)
|
|
1472
|
+
}, valueSchemas = {
|
|
1473
|
+
...fieldValueSchemas,
|
|
1474
|
+
query: v.any()
|
|
653
1475
|
};
|
|
654
1476
|
|
|
655
1477
|
function shapeValueSchema(shape, leaf) {
|
|
@@ -736,7 +1558,7 @@ function isBareSeedId(id) {
|
|
|
736
1558
|
|
|
737
1559
|
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({
|
|
738
1560
|
id: AuthoringRefId,
|
|
739
|
-
type:
|
|
1561
|
+
type: NonEmptyString
|
|
740
1562
|
}), seedValueSchemas = {
|
|
741
1563
|
...valueSchemas,
|
|
742
1564
|
"doc.ref": v.union([ v.null(), AuthoringGdrShape ]),
|
|
@@ -744,7 +1566,7 @@ const AuthoringRefId = v.pipe(v.string(), v.check(isAuthoringRefId, "must be a b
|
|
|
744
1566
|
"release.ref": v.union([ v.null(), v.looseObject({
|
|
745
1567
|
id: AuthoringRefId,
|
|
746
1568
|
type: v.literal("system.release"),
|
|
747
|
-
releaseName:
|
|
1569
|
+
releaseName: NonEmptyString
|
|
748
1570
|
}) ])
|
|
749
1571
|
};
|
|
750
1572
|
|
|
@@ -787,13 +1609,7 @@ function formatIssues(issues) {
|
|
|
787
1609
|
});
|
|
788
1610
|
}
|
|
789
1611
|
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
function isTerminalActivityStatus(status) {
|
|
793
|
-
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
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" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ], 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_]*$/;
|
|
1612
|
+
const NonEmpty = NonEmptyString, PositiveInt = v.pipe(v.number(), v.integer(), v.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
797
1613
|
|
|
798
1614
|
function groqIdentifier(referencedAs) {
|
|
799
1615
|
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`));
|
|
@@ -892,7 +1708,7 @@ const StoredFieldOpSchema = v.variant("type", [ ...opSchemas(StoredFieldRefSchem
|
|
|
892
1708
|
type: v.literal("status.set"),
|
|
893
1709
|
activity: NonEmpty,
|
|
894
1710
|
status: picklist(ACTIVITY_STATUSES)
|
|
895
|
-
}) ]),
|
|
1711
|
+
}) ]), AuditOpSchema = v.strictObject({
|
|
896
1712
|
type: v.literal("audit"),
|
|
897
1713
|
target: AuthoringFieldRefSchema,
|
|
898
1714
|
value: ValueExprSchema,
|
|
@@ -1020,6 +1836,15 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
|
|
|
1020
1836
|
bindings: v.optional(v.record(v.string(), ConditionSchema)),
|
|
1021
1837
|
input: v.optional(v.record(v.string(), v.unknown())),
|
|
1022
1838
|
outputs: v.optional(v.array(FieldShapeSchema))
|
|
1839
|
+
}), DefinitionRefSchema = v.strictObject({
|
|
1840
|
+
name: NonEmpty,
|
|
1841
|
+
version: v.optional(v.union([ PositiveInt, v.literal("latest") ]))
|
|
1842
|
+
}), SubworkflowsSchema = v.strictObject({
|
|
1843
|
+
forEach: NonEmpty,
|
|
1844
|
+
definition: DefinitionRefSchema,
|
|
1845
|
+
with: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1846
|
+
context: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1847
|
+
onExit: v.optional(picklist([ "detach", "abort" ]))
|
|
1023
1848
|
}), ActionParamSchema = v.strictObject({
|
|
1024
1849
|
type: picklist([ "string", "number", "boolean", "url", "dateTime", "actor", "doc.ref", "doc.refs", "json" ]),
|
|
1025
1850
|
name: NonEmpty,
|
|
@@ -1034,14 +1859,19 @@ function actionFields(op, group) {
|
|
|
1034
1859
|
title: v.optional(v.string()),
|
|
1035
1860
|
description: v.optional(v.string()),
|
|
1036
1861
|
group: v.optional(group),
|
|
1862
|
+
when: v.optional(ConditionSchema),
|
|
1037
1863
|
filter: v.optional(ConditionSchema),
|
|
1038
1864
|
params: v.optional(v.array(ActionParamSchema)),
|
|
1039
1865
|
ops: v.optional(v.array(op)),
|
|
1040
|
-
effects: v.optional(v.array(EffectSchema))
|
|
1866
|
+
effects: v.optional(v.array(EffectSchema)),
|
|
1867
|
+
spawn: v.optional(SubworkflowsSchema)
|
|
1041
1868
|
};
|
|
1042
1869
|
}
|
|
1043
1870
|
|
|
1044
|
-
const StoredActionSchema = pinned()(v.strictObject(
|
|
1871
|
+
const StoredActionSchema = pinned()(v.strictObject({
|
|
1872
|
+
...actionFields(StoredOpSchema, StoredGroupMembershipSchema),
|
|
1873
|
+
roles: v.optional(v.array(NonEmpty))
|
|
1874
|
+
})), TerminalActivityStatus = picklist(TERMINAL_ACTIVITY_STATUSES), RawAuthoringActionSchema = pinned()(v.strictObject({
|
|
1045
1875
|
...actionFields(AuthoringOpSchema, AuthoringGroupMembershipSchema),
|
|
1046
1876
|
roles: v.optional(v.array(NonEmpty)),
|
|
1047
1877
|
status: v.optional(TerminalActivityStatus)
|
|
@@ -1056,35 +1886,19 @@ const StoredActionSchema = pinned()(v.strictObject(actionFields(StoredOpSchema,
|
|
|
1056
1886
|
filter: v.optional(ConditionSchema),
|
|
1057
1887
|
params: v.optional(v.array(ActionParamSchema)),
|
|
1058
1888
|
effects: v.optional(v.array(EffectSchema))
|
|
1059
|
-
})), AuthoringActionSchema = pinned()(v.
|
|
1060
|
-
name: NonEmpty,
|
|
1061
|
-
version: v.optional(v.union([ PositiveInt, v.literal("latest") ]))
|
|
1062
|
-
}), SubworkflowsSchema = v.strictObject({
|
|
1063
|
-
forEach: NonEmpty,
|
|
1064
|
-
definition: DefinitionRefSchema,
|
|
1065
|
-
with: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1066
|
-
context: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1067
|
-
onExit: v.optional(picklist([ "detach", "abort" ]))
|
|
1068
|
-
});
|
|
1889
|
+
})), AuthoringActionSchema = pinned()(v.lazy(input => typeof input == "object" && input !== null && "type" in input ? ClaimActionSchema : RawAuthoringActionSchema));
|
|
1069
1890
|
|
|
1070
|
-
function activityFields({field: field, action: action,
|
|
1891
|
+
function activityFields({field: field, action: action, target: target, group: group}) {
|
|
1071
1892
|
return {
|
|
1072
1893
|
name: NonEmpty,
|
|
1073
1894
|
title: v.optional(v.string()),
|
|
1074
1895
|
description: v.optional(v.string()),
|
|
1075
1896
|
groups: v.optional(v.array(GroupSchema)),
|
|
1076
1897
|
group: v.optional(group),
|
|
1077
|
-
kind: v.optional(picklist(ACTIVITY_KINDS)),
|
|
1078
1898
|
target: v.optional(target),
|
|
1079
|
-
activation: v.optional(picklist([ "auto", "manual" ])),
|
|
1080
1899
|
filter: v.optional(ConditionSchema),
|
|
1081
1900
|
requirements: v.optional(v.record(NonEmpty, ConditionSchema)),
|
|
1082
|
-
completeWhen: v.optional(ConditionSchema),
|
|
1083
|
-
failWhen: v.optional(ConditionSchema),
|
|
1084
|
-
ops: v.optional(v.array(op)),
|
|
1085
|
-
effects: v.optional(v.array(EffectSchema)),
|
|
1086
1901
|
actions: v.optional(v.array(action)),
|
|
1087
|
-
subworkflows: v.optional(SubworkflowsSchema),
|
|
1088
1902
|
fields: v.optional(v.array(field))
|
|
1089
1903
|
};
|
|
1090
1904
|
}
|
|
@@ -1092,30 +1906,26 @@ function activityFields({field: field, action: action, op: op, target: target, g
|
|
|
1092
1906
|
const StoredActivitySchema = pinned()(v.strictObject(activityFields({
|
|
1093
1907
|
field: FieldEntrySchema,
|
|
1094
1908
|
action: StoredActionSchema,
|
|
1095
|
-
op: StoredOpSchema,
|
|
1096
1909
|
target: StoredManualTargetSchema,
|
|
1097
1910
|
group: StoredGroupMembershipSchema
|
|
1098
1911
|
}))), AuthoringActivitySchema = pinned()(v.strictObject(activityFields({
|
|
1099
1912
|
field: AuthoringFieldEntrySchema,
|
|
1100
1913
|
action: AuthoringActionSchema,
|
|
1101
|
-
op: AuthoringOpSchema,
|
|
1102
1914
|
target: AuthoringManualTargetSchema,
|
|
1103
1915
|
group: AuthoringGroupMembershipSchema
|
|
1104
1916
|
})));
|
|
1105
1917
|
|
|
1106
|
-
function transitionFields(
|
|
1918
|
+
function transitionFields(when) {
|
|
1107
1919
|
return {
|
|
1108
1920
|
name: NonEmpty,
|
|
1109
1921
|
title: v.optional(v.string()),
|
|
1110
1922
|
description: v.optional(v.string()),
|
|
1111
1923
|
to: NonEmpty,
|
|
1112
|
-
|
|
1113
|
-
ops: v.optional(v.array(op)),
|
|
1114
|
-
effects: v.optional(v.array(EffectSchema))
|
|
1924
|
+
when: when
|
|
1115
1925
|
};
|
|
1116
1926
|
}
|
|
1117
1927
|
|
|
1118
|
-
const StoredTransitionSchema = pinned()(v.strictObject(transitionFields(
|
|
1928
|
+
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({
|
|
1119
1929
|
type: v.literal("self")
|
|
1120
1930
|
}), v.strictObject({
|
|
1121
1931
|
type: v.literal("now")
|
|
@@ -1177,16 +1987,26 @@ const StoredStageSchema = pinned()(v.strictObject(stageFields({
|
|
|
1177
1987
|
transition: AuthoringTransitionSchema,
|
|
1178
1988
|
guard: AuthoringGuardSchema,
|
|
1179
1989
|
editable: AuthoringEditableSchema
|
|
1180
|
-
}))), 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" ];
|
|
1990
|
+
}))), 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" ];
|
|
1991
|
+
|
|
1992
|
+
function startFields(kind) {
|
|
1993
|
+
return {
|
|
1994
|
+
kind: kind,
|
|
1995
|
+
filter: v.optional(ConditionSchema),
|
|
1996
|
+
allowed: v.optional(ConditionSchema)
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1181
1999
|
|
|
1182
|
-
|
|
2000
|
+
const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KINDS)))), AuthoringStartSchema = pinned()(v.strictObject(startFields(v.optional(picklist(START_KINDS)))));
|
|
2001
|
+
|
|
2002
|
+
function workflowFields({field: field, stage: stage, start: start}) {
|
|
1183
2003
|
return {
|
|
1184
2004
|
name: NonEmpty,
|
|
1185
2005
|
title: NonEmpty,
|
|
1186
2006
|
description: v.optional(v.string()),
|
|
1187
2007
|
groups: v.optional(v.array(GroupSchema)),
|
|
1188
2008
|
lifecycle: v.optional(picklist(WORKFLOW_LIFECYCLES)),
|
|
1189
|
-
|
|
2009
|
+
start: v.optional(start),
|
|
1190
2010
|
initialStage: NonEmpty,
|
|
1191
2011
|
fields: v.optional(v.array(field)),
|
|
1192
2012
|
stages: v.pipe(v.array(stage), v.minLength(1, "must declare at least one stage")),
|
|
@@ -1195,7 +2015,11 @@ function workflowFields(field, stage) {
|
|
|
1195
2015
|
};
|
|
1196
2016
|
}
|
|
1197
2017
|
|
|
1198
|
-
const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields(
|
|
2018
|
+
const WorkflowDefinitionSchema = pinned()(v.strictObject(workflowFields({
|
|
2019
|
+
field: FieldEntrySchema,
|
|
2020
|
+
stage: StoredStageSchema,
|
|
2021
|
+
start: StoredStartSchema
|
|
2022
|
+
})));
|
|
1199
2023
|
|
|
1200
2024
|
function parseStoredDefinition(input, label) {
|
|
1201
2025
|
return parseOrThrow({
|
|
@@ -1205,28 +2029,26 @@ function parseStoredDefinition(input, label) {
|
|
|
1205
2029
|
});
|
|
1206
2030
|
}
|
|
1207
2031
|
|
|
1208
|
-
const AuthoringWorkflowSchema = pinned()(v.strictObject(workflowFields(
|
|
2032
|
+
const AuthoringWorkflowSchema = pinned()(v.strictObject(workflowFields({
|
|
2033
|
+
field: AuthoringFieldEntrySchema,
|
|
2034
|
+
stage: AuthoringStageSchema,
|
|
2035
|
+
start: AuthoringStartSchema
|
|
2036
|
+
}))), WORKFLOW_DEFINITION_TYPE = "sanity.workflow.definition";
|
|
1209
2037
|
|
|
1210
2038
|
function isStartableDefinition(definition) {
|
|
1211
2039
|
return definition.lifecycle !== "child";
|
|
1212
2040
|
}
|
|
1213
2041
|
|
|
1214
|
-
function
|
|
1215
|
-
|
|
1216
|
-
return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
|
|
2042
|
+
function startKindOf(definition) {
|
|
2043
|
+
return definition.start?.kind ?? "interactive";
|
|
1217
2044
|
}
|
|
1218
2045
|
|
|
1219
|
-
function
|
|
1220
|
-
return
|
|
1221
|
-
path: issue.path ? issue.path.map(item => item.key) : [],
|
|
1222
|
-
message: issue.message
|
|
1223
|
-
}));
|
|
2046
|
+
function isSubjectEntry(entry) {
|
|
2047
|
+
return entry.required === !0 && (entry.type === "doc.ref" || entry.type === "doc.refs");
|
|
1224
2048
|
}
|
|
1225
2049
|
|
|
1226
|
-
function
|
|
1227
|
-
|
|
1228
|
-
for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
|
|
1229
|
-
return out;
|
|
2050
|
+
function isInputSourced(entry) {
|
|
2051
|
+
return entry.initialValue?.type === "input";
|
|
1230
2052
|
}
|
|
1231
2053
|
|
|
1232
2054
|
function parseOrThrow({schema: schema, input: input, label: label}) {
|
|
@@ -1257,6 +2079,13 @@ function checkDuplicates({names: names, what: what, issues: issues}) {
|
|
|
1257
2079
|
return seen;
|
|
1258
2080
|
}
|
|
1259
2081
|
|
|
2082
|
+
function checkDefinitionName(def, issues) {
|
|
2083
|
+
LAKE_ID_SEGMENT_RE.test(def.name) || issues.push({
|
|
2084
|
+
path: [ "name" ],
|
|
2085
|
+
message: `definition name "${def.name}" does not match the definition-name grammar ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS}) — the name is interpolated into every deployed document id (\`<tag>.<name>.v<version>\`), which the engine keeps to the same segment grammar as the tag`
|
|
2086
|
+
});
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1260
2089
|
function checkStages(def, issues) {
|
|
1261
2090
|
const stageNames = checkDuplicates({
|
|
1262
2091
|
names: def.stages.map((stage, i) => ({
|
|
@@ -1313,12 +2142,6 @@ function checkActivities({def: def, i: i, activityNames: activityNames, issues:
|
|
|
1313
2142
|
}
|
|
1314
2143
|
|
|
1315
2144
|
function checkStatusSetTargets({activity: activity, activityNames: activityNames, path: path, issues: issues}) {
|
|
1316
|
-
checkStatusSetOps({
|
|
1317
|
-
ops: activity.ops,
|
|
1318
|
-
activityNames: activityNames,
|
|
1319
|
-
path: [ ...path, "ops" ],
|
|
1320
|
-
issues: issues
|
|
1321
|
-
});
|
|
1322
2145
|
for (const [a, action] of (activity.actions ?? []).entries()) checkStatusSetOps({
|
|
1323
2146
|
ops: action.ops,
|
|
1324
2147
|
activityNames: activityNames,
|
|
@@ -1366,25 +2189,11 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
|
|
|
1366
2189
|
|
|
1367
2190
|
function effectNameSites(def) {
|
|
1368
2191
|
const sites = [];
|
|
1369
|
-
for (const [i, stage] of def.stages.entries()) {
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
sites: sites
|
|
1375
|
-
});
|
|
1376
|
-
for (const [a, action] of (activity.actions ?? []).entries()) collectEffects({
|
|
1377
|
-
effects: action.effects,
|
|
1378
|
-
path: [ "stages", i, "activities", j, "actions", a, "effects" ],
|
|
1379
|
-
sites: sites
|
|
1380
|
-
});
|
|
1381
|
-
}
|
|
1382
|
-
for (const [k, t] of (stage.transitions ?? []).entries()) collectEffects({
|
|
1383
|
-
effects: t.effects,
|
|
1384
|
-
path: [ "stages", i, "transitions", k, "effects" ],
|
|
1385
|
-
sites: sites
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
2192
|
+
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({
|
|
2193
|
+
effects: action.effects,
|
|
2194
|
+
path: [ "stages", i, "activities", j, "actions", a, "effects" ],
|
|
2195
|
+
sites: sites
|
|
2196
|
+
});
|
|
1388
2197
|
return sites;
|
|
1389
2198
|
}
|
|
1390
2199
|
|
|
@@ -1413,6 +2222,10 @@ function checkGuardNames(def, issues) {
|
|
|
1413
2222
|
what: "guard name (the guard lake _id derives from it — unique per definition)",
|
|
1414
2223
|
issues: issues
|
|
1415
2224
|
});
|
|
2225
|
+
for (const {name: name, path: path} of sites) LAKE_ID_SEGMENT_RE.test(name) || issues.push({
|
|
2226
|
+
path: path,
|
|
2227
|
+
message: `guard name "${name}" does not match the guard-name grammar ${LAKE_ID_SEGMENT_RE.source} (${LAKE_ID_SEGMENT_GLOSS}) — the guard's lake _id derives from it at stage entry, so an invalid name would wedge the entering commit with an opaque lake error instead of failing here`
|
|
2228
|
+
});
|
|
1416
2229
|
}
|
|
1417
2230
|
|
|
1418
2231
|
function fieldScopes(def) {
|
|
@@ -1471,19 +2284,23 @@ function checkPredicates(def, issues) {
|
|
|
1471
2284
|
path: [ "predicates", name ],
|
|
1472
2285
|
message: `predicate "${name}" shadows the built-in $${name} — pick another name`
|
|
1473
2286
|
});
|
|
1474
|
-
for (const [name,
|
|
2287
|
+
for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkPredicateBody({
|
|
1475
2288
|
name: name,
|
|
1476
|
-
groq:
|
|
2289
|
+
groq: groq2,
|
|
1477
2290
|
names: names,
|
|
1478
2291
|
issues: issues
|
|
1479
2292
|
});
|
|
1480
2293
|
}
|
|
1481
2294
|
|
|
1482
|
-
function checkPredicateBody({name: name, groq:
|
|
1483
|
-
const reads = conditionParameterNames(
|
|
2295
|
+
function checkPredicateBody({name: name, groq: groq2, names: names, issues: issues}) {
|
|
2296
|
+
const reads = conditionParameterNames(groq2);
|
|
1484
2297
|
for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
|
|
1485
2298
|
path: [ "predicates", name ],
|
|
1486
|
-
message: `predicate "${name}" reads $${caller} — predicates
|
|
2299
|
+
message: `predicate "${name}" reads $${caller} — predicates pre-evaluate caller-free; compose caller gates at the condition site instead`
|
|
2300
|
+
});
|
|
2301
|
+
reads.has("row") && issues.push({
|
|
2302
|
+
path: [ "predicates", name ],
|
|
2303
|
+
message: `predicate "${name}" reads $row — ${ROW_BINDING_CLAUSE}; predicates pre-evaluate once per instance, so the read is GROQ null and the predicate can never hold`
|
|
1487
2304
|
});
|
|
1488
2305
|
for (const other of names) other === name || !reads.has(other) || issues.push({
|
|
1489
2306
|
path: [ "predicates", name ],
|
|
@@ -1491,32 +2308,43 @@ function checkPredicateBody({name: name, groq: groq, names: names, issues: issue
|
|
|
1491
2308
|
});
|
|
1492
2309
|
}
|
|
1493
2310
|
|
|
1494
|
-
function
|
|
1495
|
-
return new
|
|
2311
|
+
function entryMap(entries) {
|
|
2312
|
+
return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
|
|
1496
2313
|
}
|
|
1497
2314
|
|
|
1498
|
-
function
|
|
2315
|
+
function checkUnboundConditionVars(def, issues) {
|
|
1499
2316
|
for (const site of conditionSites(def)) {
|
|
1500
2317
|
const reads = conditionParameterNames(site.groq);
|
|
1501
|
-
for (const name of
|
|
2318
|
+
for (const name of unboundVarsAt(site)) reads.has(name) && issues.push({
|
|
1502
2319
|
path: site.path,
|
|
1503
|
-
message: callerVarMessage(site, name)
|
|
2320
|
+
message: name === "row" ? rowVarMessage(site) : callerVarMessage(site, name)
|
|
1504
2321
|
});
|
|
1505
2322
|
}
|
|
1506
2323
|
}
|
|
1507
2324
|
|
|
2325
|
+
function unboundVarsAt(site) {
|
|
2326
|
+
const callerVars = unboundCallerVars(site.policy);
|
|
2327
|
+
return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
|
|
2328
|
+
}
|
|
2329
|
+
|
|
1508
2330
|
function unboundCallerVars(policy) {
|
|
1509
|
-
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : [ "can" ];
|
|
2331
|
+
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
const ROW_BINDING_CLAUSE = "$row (the discovered row in a spawn projection; the stored row under test in a where-op) is bound only while a spawn `with` projection or a where-op `where` evaluates";
|
|
2335
|
+
|
|
2336
|
+
function rowVarMessage(site) {
|
|
2337
|
+
return `${site.label} reads $row — ${ROW_BINDING_CLAUSE}; this site never binds it, so the read is GROQ null and the condition can never resolve. Move the per-row read into \`spawn.with\` or a where-op, or gate on instance state instead`;
|
|
1510
2338
|
}
|
|
1511
2339
|
|
|
1512
2340
|
function callerVarMessage(site, name) {
|
|
1513
|
-
return site.policy === "cascade" ? `${site.label} reads $${name} —
|
|
2341
|
+
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`;
|
|
1514
2342
|
}
|
|
1515
2343
|
|
|
1516
2344
|
function conditionSites(def) {
|
|
1517
2345
|
const sites = [], workflowLayer = {
|
|
1518
2346
|
scope: "workflow",
|
|
1519
|
-
|
|
2347
|
+
entries: entryMap(def.fields)
|
|
1520
2348
|
};
|
|
1521
2349
|
collectEditableSites({
|
|
1522
2350
|
entries: def.fields,
|
|
@@ -1537,35 +2365,24 @@ function conditionSites(def) {
|
|
|
1537
2365
|
function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
|
|
1538
2366
|
const stageLayer = {
|
|
1539
2367
|
scope: "stage",
|
|
1540
|
-
|
|
2368
|
+
entries: entryMap(stage.fields)
|
|
1541
2369
|
}, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
|
|
1542
2370
|
scope: "activity",
|
|
1543
|
-
|
|
2371
|
+
entries: entryMap(activity.fields)
|
|
1544
2372
|
})) ];
|
|
1545
2373
|
for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
|
|
1546
|
-
groq: t.
|
|
1547
|
-
path: [ "stages", i, "transitions", k, "
|
|
1548
|
-
label: `transition "${t.name}"
|
|
2374
|
+
groq: t.when,
|
|
2375
|
+
path: [ "stages", i, "transitions", k, "when" ],
|
|
2376
|
+
label: `transition "${t.name}" when`,
|
|
1549
2377
|
policy: "cascade",
|
|
1550
2378
|
fields: stageFields2
|
|
1551
|
-
}), collectBindingSites({
|
|
1552
|
-
effects: t.effects,
|
|
1553
|
-
path: [ "stages", i, "transitions", k, "effects" ],
|
|
1554
|
-
label: `transition "${t.name}"`,
|
|
1555
|
-
fields: stageFields2,
|
|
1556
|
-
sites: sites
|
|
1557
|
-
}), collectOpWhereSites({
|
|
1558
|
-
ops: t.ops,
|
|
1559
|
-
path: [ "stages", i, "transitions", k, "ops" ],
|
|
1560
|
-
label: `transition "${t.name}"`,
|
|
1561
|
-
fields: stageFields2,
|
|
1562
|
-
sites: sites
|
|
1563
2379
|
});
|
|
1564
2380
|
collectEditableSites({
|
|
1565
2381
|
entries: stage.fields,
|
|
1566
2382
|
path: [ "stages", i, "fields" ],
|
|
1567
2383
|
labelPrefix: `stage "${stage.name}"`,
|
|
1568
2384
|
fields: editableWindow,
|
|
2385
|
+
unionWindow: !0,
|
|
1569
2386
|
sites: sites
|
|
1570
2387
|
});
|
|
1571
2388
|
for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
|
|
@@ -1573,70 +2390,64 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
|
|
|
1573
2390
|
path: [ "stages", i, "editable", name ],
|
|
1574
2391
|
label: `stage "${stage.name}" editable override "${name}"`,
|
|
1575
2392
|
policy: "caller-bound",
|
|
1576
|
-
fields: editableWindow
|
|
2393
|
+
fields: editableWindow,
|
|
2394
|
+
unionWindow: !0
|
|
1577
2395
|
});
|
|
1578
2396
|
for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
|
|
1579
2397
|
activity: activity,
|
|
1580
2398
|
path: [ "stages", i, "activities", j ],
|
|
1581
2399
|
stageFields: stageFields2,
|
|
2400
|
+
workflowLayer: workflowLayer,
|
|
1582
2401
|
sites: sites
|
|
1583
2402
|
});
|
|
1584
2403
|
}
|
|
1585
2404
|
|
|
1586
|
-
function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
|
|
2405
|
+
function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, unionWindow: unionWindow, sites: sites}) {
|
|
1587
2406
|
for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
|
|
1588
2407
|
groq: entry.editable,
|
|
1589
2408
|
path: [ ...path, n, "editable" ],
|
|
1590
2409
|
label: `${labelPrefix} field "${entry.name}" editable`,
|
|
1591
2410
|
policy: "caller-bound",
|
|
1592
|
-
fields: fields
|
|
2411
|
+
fields: fields,
|
|
2412
|
+
...unionWindow === !0 ? {
|
|
2413
|
+
unionWindow: !0
|
|
2414
|
+
} : {}
|
|
1593
2415
|
});
|
|
1594
2416
|
}
|
|
1595
2417
|
|
|
1596
|
-
function collectOpWhereSites({ops: ops, path: path, label: label, fields: fields, sites: sites}) {
|
|
2418
|
+
function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
|
|
1597
2419
|
for (const [o, op] of (ops ?? []).entries()) op.where !== void 0 && sites.push({
|
|
1598
2420
|
groq: op.where,
|
|
1599
2421
|
path: [ ...path, o, "where" ],
|
|
1600
2422
|
label: `${label} ${op.type} where`,
|
|
2423
|
+
...policy !== void 0 ? {
|
|
2424
|
+
policy: policy
|
|
2425
|
+
} : {},
|
|
2426
|
+
bindsRow: !0,
|
|
1601
2427
|
fields: fields
|
|
1602
2428
|
});
|
|
1603
2429
|
}
|
|
1604
2430
|
|
|
1605
|
-
function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
|
|
2431
|
+
function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, workflowLayer: workflowLayer, sites: sites}) {
|
|
1606
2432
|
const fields = [ {
|
|
1607
2433
|
scope: "activity",
|
|
1608
|
-
|
|
2434
|
+
entries: entryMap(activity.fields)
|
|
1609
2435
|
}, ...stageFields2 ];
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
}
|
|
1620
|
-
for (const [name, groq] of Object.entries(activity.requirements ?? {})) sites.push({
|
|
1621
|
-
groq: groq,
|
|
2436
|
+
activity.filter !== void 0 && sites.push({
|
|
2437
|
+
groq: activity.filter,
|
|
2438
|
+
path: [ ...path, "filter" ],
|
|
2439
|
+
label: `activity "${activity.name}".filter`,
|
|
2440
|
+
policy: "cascade",
|
|
2441
|
+
fields: fields
|
|
2442
|
+
});
|
|
2443
|
+
for (const [name, groq2] of Object.entries(activity.requirements ?? {})) sites.push({
|
|
2444
|
+
groq: groq2,
|
|
1622
2445
|
path: [ ...path, "requirements", name ],
|
|
1623
2446
|
label: `activity "${activity.name}" requirement "${name}"`,
|
|
1624
2447
|
policy: "caller-bound",
|
|
1625
2448
|
fields: fields
|
|
1626
2449
|
});
|
|
1627
|
-
|
|
1628
|
-
effects: activity.effects,
|
|
1629
|
-
path: [ ...path, "effects" ],
|
|
1630
|
-
label: `activity "${activity.name}"`,
|
|
1631
|
-
fields: fields,
|
|
1632
|
-
sites: sites
|
|
1633
|
-
}), collectOpWhereSites({
|
|
1634
|
-
ops: activity.ops,
|
|
1635
|
-
path: [ ...path, "ops" ],
|
|
1636
|
-
label: `activity "${activity.name}"`,
|
|
1637
|
-
fields: fields,
|
|
1638
|
-
sites: sites
|
|
1639
|
-
}), collectEditableSites({
|
|
2450
|
+
collectEditableSites({
|
|
1640
2451
|
entries: activity.fields,
|
|
1641
2452
|
path: [ ...path, "fields" ],
|
|
1642
2453
|
labelPrefix: `activity "${activity.name}"`,
|
|
@@ -1646,60 +2457,84 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
|
|
|
1646
2457
|
activity: activity,
|
|
1647
2458
|
path: path,
|
|
1648
2459
|
fields: fields,
|
|
1649
|
-
|
|
1650
|
-
}), collectSubworkflowSites({
|
|
1651
|
-
activity: activity,
|
|
1652
|
-
path: path,
|
|
1653
|
-
fields: fields,
|
|
2460
|
+
workflowLayer: workflowLayer,
|
|
1654
2461
|
sites: sites
|
|
1655
2462
|
});
|
|
1656
2463
|
}
|
|
1657
2464
|
|
|
1658
|
-
function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
|
|
1659
|
-
for (const [a, action] of (activity.actions ?? []).entries())
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2465
|
+
function collectActionConditionSites({activity: activity, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
|
|
2466
|
+
for (const [a, action] of (activity.actions ?? []).entries()) {
|
|
2467
|
+
const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
|
|
2468
|
+
action.when !== void 0 && sites.push({
|
|
2469
|
+
groq: action.when,
|
|
2470
|
+
path: [ ...actionPath, "when" ],
|
|
2471
|
+
label: `action "${action.name}" when`,
|
|
2472
|
+
policy: "cascade",
|
|
2473
|
+
fields: fields
|
|
2474
|
+
}), action.filter !== void 0 && sites.push({
|
|
2475
|
+
groq: action.filter,
|
|
2476
|
+
path: [ ...actionPath, "filter" ],
|
|
2477
|
+
label: `action "${action.name}" filter`,
|
|
2478
|
+
policy: cascadeFired ? "cascade" : "caller-bound",
|
|
2479
|
+
fields: fields
|
|
2480
|
+
});
|
|
2481
|
+
const payloadPolicy = cascadeFired ? "triggered-payload" : void 0;
|
|
2482
|
+
collectBindingSites({
|
|
2483
|
+
effects: action.effects,
|
|
2484
|
+
path: [ ...actionPath, "effects" ],
|
|
2485
|
+
label: `action "${action.name}"`,
|
|
2486
|
+
policy: payloadPolicy,
|
|
2487
|
+
fields: fields,
|
|
2488
|
+
sites: sites
|
|
2489
|
+
}), collectOpWhereSites({
|
|
2490
|
+
ops: action.ops,
|
|
2491
|
+
path: [ ...actionPath, "ops" ],
|
|
2492
|
+
label: `action "${action.name}"`,
|
|
2493
|
+
policy: payloadPolicy,
|
|
2494
|
+
fields: fields,
|
|
2495
|
+
sites: sites
|
|
2496
|
+
}), collectSpawnSites({
|
|
2497
|
+
action: action,
|
|
2498
|
+
path: actionPath,
|
|
2499
|
+
fields: fields,
|
|
2500
|
+
workflowLayer: workflowLayer,
|
|
2501
|
+
sites: sites
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
1678
2504
|
}
|
|
1679
2505
|
|
|
1680
|
-
function
|
|
1681
|
-
const
|
|
1682
|
-
if (
|
|
1683
|
-
const
|
|
2506
|
+
function collectSpawnSites({action: action, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
|
|
2507
|
+
const spawn = action.spawn;
|
|
2508
|
+
if (spawn === void 0) return;
|
|
2509
|
+
const spawnPath = [ ...path, "spawn" ];
|
|
1684
2510
|
sites.push({
|
|
1685
|
-
groq:
|
|
1686
|
-
path: [ ...
|
|
1687
|
-
label: `
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2511
|
+
groq: spawn.forEach,
|
|
2512
|
+
path: [ ...spawnPath, "forEach" ],
|
|
2513
|
+
label: `action "${action.name}".spawn.forEach`,
|
|
2514
|
+
policy: "cascade",
|
|
2515
|
+
fields: [ workflowLayer ],
|
|
2516
|
+
windowNote: "spawn.forEach evaluates against workflow-scope $fields only — stage/activity fields are never bound at discovery time"
|
|
2517
|
+
});
|
|
2518
|
+
for (const [group, record, bindsRow] of [ [ "with", spawn.with, !0 ], [ "context", spawn.context, !1 ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
|
|
2519
|
+
groq: groq2,
|
|
2520
|
+
path: [ ...spawnPath, group, key ],
|
|
2521
|
+
label: `action "${action.name}".spawn.${group} "${key}"`,
|
|
2522
|
+
policy: "triggered-payload",
|
|
2523
|
+
...bindsRow ? {
|
|
2524
|
+
bindsRow: !0
|
|
2525
|
+
} : {},
|
|
1694
2526
|
fields: fields
|
|
1695
2527
|
});
|
|
1696
2528
|
}
|
|
1697
2529
|
|
|
1698
|
-
function collectBindingSites({effects: effects, path: path, label: label, fields: fields, sites: sites}) {
|
|
1699
|
-
for (const [e, effect] of (effects ?? []).entries()) for (const [key,
|
|
1700
|
-
groq:
|
|
2530
|
+
function collectBindingSites({effects: effects, path: path, label: label, policy: policy, fields: fields, sites: sites}) {
|
|
2531
|
+
for (const [e, effect] of (effects ?? []).entries()) for (const [key, groq2] of Object.entries(effect.bindings ?? {})) sites.push({
|
|
2532
|
+
groq: groq2,
|
|
1701
2533
|
path: [ ...path, e, "bindings", key ],
|
|
1702
2534
|
label: `${label} effect "${effect.name}" binding "${key}"`,
|
|
2535
|
+
...policy !== void 0 ? {
|
|
2536
|
+
policy: policy
|
|
2537
|
+
} : {},
|
|
1703
2538
|
fields: fields
|
|
1704
2539
|
});
|
|
1705
2540
|
}
|
|
@@ -1711,34 +2546,102 @@ function checkAssigneesEntries(def, issues) {
|
|
|
1711
2546
|
});
|
|
1712
2547
|
}
|
|
1713
2548
|
|
|
1714
|
-
function
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2549
|
+
function checkActivityTerminalPaths(def, issues) {
|
|
2550
|
+
for (const [i, stage] of def.stages.entries()) {
|
|
2551
|
+
const resolvable = terminallyResolvableActivities(stage);
|
|
2552
|
+
for (const [j, activity] of (stage.activities ?? []).entries()) resolvable.has(activity.name) || issues.push({
|
|
2553
|
+
path: [ "stages", i, "activities", j ],
|
|
2554
|
+
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`
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
1721
2557
|
}
|
|
1722
2558
|
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
receive: s => [ s.hasCompleteWhen || s.hasSubworkflows ? void 0 : "needs a `completeWhen` (or `subworkflows`) condition to wait on", s.hasActions ? "has no actor, so it cannot carry actions" : void 0 ].filter(m => m !== void 0),
|
|
1728
|
-
script: s => [ s.hasActions ? "runs inline with no actor, so it cannot carry actions" : void 0, s.hasCompleteWhen ? "resolves on activation, so it cannot carry a `completeWhen` (that is a `receive`/`service` wait)" : void 0, s.hasSubworkflows ? "resolves on activation, so it cannot fan out with `subworkflows`" : void 0 ].filter(m => m !== void 0)
|
|
1729
|
-
};
|
|
2559
|
+
function terminallyResolvableActivities(stage) {
|
|
2560
|
+
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));
|
|
2561
|
+
return new Set(terminalSets.map(op => op.activity));
|
|
2562
|
+
}
|
|
1730
2563
|
|
|
1731
|
-
function
|
|
1732
|
-
for (const [i, stage] of def.stages.entries())
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2564
|
+
function checkTerminalStageActivities(def, issues) {
|
|
2565
|
+
for (const [i, stage] of def.stages.entries()) (stage.transitions ?? []).length > 0 || (stage.activities ?? []).length === 0 || issues.push({
|
|
2566
|
+
path: [ "stages", i, "activities" ],
|
|
2567
|
+
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`
|
|
2568
|
+
});
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
function storedRolesIssue(action) {
|
|
2572
|
+
if (action.roles !== void 0) {
|
|
2573
|
+
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`;
|
|
2574
|
+
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`;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
function checkStoredRolesPlacement(def, issues) {
|
|
2579
|
+
for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) {
|
|
2580
|
+
const message = storedRolesIssue(action);
|
|
2581
|
+
message !== void 0 && issues.push({
|
|
2582
|
+
path: [ "stages", i, "activities", j, "actions", a, "roles" ],
|
|
2583
|
+
message: message
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
function checkTriggeredActionParams(def, issues) {
|
|
2589
|
+
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({
|
|
2590
|
+
path: [ "stages", i, "activities", j, "actions", a, "params" ],
|
|
2591
|
+
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`
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
function checkStart(def, issues) {
|
|
2596
|
+
if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
|
|
2597
|
+
path: [ "start" ],
|
|
2598
|
+
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'`"
|
|
2599
|
+
}), checkStartFilterReads(def, issues), checkStartAllowedReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
|
|
2600
|
+
path: [ "fields", n, "required" ],
|
|
2601
|
+
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`
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
function checkStartFilterReads(def, issues) {
|
|
2606
|
+
const filter = def.start?.filter;
|
|
2607
|
+
filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
|
|
2608
|
+
path: [ "start", "filter" ],
|
|
2609
|
+
message: "start.filter reads $fields, but the filter is browse-time-pure — a start surface evaluates it per document, before any inputs exist, so $fields cannot be bound. Move the input-dependent rule to start.allowed, the start-time permission predicate that binds $fields and is enforced by startInstance"
|
|
2610
|
+
}), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
2611
|
+
path: [ "start", "filter" ],
|
|
2612
|
+
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 every root read is GROQ null and the filter silently misevaluates. Declare a required subject entry, or gate on the dataset instead"
|
|
2613
|
+
}));
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
function checkStartAllowedReads(def, issues) {
|
|
2617
|
+
const allowed = def.start?.allowed;
|
|
2618
|
+
if (allowed === void 0) return;
|
|
2619
|
+
const bindable = new Map((def.fields ?? []).filter(isInputSourced).map(entry => [ entry.name, entry ])), declared = new Set((def.fields ?? []).map(entry => entry.name)), nullVerdict = `so the read is GROQ null and null semantics decide the verdict (refuse-all or vacuously-allow, by the expression's shape), never the caller's real input. Bindable (input) fields: ${knownList(bindable.keys())}`;
|
|
2620
|
+
for (const read of conditionFieldReads(allowed)) {
|
|
2621
|
+
const entry = bindable.get(read.name);
|
|
2622
|
+
if (entry === void 0) {
|
|
2623
|
+
issues.push({
|
|
2624
|
+
path: [ "start", "allowed" ],
|
|
2625
|
+
message: declared.has(read.name) ? `start.allowed reads $fields.${read.name}, but "${read.name}" is not an \`input\`-sourced entry — $fields binds only the caller's input entries (query/literal/fieldRead entries resolve at materialisation, after the start gate), ${nullVerdict}` : `start.allowed reads $fields.${read.name}, but no workflow-scope field entry named "${read.name}" is declared — $fields binds only the caller's input entries, ${nullVerdict}`
|
|
2626
|
+
});
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
pushFieldReadPathIssue({
|
|
2630
|
+
where: "start.allowed",
|
|
2631
|
+
read: {
|
|
2632
|
+
field: read.name,
|
|
2633
|
+
path: read.path
|
|
2634
|
+
},
|
|
2635
|
+
target: entry,
|
|
2636
|
+
path: [ "start", "allowed" ],
|
|
2637
|
+
issues: issues,
|
|
2638
|
+
nodes: START_ALLOWED_VALUE_NODES
|
|
1740
2639
|
});
|
|
1741
2640
|
}
|
|
2641
|
+
readsRootDocument(allowed) && issues.push({
|
|
2642
|
+
path: [ "start", "allowed" ],
|
|
2643
|
+
message: "start.allowed reads the candidate document as its root, but no root ever binds in the start-allowed context — startInstance holds inputs, not a loaded document, so the read is GROQ null and null semantics decide the verdict, never the workflow's real state. Read the subject as $fields.<entry> (its GDR URI is $fields.<entry>.id); a per-document visibility rule belongs in start.filter"
|
|
2644
|
+
});
|
|
1742
2645
|
}
|
|
1743
2646
|
|
|
1744
2647
|
function checkGroups(def, issues) {
|
|
@@ -1848,39 +2751,80 @@ function checkGroupMembership({group: group, reachable: reachable, path: path, l
|
|
|
1848
2751
|
}
|
|
1849
2752
|
|
|
1850
2753
|
function checkConditionFieldReads(def, issues) {
|
|
1851
|
-
const everywhere =
|
|
1852
|
-
for (const site of conditionSites(def))
|
|
2754
|
+
const everywhere = allDeclaredFieldEntries(def);
|
|
2755
|
+
for (const site of conditionSites(def)) checkSiteFieldReads({
|
|
1853
2756
|
site: site,
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
}) || issues.push({
|
|
1857
|
-
path: site.path,
|
|
1858
|
-
message: undeclaredFieldReadMessage(site, name)
|
|
2757
|
+
everywhere: everywhere,
|
|
2758
|
+
issues: issues
|
|
1859
2759
|
});
|
|
1860
|
-
for (const [name,
|
|
1861
|
-
|
|
1862
|
-
|
|
2760
|
+
for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkSiteFieldReads({
|
|
2761
|
+
site: {
|
|
2762
|
+
groq: groq2,
|
|
2763
|
+
path: [ "predicates", name ],
|
|
2764
|
+
label: `predicate "${name}"`,
|
|
2765
|
+
fields: "context-dependent"
|
|
2766
|
+
},
|
|
2767
|
+
everywhere: everywhere,
|
|
2768
|
+
issues: issues
|
|
1863
2769
|
});
|
|
1864
2770
|
}
|
|
1865
2771
|
|
|
1866
|
-
function
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1869
|
-
|
|
2772
|
+
function checkSiteFieldReads({site: site, everywhere: everywhere, issues: issues}) {
|
|
2773
|
+
for (const read of conditionFieldReads(site.groq)) {
|
|
2774
|
+
const targets = readTargets({
|
|
2775
|
+
site: site,
|
|
2776
|
+
read: read,
|
|
2777
|
+
everywhere: everywhere
|
|
2778
|
+
});
|
|
2779
|
+
if (targets === "undeclared") {
|
|
2780
|
+
issues.push({
|
|
2781
|
+
path: site.path,
|
|
2782
|
+
message: undeclaredFieldReadMessage(site, read.name)
|
|
2783
|
+
});
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
const path = read.path;
|
|
2787
|
+
path !== void 0 && (targets.some(target => fieldValuePathIssue({
|
|
2788
|
+
target: target,
|
|
2789
|
+
path: path
|
|
2790
|
+
}) === void 0) || pushFieldReadPathIssue({
|
|
2791
|
+
where: site.label,
|
|
2792
|
+
read: {
|
|
2793
|
+
field: read.name,
|
|
2794
|
+
path: path
|
|
2795
|
+
},
|
|
2796
|
+
target: targets[0],
|
|
2797
|
+
path: site.path,
|
|
2798
|
+
issues: issues
|
|
2799
|
+
}));
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
function readTargets({site: site, read: read, everywhere: everywhere}) {
|
|
2804
|
+
if (site.fields === "context-dependent") return everywhere.get(read.name) ?? "undeclared";
|
|
2805
|
+
const layers = site.fields.filter(layer => layer.entries.has(read.name));
|
|
2806
|
+
if (layers.length === 0) return "undeclared";
|
|
2807
|
+
const entries = layers.map(layer => layer.entries.get(read.name));
|
|
2808
|
+
return site.unionWindow === !0 ? entries : [ entries[0] ];
|
|
1870
2809
|
}
|
|
1871
2810
|
|
|
1872
|
-
function
|
|
1873
|
-
|
|
2811
|
+
function allDeclaredFieldEntries(def) {
|
|
2812
|
+
const declared = /* @__PURE__ */ new Map;
|
|
2813
|
+
for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) {
|
|
2814
|
+
const list = declared.get(entry.name);
|
|
2815
|
+
list === void 0 ? declared.set(entry.name, [ entry ]) : list.push(entry);
|
|
2816
|
+
}
|
|
2817
|
+
return declared;
|
|
1874
2818
|
}
|
|
1875
2819
|
|
|
1876
2820
|
function noFieldAnywhereMessage(label, name) {
|
|
1877
|
-
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
|
|
2821
|
+
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 misevaluates`;
|
|
1878
2822
|
}
|
|
1879
2823
|
|
|
1880
2824
|
function undeclaredFieldReadMessage(site, name) {
|
|
1881
2825
|
if (site.fields === "context-dependent") return noFieldAnywhereMessage(site.label, name);
|
|
1882
|
-
const visible = site.fields.flatMap(layer => [ ...layer.
|
|
1883
|
-
return `${site.label} reads $fields.${name}, but
|
|
2826
|
+
const visible = site.fields.flatMap(layer => [ ...layer.entries.keys() ].map(n => `${layer.scope}:${n}`)), window = site.windowNote === void 0 ? `no field entry named "${name}" is lexically visible here` : `"${name}" is not in this site's window (${site.windowNote})`;
|
|
2827
|
+
return `${site.label} reads $fields.${name}, but ${window} — the read evaluates to GROQ null, so the condition silently misevaluates. Visible: ${visible.join(", ") || "(none)"}`;
|
|
1884
2828
|
}
|
|
1885
2829
|
|
|
1886
2830
|
function checkFieldReadSeeds(def, issues) {
|
|
@@ -1961,38 +2905,29 @@ function seedEarlierSiblingTarget(args) {
|
|
|
1961
2905
|
});
|
|
1962
2906
|
}
|
|
1963
2907
|
|
|
2908
|
+
function opSites(def) {
|
|
2909
|
+
const sites = [];
|
|
2910
|
+
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({
|
|
2911
|
+
ops: action.ops,
|
|
2912
|
+
path: [ "stages", i, "activities", j, "actions", a, "ops" ],
|
|
2913
|
+
label: `action "${action.name}"`,
|
|
2914
|
+
stage: stage,
|
|
2915
|
+
activity: activity
|
|
2916
|
+
});
|
|
2917
|
+
return sites;
|
|
2918
|
+
}
|
|
2919
|
+
|
|
1964
2920
|
function checkFieldReadOpValues(def, issues) {
|
|
1965
2921
|
const workflowEntries = def.fields ?? [];
|
|
1966
|
-
for (const
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
path: [ "stages", i, "transitions", k, "ops" ],
|
|
1976
|
-
label: `transition "${t.name}"`
|
|
1977
|
-
});
|
|
1978
|
-
for (const [j, activity] of (stage.activities ?? []).entries()) {
|
|
1979
|
-
const activityNames = entryNames(activity.fields), activityPath = [ "stages", i, "activities", j ];
|
|
1980
|
-
checkOpsFieldReads({
|
|
1981
|
-
...shared,
|
|
1982
|
-
ops: activity.ops,
|
|
1983
|
-
path: [ ...activityPath, "ops" ],
|
|
1984
|
-
label: `activity "${activity.name}"`,
|
|
1985
|
-
activityNames: activityNames
|
|
1986
|
-
});
|
|
1987
|
-
for (const [a, action] of (activity.actions ?? []).entries()) checkOpsFieldReads({
|
|
1988
|
-
...shared,
|
|
1989
|
-
ops: action.ops,
|
|
1990
|
-
path: [ ...activityPath, "actions", a, "ops" ],
|
|
1991
|
-
label: `action "${action.name}"`,
|
|
1992
|
-
activityNames: activityNames
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
}
|
|
2922
|
+
for (const site of opSites(def)) checkOpsFieldReads({
|
|
2923
|
+
workflowEntries: workflowEntries,
|
|
2924
|
+
stageEntries: site.stage.fields ?? [],
|
|
2925
|
+
issues: issues,
|
|
2926
|
+
ops: site.ops,
|
|
2927
|
+
path: site.path,
|
|
2928
|
+
label: site.label,
|
|
2929
|
+
activityEntries: entryMap(site.activity.fields)
|
|
2930
|
+
});
|
|
1996
2931
|
}
|
|
1997
2932
|
|
|
1998
2933
|
function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
|
|
@@ -2011,7 +2946,7 @@ function fieldReadsIn(value, path) {
|
|
|
2011
2946
|
} ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
|
|
2012
2947
|
}
|
|
2013
2948
|
|
|
2014
|
-
function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries,
|
|
2949
|
+
function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
|
|
2015
2950
|
const hosts = [ {
|
|
2016
2951
|
scope: "stage",
|
|
2017
2952
|
entries: stageEntries
|
|
@@ -2026,7 +2961,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
|
|
|
2026
2961
|
read: read,
|
|
2027
2962
|
where: where,
|
|
2028
2963
|
hosts: hosts,
|
|
2029
|
-
|
|
2964
|
+
activityEntries: activityEntries
|
|
2030
2965
|
})
|
|
2031
2966
|
});
|
|
2032
2967
|
return;
|
|
@@ -2040,11 +2975,54 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
|
|
|
2040
2975
|
});
|
|
2041
2976
|
}
|
|
2042
2977
|
|
|
2043
|
-
function opFieldReadMissMessage({read: read, where: where, hosts: hosts,
|
|
2044
|
-
const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint =
|
|
2978
|
+
function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityEntries: activityEntries}) {
|
|
2979
|
+
const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint = activityEntries?.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}`));
|
|
2045
2980
|
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)"}`;
|
|
2046
2981
|
}
|
|
2047
2982
|
|
|
2983
|
+
function checkUpdateWhereOps(def, issues) {
|
|
2984
|
+
const workflow = def.fields ?? [];
|
|
2985
|
+
for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
|
|
2986
|
+
if (op.type !== "field.updateWhere") continue;
|
|
2987
|
+
const scopes = {
|
|
2988
|
+
workflow: workflow,
|
|
2989
|
+
stage: site.stage.fields ?? [],
|
|
2990
|
+
activity: site.activity.fields ?? []
|
|
2991
|
+
};
|
|
2992
|
+
checkUpdateWhereTargetKind({
|
|
2993
|
+
op: op,
|
|
2994
|
+
path: [ ...site.path, o ],
|
|
2995
|
+
label: site.label,
|
|
2996
|
+
scopes: scopes,
|
|
2997
|
+
issues: issues
|
|
2998
|
+
}), checkUpdateWhereMergeKeys({
|
|
2999
|
+
op: op,
|
|
3000
|
+
path: [ ...site.path, o ],
|
|
3001
|
+
label: site.label,
|
|
3002
|
+
issues: issues
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
function checkUpdateWhereTargetKind({op: op, path: path, label: label, scopes: scopes, issues: issues}) {
|
|
3008
|
+
const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
|
|
3009
|
+
target === void 0 || target.type === "array" || issues.push({
|
|
3010
|
+
path: [ ...path, "target" ],
|
|
3011
|
+
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)}`
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
function rowOpsHint(type) {
|
|
3016
|
+
return type === "doc.refs" || type === "assignees" ? `; to add or drop ${type} rows use field.append / field.removeWhere` : "";
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function checkUpdateWhereMergeKeys({op: op, path: path, label: label, issues: issues}) {
|
|
3020
|
+
if (op.value.type === "object") for (const key of Object.keys(op.value.fields)) key !== "_key" && key !== "_type" || issues.push({
|
|
3021
|
+
path: [ ...path, "value", "fields", key ],
|
|
3022
|
+
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`
|
|
3023
|
+
});
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2048
3026
|
function checkGuardFieldReads(def, issues) {
|
|
2049
3027
|
const reads = {
|
|
2050
3028
|
workflowEntries: def.fields ?? [],
|
|
@@ -2106,11 +3084,14 @@ function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, pa
|
|
|
2106
3084
|
});
|
|
2107
3085
|
}
|
|
2108
3086
|
|
|
2109
|
-
function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
|
|
3087
|
+
function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues, nodes: nodes}) {
|
|
2110
3088
|
if (read.path === void 0) return;
|
|
2111
3089
|
const pathIssue = fieldValuePathIssue({
|
|
2112
3090
|
target: target,
|
|
2113
|
-
path: read.path
|
|
3091
|
+
path: read.path,
|
|
3092
|
+
...nodes ? {
|
|
3093
|
+
nodes: nodes
|
|
3094
|
+
} : {}
|
|
2114
3095
|
});
|
|
2115
3096
|
pathIssue !== void 0 && issues.push({
|
|
2116
3097
|
path: path,
|
|
@@ -2181,22 +3162,26 @@ const SCALAR = {
|
|
|
2181
3162
|
kind: "rows",
|
|
2182
3163
|
of: shape.of ?? []
|
|
2183
3164
|
})
|
|
3165
|
+
}, START_ALLOWED_VALUE_NODES = {
|
|
3166
|
+
...VALUE_NODES,
|
|
3167
|
+
"doc.ref": () => GDR_VALUE
|
|
2184
3168
|
};
|
|
2185
3169
|
|
|
2186
|
-
function valueNodeFor(shape) {
|
|
2187
|
-
return Object.hasOwn(
|
|
3170
|
+
function valueNodeFor(shape, nodes) {
|
|
3171
|
+
return Object.hasOwn(nodes, shape.type) ? nodes[shape.type](shape) : void 0;
|
|
2188
3172
|
}
|
|
2189
3173
|
|
|
2190
3174
|
const INDEX_SEGMENT = /^\d+$/;
|
|
2191
3175
|
|
|
2192
|
-
function stepIntoValueNode(
|
|
3176
|
+
function stepIntoValueNode(args) {
|
|
3177
|
+
const {node: node, segment: segment, nodes: nodes} = args;
|
|
2193
3178
|
switch (node.kind) {
|
|
2194
3179
|
case "scalar":
|
|
2195
3180
|
return "the value is a scalar with no sub-paths";
|
|
2196
3181
|
|
|
2197
3182
|
case "object":
|
|
2198
3183
|
{
|
|
2199
|
-
const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
|
|
3184
|
+
const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub, nodes);
|
|
2200
3185
|
return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
|
|
2201
3186
|
}
|
|
2202
3187
|
|
|
@@ -2217,18 +3202,24 @@ function stepIntoValueNode(node, segment) {
|
|
|
2217
3202
|
}
|
|
2218
3203
|
}
|
|
2219
3204
|
|
|
2220
|
-
function fieldValuePathIssue({target: target, path: path}) {
|
|
2221
|
-
let node = valueNodeFor(target);
|
|
3205
|
+
function fieldValuePathIssue({target: target, path: path, nodes: nodes = VALUE_NODES}) {
|
|
3206
|
+
let node = valueNodeFor(target, nodes);
|
|
2222
3207
|
if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
|
|
2223
3208
|
for (const segment of path.split(".")) {
|
|
2224
|
-
const next = stepIntoValueNode(
|
|
3209
|
+
const next = stepIntoValueNode({
|
|
3210
|
+
node: node,
|
|
3211
|
+
segment: segment,
|
|
3212
|
+
nodes: nodes
|
|
3213
|
+
});
|
|
2225
3214
|
if (typeof next == "string") return `at segment "${segment}": ${next}`;
|
|
2226
3215
|
node = next;
|
|
2227
3216
|
}
|
|
2228
3217
|
}
|
|
2229
3218
|
|
|
2230
3219
|
function checkWorkflowInvariants(def) {
|
|
2231
|
-
const issues = []
|
|
3220
|
+
const issues = [];
|
|
3221
|
+
checkDefinitionName(def, issues);
|
|
3222
|
+
const stageNames = checkStages(def, issues);
|
|
2232
3223
|
return checkInitialStage({
|
|
2233
3224
|
def: def,
|
|
2234
3225
|
stageNames: stageNames,
|
|
@@ -2242,10 +3233,12 @@ function checkWorkflowInvariants(def) {
|
|
|
2242
3233
|
stageNames: stageNames,
|
|
2243
3234
|
issues: issues
|
|
2244
3235
|
}), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
|
|
2245
|
-
checkRequiredField(def, issues),
|
|
2246
|
-
|
|
2247
|
-
|
|
3236
|
+
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
3237
|
+
checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
3238
|
+
checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
|
|
3239
|
+
checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
|
|
3240
|
+
checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
|
|
2248
3241
|
checkGroups(def, issues), issues;
|
|
2249
3242
|
}
|
|
2250
3243
|
|
|
2251
|
-
export { ACTIVITY_KINDS, ACTOR_KINDS, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EffectNotFoundError, EffectSchema, FIELD_READ, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GroupSchema, InstanceNotFoundError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, StoredFieldOpSchema, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkFieldValue, checkWorkflowInvariants, clientConfigFromResource, conditionEffectReads, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates,
|
|
3244
|
+
export { ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ActorShape, 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, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, MUTATION_GUARD_ACTIONS, NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, START_ALLOWED_VARS, 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, fieldValueSchemas, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|