@sanity/workflow-engine 0.14.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 +779 -0
- package/DATAMODEL.md +252 -0
- package/README.md +2 -2
- package/dist/_chunks-cjs/invariants.cjs +3208 -0
- package/dist/_chunks-es/invariants.js +2976 -0
- package/dist/define.cjs +106 -944
- package/dist/define.d.cts +263 -411
- package/dist/define.d.ts +263 -411
- package/dist/define.js +81 -941
- package/dist/index.cjs +9882 -5220
- package/dist/index.d.cts +3099 -724
- package/dist/index.d.ts +3099 -724
- package/dist/index.js +9468 -5284
- package/package.json +9 -5
- package/dist/_chunks-cjs/schema.cjs +0 -1289
- package/dist/_chunks-cjs/schema.cjs.map +0 -1
- package/dist/_chunks-es/schema.js +0 -1274
- package/dist/_chunks-es/schema.js.map +0 -1
- package/dist/define.cjs.map +0 -1
- package/dist/define.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
package/DATAMODEL.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# The engine's persisted data model
|
|
2
|
+
|
|
3
|
+
The engine owns three standalone document types in the Content Lake:
|
|
4
|
+
|
|
5
|
+
| Doc type | Constructed by | Mutated after create |
|
|
6
|
+
| ---------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
7
|
+
| `sanity.workflow.definition` | `planDefinitionDeploy` (`src/api/deploy.ts`) | never — deploys are create-only |
|
|
8
|
+
| `sanity.workflow.instance` | `buildInstanceBase` (`src/instance.ts`) | every persist writes the canonical state via `instanceStateFields` (`src/engine/mutation.ts`) |
|
|
9
|
+
| `temp.system.guard` | `compileGuard` (`src/core/guards.ts`) | full-body refresh on stage re-entry; predicate lift on retract |
|
|
10
|
+
|
|
11
|
+
These field trees are **our contract with every past and future engine
|
|
12
|
+
version reading the same dataset**. This file is where changes to them are
|
|
13
|
+
declared; the model-surface gates keep undeclared drift red:
|
|
14
|
+
|
|
15
|
+
- **The shape ledger** — `src/__tests__/model-shapes/model-<N>.json`, one
|
|
16
|
+
file per `DATA_MODEL_VERSION`, **append-only**: only the current version's
|
|
17
|
+
file is ever regenerated (`vitest -u` on the gate test), so past model
|
|
18
|
+
shapes stay checked in verbatim — the historical corpus future upgrade
|
|
19
|
+
machinery tests against. It pins the field trees of every doc
|
|
20
|
+
type built through the real constructors, plus the stored grammar's
|
|
21
|
+
vocabulary (every enum's values, every variant discriminator), so a value
|
|
22
|
+
rename is as red as a key rename.
|
|
23
|
+
- **Coverage tests** (`src/__tests__/model-surface.test.ts`) walk the stored
|
|
24
|
+
definition schema and fail when any declared key, discriminator, or
|
|
25
|
+
instantiable enum value stops appearing in the canonical document — the
|
|
26
|
+
grammar cannot grow past the fixture silently.
|
|
27
|
+
- **The lifecycle ledger** (`model-shapes/model-<N>.lifecycle.json` in
|
|
28
|
+
`workflow-engine-test`) drives real instances through the verbs —
|
|
29
|
+
including the recovery paths (expired-lease sweep, loop-back re-entry
|
|
30
|
+
adoption, dead-end propagation, abort) — and pins the interior trees a
|
|
31
|
+
fresh construction can't reach: `StageEntry` / `ActivityEntry` /
|
|
32
|
+
`SubworkflowEntry` rows, pending effects (claim included), effect
|
|
33
|
+
history, the idempotency ledger, and ALL `HistoryEntry` variants (the
|
|
34
|
+
accounting is compile-checked against the union, so a new variant fails
|
|
35
|
+
the build until the scenarios produce it).
|
|
36
|
+
|
|
37
|
+
Residual review responsibility: each ledger entry pins one shape per
|
|
38
|
+
variant (the scenario's first), so an _optional_ key that only appears on
|
|
39
|
+
some entries of an already-covered variant — or a conditionally-written
|
|
40
|
+
field no fixture or scenario reaches — still needs a reviewer to demand a
|
|
41
|
+
ledger-visible sample. Fixture width is part of reviewing a model change.
|
|
42
|
+
|
|
43
|
+
This is **not** definition-content versioning: `version` / `pinnedVersion` /
|
|
44
|
+
`contentHash` track what an author deployed; the model stamp tracks the shape
|
|
45
|
+
of the documents the engine itself writes.
|
|
46
|
+
|
|
47
|
+
## The stamp pair
|
|
48
|
+
|
|
49
|
+
The two doc types whose format the engine owns outright — definition and
|
|
50
|
+
instance — carry two numbers (`MODEL_STAMP` in `src/core/model-version.ts`).
|
|
51
|
+
Provenance and compatibility are deliberately separate, because consumers
|
|
52
|
+
sharing a dataset upgrade months apart (Functions lag Studios lag CLIs) and
|
|
53
|
+
"newer" must not mean "incompatible".
|
|
54
|
+
|
|
55
|
+
**The guard doc carries NO stamp.** `temp.system.guard` is a foreign
|
|
56
|
+
contract: the persisted format mirrors the Content Lake's forthcoming guard
|
|
57
|
+
primitive, and the engine must not grow fields on a shape it doesn't own —
|
|
58
|
+
the lake dictates that format long-term. The guard doc's versioning event is
|
|
59
|
+
the `_type` cutover itself (an engine that predates `system.guard` simply
|
|
60
|
+
never matches the new type).
|
|
61
|
+
|
|
62
|
+
- **`modelVersion` — provenance ("conforms-to").** The value of
|
|
63
|
+
`DATA_MODEL_VERSION` at write time: "this document conforms to model N
|
|
64
|
+
now". Bumps on **every** declared shape change, additive included.
|
|
65
|
+
Instances are re-stamped on every full persist (and rollback restore).
|
|
66
|
+
Partial patches deliberately leave the pair alone — they don't normalize
|
|
67
|
+
the shape, so restamping there would lie.
|
|
68
|
+
- **`minReaderModel` — the reader floor.** The value of
|
|
69
|
+
`DATA_MODEL_MIN_READER` at write time: the oldest engine model that can
|
|
70
|
+
safely _interpret_ this document. An **additive** change leaves the floor
|
|
71
|
+
untouched — older engines keep reading and writing newer docs, so
|
|
72
|
+
mixed-version fleets interoperate. Only a **breaking** change raises it,
|
|
73
|
+
and raising it is a declared decision to fence out older readers (fleet
|
|
74
|
+
must upgrade readers before such writers deploy).
|
|
75
|
+
- **Missing stamp = model 0** (floor 0) — the document was last written
|
|
76
|
+
before the stamp existed. A `modelVersion` with no `minReaderModel` is
|
|
77
|
+
malformed foreign data and gates conservatively: the stamp is the floor.
|
|
78
|
+
- **Backward compatibility is unconditional.** An engine at model N reads
|
|
79
|
+
every stamp `0..N`. There is no supported-version floor for _old_ docs;
|
|
80
|
+
introducing one is a deliberate future mechanism, never a side effect of
|
|
81
|
+
a shape change.
|
|
82
|
+
- **Fail hard on `minReaderModel` ahead — and only that.** Every engine read
|
|
83
|
+
of a stamped doc type (definition, instance) passes through
|
|
84
|
+
`assertReadableModel` — point reads, list queries, the drained-children
|
|
85
|
+
projection, snapshot hydration's raw reads. A floor ahead of the engine's
|
|
86
|
+
`DATA_MODEL_VERSION` throws
|
|
87
|
+
`ModelVersionAheadError` ("upgrade `@sanity/workflow-engine`") instead of
|
|
88
|
+
silently misreading a shape this engine predates. Deliberately wholesale:
|
|
89
|
+
one such document in a read's scope fails the whole read (a listing, a
|
|
90
|
+
verdict computation) rather than degrading silently. Two structural
|
|
91
|
+
exceptions: existence-only probes (no shape is interpreted), and lake-side
|
|
92
|
+
GROQ filters, which evaluate persisted fields server-side before any
|
|
93
|
+
client gate can run — their safety rests entirely on rule 3's
|
|
94
|
+
additive-only constraint.
|
|
95
|
+
- **Old writers restamp down — the round-trip rule.** An older engine
|
|
96
|
+
persisting a newer-but-compatible instance stamps its own `modelVersion`
|
|
97
|
+
(honest conforms-to; the stamp oscillates in mixed fleets, harmlessly).
|
|
98
|
+
This is safe because full persists are set-only patches and mutation
|
|
99
|
+
copies are spreads, so fields the older engine doesn't know **survive the
|
|
100
|
+
round-trip**. That is a load-bearing invariant: an additive change whose
|
|
101
|
+
fields would NOT survive an old writer's round-trip is not additive — it
|
|
102
|
+
is breaking, and must raise the floor.
|
|
103
|
+
- **The floor is raise-only.** A rewrite writes
|
|
104
|
+
`max(stored minReaderModel, own DATA_MODEL_MIN_READER)` — it never lowers
|
|
105
|
+
or drops the stored floor, because the preserved unknown fields of a newer
|
|
106
|
+
model may still require it. (The failure mode this fences off is
|
|
107
|
+
documented across the industry: a rewriter that drops the compatibility
|
|
108
|
+
gate it doesn't understand silently reopens the door the floor closed.)
|
|
109
|
+
- **Raising the floor is an availability decision, sequenced readers-first.**
|
|
110
|
+
Before any writer that raises `DATA_MODEL_MIN_READER` deploys, every
|
|
111
|
+
reader in the fleet — customer-deployed Functions foremost — must already
|
|
112
|
+
run an engine at or above the new floor (the BACKWARD deployment order:
|
|
113
|
+
upgrade consumers before producers). And like every engine-side check the
|
|
114
|
+
floor is advisory — it gates our own clients; the Content Lake stays the
|
|
115
|
+
sole enforcement point.
|
|
116
|
+
- **The gate ships from the engine.** Consumer surfaces that read these docs
|
|
117
|
+
without routing through the engine (the CLI's direct reads, MCP query
|
|
118
|
+
projections, the reactive studio/sdk adapters) adopt `assertReadableModel`
|
|
119
|
+
with the parse-don't-cast follow-up; until then they render newer-model
|
|
120
|
+
docs unchecked.
|
|
121
|
+
|
|
122
|
+
## Rules for changing the model
|
|
123
|
+
|
|
124
|
+
1. **Any change to a persisted field tree is a model change** and must be
|
|
125
|
+
declared: update the model-surface snapshot, add an entry to the model
|
|
126
|
+
log below, and decide — in the same change — whether `DATA_MODEL_VERSION`
|
|
127
|
+
moves **and whether the reader floor moves with it**. Nothing lands
|
|
128
|
+
"incidentally".
|
|
129
|
+
2. **Additive optional fields are allowed** without machinery: bump
|
|
130
|
+
`DATA_MODEL_VERSION`, leave `DATA_MODEL_MIN_READER` untouched, and
|
|
131
|
+
declare the tolerant read (what an absent value means on docs written
|
|
132
|
+
before the field existed) plus why the field survives an old writer's
|
|
133
|
+
round-trip.
|
|
134
|
+
3. **Renames, removals, and semantic changes are not allowed yet.** They
|
|
135
|
+
require versioned upgrade machinery (per-model normalization steps applied
|
|
136
|
+
on read) that does not exist; until it does, the model may only grow
|
|
137
|
+
additively. When such a change eventually lands, it raises
|
|
138
|
+
`DATA_MODEL_MIN_READER` — a declared fence against older readers, shipped
|
|
139
|
+
only after the fleet story is written.
|
|
140
|
+
4. **`DATA_MODEL_VERSION` moves when compatibility reasoning changes** — when
|
|
141
|
+
a reader would need to know which shape it is looking at. A purely
|
|
142
|
+
internal refactor that provably writes an identical tree does not bump.
|
|
143
|
+
5. **Classify by misinterpretation, not parseability.** The snapshot gate
|
|
144
|
+
catches structural drift only. A change can be structurally additive yet
|
|
145
|
+
change the MEANING of existing fields — old readers parse it fine and
|
|
146
|
+
silently misinterpret. That judgment (would an old reader misread, not
|
|
147
|
+
just fail to parse?) is exactly what decides whether
|
|
148
|
+
`DATA_MODEL_MIN_READER` moves, and no snapshot can make it — each model
|
|
149
|
+
log entry states it explicitly.
|
|
150
|
+
|
|
151
|
+
## Model log
|
|
152
|
+
|
|
153
|
+
### Model 0 — pre-governance
|
|
154
|
+
|
|
155
|
+
Everything written before the stamp existed. Documents carry no
|
|
156
|
+
`modelVersion`. The shape history of model 0 was absorbed by tolerant reads
|
|
157
|
+
that predate this file — recorded here so they are declared compatibility
|
|
158
|
+
rules, not folklore:
|
|
159
|
+
|
|
160
|
+
- **Instances without `subworkflows[]`** (persisted before the subworkflow
|
|
161
|
+
registry existed) — read as `[]`; the next full persist materializes it.
|
|
162
|
+
- **Instances without `processedRequests[]`** (persisted before the
|
|
163
|
+
idempotency ledger existed) — read as `[]`; the next full persist
|
|
164
|
+
materializes it.
|
|
165
|
+
- **Definitions without `contentHash`** (deployed before content-addressing)
|
|
166
|
+
— always read as changed, so the first redeploy after upgrade mints a
|
|
167
|
+
fresh version.
|
|
168
|
+
|
|
169
|
+
### Model 1 — stamping introduced; actions-only payload model (reader floor: 0)
|
|
170
|
+
|
|
171
|
+
The `modelVersion` + `minReaderModel` pair is stamped on the definition and
|
|
172
|
+
instance doc types; the guard doc deliberately carries none (foreign lake
|
|
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.
|
|
245
|
+
|
|
246
|
+
## Pending governed changes
|
|
247
|
+
|
|
248
|
+
- **`temp.system.guard` → `system.guard`** — the guard doc type's `temp.`
|
|
249
|
+
prefix drops when the lake's own guard primitive ships (see
|
|
250
|
+
`GUARD_DOC_TYPE` in `src/types/authorization.ts`). A breaking type change
|
|
251
|
+
for already-persisted guard docs; it must land as a declared model change
|
|
252
|
+
with a migration story, through this process.
|
package/README.md
CHANGED
|
@@ -38,8 +38,8 @@ different at each — read it by where it sits:
|
|
|
38
38
|
lightweight `{type, name}` shapes for the value's structure, mirroring
|
|
39
39
|
Sanity's `object.fields` / `array.of`.
|
|
40
40
|
3. **Object value expressions** — a `{type: 'object', fields: {...}}` value
|
|
41
|
-
expression in an op
|
|
42
|
-
|
|
41
|
+
expression in an op payload: one expression per key of the computed
|
|
42
|
+
object value.
|
|
43
43
|
4. **Resolved runtime values** — `fields` on a workflow instance document: the
|
|
44
44
|
declared entries with their current values, written by ops and edits.
|
|
45
45
|
|