@sanity/workflow-engine 0.18.0 → 0.19.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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # @sanity/workflow-engine
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 24b11dd: Effect handlers can report intermediate field state mid-dispatch. The handler context gains `ctx.commitOps({ops, idempotencyKey})` — field ops only, validated like completion ops, executed through the new `workflow.commitEffectOps` verb as a standard engine commit (gate, history, idempotency ledger, compare-and-swap persist, guard refresh, cascade) — and `ctx.setProgress(field, value)`, sugar that coalesces pending absolute sets per field. Calls enqueue synchronously into a bounded per-dispatch queue (depth 32, 200 commits per dispatch) that serializes commits in call order; overflow throws synchronously at the call site, and settlement drains the queue before completion so an un-awaited failed report still fails the effect. Pending-effect claims now carry a `claimToken` minted on every claim and takeover: a report from an expired, released, or superseded dispatch is rejected before writing (`StaleEffectClaimError`), and a successful report renews the claim's lease. Completion semantics are unchanged and remain claim-blind first-writer-wins. Each report emits the sampled `Editorial Workflows Effect State Reported` telemetry event (adoption signal, max one per minute). Persisted-model note: the claim token rides persisted model 3 additively (see `DATAMODEL.md`).
8
+ - 2005ab7: Action evaluations carry a `firing` consequence (`FiringConsequence` `{exitsStage, transition?}`): what firing the action would do to the flow right now, computed by replaying the fire in memory through the same machinery a real commit runs — ops, the triggered fixpoint, then transition selection — against the projection's state. The answer is total for the current state and conditional on the commit landing; it is omitted where the replay can't be faithful (caller params, a spawn's dataset-driven fan-out in the hop, a terminal instance, a stage visit whose entries have diverged from the pinned definition) and on cascade-fired actions. Advisory, like every derived verdict. Wrong-shape `field.updateWhere` aborts (a merge resolving to a non-object; a resolved merge carrying reserved row keys) now throw the typed `FieldValueShapeError` on the commit path, joining the other wrong-shape write aborts.
9
+ - 24b11dd: Add the `progress` field kind: an elevated `number` carrying application-defined 0–100 completion, so Studio and SDK surfaces can identify and elevate it without guessing which number field means "how far along". Every non-null value must be finite and within 0–100 inclusive (fractions allowed), enforced by one value schema at every write boundary — declaration seeds, start values, edits, ops, nested sub-fields, and effect outputs. Choice lists are rejected on the kind at authoring time. Mints persisted model 3 additively (see `DATAMODEL.md`).
10
+ - 59b40fd: **BREAKING:** a deployment's `name` is now its identity in `defineWorkflowConfig`: enforced unique across the config and constrained to the same grammar as `tag` (lowercase letters, digits, dashes — free text like "My cool workflow" is rejected). Tags may now repeat across deployments; the enforced storage invariant is instead the `(workflowResource, tag)` pair — two deployments sharing both are rejected with each entry named, since they would write into the same partition and fight over definition versions.
11
+
3
12
  ## 0.18.0
4
13
 
5
14
  ### Minor Changes
package/DATAMODEL.md CHANGED
@@ -445,6 +445,59 @@ Spread-based mutation copies preserve `validation` across every full persist;
445
445
  the derived floor prevents older writers from reaching constrained model-2
446
446
  instances.
447
447
 
448
+ ### Model 3 — the `progress` field kind + effect claim tokens (reader floor: 2)
449
+
450
+ Two additive growths ship together as model 3 — model 2 was released (npm
451
+ `@sanity/workflow-engine@0.18.0`) before either landed, so they mint the next
452
+ version rather than amending a shipped one. Neither raises the reader floor;
453
+ the writer maximum stays 2.
454
+
455
+ **The `progress` field kind.** An elevated `number` (same stored numeric
456
+ value) carrying application-defined 0–100 completion, so surfaces can
457
+ identify and elevate it without guessing which number field means "how far
458
+ along". Every non-null value is finite and within 0–100 inclusive (fractions
459
+ allowed), enforced at every write boundary by the kind's one value schema.
460
+ Model 2's `validation` tree composes with it as NARROWING only: each declared
461
+ bound must itself sit within 0–100, gated at authoring. Both stamped trees
462
+ grow the value:
463
+
464
+ - **Definition tree** — `fields[].type` (and nested `FieldShape.type`)
465
+ admits `'progress'`.
466
+ - **Instance tree** — resolved `fields[]` entries admit `_type: 'progress'`
467
+ (the same arm shape as `number`).
468
+
469
+ Manifest feature: `progress-field-kind` (definition + instance, additive,
470
+ detectable, floor 0).
471
+
472
+ Would an old reader misread the kind (rule 5)? No — the same loud refusal as
473
+ the `subject` kind: an older engine (model 2 and below) that loaded a
474
+ progress-carrying instance fails the strict `fields[]` variant with
475
+ `PersistedDocShapeError` before interpreting anything, and cannot deploy or
476
+ evaluate a progress-declaring definition. No older code path can write a
477
+ progress field while skipping its bound, so there is no silent bypass to
478
+ fence — the kind leaves the floor at 0.
479
+
480
+ **Effect claim tokens.** `pendingEffects[].claim` grows an optional
481
+ `claimToken` — an opaque identifier minted fresh on every claim and
482
+ takeover. It is the exact-claim identity a mid-dispatch state report
483
+ (`commitEffectOps`) must present: `effectKey` cannot serve because a lease
484
+ takeover keeps the same key. A successful report also renews the claim's
485
+ `leaseExpiresAt` in the same commit.
486
+
487
+ Manifest feature: `effect-claim-tokens` (instance, additive, detectable,
488
+ floor 0).
489
+
490
+ Tolerant read: an older engine's claim parse admits unknown keys, so the
491
+ token rides through untouched and means nothing to it — older claim
492
+ semantics (presence + lease) are unchanged. Round-trip survival: full
493
+ persists derive from the complete loaded post-state, so an older writer
494
+ carries the token along; the claim-REPLACING writes (takeover, sweep
495
+ release, completion drain) discard it deliberately, which is the token's
496
+ own lifecycle — a report presenting a discarded token is rejected before
497
+ writing, failing closed. A claim without a token (older writer's takeover)
498
+ simply cannot authorise mid-dispatch reports; dispatch and completion are
499
+ unaffected.
500
+
448
501
  ## Pending governed changes
449
502
 
450
503
  - **`temp.system.guard` → `system.guard`** — the guard doc type's `temp.`
@@ -129,7 +129,7 @@ function parentRef(instance) {
129
129
  return instance.ancestors.at(-1);
130
130
  }
131
131
 
132
- const DATA_MODEL_VERSION = 2, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
132
+ const DATA_MODEL_VERSION = 3, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
133
133
 
134
134
  class ReaderModelAcknowledgementError extends WorkflowError {
135
135
  code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
@@ -188,6 +188,22 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
188
188
  compatibility: "reader-floor",
189
189
  applicability: "detectable",
190
190
  summary: "String, text, and number values may carry persisted inclusive bounds."
191
+ }), Object.freeze({
192
+ id: "progress-field-kind",
193
+ introducedInModel: 3,
194
+ minReaderModel: 0,
195
+ documentTypes: Object.freeze([ "definition", "instance" ]),
196
+ compatibility: "additive",
197
+ applicability: "detectable",
198
+ summary: "A progress field kind carries application-defined 0–100 completion."
199
+ }), Object.freeze({
200
+ id: "effect-claim-tokens",
201
+ introducedInModel: 3,
202
+ minReaderModel: 0,
203
+ documentTypes: Object.freeze([ "instance" ]),
204
+ compatibility: "additive",
205
+ applicability: "detectable",
206
+ summary: "Pending-effect claims carry an exact-claim token gating mid-dispatch state reports."
191
207
  }) ]);
