@sanity/workflow-engine 0.20.0 → 0.21.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,98 @@
1
1
  # @sanity/workflow-engine
2
2
 
3
+ ## 0.21.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d9394e5: **BREAKING:** Settle every child created by one spawn commit before the parent reacts to the cohort.
8
+
9
+ Parent transitions and triggered actions now observe the complete same-commit cohort instead of intermediate prefixes where later children are still unprimed. Spawn propagation stamps terminal child rows in one parent commit, cascade hops reuse an unchanged drain read, and successful aborts reuse their committed instance for immediate drain and ancestor propagation.
10
+
11
+ - f4bc057: **BREAKING:** `list_workflow_instances` now returns one page with `has_more` and an optional `next_cursor`; callers that need every match must continue with the cursor. The optional `limit` accepts 1–100 rows and defaults to 25.
12
+
13
+ Bound instance and definition list reads with lake-side filtering and consumer-specific projections.
14
+
15
+ Document filtering now excludes exited-stage references and includes live unresolved child-workflow references. Definition discovery selects and model-gates only the latest deployed version of each workflow name; historical versions remain stored and available to history-oriented APIs.
16
+
17
+ The MCP eval suite now verifies that agents follow list cursors to find a target beyond the first default page, including in persisted Braintrust runs.
18
+
19
+ MCP tool telemetry reports a `cursorUsed` boolean so continued list-page adoption is measurable without sending cursor values, arguments, or results.
20
+
21
+ - bcc30fe: **BREAKING:** One identity namespace inside the engine — every principal id in live state is the account-global user id (`sanityUserId`). The write boundary admits global-namespace ids only (`g…` humans, `p-…` robots verbatim; `e-…` third-party ids normalize to their embedded global form in the persisted output; plain project-scoped ids and unclassifiable forms are rejected with the remediation named). Actor resolution reads `/users/me` on both the workflow resource host and the global host: the global record is the canonical actor id, the resource host supplies roles, and the resource-local principal id rides the in-memory access context to bind `identity()` in guard previews and grant-filter evaluation — so advisory verdicts agree with the lake's own dialect, including per-resource identity for foreign-subject forecasts. Identity-kind guard `metadata` projections deploy in the target dataset's LOCAL spellings (translated through the project member list at deploy/refresh), so attribution predicates work without the author knowing two namespaces exist. Instances written before ids were namespace-classified resolve their live-state ids to global through the project-users directory at the read funnel (all-or-nothing per document, loud when unresolvable) and converge to global spellings on their next commit; `history[]` stays verbatim as audit record. Ships as data model 4 with reader floor 4: pre-classifier engines refuse new instances rather than mis-compare them, and deployment acknowledgements move to `expectedMinReaderModel: 4`. The prefix classifier is public API (`classifyPrincipalId`, `lakePrincipalId`, the `identity()` sentinels) so adapters can share the one namespace rule. **Existing instances written with project-scoped ids may no longer match user-identity queries and list filters until their next engine commit self-heals them; a prerelease deployment with no data worth keeping can reset with `sanity-workflows nuke` and start fresh.** There is no fallback to a project-scoped actor: when the workflow resource host reports a project-scoped principal, resolving the account-global record is required and its failure refuses the call. **Docs impact:** upgrade guidance is canonical in `docs/reader-model-rollout.md` § "Crossing to model 4" — any changelog-derived docs, upgrade notes, or assistant guidance should point there (readers-first sequencing, self-heal semantics, and the `sanity-workflows nuke` reset recommendation for prerelease data).
22
+ - e7392af: Add the `resetActivity` admin verb — reset a failed (or otherwise terminal) activity in an instance's current stage back to `active` (re-run) or `skipped` (bypass), then cascade so a `$allActivitiesDone`-gated exit transition can fire. `workflow.diagnose` now marks its `reset-activity` remediation `available`, and the CLI ships the real `reset-activity <instanceId> <activity> [--skip]` command in place of the stub. Emits the `Editorial Workflows Activity Reset` adoption event on every attempt (`changed: false` on a no-op), matching the `set-stage` and `abort` overrides.
23
+
24
+ Writes strictly within the existing model-3 grammar: it reuses the `activityStatusChanged` history variant and the existing activity statuses, so it adds no persisted shape — there is no data-model version bump and no reader-floor change.
25
+
26
+ - fa9c796: **BREAKING:** Replace the singular `start.allowed` and activity requirement record with ordered, named requirement arrays.
27
+
28
+ Start readiness now accepts polymorphic `groq` and `singleSubject` nodes:
29
+
30
+ ```ts
31
+ // Before
32
+ start: {allowed: '$fields.approved == true'}
33
+
34
+ // After
35
+ start: {
36
+ requirements: [
37
+ {type: 'groq', name: 'approved', title: 'Approval required', query: '$fields.approved == true'},
38
+ ],
39
+ }
40
+ ```
41
+
42
+ Use `singleSubject` instead of `$subjectHasInFlightInstance` to allow at most one in-flight run of the same definition for a subject. The requirement is definition-scoped and version-blind across deployments:
43
+
44
+ ```ts
45
+ // Before
46
+ start: {allowed: '!$subjectHasInFlightInstance'}
47
+
48
+ // After
49
+ start: {
50
+ requirements: [
51
+ {type: 'singleSubject', name: 'single-subject', description: 'Finish the existing run first.'},
52
+ ],
53
+ }
54
+ ```
55
+
56
+ Activity readiness uses the same ordered descriptor model with `groq` nodes:
57
+
58
+ ```ts
59
+ // Before
60
+ requirements: {
61
+ approved: 'defined($fields.approval)'
62
+ }
63
+
64
+ // After
65
+ requirements: [{type: 'groq', name: 'approved', query: 'defined($fields.approval)'}]
66
+ ```
67
+
68
+ Requirement names must be unique within their owning array. Evaluation preserves author order and reports every unmet requirement with its `name` and optional editor-facing `title` and `description`. Fresh standalone starts enforce the requirements after validating inputs. Resuming an unfinished start and parent-owned spawning continue to bypass start requirements.
69
+
70
+ `evaluateStart()` now returns ordered `requirements` entries containing each descriptor, outcome, and GROQ insight where applicable. Its singular top-level `insight` is removed. `StartNotAllowedError.insight` is replaced by `StartNotAllowedError.unmetRequirements`. Activity evaluation likewise reports unmet requirement descriptors instead of names alone.
71
+
72
+ Rename the public start-requirement analysis helpers and constants:
73
+
74
+ ```ts
75
+ // Before
76
+ explainStartAllowed(args)
77
+ unboundAllowedReads(query, fields)
78
+ START_ALLOWED_VARS
79
+
80
+ // After
81
+ explainStartRequirement(args)
82
+ unboundRequirementReads(query, fields)
83
+ START_REQUIREMENT_VARS
84
+ ```
85
+
86
+ `definitionsForDocument({document, subject})` becomes `definitionsForDocument({document})`. Start applicability no longer needs a separately supplied subject. The engine-owned start dataset projection exposes only `definition`, `subject`, and `completedAt`; authors no longer query raw instance storage for deduplication.
87
+
88
+ Studio pre-flights both requirement kinds against the live document session, disables starts whose requirements are unmet, and renders the authored description or title with a humanized requirement-name fallback. Controls re-enable on the same mount when a blocking run completes.
89
+
90
+ Persisted data model 4 unconditionally raises `minReaderModel` to 4 for new definitions and instances because older readers would ignore readiness arrays and could commit invalid transitions. Upgrade every reader and Function before deploying model-4 writers. Legacy deployed definitions are not normalized; prerelease environments with incompatible definitions may use `sanity-workflows nuke` before redeploying.
91
+
92
+ ### Patch Changes
93
+
94
+ - 92e28bd: Fix reset telemetry test fixtures to use the current reader model.
95
+
3
96
  ## 0.20.0
