@sanity/workflow-engine 0.15.0 → 0.16.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,41 @@
1
1
  # @sanity/workflow-engine
2
2
 
3
+ ## 0.16.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 11e2d6f: Export `DEFAULT_TRANSITION_WHEN` — the trigger a transition desugars to when the author declares none — so consumers describing transition triggers can recognize the default's spelling instead of copying the literal.
8
+ - 7510fd5: Delivery-surface helpers and an honest remediation contract. The start-surface boundary is now engine-owned and shared: `buildInitialFields` (typing caller-supplied start values against a workflow's declared field entries) and `startRefusal` (the spawn-only refusal message), both previously private to the CLI. Also exported: `documentPrefilter` and `inFlightFilter` (the instance-list document and in-flight arms, for list builders that compose their own conditions) and `parseDefinitionInput` (the deploy boundary's envelope-strip + stored-form parse). The `drain-effects` remediation verb is now flagged `available: true` — the engine's `drainEffects` verb ships, so a hung-effect diagnosis names a fix the consumer can actually run instead of describing it as not yet callable.
9
+ - 79c54db: Reactive session reads now match the engine's own reads.
10
+ - The session feeder projects every observed doc onto its watch ref's identity the way the lake's perspective reads do: the published-form id becomes `_id` (Studio/SDK stores resolve drafts and release versions under `drafts.*`/`versions.*` ids, which keyed the session's overlay and snapshot where no transition `when` condition or `$fields.<docref>` deref ever looks — an editor's uncommitted draft could not satisfy a condition the engine's `tick()` would read as satisfied), and a draft/version's stored id rides along as `_originalId`, so conditions like `_originalId in path("versions.**")` read what the engine's own hydration returns. The projection is an engine export (`projectToWatchRef`), one encoding beside `contentDraftFallback`/`contentDocQuery`, and it is strict: only the exact representations a store may legitimately resolve under the instance's perspective are accepted — anything else (another doc's dotted id under a version prefix, a version from an unbound release, a draft the perspective makes invisible) fails loudly instead of being fed under the watched identity.
11
+ - Watches whose perspective makes drafts invisible to the engine no longer fall back to the draft. The engine hydrates a `[release]` stack as version-over-published (and `'published'`/`'raw'` scalars as published/raw) with drafts invisible; the Studio adapter resolved `version ?? draft ?? published` and the SDK's per-doc store has the same fallback built in. The Studio adapter now skips the draft arm unless the effective perspective names `'drafts'`, and the SDK adapter observes such refs through the query store under the instance's exact perspective stack. The SDK's query-store route reads committed state (live), so local uncommitted edits to a release version doc no longer feed the session there — Studio keeps its optimistic `editState` reads. New engine helpers `contentDraftFallback` and `contentDocQuery` are the single encoding of the rule, shared with the engine's own hydration.
12
+ - `useWorkflowSession` resets its surfaced `evaluation` to `undefined` on an `instanceId` swap, as the contract promises, instead of leaving the previous instance's projection visible until the new instance first evaluates.
13
+
14
+ - 6a46db1: **BREAKING:** the definition root's `applicableWhen` moved to `start.filter`, inside a new `start` block — `{kind?: 'interactive' | 'autonomous', filter?: GROQ}` — describing how standalone runs begin. No alias, no dual-read: definitions authored with `applicableWhen` fail the strict parse until rewritten.
15
+ - `start.kind` classifies who initiates a run — a person from a start surface (`'interactive'`, the default; `startKindOf(def)` is the one reading rule) or a system reacting to a document (`'autonomous'`, hidden from human start pickers). Classification only: `startInstance` stays one verb and one code path for every caller and kind — an interactive start of an autonomous workflow is legal. Deploy invariants: a spawn-only (`lifecycle: 'child'`) definition may not declare `start`; an `'autonomous'` kind requires every required input entry to be subject-style (`doc.ref`/`doc.refs` — the shared `isSubjectEntry` rule); a `start.filter` may read only declared **input-sourced** entries via `$fields` (a typo or a query/literal entry is a silent never-pass); and a root-reading filter (`_type == …`, `@`, or a `^` escaping a scan) requires a subject entry, since a read surface with no subject never binds a root.
16
+ - `start.filter` is a **read-side visibility rule, not a gate** — `startInstance` never evaluates it and never throws on it; it starts whenever the caller supplies the required inputs. The filter decides what appears in / is chosen from Start lists: `definitionsForDocument`/applicability and the Studio start control evaluate it in a widened, named start-filter context (`START_FILTER_VARS`) — the candidate document as root, `$tag`/`$definition`/`$now`/`$fields` bound (`$fields` = the caller's input entries as GDR envelopes, so `$fields.<entry>.id` is the GDR URI), and `*[...]` = the workflow resource's dataset. Filters that never scan keep the cheap pure evaluation (`analyzeCondition(...).readsDataset` decides); scanning filters evaluate over a fetched slice of EVERY instance in the tag (completed included — `*` carries no hidden predicate; filters qualify in-flight themselves with `!defined(completedAt)`), so a filter can bound what surfaces (WIP limits, one-run-per-subject). Absent bindings evaluate GROQ-null and fail closed.
17
+
18
+ - 35b9b85: Commit/recovery hardening and a locale-independent content fingerprint:
19
+ - `completeEffect` now commits under the engine's shared optimistic-locking retry, like every other claim-protocol write: a lost revision race reloads and re-commits the same reported outcome instead of escaping as a raw 409 — which discarded the handler's outputs and re-dispatched the effect after lease expiry, running the external side effect twice. Exhausting every attempt throws the new `ConcurrentCompleteEffectError` (kind `concurrent-complete-effect`).
20
+ - A retried `abortInstance` of an already-terminal instance now propagates to ancestors unconditionally, matching the condemned-children drain: a crash between the terminal commit and the ancestor walk no longer wedges the parent's subworkflow gate on a child that already died.
21
+ - `hashDefinitionContent` and the model-shape canonicaliser sort object keys by UTF-16 code unit instead of the locale-dependent `localeCompare`, so identical definitions fingerprint identically on every machine. A definition whose key ordering differed under the deploying machine's locale hashes differently once — the next redeploy of such content mints one new version instead of reading as unchanged.
22
+
23
+ - e5f5b77: **BREAKING:** actions become the only payload mechanism — every op, effect, and spawn in the system lives on an action; structure never does.
24
+ - Actions gain `when` (cascade-fired trigger: the engine fires it the moment it's true, at most once per stage visit, level-triggered; never fireAction-able) and `spawn` (the subworkflow block, unlocking caller-triggered and conditional spawn). Fires-on-entry is spelled `when: 'true'`.
25
+ - Deleted from the model: `activation`, activity-level `ops`/`effects`/`subworkflows`, `completeWhen`/`failWhen`, `kind`, and transition `ops`/`effects`. Transitions are pure edges `{name, when, to}` (the condition key renamed from `filter` — trigger semantics). `target` survives as the off-system marker; `kind` is now always derived, plus a new derived executor classification (autonomous / interactive / off-system / hybrid) on activity evaluations.
26
+ - Activities are `active` from stage entry (no `pending` status, no activation moment, no machine-step implicit); a new deploy invariant requires every activity to have a reachable path to a terminal status.
27
+ - A cascade hop is ONE transaction: the current stage's triggered actions run to in-memory fixpoint, then transition selection, then a single CAS commit; chain hops stay separate commits. The per-visit `firedActions` ledger on the activity entry re-arms on re-entry (a stage resets on re-entry). The cascading token executes triggered actions; `roles` on a triggered action pins which identities may execute it (an incapable token leaves it armed).
28
+ - The context bag split: `$effects` now carries completed effects' outputs ONLY (derived from `effectHistory`), and the new `$context` carries the `startInstance` seed plus a parent's `spawn.context` handoff (the persisted `effectsContext` field renamed `context`; `startInstance`'s seed arg renamed accordingly). `$effectStatus` is unchanged.
29
+ - Spawn registry rows key per (activity entry, spawning action), so two spawn actions on one activity can never cross-adopt each other's children.
30
+ - `fireAction` rejects a `when` action (`cascade-fired` verdict); evaluation projects triggered actions as narratable (`triggered: true` + `whenInsight`), never as buttons.
31
+
32
+ ### Patch Changes
33
+
34
+ - 01e8042: Fix `fieldRead` values landing in `doc.ref`-kinded slots: a bare (no-path) read of a `doc.ref` source now preserves the stored reference instead of handing over the dereferenced document wherever the result feeds a `doc.ref`-kinded slot — a `doc.ref` entry's `initialValue` seed (the mirror pattern no longer fails `startInstance` with `FieldValueShapeError`), a `field.set` op targeting a `doc.ref` entry, a `field.append` op item landing in a `doc.refs` entry, and `doc.ref` sub-fields reached through an object-expression op value (`field.set` object entries, `field.append` rows, `field.updateWhere` merges — sub-values resolve against the entry's declared sub-field shapes). Identity preservation applies only to bare reads into `doc.ref`-kinded slots; every other surface (conditions, effect bindings, guard metadata, path-carrying reads, undeclared object-expression keys) keeps the doc-centric dereference.
35
+ - 01e8042: Close the unvalidated `field.updateWhere` write path. The merged rows now pass the same shape gate as `field.set` / `field.append` — a wrong-kinded merge value for a declared row sub-field throws `FieldValueShapeError` and aborts the commit instead of persisting silently. An `updateWhere` may only target an `array` entry (the one kind whose rows carry declared sub-fields): a definition-carried op against any other kind is rejected at define/deploy time, and the op applier enforces the same gate for the boundaries deploy never sees (effect-completion ops, definition/instance divergence) — previously a merge into a `doc.refs` entry spread the merge value's fields (for a bare `fieldRead` of a `doc.ref`, the whole dereferenced document) into the stored GDR rows with no error. A merge may also never write the reserved row keys `_key` / `_type` — row identity and bookkeeping are engine-stamped, and a merge would restamp every matched row with the same literal; statically-named keys are rejected at define/deploy, and the applier re-checks the resolved merge. `field.removeWhere` is unchanged and keeps accepting every array-valued kind, `doc.refs` included — dropping rows needs no row shape.
36
+ - Updated dependencies [7510fd5]
37
+ - @sanity/groq-condition-describe@0.2.0
38
+
3
39
  ## 0.15.0
4
40
 
5
41
  ### Minor Changes
package/DATAMODEL.md CHANGED
@@ -166,13 +166,82 @@ rules, not folklore:
166
166
  — always read as changed, so the first redeploy after upgrade mints a
167
167
  fresh version.
168
168
 
169
- ### Model 1 — stamping introduced (reader floor: 0)
169
+ ### Model 1 — stamping introduced; actions-only payload model (reader floor: 0)
170
170
 
171
171
  The `modelVersion` + `minReaderModel` pair is stamped on the definition and
172
172
  instance doc types; the guard doc deliberately carries none (foreign lake
173
- contract — see "The stamp pair"). No other shape change. The floor is 0: any
174
- engine ever released reads model-1 documents fine, and model-0 documents
175
- keep reading exactly as before.
173
+ contract — see "The stamp pair"). The floor is 0: model-0 documents keep
174
+ reading exactly as before, and every engine from the amendment below onward
175
+ reads model-1 documents fine (the amendment's sanction — no readers or
176
+ writers existed outside this repo — is what made the pre-amendment window
177
+ irrelevant).
178
+
179
+ **Amended in place before any consumer existed** (a sanctioned exception to
180
+ rule 3 — model 1 had no readers or writers outside this repo when the
181
+ reshape landed, so the freeze point moved rather than minting a model 2):
182
+ actions became the only payload mechanism, and the persisted trees reshaped
183
+ accordingly. One caveat was weighed and accepted when the exception was
184
+ confirmed: released engines predating the amendment stamp model 1 while
185
+ writing the pre-amendment shapes, so documents they wrote pass the stamp
186
+ gate and then fail ungoverned (a raw shape error, not
187
+ `ModelVersionAheadError`). Those documents are declared disposable — every
188
+ dataset holding them is a this-repo test dataset and must be reset before a
189
+ post-amendment engine touches it. Nothing migrates them.
190
+
191
+ Definition tree (the stored authoring model):
192
+
193
+ - Activities carry no payload: `activation`, `ops`, `effects`,
194
+ `completeWhen`, `failWhen`, `subworkflows`, and `kind` are gone from the
195
+ activity tree. `target` survives as the off-system marker.
196
+ - Actions gained `when` (a cascade-fired trigger condition), `spawn` (the
197
+ subworkflow block formerly on the activity), and `roles` (stored verbatim
198
+ only on cascade-fired actions — the execute pin; fireAction actions still
199
+ fold roles into `filter` at desugar).
200
+ - Transitions are pure edges: `{name, when, to}` plus presentation —
201
+ `filter` renamed to `when`, `ops`/`effects` gone.
202
+
203
+ Instance tree:
204
+
205
+ - `effectsContext` renamed to `context` (entry `_type`s
206
+ `effectsContext.<kind>` → `context.<kind>`); it now holds ONLY the
207
+ start-time seed and a parent's spawn handoff. Completed effects' outputs
208
+ live solely on `effectHistory[].outputs` (the `$effects` read derives from
209
+ there).
210
+ - `ActivityEntry` statuses lost `pending` (entries are `active` from stage
211
+ entry, or `skipped`) and gained `firedActions: string[]` — the per-visit
212
+ ledger of cascade-fired actions.
213
+ - `SubworkflowEntry` gained required `action` (the spawning action's name;
214
+ rows key per activity + action).
215
+ - History: the `activityActivated` variant is gone; `actionFired` gained
216
+ `triggered?: true`; `opApplied` lost its `transition` origin key;
217
+ `EffectOrigin.kind` is only ever `'action'`.
218
+
219
+ **Pre-adoption model-1 amendment: moved `applicableWhen` to `start.filter`,
220
+ added `start.kind` — a deliberate freeze bypass; no external consumers
221
+ exist.** Same sanction as the amendment above (rule 3 untouched, the freeze
222
+ point moves, no model 2 minted). The definition root gained an optional
223
+ `start` block: `kind` (`'interactive' | 'autonomous'`, stored explicitly —
224
+ desugar fills the default; the block itself stays absent on definitions that
225
+ never declared one, and an absent block reads as interactive with no filter)
226
+ and `filter` — the former `applicableWhen`, re-scoped from a pure
227
+ candidate-doc predicate to the start-filter context (`$tag` / `$definition` /
228
+ `$now` / `$fields` bound, `*[...]` reading the workflow dataset; the
229
+ `startFilterVars` vocabulary joins the ledger). `start.filter` is a READ-SIDE
230
+ visibility rule — evaluated by `definitionsForDocument`/applicability and the
231
+ Studio start control to decide what appears in Start lists; `startInstance`
232
+ never reads it and never throws on it. An earlier iteration of this amendment
233
+ added a `startFilterOverridden` instance-history variant (paired with a
234
+ throwing start-time gate and an `overrideStartFilter` verb arg); that gate was
235
+ removed before any consumer existed, so this same in-place amendment ALSO
236
+ removed the `startFilterOverridden` variant — no gate, no override, nothing to
237
+ audit. The instance tree is back to its pre-amendment history variants.
238
+ Would an old reader misread (rule 5)? No reader exists outside this repo —
239
+ which is the sanction; post-amendment engines read pre-amendment model-1
240
+ definitions fine (no `start` block ⇒ the defaults above). The released-engine
241
+ caveat above covers this amendment identically: definitions a released engine
242
+ wrote with `applicableWhen` stamp model 1 and then fail the strict parse
243
+ ungoverned — same disposition, those datasets are this-repo test data and are
244
+ reset, not migrated.
176
245
 
177
246
  ## Pending governed changes
178
247