192
208
 
193
209
  function recordOf(value) {
@@ -224,8 +240,8 @@ function hasChoiceList(document) {
224
240
  });
225
241
  }
226
242
 
227
- function hasSubjectField(document) {
228
- return persistedFieldEntries(document).some(entry => entry.type === "subject" || entry._type === "subject");
243
+ function hasFieldKind(document, kind) {
244
+ return persistedFieldEntries(document).some(entry => entry.type === kind || entry._type === kind);
229
245
  }
230
246
 
231
247
  function hasActionSemantics(document) {
@@ -240,12 +256,22 @@ function hasScalarValidation(document) {
240
256
  });
241
257
  }
242
258
 
259
+ function hasClaimTokens(document) {
260
+ const root = recordOf(document);
261
+ return root === void 0 ? !1 : recordsAt(root, "pendingEffects").some(entry => {
262
+ const claim = recordOf(entry.claim);
263
+ return claim !== void 0 && typeof claim.claimToken == "string";
264
+ });
265
+ }
266
+
243
267
  const featureDetectors = {
244
268
  "governed-model-stamps": () => !0,
245
- "subject-field-kind": hasSubjectField,
269
+ "subject-field-kind": document => hasFieldKind(document, "subject"),
246
270
  "typed-scalar-choice-lists": hasChoiceList,
247
271
  "action-semantics": hasActionSemantics,
248
- "inclusive-scalar-bounds": hasScalarValidation
272
+ "inclusive-scalar-bounds": hasScalarValidation,
273
+ "progress-field-kind": document => hasFieldKind(document, "progress"),
274
+ "effect-claim-tokens": hasClaimTokens
249
275
  };