4
97
 
5
98
  ### Minor Changes
package/DATAMODEL.md CHANGED
@@ -152,6 +152,63 @@ never matches the new type).
152
152
  direct reads at gate only (see "Consumer surfaces adopt the gate" above
153
153
  for the split and its rationale).
154
154
 
155
+ ## Definition-language philosophy
156
+
157
+ The authoring DSL is one language, not a bag of options. The principle of
158
+ least surprise applies to the WHOLE surface, and the test for any addition is
159
+ self-similarity: **someone who knows one part of the DSL should be able to
160
+ guess the rest.** Every change to the definition tree is designed against
161
+ these principles before the change-process rules below apply.
162
+
163
+ 1. **Same concept, same word, same shape — at every level.** The gating
164
+ vocabulary is the canonical example: `filter` is **existence** (a
165
+ non-match might as well not exist — hidden, never merely disabled),
166
+ `requirements` is **readiness** (named readiness gates; an unmet entry
167
+ keeps the node visible but disabled, with the failing entries named),
168
+ `when` is **trigger**. A position that needs one of these concepts uses
169
+ the name and shape the concept has everywhere else; a concept that
170
+ changes name or shape between levels is a defect in the language, not a
171
+ local convenience.
172
+ 2. **Surfaced things are nodes; mappings are internal.** Anything a person
173
+ will see — stages, activities, actions, transitions, fields, guards —
174
+ is an entry in a **polymorphic array of named objects**:
175
+ `{name, title?, description?, …}`, discriminated by `type` where
176
+ variants exist, so every surfaced entry can carry its own human copy.
177
+ Plain records (name → expression) are for internal mappings no surface
178
+ renders (predicates, effect bindings, role aliases). If an entry can be
179
+ shown to a person, it is an object in an array; a concept that outgrows
180
+ a record becomes an array of nodes, never a record of objects. Existence filters need none of this: a filtered-out entry
181
+ renders nothing, so there is nothing to title — bare condition strings
182
+ remain correct there.
183
+ 3. **New capability is a new value in an existing slot, never a parallel
184
+ mechanism.** Extend the vocabulary of an existing slot — a new named
185
+ entry, a new declared value, a new synthetic variable — before adding a
186
+ sibling property. A proposal arriving with its own error type, its own
187
+ ordering rule against an existing gate, or its own verdict leg beside an
188
+ existing mechanism's is the smell that it duplicates that mechanism.
189
+ 4. **Legibility comes from structure, not from bespoke props.** When a
190
+ surface needs to explain a refusal, the fix is named or structured slots
191
+ whose entries surfaces can address — never a one-off property added so
192
+ the engine can recognize a single policy. If the existing mechanism is
193
+ illegible or undiscoverable, restructure that mechanism. Authoring-time
194
+ discoverability can also live at the library layer — typed, documented
195
+ helpers that emit canonical expressions — before the model grows any
196
+ surface for it.
197
+ 5. **If the gap is scoping, add a scope, not a mechanism.** A rule that
198
+ duplicates an existing behavior with different scoping (per-definition
199
+ vs cross-definition, per-subject vs per-run) is the existing mechanism
200
+ plus a scoping value or variable, not a new property.
201
+ 6. **A persisted name is forever.** The shape ledger is append-only and
202
+ renames are forbidden (rule 3 below), so the name and shape of a new
203
+ definition-tree property are merge-blocking review concerns, not
204
+ bikeshedding. Names describe the check (`requirements`), never promise
205
+ an outcome the advisory engine cannot deliver (`unique`, `locked`).
206
+ Value vocabularies follow the established scheme — single lowercase
207
+ words, camelCase for multiword values (`todoList`, `fieldRead`), dotted
208
+ namespaces for families (`doc.ref`, `field.set`) — and nothing else: the
209
+ definition grammar has no kebab-case values, and no addition introduces
210
+ the first.
211
+
155
212
  ## Rules for changing the model
