@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
|
@@ -539,6 +539,7 @@ function desugarWorkflow(authoring) {
|
|
|
539
539
|
return {
|
|
540
540
|
...stripUndefined({
|
|
541
541
|
name: stage.name,
|
|
542
|
+
semantics: stage.semantics,
|
|
542
543
|
title: stage.title,
|
|
543
544
|
description: stage.description,
|
|
544
545
|
groups: stage.groups,
|
|
@@ -562,6 +563,7 @@ function desugarWorkflow(authoring) {
|
|
|
562
563
|
definition: {
|
|
563
564
|
...stripUndefined({
|
|
564
565
|
name: authoring.name,
|
|
566
|
+
semantics: authoring.semantics,
|
|
565
567
|
title: authoring.title,
|
|
566
568
|
description: authoring.description,
|
|
567
569
|
groups: authoring.groups,
|
|
@@ -788,6 +790,7 @@ function desugarActivity({activity: activity, path: path, stageEnv: stageEnv, ct
|
|
|
788
790
|
return {
|
|
789
791
|
...stripUndefined({
|
|
790
792
|
name: activity.name,
|
|
793
|
+
semantics: activity.semantics,
|
|
791
794
|
title: activity.title,
|
|
792
795
|
description: activity.description,
|
|
793
796
|
groups: activity.groups,
|
|
@@ -1259,7 +1262,58 @@ function isTerminalActivityStatus(status) {
|
|
|
1259
1262
|
return TERMINAL_ACTIVITY_STATUSES.includes(status);
|
|
1260
1263
|
}
|
|
1261
1264
|
|
|
1262
|
-
const
|
|
1265
|
+
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" ];
|
|
1266
|
+
|
|
1267
|
+
function releaseDocId(releaseName) {
|
|
1268
|
+
return `_.releases.${releaseName}`;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1272
|
+
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1273
|
+
return {
|
|
1274
|
+
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1275
|
+
type: "system.release",
|
|
1276
|
+
releaseName: releaseName
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function isAlwaysArrayFieldKind(kind) {
|
|
1281
|
+
return kind === "doc.refs" || kind === "assignees" || kind === "array";
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function isSingleDocRefKind(kind) {
|
|
1285
|
+
return kind === "doc.ref" || kind === "subject";
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function refKindAcceptsTypes(kind) {
|
|
1289
|
+
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function isSingleDocRefEntry(entry) {
|
|
1293
|
+
return isSingleDocRefKind(entry._type);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function isTodoListItem(row) {
|
|
1297
|
+
if (typeof row != "object" || row === null) return !1;
|
|
1298
|
+
const candidate = row, status = candidate.status;
|
|
1299
|
+
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function declaredRowColumns(entry) {
|
|
1303
|
+
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function isTodoListEntry(entry) {
|
|
1307
|
+
const columns = declaredRowColumns(entry);
|
|
1308
|
+
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function isNotesEntry(entry) {
|
|
1312
|
+
const columns = declaredRowColumns(entry);
|
|
1313
|
+
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const CONDITION_VARS = [ {
|
|
1263
1317
|
name: "self",
|
|
1264
1318
|
binding: "always",
|
|
1265
1319
|
label: "this workflow instance",
|
|
@@ -1334,6 +1388,11 @@ const ACTION_SEMANTICS = [ "decision.accept", "decision.decline" ], FIELD_SCOPES
|
|
|
1334
1388
|
binding: "caller",
|
|
1335
1389
|
label: "your permissions",
|
|
1336
1390
|
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."
|
|
1391
|
+
}, {
|
|
1392
|
+
name: "attributes",
|
|
1393
|
+
binding: "caller",
|
|
1394
|
+
label: "your attributes",
|
|
1395
|
+
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."
|
|
1337
1396
|
}, {
|
|
1338
1397
|
name: "row",
|
|
1339
1398
|
binding: "spawn",
|
|
@@ -1499,54 +1558,7 @@ function documentIdOf(doc) {
|
|
|
1499
1558
|
return "(unknown id)";
|
|
1500
1559
|
}
|
|
1501
1560
|
|
|
1502
|
-
const ACTOR_KINDS = [ "person", "agent", "system" ]
|
|
1503
|
-
|
|
1504
|
-
function releaseDocId(releaseName) {
|
|
1505
|
-
return `_.releases.${releaseName}`;
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
function releaseRef({res: res, releaseName: releaseName}) {
|
|
1509
|
-
if (releaseName.length === 0) throw new ContractViolationError("releaseRef: releaseName must be a non-empty release name");
|
|
1510
|
-
return {
|
|
1511
|
-
id: gdrFromResource(res, releaseDocId(releaseName)),
|
|
1512
|
-
type: "system.release",
|
|
1513
|
-
releaseName: releaseName
|
|
1514
|
-
};
|
|
1515
|
-
}
|
|
1516
|
-
|
|
1517
|
-
function isSingleDocRefKind(kind) {
|
|
1518
|
-
return kind === "doc.ref" || kind === "subject";
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
function refKindAcceptsTypes(kind) {
|
|
1522
|
-
return isSingleDocRefKind(kind) || kind === "doc.refs";
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
function isSingleDocRefEntry(entry) {
|
|
1526
|
-
return isSingleDocRefKind(entry._type);
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
function isTodoListItem(row) {
|
|
1530
|
-
if (typeof row != "object" || row === null) return !1;
|
|
1531
|
-
const candidate = row, status = candidate.status;
|
|
1532
|
-
return typeof candidate._key == "string" && typeof candidate.label == "string" && (status == null || typeof status == "string");
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
function declaredRowColumns(entry) {
|
|
1536
|
-
if (("_type" in entry ? entry._type : entry.type) === "array") return new Set((("of" in entry ? entry.of : void 0) ?? []).map(shape => shape.name));
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
function isTodoListEntry(entry) {
|
|
1540
|
-
const columns = declaredRowColumns(entry);
|
|
1541
|
-
return columns !== void 0 && columns.has("label") && columns.has("status");
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
function isNotesEntry(entry) {
|
|
1545
|
-
const columns = declaredRowColumns(entry);
|
|
1546
|
-
return columns !== void 0 && columns.has("body") && columns.has("actor") && columns.has("at");
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
const ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1561
|
+
const ACTOR_KINDS = [ "person", "agent", "system" ], ANONYMOUS_IDENTITY = "<anonymous>", SYSTEM_IDENTITY = "<system>", E_PREFIXED_PROJECT_ID = /^e-(.+)$/;
|
|
1550
1562
|
|
|
1551
1563
|
function classifyPrincipalId(id) {
|
|
1552
1564
|
if (id === ANONYMOUS_IDENTITY || id === SYSTEM_IDENTITY) return {
|
|
@@ -1975,6 +1987,10 @@ function opSchemas(targetSchema) {
|
|
|
1975
1987
|
type: v__namespace.literal("field.set"),
|
|
1976
1988
|
target: targetSchema,
|
|
1977
1989
|
value: ValueExprSchema
|
|
1990
|
+
}), v__namespace.strictObject({
|
|
1991
|
+
type: v__namespace.literal("field.setIfMissing"),
|
|
1992
|
+
target: targetSchema,
|
|
1993
|
+
value: ValueExprSchema
|
|
1978
1994
|
}), v__namespace.strictObject({
|
|
1979
1995
|
type: v__namespace.literal("field.unset"),
|
|
1980
1996
|
target: targetSchema
|
|
@@ -1982,6 +1998,14 @@ function opSchemas(targetSchema) {
|
|
|
1982
1998
|
type: v__namespace.literal("field.append"),
|
|
1983
1999
|
target: targetSchema,
|
|
1984
2000
|
value: ValueExprSchema
|
|
2001
|
+
}), v__namespace.strictObject({
|
|
2002
|
+
type: v__namespace.literal("field.inc"),
|
|
2003
|
+
target: targetSchema,
|
|
2004
|
+
value: v__namespace.optional(ValueExprSchema)
|
|
2005
|
+
}), v__namespace.strictObject({
|
|
2006
|
+
type: v__namespace.literal("field.dec"),
|
|
2007
|
+
target: targetSchema,
|
|
2008
|
+
value: v__namespace.optional(ValueExprSchema)
|
|
1985
2009
|
}), v__namespace.strictObject({
|
|
1986
2010
|
type: v__namespace.literal("field.updateWhere"),
|
|
1987
2011
|
target: targetSchema,
|
|
@@ -2198,17 +2222,27 @@ const TodoListFieldSchema = pinned()(v__namespace.strictObject(listSugarFields("
|
|
|
2198
2222
|
required: v__namespace.optional(v__namespace.boolean()),
|
|
2199
2223
|
options: v__namespace.optional(ChoiceOptionsSchema),
|
|
2200
2224
|
validation: v__namespace.optional(ScalarValidationSchema)
|
|
2201
|
-
}), choiceOptionsCheck(), scalarValidationCheck());
|
|
2225
|
+
}), choiceOptionsCheck(), scalarValidationCheck()), CUSTOM_SEMANTIC_HINT = "`custom.<camelCaseMeaning>`", CustomSemanticSchema = v__namespace.custom(input => typeof input == "string" && /^custom\.[a-z][a-zA-Z0-9]*$/.test(input)), SemanticSchema = v__namespace.union([ picklist(SIGNAL_SEMANTICS), CustomSemanticSchema ], `expected ${SIGNAL_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`), ActionSemanticSchema = v__namespace.union([ picklist(ACTION_SEMANTICS), CustomSemanticSchema ], `expected ${ACTION_SEMANTICS.join(", ")}, or ${CUSTOM_SEMANTIC_HINT}`);
|
|
2226
|
+
|
|
2227
|
+
function semanticNamespace(semantic) {
|
|
2228
|
+
return semantic.startsWith("custom.") ? semantic : semantic.split(".", 1)[0] ?? semantic;
|
|
2229
|
+
}
|
|
2202
2230
|
|
|
2203
2231
|
function hasUniqueSemanticNamespaces(semantics) {
|
|
2204
|
-
const namespaces = semantics.map(
|
|
2232
|
+
const namespaces = semantics.map(semanticNamespace);
|
|
2205
2233
|
return new Set(namespaces).size === namespaces.length;
|
|
2206
2234
|
}
|
|
2207
2235
|
|
|
2236
|
+
function semanticsFieldSchema(semantic) {
|
|
2237
|
+
return v__namespace.optional(v__namespace.pipe(v__namespace.array(semantic), v__namespace.minLength(1, "declare at least one semantic, or omit `semantics`"), v__namespace.check(hasUniqueSemanticNamespaces, "declare at most one semantic from each namespace")));
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
const SemanticsFieldSchema = semanticsFieldSchema(SemanticSchema), ActionSemanticsFieldSchema = semanticsFieldSchema(ActionSemanticSchema);
|
|
2241
|
+
|
|
2208
2242
|
function actionFields(op, group) {
|
|
2209
2243
|
return {
|
|
2210
2244
|
name: NonEmpty,
|
|
2211
|
-
semantics:
|
|
2245
|
+
semantics: ActionSemanticsFieldSchema,
|
|
2212
2246
|
title: v__namespace.optional(v__namespace.string()),
|
|
2213
2247
|
description: v__namespace.optional(v__namespace.string()),
|
|
2214
2248
|
group: v__namespace.optional(group),
|
|
@@ -2255,6 +2289,7 @@ const StoredActionSchema = pinned()(v__namespace.strictObject({
|
|
|
2255
2289
|
function activityFields({field: field, action: action, target: target, group: group}) {
|
|
2256
2290
|
return {
|
|
2257
2291
|
name: NonEmpty,
|
|
2292
|
+
semantics: SemanticsFieldSchema,
|
|
2258
2293
|
title: v__namespace.optional(v__namespace.string()),
|
|
2259
2294
|
description: v__namespace.optional(v__namespace.string()),
|
|
2260
2295
|
groups: v__namespace.optional(v__namespace.array(GroupSchema)),
|
|
@@ -2328,6 +2363,7 @@ const GuardSchema = v__namespace.strictObject(guardFields(NonEmpty)), AuthoringG
|
|
|
2328
2363
|
function stageFields({field: field, activity: activity, transition: transition, guard: guard, editable: editable}) {
|
|
2329
2364
|
return {
|
|
2330
2365
|
name: NonEmpty,
|
|
2366
|
+
semantics: SemanticsFieldSchema,
|
|
2331
2367
|
title: v__namespace.optional(v__namespace.string()),
|
|
2332
2368
|
description: v__namespace.optional(v__namespace.string()),
|
|
2333
2369
|
groups: v__namespace.optional(v__namespace.array(GroupSchema)),
|
|
@@ -2366,6 +2402,7 @@ const StoredStartSchema = pinned()(v__namespace.strictObject(startFields(picklis
|
|
|
2366
2402
|
function workflowFields({field: field, stage: stage, start: start}) {
|
|
2367
2403
|
return {
|
|
2368
2404
|
name: NonEmpty,
|
|
2405
|
+
semantics: SemanticsFieldSchema,
|
|
2369
2406
|
title: NonEmpty,
|
|
2370
2407
|
description: v__namespace.optional(v__namespace.string()),
|
|
2371
2408
|
groups: v__namespace.optional(v__namespace.array(GroupSchema)),
|
|
@@ -2562,11 +2599,20 @@ function checkStageReachability({def: def, stageNames: stageNames, issues: issue
|
|
|
2562
2599
|
});
|
|
2563
2600
|
}
|
|
2564
2601
|
|
|
2602
|
+
function actionSites(def) {
|
|
2603
|
+
return def.stages.flatMap((stage, i) => (stage.activities ?? []).flatMap((activity, j) => (activity.actions ?? []).map((action, a) => ({
|
|
2604
|
+
action: action,
|
|
2605
|
+
activity: activity,
|
|
2606
|
+
stage: stage,
|
|
2607
|
+
path: [ "stages", i, "activities", j, "actions", a ]
|
|
2608
|
+
}))));
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2565
2611
|
function effectNameSites(def) {
|
|
2566
2612
|
const sites = [];
|
|
2567
|
-
for (const
|
|
2613
|
+
for (const {action: action, path: path} of actionSites(def)) collectEffects({
|
|
2568
2614
|
effects: action.effects,
|
|
2569
|
-
path: [
|
|
2615
|
+
path: [ ...path, "effects" ],
|
|
2570
2616
|
sites: sites
|
|
2571
2617
|
});
|
|
2572
2618
|
return sites;
|
|
@@ -2697,13 +2743,22 @@ function checkUnboundConditionVars(def, issues) {
|
|
|
2697
2743
|
}
|
|
2698
2744
|
}
|
|
2699
2745
|
|
|
2746
|
+
const SOFT_GATE_VARS = {
|
|
2747
|
+
can: {
|
|
2748
|
+
noun: "the caller's grants"
|
|
2749
|
+
},
|
|
2750
|
+
attributes: {
|
|
2751
|
+
noun: "the caller's org-level user attributes"
|
|
2752
|
+
}
|
|
2753
|
+
}, SOFT_GATE_VAR_NAMES = Object.keys(SOFT_GATE_VARS);
|
|
2754
|
+
|
|
2700
2755
|
function unboundVarsAt(site) {
|
|
2701
2756
|
const callerVars = unboundCallerVars(site.policy);
|
|
2702
2757
|
return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
|
|
2703
2758
|
}
|
|
2704
2759
|
|
|
2705
2760
|
function unboundCallerVars(policy) {
|
|
2706
|
-
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [
|
|
2761
|
+
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ ...SOFT_GATE_VAR_NAMES, "params" ] : SOFT_GATE_VAR_NAMES;
|
|
2707
2762
|
}
|
|
2708
2763
|
|
|
2709
2764
|
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";
|
|
@@ -2713,7 +2768,12 @@ function rowVarMessage(site) {
|
|
|
2713
2768
|
}
|
|
2714
2769
|
|
|
2715
2770
|
function callerVarMessage(site, name) {
|
|
2716
|
-
|
|
2771
|
+
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\``;
|
|
2772
|
+
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`;
|
|
2773
|
+
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`;
|
|
2774
|
+
const softGate = SOFT_GATE_VARS[name];
|
|
2775
|
+
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}`;
|
|
2776
|
+
throw new Error(`callerVarMessage: unreachable for $${name} at ${site.label} (policy ${site.policy})`);
|
|
2717
2777
|
}
|
|
2718
2778
|
|
|
2719
2779
|
function conditionSites(def) {
|
|
@@ -2968,9 +3028,9 @@ function belowScopeNestedSites(args) {
|
|
|
2968
3028
|
}
|
|
2969
3029
|
|
|
2970
3030
|
function checkLevelKindEffectOutputs(def, issues) {
|
|
2971
|
-
for (const
|
|
3031
|
+
for (const {action: action, path: path} of actionSites(def)) pushOutputIssues({
|
|
2972
3032
|
action: action,
|
|
2973
|
-
path:
|
|
3033
|
+
path: path,
|
|
2974
3034
|
issues: issues
|
|
2975
3035
|
});
|
|
2976
3036
|
}
|
|
@@ -3052,18 +3112,18 @@ function storedRolesIssue(action) {
|
|
|
3052
3112
|
}
|
|
3053
3113
|
|
|
3054
3114
|
function checkStoredRolesPlacement(def, issues) {
|
|
3055
|
-
for (const
|
|
3115
|
+
for (const {action: action, path: path} of actionSites(def)) {
|
|
3056
3116
|
const message = storedRolesIssue(action);
|
|
3057
3117
|
message !== void 0 && issues.push({
|
|
3058
|
-
path: [
|
|
3118
|
+
path: [ ...path, "roles" ],
|
|
3059
3119
|
message: message
|
|
3060
3120
|
});
|
|
3061
3121
|
}
|
|
3062
3122
|
}
|
|
3063
3123
|
|
|
3064
3124
|
function checkTriggeredActionParams(def, issues) {
|
|
3065
|
-
for (const
|
|
3066
|
-
path: [
|
|
3125
|
+
for (const {action: action, path: path} of actionSites(def)) action.when === void 0 || (action.params ?? []).length === 0 || issues.push({
|
|
3126
|
+
path: [ ...path, "params" ],
|
|
3067
3127
|
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`
|
|
3068
3128
|
});
|
|
3069
3129
|
}
|
|
@@ -3108,13 +3168,20 @@ function checkSingleSubjectRequirements(def, issues) {
|
|
|
3108
3168
|
|
|
3109
3169
|
function checkStartFilterReads(def, issues) {
|
|
3110
3170
|
const filter = def.start?.filter;
|
|
3111
|
-
filter
|
|
3171
|
+
if (filter === void 0) return;
|
|
3172
|
+
const params = conditionParameterNames(filter);
|
|
3173
|
+
params.has("fields") && issues.push({
|
|
3112
3174
|
path: [ "start", "filter" ],
|
|
3113
3175
|
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"
|
|
3114
|
-
})
|
|
3176
|
+
});
|
|
3177
|
+
for (const name of CALLER_BOUND_VARS) params.has(name) && issues.push({
|
|
3178
|
+
path: [ "start", "filter" ],
|
|
3179
|
+
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`
|
|
3180
|
+
});
|
|
3181
|
+
readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
3115
3182
|
path: [ "start", "filter" ],
|
|
3116
3183
|
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"
|
|
3117
|
-
})
|
|
3184
|
+
});
|
|
3118
3185
|
}
|
|
3119
3186
|
|
|
3120
3187
|
function checkStartRequirementReads(def, issues) {
|
|
@@ -3410,15 +3477,13 @@ function seedEarlierSiblingTarget(args) {
|
|
|
3410
3477
|
}
|
|
3411
3478
|
|
|
3412
3479
|
function opSites(def) {
|
|
3413
|
-
|
|
3414
|
-
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({
|
|
3480
|
+
return actionSites(def).map(({action: action, activity: activity, stage: stage, path: path}) => ({
|
|
3415
3481
|
ops: action.ops,
|
|
3416
|
-
path: [
|
|
3482
|
+
path: [ ...path, "ops" ],
|
|
3417
3483
|
label: `action "${action.name}"`,
|
|
3418
3484
|
stage: stage,
|
|
3419
3485
|
activity: activity
|
|
3420
|
-
});
|
|
3421
|
-
return sites;
|
|
3486
|
+
}));
|
|
3422
3487
|
}
|
|
3423
3488
|
|
|
3424
3489
|
function checkFieldReadOpValues(def, issues) {
|
|
@@ -3435,7 +3500,7 @@ function checkFieldReadOpValues(def, issues) {
|
|
|
3435
3500
|
}
|
|
3436
3501
|
|
|
3437
3502
|
function checkOpsFieldReads({ops: ops, path: path, label: label, ...ctx}) {
|
|
3438
|
-
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({
|
|
3503
|
+
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({
|
|
3439
3504
|
...ctx,
|
|
3440
3505
|
read: read,
|
|
3441
3506
|
path: readPath,
|
|
@@ -3486,35 +3551,55 @@ function opFieldReadMissMessage({read: read, where: where, hosts: hosts}) {
|
|
|
3486
3551
|
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)"}`;
|
|
3487
3552
|
}
|
|
3488
3553
|
|
|
3489
|
-
function
|
|
3554
|
+
function checkFieldTargetOps(def, issues) {
|
|
3490
3555
|
const workflow = def.fields ?? [];
|
|
3491
3556
|
for (const site of opSites(def)) for (const [o, op] of (site.ops ?? []).entries()) {
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
workflow: workflow,
|
|
3495
|
-
stage: site.stage.fields ?? [],
|
|
3496
|
-
activity: site.activity.fields ?? []
|
|
3497
|
-
};
|
|
3498
|
-
checkUpdateWhereTargetKind({
|
|
3557
|
+
const path = [ ...site.path, o ], rule = TARGET_KIND_RULES[op.type];
|
|
3558
|
+
rule !== void 0 && "target" in op && checkTargetKind({
|
|
3499
3559
|
op: op,
|
|
3500
|
-
path: [ ...
|
|
3560
|
+
path: [ ...path, "target" ],
|
|
3501
3561
|
label: site.label,
|
|
3502
|
-
scopes:
|
|
3562
|
+
scopes: opTargetScopes(workflow, site),
|
|
3563
|
+
rule: rule,
|
|
3503
3564
|
issues: issues
|
|
3504
|
-
}), checkUpdateWhereMergeKeys({
|
|
3565
|
+
}), op.type === "field.updateWhere" && checkUpdateWhereMergeKeys({
|
|
3505
3566
|
op: op,
|
|
3506
|
-
path:
|
|
3567
|
+
path: path,
|
|
3507
3568
|
label: site.label,
|
|
3508
3569
|
issues: issues
|
|
3509
3570
|
});
|
|
3510
3571
|
}
|
|
3511
3572
|
}
|
|
3512
3573
|
|
|
3513
|
-
|
|
3574
|
+
const arithmeticTargetRule = {
|
|
3575
|
+
accepts: target => target.type === "number",
|
|
3576
|
+
issue: () => "arithmetic ops target `number` entries only"
|
|
3577
|
+
}, TARGET_KIND_RULES = {
|
|
3578
|
+
"field.inc": arithmeticTargetRule,
|
|
3579
|
+
"field.dec": arithmeticTargetRule,
|
|
3580
|
+
"field.setIfMissing": {
|
|
3581
|
+
accepts: target => !isAlwaysArrayFieldKind(target.type),
|
|
3582
|
+
issue: () => "setIfMissing applies to nullable entries only; an empty array entry already holds []"
|
|
3583
|
+
},
|
|
3584
|
+
"field.updateWhere": {
|
|
3585
|
+
accepts: target => target.type === "array",
|
|
3586
|
+
issue: target => "updateWhere merges declared row sub-fields, so its target must be an `array` entry" + rowOpsHint(target.type)
|
|
3587
|
+
}
|
|
3588
|
+
};
|
|
3589
|
+
|
|
3590
|
+
function opTargetScopes(workflow, site) {
|
|
3591
|
+
return {
|
|
3592
|
+
workflow: workflow,
|
|
3593
|
+
stage: site.stage.fields ?? [],
|
|
3594
|
+
activity: site.activity.fields ?? []
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
function checkTargetKind({op: op, path: path, label: label, scopes: scopes, rule: rule, issues: issues}) {
|
|
3514
3599
|
const target = scopes[op.target.scope]?.find(entry => entry.name === op.target.field);
|
|
3515
|
-
target === void 0 || target
|
|
3516
|
-
path:
|
|
3517
|
-
message: `${label}
|
|
3600
|
+
target === void 0 || rule.accepts(target) || issues.push({
|
|
3601
|
+
path: path,
|
|
3602
|
+
message: `${label} ${op.type} targets ${op.target.scope}-scope "${op.target.field}" (${target.type}) — ${rule.issue(target)}`
|
|
3518
3603
|
});
|
|
3519
3604
|
}
|
|
3520
3605
|
|
|
@@ -3746,7 +3831,7 @@ function checkWorkflowInvariants(def) {
|
|
|
3746
3831
|
}), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
|
|
3747
3832
|
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
3748
3833
|
checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
3749
|
-
checkFieldReadOpValues(def, issues),
|
|
3834
|
+
checkFieldReadOpValues(def, issues), checkFieldTargetOps(def, issues), checkGuardFieldReads(def, issues),
|
|
3750
3835
|
checkAssigneesEntries(def, issues), checkDueDateEntries(def, issues), checkSubjectEntries(def, issues),
|
|
3751
3836
|
checkLevelKindEffectOutputs(def, issues), checkActivityTerminalPaths(def, issues),
|
|
3752
3837
|
checkTerminalStageActivities(def, issues), checkTriggeredActionParams(def, issues),
|
|
@@ -3787,6 +3872,8 @@ exports.CONDITION_VARS = CONDITION_VARS;
|
|
|
3787
3872
|
|
|
3788
3873
|
exports.ContractViolationError = ContractViolationError;
|
|
3789
3874
|
|
|
3875
|
+
exports.DECISION_SEMANTICS = DECISION_SEMANTICS;
|
|
3876
|
+
|
|
3790
3877
|
exports.DEFAULT_TRANSITION_WHEN = DEFAULT_TRANSITION_WHEN;
|
|
3791
3878
|
|
|
3792
3879
|
exports.DOCUMENT_VALUE_PERMISSIONS = DOCUMENT_VALUE_PERMISSIONS;
|
|
@@ -3837,6 +3924,8 @@ exports.RESERVED_CONDITION_VARS = RESERVED_CONDITION_VARS;
|
|
|
3837
3924
|
|
|
3838
3925
|
exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
|
|
3839
3926
|
|
|
3927
|
+
exports.SIGNAL_SEMANTICS = SIGNAL_SEMANTICS;
|
|
3928
|
+
|
|
3840
3929
|
exports.START_FILTER_VARS = START_FILTER_VARS;
|
|
3841
3930
|
|
|
3842
3931
|
exports.START_REQUIREMENT_VARS = START_REQUIREMENT_VARS;
|
|
@@ -3921,6 +4010,8 @@ exports.groq = groq;
|
|
|
3921
4010
|
|
|
3922
4011
|
exports.groupMembershipNames = groupMembershipNames;
|
|
3923
4012
|
|
|
4013
|
+
exports.isAlwaysArrayFieldKind = isAlwaysArrayFieldKind;
|
|
4014
|
+
|
|
3924
4015
|
exports.isBareSeedId = isBareSeedId;
|
|
3925
4016
|
|
|
3926
4017
|
exports.isCascadeFired = isCascadeFired;
|