250
276
 
251
277
  function requiredModelFeatures(documentType, document) {
@@ -547,7 +573,13 @@ function asPredicate(validate) {
547
573
  };
548
574
  }
549
575
 
550
- const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v__namespace.variant("type", [ v__namespace.object({
576
+ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts);
577
+
578
+ function lakeSegment(label) {
579
+ return v__namespace.pipe(v__namespace.string(), v__namespace.nonEmpty(), v__namespace.check(isValidTag, `invalid ${label} — ${LAKE_ID_SEGMENT_GLOSS}`));
580
+ }
581
+
582
+ const WorkflowResourceSchema = v__namespace.variant("type", [ v__namespace.object({
551
583
  type: v__namespace.literal("dataset"),
552
584
  id: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
553
585
  }), v__namespace.object({
@@ -563,14 +595,46 @@ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(vali
563
595
  name: v__namespace.pipe(NonEmptyString$1, v__namespace.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
564
596
  resource: WorkflowResourceSchema
565
597
  }), 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({
566
- name: NonEmptyString$1,
598
+ name: lakeSegment("name"),
567
599
  expectedMinReaderModel: v__namespace.optional(v__namespace.custom(() => !0), void 0),
568
- 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")),
600
+ tag: lakeSegment("tag"),
569
601
  workflowResource: WorkflowResourceSchema,
570
- 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"))),
602
+ resourceAliases: v__namespace.optional(v__namespace.pipe(v__namespace.array(ResourceBindingSchema), v__namespace.check(bindings => duplicateHandleMessage(bindings) === void 0, issue => duplicateHandleMessage(issue.input) ?? "duplicate resource handle name"))),
571
603
  definitions: v__namespace.pipe(v__namespace.array(DefinitionSchema), v__namespace.minLength(1, "a deployment needs at least one definition"))
572
- }), TelemetryLoggerSchema = v__namespace.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v__namespace.object({
573
- deployments: v__namespace.pipe(v__namespace.array(DeploymentSchema), v__namespace.minLength(1, "a config needs at least one deployment"), v__namespace.check(deployments => new Set(deployments.map(deployment => deployment.tag)).size === deployments.length, "duplicate deployment tag — each deployment must use a unique tag")),
604
+ });
605
+
606
+ function firstDuplicatePair(items, keyOf) {
607
+ const seen = /* @__PURE__ */ new Map;
608
+ for (const item of items) {
609
+ const key = keyOf(item), earlier = seen.get(key);
610
+ if (earlier !== void 0) return [ earlier, item ];
611
+ seen.set(key, item);
612
+ }
613
+ }
614
+
615
+ function duplicateHandleMessage(bindings) {
616
+ const pair = firstDuplicatePair(bindings, binding => binding.name);
617
+ if (pair !== void 0) return `duplicate resource handle name "${pair[1].name}" — each binding name must be unique within a deployment`;
618
+ }
619
+
620
+ function duplicateNameMessage(deployments) {
621
+ const pair = firstDuplicatePair(deployments, deployment => deployment.name);
622
+ if (pair !== void 0) return `duplicate deployment name "${pair[1].name}" — each deployment must use a unique name`;
623
+ }
624
+
625
+ function partitionKey(deployment) {
626
+ return `${resourceGdr(deployment.workflowResource)} ${deployment.tag}`;
627
+ }
628
+
629
+ function partitionCollisionMessage(deployments) {
630
+ const pair = firstDuplicatePair(deployments, partitionKey);
631
+ if (pair === void 0) return;
632
+ const [first, second] = pair;
633
+ return `deployments "${first.name}" and "${second.name}" share workflow resource + tag "${second.tag}" — both would write into the same partition; change one deployment’s tag or resource`;
634
+ }
635
+
636
+ const TelemetryLoggerSchema = v__namespace.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v__namespace.object({
637
+ deployments: v__namespace.pipe(v__namespace.array(DeploymentSchema), v__namespace.minLength(1, "a config needs at least one deployment"), v__namespace.check(deployments => duplicateNameMessage(deployments) === void 0, issue => duplicateNameMessage(issue.input) ?? "duplicate deployment name"), v__namespace.check(deployments => partitionCollisionMessage(deployments) === void 0, issue => partitionCollisionMessage(issue.input) ?? "duplicate deployment partition")),
574
638
  telemetry: v__namespace.optional(TelemetryLoggerSchema)
575
639
  });
576
640
 
@@ -1724,7 +1788,7 @@ const GdrUriSchema = v__namespace.custom(s => typeof s == "string" && isGdrUri(s
1724
1788
  }), tolerantObject()({
1725
1789
  type: v__namespace.literal("role"),
1726
1790
  role: NonEmptyString
1727
- }) ]), 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, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
1791
+ }) ]), 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() ]), NullableProgress = v__namespace.union([ v__namespace.null(), v__namespace.pipe(v__namespace.number(), v__namespace.finite("progress must be a finite number"), v__namespace.minValue(0, "progress must be at least 0"), v__namespace.maxValue(100, "progress must be at most 100")) ]), 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, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
1728
1792
 
