@sanity/workflow-engine 0.16.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 +43 -0
- package/DATAMODEL.md +104 -29
- package/dist/_chunks-cjs/invariants.cjs +420 -120
- package/dist/_chunks-es/invariants.js +389 -121
- package/dist/define.d.cts +35 -12
- package/dist/define.d.ts +35 -12
- package/dist/index.cjs +4034 -2714
- package/dist/index.d.cts +737 -131
- package/dist/index.d.ts +737 -131
- package/dist/index.js +3994 -2731
- package/package.json +2 -2
|
@@ -315,17 +315,17 @@ function effectNotFoundMessage(args) {
|
|
|
315
315
|
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}`;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
const
|
|
318
|
+
const LAKE_ID_SEGMENT_RE = /^[a-z0-9][a-z0-9-]*$/, LAKE_ID_SEGMENT_GLOSS = "ASCII lowercase + digits + dashes, no leading dash, no dots";
|
|
319
319
|
|
|
320
320
|
function validateTag(tag) {
|
|
321
|
-
if (!
|
|
321
|
+
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})`);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
function tagScopeFilter() {
|
|
325
325
|
return "tag == $tag";
|
|
326
326
|
}
|
|
327
327
|
|
|
328
|
-
const NonEmptyString = v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty("must not be empty"));
|
|
328
|
+
const NonEmptyString$1 = v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty("must not be empty"));
|
|
329
329
|
|
|
330
330
|
function asPredicate(validate) {
|
|
331
331
|
return value => {
|
|
@@ -339,21 +339,21 @@ function asPredicate(validate) {
|
|
|
339
339
|
|
|
340
340
|
const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v__namespace.variant("type", [ v__namespace.object({
|
|
341
341
|
type: v__namespace.literal("dataset"),
|
|
342
|
-
id: v__namespace.pipe(NonEmptyString, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
|
|
342
|
+
id: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
|
|
343
343
|
}), v__namespace.object({
|
|
344
344
|
type: v__namespace.literal("canvas"),
|
|
345
|
-
id: NonEmptyString
|
|
345
|
+
id: NonEmptyString$1
|
|
346
346
|
}), v__namespace.object({
|
|
347
347
|
type: v__namespace.literal("media-library"),
|
|
348
|
-
id: NonEmptyString
|
|
348
|
+
id: NonEmptyString$1
|
|
349
349
|
}), v__namespace.object({
|
|
350
350
|
type: v__namespace.literal("dashboard"),
|
|
351
|
-
id: NonEmptyString
|
|
351
|
+
id: NonEmptyString$1
|
|
352
352
|
}) ]), ResourceBindingSchema = v__namespace.object({
|
|
353
|
-
name: v__namespace.pipe(NonEmptyString, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
|
|
353
|
+
name: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
|
|
354
354
|
resource: WorkflowResourceSchema
|
|
355
355
|
}), DefinitionSchema = v__namespace.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v__namespace.object({
|
|
356
|
-
name: NonEmptyString,
|
|
356
|
+
name: NonEmptyString$1,
|
|
357
357
|
tag: v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty(), v__namespace.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
|
|
358
358
|
workflowResource: WorkflowResourceSchema,
|
|
359
359
|
resourceAliases: v__namespace.optional(v__namespace.pipe(v__namespace.array(ResourceBindingSchema), v__namespace.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"))),
|
|
@@ -518,6 +518,9 @@ function desugarStart(start) {
|
|
|
518
518
|
kind: start.kind ?? "interactive",
|
|
519
519
|
...start.filter !== void 0 ? {
|
|
520
520
|
filter: start.filter
|
|
521
|
+
} : {},
|
|
522
|
+
...start.allowed !== void 0 ? {
|
|
523
|
+
allowed: start.allowed
|
|
521
524
|
} : {}
|
|
522
525
|
};
|
|
523
526
|
}
|
|
@@ -1066,12 +1069,43 @@ function conditionParameterNames(groq2) {
|
|
|
1066
1069
|
}
|
|
1067
1070
|
|
|
1068
1071
|
function conditionFieldReadNames(groq2) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1072
|
+
return new Set(conditionFieldReads(groq2).map(read => read.name));
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function conditionFieldReads(groq2) {
|
|
1076
|
+
const reads = /* @__PURE__ */ new Map;
|
|
1077
|
+
return collectFieldReads(tryParseGroq(groq2), reads), [ ...reads.values() ];
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function collectFieldReads(node, reads) {
|
|
1081
|
+
if (Array.isArray(node)) {
|
|
1082
|
+
for (const item of node) collectFieldReads(item, reads);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (typeof node != "object" || node === null) return;
|
|
1086
|
+
const chain = fieldsAttributeChain(node);
|
|
1087
|
+
if (chain !== void 0) {
|
|
1088
|
+
const [name, ...tail] = chain, path = tail.length > 0 ? tail.join(".") : void 0;
|
|
1089
|
+
reads.set(`${name}.${path ?? ""}`, {
|
|
1090
|
+
name: name,
|
|
1091
|
+
path: path
|
|
1092
|
+
});
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
for (const value of Object.values(node)) collectFieldReads(value, reads);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function fieldsAttributeChain(node) {
|
|
1099
|
+
const names = [];
|
|
1100
|
+
let current = node;
|
|
1101
|
+
for (;typeof current == "object" && current !== null; ) {
|
|
1102
|
+
const typed = current;
|
|
1103
|
+
if (typed.type !== "AccessAttribute" || typeof typed.name != "string") return;
|
|
1104
|
+
names.unshift(typed.name);
|
|
1105
|
+
const base = typed.base;
|
|
1106
|
+
if (base?.type === "Parameter" && base.name === "fields") return names;
|
|
1107
|
+
current = base;
|
|
1108
|
+
}
|
|
1075
1109
|
}
|
|
1076
1110
|
|
|
1077
1111
|
function conditionEffectReads(groq2) {
|
|
@@ -1145,7 +1179,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
|
|
|
1145
1179
|
name: "fields",
|
|
1146
1180
|
binding: "always",
|
|
1147
1181
|
label: "the workflow's fields",
|
|
1148
|
-
description: "Declared field entries rendered by name (`$fields.<name>` is the value, no
|
|
1182
|
+
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."
|
|
1149
1183
|
}, {
|
|
1150
1184
|
name: "parent",
|
|
1151
1185
|
binding: "always",
|
|
@@ -1215,7 +1249,7 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
|
|
|
1215
1249
|
name: "row",
|
|
1216
1250
|
binding: "spawn",
|
|
1217
1251
|
label: "the spawned row",
|
|
1218
|
-
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)."
|
|
1252
|
+
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."
|
|
1219
1253
|
}, {
|
|
1220
1254
|
name: "params",
|
|
1221
1255
|
binding: "caller",
|
|
@@ -1231,20 +1265,148 @@ const FIELD_SCOPES = [ "workflow", "stage", "activity" ], DOCUMENT_VALUE_PERMISS
|
|
|
1231
1265
|
description: "The engine's tag partition — scope `*[...]` instance scans with `tag == $tag`."
|
|
1232
1266
|
}, {
|
|
1233
1267
|
name: "definition",
|
|
1234
|
-
description: "The `name` of the definition under evaluation (its own start
|
|
1268
|
+
description: "The `name` of the definition under evaluation (its own start block binds it)."
|
|
1235
1269
|
}, {
|
|
1236
1270
|
name: "now",
|
|
1237
1271
|
description: "The ISO clock reading of the evaluating engine."
|
|
1238
|
-
}, {
|
|
1272
|
+
} ], START_ALLOWED_VARS = [ ...START_FILTER_VARS, {
|
|
1239
1273
|
name: "fields",
|
|
1240
|
-
description: "The
|
|
1274
|
+
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."
|
|
1241
1275
|
} ], GUARD_PREDICATE_VARS = [ {
|
|
1242
1276
|
name: "guard",
|
|
1243
1277
|
description: "The guard document itself (its `metadata` carries deploy-time resolved values)."
|
|
1244
1278
|
}, {
|
|
1245
1279
|
name: "mutation",
|
|
1246
1280
|
description: "The attempted mutation — `mutation.action` is the write kind being gated."
|
|
1247
|
-
} ],
|
|
1281
|
+
} ], NonEmptyString = v__namespace.pipe(v__namespace.string(), v__namespace.minLength(1, "must be a non-empty string"));
|
|
1282
|
+
|
|
1283
|
+
function isParseableInstant(value) {
|
|
1284
|
+
return typeof value == "string" && !Number.isNaN(Date.parse(value));
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const IsoTimestamp = v__namespace.pipe(v__namespace.string(), v__namespace.check(s => isParseableInstant(s), "must be an ISO-8601 datetime string"));
|
|
1288
|
+
|
|
1289
|
+
function tolerantObject() {
|
|
1290
|
+
return (entries, ..._exact) => v__namespace.looseObject(entries);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function tolerantEntries() {
|
|
1294
|
+
return (entries, ..._exact) => entries;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function schemaTreeShape(schema) {
|
|
1298
|
+
return walkSchemaShape(schema, /* @__PURE__ */ new Set);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function walkSchemaShape(node, path) {
|
|
1302
|
+
if (typeof node != "object" || node === null) return "unknown";
|
|
1303
|
+
if (path.has(node)) return "(circular)";
|
|
1304
|
+
path.add(node);
|
|
1305
|
+
try {
|
|
1306
|
+
const schema = node;
|
|
1307
|
+
return containerShape(schema, path) ?? leafShape(schema);
|
|
1308
|
+
} finally {
|
|
1309
|
+
path.delete(node);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const objectWalker = (schema, path) => objectEntriesShape(schema.entries, path), unionWalker = (schema, path) => ({
|
|
1314
|
+
union: schema.options.map(option => walkSchemaShape(option, path))
|
|
1315
|
+
}), unwrapWalker = (schema, path) => walkSchemaShape(schema.wrapped, path), CONTAINER_WALKERS = {
|
|
1316
|
+
strict_object: objectWalker,
|
|
1317
|
+
loose_object: objectWalker,
|
|
1318
|
+
object: objectWalker,
|
|
1319
|
+
array: (schema, path) => ({
|
|
1320
|
+
array: walkSchemaShape(schema.item, path)
|
|
1321
|
+
}),
|
|
1322
|
+
record: (schema, path) => ({
|
|
1323
|
+
record: walkSchemaShape(schema.value, path)
|
|
1324
|
+
}),
|
|
1325
|
+
union: unionWalker,
|
|
1326
|
+
variant: unionWalker,
|
|
1327
|
+
lazy: (schema, path) => walkSchemaShape(schema.getter(void 0), path),
|
|
1328
|
+
exact_optional: unwrapWalker,
|
|
1329
|
+
optional: unwrapWalker,
|
|
1330
|
+
nullable: unwrapWalker
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
function containerShape(schema, path) {
|
|
1334
|
+
return CONTAINER_WALKERS[String(schema.type)]?.(schema, path);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function objectEntriesShape(entries, path) {
|
|
1338
|
+
const out = {};
|
|
1339
|
+
for (const [key, entry] of Object.entries(entries)) {
|
|
1340
|
+
const optional = isOptionalEntry(entry), wrapped = optional ? entry.wrapped : entry;
|
|
1341
|
+
out[optional ? `${key}?` : key] = walkSchemaShape(wrapped, path);
|
|
1342
|
+
}
|
|
1343
|
+
return Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1));
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function isOptionalEntry(entry) {
|
|
1347
|
+
if (typeof entry != "object" || entry === null) return !1;
|
|
1348
|
+
const kind = entry.type;
|
|
1349
|
+
return kind === "exact_optional" || kind === "optional";
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
const LEAF_KINDS = /* @__PURE__ */ new Set([ "string", "number", "boolean", "null", "undefined", "unknown", "any" ]);
|
|
1353
|
+
|
|
1354
|
+
function leafShape(schema) {
|
|
1355
|
+
if (schema.type === "picklist") return schema.options.join(" | ");
|
|
1356
|
+
if (schema.type === "literal") return `literal ${String(schema.literal)}`;
|
|
1357
|
+
if (schema.type === "custom") return typeof schema.message == "string" ? `custom(${schema.message})` : "custom";
|
|
1358
|
+
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`);
|
|
1359
|
+
return String(schema.type);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function formatValidationError(label, issues) {
|
|
1363
|
+
const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
|
|
1364
|
+
return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function issuesFromValibot(issues) {
|
|
1368
|
+
return issues.map(issue => ({
|
|
1369
|
+
path: issue.path ? issue.path.map(item => item.key) : [],
|
|
1370
|
+
message: issue.message
|
|
1371
|
+
}));
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function formatIssuePath(path) {
|
|
1375
|
+
let out = "";
|
|
1376
|
+
for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
|
|
1377
|
+
return out;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
class PersistedDocShapeError extends WorkflowError {
|
|
1381
|
+
documentId;
|
|
1382
|
+
documentType;
|
|
1383
|
+
issues;
|
|
1384
|
+
constructor(args) {
|
|
1385
|
+
super("persisted-doc-shape", formatValidationError(`Persisted ${args.documentType} document "${args.documentId}"`, args.issues)),
|
|
1386
|
+
this.name = "PersistedDocShapeError", this.documentId = args.documentId, this.documentType = args.documentType,
|
|
1387
|
+
this.issues = args.issues;
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
function parsePersistedDoc(args) {
|
|
1392
|
+
const result = v__namespace.safeParse(args.schema, args.doc);
|
|
1393
|
+
if (!result.success) throw new PersistedDocShapeError({
|
|
1394
|
+
documentId: documentIdOf(args.doc),
|
|
1395
|
+
documentType: args.docType,
|
|
1396
|
+
issues: issuesFromValibot(result.issues)
|
|
1397
|
+
});
|
|
1398
|
+
return result.output;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function documentIdOf(doc) {
|
|
1402
|
+
if (typeof doc == "object" && doc !== null) {
|
|
1403
|
+
const id = doc._id;
|
|
1404
|
+
if (typeof id == "string") return id;
|
|
1405
|
+
}
|
|
1406
|
+
return "(unknown id)";
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const ACTOR_KINDS = [ "person", "agent", "system" ];
|
|
1248
1410
|
|
|
1249
1411
|
function releaseDocId(releaseName) {
|
|
1250
1412
|
return `_.releases.${releaseName}`;
|
|
@@ -1291,29 +1453,28 @@ class FieldValueShapeError extends WorkflowError {
|
|
|
1291
1453
|
}
|
|
1292
1454
|
}
|
|
1293
1455
|
|
|
1294
|
-
const
|
|
1295
|
-
id:
|
|
1296
|
-
type:
|
|
1297
|
-
}), ReleaseRefShape = v__namespace.pipe(
|
|
1298
|
-
id:
|
|
1456
|
+
const GdrUriSchema = v__namespace.custom(s => typeof s == "string" && isGdrUri(s), "must be a GDR URI"), GdrShape = tolerantObject()({
|
|
1457
|
+
id: GdrUriSchema,
|
|
1458
|
+
type: NonEmptyString
|
|
1459
|
+
}), ReleaseRefShape = v__namespace.pipe(tolerantObject()({
|
|
1460
|
+
id: GdrUriSchema,
|
|
1299
1461
|
type: v__namespace.literal("system.release"),
|
|
1300
|
-
releaseName:
|
|
1301
|
-
}), v__namespace.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape =
|
|
1462
|
+
releaseName: NonEmptyString
|
|
1463
|
+
}), v__namespace.check(ref => !isGdrUri(ref.id) || extractDocumentId(ref.id) === releaseDocId(ref.releaseName), "id must point at the `_.releases.<releaseName>` doc named by releaseName")), ActorShape = tolerantObject()({
|
|
1302
1464
|
kind: v__namespace.picklist(ACTOR_KINDS),
|
|
1303
|
-
id:
|
|
1304
|
-
roles: v__namespace.
|
|
1305
|
-
onBehalfOf: v__namespace.
|
|
1306
|
-
}), AssigneeShape = v__namespace.union([
|
|
1465
|
+
id: NonEmptyString,
|
|
1466
|
+
roles: v__namespace.exactOptional(v__namespace.array(v__namespace.string())),
|
|
1467
|
+
onBehalfOf: v__namespace.exactOptional(v__namespace.string())
|
|
1468
|
+
}), AssigneeShape = v__namespace.union([ tolerantObject()({
|
|
1307
1469
|
type: v__namespace.literal("user"),
|
|
1308
|
-
id:
|
|
1309
|
-
}),
|
|
1470
|
+
id: NonEmptyString
|
|
1471
|
+
}), tolerantObject()({
|
|
1310
1472
|
type: v__namespace.literal("role"),
|
|
1311
|
-
role:
|
|
1312
|
-
}) ]), NullableString = v__namespace.union([ v__namespace.null(), v__namespace.string() ]), NullableNumber = v__namespace.union([ v__namespace.null(), v__namespace.number() ]), NullableBoolean = v__namespace.union([ v__namespace.null(), v__namespace.boolean() ]), NullableDateTime = v__namespace.union([ v__namespace.null(),
|
|
1473
|
+
role: NonEmptyString
|
|
1474
|
+
}) ]), NullableString = v__namespace.union([ v__namespace.null(), v__namespace.string() ]), NullableNumber = v__namespace.union([ v__namespace.null(), v__namespace.number() ]), NullableBoolean = v__namespace.union([ v__namespace.null(), v__namespace.boolean() ]), NullableDateTime = v__namespace.union([ v__namespace.null(), IsoTimestamp ]), NullableDate = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.string(), v__namespace.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, fieldValueSchemas = {
|
|
1313
1475
|
"doc.ref": v__namespace.union([ v__namespace.null(), GdrShape ]),
|
|
1314
1476
|
"doc.refs": v__namespace.array(GdrShape),
|
|
1315
1477
|
"release.ref": v__namespace.union([ v__namespace.null(), ReleaseRefShape ]),
|
|
1316
|
-
query: v__namespace.any(),
|
|
1317
1478
|
string: NullableString,
|
|
1318
1479
|
text: NullableString,
|
|
1319
1480
|
number: NullableNumber,
|
|
@@ -1324,6 +1485,9 @@ const GdrShape = v__namespace.looseObject({
|
|
|
1324
1485
|
actor: v__namespace.union([ v__namespace.null(), ActorShape ]),
|
|
1325
1486
|
assignee: v__namespace.union([ v__namespace.null(), AssigneeShape ]),
|
|
1326
1487
|
assignees: v__namespace.array(AssigneeShape)
|
|
1488
|
+
}, valueSchemas = {
|
|
1489
|
+
...fieldValueSchemas,
|
|
1490
|
+
query: v__namespace.any()
|
|
1327
1491
|
};
|
|
1328
1492
|
|
|
1329
1493
|
function shapeValueSchema(shape, leaf) {
|
|
@@ -1410,7 +1574,7 @@ function isBareSeedId(id) {
|
|
|
1410
1574
|
|
|
1411
1575
|
const AuthoringRefId = v__namespace.pipe(v__namespace.string(), v__namespace.check(isAuthoringRefId, "must be a bare document id, a GDR URI, or a portable `@<alias>:<id>` reference")), AuthoringGdrShape = v__namespace.looseObject({
|
|
1412
1576
|
id: AuthoringRefId,
|
|
1413
|
-
type:
|
|
1577
|
+
type: NonEmptyString
|
|
1414
1578
|
}), seedValueSchemas = {
|
|
1415
1579
|
...valueSchemas,
|
|
1416
1580
|
"doc.ref": v__namespace.union([ v__namespace.null(), AuthoringGdrShape ]),
|
|
@@ -1418,7 +1582,7 @@ const AuthoringRefId = v__namespace.pipe(v__namespace.string(), v__namespace.che
|
|
|
1418
1582
|
"release.ref": v__namespace.union([ v__namespace.null(), v__namespace.looseObject({
|
|
1419
1583
|
id: AuthoringRefId,
|
|
1420
1584
|
type: v__namespace.literal("system.release"),
|
|
1421
|
-
releaseName:
|
|
1585
|
+
releaseName: NonEmptyString
|
|
1422
1586
|
}) ])
|
|
1423
1587
|
};
|
|
1424
1588
|
|
|
@@ -1461,7 +1625,7 @@ function formatIssues(issues) {
|
|
|
1461
1625
|
});
|
|
1462
1626
|
}
|
|
1463
1627
|
|
|
1464
|
-
const NonEmpty =
|
|
1628
|
+
const NonEmpty = NonEmptyString, PositiveInt = v__namespace.pipe(v__namespace.number(), v__namespace.integer(), v__namespace.minValue(1)), GROQ_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1465
1629
|
|
|
1466
1630
|
function groqIdentifier(referencedAs) {
|
|
1467
1631
|
return v__namespace.pipe(v__namespace.string(), v__namespace.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`));
|
|
@@ -1844,7 +2008,8 @@ const StoredStageSchema = pinned()(v__namespace.strictObject(stageFields({
|
|
|
1844
2008
|
function startFields(kind) {
|
|
1845
2009
|
return {
|
|
1846
2010
|
kind: kind,
|
|
1847
|
-
filter: v__namespace.optional(ConditionSchema)
|
|
2011
|
+
filter: v__namespace.optional(ConditionSchema),
|
|
2012
|
+
allowed: v__namespace.optional(ConditionSchema)
|
|
1848
2013
|
};
|
|
1849
2014
|
}
|
|
1850
2015
|
|
|
@@ -1902,24 +2067,6 @@ function isInputSourced(entry) {
|
|
|
1902
2067
|
return entry.initialValue?.type === "input";
|
|
1903
2068
|
}
|
|
1904
2069
|
|
|
1905
|
-
function formatValidationError(label, issues) {
|
|
1906
|
-
const lines = issues.map(issue => ` - ${issue.path.length === 0 ? "(root)" : formatIssuePath(issue.path)}: ${issue.message}`);
|
|
1907
|
-
return `${label} failed validation (${issues.length} issue${issues.length === 1 ? "" : "s"}):\n${lines.join(`\n`)}`;
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
function issuesFromValibot(issues) {
|
|
1911
|
-
return issues.map(issue => ({
|
|
1912
|
-
path: issue.path ? issue.path.map(item => item.key) : [],
|
|
1913
|
-
message: issue.message
|
|
1914
|
-
}));
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
function formatIssuePath(path) {
|
|
1918
|
-
let out = "";
|
|
1919
|
-
for (const seg of path) typeof seg == "number" ? out += `[${seg}]` : out += out.length === 0 ? String(seg) : `.${String(seg)}`;
|
|
1920
|
-
return out;
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
2070
|
function parseOrThrow({schema: schema, input: input, label: label}) {
|
|
1924
2071
|
const result = v__namespace.safeParse(schema, input);
|
|
1925
2072
|
if (!result.success) throw new Error(formatValidationError(label, issuesFromValibot(result.issues)));
|
|
@@ -1948,6 +2095,13 @@ function checkDuplicates({names: names, what: what, issues: issues}) {
|
|
|
1948
2095
|
return seen;
|
|
1949
2096
|
}
|
|
1950
2097
|
|
|
2098
|
+
function checkDefinitionName(def, issues) {
|
|
2099
|
+
LAKE_ID_SEGMENT_RE.test(def.name) || issues.push({
|
|
2100
|
+
path: [ "name" ],
|
|
2101
|
+
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`
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1951
2105
|
function checkStages(def, issues) {
|
|
1952
2106
|
const stageNames = checkDuplicates({
|
|
1953
2107
|
names: def.stages.map((stage, i) => ({
|
|
@@ -2084,6 +2238,10 @@ function checkGuardNames(def, issues) {
|
|
|
2084
2238
|
what: "guard name (the guard lake _id derives from it — unique per definition)",
|
|
2085
2239
|
issues: issues
|
|
2086
2240
|
});
|
|
2241
|
+
for (const {name: name, path: path} of sites) LAKE_ID_SEGMENT_RE.test(name) || issues.push({
|
|
2242
|
+
path: path,
|
|
2243
|
+
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`
|
|
2244
|
+
});
|
|
2087
2245
|
}
|
|
2088
2246
|
|
|
2089
2247
|
function fieldScopes(def) {
|
|
@@ -2154,7 +2312,11 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
|
|
|
2154
2312
|
const reads = conditionParameterNames(groq2);
|
|
2155
2313
|
for (const caller of CALLER_BOUND_VARS) reads.has(caller) && issues.push({
|
|
2156
2314
|
path: [ "predicates", name ],
|
|
2157
|
-
message: `predicate "${name}" reads $${caller} — predicates
|
|
2315
|
+
message: `predicate "${name}" reads $${caller} — predicates pre-evaluate caller-free; compose caller gates at the condition site instead`
|
|
2316
|
+
});
|
|
2317
|
+
reads.has("row") && issues.push({
|
|
2318
|
+
path: [ "predicates", name ],
|
|
2319
|
+
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`
|
|
2158
2320
|
});
|
|
2159
2321
|
for (const other of names) other === name || !reads.has(other) || issues.push({
|
|
2160
2322
|
path: [ "predicates", name ],
|
|
@@ -2162,24 +2324,35 @@ function checkPredicateBody({name: name, groq: groq2, names: names, issues: issu
|
|
|
2162
2324
|
});
|
|
2163
2325
|
}
|
|
2164
2326
|
|
|
2165
|
-
function
|
|
2166
|
-
return new
|
|
2327
|
+
function entryMap(entries) {
|
|
2328
|
+
return new Map((entries ?? []).map(entry => [ entry.name, entry ]));
|
|
2167
2329
|
}
|
|
2168
2330
|
|
|
2169
|
-
function
|
|
2331
|
+
function checkUnboundConditionVars(def, issues) {
|
|
2170
2332
|
for (const site of conditionSites(def)) {
|
|
2171
2333
|
const reads = conditionParameterNames(site.groq);
|
|
2172
|
-
for (const name of
|
|
2334
|
+
for (const name of unboundVarsAt(site)) reads.has(name) && issues.push({
|
|
2173
2335
|
path: site.path,
|
|
2174
|
-
message: callerVarMessage(site, name)
|
|
2336
|
+
message: name === "row" ? rowVarMessage(site) : callerVarMessage(site, name)
|
|
2175
2337
|
});
|
|
2176
2338
|
}
|
|
2177
2339
|
}
|
|
2178
2340
|
|
|
2341
|
+
function unboundVarsAt(site) {
|
|
2342
|
+
const callerVars = unboundCallerVars(site.policy);
|
|
2343
|
+
return site.bindsRow === !0 ? callerVars : [ ...callerVars, "row" ];
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2179
2346
|
function unboundCallerVars(policy) {
|
|
2180
2347
|
return policy === "cascade" ? CALLER_BOUND_VARS : policy === "caller-bound" ? [ "params" ] : policy === "triggered-payload" ? [ "can", "params" ] : [ "can" ];
|
|
2181
2348
|
}
|
|
2182
2349
|
|
|
2350
|
+
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";
|
|
2351
|
+
|
|
2352
|
+
function rowVarMessage(site) {
|
|
2353
|
+
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`;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2183
2356
|
function callerVarMessage(site, name) {
|
|
2184
2357
|
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`;
|
|
2185
2358
|
}
|
|
@@ -2187,7 +2360,7 @@ function callerVarMessage(site, name) {
|
|
|
2187
2360
|
function conditionSites(def) {
|
|
2188
2361
|
const sites = [], workflowLayer = {
|
|
2189
2362
|
scope: "workflow",
|
|
2190
|
-
|
|
2363
|
+
entries: entryMap(def.fields)
|
|
2191
2364
|
};
|
|
2192
2365
|
collectEditableSites({
|
|
2193
2366
|
entries: def.fields,
|
|
@@ -2208,10 +2381,10 @@ function conditionSites(def) {
|
|
|
2208
2381
|
function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflowLayer, sites: sites}) {
|
|
2209
2382
|
const stageLayer = {
|
|
2210
2383
|
scope: "stage",
|
|
2211
|
-
|
|
2384
|
+
entries: entryMap(stage.fields)
|
|
2212
2385
|
}, stageFields2 = [ stageLayer, workflowLayer ], editableWindow = [ stageLayer, workflowLayer, ...(stage.activities ?? []).map(activity => ({
|
|
2213
2386
|
scope: "activity",
|
|
2214
|
-
|
|
2387
|
+
entries: entryMap(activity.fields)
|
|
2215
2388
|
})) ];
|
|
2216
2389
|
for (const [k, t] of (stage.transitions ?? []).entries()) sites.push({
|
|
2217
2390
|
groq: t.when,
|
|
@@ -2225,6 +2398,7 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
|
|
|
2225
2398
|
path: [ "stages", i, "fields" ],
|
|
2226
2399
|
labelPrefix: `stage "${stage.name}"`,
|
|
2227
2400
|
fields: editableWindow,
|
|
2401
|
+
unionWindow: !0,
|
|
2228
2402
|
sites: sites
|
|
2229
2403
|
});
|
|
2230
2404
|
for (const [name, editable] of Object.entries(stage.editable ?? {})) typeof editable == "string" && sites.push({
|
|
@@ -2232,23 +2406,28 @@ function collectStageConditionSites({stage: stage, i: i, workflowLayer: workflow
|
|
|
2232
2406
|
path: [ "stages", i, "editable", name ],
|
|
2233
2407
|
label: `stage "${stage.name}" editable override "${name}"`,
|
|
2234
2408
|
policy: "caller-bound",
|
|
2235
|
-
fields: editableWindow
|
|
2409
|
+
fields: editableWindow,
|
|
2410
|
+
unionWindow: !0
|
|
2236
2411
|
});
|
|
2237
2412
|
for (const [j, activity] of (stage.activities ?? []).entries()) collectActivityConditionSites({
|
|
2238
2413
|
activity: activity,
|
|
2239
2414
|
path: [ "stages", i, "activities", j ],
|
|
2240
2415
|
stageFields: stageFields2,
|
|
2416
|
+
workflowLayer: workflowLayer,
|
|
2241
2417
|
sites: sites
|
|
2242
2418
|
});
|
|
2243
2419
|
}
|
|
2244
2420
|
|
|
2245
|
-
function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, sites: sites}) {
|
|
2421
|
+
function collectEditableSites({entries: entries, path: path, labelPrefix: labelPrefix, fields: fields, unionWindow: unionWindow, sites: sites}) {
|
|
2246
2422
|
for (const [n, entry] of (entries ?? []).entries()) typeof entry.editable == "string" && sites.push({
|
|
2247
2423
|
groq: entry.editable,
|
|
2248
2424
|
path: [ ...path, n, "editable" ],
|
|
2249
2425
|
label: `${labelPrefix} field "${entry.name}" editable`,
|
|
2250
2426
|
policy: "caller-bound",
|
|
2251
|
-
fields: fields
|
|
2427
|
+
fields: fields,
|
|
2428
|
+
...unionWindow === !0 ? {
|
|
2429
|
+
unionWindow: !0
|
|
2430
|
+
} : {}
|
|
2252
2431
|
});
|
|
2253
2432
|
}
|
|
2254
2433
|
|
|
@@ -2260,14 +2439,15 @@ function collectOpWhereSites({ops: ops, path: path, label: label, policy: policy
|
|
|
2260
2439
|
...policy !== void 0 ? {
|
|
2261
2440
|
policy: policy
|
|
2262
2441
|
} : {},
|
|
2442
|
+
bindsRow: !0,
|
|
2263
2443
|
fields: fields
|
|
2264
2444
|
});
|
|
2265
2445
|
}
|
|
2266
2446
|
|
|
2267
|
-
function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, sites: sites}) {
|
|
2447
|
+
function collectActivityConditionSites({activity: activity, path: path, stageFields: stageFields2, workflowLayer: workflowLayer, sites: sites}) {
|
|
2268
2448
|
const fields = [ {
|
|
2269
2449
|
scope: "activity",
|
|
2270
|
-
|
|
2450
|
+
entries: entryMap(activity.fields)
|
|
2271
2451
|
}, ...stageFields2 ];
|
|
2272
2452
|
activity.filter !== void 0 && sites.push({
|
|
2273
2453
|
groq: activity.filter,
|
|
@@ -2293,11 +2473,12 @@ function collectActivityConditionSites({activity: activity, path: path, stageFie
|
|
|
2293
2473
|
activity: activity,
|
|
2294
2474
|
path: path,
|
|
2295
2475
|
fields: fields,
|
|
2476
|
+
workflowLayer: workflowLayer,
|
|
2296
2477
|
sites: sites
|
|
2297
2478
|
});
|
|
2298
2479
|
}
|
|
2299
2480
|
|
|
2300
|
-
function collectActionConditionSites({activity: activity, path: path, fields: fields, sites: sites}) {
|
|
2481
|
+
function collectActionConditionSites({activity: activity, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
|
|
2301
2482
|
for (const [a, action] of (activity.actions ?? []).entries()) {
|
|
2302
2483
|
const actionPath = [ ...path, "actions", a ], cascadeFired = action.when !== void 0;
|
|
2303
2484
|
action.when !== void 0 && sites.push({
|
|
@@ -2332,12 +2513,13 @@ function collectActionConditionSites({activity: activity, path: path, fields: fi
|
|
|
2332
2513
|
action: action,
|
|
2333
2514
|
path: actionPath,
|
|
2334
2515
|
fields: fields,
|
|
2516
|
+
workflowLayer: workflowLayer,
|
|
2335
2517
|
sites: sites
|
|
2336
2518
|
});
|
|
2337
2519
|
}
|
|
2338
2520
|
}
|
|
2339
2521
|
|
|
2340
|
-
function collectSpawnSites({action: action, path: path, fields: fields, sites: sites}) {
|
|
2522
|
+
function collectSpawnSites({action: action, path: path, fields: fields, workflowLayer: workflowLayer, sites: sites}) {
|
|
2341
2523
|
const spawn = action.spawn;
|
|
2342
2524
|
if (spawn === void 0) return;
|
|
2343
2525
|
const spawnPath = [ ...path, "spawn" ];
|
|
@@ -2346,13 +2528,17 @@ function collectSpawnSites({action: action, path: path, fields: fields, sites: s
|
|
|
2346
2528
|
path: [ ...spawnPath, "forEach" ],
|
|
2347
2529
|
label: `action "${action.name}".spawn.forEach`,
|
|
2348
2530
|
policy: "cascade",
|
|
2349
|
-
fields:
|
|
2531
|
+
fields: [ workflowLayer ],
|
|
2532
|
+
windowNote: "spawn.forEach evaluates against workflow-scope $fields only — stage/activity fields are never bound at discovery time"
|
|
2350
2533
|
});
|
|
2351
|
-
for (const [group, record] of [ [ "with", spawn.with ], [ "context", spawn.context ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
|
|
2534
|
+
for (const [group, record, bindsRow] of [ [ "with", spawn.with, !0 ], [ "context", spawn.context, !1 ] ]) for (const [key, groq2] of Object.entries(record ?? {})) sites.push({
|
|
2352
2535
|
groq: groq2,
|
|
2353
2536
|
path: [ ...spawnPath, group, key ],
|
|
2354
2537
|
label: `action "${action.name}".spawn.${group} "${key}"`,
|
|
2355
2538
|
policy: "triggered-payload",
|
|
2539
|
+
...bindsRow ? {
|
|
2540
|
+
bindsRow: !0
|
|
2541
|
+
} : {},
|
|
2356
2542
|
fields: fields
|
|
2357
2543
|
});
|
|
2358
2544
|
}
|
|
@@ -2426,7 +2612,7 @@ function checkStart(def, issues) {
|
|
|
2426
2612
|
if (def.start !== void 0 && (def.lifecycle === "child" && issues.push({
|
|
2427
2613
|
path: [ "start" ],
|
|
2428
2614
|
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'`"
|
|
2429
|
-
}), checkStartFilterReads(def, issues), def.start.kind === "autonomous")) for (const [n, entry] of (def.fields ?? []).entries()) entry.required !== !0 || isSubjectEntry(entry) || issues.push({
|
|
2615
|
+
}), 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({
|
|
2430
2616
|
path: [ "fields", n, "required" ],
|
|
2431
2617
|
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`
|
|
2432
2618
|
});
|
|
@@ -2434,15 +2620,43 @@ function checkStart(def, issues) {
|
|
|
2434
2620
|
|
|
2435
2621
|
function checkStartFilterReads(def, issues) {
|
|
2436
2622
|
const filter = def.start?.filter;
|
|
2437
|
-
|
|
2438
|
-
const bindable = new Set((def.fields ?? []).filter(isInputSourced).map(entry => entry.name)), declared = new Set((def.fields ?? []).map(entry => entry.name));
|
|
2439
|
-
for (const name of conditionFieldReadNames(filter)) bindable.has(name) || issues.push({
|
|
2623
|
+
filter !== void 0 && (conditionParameterNames(filter).has("fields") && issues.push({
|
|
2440
2624
|
path: [ "start", "filter" ],
|
|
2441
|
-
message:
|
|
2442
|
-
})
|
|
2443
|
-
readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
2625
|
+
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"
|
|
2626
|
+
}), readsRootDocument(filter) && !(def.fields ?? []).some(isSubjectEntry) && issues.push({
|
|
2444
2627
|
path: [ "start", "filter" ],
|
|
2445
|
-
message: "start.filter reads the candidate document (its root), but the definition declares no required subject entry (a required doc.ref / doc.refs input) — a read surface would never have a document to bind as root, so the filter
|
|
2628
|
+
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"
|
|
2629
|
+
}));
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
function checkStartAllowedReads(def, issues) {
|
|
2633
|
+
const allowed = def.start?.allowed;
|
|
2634
|
+
if (allowed === void 0) return;
|
|
2635
|
+
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())}`;
|
|
2636
|
+
for (const read of conditionFieldReads(allowed)) {
|
|
2637
|
+
const entry = bindable.get(read.name);
|
|
2638
|
+
if (entry === void 0) {
|
|
2639
|
+
issues.push({
|
|
2640
|
+
path: [ "start", "allowed" ],
|
|
2641
|
+
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}`
|
|
2642
|
+
});
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
pushFieldReadPathIssue({
|
|
2646
|
+
where: "start.allowed",
|
|
2647
|
+
read: {
|
|
2648
|
+
field: read.name,
|
|
2649
|
+
path: read.path
|
|
2650
|
+
},
|
|
2651
|
+
target: entry,
|
|
2652
|
+
path: [ "start", "allowed" ],
|
|
2653
|
+
issues: issues,
|
|
2654
|
+
nodes: START_ALLOWED_VALUE_NODES
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
readsRootDocument(allowed) && issues.push({
|
|
2658
|
+
path: [ "start", "allowed" ],
|
|
2659
|
+
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"
|
|
2446
2660
|
});
|
|
2447
2661
|
}
|
|
2448
2662
|
|
|
@@ -2553,39 +2767,80 @@ function checkGroupMembership({group: group, reachable: reachable, path: path, l
|
|
|
2553
2767
|
}
|
|
2554
2768
|
|
|
2555
2769
|
function checkConditionFieldReads(def, issues) {
|
|
2556
|
-
const everywhere =
|
|
2557
|
-
for (const site of conditionSites(def))
|
|
2770
|
+
const everywhere = allDeclaredFieldEntries(def);
|
|
2771
|
+
for (const site of conditionSites(def)) checkSiteFieldReads({
|
|
2558
2772
|
site: site,
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
}) || issues.push({
|
|
2562
|
-
path: site.path,
|
|
2563
|
-
message: undeclaredFieldReadMessage(site, name)
|
|
2773
|
+
everywhere: everywhere,
|
|
2774
|
+
issues: issues
|
|
2564
2775
|
});
|
|
2565
|
-
for (const [name, groq2] of Object.entries(def.predicates ?? {}))
|
|
2566
|
-
|
|
2567
|
-
|
|
2776
|
+
for (const [name, groq2] of Object.entries(def.predicates ?? {})) checkSiteFieldReads({
|
|
2777
|
+
site: {
|
|
2778
|
+
groq: groq2,
|
|
2779
|
+
path: [ "predicates", name ],
|
|
2780
|
+
label: `predicate "${name}"`,
|
|
2781
|
+
fields: "context-dependent"
|
|
2782
|
+
},
|
|
2783
|
+
everywhere: everywhere,
|
|
2784
|
+
issues: issues
|
|
2568
2785
|
});
|
|
2569
2786
|
}
|
|
2570
2787
|
|
|
2571
|
-
function
|
|
2572
|
-
const
|
|
2573
|
-
|
|
2574
|
-
|
|
2788
|
+
function checkSiteFieldReads({site: site, everywhere: everywhere, issues: issues}) {
|
|
2789
|
+
for (const read of conditionFieldReads(site.groq)) {
|
|
2790
|
+
const targets = readTargets({
|
|
2791
|
+
site: site,
|
|
2792
|
+
read: read,
|
|
2793
|
+
everywhere: everywhere
|
|
2794
|
+
});
|
|
2795
|
+
if (targets === "undeclared") {
|
|
2796
|
+
issues.push({
|
|
2797
|
+
path: site.path,
|
|
2798
|
+
message: undeclaredFieldReadMessage(site, read.name)
|
|
2799
|
+
});
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
const path = read.path;
|
|
2803
|
+
path !== void 0 && (targets.some(target => fieldValuePathIssue({
|
|
2804
|
+
target: target,
|
|
2805
|
+
path: path
|
|
2806
|
+
}) === void 0) || pushFieldReadPathIssue({
|
|
2807
|
+
where: site.label,
|
|
2808
|
+
read: {
|
|
2809
|
+
field: read.name,
|
|
2810
|
+
path: path
|
|
2811
|
+
},
|
|
2812
|
+
target: targets[0],
|
|
2813
|
+
path: site.path,
|
|
2814
|
+
issues: issues
|
|
2815
|
+
}));
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
function readTargets({site: site, read: read, everywhere: everywhere}) {
|
|
2820
|
+
if (site.fields === "context-dependent") return everywhere.get(read.name) ?? "undeclared";
|
|
2821
|
+
const layers = site.fields.filter(layer => layer.entries.has(read.name));
|
|
2822
|
+
if (layers.length === 0) return "undeclared";
|
|
2823
|
+
const entries = layers.map(layer => layer.entries.get(read.name));
|
|
2824
|
+
return site.unionWindow === !0 ? entries : [ entries[0] ];
|
|
2575
2825
|
}
|
|
2576
2826
|
|
|
2577
|
-
function
|
|
2578
|
-
|
|
2827
|
+
function allDeclaredFieldEntries(def) {
|
|
2828
|
+
const declared = /* @__PURE__ */ new Map;
|
|
2829
|
+
for (const {entries: entries} of fieldScopes(def)) for (const entry of entries ?? []) {
|
|
2830
|
+
const list = declared.get(entry.name);
|
|
2831
|
+
list === void 0 ? declared.set(entry.name, [ entry ]) : list.push(entry);
|
|
2832
|
+
}
|
|
2833
|
+
return declared;
|
|
2579
2834
|
}
|
|
2580
2835
|
|
|
2581
2836
|
function noFieldAnywhereMessage(label, name) {
|
|
2582
|
-
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
|
|
2837
|
+
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`;
|
|
2583
2838
|
}
|
|
2584
2839
|
|
|
2585
2840
|
function undeclaredFieldReadMessage(site, name) {
|
|
2586
2841
|
if (site.fields === "context-dependent") return noFieldAnywhereMessage(site.label, name);
|
|
2587
|
-
const visible = site.fields.flatMap(layer => [ ...layer.
|
|
2588
|
-
return `${site.label} reads $fields.${name}, but
|
|
2842
|
+
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})`;
|
|
2843
|
+
return `${site.label} reads $fields.${name}, but ${window} — the read evaluates to GROQ null, so the condition silently misevaluates. Visible: ${visible.join(", ") || "(none)"}`;
|
|
2589
2844
|
}
|
|
2590
2845
|
|
|
2591
2846
|
function checkFieldReadSeeds(def, issues) {
|
|
@@ -2687,7 +2942,7 @@ function checkFieldReadOpValues(def, issues) {
|
|
|
2687
2942
|
ops: site.ops,
|
|
2688
2943
|
path: site.path,
|
|
2689
2944
|
label: site.label,
|
|
2690
|
-
|
|
2945
|
+
activityEntries: entryMap(site.activity.fields)
|
|
2691
2946
|
});
|
|
2692
2947
|
}
|
|
2693
2948
|
|
|
@@ -2707,7 +2962,7 @@ function fieldReadsIn(value, path) {
|
|
|
2707
2962
|
} ] : value.type !== "object" ? [] : Object.entries(value.fields).flatMap(([key, sub]) => fieldReadsIn(sub, [ ...path, "fields", key ]));
|
|
2708
2963
|
}
|
|
2709
2964
|
|
|
2710
|
-
function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries,
|
|
2965
|
+
function checkOpFieldRead({read: read, where: where, path: path, workflowEntries: workflowEntries, stageEntries: stageEntries, activityEntries: activityEntries, issues: issues}) {
|
|
2711
2966
|
const hosts = [ {
|
|
2712
2967
|
scope: "stage",
|
|
2713
2968
|
entries: stageEntries
|
|
@@ -2722,7 +2977,7 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
|
|
|
2722
2977
|
read: read,
|
|
2723
2978
|
where: where,
|
|
2724
2979
|
hosts: hosts,
|
|
2725
|
-
|
|
2980
|
+
activityEntries: activityEntries
|
|
2726
2981
|
})
|
|
2727
2982
|
});
|
|
2728
2983
|
return;
|
|
@@ -2736,8 +2991,8 @@ function checkOpFieldRead({read: read, where: where, path: path, workflowEntries
|
|
|
2736
2991
|
});
|
|
2737
2992
|
}
|
|
2738
2993
|
|
|
2739
|
-
function opFieldReadMissMessage({read: read, where: where, hosts: hosts,
|
|
2740
|
-
const searched = read.scope === void 0 ? "stage or workflow scope" : `${read.scope} scope`, activityHint =
|
|
2994
|
+
function opFieldReadMissMessage({read: read, where: where, hosts: hosts, activityEntries: activityEntries}) {
|
|
2995
|
+
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}`));
|
|
2741
2996
|
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)"}`;
|
|
2742
2997
|
}
|
|
2743
2998
|
|
|
@@ -2845,11 +3100,14 @@ function checkGuardFieldRead({name: name, valuePath: valuePath, where: where, pa
|
|
|
2845
3100
|
});
|
|
2846
3101
|
}
|
|
2847
3102
|
|
|
2848
|
-
function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues}) {
|
|
3103
|
+
function pushFieldReadPathIssue({where: where, read: read, target: target, path: path, issues: issues, nodes: nodes}) {
|
|
2849
3104
|
if (read.path === void 0) return;
|
|
2850
3105
|
const pathIssue = fieldValuePathIssue({
|
|
2851
3106
|
target: target,
|
|
2852
|
-
path: read.path
|
|
3107
|
+
path: read.path,
|
|
3108
|
+
...nodes ? {
|
|
3109
|
+
nodes: nodes
|
|
3110
|
+
} : {}
|
|
2853
3111
|
});
|
|
2854
3112
|
pathIssue !== void 0 && issues.push({
|
|
2855
3113
|
path: path,
|
|
@@ -2920,22 +3178,26 @@ const SCALAR = {
|
|
|
2920
3178
|
kind: "rows",
|
|
2921
3179
|
of: shape.of ?? []
|
|
2922
3180
|
})
|
|
3181
|
+
}, START_ALLOWED_VALUE_NODES = {
|
|
3182
|
+
...VALUE_NODES,
|
|
3183
|
+
"doc.ref": () => GDR_VALUE
|
|
2923
3184
|
};
|
|
2924
3185
|
|
|
2925
|
-
function valueNodeFor(shape) {
|
|
2926
|
-
return Object.hasOwn(
|
|
3186
|
+
function valueNodeFor(shape, nodes) {
|
|
3187
|
+
return Object.hasOwn(nodes, shape.type) ? nodes[shape.type](shape) : void 0;
|
|
2927
3188
|
}
|
|
2928
3189
|
|
|
2929
3190
|
const INDEX_SEGMENT = /^\d+$/;
|
|
2930
3191
|
|
|
2931
|
-
function stepIntoValueNode(
|
|
3192
|
+
function stepIntoValueNode(args) {
|
|
3193
|
+
const {node: node, segment: segment, nodes: nodes} = args;
|
|
2932
3194
|
switch (node.kind) {
|
|
2933
3195
|
case "scalar":
|
|
2934
3196
|
return "the value is a scalar with no sub-paths";
|
|
2935
3197
|
|
|
2936
3198
|
case "object":
|
|
2937
3199
|
{
|
|
2938
|
-
const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub);
|
|
3200
|
+
const sub = node.fields.find(f => f.name === segment), next = sub === void 0 ? void 0 : valueNodeFor(sub, nodes);
|
|
2939
3201
|
return next !== void 0 ? next : `an object value has sub-fields ${knownList(node.fields.map(f => f.name))} — "${segment}" is not one`;
|
|
2940
3202
|
}
|
|
2941
3203
|
|
|
@@ -2956,18 +3218,24 @@ function stepIntoValueNode(node, segment) {
|
|
|
2956
3218
|
}
|
|
2957
3219
|
}
|
|
2958
3220
|
|
|
2959
|
-
function fieldValuePathIssue({target: target, path: path}) {
|
|
2960
|
-
let node = valueNodeFor(target);
|
|
3221
|
+
function fieldValuePathIssue({target: target, path: path, nodes: nodes = VALUE_NODES}) {
|
|
3222
|
+
let node = valueNodeFor(target, nodes);
|
|
2961
3223
|
if (node === void 0) return `"${String(target.type)}" is not a known field kind`;
|
|
2962
3224
|
for (const segment of path.split(".")) {
|
|
2963
|
-
const next = stepIntoValueNode(
|
|
3225
|
+
const next = stepIntoValueNode({
|
|
3226
|
+
node: node,
|
|
3227
|
+
segment: segment,
|
|
3228
|
+
nodes: nodes
|
|
3229
|
+
});
|
|
2964
3230
|
if (typeof next == "string") return `at segment "${segment}": ${next}`;
|
|
2965
3231
|
node = next;
|
|
2966
3232
|
}
|
|
2967
3233
|
}
|
|
2968
3234
|
|
|
2969
3235
|
function checkWorkflowInvariants(def) {
|
|
2970
|
-
const issues = []
|
|
3236
|
+
const issues = [];
|
|
3237
|
+
checkDefinitionName(def, issues);
|
|
3238
|
+
const stageNames = checkStages(def, issues);
|
|
2971
3239
|
return checkInitialStage({
|
|
2972
3240
|
def: def,
|
|
2973
3241
|
stageNames: stageNames,
|
|
@@ -2982,7 +3250,7 @@ function checkWorkflowInvariants(def) {
|
|
|
2982
3250
|
issues: issues
|
|
2983
3251
|
}), checkEffectNames(def, issues), checkGuardNames(def, issues), checkFieldEntryNames(def, issues),
|
|
2984
3252
|
checkRequiredField(def, issues), checkStart(def, issues), checkPredicates(def, issues),
|
|
2985
|
-
|
|
3253
|
+
checkUnboundConditionVars(def, issues), checkConditionFieldReads(def, issues), checkFieldReadSeeds(def, issues),
|
|
2986
3254
|
checkFieldReadOpValues(def, issues), checkUpdateWhereOps(def, issues), checkGuardFieldReads(def, issues),
|
|
2987
3255
|
checkAssigneesEntries(def, issues), checkActivityTerminalPaths(def, issues), checkTerminalStageActivities(def, issues),
|
|
2988
3256
|
checkTriggeredActionParams(def, issues), checkStoredRolesPlacement(def, issues),
|
|
@@ -2991,8 +3259,12 @@ function checkWorkflowInvariants(def) {
|
|
|
2991
3259
|
|
|
2992
3260
|
exports.ACTIVITY_KINDS = ACTIVITY_KINDS;
|
|
2993
3261
|
|
|
3262
|
+
exports.ACTIVITY_STATUSES = ACTIVITY_STATUSES;
|
|
3263
|
+
|
|
2994
3264
|
exports.ACTOR_KINDS = ACTOR_KINDS;
|
|
2995
3265
|
|
|
3266
|
+
exports.ActorShape = ActorShape;
|
|
3267
|
+
|
|
2996
3268
|
exports.AuthoringActionSchema = AuthoringActionSchema;
|
|
2997
3269
|
|
|
2998
3270
|
exports.AuthoringActivitySchema = AuthoringActivitySchema;
|
|
@@ -3035,6 +3307,10 @@ exports.EffectSchema = EffectSchema;
|
|
|
3035
3307
|
|
|
3036
3308
|
exports.FIELD_READ = FIELD_READ;
|
|
3037
3309
|
|
|
3310
|
+
exports.FIELD_SCOPES = FIELD_SCOPES;
|
|
3311
|
+
|
|
3312
|
+
exports.FIELD_VALUE_KINDS = FIELD_VALUE_KINDS;
|
|
3313
|
+
|
|
3038
3314
|
exports.FILTER_SCOPE_VARS = FILTER_SCOPE_VARS;
|
|
3039
3315
|
|
|
3040
3316
|
exports.FieldValueShapeError = FieldValueShapeError;
|
|
@@ -3043,14 +3319,26 @@ exports.GROUP_KINDS = GROUP_KINDS;
|
|
|
3043
3319
|
|
|
3044
3320
|
exports.GUARD_PREDICATE_VARS = GUARD_PREDICATE_VARS;
|
|
3045
3321
|
|
|
3322
|
+
exports.GdrShape = GdrShape;
|
|
3323
|
+
|
|
3046
3324
|
exports.GroupSchema = GroupSchema;
|
|
3047
3325
|
|
|
3048
3326
|
exports.InstanceNotFoundError = InstanceNotFoundError;
|
|
3049
3327
|
|
|
3328
|
+
exports.IsoTimestamp = IsoTimestamp;
|
|
3329
|
+
|
|
3330
|
+
exports.MUTATION_GUARD_ACTIONS = MUTATION_GUARD_ACTIONS;
|
|
3331
|
+
|
|
3332
|
+
exports.NonEmptyString = NonEmptyString;
|
|
3333
|
+
|
|
3334
|
+
exports.PersistedDocShapeError = PersistedDocShapeError;
|
|
3335
|
+
|
|
3050
3336
|
exports.RESERVED_CONDITION_VARS = RESERVED_CONDITION_VARS;
|
|
3051
3337
|
|
|
3052
3338
|
exports.RESOURCE_ALIAS_NAME_SOURCE = RESOURCE_ALIAS_NAME_SOURCE;
|
|
3053
3339
|
|
|
3340
|
+
exports.START_ALLOWED_VARS = START_ALLOWED_VARS;
|
|
3341
|
+
|
|
3054
3342
|
exports.START_FILTER_VARS = START_FILTER_VARS;
|
|
3055
3343
|
|
|
3056
3344
|
exports.StoredFieldOpSchema = StoredFieldOpSchema;
|
|
@@ -3101,6 +3389,8 @@ exports.evaluatePredicates = evaluatePredicates;
|
|
|
3101
3389
|
|
|
3102
3390
|
exports.extractDocumentId = extractDocumentId;
|
|
3103
3391
|
|
|
3392
|
+
exports.fieldValueSchemas = fieldValueSchemas;
|
|
3393
|
+
|
|
3104
3394
|
exports.formatIssuePath = formatIssuePath;
|
|
3105
3395
|
|
|
3106
3396
|
exports.formatIssues = formatIssues;
|
|
@@ -3133,6 +3423,8 @@ exports.isInputSourced = isInputSourced;
|
|
|
3133
3423
|
|
|
3134
3424
|
exports.isNotesEntry = isNotesEntry;
|
|
3135
3425
|
|
|
3426
|
+
exports.isParseableInstant = isParseableInstant;
|
|
3427
|
+
|
|
3136
3428
|
exports.isStartableDefinition = isStartableDefinition;
|
|
3137
3429
|
|
|
3138
3430
|
exports.isSubjectEntry = isSubjectEntry;
|
|
@@ -3151,6 +3443,8 @@ exports.parseGdr = parseGdr;
|
|
|
3151
3443
|
|
|
3152
3444
|
exports.parseOrThrow = parseOrThrow;
|
|
3153
3445
|
|
|
3446
|
+
exports.parsePersistedDoc = parsePersistedDoc;
|
|
3447
|
+
|
|
3154
3448
|
exports.parseResourceGdr = parseResourceGdr;
|
|
3155
3449
|
|
|
3156
3450
|
exports.parseStoredDefinition = parseStoredDefinition;
|
|
@@ -3187,6 +3481,8 @@ exports.runGroq = runGroq;
|
|
|
3187
3481
|
|
|
3188
3482
|
exports.sameResource = sameResource;
|
|
3189
3483
|
|
|
3484
|
+
exports.schemaTreeShape = schemaTreeShape;
|
|
3485
|
+
|
|
3190
3486
|
exports.selfGdr = selfGdr;
|
|
3191
3487
|
|
|
3192
3488
|
exports.startKindOf = startKindOf;
|
|
@@ -3197,6 +3493,10 @@ exports.toBareId = toBareId;
|
|
|
3197
3493
|
|
|
3198
3494
|
exports.toPhysicalGdr = toPhysicalGdr;
|
|
3199
3495
|
|
|
3496
|
+
exports.tolerantEntries = tolerantEntries;
|
|
3497
|
+
|
|
3498
|
+
exports.tolerantObject = tolerantObject;
|
|
3499
|
+
|
|
3200
3500
|
exports.tryParseGdr = tryParseGdr;
|
|
3201
3501
|
|
|
3202
3502
|
exports.validateFieldAppendItem = validateFieldAppendItem;
|