@objectstack/plugin-approvals 17.0.0-rc.1 → 17.0.0-rc.3

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,441 @@
1
1
  # @objectstack/plugin-approvals
2
2
 
3
+ ## 17.0.0-rc.3
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [2e284b2]
8
+ - @objectstack/spec@17.0.0-rc.3
9
+ - @objectstack/core@17.0.0-rc.3
10
+ - @objectstack/formula@17.0.0-rc.3
11
+ - @objectstack/metadata-core@17.0.0-rc.3
12
+ - @objectstack/platform-objects@17.0.0-rc.3
13
+ - @objectstack/types@17.0.0-rc.3
14
+
15
+ ## 17.0.0-rc.2
16
+
17
+ ### Minor Changes
18
+
19
+ - 2826d1e: fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420)
20
+
21
+ A flow paused at an `approval` node, a deploy, then an approver clicking
22
+ Approve: the request row flipped to `approved`, the UI toasted success — and
23
+ the flow never moved. No next-stage request, no error, the record's mirrored
24
+ status frozen mid-workflow. Approval flows pause for days by design, so a
25
+ restart mid-flight is the normal case: every release could quietly zombify
26
+ every in-flight approval, with the approvers none the wiser.
27
+
28
+ Durable suspended runs (#1518) had shipped and were not the missing piece. Two
29
+ other things were.
30
+
31
+ **The wiring could enable a store over a table nobody had created.** Object
32
+ registration and store activation resolve different services in different
33
+ phases — `manifest` at `init()`, `objectql` at `start()` — and the plugin
34
+ declared no ordering. Composed ahead of ObjectQL, `init()` found no `manifest`,
35
+ warned, and continued; `start()` then attached the DB-backed store anyway. Every
36
+ suspend failed with `no such table: sys_automation_run` into a log line nobody
37
+ read, pauses silently stayed in memory, and the next restart lost them all.
38
+ Now: `AutomationServicePlugin` declares `optionalDependencies:
39
+ ['com.objectstack.engine.objectql']` (order-if-present, per ADR-0116 — an
40
+ engine-less kernel must still boot); a registration missed at `init()` is
41
+ retried at `start()`, which still lands before ObjectQL's schema sync; the
42
+ store is never attached when registration did not happen, and says so at
43
+ **error** level instead of warning; the table is probed once at boot so a
44
+ broken setup surfaces there rather than one failed write at a time; and a
45
+ failed durable write of a paused run is logged at error — it is data loss in
46
+ waiting, not a warning.
47
+
48
+ **A reported resume failure read as success.** `AutomationEngine.resume()`
49
+ answers a lost run by _returning_ `{ success: false }`, never by throwing.
50
+ `ApprovalService` discarded that return value, and `decide()` counted only a
51
+ thrown error as failure — so a decision against a dead run came back
52
+ `resumed: true`, HTTP 200. Resume failures are now classified
53
+ (`RUN_NOT_FOUND`, `STORE_UNAVAILABLE`, `RESUME_IN_PROGRESS`, joining
54
+ `PERMISSION_DENIED` / `INVALID_SIGNAL`), so a run that is gone for good is
55
+ distinguishable from a store that is merely unreachable, and the raw resume
56
+ route maps them to 404 / 503 / 409.
57
+
58
+ Approvals acts on them. A new `AutomationEngine.hasSuspendedRun(runId)` — which
59
+ reads the suspension store, unlike `getRun()`, and throws rather than answering
60
+ `false` when the store is unreadable — pre-flights every flow-advancing
61
+ operation (`decide`, `sendBack`, `resubmit`) **before its first write**, so the
62
+ zombie half-state is never created rather than merely reported: the decision
63
+ fails with `RESUME_TARGET_LOST` (HTTP 409) and the request stays actionable. A
64
+ resume that fails after the decision is durable can no longer be undone, but it
65
+ now throws `RESUME_FAILED` (HTTP 500) naming the stranded run instead of
66
+ reporting success. A concurrent duplicate resume stays benign — the engine's
67
+ idempotency guard is doing its job — and reports through the new optional
68
+ `resumeError` field. Recall and revise-window cancellation stay non-fatal by
69
+ design (they abandon the request), but log at error with the reason instead of
70
+ swallowing it. Compositions with no automation engine attached are unaffected.
71
+
72
+ Existing zombie requests from affected deployments (already `approved`, run
73
+ stranded) are not repaired by this change — `releaseDeadRunRequests` only
74
+ sweeps requests that are still `pending`.
75
+
76
+ - 0848bea: feat(spec)!: retire the overloaded `managedBy: 'system'` bucket — the residue becomes `system-data` (#3355)
77
+
78
+ **FROM → TO: `managedBy: 'system'` → `managedBy: 'system-data'`.** One-line fix:
79
+ rename the value. Nothing else about the object changes. `os migrate meta --from 16`
80
+ rewrites it for you; stored metadata is CONVERTED by the ADR-0087 entry
81
+ `object-managed-by-system-to-system-data`, never silently reinterpreted.
82
+
83
+ ADR-0103 split the overloaded `system` bucket in v16, and it split it
84
+ **additively**: the 20 engine-owned objects moved to the new explicit
85
+ `engine-owned`, while the 8 admin/user-writable ones — the RBAC link tables
86
+ (`sys_user_position`, `sys_user_permission_set`, `sys_position_permission_set`),
87
+ `sys_user_preference`, `sys_approval_delegation`, and the three messaging config
88
+ grids — stayed behind on `system`. That was the right move for a v16 that could
89
+ not break authors, but it left the enum in a state where the surviving value
90
+ names the half that had already moved out: `system` sitting on precisely the
91
+ objects a user writes.
92
+
93
+ That is not a cosmetic complaint. An author choosing between `system` and
94
+ `engine-owned` had nothing in the vocabulary to choose _on_, so the bucket was
95
+ re-overloadable by anyone reading the name in good faith — a model author most
96
+ of all, since "system table" reads as "the engine owns this" in every other
97
+ codebase. `system-data` states both boundaries explicitly: the **schema** is the
98
+ platform's (versus `platform`, which is tenant-modelled), the **data** is the
99
+ admin's or the user's (versus `engine-owned`, where the engine owns both).
100
+
101
+ Because v16 already drained the engine side, the conversion is a **one-to-one
102
+ mechanical value rename** with no judgement call — by construction every
103
+ remaining `system` declaration is writable platform data.
104
+
105
+ **One deliberate consequence — the affordance default flips.** `system` defaulted
106
+ LOCKED and each of the 8 objects re-opened its writes with a
107
+ `userActions: { create: true, edit: true, delete: true }` block. `system-data`
108
+ defaults **WRITABLE** (full CRUD), because a bucket that exists to say "the data
109
+ is yours" should not make every member ask for it back. Those blocks are now
110
+ redundant and have been deleted from the 8 platform objects; keep `userActions`
111
+ only to **NARROW**. If you converted an object that carried no `userActions`, it
112
+ gains the generic affordances — the honest reading of the bucket it moved into.
113
+
114
+ **No enforcement moves.** The engine write guard, the `DelegatedAdminGate`, RLS
115
+ and permission sets all adjudicate off resolved affordances and the principal,
116
+ never off the bucket name. `system-data` simply joins `platform` / `config` as a
117
+ bucket the fail-closed guard does not cover, because a writable default has
118
+ nothing to close on. The 8 objects passed that guard before (via `userActions`)
119
+ and pass it now (via the bucket default), for the same resolved-affordance
120
+ reason.
121
+
122
+ `'system'` is **retired from the load path**: the enum rejects it with a
123
+ prescription naming `system-data` and the one-line fix. Absorbing it silently at
124
+ load would leave every author still writing the name this rename exists to
125
+ unteach.
126
+
127
+ ### Patch Changes
128
+
129
+ - 5a84d41: fix(approvals): record an admin override of a staffed approver slate AS an override (#4466)
130
+
131
+ An admin who is not in a request's `pending_approvers` may still act on it — the
132
+ `#3424` privileged-override path exists so a request routed to an unstaffed
133
+ position, or to approvers who have all left, is not undecidable forever. The
134
+ override is defensible; what was not is what the audit trail recorded.
135
+
136
+ `sys_approval_action` had no override column at all. So an admin overriding a
137
+ properly-staffed slate wrote a row **byte-for-byte identical** to the designated
138
+ approver approving normally: a reader of the timeline saw `approve` by the admin
139
+ and could not tell whether the admin _was_ an approver or _overrode_ the ones who
140
+ were, and the bypassed approver's later `409 INVALID_STATE` was the only trace —
141
+ existing only if they happened to try. The platform knows at decision time (it
142
+ took the `isOverrideActor` branch to admit the call at all), so this was dropped
143
+ information, not unavailable information. The whole point of an approval record
144
+ is to answer "who authorized this, and were they entitled to?".
145
+
146
+ `sys_approval_action` now carries **`via_override`** (boolean, optional), set on
147
+ exactly the actions admitted by that branch — `decideNode`'s approve/reject and
148
+ `reassign`'s admin rescue. It is surfaced on `ApprovalActionRow.via_override`
149
+ (`@objectstack/spec/contracts`), returned by `listActions`, and added to the
150
+ object's `highlightFields` and two grid list views so a timeline can say
151
+ "overrode the approver slate" instead of rendering it as an ordinary approval.
152
+
153
+ Three distinctions the column keeps apart deliberately:
154
+
155
+ - **`true`** — the actor held no slot in the slate and was admitted only by the
156
+ override branch.
157
+ - **`false`** — checked, and it was not an override. An admin who _is_ a
158
+ designated approver is approving normally and records `false`: the marker is
159
+ about which branch admitted the call, not about whether the actor holds admin
160
+ rights.
161
+ - **absent** — a row written before this column existed. "Not recorded" is not
162
+ the same claim as "not an override", so `rowFromAction` maps `null` to
163
+ `undefined` rather than to `false`.
164
+
165
+ Additive and nullable, so this needs no data migration: existing rows keep
166
+ working and simply read as unrecorded. Levelled `patch` rather than `minor`
167
+ because nothing an author writes changes — but note it _is_ an observable
168
+ behaviour change on a read surface: `listActions` responses and the
169
+ `sys_approval_action` grid views now carry a field consumers did not see before,
170
+ and `sys_approval_action` gains a column on next schema sync.
171
+
172
+ - 0b795da: fix(approvals): the record lock now holds for predicate (`multi`) updates (#4778)
173
+
174
+ The ADR-0019 record lock — "while a record has a pending `sys_approval_request`,
175
+ block edits to it" — was enforced only for updates that reach the hook with an
176
+ `input.id`. The engine extracts that id from a **scalar** `where.id` alone; an
177
+ operator object (`{ $in: [...] }`) or any other predicate is a multi-row write
178
+ that routes to `updateMany` and arrives with no id. The hook opened with
179
+ `if (!id) return`, so it read _"no row was resolved"_ as _"there is nothing to
180
+ authorize"_ when the truth was _"nothing was ever queried"_.
181
+
182
+ Rewriting the very same edit as `multi: true` therefore walked straight past the
183
+ lock:
184
+
185
+ ```ts
186
+ // rec_1 carries a pending approval, lockRecord is not disabled
187
+ await ql.update(
188
+ "crm_opportunity",
189
+ { amount: 999 },
190
+ { where: { id: "rec_1" } }
191
+ ); // RECORD_LOCKED
192
+ await ql.update(
193
+ "crm_opportunity",
194
+ { amount: 999 },
195
+ { where: { id: { $in: ["rec_1"] } }, multi: true }
196
+ ); // went through
197
+ await ql.update(
198
+ "crm_opportunity",
199
+ { amount: 999 },
200
+ { where: { name: "x" }, multi: true }
201
+ ); // went through
202
+ ```
203
+
204
+ No privilege was needed for that bypass — not an `admin` role, not `isSystem`,
205
+ not `lockRecord: false`, not a whitelisted `approvalStatusField`. Every caller
206
+ shape that can spell a predicate (SDK, ObjectQL, a flow's `update_record`) could
207
+ produce it. It is the same fail-open reasoning fixed for `sys_attachment`
208
+ (#4757) and `sys_comment` (#4630), in the one place where it needed no
209
+ privilege at all.
210
+
211
+ **The hook now resolves the rows a write touches before deciding.** By-id writes
212
+ are unchanged (the driver writes by primary key, so the rest of `where` must not
213
+ narrow the verdict). A predicate write is decided by intersecting the caller's
214
+ predicate with the records that are actually locked — which is also what keeps
215
+ it cheap: the query is bounded by the object's **pending approvals**, never by
216
+ the update's match set, so a mass update of 50 000 unlocked rows costs one
217
+ bookkeeping probe and is allowed. An unscoped `multi` update over the whole
218
+ table reaches every locked row of the object and is refused while any is held.
219
+
220
+ **Fail-closed, both ways.** Past 1 000 locked records — the bound the attachment
221
+ and comment guards use — or if the intersection query fails, the write is
222
+ refused rather than allowed: the lock could not prove the write misses a locked
223
+ row. The approvals bookkeeping being unreadable at all stays the one fail-open,
224
+ as before: this hook is global over every object, so a kernel without
225
+ `sys_approval_request` would otherwise refuse every update in the deployment.
226
+ Both the bookkeeping and the match-set resolution are read under a **system**
227
+ context — a guard's own input must never be narrowed by the caller's
228
+ visibility, since a locked row you cannot read is still a row you may not write.
229
+
230
+ **Every exemption moved with the guard**, which is the other way this class of
231
+ fix goes wrong — a guard extended to more rows that carries only its deny rules
232
+ turns a fail-open into a false-positive. `isSystem`, the `admin` override, the
233
+ `approvalStatusField` status mirror, `lockRecord: false` and the owning run's
234
+ `flowRunId` (#3456 / #3712) all decide a predicate write exactly as they decide
235
+ a by-id write, each pinned by tests on both predicate shapes. Refusals now name
236
+ the record and object that are locked.
237
+
238
+ - c2a1134: fix(approvals): find the zombie requests nothing was looking at (#4469)
239
+
240
+ #4460 stopped new zombies being produced; the rows already stuck had no mechanism
241
+ to find or release them. The failure shape (#4420) is a request flipped to
242
+ `approved` / `rejected` / `returned` whose `flow_run_id` points at a run that no
243
+ longer exists — the decision landed, the flow never moved. Any deployment on
244
+ 17.0.0-rc.1 that hit the wiring hole and crossed a restart mid-approval can be
245
+ carrying these rows.
246
+
247
+ `releaseDeadRunRequests` could not see them, and the reason is worth stating
248
+ plainly: it scans `status: 'pending'`, and the very step that zombifies a request
249
+ is the one that takes it OUT of `pending`. The act of breaking it removed it from
250
+ the only sweeper's field of view — a large part of why this class of failure
251
+ stayed silent. It could not have answered the question even if it had looked: its
252
+ liveness oracle is `getRun`, which reads the execution LOG and returns `null` for
253
+ a perfectly ALIVE suspended run after a restart. It treats `null` as alive
254
+ (conservative, and correct for what it does) — which is exactly why it has no way
255
+ to say "this run is really gone".
256
+
257
+ Adds `ApprovalService.inspectStrandedRequests()`, which uses BOTH oracles and
258
+ reports only rows that fail both:
259
+
260
+ - `hasSuspendedRun(runId) === false` — the suspension store itself says no live
261
+ pause exists. It THROWS when the store cannot be read, and that case is
262
+ SKIPPED and counted as `undetermined`, never condemned: an unreadable store
263
+ means "unknown", and a storage outage must not be published as a lost run.
264
+ - `getRun(runId) == null` — no terminal history row either. A run that merely
265
+ finished is not stranded; a request whose run neither waits nor ever completed
266
+ is.
267
+
268
+ **It reports; it never rewrites.** No status is changed and no run is cancelled.
269
+ The decision genuinely happened — a human approved or rejected — and silently
270
+ rolling it back would make the audit trail disagree with the facts. The report
271
+ carries what an operator needs to decide: which requests are stuck at which step,
272
+ and what the mirrored status field on the business record still reads (usually
273
+ the stale value the user is staring at). Whether to re-run the downstream actions
274
+ or re-open the approval is a judgement call this cannot make.
275
+
276
+ It rides the existing escalation/dead-run sweep clock, so the finding surfaces in
277
+ the logs without an operator knowing to go looking for it. `recalled` is
278
+ deliberately out of scope: a recall abandons its run on purpose, and reporting
279
+ those would bury the real findings under expected ones.
280
+
281
+ New export: `StrandedApprovalRequest` (the report row shape).
282
+
283
+ - 25784cf: fix(automation,approvals): 节点类型校验推迟到插件贡献完成之后 —— approval flow 不再被误报"运行时会失败" (#4771)
284
+
285
+ showcase 每次冷启都打印 8 条断言:这些 flow "will fail at execution time"。8 条全是假的。
286
+ `AutomationServicePlugin.start()` 从 ObjectQL registry 拉起 flow 并**当场**校验节点类型,而
287
+ `ApprovalsServicePlugin.start()` 在 0.8 秒后才注册 `approval` 执行器 —— 校验器在词汇表还没
288
+ 成型的时候就下了结论。
289
+
290
+ 真正的代价不是噪音,是信号丢失:**真的没装 approvals 插件**的部署会得到一模一样的 8 条告警,
291
+ 所以这条 warn 无法区分"健康"和"坏掉",信噪比为 0。
292
+
293
+ ADR-0018 明确把节点词汇表定义为**开放、可运行时扩展**的(插件通过
294
+ `registerNodeExecutor(type)` 贡献类型)。因此校验只在词汇表**封闭**的那一刻才成立:
295
+
296
+ - `AutomationEngine.sealNodeTypeVocabulary()` —— 宣告词汇表封闭,对**所有**已注册 flow 跑一次
297
+ 权威校验,每个有问题的 flow warn 一条。`AutomationServicePlugin` 在 `kernel:bootstrapped`
298
+ 调用它(严格晚于每个插件的 `start()` 和每个 `kernel:ready` handler —— 本插件自己的
299
+ `kernel:ready` 还会再注册一批 flow,别的插件也可能在它的 `kernel:ready` 里贡献执行器)。
300
+ - `AutomationEngine.getUnknownNodeTypeAudit(): UnknownNodeTypeAuditEntry[]` —— 同一发现的
301
+ **状态**形态,供 host(CLI 启动摘要、健康检查)直接读,而不是去 grep 日志。与
302
+ `getTriggerBindingAudit()` 同一套路。
303
+ - 封闭之后 `registerFlow` **恢复即时告警**:Studio 发布 / dev reload 进正在运行的服务器时,
304
+ 词汇表确实是完整的,那句断言此时为真。所以这是时序修复,不是把告警静音。
305
+
306
+ 告警文案也随之改成它现在能承诺的事:"Every plugin has started, so nothing will register them
307
+ now — these nodes fail at execution time with NO_EXECUTOR",并给出补救动作。
308
+
309
+ 一并修掉同一缺陷类的另一半:`ApprovalsServicePlugin` 在**拿不到 automation 引擎**时,把
310
+ "`approval` 节点没注册"记成 `info` —— 而 dev 的默认日志级别是 `warn`,于是**真降级发生时反而
311
+ 看不见**(#4632:静默降级必须响亮)。现在是 `warn`,写明后果(该部署里每个 ADR-0019 approval
312
+ flow 都会以 NO_EXECUTOR 失败)和补救(装 `@objectstack/service-automation`)。`catch` 同时收窄
313
+ 到"服务查找"这一步,`registerApprovalNode` 内部真出错时会以自己的身份抛出,而不再被贴上
314
+ "no automation engine" 的错误标签;`automation` 服务存在但不接受节点执行器的分支从前**一条日志
315
+ 都不打**,现在同样 warn。
316
+
317
+ **嵌入式 host 注意**:直接 `new AutomationEngine()` 而不经过 `AutomationServicePlugin` 的宿主,
318
+ 需要在自己的插件都装好之后调用一次 `sealNodeTypeVocabulary()`,才能拿到这条告警(以及之后的
319
+ 即时校验)。
320
+
321
+ - Updated dependencies [430dcc2]
322
+ - Updated dependencies [e6ac4bd]
323
+ - Updated dependencies [80334c7]
324
+ - Updated dependencies [ce5242c]
325
+ - Updated dependencies [a7163ea]
326
+ - Updated dependencies [e6e9379]
327
+ - Updated dependencies [98877c9]
328
+ - Updated dependencies [98877c9]
329
+ - Updated dependencies [c44dd5e]
330
+ - Updated dependencies [e6b1b69]
331
+ - Updated dependencies [ad047d2]
332
+ - Updated dependencies [2826d1e]
333
+ - Updated dependencies [5a84d41]
334
+ - Updated dependencies [20b1a9e]
335
+ - Updated dependencies [203a449]
336
+ - Updated dependencies [ac37fc6]
337
+ - Updated dependencies [4820f55]
338
+ - Updated dependencies [462d9c4]
339
+ - Updated dependencies [7d21581]
340
+ - Updated dependencies [f2445c9]
341
+ - Updated dependencies [23338c3]
342
+ - Updated dependencies [5b843fb]
343
+ - Updated dependencies [b4487aa]
344
+ - Updated dependencies [65ca83a]
345
+ - Updated dependencies [67bf2e2]
346
+ - Updated dependencies [c6d1cb4]
347
+ - Updated dependencies [36030ff]
348
+ - Updated dependencies [6117f7b]
349
+ - Updated dependencies [e533b0b]
350
+ - Updated dependencies [cdf4d9a]
351
+ - Updated dependencies [aee1806]
352
+ - Updated dependencies [c13350b]
353
+ - Updated dependencies [c13350b]
354
+ - Updated dependencies [9ca2d85]
355
+ - Updated dependencies [c13350b]
356
+ - Updated dependencies [891d345]
357
+ - Updated dependencies [a52e2ef]
358
+ - Updated dependencies [5293114]
359
+ - Updated dependencies [20bc357]
360
+ - Updated dependencies [5966c2a]
361
+ - Updated dependencies [2382580]
362
+ - Updated dependencies [d9fa683]
363
+ - Updated dependencies [3c7bcc0]
364
+ - Updated dependencies [4b6cac7]
365
+ - Updated dependencies [7631964]
366
+ - Updated dependencies [ac471a0]
367
+ - Updated dependencies [60ae58e]
368
+ - Updated dependencies [ce92674]
369
+ - Updated dependencies [9f601e8]
370
+ - Updated dependencies [51c5227]
371
+ - Updated dependencies [a4a85c8]
372
+ - Updated dependencies [07a4e26]
373
+ - Updated dependencies [ec975f1]
374
+ - Updated dependencies [eb4204b]
375
+ - Updated dependencies [4f13be2]
376
+ - Updated dependencies [61cc079]
377
+ - Updated dependencies [0e96e46]
378
+ - Updated dependencies [b25a116]
379
+ - Updated dependencies [d52d4fe]
380
+ - Updated dependencies [742cebb]
381
+ - Updated dependencies [ce92674]
382
+ - Updated dependencies [cf2c9b7]
383
+ - Updated dependencies [833b512]
384
+ - Updated dependencies [0f9faa2]
385
+ - Updated dependencies [7cf42fe]
386
+ - Updated dependencies [5966c2a]
387
+ - Updated dependencies [f78dd83]
388
+ - Updated dependencies [a2cd18a]
389
+ - Updated dependencies [4638aaa]
390
+ - Updated dependencies [0222d3c]
391
+ - Updated dependencies [071d0dc]
392
+ - Updated dependencies [0a936ea]
393
+ - Updated dependencies [023c00b]
394
+ - Updated dependencies [155507e]
395
+ - Updated dependencies [7bba90b]
396
+ - Updated dependencies [7e05d8e]
397
+ - Updated dependencies [061406d]
398
+ - Updated dependencies [c1f344b]
399
+ - Updated dependencies [9c93465]
400
+ - Updated dependencies [ebb209c]
401
+ - Updated dependencies [65f184b]
402
+ - Updated dependencies [63b33e6]
403
+ - Updated dependencies [2a44c1d]
404
+ - Updated dependencies [695cfbd]
405
+ - Updated dependencies [7445149]
406
+ - Updated dependencies [071d0dc]
407
+ - Updated dependencies [0848bea]
408
+ - Updated dependencies [d51bed2]
409
+ - Updated dependencies [b8b3c64]
410
+ - Updated dependencies [0c0fbd9]
411
+ - Updated dependencies [f3141d8]
412
+ - Updated dependencies [5a84d41]
413
+ - Updated dependencies [fd3013a]
414
+ - Updated dependencies [21676eb]
415
+ - Updated dependencies [e336549]
416
+ - Updated dependencies [d40f43a]
417
+ - Updated dependencies [e5e7ee0]
418
+ - Updated dependencies [a2ebea2]
419
+ - Updated dependencies [800bdb0]
420
+ - Updated dependencies [04f1182]
421
+ - Updated dependencies [5647006]
422
+ - Updated dependencies [38f7e4f]
423
+ - Updated dependencies [c57f3cf]
424
+ - Updated dependencies [97faca3]
425
+ - Updated dependencies [ad5fe25]
426
+ - Updated dependencies [ea90179]
427
+ - Updated dependencies [ce92674]
428
+ - Updated dependencies [5ef0b5b]
429
+ - Updated dependencies [48fbacb]
430
+ - Updated dependencies [355e951]
431
+ - Updated dependencies [dadb43f]
432
+ - @objectstack/spec@17.0.0-rc.2
433
+ - @objectstack/platform-objects@17.0.0-rc.2
434
+ - @objectstack/core@17.0.0-rc.2
435
+ - @objectstack/types@17.0.0-rc.2
436
+ - @objectstack/metadata-core@17.0.0-rc.2
437
+ - @objectstack/formula@17.0.0-rc.2
438
+
3
439
  ## 17.0.0-rc.1
4
440
 
5
441
  ### Minor Changes