1729
1793
  function normalizedChoiceKind(kind) {
1730
1794
  return kind === "dateTime" ? "datetime" : kind;
@@ -1756,6 +1820,7 @@ const fieldValueSchemas = {
1756
1820
  string: NullableString,
1757
1821
  text: NullableString,
1758
1822
  number: NullableNumber,
1823
+ progress: NullableProgress,
1759
1824
  boolean: NullableBoolean,
1760
1825
  date: NullableDate,
1761
1826
  datetime: NullableDateTime,
@@ -1780,13 +1845,13 @@ function shapeValueSchema(shape, leaf) {
1780
1845
  }
1781
1846
 
1782
1847
  function scalarMeasurement(entryType, value) {
1783
- if (entryType === "number" && typeof value == "number") return value;
1848
+ if ((entryType === "number" || entryType === "progress") && typeof value == "number") return value;
1784
1849
  if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
1785
1850
  }
1786
1851
 
1787
1852
  function scalarBoundIssue(args) {
1788
1853
  const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
1789
- return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
1854
+ return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" || entryType === "progress" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
1790
1855
  }
1791
1856
 
1792
1857
  function scalarValidationIssues(args) {
@@ -2079,7 +2144,7 @@ function groupMembershipNames(group) {
2079
2144
  return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
2080
2145
  }
2081
2146
 
2082
- const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v__namespace.pipe(v__namespace.number(), v__namespace.finite("must be finite")), ScalarValidationSchema = v__namespace.pipe(v__namespace.strictObject({
2147
+ const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "progress", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v__namespace.pipe(v__namespace.number(), v__namespace.finite("must be finite")), ScalarValidationSchema = v__namespace.pipe(v__namespace.strictObject({
2083
2148
  min: v__namespace.optional(FiniteNumber),
2084
2149
  max: v__namespace.optional(FiniteNumber)
2085
2150
  }), v__namespace.check(validation => validation.min !== void 0 || validation.max !== void 0, "declare at least one bound, or omit `validation`"), v__namespace.check(validation => validation.min === void 0 || validation.max === void 0 || validation.min <= validation.max, "`min` must be less than or equal to `max`")), ChoiceOptionsSchema = v__namespace.strictObject({
@@ -2188,7 +2253,7 @@ function choiceOptionsCheck() {
2188
2253
  }) ?? []).join("; "));
2189
2254
  }
2190
2255
 
2191
- const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number" ]);
2256
+ const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "progress" ]);
2192
2257
 
2193
2258
  function scalarValidationCheck() {
2194
2259
  return v__namespace.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
@@ -2197,7 +2262,11 @@ function scalarValidationCheck() {
2197
2262
  function scalarValidationDeclarationIssues(entry) {
2198
2263
  const {type: type, validation: validation} = entry;
2199
2264
  if (validation === void 0) return;
2200
- if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` values, not "${type}"` ];
2265
+ if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` / \`progress\` values, not "${type}"` ];
2266
+ if (type === "progress") {
2267
+ const issues2 = Object.entries(validation).flatMap(([bound, value]) => typeof value == "number" && value >= 0 && value <= 100 ? [] : [ `\`validation.${bound}\` must stay within the progress kind's 0–100 contract` ]);
2268
+ return issues2.length === 0 ? void 0 : issues2;
2269
+ }
2201
2270
  if (type === "number") return;
2202
2271
  const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
2203
2272
  return issues.length === 0 ? void 0 : issues;
@@ -3608,6 +3677,7 @@ const SCALAR = {
3608
3677
  string: () => SCALAR,
3609
3678
  text: () => SCALAR,
3610
3679
  number: () => SCALAR,
3680
+ progress: () => SCALAR,
3611
3681
  boolean: () => SCALAR,
3612
3682
  date: () => SCALAR,
3613
3683
  datetime: () => SCALAR,
@@ -113,7 +113,7 @@ function parentRef(instance) {
113
113
  return instance.ancestors.at(-1);
114
114
  }
115
115
 
116
- const DATA_MODEL_VERSION = 2, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
116
+ const DATA_MODEL_VERSION = 3, DATA_MODEL_MIN_READER = 2, READER_MODEL_ROLLOUT_URL = "https://github.com/sanity-io/workflows/blob/main/docs/reader-model-rollout.md";
117
117
 
118
118
  class ReaderModelAcknowledgementError extends WorkflowError {
119
119
  code="WORKFLOW_READER_MODEL_ACKNOWLEDGEMENT_MISMATCH";
@@ -172,6 +172,22 @@ const DATA_MODEL_CHANGES = Object.freeze([ Object.freeze({
172
172
  compatibility: "reader-floor",
173
173
  applicability: "detectable",
174
174
  summary: "String, text, and number values may carry persisted inclusive bounds."
175
+ }), Object.freeze({
176
+ id: "progress-field-kind",
177
+ introducedInModel: 3,
178
+ minReaderModel: 0,
179
+ documentTypes: Object.freeze([ "definition", "instance" ]),
180
+ compatibility: "additive",
181
+ applicability: "detectable",
182
+ summary: "A progress field kind carries application-defined 0–100 completion."
183
+ }), Object.freeze({
184
+ id: "effect-claim-tokens",
185
+ introducedInModel: 3,
186
+ minReaderModel: 0,
187
+ documentTypes: Object.freeze([ "instance" ]),
188
+ compatibility: "additive",
189
+ applicability: "detectable",
190
+ summary: "Pending-effect claims carry an exact-claim token gating mid-dispatch state reports."
175
191
  }) ]);
176
192
 
177
193
  function recordOf(value) {
@@ -208,8 +224,8 @@ function hasChoiceList(document) {
208
224
  });
209
225
  }
210
226
 
211
- function hasSubjectField(document) {
212
- return persistedFieldEntries(document).some(entry => entry.type === "subject" || entry._type === "subject");
227
+ function hasFieldKind(document, kind) {
228
+ return persistedFieldEntries(document).some(entry => entry.type === kind || entry._type === kind);
213
229
  }
214
230
 
215
231
  function hasActionSemantics(document) {
@@ -224,12 +240,22 @@ function hasScalarValidation(document) {
224
240
  });
225
241
  }
226
242
 
243
+ function hasClaimTokens(document) {
244
+ const root = recordOf(document);
245
+ return root === void 0 ? !1 : recordsAt(root, "pendingEffects").some(entry => {
246
+ const claim = recordOf(entry.claim);
247
+ return claim !== void 0 && typeof claim.claimToken == "string";
248
+ });
249
+ }
250
+
227
251
  const featureDetectors = {
228
252
  "governed-model-stamps": () => !0,
229
- "subject-field-kind": hasSubjectField,
253
+ "subject-field-kind": document => hasFieldKind(document, "subject"),
230
254
  "typed-scalar-choice-lists": hasChoiceList,
231
255
  "action-semantics": hasActionSemantics,
232
- "inclusive-scalar-bounds": hasScalarValidation
256
+ "inclusive-scalar-bounds": hasScalarValidation,
257
+ "progress-field-kind": document => hasFieldKind(document, "progress"),
258
+ "effect-claim-tokens": hasClaimTokens
233
259
  };
234
260
 
235
261
  function requiredModelFeatures(documentType, document) {
@@ -531,7 +557,13 @@ function asPredicate(validate) {
531
557
  };
532
558
  }
533
559
 
534
- const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts), WorkflowResourceSchema = v.variant("type", [ v.object({
560
+ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(validateResourceAliasName), isValidDatasetId = asPredicate(datasetResourceParts);
561
+
562
+ function lakeSegment(label) {
563
+ return v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, `invalid ${label} — ${LAKE_ID_SEGMENT_GLOSS}`));
564
+ }
565
+
566
+ const WorkflowResourceSchema = v.variant("type", [ v.object({
535
567
  type: v.literal("dataset"),
536
568
  id: v.pipe(NonEmptyString$1, v.check(isValidDatasetId, 'invalid dataset resource id — expected "<projectId>.<dataset>"'))
537
569
  }), v.object({
@@ -547,14 +579,46 @@ const isValidTag = asPredicate(validateTag), isValidAliasName = asPredicate(vali
547
579
  name: v.pipe(NonEmptyString$1, v.check(isValidAliasName, "invalid resource handle name — lowercase letters, digits and dashes only, no leading dash")),
548
580
  resource: WorkflowResourceSchema
549
581
  }), DefinitionSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.name == "string", "expected a workflow definition (an object with a string `name`)"), DeploymentSchema = v.object({
550
- name: NonEmptyString$1,
582
+ name: lakeSegment("name"),
551
583
  expectedMinReaderModel: v.optional(v.custom(() => !0), void 0),
552
- tag: v.pipe(v.string(), v.nonEmpty(), v.check(isValidTag, "invalid tag — lowercase letters, digits and dashes only, no leading dash, no dots")),
584
+ tag: lakeSegment("tag"),
553
585
  workflowResource: WorkflowResourceSchema,
554
- resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => new Set(bindings.map(binding => binding.name)).size === bindings.length, "duplicate resource handle name — each binding name must be unique within a deployment"))),
586
+ resourceAliases: v.optional(v.pipe(v.array(ResourceBindingSchema), v.check(bindings => duplicateHandleMessage(bindings) === void 0, issue => duplicateHandleMessage(issue.input) ?? "duplicate resource handle name"))),
555
587
  definitions: v.pipe(v.array(DefinitionSchema), v.minLength(1, "a deployment needs at least one definition"))
556
- }), TelemetryLoggerSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v.object({
557
- deployments: v.pipe(v.array(DeploymentSchema), v.minLength(1, "a config needs at least one deployment"), v.check(deployments => new Set(deployments.map(deployment => deployment.tag)).size === deployments.length, "duplicate deployment tag — each deployment must use a unique tag")),
588
+ });
589
+
590
+ function firstDuplicatePair(items, keyOf) {
591
+ const seen = /* @__PURE__ */ new Map;
592
+ for (const item of items) {
593
+ const key = keyOf(item), earlier = seen.get(key);
594
+ if (earlier !== void 0) return [ earlier, item ];
595
+ seen.set(key, item);
596
+ }
597
+ }
598
+
599
+ function duplicateHandleMessage(bindings) {
600
+ const pair = firstDuplicatePair(bindings, binding => binding.name);
601
+ if (pair !== void 0) return `duplicate resource handle name "${pair[1].name}" — each binding name must be unique within a deployment`;
602
+ }
603
+
604
+ function duplicateNameMessage(deployments) {
605
+ const pair = firstDuplicatePair(deployments, deployment => deployment.name);
606
+ if (pair !== void 0) return `duplicate deployment name "${pair[1].name}" — each deployment must use a unique name`;
607
+ }
608
+
609
+ function partitionKey(deployment) {
610
+ return `${resourceGdr(deployment.workflowResource)} ${deployment.tag}`;
611
+ }
612
+
613
+ function partitionCollisionMessage(deployments) {
614
+ const pair = firstDuplicatePair(deployments, partitionKey);
615
+ if (pair === void 0) return;
616
+ const [first, second] = pair;
617
+ return `deployments "${first.name}" and "${second.name}" share workflow resource + tag "${second.tag}" — both would write into the same partition; change one deployment’s tag or resource`;
618
+ }
619
+
620
+ const TelemetryLoggerSchema = v.custom(input => typeof input == "object" && input !== null && typeof input.log == "function", "expected a telemetry logger (an object with a `log` function)"), WorkflowConfigSchema = v.object({
621
+ deployments: v.pipe(v.array(DeploymentSchema), v.minLength(1, "a config needs at least one deployment"), v.check(deployments => duplicateNameMessage(deployments) === void 0, issue => duplicateNameMessage(issue.input) ?? "duplicate deployment name"), v.check(deployments => partitionCollisionMessage(deployments) === void 0, issue => partitionCollisionMessage(issue.input) ?? "duplicate deployment partition")),
558
622
  telemetry: v.optional(TelemetryLoggerSchema)
559
623
  });
560
624
 
@@ -1708,7 +1772,7 @@ const GdrUriSchema = v.custom(s => typeof s == "string" && isGdrUri(s), "must be
1708
1772
  }), tolerantObject()({
1709
1773
  type: v.literal("role"),
1710
1774
  role: NonEmptyString
1711
- }) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
1775
+ }) ]), NullableString = v.union([ v.null(), v.string() ]), NullableNumber = v.union([ v.null(), v.number() ]), NullableBoolean = v.union([ v.null(), v.boolean() ]), NullableProgress = v.union([ v.null(), v.pipe(v.number(), v.finite("progress must be a finite number"), v.minValue(0, "progress must be at least 0"), v.maxValue(100, "progress must be at most 100")) ]), NullableDateTime = v.union([ v.null(), IsoTimestamp ]), NullableDate = v.union([ v.null(), v.pipe(v.string(), v.regex(/^\d{4}-\d{2}-\d{2}$/, "must be a `YYYY-MM-DD` date")) ]), NullableUrl = NullableString, CHOICE_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "url", "date", "datetime", "dateTime" ]);
1712
1776
 