156
213
 
157
214
  `DATA_MODEL_CHANGES` is the append-only, machine-readable counterpart of the
@@ -208,6 +265,11 @@ floor.
208
265
  readers still work for those documents. Give the rollout order and scope:
209
266
  readers-first before any writer can persist a floor-bearing feature, not a
210
267
  misleading fleet-wide minimum when floor derivation is document-specific.
268
+ 8. **A new definition-tree property justifies itself against the
269
+ definition-language philosophy above.** The model-log entry states why no
270
+ existing slot, value, or variable could carry the behavior. "The existing
271
+ mechanism is illegible or undiscoverable to surfaces" is an argument for
272
+ restructuring that mechanism, not for a sibling property beside it.
211
273
 
212
274
  ## Model log
213
275
 
@@ -498,6 +560,112 @@ writing, failing closed. A claim without a token (older writer's takeover)
498
560
  simply cannot authorise mid-dispatch reports; dispatch and completion are
499
561
  unaffected.
500
562
 
563
+ ### Model 4 — classified principal ids (reader floor: 4)
564
+
565
+ One namespace change ships as model 4, and it is the first UNCONDITIONAL
566
+ reader-floor raise — model 2's floor-bearing features were detectable,
567
+ fencing only the documents that carried them; this one fences every
568
+ instance the engine writes: **every principal id the engine writes into
569
+ live state is the account-global user id** (`sanityUserId` — the `g…`-prefixed identity
570
+ that is stable across projects and org-level resources). Model 3 was
571
+ released before it landed, so it mints the next version.
572
+
573
+ The platform has two identity namespace families: project datasets speak
574
+ per-project principal ids (lake `identity()` returns the project user id — a
575
+ different value per project for the same person), while org-level resources
576
+ (Canvas, Media Library) and the global API host speak the account-global id.
577
+ Documents written before this model carry whichever namespace their engine's
578
+ client host happened to resolve — for dataset-anchored workflows, the
579
+ workflow resource project's local ids. The id string itself carries its
580
+ namespace (`g…` global, `p-…` robot, `e-<globalId>` third-party project form,
581
+ `p…` project-scoped, `<anonymous>`/`<system>` sentinels) — the discrimination
582
+ Sanity's own session middleware applies; `src/core/identity.ts` is the one
583
+ classifier.
584
+
585
+ **What changes.** No field is added, removed, or renamed — the stamped trees
586
+ are shape-identical to model 3. The declared change is the namespace
587
+ semantics of the principal-id VALUES in live state (`Assignee` user items,
588
+ `Actor` stamps in field values, `completedBy`), plus the read/write discipline
589
+ around them:
590
+
591
+ - **Write boundary** — identity kinds admit global-namespace ids only:
592
+ `g…` and `p-…` verbatim, `e-…` normalized to its embedded global id in the
593
+ persisted output; plain project ids and unclassifiable forms are rejected
594
+ with the remediation named. Every persisted id has passed classification —
595
+ the `e-…` rewrite is the only spelling change.
596
+ - **Read funnel (the versioned upgrade step)** — the instance point-read
597
+ resolves legacy project-scoped ids in live state to their global form
598
+ through the project-users directory (`/projects/<id>/users/<ids>`, the
599
+ sanctioned namespace bridge), ALL-OR-NOTHING per document: if any id
600
+ cannot be resolved the document is read as stored (comparisons against
601
+ those values fail closed, one loud warning). Because every commit persists
602
+ the in-memory instance wholesale, a resolved document's next commit
603
+ converges its stored spellings — normalize-on-rewrite IS this change's
604
+ migration mechanism; there is no separate tooling and no bulk migration.
605
+ - **The anchor-scope invariant** — a bare project-scoped id in stored live
606
+ state belongs to the workflow resource's own project (the document's home
607
+ resource). This held by construction for every pre-model-4 writer
608
+ (assignment surfaces loaded the anchor project's members; the actor
609
+ resolved against the anchor host), and the read funnel resolves under it.
610
+ - **`history[]` and `effectHistory[]` stay verbatim forever** — append-only
611
+ audit rows record who-acted-as-written; engine logic never compares
612
+ history actor identity for decisions (topology and provenance reads only),
613
+ and display resolves rows through the directory at presentation time.
614
+
615
+ **Why the floor rises (rule 5).** An older engine reading a model-4 instance
616
+ would not refuse it — it would MIS-COMPARE it: `$assigned` and the baked
617
+ claim gates are raw string equality, so a global-id assignee against an
618
+ older engine's project-scoped actor silently evaluates false (and the
619
+ mirror-image for legacy values under a new actor). Misinterpretation, not
620
+ parse failure — exactly the case the floor exists for. `minReaderModel: 4`
621
+ makes pre-classifier engines refuse the document loudly instead. The writer
622
+ maximum (`DATA_MODEL_MIN_READER`) rises to 4; deployment acknowledgements
623
+ follow the readers-first rollout (`docs/reader-model-rollout.md`).
624
+
625
+ Manifest feature: `classified-principal-ids` (instance, reader-floor,
626
+ **unconditional** — every instance this engine writes stamps actor identity
627
+ in the global namespace, so no instance persist can leave the floor below 4;
628
+ definitions carry no principal ids and are unaffected).
629
+
630
+ Round-trip survival: not applicable in the old-writer direction — the floor
631
+ raise means a pre-model-4 writer refuses the document before any write; the
632
+ one legal mixed state is old documents under new readers, which the read
633
+ funnel owns.
634
+
635
+ #### Model 4 also adds named polymorphic readiness requirements
636
+
637
+ Readiness becomes an ordered array of named nodes. `start.requirements`
638
+ accepts authored GROQ nodes and the declared `singleSubject` node;
639
+ `activity.requirements` becomes an array of named GROQ nodes. Each node may
640
+ carry editor-facing `title` and `description` metadata. The singular
641
+ `start.allowed` and the activity requirement record are removed.
642
+
643
+ - **Definition tree** — start and activity readiness use requirement arrays
644
+ discriminated by `type`; names are unique within their owning array.
645
+ - **Instance definition snapshot** — the same definition tree is pinned into
646
+ every new instance. Readers normalize legacy activity requirement maps in
647
+ existing snapshots into named GROQ nodes before evaluation; newly persisted
648
+ snapshots use only the array form.
649
+
650
+ Manifest feature: `readiness-requirements` (definition, reader-floor,
651
+ detectable, floor 4).
652
+
653
+ Model 4 is the default floor after this change, not a compatibility mode for
654
+ legacy deployed definitions. Deploy the model-4 definition set before running
655
+ the new reader; environments that retain incompatible earlier definitions
656
+ should follow the documented destructive reset (`nuke`) rollout path instead
657
+ of expecting those definitions to be normalized on read.
658
+
659
+ Would an old reader misread the field (rule 5)? Yes. An older engine ignores
660
+ the new arrays and therefore treats gated starts and activities as ready,
661
+ which changes the state-machine decisions it may commit. Missing arrays keep
662
+ their historical meaning (no requirements), but a present array requires a
663
+ model-4 reader. The writer floor is raised unconditionally to 4 because every
664
+ new instance embeds a model-4 definition snapshot and can later be evaluated
665
+ from that snapshot. Definitions are create-only and instance persists retain
666
+ unknown fields, so model-4 data survives round trips; the floor prevents an
667
+ older engine from interpreting it in the first place.
668
+
501
669
  ## Pending governed changes
502
670
 
503
671
  - **`temp.system.guard` → `system.guard`** — the guard doc type's `temp.`