@sanity/workflow-engine 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +150 -0
- package/DATAMODEL.md +107 -0
- package/dist/_chunks-cjs/invariants.cjs +179 -88
- package/dist/_chunks-es/invariants.js +174 -89
- package/dist/define.d.cts +187 -9
- package/dist/define.d.ts +187 -9
- package/dist/index.cjs +883 -468
- package/dist/index.d.cts +414 -19
- package/dist/index.d.ts +414 -19
- package/dist/index.js +880 -478
- package/package.json +1 -1
|
@@ -525,6 +525,7 @@ function desugarWorkflow(authoring) {
|
|
|
525
525
|
return {
|
|
526
526
|
...stripUndefined({
|
|
527
527
|
name: stage.name,
|
|
528
|
+
semantics: stage.semantics,
|
|
528
529
|
title: stage.title,
|
|
529
530
|
description: stage.description,
|
|
530
531
|
groups: stage.groups,
|
|
@@ -548,6 +549,7 @@ function desugarWorkflow(authoring) {
|
|
|
548
549
|
definition: {
|
|
549
550
|
...stripUndefined({
|
|
550
551
|
name: authoring.name,
|
|
552
|
+
semantics: authoring.semantics,
|
|
551
553
|
title: authoring.title,
|
|
552
554
|
description: authoring.description,
|
|
553
555
|
groups: authoring.groups,
|
|
@@ -774,6 +776,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
|
|
|
774
776
|
return {
|
|
775
777
|
...stripUndefined({
|
|
776
778
|
name: activity.name,
|
|
779
|
+
semantics: activity.semantics,
|
|
777
780
|
title: activity.title,
|
|
778
781
|
description: activity.description,
|
|
779
782
|
groups: activity.groups,
|
|
@@ -1245,7 +1248,58 @@ function isTerminalActivityStatus(status) {
|
|
|
1245
1248
|
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
1246
1249
|
}
|
|
1247
1250
|
|
|
1248
|
-
const
|
|
1251
|
+
const SIGNAL_SEMANTICS = [ "signal.positive", "signal.caution", "signal.critical" ], DECISION_SEMANTICS = [ "decision.accept", "decision.decline" ], ACTION_SEMANTICS = [ ...DECISION_SEMANTICS, ...SIGNAL_SEMANTICS ], FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISSIONS = [ "create", "read", "update" ], MUTATION_GUARD_ACTIONS = [ "create", "update", "delete", "publish", "unpublish" ], ACTIVITY_KINDS = [ "user", "service", "script", "manual", "receive" ], EXECUTOR_CLASSIFICATIONS = [ "autonomous", "interactive", "off-system", "hybrid" ], GROUP_KINDS = [ "core", "details" ], DRIVER_KINDS = [ "person", "agent", "service", "engine" ];
|
|
1252
|
+
|
|
1253
|
+
function releaseDocId(releaseName) {
|
|
1254
|
+
return `_.releases.${releaseName}`;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1258
|
+
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1259
|
+
return {
|
|
1260
|
+
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1261
|
+
type: "system.release",
|
|
1262
|
+
releaseName: releaseName
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function isAlwaysArrayFieldKind(kind) {
|
|
1267
|
+
return kind === "doc.refs" || kind === "assignees" || kind === "array";
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function isSingleDocRefKind(kind) {
|
|
1271
|
+
return kind === "doc.ref" || kind === "subject";
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function refKindAcceptsTypes(kind) {
|
|
1275
|
+
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
function isSingleDocRefEntry(entry) {
|
|
1279
|
+
return isSingleDocRefKind(entry._type);
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
function isTodoListItem(row) {
|
|
1283
|
+
if (typeof row != "object" || row === null) return !1;
|
|
1284
|
+
const candidate = row, status = candidate.status;
|
|
1285
|
+
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function declaredRowColumns(entry) {
|
|
1289
|
+
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function isTodoListEntry(entry) {
|
|
1293
|
+
const columns = declaredRowColumns(entry);
|
|
1294
|
+
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function isNotesEntry(entry) {
|
|
1298
|
+
const columns = declaredRowColumns(entry);
|
|
1299
|
+
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const CONDITION_VARS = [ {
|
|
1249
1303
|
name: "self",
|
|
1250
1304
|
binding: "always",
|
|
1251
1305
|
label: "this workflow instance",
|
|
@@ -1320,6 +1374,11 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
|
|
|
1320
1374
|
binding: "caller",
|
|
1321
1375
|
label: "your permissions",
|
|
1322
1376
|
description: "Advisory per-permission booleans computed from the caller's grants; `undefined` without grants. Bound wherever grants ride the evaluation: the projection's rendered scope (fireAction-action filters, activity requirements, editability predicates) and the fireAction/editField commit gates. Deploy rejects it at every site that evaluates without grants: transition `when`s, activity filters, cascade-fired actions' `when`/`filter`, effect bindings, where-op `where`s, and the spawn `forEach`/`with`/`context` sites."
|
|
1377
|
+
}, {
|
|
1378
|
+
name: "attributes",
|
|
1379
|
+
binding: "caller",
|
|
1380
|
+
label: "your attributes",
|
|
1381
|
+
description: "Advisory org-level User Attributes for the caller (Enterprise — same values as lake `user::attributes()`), keyed by attribute name with each active scalar or array value. `undefined` on expected HTTP absence; unexpected fetch failures throw; empty page binds `{}`. Bound wherever grants ride the evaluation (same sites as `$can`). Soft-gate paths fetch at most 100 attributes (no further pages) and warn when the envelope reports `hasMore: true` (partial bag still binds). Not a security boundary — the Content Lake remains the only enforcement point."
|
|
1323
1382
|
}, {
|
|
1324
1383
|
name: "row",
|
|
1325
1384
|
binding: "spawn",
|
|
@@ -1485,54 +1544,7 @@ function documentIdOf(doc) {
|
|
|
1485
1544
|
return "(unknown id)";
|
|
1486
1545
|
}
|
|
1487
1546
|
|
|
1488
|
-
const ACTOR_KINDS = [ "person", "agent", "system" ]
|
|
1489
|
-
|
|
1490
|
-
function releaseDocId(releaseName) {
|
|
1491
|
-
return `_.releases.${releaseName}`;
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1495
|
-
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1496
|
-
return {
|
|
1497
|
-
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1498
|
-
type: "system.release",
|
|
1499
|
-
releaseName: releaseName
|
|
1500
|
-
};
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
function isSingleDocRefKind(kind) {
|
|
1504
|
-
return kind === "doc.ref" || kind === "subject";
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function refKindAcceptsTypes(kind) {
|
|
1508
|
-
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
function isSingleDocRefEntry(entry) {
|
|
1512
|
-
return isSingleDocRefKind(entry._type);
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
function isTodoListItem(row) {
|
|
1516
|
-
if (typeof row != "object" || row === null) return !1;
|
|
1517
|
-
const candidate = row, status = candidate.status;
|
|
1518
|
-
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
function declaredRowColumns(entry) {
|
|
1522
|
-
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
function isTodoListEntry(entry) {
|
|
1526
|
-
const columns = declaredRowColumns(entry);
|
|
1527
|
-
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function isNotesEntry(entry) {
|
|
1531
|
-
const columns = declaredRowColumns(entry);
|
|
1532
|
-
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1547
|
+
const ACTOR_KINDS = [ "person", "agent", "system" ], ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1536
1548
|
|
|
1537
1549
|
function classifyPrincipalId(id) {
|
|
1538
1550
|
if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
|
|
@@ -1961,6 +1973,10 @@ function opSchemas(targetSchema) {
|
|
|
1961
1973
|
type: v.literal("field.set"),
|
|
1962
1974
|
target: targetSchema,
|
|
1963
1975
|
value: ValueExprSchema
|
|
1976
|
+
}), v.strictObject({
|
|
1977
|
+
type: v.literal("field.setIfMissing"),
|
|
1978
|
+
target: targetSchema,
|
|
1979
|
+
value: ValueExprSchema
|
|
1964
1980
|
}), v.strictObject({
|
|
1965
1981
|
type: v.literal("field.unset"),
|
|
1966
1982
|
target: targetSchema
|
|
@@ -1968,6 +1984,14 @@ function opSchemas(targetSchema) {
|
|
|
1968
1984
|
type: v.literal("field.append"),
|
|
1969
1985
|
target: targetSchema,
|
|
1970
1986
|
value: ValueExprSchema
|
|
1987
|
+
}), v.strictObject({
|
|
1988
|
+
type: v.literal("field.inc"),
|
|
1989
|
+
target: targetSchema,
|
|
1990
|
+
value: v.optional(ValueExprSchema)
|
|
1991
|
+
}), v.strictObject({
|
|
1992
|
+
type: v.literal("field.dec"),
|
|
1993
|
+
target: targetSchema,
|
|
1994
|
+
value: v.optional(ValueExprSchema)
|
|
1971
1995
|
}), v.strictObject({
|
|
1972
1996
|
type: v.literal("field.updateWhere"),
|
|
1973
1997
|
target: targetSchema,
|
|
@@ -2184,17 +2208,27 @@ const TodoListFieldSchema = pinned()(v.strictObject(listSugarFields("todoList"))
|
|
|
2184
2208
|
required: v.optional(v.boolean()),
|
|
2185
2209
|
options: v.optional(ChoiceOptionsSchema),
|
|
2186
2210
|
validation: v.optional(ScalarValidationSchema)
|
|
2187
|
-
}), choiceOptionsCheck(), scalarValidationCheck());
|
|
2211
|
+
}), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
|
|
2212
|
+
|
|
2213
|
+
function semanticNamespace(semantic) {
|
|
2214
|
+
return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
|
|
2215
|
+
}
|
|
2188
2216
|
|
|
2189
2217
|
function hasUniqueSemanticNamespaces(semantics) {
|
|
2190
|
-
const namespaces = semantics.map(
|
|
2218
|
+
const namespaces = semantics.map(semanticNamespace);
|
|
2191
2219
|
return new Set(namespaces).size === namespaces.length;
|
|
2192
2220
|
}
|
|
2193
2221
|
|
|
2222
|
+
function semanticsFieldSchema(semantic) {
|
|
2223
|
+
return v.optional(v.pipe(v.array(semantic), v.minLength(1, "declare at least one semantic, or omit `semantics`"), v.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
|
|
2227
|
+
|
|
2194
2228
|
function actionFields(op, group) {
|
|
2195
2229
|
return {
|
|
2196
2230
|
name: NonEmpty,
|
|
2197
|
-
semantics:
|
|
2231
|
+
semantics: ActionSemanticsFieldSchema,
|
|
2198
2232
|
title: v.optional(v.string()),
|
|
2199
2233
|
description: v.optional(v.string()),
|
|
2200
2234
|
group: v.optional(group),
|
|
@@ -2241,6 +2275,7 @@ const StoredActionSchema = pinned()(v.strictObject({
|
|
|
2241
2275
|
function activityFields({field: field, action: action, target: target, group: group}) {
|
|
2242
2276
|
return {
|
|
2243
2277
|
name: NonEmpty,
|
|
2278
|
+
semantics: SemanticsFieldSchema,
|
|
2244
2279
|
title: v.optional(v.string()),
|
|
2245
2280
|
description: v.optional(v.string()),
|
|
2246
2281
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2314,6 +2349,7 @@ const GuardSchema = v.strictObject(guardFields(NonEmpty)), AuthoringGuardSchema
|
|
|
2314
2349
|
function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
|
|
2315
2350
|
return {
|
|
2316
2351
|
name: NonEmpty,
|
|
2352
|
+
semantics: SemanticsFieldSchema,
|
|
2317
2353
|
title: v.optional(v.string()),
|
|
2318
2354
|
description: v.optional(v.string()),
|
|
2319
2355
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2352,6 +2388,7 @@ const StoredStartSchema = pinned()(v.strictObject(startFields(picklist(START_KIN
|
|
|
2352
2388
|
function workflowFields({field: field, stage: stage, start: start}) {
|
|
2353
2389
|
return {
|
|
2354
2390
|
name: NonEmpty,
|
|
2391
|
+
semantics: SemanticsFieldSchema,
|
|
2355
2392
|
title: NonEmpty,
|
|
2356
2393
|
description: v.optional(v.string()),
|
|
2357
2394
|
groups: v.optional(v.array(GroupSchema)),
|
|
@@ -2548,11 +2585,20 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
|
|
|
2548
2585
|
});
|
|
2549
2586
|
}
|
|
2550
2587
|
|
|
2588
|
+
function actionSites(def) {
|
|
2589
|
+
return def.stages.flatMap((stage, i) => (stage.activities ?? []).flatMap((activity, j) => (activity.actions ?? []).map((action, a) => ({
|
|
2590
|
+
action: action,
|
|
2591
|
+
activity: activity,
|
|
2592
|
+
stage: stage,
|
|
2593
|
+
path: [ "stages", i, "activities", j, "actions", a ]
|
|
2594
|
+
}))));
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2551
2597
|
function effectNameSites(def) {
|
|
2552
2598
|
const sites = [];
|
|
2553
|
-
for (const
|
|
2599
|
+
for (const {action: action, path: path} of actionSites(def)) collectEffects({
|
|
2554
2600
|
effects: action.effects,
|
|
2555
|
-
path: [
|
|
2601
|
+
path: [ ...path, "effects" ],
|
|
2556
2602
|
sites: sites
|
|
2557
2603
|
});
|
|
2558
2604
|
return sites;
|
|
@@ -2683,13 +2729,22 @@ function checkUnboundConditionVars(def, issues) {
|
|
|
2683
2729
|
}
|
|
2684
2730
|
}
|
|
2685
2731
|
|
|
2732
|
+
const SOFT_GATE_VARS = {
|
|
2733
|
+
can: {
|
|
2734
|
+
noun: "the caller's grants"
|
|
2735
|
+
},
|
|
2736
|
+
attributes: {
|
|
2737
|
+
noun: "the caller's org-level user attributes"
|
|
2738
|
+
}
|
|
2739
|
+
}, SOFT_GATE_VAR_NAMES = Object.keys(SOFT_GATE_VARS);
|
|
2740
|
+
|
|
2686
2741
|
function unboundVarsAt(site) {
|
|
2687
2742
|
const callerVars = unboundCallerVars(site.policy);
|
|
2688
2743
|
return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
|
|
2689
2744
|
}
|
|
2690
2745
|
|
|
2691
2746
|
function unboundCallerVars(policy) {
|
|
2692
|
-
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [
|
|
2747
|
+
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ ...SOFT_GATE_VAR_NAMES, "params" ] : SOFT_GATE_VAR_NAMES;
|
|
2693
2748
|
}
|
|
2694
2749
|
|
|
2695
2750
|
const ROW_BINDING_CLAUSE = "$row (the discovered row in a spawn projection; the stored row under test in a where-op) is bound only while a spawn `with` projection or a where-op `where` evaluates";
|
|
@@ -2699,7 +2754,12 @@ function rowVarMessage(site) {
|
|
|
2699
2754
|
}
|
|
2700
2755
|
|
|
2701
2756
|
function callerVarMessage(site, name) {
|
|
2702
|
-
|
|
2757
|
+
if (site.policy === "cascade") return `${site.label} reads $${name} — cascade gates (transition \`when\`s, activity \`filter\`s, a cascade-fired action's \`when\`/\`filter\`) must resolve identically no matter whose token drives the cascade ($assigned is constant false, the other caller vars hold no value); gate on instance state (e.g. a field an action wrote), or pin executing identities with \`roles\``;
|
|
2758
|
+
if (site.policy === "caller-bound") return `${site.label} reads $${name} — $params (the firing action's args) is bound only while the action's effect bindings and where-op \`where\`s evaluate; this site never binds it, so the condition could never pass. Bind $params in an effect binding or a where-op instead, or gate on a field an action wrote`;
|
|
2759
|
+
if (site.policy === "triggered-payload" && name === "params") return `${site.label} reads $params — a cascade-fired action has no caller to supply args, so $params never holds a value in its payload; read fields or effect outputs instead`;
|
|
2760
|
+
const softGate = SOFT_GATE_VARS[name];
|
|
2761
|
+
if (softGate !== void 0) return `${site.label} reads $${name} — $${name} (${softGate.noun}) is bound only in the caller-bound projection (action filters, requirements, editable predicates); this site never binds it. Move the check to one of those sites or drop $${name}`;
|
|
2762
|
+
throw new Error(`callerVarMessage: unreachable for $${name} at ${site.label} (policy ${site.policy})`);
|
|
2703
2763
|
}
|
|
2704
2764
|
|
|
2705
2765
|
function conditionSites(def) {
|
|
@@ -2954,9 +3014,9 @@ function belowScopeNestedSites(args) {
|
|
|
2954
3014
|
}
|
|
2955
3015
|
|
|
2956
3016
|
function checkLevelKindEffectOutputs(def, issues) {
|
|
2957
|
-
for (const
|
|
3017
|
+
for (const {action: action, path: path} of actionSites(def)) pushOutputIssues({
|
|
2958
3018
|
action: action,
|
|
2959
|
-
path:
|
|
3019
|
+
path: path,
|
|
2960
3020
|
issues: issues
|
|
2961
3021
|
});
|
|
2962
3022
|
}
|
|
@@ -3038,18 +3098,18 @@ function storedRolesIssue(action) {
|
|
|
3038
3098
|
}
|
|
3039
3099
|
|
|
3040
3100
|
function checkStoredRolesPlacement(def, issues) {
|
|
3041
|
-
for (const
|
|
3101
|
+
for (const {action: action, path: path} of actionSites(def)) {
|
|
3042
3102
|
const message = storedRolesIssue(action);
|
|
3043
3103
|
message !== void 0 && issues.push({
|
|
3044
|
-
path: [
|
|
3104
|
+
path: [ ...path, "roles" ],
|
|
3045
3105
|
message: message
|
|
3046
3106
|
});
|
|
3047
3107
|
}
|
|
3048
3108
|
}
|
|
3049
3109
|
|
|
3050
3110
|
function checkTriggeredActionParams(def, issues) {
|
|
3051
|
-
for (const
|
|
3052
|
-
path: [
|
|
3111
|
+
for (const {action: action, path: path} of actionSites(def)) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
|
|
3112
|
+
path: [ ...path, "params" ],
|
|
3053
3113
|
message: `action "${action.name}" declares params but is cascade-fired (\`when\`) — no caller ever supplies args to a trigger. Drop the params, or drop \`when\` to make it a fireAction-fired action`
|
|
3054
3114
|
});
|
|
3055
3115
|
}
|
|
@@ -3094,13 +3154,20 @@ function checkSingleSubjectRequirements(def, issues) {
|
|
|
3094
3154
|
|
|
3095
3155
|
function checkStartFilterReads(def, issues) {
|
|
3096
3156
|
const filter = def.start?.filter;
|
|
3097
|
-
filter
|
|
3157
|
+
if (filter === void 0) return;
|
|
3158
|
+
const params = conditionParameterNames(filter);
|
|
3159
|
+
params.has("fields") && issues.push({
|
|
3098
3160
|
path: [ "start", "filter" ],
|
|
3099
3161
|
message: "start.filter reads $fields, but the filter is browse-time-pure — a start surface evaluates it per document, before any inputs exist, so $fields cannot be bound. Move the input-dependent rule to a start requirement, the start-time readiness predicate that binds $fields and is enforced by startInstance"
|
|
3100
|
-
})
|
|
3162
|
+
});
|
|
3163
|
+
for (const name of CALLER_BOUND_VARS) params.has(name) && issues.push({
|
|
3164
|
+
path: [ "start", "filter" ],
|
|
3165
|
+
message: `start.filter reads $${name} — start.filter is a read-side document-visibility rule (definitionsForDocument / start controls); startInstance never evaluates it, and no caller bag binds there, so the read is GROQ null and the filter can never match. Drop $${name} from start.filter. A "who may start" rule belongs in a start requirement, not in the browse-time filter`
|
|
3166
|
+
});
|
|
3167
|
+
readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
3101
3168
|
path: [ "start", "filter" ],
|
|
3102
3169
|
message: "start.filter reads the candidate document (its root), but the definition declares no `subject` entry — a read surface would never have a document to bind as root, so every root read is GROQ null and the filter silently misevaluates. Declare a `subject` entry (the document the workflow is about), or gate on the dataset instead"
|
|
3103
|
-
})
|
|
3170
|
+
});
|
|
3104
3171
|
}
|
|
3105
3172
|
|
|
3106
3173
|
function checkStartRequirementReads(def, issues) {
|
|
@@ -3396,15 +3463,13 @@ function seedEarlierSiblingTarget(args) {
|
|
|
3396
3463
|
}
|
|
3397
3464
|
|
|
3398
3465
|
function opSites(def) {
|
|
3399
|
-
|
|
3400
|
-
for (const [i, stage] of def.stages.entries()) for (const [j, activity] of (stage.activities ?? []).entries()) for (const [a, action] of (activity.actions ?? []).entries()) sites.push({
|
|
3466
|
+
return actionSites(def).map(({action: action, activity: activity, stage: stage, path: path}) => ({
|
|
3401
3467
|
ops: action.ops,
|
|
3402
|
-
path: [
|
|
3468
|
+
path: [ ...path, "ops" ],
|
|
3403
3469
|
label: `action "${action.name}"`,
|
|
3404
3470
|
stage: stage,
|
|
3405
3471
|
activity: activity
|
|
3406
|
-
});
|
|
3407
|
-
return sites;
|
|
3472
|
+
}));
|
|
3408
3473
|
}
|
|
3409
3474
|
|
|
3410
3475
|
function checkFieldReadOpValues(def, issues) {
|
|
@@ -3421,7 +3486,7 @@ function checkFieldReadOpValues(def, issues) {
|
|
|
3421
3486
|
}
|
|
3422
3487
|
|
|
3423
3488
|
function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
|
|
3424
|
-
for (const [o, op] of (ops ?? []).entries()) if ("value" in op) for (const {read: read, path: readPath} of fieldReadsIn(op.value, [ ...path, o, "value" ])) checkOpFieldRead({
|
|
3489
|
+
for (const [o, op] of (ops ?? []).entries()) if (!(!("value" in op) || op.value === void 0)) for (const {read: read, path: readPath} of fieldReadsIn(op.value, [ ...path, o, "value" ])) checkOpFieldRead({
|
|
3425
3490
|
...ctx,
|
|
3426
3491
|
read: read,
|
|
3427
3492
|
path: readPath,
|
|
@@ -3472,35 +3537,55 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
|
|
|
3472
3537
|
return `${where} reads field "${read.field}", which is not declared at ${searched} — the read resolves to undefined at op time, so the write silently lands empty. Known: ${known.join(", ") || "(none)"}`;
|
|
3473
3538
|
}
|
|
3474
3539
|
|
|
3475
|
-
function
|
|
3540
|
+
function checkFieldTargetOps(def, issues) {
|
|
3476
3541
|
const workflow = def.fields ?? [];
|
|
3477
3542
|
for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
workflow: workflow,
|
|
3481
|
-
stage: site.stage.fields ?? [],
|
|
3482
|
-
activity: site.activity.fields ?? []
|
|
3483
|
-
};
|
|
3484
|
-
checkUpdateWhereTargetKind({
|
|
3543
|
+
const path = [ ...site.path, o ], rule = TARGET_KIND_RULES[op.type];
|
|
3544
|
+
rule !== void 0 && "target" in op && checkTargetKind({
|
|
3485
3545
|
op: op,
|
|
3486
|
-
path: [ ...
|
|
3546
|
+
path: [ ...path, "target" ],
|
|
3487
3547
|
label: site.label,
|
|
3488
|
-
scopes:
|
|
3548
|
+
scopes: opTargetScopes(workflow, site),
|
|
3549
|
+
rule: rule,
|
|
3489
3550
|
issues: issues
|
|
3490
|
-
}), checkUpdateWhereMergeKeys({
|
|
3551
|
+
}), op.type === "field.updateWhere" && checkUpdateWhereMergeKeys({
|
|
3491
3552
|
op: op,
|
|
3492
|
-
path:
|
|
3553
|
+
path: path,
|
|
3493
3554
|
label: site.label,
|
|
3494
3555
|
issues: issues
|
|
3495
3556
|
});
|
|
3496
3557
|
}
|
|
3497
3558
|
}
|
|
3498
3559
|
|
|
3499
|
-
|
|
3560
|
+
const arithmeticTargetRule = {
|
|
3561
|
+
accepts: target => target.type === "number",
|
|
3562
|
+
issue: () => "arithmetic ops target `number` entries only"
|
|
3563
|
+
}, TARGET_KIND_RULES = {
|
|
3564
|
+
"field.inc": arithmeticTargetRule,
|
|
3565
|
+
"field.dec": arithmeticTargetRule,
|
|
3566
|
+
"field.setIfMissing": {
|
|
3567
|
+
accepts: target => !isAlwaysArrayFieldKind(target.type),
|
|
3568
|
+
issue: () => "setIfMissing applies to nullable entries only; an empty array entry already holds []"
|
|
3569
|
+
},
|
|
3570
|
+
"field.updateWhere": {
|
|
3571
|
+
accepts: target => target.type === "array",
|
|
3572
|
+
issue: target => "updateWhere merges declared row sub-fields, so its target must be an `array` entry" + rowOpsHint(target.type)
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
|
|
3576
|
+
function opTargetScopes(workflow, site) {
|
|
3577
|
+
return {
|
|
3578
|
+
workflow: workflow,
|
|
3579
|
+
stage: site.stage.fields ?? [],
|
|
3580
|
+
activity: site.activity.fields ?? []
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
function checkTargetKind({op: op, path: path, label: label, scopes: scopes, rule: rule, issues: issues}) {
|
|
3500
3585
|
const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
|
|
3501
|
-
target === void 0 || target
|
|
3502
|
-
path:
|
|
3503
|
-
message: `${label}
|
|
3586
|
+
target === void 0 || rule.accepts(target) || issues.push({
|
|
3587
|
+
path: path,
|
|
3588
|
+
message: `${label} ${op.type} targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — ${rule.issue(target)}`
|
|
3504
3589
|
});
|
|
3505
3590
|
}
|
|
3506
3591
|
|
|
@@ -3732,11 +3817,11 @@ function checkWorkflowInvariants(def) {
|
|
|
3732
3817
|
}), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
|
|
3733
3818
|
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
3734
3819
|
checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
3735
|
-
checkFieldReadOpValues(def, issues),
|
|
3820
|
+
checkFieldReadOpValues(def, issues), checkFieldTargetOps(def, issues), checkGuardFieldReads(def, issues),
|
|
3736
3821
|
checkAssigneesEntries(def, issues), checkDueDateEntries(def, issues), checkSubjectEntries(def, issues),
|
|
3737
3822
|
checkLevelKindEffectOutputs(def, issues), checkActivityTerminalPaths(def, issues),
|
|
3738
3823
|
checkTerminalStageActivities(def, issues), checkTriggeredActionParams(def, issues),
|
|
3739
3824
|
checkStoredRolesPlacement(def, issues), checkGroups(def, issues), issues;
|
|
3740
3825
|
}
|
|
3741
3826
|
|
|
3742
|
-
export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, 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_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|
|
3827
|
+
export { ACTION_SEMANTICS, ACTIVITY_KINDS, ACTIVITY_STATUSES, ACTOR_KINDS, ANONYMOUS_IDENTITY, ActorShape, AuthoringActionSchema, AuthoringActivitySchema, AuthoringFieldEntrySchema, AuthoringGuardSchema, AuthoringOpSchema, AuthoringStageSchema, AuthoringTransitionSchema, AuthoringWorkflowSchema, CALLER_BOUND_VARS, CONDITION_VARS, ContractViolationError, DECISION_SEMANTICS, DEFAULT_TRANSITION_WHEN, DOCUMENT_VALUE_PERMISSIONS, DRIVER_KINDS, DefinitionInUseError, DefinitionNotFoundError, EFFECTS_READ, EXECUTOR_CLASSIFICATIONS, EffectNotFoundError, EffectSchema, FIELD_READ, FIELD_SCOPES, FIELD_VALUE_KINDS, FILTER_SCOPE_VARS, FieldValueShapeError, GROUP_KINDS, GUARD_PREDICATE_VARS, GdrShape, GroupSchema, InstanceNotFoundError, IsoTimestamp, MUTATION_GUARD_ACTIONS, NonEmptyString, PersistedDocShapeError, RESERVED_CONDITION_VARS, RESOURCE_ALIAS_NAME_SOURCE, SIGNAL_SEMANTICS, START_FILTER_VARS, START_REQUIREMENT_VARS, SYSTEM_IDENTITY, SpawnContractsInvalidError, StoredFieldOpSchema, VersionSpecificDatasetGdrError, WORKFLOW_DEFINITION_TYPE, WorkflowConfigSchema, WorkflowError, actorFulfillsRole, andConditions, checkWorkflowInvariants, choiceValueIssues, classifyPrincipalId, clientConfigFromResource, conditionEffectReads, conditionFieldReadNames, conditionParameterNames, conditionSyntaxIssues, datasetResourceParts, definitionDocId, deriveActivityKind, deriveExecutorClassification, desugarWorkflow, directoryBridgeId, driverKind, errorMessage, evaluateCondition, evaluateConditionOutcome, evaluatePredicates, extractDocumentId, fieldValueSchemas, firstCarriedGlobalId, formatIssuePath, formatIssues, formatValidationError, gdrFromResource, gdrRef, gdrResourcePrefix, gdrUri, groq, groupMembershipNames, isAlwaysArrayFieldKind, isBareSeedId, isCascadeFired, isGdr, isGdrUri, isGuardReadExpr, isInputSourced, isNotesEntry, isParseableInstant, isSingleDocRefEntry, isSingleDocRefKind, isStartableDefinition, isSubjectEntry, isTerminalActivityStatus, isTodoListEntry, isTodoListItem, isUnevaluable, labelFor, lakePrincipalId, parseFieldValue, parseGdr, parseOrThrow, parsePersistedDoc, parseResourceGdr, parseStoredDefinition, readsRootDocument, refCanvas, refDashboard, refDataset, refKindAcceptsTypes, refMediaLibrary, refTypeIssues, rejectedRefTypes, releaseDocId, releaseRef, resourceAliasesToMap, resourceFromGdrUri, resourceFromParsed, resourceGdr, rethrowWithContext, runGroq, sameResource, scalarValidationIssues, schemaTreeShape, selfGdr, startKindOf, tagScopeFilter, toBareId, toPhysicalGdr, tolerantEntries, tolerantObject, tryParseGdr, validateFieldAppendItem, validateFieldValue, validateResourceAliasName, validateTag };
|