1713
1777
  function normalizedChoiceKind(kind) {
1714
1778
  return kind === "dateTime" ? "datetime" : kind;
@@ -1740,6 +1804,7 @@ const fieldValueSchemas = {
1740
1804
  string: NullableString,
1741
1805
  text: NullableString,
1742
1806
  number: NullableNumber,
1807
+ progress: NullableProgress,
1743
1808
  boolean: NullableBoolean,
1744
1809
  date: NullableDate,
1745
1810
  datetime: NullableDateTime,
@@ -1764,13 +1829,13 @@ function shapeValueSchema(shape, leaf) {
1764
1829
  }
1765
1830
 
1766
1831
  function scalarMeasurement(entryType, value) {
1767
- if (entryType === "number" && typeof value == "number") return value;
1832
+ if ((entryType === "number" || entryType === "progress") && typeof value == "number") return value;
1768
1833
  if ((entryType === "string" || entryType === "text") && typeof value == "string") return value.length;
1769
1834
  }
1770
1835
 
1771
1836
  function scalarBoundIssue(args) {
1772
1837
  const {entryType: entryType, measured: measured, bound: bound, limit: limit} = args;
1773
- return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
1838
+ return bound === void 0 || limit === "min" && measured >= bound || limit === "max" && measured <= bound ? void 0 : `${entryType === "number" || entryType === "progress" ? "" : "length "}must be ${limit === "min" ? "greater than or equal to" : "less than or equal to"} ${bound}`;
1774
1839
  }
1775
1840
 
1776
1841
  function scalarValidationIssues(args) {
@@ -2063,7 +2128,7 @@ function groupMembershipNames(group) {
2063
2128
  return group === void 0 ? [] : typeof group == "string" ? [ group ] : [ ...group ];
2064
2129
  }
2065
2130
 
2066
- const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v.pipe(v.number(), v.finite("must be finite")), ScalarValidationSchema = v.pipe(v.strictObject({
2131
+ const FIELD_VALUE_KINDS = [ "doc.ref", "doc.refs", "subject", "release.ref", "string", "text", "number", "progress", "boolean", "date", "datetime", "url", "actor", "assignee", "assignees", "object", "array" ], FieldValueKindSchema = picklist(FIELD_VALUE_KINDS), FieldKindSchema = picklist(FIELD_VALUE_KINDS), FieldEntryName = groqIdentifier("`$fields.<name>`"), FiniteNumber = v.pipe(v.number(), v.finite("must be finite")), ScalarValidationSchema = v.pipe(v.strictObject({
2067
2132
  min: v.optional(FiniteNumber),
2068
2133
  max: v.optional(FiniteNumber)
2069
2134
  }), v.check(validation => validation.min !== void 0 || validation.max !== void 0, "declare at least one bound, or omit `validation`"), v.check(validation => validation.min === void 0 || validation.max === void 0 || validation.min <= validation.max, "`min` must be less than or equal to `max`")), ChoiceOptionsSchema = v.strictObject({
@@ -2172,7 +2237,7 @@ function choiceOptionsCheck() {
2172
2237
  }) ?? []).join("; "));
2173
2238
  }
2174
2239
 
2175
- const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number" ]);
2240
+ const SCALAR_VALIDATION_KINDS = /* @__PURE__ */ new Set([ "string", "text", "number", "progress" ]);
2176
2241
 
2177
2242
  function scalarValidationCheck() {
2178
2243
  return v.check(entry => scalarValidationDeclarationIssues(entry) === void 0, issue => (scalarValidationDeclarationIssues(issue.input) ?? []).join("; "));
@@ -2181,7 +2246,11 @@ function scalarValidationCheck() {
2181
2246
  function scalarValidationDeclarationIssues(entry) {
2182
2247
  const {type: type, validation: validation} = entry;
2183
2248
  if (validation === void 0) return;
2184
- if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` values, not "${type}"` ];
2249
+ if (!SCALAR_VALIDATION_KINDS.has(type)) return [ `\`validation\` is only valid on \`string\` / \`text\` / \`number\` / \`progress\` values, not "${type}"` ];
2250
+ if (type === "progress") {
2251
+ const issues2 = Object.entries(validation).flatMap(([bound, value]) => typeof value == "number" && value >= 0 && value <= 100 ? [] : [ `\`validation.${bound}\` must stay within the progress kind's 0–100 contract` ]);
2252
+ return issues2.length === 0 ? void 0 : issues2;
2253
+ }
2185
2254
  if (type === "number") return;
2186
2255
  const issues = Object.entries(validation).flatMap(([bound, value]) => Number.isInteger(value) && value >= 0 ? [] : [ `\`validation.${bound}\` must be a non-negative integer for ${type} length` ]);
2187
2256
  return issues.length === 0 ? void 0 : issues;
@@ -3592,6 +3661,7 @@ const SCALAR = {
3592
3661
  string: () => SCALAR,
3593
3662
  text: () => SCALAR,
3594
3663
  number: () => SCALAR,
3664
+ progress: () => SCALAR,
3595
3665
  boolean: () => SCALAR,
3596
3666
  date: () => SCALAR,
3597
3667
  datetime: () => SCALAR,