@objectstack/plugin-approvals 17.0.0-rc.6 → 17.1.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,2909 @@
1
1
  # @objectstack/plugin-approvals
2
2
 
3
+ ## 17.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 08d6d3c: feat(approvals): read-only approval visibility for users who can read the target record, per object, default OFF (#8652)
8
+
9
+ A new `ApprovalsPluginOptions.recordReaderVisibleObjects` names the objects on
10
+ which **a user who can READ a business record may also see that record's
11
+ approval requests and full action history** — read-only. Omitted or empty (the
12
+ default) leaves visibility exactly as it is today, so an existing deployment
13
+ sees no behaviour change on upgrade. **This is not a no-op change**: on an
14
+ object you list, a population that could previously see nothing gains a real
15
+ read.
16
+
17
+ ```ts
18
+ new ApprovalsServicePlugin({ recordReaderVisibleObjects: ['exam_sheet'] })
19
+ ```
20
+
21
+ **Who gains visibility.** Until now the visible set was submitter ∪ current
22
+ approver ∪ historical actor, with a platform/tenant admin override as the only
23
+ bypass — so a ledger or supervisor role that holds full read on the record but
24
+ never appears in the approval itself received `200` with an empty list, and the
25
+ Console's approval tab never rendered. On an enabled object, that role now sees
26
+ the record's approvals.
27
+
28
+ **What becomes visible on an enabled object**, stated plainly because the switch
29
+ is an opt-in decision about confidentiality:
30
+
31
+ - the approval request row, including its `payload` snapshot of the record as it
32
+ stood at submission time;
33
+ - the full action history — each actor, their decision, the timestamp, **and the
34
+ action's comment text** (意见正文);
35
+ - decision attachments on those actions, which are gated on the same rule.
36
+
37
+ Enable it on objects whose approval commentary the record's readers are meant to
38
+ see; the comment text is often evaluative, and it is per object precisely so
39
+ that enabling it for a ledger object does not enable it for anything else.
40
+
41
+ **What does NOT change.**
42
+
43
+ - **Read-only.** No approval action is delivered through this tier. Approve,
44
+ reject, reassign, recall and comment keep authorizing exactly as before — on
45
+ the pending-approver slate, the submitter, or admin override — and a viewer
46
+ admitted by this tier gets `can_act: false`. Seeing a request confers nothing.
47
+ - **No new permission concept.** The tier is anchored on the existing
48
+ record-read permission: the service asks the engine to read the record **as
49
+ the caller**, so ordinary object CRUD and RLS decide. No new role, grant type
50
+ or policy, and no host-injected visibility hook — a security predicate the
51
+ platform can neither constrain nor audit was considered and rejected.
52
+ - **The inbox.** An untargeted list is unchanged. The rule is anchored on one
53
+ record, so it applies only where a record is named — a list filtered by
54
+ `object` + `recordId` (what a record page's approval tab sends), or a request
55
+ loaded by id. A work queue does not become a browse surface.
56
+ - **Tenant isolation, and everything else about the existing visible set.** The
57
+ tier only ever adds ids to the participant set; it can never return the "sees
58
+ everything" verdict and never relaxes an existing constraint.
59
+
60
+ ### Patch Changes
61
+
62
+ - 66beee0: Guard every authored record-scoped action predicate for the sparse action face, so a list row that did not project the gated column no longer silently drops the button.
63
+
64
+ An action's `visible` / `disabled` predicate binds whatever record the client already fetched — a record-detail read, or a list row carrying only the view's `$select` projection. That binding stays sparse by decision (it is the one record binding the platform does not make total), and CEL aborts the whole expression at key resolution when a key is absent. The abort is fail-closed, so the button is simply not offered — indistinguishable to the user from the gate having said no, and reported nowhere.
65
+
66
+ Every authored predicate on `sys_user`, `sys_invitation`, `sys_member`, `sys_oauth_application` and `sys_approval_request` now opens each `record.*` read with `has()`. The guard is the minimal measured form per predicate, not one blanket rewrite: a bare equality against a literal needs `has()` alone, because CEL compares heterogeneously and answers `false` on a projected-null column rather than faulting.
67
+
68
+ Two predicates change what a user sees, both on `sys_oauth_application`, whose `disabled` column is nullable upstream and therefore null on every application nobody has ever toggled:
69
+
70
+ - `disable_oauth_application` was `!record.disabled`, which faulted on a projected-null row (`!` needs a bool) — so the Disable button was missing from every never-toggled application in the list. It is now `has(record.disabled) && record.disabled != true` and is offered.
71
+ - `enable_oauth_application` was `record.disabled`, which answered `null` rather than a boolean and left the decision to the renderer. It is now `has(record.disabled) && record.disabled == true`.
72
+
73
+ `sys_approval_request`'s decision levers gate on the attached `record.viewer` block and traverse, so they are guarded at the leaf (`has(record.viewer) && has(record.viewer.can_act) && record.viewer.can_act == true`). Measured, that is the minimal safe form for a nested read: the canonical `has(x) && x != null` conjunction still faults when the block is present but the flag is absent or null, while a leaf `has()` subsumes the parent `!= null` half. Their intended fail-closed behaviour is unchanged — it is now a real `false` instead of an evaluation fault.
74
+ - 7ff5aa2: `sys_automation_run` resolves its organization from the DECLARED `AutomationContext.tenantId` and from no other spelling (cloud#1395).
75
+
76
+ The suspended-run store read `context.organizationId ?? context.tenantId`. `AutomationContext` declares `tenantId` and not `organizationId`, and no producer writes the latter — `RecordChangeTrigger.buildContext` maps the hook session's organization onto `tenantId`, and the runtime's automation domain sets `tenantId` directly. The dead limb was not inert: the one test covering `sys_automation_run.organization_id` fed the phantom key, so the column's only coverage exercised a path production cannot reach and said nothing about the live one. The limb is removed, the fixture speaks the declared contract, and a test now asserts the absence so restoring the alias goes red.
77
+
78
+ Both `sys_approval_request.organization_id` and `sys_automation_run.organization_id` now document the measured attribution defect this uncovered and the negative control that makes it a defect: on a walled single-database boot these two tables stored customer activity with no organization (27/27 and 31/31) while `sys_audit_log` (1669 rows) was correctly attributed on the same boot, because the audit writer resolves the organization from the record the row is ABOUT rather than from the acting context. The write-side repair is not in this change — which column a side-table row should follow is an open contract question, since the audit resolver is scope-pinned to audit stamping by the #8778 ruling. The current behaviour is pinned by test so the fix must promote the assertion rather than quietly satisfy it.
79
+ - Updated dependencies [56656aa]
80
+ - Updated dependencies [c9f5950]
81
+ - Updated dependencies [d6e80b2]
82
+ - Updated dependencies [07e630e]
83
+ - Updated dependencies [66beee0]
84
+ - Updated dependencies [2f65b1b]
85
+ - Updated dependencies [720ee95]
86
+ - Updated dependencies [f287435]
87
+ - Updated dependencies [2782805]
88
+ - Updated dependencies [e43d63a]
89
+ - Updated dependencies [9aa8890]
90
+ - Updated dependencies [7c9c1dd]
91
+ - Updated dependencies [03520eb]
92
+ - Updated dependencies [75b7c24]
93
+ - Updated dependencies [d5552ca]
94
+ - Updated dependencies [d9813a9]
95
+ - Updated dependencies [8640fb2]
96
+ - Updated dependencies [2420641]
97
+ - Updated dependencies [2ad91c3]
98
+ - Updated dependencies [f57fb38]
99
+ - Updated dependencies [00777a0]
100
+ - Updated dependencies [d491625]
101
+ - Updated dependencies [2d0af57]
102
+ - Updated dependencies [420804d]
103
+ - Updated dependencies [716ac9b]
104
+ - Updated dependencies [a38408a]
105
+ - Updated dependencies [62b1427]
106
+ - Updated dependencies [7ea1372]
107
+ - Updated dependencies [23abe27]
108
+ - Updated dependencies [985a9cd]
109
+ - Updated dependencies [5f5e234]
110
+ - Updated dependencies [a8189ae]
111
+ - Updated dependencies [26e70fb]
112
+ - Updated dependencies [27a567d]
113
+ - Updated dependencies [42b05af]
114
+ - Updated dependencies [2b292ce]
115
+ - Updated dependencies [abcf853]
116
+ - Updated dependencies [8b9eba5]
117
+ - Updated dependencies [d575779]
118
+ - Updated dependencies [94f7ef8]
119
+ - Updated dependencies [c5ac5e4]
120
+ - Updated dependencies [a777944]
121
+ - Updated dependencies [dd88e1c]
122
+ - Updated dependencies [856527c]
123
+ - Updated dependencies [870f710]
124
+ - Updated dependencies [79c46da]
125
+ - Updated dependencies [7ff3975]
126
+ - Updated dependencies [29d055b]
127
+ - Updated dependencies [65589d6]
128
+ - Updated dependencies [2c86fe3]
129
+ - Updated dependencies [e196c6a]
130
+ - Updated dependencies [24173e9]
131
+ - Updated dependencies [4ab7523]
132
+ - Updated dependencies [19539b4]
133
+ - Updated dependencies [f8eb736]
134
+ - Updated dependencies [11b779e]
135
+ - Updated dependencies [739fe5b]
136
+ - Updated dependencies [4bfe1a5]
137
+ - Updated dependencies [2065e31]
138
+ - Updated dependencies [b69d0f5]
139
+ - Updated dependencies [4d47afe]
140
+ - Updated dependencies [e4e5c6e]
141
+ - Updated dependencies [9a56784]
142
+ - Updated dependencies [d00d2f6]
143
+ - Updated dependencies [df0c12d]
144
+ - Updated dependencies [d31785f]
145
+ - Updated dependencies [c308a4f]
146
+ - Updated dependencies [e2899f6]
147
+ - Updated dependencies [b6c7690]
148
+ - Updated dependencies [3851f87]
149
+ - Updated dependencies [845e164]
150
+ - Updated dependencies [2a29caa]
151
+ - Updated dependencies [09a6eee]
152
+ - Updated dependencies [1a7f907]
153
+ - Updated dependencies [cd455c8]
154
+ - Updated dependencies [e1bb0ca]
155
+ - Updated dependencies [30d3752]
156
+ - Updated dependencies [c80e7ae]
157
+ - Updated dependencies [09a9a8a]
158
+ - Updated dependencies [07026cf]
159
+ - Updated dependencies [5d4f3d5]
160
+ - Updated dependencies [4d80e8b]
161
+ - Updated dependencies [30b1c63]
162
+ - Updated dependencies [7fc01db]
163
+ - Updated dependencies [079b457]
164
+ - Updated dependencies [e43b211]
165
+ - Updated dependencies [890b38f]
166
+ - Updated dependencies [8bee54b]
167
+ - Updated dependencies [04f8fdb]
168
+ - Updated dependencies [7a537ce]
169
+ - Updated dependencies [593c4bf]
170
+ - Updated dependencies [6158146]
171
+ - Updated dependencies [84cb121]
172
+ - Updated dependencies [ca19ee8]
173
+ - Updated dependencies [a675b4d]
174
+ - Updated dependencies [b887013]
175
+ - Updated dependencies [ff08691]
176
+ - Updated dependencies [60e0f90]
177
+ - Updated dependencies [90c5285]
178
+ - Updated dependencies [402c125]
179
+ - Updated dependencies [7901b2d]
180
+ - Updated dependencies [56bca91]
181
+ - Updated dependencies [b3f9831]
182
+ - Updated dependencies [79394d7]
183
+ - Updated dependencies [730fd9a]
184
+ - Updated dependencies [44bc51d]
185
+ - Updated dependencies [bbbfcfc]
186
+ - Updated dependencies [73cfddf]
187
+ - Updated dependencies [d634e66]
188
+ - @objectstack/spec@17.1.0
189
+ - @objectstack/platform-objects@17.1.0
190
+ - @objectstack/types@17.1.0
191
+ - @objectstack/core@17.1.0
192
+ - @objectstack/metadata-core@17.1.0
193
+ - @objectstack/formula@17.1.0
194
+
195
+ ## 17.0.0
196
+
197
+ ### Minor Changes
198
+
199
+ - 14252d3: feat(approvals): cross-organization approver targeting — a plant document can
200
+ require a group-side sign-off (ADR-0105 D9)
201
+
202
+ One organization id used to decide three different things at once in
203
+ `openNodeRequest`: where the request row lives, where its inbox index rows
204
+ live, and **where its approvers are looked up**. The first two are the
205
+ request's own organization by definition. The third is not — a group CFO holds
206
+ her `cfo` position in the GROUP organization while the purchase order she signs
207
+ off lives in the PLANT organization. `expandPositionUsers('cfo', <plant>)`
208
+ matched nobody, the slot fell back to the dead `position:cfo` literal, and a
209
+ group escalation could not be expressed at all.
210
+
211
+ An approver may now declare which organization's directory resolves it:
212
+
213
+ ```yaml
214
+ approvers:
215
+ - { type: position, value: plant_manager, group: plant }
216
+ - { type: position, value: cfo, organization: $root, group: finance }
217
+ behavior: per_group
218
+ ```
219
+
220
+ - **`$root` / `$parent`** walk D6's `parent_organization_id` tree, so the two
221
+ common intents need **no deployment knowledge** — flow metadata is portable
222
+ across environments while organization ids are minted per deployment. A slug
223
+ covers what the symbols cannot, notably a **sibling** organization (a
224
+ shared-services centre approving payables for every plant).
225
+ - Declared **per approver**, so one node can require a plant manager and a
226
+ group CFO in parallel. A node-level form cannot express that without
227
+ splitting into serial nodes, which changes the semantics.
228
+ - **Bounded, not free:** the target must share a `parent_organization_id` root
229
+ with the request's organization. The rule reads only the organization tree —
230
+ never the submitter — so one flow routes identically for everyone.
231
+
232
+ Everything else fails loudly rather than quietly:
233
+
234
+ - a non-`group` posture **refuses** the declaration (a `group` → `isolated`
235
+ migration must not silently reroute approvals);
236
+ - an approver type with no org-scoped directory (`user` / `field` / `manager` /
237
+ `team`) refuses it too, and a new `approval-approver-cross-org-unsupported`
238
+ lint catches that at author time;
239
+ - a targeted approver holding no membership in the request's organization is
240
+ dropped with a warning naming them — D2's union wall would otherwise hide the
241
+ request from someone already routed to, so the node's existing
242
+ `onEmptyApprovers` policy takes over instead of leaving an unopenable task.
243
+
244
+ Nothing changes for an approver without `organization`: same resolution, same
245
+ queries, no extra reads.
246
+
247
+ - f5a4ef0: refactor!: ADR-0112 batch 2 — sweep the lowercase error-code emitters (#4003)
248
+
249
+ Continues #3841 per ADR-0112. Batch 1 (#3988) settled the vocabulary and closed
250
+ the set; this batch moves the emitters that still spoke lowercase `snake_case`
251
+ onto it.
252
+
253
+ **Wire-visible change.** Error codes on these surfaces change spelling. Generic
254
+ conditions collapse onto the standard catalog rather than keeping a synonym:
255
+ `unauthorized`/`unauthenticated` → `UNAUTHENTICATED`, `forbidden` →
256
+ `PERMISSION_DENIED`, `not_found` → `RESOURCE_NOT_FOUND`, `internal` →
257
+ `INTERNAL_ERROR`, `unavailable` → `SERVICE_UNAVAILABLE`, `not_supported` →
258
+ `NOT_IMPLEMENTED`, `bad_request` → `INVALID_REQUEST`. Domain conditions get codes
259
+ registered in `ERROR_CODE_LEDGER` (`MARKETPLACE_STORAGE_FAILED`,
260
+ `PLUGIN_MANIFEST_INVALID`, `ITEM_LOCKED`, `DELIVERY_NOT_ELIGIBLE`, …). Swept:
261
+ `cloud-connection`, `plugin-auth`, `hono`, `metadata-protocol`, `rest`,
262
+ `service-messaging`, `service-automation`, `trigger-api`.
263
+
264
+ Branch on `error.code` values rather than pattern-matching their case: the
265
+ console's fix for the same rename (objectui#2977) reads codes case-insensitively
266
+ for exactly this reason, and that is the pattern to copy in your own consumers if
267
+ you support servers on both sides of the change.
268
+
269
+ **Four routes stop putting a code in the message slot.** The webhook redeliver
270
+ route, the API-trigger webhook, and two `rest` routes answered
271
+ `{ success: false, error: '<code>', message }` — the code occupying `error`, the
272
+ declared object envelope nowhere. They now emit `error: { code, message }`, and
273
+ three API-trigger branches gained a message they never had. Clients reading
274
+ `body.error` as a string on those routes must read `body.error.code`.
275
+
276
+ **`ConnectorErrorCategory` / `ConnectorRetryStrategy`** (ADR-0112 D9a):
277
+ `@objectstack/spec` exported two mutually incompatible `ErrorCategory` types and
278
+ two `RetryStrategy` types. The connector-side pair is renamed; importers of the
279
+ `integration` subpath update the name. Side effect: the api-side `ErrorCategory`
280
+ and `RetryStrategy` now appear in the generated API reference at all — the name
281
+ collision had been silently dropping them.
282
+
283
+ **`OAUTH_REGISTER_FAILED` replaces an unbounded code source.** The OAuth client
284
+ registration route put better-auth's arbitrary `body.error` string straight into
285
+ `error.code`. The code is now ours and the upstream discriminator moved to
286
+ `details.upstreamError`.
287
+
288
+ **Not swept, deliberately.** `sys_metadata_audit.code` keeps its lowercase values
289
+ (ADR-0112 D6b): it is persisted audit history, and the same column holds
290
+ non-error outcomes (`ok`, `lock_override`). Diagnostics records that ship inside a
291
+ 200 keep theirs (D6c), as do field-level codes (D6, #3977) and the CLI's
292
+ `--json` output contract.
293
+
294
+ A `check:error-code-casing` CI guard now fails on a new lowercase literal in a
295
+ code position, since the ledger's casing rule can only police codes that someone
296
+ registers.
297
+
298
+ - f92096b: fix(approvals): an approval action is recorded against the authenticated caller, never a body field (#3800)
299
+
300
+ Every mutating approvals entrypoint takes an `actorId`, and the REST routes
301
+ filled it from `body.actorId ?? body.actor_id ?? context.userId` — so the body
302
+ won. The service then authorized _that value_: `pending_approvers.includes(
303
+ input.actorId)` for a decision, `submitter_id === actorId` for a recall. It never
304
+ checked that the value named the caller.
305
+
306
+ So any authenticated user could POST `{"actorId": "<someone else>"}` and have
307
+ that person's approval recorded, the request finalized, and the owning flow run
308
+ resumed down the `approve` edge — or name a request's submitter and recall it.
309
+ With `api.requireAuth` unset the anonymous-deny never fires either, so an
310
+ unauthenticated request could do the same.
311
+
312
+ #3783 drew this line for the _data-write_ identity and called the audit-row half
313
+ "tolerable". It was not: the same unchecked string was the authorization key, so
314
+ naming someone else was not a mislabelled audit row, it was how you got through
315
+ the door.
316
+
317
+ The actor is now resolved server-side (`ApprovalService.resolveActor`) on all
318
+ nine entrypoints — `decide` / `decideNode`, `recall`, `sendBack`, `resubmit`,
319
+ `reassign`, `remind`, `requestInfo`, `comment`.
320
+
321
+ **The rule is not "`actorId` must equal `context.userId`."** A slot can
322
+ legitimately be keyed by something else: the approver resolver stores the
323
+ `type:value` literal when a graph lookup finds no holders, and the Console picks
324
+ from the caller's own identity list — user id, email, or `role:<r>`. The rule is
325
+ **"the actor must be an identity the server can prove belongs to the caller"**:
326
+
327
+ - A **system** context keeps its explicit actor. The SLA sweep's reserved
328
+ `system:sla` sentinel and the ADR-0043 action link — whose single-use hashed
329
+ token binds exactly one approver — are unchanged. They are the only callers
330
+ holding a trustworthy actor with no session behind them.
331
+ - A caller with **no identity at all** is now refused. This is the anonymous case
332
+ above.
333
+ - **No `actorId`, or one naming the caller**, resolves to the caller. This is the
334
+ common path and what the Console already sends.
335
+ - **Any other value** is accepted only when the server can prove the caller holds
336
+ it — `position:<p>` / `role:<p>` against the positions on the resolved authz
337
+ context, or the caller's own email (one lazy `sys_user` read, taken only when
338
+ nothing cheaper matched). Otherwise `FORBIDDEN`.
339
+
340
+ REST still forwards the body value; it is now a _hint_ the service validates,
341
+ which is what keeps the email and `type:value` slot cases working.
342
+
343
+ **Upgrade note.** A client that deliberately sent another user's `actorId` now
344
+ gets `403 FORBIDDEN` instead of silently succeeding. Send the action as the
345
+ acting user's own session — the field can be omitted entirely, and the caller is
346
+ used. Server-to-server callers that legitimately act for someone else should
347
+ present a system context, as the SLA sweep and the action link already do.
348
+
349
+ This also makes two existing claims true that were previously aspirational: the
350
+ approval object's declared actions say "`actorId` defaults to the caller
351
+ server-side… the service remains the authority on who may act", and
352
+ `attachViewers` documents `can_act` as mirroring "the exact authorization the
353
+ decision methods enforce".
354
+
355
+ - 2826d1e: fix(automation,approvals): an approval decision can no longer succeed while its flow stays parked (#4420)
356
+
357
+ A flow paused at an `approval` node, a deploy, then an approver clicking
358
+ Approve: the request row flipped to `approved`, the UI toasted success — and
359
+ the flow never moved. No next-stage request, no error, the record's mirrored
360
+ status frozen mid-workflow. Approval flows pause for days by design, so a
361
+ restart mid-flight is the normal case: every release could quietly zombify
362
+ every in-flight approval, with the approvers none the wiser.
363
+
364
+ Durable suspended runs (#1518) had shipped and were not the missing piece. Two
365
+ other things were.
366
+
367
+ **The wiring could enable a store over a table nobody had created.** Object
368
+ registration and store activation resolve different services in different
369
+ phases — `manifest` at `init()`, `objectql` at `start()` — and the plugin
370
+ declared no ordering. Composed ahead of ObjectQL, `init()` found no `manifest`,
371
+ warned, and continued; `start()` then attached the DB-backed store anyway. Every
372
+ suspend failed with `no such table: sys_automation_run` into a log line nobody
373
+ read, pauses silently stayed in memory, and the next restart lost them all.
374
+ Now: `AutomationServicePlugin` declares `optionalDependencies:
375
+ ['com.objectstack.engine.objectql']` (order-if-present, per ADR-0116 — an
376
+ engine-less kernel must still boot); a registration missed at `init()` is
377
+ retried at `start()`, which still lands before ObjectQL's schema sync; the
378
+ store is never attached when registration did not happen, and says so at
379
+ **error** level instead of warning; the table is probed once at boot so a
380
+ broken setup surfaces there rather than one failed write at a time; and a
381
+ failed durable write of a paused run is logged at error — it is data loss in
382
+ waiting, not a warning.
383
+
384
+ **A reported resume failure read as success.** `AutomationEngine.resume()`
385
+ answers a lost run by _returning_ `{ success: false }`, never by throwing.
386
+ `ApprovalService` discarded that return value, and `decide()` counted only a
387
+ thrown error as failure — so a decision against a dead run came back
388
+ `resumed: true`, HTTP 200. Resume failures are now classified
389
+ (`RUN_NOT_FOUND`, `STORE_UNAVAILABLE`, `RESUME_IN_PROGRESS`, joining
390
+ `PERMISSION_DENIED` / `INVALID_SIGNAL`), so a run that is gone for good is
391
+ distinguishable from a store that is merely unreachable, and the raw resume
392
+ route maps them to 404 / 503 / 409.
393
+
394
+ Approvals acts on them. A new `AutomationEngine.hasSuspendedRun(runId)` — which
395
+ reads the suspension store, unlike `getRun()`, and throws rather than answering
396
+ `false` when the store is unreadable — pre-flights every flow-advancing
397
+ operation (`decide`, `sendBack`, `resubmit`) **before its first write**, so the
398
+ zombie half-state is never created rather than merely reported: the decision
399
+ fails with `RESUME_TARGET_LOST` (HTTP 409) and the request stays actionable. A
400
+ resume that fails after the decision is durable can no longer be undone, but it
401
+ now throws `RESUME_FAILED` (HTTP 500) naming the stranded run instead of
402
+ reporting success. A concurrent duplicate resume stays benign — the engine's
403
+ idempotency guard is doing its job — and reports through the new optional
404
+ `resumeError` field. Recall and revise-window cancellation stay non-fatal by
405
+ design (they abandon the request), but log at error with the reason instead of
406
+ swallowing it. Compositions with no automation engine attached are unaffected.
407
+
408
+ Existing zombie requests from affected deployments (already `approved`, run
409
+ stranded) are not repaired by this change — `releaseDeadRunRequests` only
410
+ sweeps requests that are still `pending`.
411
+
412
+ - 91f4c78: feat(approvals,spec): structured reassign hand-off parties on `sys_approval_action` (#4365)
413
+
414
+ A reassign's audit row used to encode "who handed the slot to whom" only inside
415
+ a default free-text comment — `"<from_id> → <to_id>"`, two raw user ids — which
416
+ clients could neither parse reliably nor render readably, so the approvals
417
+ timeline showed opaque identifier soup for the single most important fact of
418
+ the entry.
419
+
420
+ - `sys_approval_action` gains `reassign_from` / `reassign_to`
421
+ (`lookup('sys_user')`), written by `ApprovalService.reassign()`.
422
+ - `comment` is pure user input again: nothing is invented when the actor
423
+ supplies none.
424
+ - `listActions()` resolves both parties' display names into
425
+ `reassign_from_name` / `reassign_to_name`, alongside the existing
426
+ `actor_name`, so timelines can render "from A to B" without extra lookups.
427
+ - `ApprovalActionRow` (spec contract) declares the four new fields.
428
+
429
+ Pre-existing rows keep their legacy comment; clients should prefer the
430
+ structured fields when present and fall back to `comment` otherwise.
431
+
432
+ - ddc2527: fix(approvals): the ADR-0044 revise window is a service-owned node type, not a bare `wait` (#3823)
433
+
434
+ #3801 gated `POST /api/v1/automation/:name/runs/:runId/resume` on the **node type**
435
+ that produced the suspension: an `approval` pause declares
436
+ `resumeAuthority: 'service'`, so it continues only through `ApprovalService`.
437
+ ADR-0044's **revise window** was the same trust boundary in a shape that key
438
+ could not see. Send-back parked the run on an ordinary `wait` node the flow
439
+ author placed — correctly `resumeAuthority: 'any'`, because a signal wait is
440
+ _meant_ to be resumable by an external producer — and `ApprovalService.resubmit`
441
+ was the only thing that checked anything about continuing it.
442
+
443
+ Demonstrated (not reasoned) against the real engine: a raw `resume(runId)` with
444
+ an **empty body**, from any caller, walked the `resubmit` back-edge into the
445
+ approval node and opened round N+1 with **no submitter check and no `resubmit`
446
+ audit row** (`['submit','revise']` — no third row, ever). Worse, when another
447
+ request was already pending on the record — the exact case `resubmit` refuses
448
+ with `DUPLICATE_REQUEST` _specifically to keep the run alive_ — the raw resume
449
+ went around that guard: the approval node's re-entry failed **after** the engine
450
+ consumed the suspension, and the run was **permanently destroyed** with its
451
+ round-N request stuck `returned` and no resubmit able to reach it.
452
+
453
+ The revise pause is therefore its own node type:
454
+
455
+ - **`approval_revise`** (`APPROVAL_REVISE_NODE_TYPE`), registered by
456
+ `@objectstack/plugin-approvals` alongside the `approval` node, declaring
457
+ `resumeAuthority: 'service'`. It stays a first-class box on the canvas, in the
458
+ run log and in the suspended-run store — only the _reuse_ of `wait` was wrong.
459
+ It takes **no config**: the window ends on the submitter's explicit resubmit,
460
+ never on a signal or timer. The `resumeAuthority` gate itself is unchanged.
461
+ - `sendBack` refuses a `revise` edge whose target is not an `approval_revise`
462
+ node, **before any mutation** (like the existing missing-`revise`-edge check),
463
+ so no run can be parked in a window something else can advance.
464
+ - New gating lint `flow-approval-revise-target-not-service-owned`
465
+ (severity `error`, on `os build` / `os validate` / `os lint` and the runtime
466
+ metadata publish gate) rejects the old shape at authoring time.
467
+
468
+ **Upgrading a flow authored against the original ADR-0044 D3.** One token:
469
+
470
+ - **FROM:** `{ id: 'wait_revision', type: 'wait', waitEventConfig: { eventType: 'signal', … } }`
471
+ - **TO:** `{ id: 'wait_revision', type: 'approval_revise' }` — drop
472
+ `waitEventConfig` / any `config`; the window has no event to wait on.
473
+
474
+ Until you do, such a flow keeps registering and running and its approvals stay
475
+ decidable (`approve` / `reject` / `recall` / `reassign` are untouched), but
476
+ **send-back is refused** with a message naming the node and this fix, and
477
+ re-publishing it reports the lint error. A run _already parked_ in a legacy
478
+ revise window keeps its recorded node type (a republish never re-types a live
479
+ pause) and is drained by `resubmit` or `recall` as usual.
480
+
481
+ ADR-0044's 2026-07-28 amendment records the reversal of its D3 and of its
482
+ `Alternatives` rejection of a service-owned revise pause, with the evidence
483
+ above; the implementation section there records what shipped, why the approval
484
+ node does not re-suspend itself instead, and why no ADR-0087 conversion was
485
+ added for the old shape.
486
+
487
+ - fb90784: fix(approvals): the status mirror names the human who caused the transition (#3783)
488
+
489
+ When an approval moves, the service writes the new status onto the business
490
+ record (`approvalStatusField`). That write is what fires the record-change flows
491
+ bound to that object — so it is the seam "when the invoice is approved, do X"
492
+ runs through. It presented a bare `{ isSystem: true }` context with **no
493
+ `userId`**, at six call sites that each know exactly who acted: a submitter
494
+ submitting, an approver approving, rejecting, sending back, recalling.
495
+
496
+ Combined with #3760 — which stopped letting a `runAs:'user'` run with no trigger
497
+ user touch data — that identity gap made the most natural approvals automation
498
+ there is unwritable in its obvious form. The cascade inherited no user, so its
499
+ data nodes were refused, and the author's only way forward was to declare
500
+ `runAs: 'system'` and take blanket elevation for a case where a perfectly good
501
+ scoped identity existed at the call site all along.
502
+
503
+ The mirror now carries the acting user. It stays `isSystem` — the record is
504
+ normally locked while its approval is live, so only a platform write can land the
505
+ status — because elevation and anonymity are separate choices, and this write
506
+ only ever needed the first. Cascades now run as the deciding user with RLS
507
+ enforced.
508
+
509
+ - **The identity is the authenticated principal, never the request body's
510
+ `actorId`.** `actorId` arrives from the caller (`body.actorId ?? context.userId`)
511
+ and is only checked against the pending approver slate, never against the
512
+ caller. That is tolerable on an audit row; promoting it to the identity of an
513
+ RLS-scoped write would have turned a mislabelled audit trail into identity
514
+ spoofing.
515
+ - **Approval-by-email-link is attributed too.** ADR-0043 action links carry no
516
+ session, so they used to decide as pure system. The single-use hashed token
517
+ binds exactly one approver and is re-checked against the live slate at
518
+ redemption — that is an authentication — so the redeemed decision now presents
519
+ that approver, and an emailed approval cascades identically to one made in the
520
+ UI.
521
+ - **The two machine-driven transitions stay user-less on purpose**: the SLA
522
+ escalation's auto-decision and the dead-run sweep. `system:sla` and
523
+ `system:dead-run` are reserved audit actors, not users, and presenting one as a
524
+ user would put a non-user in `updated_by` and in every downstream flow's
525
+ identity. A flow that wants to react to those declares `runAs:'system'` — the
526
+ honest answer, and now a deliberate one rather than an artefact.
527
+ - **Attribution only — the write is not newly org-scoped.** On an
528
+ ExecutionContext `tenantId` is a driver-scoping knob, not attribution
529
+ (ObjectQL turns it into a tenant predicate), so passing the request's org would
530
+ have silently no-op'd the mirror on a record whose org differs. The automation
531
+ engine already back-fills a run's `tenantId` from the resolved user's grants.
532
+
533
+ **Visible change:** the mirrored record's `updated_by` now names the acting user
534
+ instead of retaining its previous value — ObjectQL's audit stamping is gated on
535
+ the write context's `userId` alone, and `isSystem` buys no exemption. That is the
536
+ attribution this fix is for: the approver who set the record to `approved` is now
537
+ its last modifier.
538
+
539
+ - a6c3f38: feat(approvals): expose the pending node's `lockRecord` policy on the request row (#3814, objectui#2902)
540
+
541
+ An approval node declares `lockRecord` (default `true`), and the record-lock
542
+ `beforeUpdate` hook enforces exactly that: `lockRecord: false` and the record
543
+ stays writable for the whole time the node waits. The behavior was correct and
544
+ has been since Phase B — but it was **invisible to every client**.
545
+
546
+ `rowFromRequest` parses `node_config_json` and projects a whitelist out of it
547
+ (`__flowLabel`, `__nodeLabel`, `__round`, `escalation.timeoutHours`,
548
+ `decisionOutputs`). `lockRecord` was never in that list, and no other field on
549
+ `ApprovalRequestRow` carried the lock either. So the strongest thing a console
550
+ could learn from `GET /approvals/requests` was _"a pending request exists"_ —
551
+ from which it can only assume the record is locked.
552
+
553
+ That assumption is wrong on every opted-out node, and a flow that chains nodes
554
+ with different policies makes it visibly wrong: the same UI state renders for
555
+ "you may edit this" and "the server will reject your save with `RECORD_LOCKED`".
556
+ The console has no third option — guessing the other way would offer an edit
557
+ that dies on save.
558
+
559
+ `ApprovalRequestRow` now carries **`lock_record: boolean`**, read from the same
560
+ snapshot the hook reads, with the same `!== false` default. Present on every
561
+ service read (`openNodeRequest` / `getRequest` / `listRequests`), so the flag a
562
+ client renders and the rule the server applies cannot drift.
563
+
564
+ Additive and backward compatible — nothing to migrate. A client that wants
565
+ node-accurate lock state reads `request.lock_record`; treat `undefined` (an
566
+ older backend) as locked, which is the pre-existing behavior.
567
+
568
+ The showcase's `showcase_budget_approval` now declares `lockRecord: false` on
569
+ its single-approver Manager Review and keeps `true` on the multi-approver
570
+ Executive Review, so both policies are exercised in one flow.
571
+
572
+ - 5fa04fb: Point the account app's **Approvals** navigation entry at the Approvals Inbox component, and contribute an **Approvals Inbox** entry to Setup (#7234).
573
+
574
+ The entry point has not moved — the account menu still shows **Approvals** with the same
575
+ label and icon in every locale. Its destination has. It used to open the raw
576
+ `sys_approval_request` grid, which is an admin/diagnostic view of the engine's own table
577
+ and cannot show an approver a single decision button: every action on that object is gated
578
+ on `record.viewer.can_act || record.viewer.can_override`, and the `viewer` block is
579
+ attached only by the approvals REST path, never by the generic data API the object route
580
+ reads. The result was a correct-looking list of rows nobody could act on. The entry is now
581
+ `{ type: 'component', componentRef: 'approvals:inbox' }`, so it opens the full inbox —
582
+ decision actions, business vocabulary, node progress and the request drawer.
583
+
584
+ - **Account app**: `nav_account_approvals` becomes a component entry gated by
585
+ `requiresService: 'approvals'`, so it disappears where `plugin-approvals` is not
586
+ installed (the previous `requiresObject` gate does not apply to a component entry).
587
+ - **Setup**: `plugin-approvals` contributes a new **Approvals Inbox** entry at the top of
588
+ **Setup → Approvals**, above the three raw tables, which stay exactly as they were —
589
+ admin-gated by `manage_platform_settings` and now unambiguously the diagnostic surface.
590
+ Labels ship in all four locales (zh-CN 审批中心).
591
+ - `sys_approval_request` is no longer surfaced raw to end users anywhere.
592
+ - **Docs**: the approver's queue is documented as the Approvals Inbox, with a snippet for
593
+ mounting it in any business app — one navigation entry naming the component-registry key
594
+ `approvals:inbox`, never a console path.
595
+
596
+ Reaching the inbox end to end in the browser additionally requires the console pin bump,
597
+ tracked separately.
598
+
599
+ - d75edb9: Approval nodes now resolve `field` / `manager` approvers against the record's **live** state at node entry, not the trigger snapshot the flow froze at submit time (#3447). An earlier step — or the approver of an earlier step — can now write the field that routes a later step's approvers, enabling dynamic routing / dynamic co-sign (e.g. a lead reviewer picking which departments co-review, then those departments resolving as parallel approvers). Graph approvers (team / position / department / tier) already resolved live; this brings the in-record types into line.
600
+
601
+ Also fixes two latent defects on the same path: a multi-select user field now fans out into one approver slot per user (previously the array was stringified to a single bogus id), and out-of-office delegation is applied per fanned-out user (previously silently skipped for multi-value fields). When the record can't be re-read (hard-deleted mid-flow, or a backend that can't serve a point read), resolution falls back to the trigger snapshot and warns rather than wedging the flow.
602
+
603
+ - 57a3bb3: fix(automation,approvals): the run-resume route is gated by the node the run is parked on (#3801)
604
+
605
+ `POST /api/v1/automation/:name/runs/:runId/resume` forwarded a caller-supplied
606
+ `{ inputs, output, branchLabel }` straight into `AutomationEngine.resume`, and
607
+ `resumeInternal` validated **machine state only** — the concurrent-resume latch,
608
+ the run exists, the flow exists, the suspended node still exists. Nothing asked
609
+ _who was calling_.
610
+
611
+ Approval nodes suspend and resume through exactly that mechanism. So a resume
612
+ carrying `branchLabel: 'approve'` walked the approve edge with **no approver
613
+ check, no `sys_approval_action` row and no status mirror** — the
614
+ `sys_approval_request` row and the run then disagreed permanently. The only
615
+ thing standing between the route and the approvals rules was convention; the
616
+ showcase spelled it out in a comment ("decide via the approvals API, never a raw
617
+ engine `resume`"), and a comment in an example is not an access control.
618
+
619
+ Removing the route was not the fix: it is load-bearing for **screen flows** —
620
+ the UI flow-runner posts `{ inputs }` there to advance a paused `screen` node.
621
+ The gate therefore keys on **what the run is parked on**:
622
+
623
+ - `ActionDescriptor.resumeAuthority` (`'any'` | `'service'`, default `'any'`) —
624
+ a pausing node declares who may continue it. `approval` declares `'service'`.
625
+ - The engine refuses a `'service'` suspension unless the signal carries
626
+ `RESUME_AUTHORITY_SERVICE` (`@objectstack/spec/contracts`), a **symbol** the
627
+ owning service stamps in-process — a JSON body can never produce one, so the
628
+ transport cannot forge it. `ApprovalService` stamps it on the tail of a
629
+ decision it has already authorized and recorded.
630
+ - The gate follows a **subflow** pause down to the child the signal would
631
+ actually reach, so resuming the parent is not a way around it.
632
+ - Refusal returns `{ success: false, code: 'forbidden' }` and the route answers
633
+ **403**. Nothing is consumed — the request stays pending and the run stays
634
+ parked, so the real decision still lands.
635
+
636
+ `screen` and `wait` pauses are unchanged, as is every path that already went
637
+ through the approvals API. What changes for consumers:
638
+
639
+ - **FROM:** finishing an approval with
640
+ `client.automation.resume(flow, runId, { branchLabel: 'approve' })`
641
+ **TO:** `client.approvals.approve(requestId, …)` (or `.reject` / `.recall`).
642
+ The old call now answers 403 and changes nothing.
643
+ - Registering your own pausing node whose continuation belongs to a service
644
+ rather than to whoever holds the run id? Declare `resumeAuthority: 'service'`
645
+ on its descriptor and stamp `RESUME_AUTHORITY_SERVICE` on the signal from that
646
+ service.
647
+
648
+ A suspension now records the node type that produced it
649
+ (`SuspendedRun.nodeType` / `sys_automation_run.node_type`), captured at suspend
650
+ time so a flow republished mid-pause cannot re-type the node out from under the
651
+ gate; rows written before this fall back to the flow definition.
652
+
653
+ - 2fa4ca1: Dynamic approver routing for approval nodes (#3447 P2) — three new declarative capabilities:
654
+
655
+ **`expression` approvers.** A new approver type whose CEL expression resolves WHO approves at node entry, over exactly three roots: `current.*` (the record's live state), `trigger.*` (the submit-time snapshot) and `vars.*` (flow variables, incl. upstream node outputs). `record` and bare field names are rejected before evaluation — on this platform `record` always means "the record at event time", which is ambiguous at an approval node — with error messages that prescribe the correct spelling. The optional `resolveAs: 'user' | 'department' | 'position' | 'team'` re-expands each resolved id through the same graph lookups the static types use; with `behavior: 'per_group'` each intermediate value (e.g. each returned department) forms its own sign-off group. A missing key fails the node loudly; only a present-but-empty result counts as an empty slate.
656
+
657
+ **`onEmptyApprovers` policy.** What an empty resolved slate does, node-level, for all approver types: `admin_rescue` (default — request opens for privileged takeover, the #3424 behaviour), `fail` (node fails), or `auto_approve` (skip the request, continue down the `approve` edge with `output.autoApproved = true`). To support auto-approve, the automation engine now honours `NodeExecutionResult.branchLabel` on the synchronous completion path — the field existed but was only ever consumed via resume signals.
658
+
659
+ **Decision outputs.** `decide(..., { outputs })` hands structured data from the approver to the flow: the author declares allowed keys on the node (`decisionOutputs`), approvers fill values only, and accepted outputs resume the run as `<nodeId>.<key>` variables — a later approval node's expression can read `vars.<nodeId>.picked_departments`, closing "the previous approver picks the next step's approvers" without a record-field detour. Undeclared keys reject the decision; `decision`/`requestId` are reserved. Multi-approver tallies now always pin to the open-time approver snapshot (previously unanimous re-resolved at each decision against the payload snapshot).
660
+
661
+ Also: `collectCelRootIdentifiers` is exported from `@objectstack/formula` (shared by the new `os lint` rules and the runtime pre-check, so they can never drift), resolution inputs are audited on the request snapshot as `__resolvedFrom`, and three new lint rules gate expressions, empty-slate policies and reserved output keys at author time.
662
+
663
+ - cd6b9f2: `decisionOutputs` entries may now be declared `required` (objectui#2955). A typed entry `{ key, label?, type?, multiple?, required?: true }` tells the runtime — not just the decision UI — that an approver must supply the value: an **approve** carrying no value, or a blank one (`''`, whitespace, `[]`, an array of blanks), is rejected with `VALIDATION_FAILED` before any write, so the audit row and the request are untouched and the run can never resume past the node with the key missing.
664
+
665
+ That gap is what the flag closes. `decisionOutputs` exists so a decision can route the next step (`approvers: [{ type: 'expression', value: 'vars.lead_review.next_reviewers' }]`), but nothing made the approver actually answer: a skipped output resumed the run with the key absent, and the next node either faulted with `EXPRESSION_FAILED` or resolved an empty slate and stalled on `onEmptyApprovers: 'admin_rescue'` — long after the one person who could have filled it in had moved on. `onEmptyApprovers` was the only backstop, and it is a recovery mechanism, not a contract.
666
+
667
+ **Reject never requires them.** The run leaves down the `reject` edge, where nothing reads the outputs — demanding routing data to say "no" would trap the rejection. Outputs still ride a reject when the approver filled them in.
668
+
669
+ **No elevation bypass.** A one-click email action link and an `auto_approve` SLA escalation both fail the same way rather than advancing into a node that would resolve nobody; the escalation sweep already isolates a throwing request, so that decision stays pending and visibly overdue instead of silently breaking the run downstream. Enforcement is per decision, so on a `unanimous` / `quorum` node every approver supplies the required outputs and the finalizing decision's values are what the flow resumes with.
670
+
671
+ `required` rides `normalizeDecisionOutputs`, so it reaches clients on `decision_output_defs` — a decision UI marks the field required and blocks locally instead of round-tripping to a 400. The console side ships in objectui#2955.
672
+
673
+ - 0848bea: feat(spec)!: retire the overloaded `managedBy: 'system'` bucket — the residue becomes `system-data` (#3355)
674
+
675
+ **FROM → TO: `managedBy: 'system'` → `managedBy: 'system-data'`.** One-line fix:
676
+ rename the value. Nothing else about the object changes. `os migrate meta --from 16`
677
+ rewrites it for you; stored metadata is CONVERTED by the ADR-0087 entry
678
+ `object-managed-by-system-to-system-data`, never silently reinterpreted.
679
+
680
+ ADR-0103 split the overloaded `system` bucket in v16, and it split it
681
+ **additively**: the 20 engine-owned objects moved to the new explicit
682
+ `engine-owned`, while the 8 admin/user-writable ones — the RBAC link tables
683
+ (`sys_user_position`, `sys_user_permission_set`, `sys_position_permission_set`),
684
+ `sys_user_preference`, `sys_approval_delegation`, and the three messaging config
685
+ grids — stayed behind on `system`. That was the right move for a v16 that could
686
+ not break authors, but it left the enum in a state where the surviving value
687
+ names the half that had already moved out: `system` sitting on precisely the
688
+ objects a user writes.
689
+
690
+ That is not a cosmetic complaint. An author choosing between `system` and
691
+ `engine-owned` had nothing in the vocabulary to choose _on_, so the bucket was
692
+ re-overloadable by anyone reading the name in good faith — a model author most
693
+ of all, since "system table" reads as "the engine owns this" in every other
694
+ codebase. `system-data` states both boundaries explicitly: the **schema** is the
695
+ platform's (versus `platform`, which is tenant-modelled), the **data** is the
696
+ admin's or the user's (versus `engine-owned`, where the engine owns both).
697
+
698
+ Because v16 already drained the engine side, the conversion is a **one-to-one
699
+ mechanical value rename** with no judgement call — by construction every
700
+ remaining `system` declaration is writable platform data.
701
+
702
+ **One deliberate consequence — the affordance default flips.** `system` defaulted
703
+ LOCKED and each of the 8 objects re-opened its writes with a
704
+ `userActions: { create: true, edit: true, delete: true }` block. `system-data`
705
+ defaults **WRITABLE** (full CRUD), because a bucket that exists to say "the data
706
+ is yours" should not make every member ask for it back. Those blocks are now
707
+ redundant and have been deleted from the 8 platform objects; keep `userActions`
708
+ only to **NARROW**. If you converted an object that carried no `userActions`, it
709
+ gains the generic affordances — the honest reading of the bucket it moved into.
710
+
711
+ **No enforcement moves.** The engine write guard, the `DelegatedAdminGate`, RLS
712
+ and permission sets all adjudicate off resolved affordances and the principal,
713
+ never off the bucket name. `system-data` simply joins `platform` / `config` as a
714
+ bucket the fail-closed guard does not cover, because a writable default has
715
+ nothing to close on. The 8 objects passed that guard before (via `userActions`)
716
+ and pass it now (via the bucket default), for the same resolved-affordance
717
+ reason.
718
+
719
+ `'system'` is **retired from the load path**: the enum rejects it with a
720
+ prescription naming `system-data` and the one-line fix. Absorbing it silently at
721
+ load would leave every author still writing the name this rename exists to
722
+ unteach.
723
+
724
+ - 57bab76: Typed `decisionOutputs` declarations (#3447 follow-up). A `decisionOutputs` entry may now be `{ key, label?, type: 'text' | 'user' | 'department' | 'position' | 'team', multiple? }` alongside the bare-string form — a typed entry tells the decision UI to render the matching record picker (id values; `multiple` collects an id array) instead of free text, turning "paste user ids" into "pick people". The type shapes only the input widget: the runtime whitelist works by `key` either way, via the new `normalizeDecisionOutputs` helper exported from `@objectstack/spec/automation` — the single reader of the union shape shared by the service, the request read, and `os lint`. The request read now carries `decision_output_defs` (normalized declarations) alongside the version-skew-safe `decision_outputs` key list.
725
+
726
+ ### Patch Changes
727
+
728
+ - e5bd768: refactor(spec)!: retire `ActionDescriptor.isAsync` — a second spelling of `supportsPause` that nothing ever read (#6748, ADR-0049)
729
+
730
+ <!-- adr-0087: registered action-descriptor-is-async-retired -->
731
+
732
+ **FROM → TO:** `isAsync: true` → delete the key; declare `supportsPause: true` (plus the
733
+ `resumeAuthority` its pauses need) and return `suspend: true` from `execute()`.
734
+ `isAsync: false` → delete the key; there was never anything to preserve.
735
+
736
+ `ActionDescriptor.isAsync` declared "suspends the flow awaiting an external reply" and no
737
+ execution path read it. Measured fresh before removal across all three repos — objectstack,
738
+ objectui and cloud — with zero property reads: every hit was the declaration itself, a
739
+ generated baseline, one of five shipped descriptors WRITING it, a fixture pinning the
740
+ shape, or prose. Declaring it never made a node suspend; omitting it never stopped one.
741
+
742
+ This is the remove leg of the ADR-0049 disposition its sibling took the other way. The two
743
+ keys said the same thing — "this node type can suspend the run" — and #6667 split them by
744
+ evidence: `supportsPause` became an enforced fact (`AutomationEngine` now refuses a
745
+ suspension whose type does not declare it, at the one seam every suspension passes
746
+ through), while `isAsync` had no consumer to grow into. Keeping both would leave the
747
+ platform publishing two names for one capability with only one of them honoured — and
748
+ `screen` declared BOTH, so a plugin author copying it had no way to tell which.
749
+
750
+ The retirement kit:
751
+
752
+ - **Tombstone, not deletion** (`retiredKey()`): `ActionDescriptorSchema` is not `.strict()`,
753
+ so a plain delete would let existing descriptors parse clean and lose the key in silence
754
+ (the ADR-0104 shape). Authoring `isAsync` now fails `tsc` at the descriptor literal and
755
+ fails the parse inside `defineActionDescriptor()` — with the prescription in the message.
756
+ - **ADR-0087 D3 `SemanticMigration`** (`action-descriptor-is-async-retired`) plus the exact
757
+ `RETIRED_KEYS_BY_MAJOR` entry. No D2 conversion, deliberately: a descriptor is published
758
+ from an executor's TypeScript and never stored in stack metadata, so there is no source
759
+ for `os migrate meta` to rewrite — the `EnhancedApiError.fieldErrors` disposition.
760
+ - The five shipped writers stop writing it (`screen`, `map`, `wait`, `approval`,
761
+ `approval_revise`); the descriptors they publish lose the key, which is why the two
762
+ runtime packages appear here.
763
+ - Generated baselines (`authorable-surface/automation.json` gains `[RETIRED]`,
764
+ `authorable-defaults/automation.json` loses the default line), `spec-changes.json`, the
765
+ upgrade guide and the reference docs regenerated.
766
+
767
+ No runtime behaviour changes — that impossibility is the reason for the removal. The same
768
+ commit also corrects `supportsPause`'s TSDoc, which still described itself as a declaration
769
+ no execution path reads; #6667 made that false (#6749).
770
+
771
+ - d058594: fix(approvals): refuse `organization` on directory-less approver types instead
772
+ of silently ignoring it (ADR-0105 D9)
773
+
774
+ `user`, `field` and `manager` return EARLY in `resolveApproverSpec` — they name
775
+ a person outright rather than expanding a directory. D9's org resolution was
776
+ placed after those returns, so an `organization` declared on one of them never
777
+ reached the check: it was silently INERT.
778
+
779
+ That is the one behaviour ADR-0105 D9 rules out and the authoring docs
780
+ explicitly promise against ("`organization` on those is refused at runtime").
781
+ The `os lint` rule caught it at author time, but the runtime claim was false —
782
+ and a stored flow that predates the lint, or one assembled programmatically,
783
+ got no signal at all.
784
+
785
+ Resolution now happens at the top of `resolveApproverSpec`, above every early
786
+ return, so the refusal reaches all three types. The ordinary path is unchanged
787
+ and still costs nothing: with no `organization` declared the resolver returns
788
+ the request's organization without reading anything.
789
+
790
+ Found by cloud's group-posture dogfood driving a real `group` boot — the
791
+ resolver's own unit tests could not see it, because they call the resolver
792
+ directly and never traverse the early return.
793
+
794
+ - 879ea13: ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a
795
+ first-class authorization dimension.
796
+
797
+ > This release carries BREAKING spec removals (see "Enforce-or-remove" below)
798
+ > but is recorded as `minor`: every publishable package is in the Changesets
799
+ > lockstep group, so one `major` would promote the whole monorepo. Breaking
800
+ > changes ship as `minor` during the launch window — the migration notes below
801
+ > are what reach consumers in `CHANGELOG.md`.
802
+
803
+ ## Tenancy is now a spectrum (D1)
804
+
805
+ `single | group | isolated`, resolved by the `tenancy` service and selected with
806
+ the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged:
807
+ `OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED`
808
+ (`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot
809
+ rather than silently landing in a posture with no organization wall.
810
+
811
+ - `single` — no wall (unchanged).
812
+ - `group` — **new.** Organizations are membership boundaries over one shared
813
+ dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC
814
+ semantics). Enforced by the OPEN engine.
815
+ - `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping`
816
+ probe and degraded-boot handling all unchanged.
817
+
818
+ ## Organization scope is a first-class context field (D2)
819
+
820
+ `ExecutionContext.accessible_org_ids` — every organization the caller holds a
821
+ currently-valid membership in (ADR-0091 validity windows) — is resolved once by
822
+ `resolveAuthzContext` and carried by every transport. The `group` wall reads it
823
+ directly; RLS policies may reference it as
824
+ `organization_id IN (current_user.accessible_org_ids)`. An empty or absent set
825
+ fails the wall closed.
826
+
827
+ Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still
828
+ computed independently of the RLS compiler, AND-composed outermost, and
829
+ crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so
830
+ ADR-0095's W1/W2 invariants hold in every posture.
831
+
832
+ ## Two P0 correctness fixes (D3, D4) — behavior changes
833
+
834
+ **D3 — app-authored org-scoped RLS policies are no longer silently dropped**
835
+ (finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose
836
+ `using` contained the substring `current_user.organization_id` when isolation was
837
+ inactive, which swallowed app-authored policies as well as the platform's own.
838
+ Stripping is now decided by PROVENANCE (identity against the shipped
839
+ declaration). **Upgrade impact:** in a deployment with no organization wall, an
840
+ app-authored policy referencing the active organization is now RETAINED and
841
+ fails closed (zero rows) with a one-time warning, where it previously vanished
842
+ and the object read unscoped. `getReadFilter` shared the defect, so analytics and
843
+ raw-SQL consumers were affected too. If a policy was only ever meant for
844
+ multi-org, delete it or install `@objectstack/organizations`.
845
+
846
+ **D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization
847
+ boundary** (finding F2, framework#3540). Under a wall-less posture nothing
848
+ bounded the wildcard superuser bits `organization_admin` carries, so a
849
+ deployment that accumulated organizations (personal orgs on signup) made every
850
+ owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a
851
+ de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and
852
+ revokes the superseded variant whenever the posture changes. **Upgrade impact:**
853
+ in `single` posture an org owner/admin keeps full CRUD but loses the blanket
854
+ ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains
855
+ available through `admin_full_access` or an explicitly authored permission set —
856
+ it just stops being a side effect of a better-auth membership role.
857
+
858
+ ## Engine-owned organization stamping (D5)
859
+
860
+ Under any wall-enforcing posture the engine stamps `organization_id` from the
861
+ caller's active organization on an insert that omits it, and validates every
862
+ supplied value against the wall. Idempotent with the enterprise auto-stamp
863
+ (neither overwrites a supplied value). This also closes a real hole: the
864
+ pre-existing post-image check required a non-array payload, so a BULK insert
865
+ could carry a forged `organization_id` per row. One forged row now denies the
866
+ whole write.
867
+
868
+ ## Group structure, extension fields and red-line lints (D6, D7)
869
+
870
+ - `sys_organization` gains `parent_organization_id` and `sort_order` — a
871
+ **reporting dimension only**.
872
+ - New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`,
873
+ `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` /
874
+ `os validate`: an RLS policy or sharing rule that walks the org tree is an
875
+ error, as is a business-unit grant on a platform-global object.
876
+ - Extension fields on better-auth-managed objects ride the existing ADR-0092
877
+ whitelist. A new guard derives better-auth's real field surface from
878
+ `getAuthTables()` at the pinned version and fails the build on any name
879
+ collision, so a library upgrade cannot silently take ownership of a column.
880
+
881
+ ## Enforce-or-remove (D11) — BREAKING
882
+
883
+ Both removals are of surface that had **zero runtime consumers**, so no
884
+ behavior changes; authoring them is now a no-op instead of a lint warning.
885
+
886
+ - **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read
887
+ it. FROM → TO: a set a policy needs as `field IN (current_user.<key>)` is now
888
+ supplied by a registered membership resolver (below); a constant belongs in
889
+ the policy itself as a literal (`status = 'published'`).
890
+ - **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`)
891
+ — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO:
892
+ matrix requirements are served by multi-position × business-unit anchoring; a
893
+ generalized dimension-security module will arrive with its own ADR.
894
+ - **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has
895
+ merged since ADR-0056 finally has a producer: register an
896
+ `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the
897
+ `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by
898
+ construction — an unresolved key makes its policies drop out. Kernel-owned
899
+ keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be
900
+ overwritten from this seam.
901
+
902
+ ## Edition boundary (D12)
903
+
904
+ The `group` posture's enforcement primitives ship OPEN — the union wall,
905
+ `accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness
906
+ fixes and the D6 lints — because the correctness of a wall is never a paid
907
+ feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing
908
+ enterprise `org-scoping` probe, so the current commercial boundary for
909
+ legal-entity isolation is unchanged by this release.
910
+
911
+ - 2ba560a: fix(plugin-approvals): give the decision actions a visual hierarchy (objectui#2762 P1-5)
912
+
913
+ The `sys_approval_request` decision actions all declared as equal-weight
914
+ buttons, so the drawer's action bar rendered five identical outlined
915
+ buttons with no emphasis on the primary path. `approval_approve` now
916
+ declares `variant: 'primary'` and `approval_reject` declares
917
+ `variant: 'danger'`, so a metadata-driven renderer highlights Approve and
918
+ styles Reject as destructive — matching the hierarchy the mobile card
919
+ already has. Pure metadata; the secondary levers stay unstyled (tertiary).
920
+
921
+ - 2dda6e7: fix(plugin-approvals): localize the declared decision-action labels (objectui#2762 P0-3)
922
+
923
+ The Approval Center's decision drawer rendered the `sys_approval_request`
924
+ declared actions with their literal metadata labels — English **Approve /
925
+ Reject / Reassign / Send back / Request info** in a zh-CN workspace, sitting
926
+ next to the same page's localized 通过 / 拒绝 inbox buttons. The plugin's
927
+ translation bundle covered fields and views but had no `_actions` node, so
928
+ the console's `_actions.<name>.label` resolution had nothing to hit.
929
+
930
+ - Re-ran `os i18n extract` against the plugin's config: the bundles now carry
931
+ `_actions` translations (label, confirmText, successMessage, param labels
932
+ and helpText) for all eight decision actions — `approval_approve`,
933
+ `approval_reject`, `approval_reassign`, `approval_send_back`,
934
+ `approval_request_info`, `approval_remind`, `approval_recall`,
935
+ `approval_resubmit` — in zh-CN, ja-JP and es-ES (en keeps the metadata
936
+ literals).
937
+ - The extract also surfaced other untranslated gaps, now filled in all three
938
+ locales: the `returned` status option, the `sys_approval_action.action`
939
+ audit options (`reassign` / `remind` / `request_info` / `comment` /
940
+ `revise` / `resubmit` / `ooo_substitute`), the `attachments` field, and the
941
+ `my_pending` / `recent` view empty states.
942
+
943
+ - 474fe39: feat(approvals): declare approver value bindings; retire `queue` approver authoring (#3508)
944
+
945
+ - `@objectstack/spec` exports `APPROVER_VALUE_BINDINGS` — the single declaration of how a
946
+ designer must source each approver row's `value`: `user`/`team`/`department`/`position`
947
+ are DATA-record lookups on the system directory objects (`sys_user` / `sys_team` /
948
+ `sys_business_unit` / `sys_position`; `position` commits the machine **name**, the
949
+ others the row id), `org_membership_level` is a closed enum (`ORG_MEMBERSHIP_LEVELS`),
950
+ `manager` is auto-resolved, `field` names a trigger-object field, and `queue` is
951
+ unsupported. Also exports `NON_AUTHORABLE_APPROVER_TYPES`.
952
+ - `queue` approver type is deprecated-for-authoring: it still parses (stored flows keep
953
+ loading and rendering) but is published in `xEnumDeprecated`, so designers stop
954
+ offering it — the runtime has no queue resolution and the slot routes to nobody. The
955
+ approver `value` xRef now also maps `manager`, so designers can render its
956
+ auto-resolved state. No authored key is removed; nothing to migrate. If a flow carries
957
+ `{ type: 'queue' }`, replace it with `team` / `department` / `position` (or a concrete
958
+ `user`) until a real ownership-queue implementation lands.
959
+ - `@objectstack/plugin-approvals` now warns at resolution time when a stored `queue`
960
+ approver is skipped.
961
+ - `@objectstack/lint` adds `approval-approver-type-unsupported` (warning) for approver
962
+ types that are declared but not implemented by the runtime.
963
+
964
+ - 0bc685a: fix(approvals): return decision attachments as file values, not "[object Object]" (#3504)
965
+
966
+ `sys_approval_action.attachments` is a `Field.file`, so the column **stores an
967
+ opaque `sys_file` id** (ADR-0104 D3 — the stored form of every media field). The
968
+ ObjectQL read path resolves that id into its expanded
969
+ `{ id, name, size, mimeType, url }` form on the way out. But `rowFromAction`
970
+ mapped the column with `.map(String)`, collapsing each expanded value to the
971
+ literal string `"[object Object]"`. Every `listActions` consumer (the approval
972
+ inbox timeline) then received garbage: the attachment chip had no filename and
973
+ its id was `"[object Object]"`, so opening it 404'd.
974
+
975
+ - `ApprovalActionRow.attachments` is now `ApprovalActionAttachment[]` — the
976
+ expanded file value plus its id, so a consumer can label and open an
977
+ attachment without needing read access to the system `sys_file` object (which
978
+ regular approvers do not have).
979
+ - Three read forms are accepted: the expanded value (the normal case), a bare id
980
+ (nothing to expand it into — storage service absent, file not committed), and
981
+ a legacy inline blob written before file-as-reference (`file_id` /
982
+ `mime_type`), until the backfill converts it. The id test reuses the
983
+ platform's `isFileIdToken`, so this and the engine's read resolver cannot
984
+ disagree about what counts as an id.
985
+ - The decision _input_ (`ApprovalDecisionInput.attachments`) is unchanged — it
986
+ still takes fileId strings, which is also exactly what the column stores. Only
987
+ the read shape changed.
988
+
989
+ - b949059: fix(approvals): a dead approval run no longer leaves the record RECORD_LOCKED (#3456)
990
+
991
+ The record lock is keyed on a **pending** `sys_approval_request`, and it could
992
+ not tell _the run that owns that request_ from _an unrelated user editing the
993
+ record_. So a flow that touched its own target record while its own approval was
994
+ still pending — a manual `resume` with no decision, or a node that writes the
995
+ record between opening the approval and the decision — died on its own
996
+ `RECORD_LOCKED`, and the record stayed locked behind the dead run. Recovery
997
+ existed (#3424 lets an admin `recall`/`reject` to release it) but nothing made it
998
+ self-healing.
999
+
1000
+ Both halves are now closed.
1001
+
1002
+ **Prevention — the owning run may write its own record.** The automation engine
1003
+ stamps `flowRunId` onto the run context at setup, alongside `runAs`, and it
1004
+ travels with every data node's ObjectQL context into `ctx.provenance`. The lock
1005
+ hook exempts a write whose `flowRunId` matches the pending request's `flow_run_id`.
1006
+ It is keyed on run identity rather than elevation on purpose: a `runAs:'user'`
1007
+ run stays fully RLS-scoped while it writes. `flowRunId` is pure provenance —
1008
+ server-constructed like `isSystem`, never client-supplied, evaluated by no
1009
+ security middleware, and the only write it permits is to the one record its own
1010
+ run already holds a pending request against.
1011
+
1012
+ **Recovery — a sweep releases records held by runs that died anyway.** A pending
1013
+ request whose owning run has reached a terminal state (`completed`, `failed`,
1014
+ `cancelled`, `timed_out`) can never be decided, so it is finalised as `recalled`
1015
+ — releasing the lock — and audited under the reserved actor `system:dead-run`
1016
+ with the run and its status in the comment, so it is never mistaken for a
1017
+ submitter's withdrawal. It runs on the existing approvals sweep clock, which also
1018
+ covers the case no in-band handler can: a run killed by a process crash.
1019
+
1020
+ The sweep is fail-safe by construction. It acts only on an explicit terminal
1021
+ status from a closed set; `paused` (the normal state of a live approval),
1022
+ `running`, an unrecognised status, an unknown run, a `getRun` that throws, and a
1023
+ deployment with no automation engine are all read as "still alive". The failure
1024
+ mode is "a dead run's lock survives until an admin recalls it" — today's
1025
+ behaviour — never "a live approval is destroyed".
1026
+
1027
+ Also fixes `AutomationEngine.getRun`, which returned the **first** log entry for
1028
+ a run id rather than the latest. A run that pauses and later finishes records two
1029
+ entries under one id, so every suspend-then-finish run — every approval, screen
1030
+ and wait flow — reported itself as `paused` forever, both on the Runs
1031
+ observability surface and to this sweep.
1032
+
1033
+ One shape was left out here and closed separately in #3712: a `runAs:'user'` run
1034
+ with no trigger user (a schedule) resolved no ObjectQL context at all, so it
1035
+ carried no `flowRunId` and stayed subject to the lock. It now passes a
1036
+ provenance-only context — the run id and nothing the security middleware keys on
1037
+ — so it is attributable without acquiring a principal, and its documented
1038
+ unscoped posture (#1888) is unchanged.
1039
+
1040
+ - 7abdd74: approvals: rejecting or recalling a request now opens ONE dialog instead of two
1041
+
1042
+ `sys_approval_request`'s `approval_reject` and `approval_recall` actions declared
1043
+ both `confirmText` and `params`. The console action runner chains confirmation
1044
+ **then** param collection, both awaited, so a single decision opened a confirm
1045
+ prompt, then a second dialog the approver never asked for — and nothing was sent
1046
+ until that second Confirm, while the first prompt already read as "the action is
1047
+ running".
1048
+
1049
+ Each action now carries its confirm question in the action's top-level
1050
+ `description` (the key added in #7367), which the param dialog renders under its
1051
+ title. The wording is unchanged in all four shipped locales — including the
1052
+ finality warning "A rejection is final for every approver." — so one decision is
1053
+ one condition, one wording, one dialog, and nothing is sent until its own Confirm.
1054
+
1055
+ - be1c52c: fix(approvals): admin override for a request routed to an unstaffed approver (#3424)
1056
+
1057
+ An `approval` node routed to a `position` (or `team`/`department`) with **no
1058
+ holders** resolved to only the unresolvable `position:<name>` literal in
1059
+ `pending_approvers` — no concrete user was in the slate. Every normal
1060
+ `decide` / `reassign` / `recall` then returned `FORBIDDEN` (not a pending
1061
+ approver) and, with `lockRecord`, the target record stayed `RECORD_LOCKED`
1062
+ forever: a data-availability dead-end with no in-product recovery (the only exit
1063
+ was editing the DB by hand). Very easy to hit in fresh/demo orgs (positions
1064
+ seeded, holders not) and whenever a role is vacated in production.
1065
+
1066
+ A **platform or tenant admin** — the same posture the engine's superuser bypass
1067
+ already trusts — may now act on any _pending_ request to release it: **approve,
1068
+ reject, reassign** it to a real approver, or **recall** it. The override finalizes
1069
+ the request (which releases the record lock, keyed on a pending request); a
1070
+ tenant admin's authority is org-scoped, a platform admin's is not, and the
1071
+ decision is audited under the admin's own id. An admin approval is authoritative,
1072
+ finalizing the node even under `unanimous` / `quorum` / `per_group` rather than
1073
+ counting as one vote among the (empty) slate.
1074
+
1075
+ - `sys_approval_request.viewer` gains `can_override` (server-computed): true for a
1076
+ privileged admin on a pending request. The `approve` / `reject` / `reassign`
1077
+ declared actions OR it into their `visible` gate, so the console surfaces the
1078
+ recovery path without a hand-wired button. Existing approver/submitter gating is
1079
+ unchanged.
1080
+ - `openNodeRequest` now logs a loud warning when a node resolves to **no concrete
1081
+ approver**, so the misconfiguration is visible instead of silently locking the
1082
+ record. The literal-fallback behavior (kept for 15.x slot back-compat) is
1083
+ otherwise unchanged.
1084
+
1085
+ - c5ff96d: fix(approvals): a schedule-triggered run can write its own locked record (#3712)
1086
+
1087
+ #3456 let the run that opened a pending approval write its own target record,
1088
+ keyed on `flowRunId`. It worked for every run that resolves an identity and
1089
+ missed the one that doesn't: an effective `runAs:'user'` run with **no trigger
1090
+ user** — a schedule being the canonical case — passed no ObjectQL context at
1091
+ all, so nothing carried the run id and the run still died on its own
1092
+ `RECORD_LOCKED`.
1093
+
1094
+ The blocker was never the lock. It was that "no identity" and "no context" were
1095
+ the same thing on the wire, so a run could not say _who it was_ without also
1096
+ claiming _what it was allowed to do_.
1097
+
1098
+ **A run with no principal now passes provenance alone.**
1099
+ `resolveRunDataContext` returns `{ flowRunId }` — no `userId`, no `positions`,
1100
+ no `permissions`, not even `isSystem: false`. Every principal gate keys on one
1101
+ of those fields (the elevation short-circuit on `isSystem`, the ADR-0103
1102
+ engine-owned write guard and the ADR-0090 D12 delegated-admin gate on `userId`,
1103
+ the empty-principal fall-open on all three), so this context authorizes
1104
+ **identically to no context at all**. The run keeps the documented #1888
1105
+ unscoped posture, its loud `[runAs]` warning, and the
1106
+ `flow-schedule-runas-unscoped` build-time lint. Nothing about what it may touch
1107
+ changed — only that it can now be attributed.
1108
+
1109
+ **Provenance moved out of the hook session, into `ctx.provenance`.** `session`
1110
+ answers _who is calling_ and is absent when no identity envelope was supplied —
1111
+ a distinction real gates depend on (the attachment access gate skips bare-kernel
1112
+ writes on exactly that test). Folding a run id into `session` would have forced
1113
+ an identity-less run to present an empty session, silently turning "no caller"
1114
+ into "an anonymous caller" and narrowing the #1888 fail-open for attachments
1115
+ alone. `HookContext.provenance.flowRunId` says what produced the write; the
1116
+ approvals lock reads it there.
1117
+
1118
+ Also relaxes `BaseEngineOptionsSchema.context` to a partial envelope
1119
+ (`ExecutionContextInput`). `positions`/`permissions`/`isSystem` carry parse-time
1120
+ defaults, which made them _required_ on a caller-supplied option and asserted
1121
+ something untrue — that every data-engine context carries a principal. Callers
1122
+ have always passed slices (`{ isSystem: true }` for a system read); the type now
1123
+ says so.
1124
+
1125
+ Migration: nothing to change unless you read the run id inside a hook. If you
1126
+ wrote `ctx.session.flowRunId`, read `ctx.provenance.flowRunId` instead — the
1127
+ field never shipped under the old name.
1128
+
1129
+ - 5a84d41: fix(approvals): record an admin override of a staffed approver slate AS an override (#4466)
1130
+
1131
+ An admin who is not in a request's `pending_approvers` may still act on it — the
1132
+ `#3424` privileged-override path exists so a request routed to an unstaffed
1133
+ position, or to approvers who have all left, is not undecidable forever. The
1134
+ override is defensible; what was not is what the audit trail recorded.
1135
+
1136
+ `sys_approval_action` had no override column at all. So an admin overriding a
1137
+ properly-staffed slate wrote a row **byte-for-byte identical** to the designated
1138
+ approver approving normally: a reader of the timeline saw `approve` by the admin
1139
+ and could not tell whether the admin _was_ an approver or _overrode_ the ones who
1140
+ were, and the bypassed approver's later `409 INVALID_STATE` was the only trace —
1141
+ existing only if they happened to try. The platform knows at decision time (it
1142
+ took the `isOverrideActor` branch to admit the call at all), so this was dropped
1143
+ information, not unavailable information. The whole point of an approval record
1144
+ is to answer "who authorized this, and were they entitled to?".
1145
+
1146
+ `sys_approval_action` now carries **`via_override`** (boolean, optional), set on
1147
+ exactly the actions admitted by that branch — `decideNode`'s approve/reject and
1148
+ `reassign`'s admin rescue. It is surfaced on `ApprovalActionRow.via_override`
1149
+ (`@objectstack/spec/contracts`), returned by `listActions`, and added to the
1150
+ object's `highlightFields` and two grid list views so a timeline can say
1151
+ "overrode the approver slate" instead of rendering it as an ordinary approval.
1152
+
1153
+ Three distinctions the column keeps apart deliberately:
1154
+
1155
+ - **`true`** — the actor held no slot in the slate and was admitted only by the
1156
+ override branch.
1157
+ - **`false`** — checked, and it was not an override. An admin who _is_ a
1158
+ designated approver is approving normally and records `false`: the marker is
1159
+ about which branch admitted the call, not about whether the actor holds admin
1160
+ rights.
1161
+ - **absent** — a row written before this column existed. "Not recorded" is not
1162
+ the same claim as "not an override", so `rowFromAction` maps `null` to
1163
+ `undefined` rather than to `false`.
1164
+
1165
+ Additive and nullable, so this needs no data migration: existing rows keep
1166
+ working and simply read as unrecorded. Levelled `patch` rather than `minor`
1167
+ because nothing an author writes changes — but note it _is_ an observable
1168
+ behaviour change on a read surface: `listActions` responses and the
1169
+ `sys_approval_action` grid views now carry a field consumers did not see before,
1170
+ and `sys_approval_action` gains a column on next schema sync.
1171
+
1172
+ - d2a8695: fix(approvals)!: an approval request is visible to its participants, not to the whole tenant (#3590)
1173
+
1174
+ `getRequest` / `listRequests` / `countRequests` deliberately query with
1175
+ `SYSTEM_CTX` to bypass RLS — as the code comments say, the approver-visibility
1176
+ rule spans identity forms RLS cannot model cleanly, so it has to be expressed in
1177
+ the service. Only the **tenant** half of that rule was ever applied. The
1178
+ participant half was named in the comment and never written, so **any
1179
+ authenticated user could read any approval request in their tenant** — its
1180
+ payload snapshot, its full decision history, and (once decision attachments
1181
+ derived their access from the request, #3580) its files.
1182
+
1183
+ `approverId` on `listRequests` is a _filter_, not authorization: omitting it
1184
+ returned the whole tenant.
1185
+
1186
+ A caller now sees a request when they are a participant — the submitter, a
1187
+ current approver (via the normalized approver index, so every identity form the
1188
+ write path recorded is covered), or someone who has already acted on it (a past
1189
+ approver whose slot has moved on, a commenter). Admins with override authority
1190
+ keep the unrestricted view the "all requests" console surface depends on, and a
1191
+ tokenless context sees nothing.
1192
+
1193
+ Keying on the concrete user id is sufficient rather than an approximation:
1194
+ position/team/manager/field approvers are resolved to concrete user ids at open
1195
+ time, and the `type:value` literal is only the fallback for a spec that resolved
1196
+ to _nobody_ — a slot no one can act on either way. So this cannot hide a request
1197
+ from someone who could actually act on it.
1198
+
1199
+ **A write path's own result is not re-gated.** Every operation echoes back the
1200
+ request it just changed; the operation already authorized itself, and re-asking
1201
+ would answer wrong for a context carrying no `userId` (a flow-driven resume, a
1202
+ service-to-service call), turning a successful write into `null`.
1203
+
1204
+ Marked breaking because a client that listed requests without an `approverId`
1205
+ filter and expected the whole tenant will now receive only its own — which is
1206
+ the point.
1207
+
1208
+ - 84e7be9: feat(plugin-approvals): expose per-group membership of pending approvers (objectui#2807)
1209
+
1210
+ `per_group` (会签) requests now carry `pending_approver_groups` on the
1211
+ enriched row — a map from each still-pending approver id to the group key(s)
1212
+ it fills (e.g. `{ "u_devadmin": ["finance", "legal"] }`). A client can label
1213
+ each "waiting on" chip with the group it represents instead of showing
1214
+ duplicate, context-free names.
1215
+
1216
+ - Resolved in `attachDecisionProgress` from the same open-time
1217
+ `__approverGroups` snapshot the `decision_progress` groups already use, so
1218
+ the two never disagree.
1219
+ - Only the **pending** slots are mapped (a resolved approver has left
1220
+ `pending_approvers`), and **synthetic** (unnamed, `#N`) group keys are
1221
+ dropped — a `· #0` sub-tag would be noise.
1222
+ - Absent for non-`per_group` behaviors. Display-only; the engine's
1223
+ finalization tally stays authoritative.
1224
+ - Added to the `ApprovalRequestRow` contract in `@objectstack/spec`.
1225
+
1226
+ - 0b795da: fix(approvals): the record lock now holds for predicate (`multi`) updates (#4778)
1227
+
1228
+ The ADR-0019 record lock — "while a record has a pending `sys_approval_request`,
1229
+ block edits to it" — was enforced only for updates that reach the hook with an
1230
+ `input.id`. The engine extracts that id from a **scalar** `where.id` alone; an
1231
+ operator object (`{ $in: [...] }`) or any other predicate is a multi-row write
1232
+ that routes to `updateMany` and arrives with no id. The hook opened with
1233
+ `if (!id) return`, so it read _"no row was resolved"_ as _"there is nothing to
1234
+ authorize"_ when the truth was _"nothing was ever queried"_.
1235
+
1236
+ Rewriting the very same edit as `multi: true` therefore walked straight past the
1237
+ lock:
1238
+
1239
+ ```ts
1240
+ // rec_1 carries a pending approval, lockRecord is not disabled
1241
+ await ql.update(
1242
+ "crm_opportunity",
1243
+ { amount: 999 },
1244
+ { where: { id: "rec_1" } }
1245
+ ); // RECORD_LOCKED
1246
+ await ql.update(
1247
+ "crm_opportunity",
1248
+ { amount: 999 },
1249
+ { where: { id: { $in: ["rec_1"] } }, multi: true }
1250
+ ); // went through
1251
+ await ql.update(
1252
+ "crm_opportunity",
1253
+ { amount: 999 },
1254
+ { where: { name: "x" }, multi: true }
1255
+ ); // went through
1256
+ ```
1257
+
1258
+ No privilege was needed for that bypass — not an `admin` role, not `isSystem`,
1259
+ not `lockRecord: false`, not a whitelisted `approvalStatusField`. Every caller
1260
+ shape that can spell a predicate (SDK, ObjectQL, a flow's `update_record`) could
1261
+ produce it. It is the same fail-open reasoning fixed for `sys_attachment`
1262
+ (#4757) and `sys_comment` (#4630), in the one place where it needed no
1263
+ privilege at all.
1264
+
1265
+ **The hook now resolves the rows a write touches before deciding.** By-id writes
1266
+ are unchanged (the driver writes by primary key, so the rest of `where` must not
1267
+ narrow the verdict). A predicate write is decided by intersecting the caller's
1268
+ predicate with the records that are actually locked — which is also what keeps
1269
+ it cheap: the query is bounded by the object's **pending approvals**, never by
1270
+ the update's match set, so a mass update of 50 000 unlocked rows costs one
1271
+ bookkeeping probe and is allowed. An unscoped `multi` update over the whole
1272
+ table reaches every locked row of the object and is refused while any is held.
1273
+
1274
+ **Fail-closed, both ways.** Past 1 000 locked records — the bound the attachment
1275
+ and comment guards use — or if the intersection query fails, the write is
1276
+ refused rather than allowed: the lock could not prove the write misses a locked
1277
+ row. The approvals bookkeeping being unreadable at all stays the one fail-open,
1278
+ as before: this hook is global over every object, so a kernel without
1279
+ `sys_approval_request` would otherwise refuse every update in the deployment.
1280
+ Both the bookkeeping and the match-set resolution are read under a **system**
1281
+ context — a guard's own input must never be narrowed by the caller's
1282
+ visibility, since a locked row you cannot read is still a row you may not write.
1283
+
1284
+ **Every exemption moved with the guard**, which is the other way this class of
1285
+ fix goes wrong — a guard extended to more rows that carries only its deny rules
1286
+ turns a fail-open into a false-positive. `isSystem`, the `admin` override, the
1287
+ `approvalStatusField` status mirror, `lockRecord: false` and the owning run's
1288
+ `flowRunId` (#3456 / #3712) all decide a predicate write exactly as they decide
1289
+ a by-id write, each pinned by tests on both predicate shapes. Refusals now name
1290
+ the record and object that are locked.
1291
+
1292
+ - 820eff9: fix(spec,plugin-approvals): the two approval vocabularies are derived, not hand-matched (#3786)
1293
+
1294
+ `sys_approval_request.status` and `sys_approval_action.action` spelled their
1295
+ option lists out — five values and twelve — each under a "Keep in sync with
1296
+ `ApprovalStatus` / `ApprovalActionKind` (spec/contracts)" comment, while the
1297
+ contract held the same sets as bare type unions. Seventeen strings matched by
1298
+ hand across a package boundary, with nothing checking them. They did all still
1299
+ agree; the sweep that found them (#3786) verified that verbatim before changing
1300
+ anything.
1301
+
1302
+ Agreeing is not the same as being held, and both directions of drift are quiet:
1303
+
1304
+ - a value the **column** accepts and the contract omits is invisible to every
1305
+ consumer typed against the contract — the row exists and nothing can narrow it;
1306
+ - a value the **contract** declares and the column rejects surfaces only at write
1307
+ time, on whichever tenant first reaches that transition.
1308
+
1309
+ An audit vocabulary is a bad place for either. So the contract now publishes the
1310
+ lists as values — `APPROVAL_STATUSES` and `APPROVAL_ACTION_KINDS` — with
1311
+ `ApprovalStatus` / `ApprovalActionKind` derived from them via
1312
+ `(typeof X)[number]`, and the two columns spread the constants. The per-entry
1313
+ rationale (which action kinds move the flow, which are thread-only, why
1314
+ `returned` differs from `recalled`) moved onto the constants, where the values
1315
+ live.
1316
+
1317
+ **New exports, no behaviour change.** The emitted option lists are byte-identical
1318
+ — verified against the built artifact before and after. Existing imports of the
1319
+ two types are unaffected; the types resolve to the same unions.
1320
+
1321
+ `approval-vocabularies.test.ts` pins the qualifier that derivation alone cannot:
1322
+ the columns agree with the contract _while the spread is there_, and the test
1323
+ fails if either is re-inlined as a literal that has drifted. It also guards the
1324
+ guard (an unresolvable import would compare two empty lists and pass) and asserts
1325
+ the two vocabularies stay distinct, since a copy-paste pointing one column at the
1326
+ other constant would satisfy "derived from the contract" while being the wrong
1327
+ vocabulary entirely.
1328
+
1329
+ Verified by mutation in both directions: adding a value to `APPROVAL_STATUSES`
1330
+ propagates into the built `sys_approval_request.status` options (the derivation
1331
+ is live, not a stale build), and re-inlining a drifted literal fails
1332
+ `sys_approval_request.status offers exactly the contract statuses, in order`.
1333
+
1334
+ - 7e5ac28: fix(approvals): 删除两处读 `session.roles` 的 admin 豁免 —— 记录锁与委托守卫回到单一权限词汇 (#4839)
1335
+
1336
+ `plugin-approvals` 的 `lifecycle-hooks.ts` 里有两处 admin 豁免,都读
1337
+ `ctx.session.roles`:审批**记录锁**的 `bindApprovalLockHook`,以及
1338
+ `sys_approval_delegation` 的 `bindDelegationWriteGuard`。两处都已删除。
1339
+
1340
+ **这不是行为变更。** `session.roles` 在整个平台没有生产者 —— ObjectQL 的
1341
+ `buildSession()` 逐字段构造 session,从不写 `roles` —— 所以两个分支在任何真实引擎
1342
+ 路径上都是死代码,记录锁一直就对 admin 生效,委托一直就只能本人管理。删除让代码
1343
+ 说出运行时本来就在做的事(spec 的 `HookContext` 声明了 `roles`,消费方在读,生产方
1344
+ 从不写:典型的 declared ≠ enforced)。
1345
+
1346
+ **为什么不是「改用正确判据」而是删除。** `roles.includes('admin')` 还是第二套权限
1347
+ 方言:本仓库的权限一律由 ADR-0095 词汇裁决(能力授予 `permissions`、任职
1348
+ `positions`、由其派生的 posture),ADR-0090 D3 更是直接禁掉 `role` 这个拼法。同包的
1349
+ `ApprovalService.isOverrideActor` 已经这么做了。维护者裁定两处都取「删除」而非改判据:
1350
+
1351
+ - **记录锁**:admin 释放锁定记录的正规路径已经存在(#3424 —— `recall` /
1352
+ `decideNode` 驳回 / `reassign`,全部由 `isOverrideActor` 把关并留痕
1353
+ `via_override`)。让审批终结来释放锁,记录就永远不会在审批在途时被改写 —— 这正是
1354
+ 合规场景购买记录锁所要的保证。
1355
+ - **委托**:最终语义确定为**仅本人管理**(`delegator_id` 必须等于写入者;只有 system
1356
+ 上下文旁路)。审批人临时不可用时,替他处置**在途**审批用的是
1357
+ `reassign`(把该审批人的名额交给替代人,连 per_group 分组归属一起带过去)/
1358
+ `recall` / 驳回。反过来,「替别人建一条委托」本来也做不到这件事:委托只在请求
1359
+ **开启**时(`resolveApproverSpec` 内的 `applyOooDelegation`)被查询,对已经挂在该
1360
+ 审批人名下的在途审批毫无作用。
1361
+
1362
+ 新增 `admin-exemption-retired.test.ts`,把上述证据变成可执行断言,并加了一道源码级
1363
+ pin:本包非测试源码中不得再出现 `roles` 标识符或与字符串 `'admin'` 的比较。
1364
+
1365
+ spec 侧 `session.roles` 的退役(至此零消费方)按 ADR-0049 enforce-or-remove 另立协议
1366
+ 单处理,不在本次改动内。
1367
+
1368
+ - 19e1a8f: fix(approvals): an approval decision can no longer strand a flow run silently when no automation engine is attached (#4420)
1369
+
1370
+ #4420's fix closed every path by which a decision could be recorded while its
1371
+ flow stayed parked — except one, and it is the one where none of the new guards
1372
+ could run. Every guard it added (`assertRunResumable`'s pre-flight, the
1373
+ `RESUME_TARGET_LOST` refusal, the `RESUME_FAILED` throw) hangs off the
1374
+ automation engine. In a process where **no engine is attached**, all of them
1375
+ were skipped by the same `typeof this.automation?.resume === 'function'`
1376
+ condition that wrapped the resume itself — so the decision was written, the
1377
+ mirrored status field advanced, and the call answered HTTP 200 with
1378
+ `resumed: false` and **nothing logged at all**. That is #4420's reported
1379
+ symptom exactly, reproduced in the one composition its fix could not see.
1380
+
1381
+ The composition is reachable the same way the original bug was: a flow parks at
1382
+ an `approval` node in a process that has the automation service, and the
1383
+ decision arrives in one that does not (the plugin failed to init, or the host
1384
+ was recomposed between releases). The request row still carries a
1385
+ `flow_run_id` — which is the row's own declaration that a run is parked on this
1386
+ decision.
1387
+
1388
+ **What changes.** The decision still stands. Rolling it back is not on the
1389
+ table (a human really decided, and the row is durable by then), and refusing
1390
+ every such call would break the standalone approvals compositions the
1391
+ pre-flight deliberately protects — so `finalized` and `resumed` are unchanged
1392
+ for every existing caller. What changes is that the gap is no longer silent:
1393
+
1394
+ - it is logged at **`error`**, per the durability rule in `AGENTS.md` —
1395
+ persisted state and runtime state disagree while nothing looks broken from
1396
+ the outside, which is the class that rule exists for;
1397
+ - the response carries **`resumeError`**, so `resumed: false` arrives with its
1398
+ reason and the stranded run's id instead of leaving the caller to guess
1399
+ whether a resume was even attempted.
1400
+
1401
+ It reuses the already-registered `RESUME_FAILED` code and the existing resume
1402
+ message shape rather than introducing a new vocabulary — the fact being
1403
+ reported (an outcome recorded whose run did not advance) is the same one.
1404
+
1405
+ Applied at all five sites that resume a recorded outcome: `decide`, the
1406
+ revision-limit auto-rejection, `sendBack`, `resubmit`, and both branches of
1407
+ `recall` (whose revise-window path needs `cancelRun` rather than `resume`).
1408
+
1409
+ A request that names **no** run is unaffected and stays quiet — there is
1410
+ nothing parked on it, and reporting one there would be the mirror-image
1411
+ failure that trains operators to skim `error`.
1412
+
1413
+ - debc23a: feat(approvals): enrich inbox rows with `payload_labels` (snapshot field labels)
1414
+
1415
+ The approvals inbox summary title-cased raw snapshot machine keys
1416
+ (`assessment_status` → "Assessment Status") because the API sent no field
1417
+ labels. `ApprovalService.enrichRows` now attaches `payload_labels` (snapshot
1418
+ field key → the target object's field label), symmetric with the existing
1419
+ `payload_display` (which resolves the values), and `ApprovalRequestRow` gains
1420
+ the field. For a single-locale project the schema label is already the
1421
+ localized string, so a client can render the human field name (e.g. "考核状态")
1422
+ instead of a prettified English key.
1423
+
1424
+ - f40c5b4: refactor(plugin-approvals,plugin-reports): enforcement implementations annotate the full `ExecutionContext` (#7135)
1425
+
1426
+ The services half of #7070, mirroring what PR #7140 did for
1427
+ `plugin-sharing` / `plugin-audit`. #6523 converged 36 contract signatures onto
1428
+ the complete `resolveAuthzContext` envelope, applying the #6206 ruling —
1429
+ enforcement adjudicates on the whole envelope, never a per-site subset. The
1430
+ implementations behind those contracts still annotated their own parameters
1431
+ with the six-field shape the contracts used to name, so nothing they could
1432
+ _read_ had widened.
1433
+
1434
+ `ApprovalService`, the approval flow-node provider and `ReportService` now
1435
+ declare `ExecutionContext` on all 43 of those positions, and the casts the
1436
+ narrow annotation forced are gone:
1437
+
1438
+ - `isOverrideActor()` read the derived `posture` (ADR-0095) through an
1439
+ unchecked `(context as any)`. That gate decides whether a platform or tenant
1440
+ admin may release a STUCK approval — one routed to an unstaffed position, the
1441
+ only in-product recovery from a permanently locked record — so an erasure sat
1442
+ directly on an enforcement input: a mistyped rung would have compiled and
1443
+ silently denied every override. It is a declared read now.
1444
+ - Both services' `SYSTEM_CTX` is typed as the envelope and passed as itself,
1445
+ retiring the `SYSTEM_CTX as unknown as …` double casts at the three sites
1446
+ that hand it to a contract method.
1447
+ - The `(context as any).userId` / `.tenantId` reads in `ApprovalService` now
1448
+ read declared fields.
1449
+ - `OwnerContextResolver` returns the envelope, which is what a scheduled report
1450
+ actually resolves for its owner (#2849 / #2980).
1451
+
1452
+ **No runtime behaviour changes.** The values were always complete — this
1453
+ family's damage was type-side — so every gate answers exactly what it answered
1454
+ before. Method parameters only WIDEN what they accept, so no caller is
1455
+ affected, and no public export changes shape.
1456
+
1457
+ Casts deliberately kept, and now documented where they sit: `organizationId`
1458
+ is not a field of the envelope at all — that spelling has its own history
1459
+ (#5858 / `check:org-identifier`) and was held out of this change by #7070. In
1460
+ `approval-node.ts` the single remaining assertion exists only because the
1461
+ literal names that key; it was reduced from `as unknown as …` to a single
1462
+ `as ExecutionContext`, which still requires the literal to be comparable to
1463
+ the envelope.
1464
+
1465
+ Because a re-narrowed annotation would compile, ship and pass every test in
1466
+ these packages, the convergence is pinned by a new compile-time module per
1467
+ package, `exec-context-annotation.pin.ts`: it hands each parameter a fresh
1468
+ literal naming envelope-only fields (`posture`, `accessible_org_ids`,
1469
+ `org_user_ids`), which TypeScript's excess-property check rejects the moment a
1470
+ parameter narrows back, plus negative cases so a parameter erased to `any`
1471
+ cannot pass either.
1472
+
1473
+ The exported `SharingExecutionContext` type itself is NOT removed here: it is
1474
+ defined in `packages/spec`, which is single-owner, so its retirement is a
1475
+ separate follow-up.
1476
+
1477
+ - c2a1134: fix(approvals): find the zombie requests nothing was looking at (#4469)
1478
+
1479
+ #4460 stopped new zombies being produced; the rows already stuck had no mechanism
1480
+ to find or release them. The failure shape (#4420) is a request flipped to
1481
+ `approved` / `rejected` / `returned` whose `flow_run_id` points at a run that no
1482
+ longer exists — the decision landed, the flow never moved. Any deployment on
1483
+ 17.0.0-rc.1 that hit the wiring hole and crossed a restart mid-approval can be
1484
+ carrying these rows.
1485
+
1486
+ `releaseDeadRunRequests` could not see them, and the reason is worth stating
1487
+ plainly: it scans `status: 'pending'`, and the very step that zombifies a request
1488
+ is the one that takes it OUT of `pending`. The act of breaking it removed it from
1489
+ the only sweeper's field of view — a large part of why this class of failure
1490
+ stayed silent. It could not have answered the question even if it had looked: its
1491
+ liveness oracle is `getRun`, which reads the execution LOG and returns `null` for
1492
+ a perfectly ALIVE suspended run after a restart. It treats `null` as alive
1493
+ (conservative, and correct for what it does) — which is exactly why it has no way
1494
+ to say "this run is really gone".
1495
+
1496
+ Adds `ApprovalService.inspectStrandedRequests()`, which uses BOTH oracles and
1497
+ reports only rows that fail both:
1498
+
1499
+ - `hasSuspendedRun(runId) === false` — the suspension store itself says no live
1500
+ pause exists. It THROWS when the store cannot be read, and that case is
1501
+ SKIPPED and counted as `undetermined`, never condemned: an unreadable store
1502
+ means "unknown", and a storage outage must not be published as a lost run.
1503
+ - `getRun(runId) == null` — no terminal history row either. A run that merely
1504
+ finished is not stranded; a request whose run neither waits nor ever completed
1505
+ is.
1506
+
1507
+ **It reports; it never rewrites.** No status is changed and no run is cancelled.
1508
+ The decision genuinely happened — a human approved or rejected — and silently
1509
+ rolling it back would make the audit trail disagree with the facts. The report
1510
+ carries what an operator needs to decide: which requests are stuck at which step,
1511
+ and what the mirrored status field on the business record still reads (usually
1512
+ the stale value the user is staring at). Whether to re-run the downstream actions
1513
+ or re-open the approval is a judgement call this cannot make.
1514
+
1515
+ It rides the existing escalation/dead-run sweep clock, so the finding surfaces in
1516
+ the logs without an operator knowing to go looking for it. `recalled` is
1517
+ deliberately out of scope: a recall abandons its run on purpose, and reporting
1518
+ those would bury the real findings under expected ones.
1519
+
1520
+ New export: `StrandedApprovalRequest` (the report row shape).
1521
+
1522
+ - 0f8ad09: feat(spec)+fix(approvals): publish approver value data sources, order the type enum for authors, stop silent dead approver slots (#3508 / #3807 follow-ups)
1523
+
1524
+ Four follow-ups from browser-verifying the #3508 approver work end to end.
1525
+
1526
+ **`APPROVER_VALUE_SOURCES` — the designer stops guessing where candidates live.**
1527
+ `xRef.map` only ever named a picker KIND (`'team'`), never where that picker's
1528
+ rows come from, so the designer carried its own copy of the data contract — and
1529
+ the first copy was wrong: every directory kind was wired to `GET
1530
+ /api/v1/meta/:type`, the metadata REGISTRY, which does not hold `sys_user` /
1531
+ `sys_team` / `sys_business_unit` / `sys_position` rows. Candidates came back
1532
+ empty and the control degraded to free text (#3508). The binding is now
1533
+ projected onto the published JSON schema as `xRef.sources` — `{ source: 'data',
1534
+ object, valueField }` for the record-backed kinds, the closed enum inline for
1535
+ `org_membership_level` — derived from `APPROVER_VALUE_BINDINGS` so the two
1536
+ cannot drift, and inheriting its `satisfies` exhaustiveness (a new
1537
+ `ApproverType` member that declares no source is a compile error). Presentation
1538
+ — which field to show, whether to open a people-picker, what subtitle to use —
1539
+ stays a renderer decision.
1540
+
1541
+ **`ApproverType` declaration order is now the authoring recommendation.**
1542
+ objectui#2834 argued for leading with indirect bindings and shipped that order
1543
+ in its own options array — which the Studio inspector never reads: it derives
1544
+ the picker from this enum via the published schema, so `user` still came first.
1545
+ The intent only takes effect if the enum carries it, so the enum now reads
1546
+ `manager, position, department, team, field, expression, org_membership_level,
1547
+ user` (deprecated `role` / `queue` still parse and stay out of every picker via
1548
+ `xEnumDeprecated`). Binding one specific person is the least portable choice an
1549
+ author can make — it breaks when the flow moves to another environment (that id
1550
+ does not exist there) and again when that person leaves.
1551
+
1552
+ **A graph approver that expands to nobody no longer does it in silence.**
1553
+ `queue` already warned (#3508); every OTHER graph type — `team`, `department`,
1554
+ `position`, `org_membership_level`, `manager` — fell back to the same
1555
+ unactionable `type:value` literal without a word. That silence is what let
1556
+ #3807 hide for as long as it did: the request opened with an empty slate and
1557
+ the first symptom was a permanently stuck approval (#3424). The fallback stays
1558
+ (15.x slots and substring fixtures depend on it); it now logs the type, value
1559
+ and organization that produced it. `user` / `field` stay quiet — they take the
1560
+ id they were given and never had an "expanded to nobody" state.
1561
+
1562
+ **`plugin-sharing`'s identical org scope is pinned by tests.**
1563
+ `BusinessUnitGraphService.orgScope` has the same strict `organization_id`
1564
+ equality #3807 fixed in approvals. It is unreachable today — every materialized
1565
+ `sys_sharing_rule` carries `organization_id = null`, so the filter is skipped —
1566
+ and widening an authorization path on a defect that cannot currently fire is
1567
+ not a change to make blind. New tests lock both the reachable paths and the
1568
+ divergence itself, so if sharing ever adopts the null-org=env-wide reading it
1569
+ is a deliberate edit to a named test rather than a silent behaviour change.
1570
+
1571
+ - 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
1572
+
1573
+ The AGENTS.md post-task checklist requires breaking changesets to carry their
1574
+ FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
1575
+ inside the npm package and is what an upgrading agent greps after the tombstone
1576
+ error." That delivery path was severed for 68 of the 69 publishable packages:
1577
+ npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
1578
+ older npm versions — not `CHANGELOG.md`, and the canonical
1579
+ `"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
1580
+ 10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
1581
+ 70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
1582
+ explicitly.
1583
+
1584
+ The tombstone-error scenario is precisely the one where the repo is out of
1585
+ reach — the upgrading agent has `node_modules` and nothing else — so the
1586
+ migration text has to ride in the tarball. Every publishable package now
1587
+ declares `CHANGELOG.md` in `files`, and the canonical whitelist is
1588
+ `["dist", "README.md", "CHANGELOG.md"]`.
1589
+
1590
+ The other half is the gate: `check:published-files` gains a fifth invariant,
1591
+ COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
1592
+ always-required lint job, so the next package cannot silently sever the path
1593
+ again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
1594
+ into the canonical set.
1595
+
1596
+ Consumer-visible change: one more file per install (the package's changelog,
1597
+ e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
1598
+ node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
1599
+ promised.
1600
+
1601
+ - 376a061: Surface the approval node's author-declared `decisionOutputs` keys on the request read as `ApprovalRequestRow.decision_outputs` (#3447 P2 UI enablement). The set varies per request (each node declares its own), so it rides the row rather than the object's static action params — a decision UI renders one input per key and POSTs `outputs` with the decision.
1602
+ - 3ea7271: fix(approvals): a `department` approver resolves against env-wide business units (#3807)
1603
+
1604
+ `expandBusinessUnitUsers` scoped its `sys_business_unit` reads with a strict
1605
+ `organization_id = <request org>` equality, so a unit whose `organization_id`
1606
+ is `null` was invisible: the seed check found no row, the expansion returned
1607
+ `[]`, and the approver fell back to the dead `department:<id>` literal that
1608
+ routes to nobody.
1609
+
1610
+ That is the normal case, not an edge case. An app's org tree is seeded, and a
1611
+ seed cannot know the organization id the runtime mints at boot, so every seeded
1612
+ unit carries `organization_id = null` — while an approval request always
1613
+ carries an org. Every business unit a flow author could pick therefore resolved
1614
+ to nobody, silently: the request opens, the slate is empty, and (with
1615
+ `lockRecord`) the record stays locked with no one able to act (#3424 is the
1616
+ downstream shape of the same dead end). Verified against a live showcase stack:
1617
+ a `{ type: 'department', value: 'bu_hq_finance' }` approver produced
1618
+ `pending_approvers: "department:bu_hq_finance"` while the unit's member sat
1619
+ right there in `sys_business_unit_member`.
1620
+
1621
+ Both the seed check and the subtree descent now scope to **this org ∪
1622
+ env-wide** — `$or: [{ organization_id: <org> }, { organization_id: null }]` —
1623
+ the same predicate `sys_metadata`'s pending-draft listing settled on for the
1624
+ identical reason (a strict equality silently dropping env-wide rows). The wall
1625
+ between two organizations is unchanged: another org's unit still fails the
1626
+ match, and a null-org parent does not drag another org's child unit into the
1627
+ subtree.
1628
+
1629
+ Note the same strict-equality scope exists in `plugin-sharing`'s
1630
+ `BusinessUnitGraphService.orgScope`. It is not reachable today — every
1631
+ materialized `sys_sharing_rule` row carries `organization_id = null`, so the
1632
+ filter is skipped — and is left alone here rather than widen an
1633
+ access-granting path on a defect that cannot currently fire.
1634
+
1635
+ - b5f9397: fix(sharing,runtime): a `sort` passed straight to the engine never ordered anything; migrate every in-repo engine call to canonical QueryAST keys (#4346)
1636
+
1637
+ Two changes with different weights, from one sweep of every in-repo engine
1638
+ call site that still speaks a deprecated alias.
1639
+
1640
+ **The bug — three dropped sorts.** #4346 made the engine fold `filter`→`where`
1641
+ and `top`→`limit` on all six methods. The other four pairs in
1642
+ `RPC_QUERY_ALIAS_SLOTS` (`select`, `sort`, `skip`, `populate`) are folded at
1643
+ the RPC/wire layer only — their values need shape lowering that belongs to
1644
+ those layers — and a **direct `engine.find()` never crosses that layer**. Three
1645
+ call sites passed `sort` there, so it rode onto the AST untouched, every
1646
+ driver's `Array.isArray(query.orderBy)` guard declined to emit an ORDER BY, and
1647
+ the query returned an ordinary-looking, arbitrarily-ordered result:
1648
+
1649
+ | call site | asked for | actually got |
1650
+ | ----------------------------------- | ------------------------------------------------- | --------------------------- |
1651
+ | `share-link-routes.ts` | shared AI conversation messages, `created_at asc` | messages in arbitrary order |
1652
+ | `runtime/domains/share-links.ts` | same route, runtime-domain copy | same |
1653
+ | `share-link-service.ts` `listLinks` | the 200 most recent share links | an arbitrary 200 |
1654
+
1655
+ All three combine the dropped sort with a `limit` — the "latest N" shape whose
1656
+ failure #4226 spelled out: an unapplied sort returns rows in arbitrary order,
1657
+ which `limit` then slices into an arbitrary page. #4226 fixed that in the wire
1658
+ normalizer; these calls sit one layer below it. `listLinks` had no test at all,
1659
+ which is why it went unnoticed. Now pinned — on the option bag the engine
1660
+ receives, not on row order, because the failure is that the key never becomes
1661
+ `orderBy` and a fake engine honouring either spelling would pass either way.
1662
+
1663
+ **The cleanup — 27 no-op renames.** Every remaining in-repo engine call passing
1664
+ `filter` now passes `where` (approvals 5, auth 2, reports 6, sharing 11,
1665
+ webhooks 2, plus the one `filters` in a spec doc example). These are strict
1666
+ no-ops since #4346 folds the alias — the point is that the framework stops
1667
+ depending on a spelling it asks users to migrate off, which is a prerequisite
1668
+ for ever retiring the aliases. Service-level `filter` PARAMETERS (each
1669
+ service's own public API, e.g. `listRequests(filter)`) are deliberately
1670
+ untouched — those are not engine option bags.
1671
+
1672
+ Two of the renamed calls were live victims of the #4346 bug rather than
1673
+ cosmetic: `auth-manager`'s `stampIdentitySource` read the table's first row via
1674
+ `findOne({filter})` and counted the whole table via `count({filter})`, so a
1675
+ federated sign-in never stamped `source: 'idp_provisioned'`. #4346 already
1676
+ corrected the behaviour; this makes the call say what it means.
1677
+
1678
+ - deb538f: fix(storage): let an object delegate file-read authorization to its service
1679
+
1680
+ Fixes a regression from the governed-download change (ADR-0104 D3 wave 2): a
1681
+ **legitimate approver could see a decision attachment's filename but got 403
1682
+ opening it**, found by driving app-showcase in a browser as a real non-admin
1683
+ approver.
1684
+
1685
+ Cause: a field-owned file's download was authorized by testing whether the
1686
+ caller can READ the owning row. For an ordinary business object that is right —
1687
+ row readability _is_ the access rule. For `sys_approval_action` it is the wrong
1688
+ authority: the audit table is deliberately closed to ordinary approver
1689
+ positions (`operation 'find' … is not permitted for positions [auditor,
1690
+ everyone]`), so the test denied the very approver the attachment was filed for.
1691
+ The approvals _service_ has always had the real rule, which is why the timeline
1692
+ listing the attachment returned 200 while the bytes returned 403.
1693
+
1694
+ An object may now name a service to answer the question instead:
1695
+
1696
+ - `ObjectSchema.fileAccessDelegate` — a kernel service that authorizes
1697
+ downloads of files owned by that object's media fields.
1698
+ - `IFileAccessDelegate.authorizeFileRead(recordId, context)` — the contract.
1699
+ - `sys_approval_action` declares `'approvals'`; `ApprovalService.authorizeFileRead`
1700
+ reuses the _same_ gate `listActions` applies (visibility of the parent
1701
+ request) rather than inventing a second, looser rule for the bytes.
1702
+
1703
+ **Fails closed**: a declared delegate that is missing or does not implement the
1704
+ method denies, rather than silently reverting to the raw read it was declared to
1705
+ replace. Objects without the declaration are unchanged.
1706
+
1707
+ Verified in the browser against app-showcase, both sides of the gate: the
1708
+ approver now downloads the real PDF (200), and an anonymous request is still
1709
+ refused (401) — the anonymous capability URL the original change closed stays
1710
+ closed. A decision attachment ends up exactly as readable as the decision it
1711
+ hangs off: never more, and no longer less.
1712
+
1713
+ - 25784cf: fix(automation,approvals): 节点类型校验推迟到插件贡献完成之后 —— approval flow 不再被误报"运行时会失败" (#4771)
1714
+
1715
+ showcase 每次冷启都打印 8 条断言:这些 flow "will fail at execution time"。8 条全是假的。
1716
+ `AutomationServicePlugin.start()` 从 ObjectQL registry 拉起 flow 并**当场**校验节点类型,而
1717
+ `ApprovalsServicePlugin.start()` 在 0.8 秒后才注册 `approval` 执行器 —— 校验器在词汇表还没
1718
+ 成型的时候就下了结论。
1719
+
1720
+ 真正的代价不是噪音,是信号丢失:**真的没装 approvals 插件**的部署会得到一模一样的 8 条告警,
1721
+ 所以这条 warn 无法区分"健康"和"坏掉",信噪比为 0。
1722
+
1723
+ ADR-0018 明确把节点词汇表定义为**开放、可运行时扩展**的(插件通过
1724
+ `registerNodeExecutor(type)` 贡献类型)。因此校验只在词汇表**封闭**的那一刻才成立:
1725
+
1726
+ - `AutomationEngine.sealNodeTypeVocabulary()` —— 宣告词汇表封闭,对**所有**已注册 flow 跑一次
1727
+ 权威校验,每个有问题的 flow warn 一条。`AutomationServicePlugin` 在 `kernel:bootstrapped`
1728
+ 调用它(严格晚于每个插件的 `start()` 和每个 `kernel:ready` handler —— 本插件自己的
1729
+ `kernel:ready` 还会再注册一批 flow,别的插件也可能在它的 `kernel:ready` 里贡献执行器)。
1730
+ - `AutomationEngine.getUnknownNodeTypeAudit(): UnknownNodeTypeAuditEntry[]` —— 同一发现的
1731
+ **状态**形态,供 host(CLI 启动摘要、健康检查)直接读,而不是去 grep 日志。与
1732
+ `getTriggerBindingAudit()` 同一套路。
1733
+ - 封闭之后 `registerFlow` **恢复即时告警**:Studio 发布 / dev reload 进正在运行的服务器时,
1734
+ 词汇表确实是完整的,那句断言此时为真。所以这是时序修复,不是把告警静音。
1735
+
1736
+ 告警文案也随之改成它现在能承诺的事:"Every plugin has started, so nothing will register them
1737
+ now — these nodes fail at execution time with NO_EXECUTOR",并给出补救动作。
1738
+
1739
+ 一并修掉同一缺陷类的另一半:`ApprovalsServicePlugin` 在**拿不到 automation 引擎**时,把
1740
+ "`approval` 节点没注册"记成 `info` —— 而 dev 的默认日志级别是 `warn`,于是**真降级发生时反而
1741
+ 看不见**(#4632:静默降级必须响亮)。现在是 `warn`,写明后果(该部署里每个 ADR-0019 approval
1742
+ flow 都会以 NO_EXECUTOR 失败)和补救(装 `@objectstack/service-automation`)。`catch` 同时收窄
1743
+ 到"服务查找"这一步,`registerApprovalNode` 内部真出错时会以自己的身份抛出,而不再被贴上
1744
+ "no automation engine" 的错误标签;`automation` 服务存在但不接受节点执行器的分支从前**一条日志
1745
+ 都不打**,现在同样 warn。
1746
+
1747
+ **嵌入式 host 注意**:直接 `new AutomationEngine()` 而不经过 `AutomationServicePlugin` 的宿主,
1748
+ 需要在自己的插件都装好之后调用一次 `sealNodeTypeVocabulary()`,才能拿到这条告警(以及之后的
1749
+ 即时校验)。
1750
+
1751
+ - 5d12675: Unify the approval-status vocabulary across the `sys_approval_request` i18n bundles (#7232).
1752
+
1753
+ A request rendered through the generic object surfaces used a different word than the same
1754
+ request in the Approvals Inbox and in the account-app navigation. The bundles now say what
1755
+ those surfaces already say:
1756
+
1757
+ - **zh-CN**: the `status` option `pending` reads 待审批 (was 待处理), and the `my_pending`
1758
+ view reads 待我审批 (was 我的待办), matching the account-app nav entry; the view's
1759
+ empty-state title was aligned to the same wording.
1760
+ - **en**: the `status` options are humanized — `Pending` / `Approved` / `Rejected` /
1761
+ `Recalled` / `Returned` — instead of shipping the raw enum values as labels.
1762
+ - **ja-JP / es-ES**: the `my_pending` view label now matches the nav wording (承認待ち /
1763
+ Aprobaciones pendientes).
1764
+
1765
+ Status **values** are unchanged — this is display wording only, so no stored data, filter,
1766
+ or API payload is affected.
1767
+
1768
+ - 8af76ae: The i18n extractor's default locale now tracks the source instead of merging (#8543), and the approval vocabularies carry authored English labels in the contract (#8580).
1769
+
1770
+ - `os i18n extract` merge mode no longer applies to the default locale: `en` is a copy of the source, not a translation, so an edited label/description/help now reaches the regenerated `en` bundle instead of being silently shadowed by the stale entry forever (53 stale entries had accumulated across 6 packages under the old behavior; all rewritten here). Translated locales (`zh-CN` / `ja-JP` / `es-ES`) keep merge semantics exactly as before — no existing translation is overwritten.
1771
+ - Bare-string and label-less select options now seed through the extractor's derived channel: the machine value still seeds the skeleton, but the coverage gate no longer demands "translations" of machine identifiers, and a copied value can no longer masquerade as authored display text.
1772
+ - New `@objectstack/spec/contracts` exports `APPROVAL_STATUS_LABELS` and `APPROVAL_ACTION_KIND_LABELS`: the authored English for `sys_approval_request.status` (previously living only in the generated `en` bundle) and `sys_approval_action.action` (previously shipping raw machine values such as `submit` / `request_info` — the #7232 humanization missed this sibling field). Both columns derive their option labels from these maps; the regenerated `en` bundles copy them verbatim.
1773
+
1774
+ - 9881074: fix(batch): the background walks seek instead of counting, so they stop skipping rows (#4363)
1775
+
1776
+ #4363 made a single paged read a partition of its result set. It could not make
1777
+ a _walk_ one: seven background scans paged with a growing `offset` while writing
1778
+ to the very rows they were reading, and an offset counts into a set those writes
1779
+ are changing. Rows slide past the cursor and are never visited.
1780
+
1781
+ That is not a slow page in any of these — it is a wrong answer wearing the shape
1782
+ of a clean run:
1783
+
1784
+ - **`rebuildApproverIndex`** built its desired state by walking
1785
+ `sys_approval_request WHERE status = 'pending'` with no `orderBy` at all, then
1786
+ **deleted** every index row that state did not explain. A skipped request
1787
+ meant an approver silently dropped from someone's queue. (The loop beside it
1788
+ ordered by `created_at` — not unique, so its pages were never a partition
1789
+ either.)
1790
+ - **`verifyFileReferences`** decides which files nothing references. A record it
1791
+ never visits is reported as an unreferenced file.
1792
+ - **`backfillFileReferences`** and the **pinyin companion backfill** rewrite
1793
+ each row they read, so their own writes were shifting the set out from under
1794
+ the cursor. Records were left unconverted and unsearchable by a run that
1795
+ reported success.
1796
+ - **`scanValueShapes`** exists to vouch that no stored value is off-shape, and
1797
+ it opens a migration gate on that evidence.
1798
+
1799
+ All of them now go through `keysetWalk` (`@objectstack/types`): order by a
1800
+ unique key, and seek past the last one instead of counting from the start. A
1801
+ row's key does not move when the row is updated, and cannot be shifted when
1802
+ another is deleted, so the walk is stable under exactly the mutation these
1803
+ functions perform. It is also O(n) rather than O(n²/page) — measured on
1804
+ Postgres over 2M rows, deep pages cost ~1.1 s by offset against ~0.09 s by seek.
1805
+
1806
+ One deliberate non-conversion: the REST **export** stream keeps its offset. It
1807
+ honors a caller-chosen sort, and a keyset walk would have to re-order the export
1808
+ by `id` to seek — changing what the user asked for to fix a cost. Its pages are
1809
+ already a partition since #4363; only the depth cost remains.
1810
+
1811
+ `keysetWalk` merges the cursor with `$and` rather than spreading it into the
1812
+ caller's filter, so a walk whose own `where` constrains the key column
1813
+ (`{ id: { $in: [...] } }`) keeps that constraint instead of having it silently
1814
+ overwritten. When a `max` cap is set it reads one row beyond the cap to tell
1815
+ "the cap stopped us" from "the source ended exactly there" — without that, a
1816
+ walk that read everything still reports `truncated`, and a caller acting on it
1817
+ goes looking for rows that were never withheld.
1818
+
1819
+ The storage suites' fake engines now **throw** on an `offset` instead of serving
1820
+ one, so the conversion is pinned rather than merely passing.
1821
+
1822
+ - cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)
1823
+
1824
+ These 20 packages declared no `files` field, so npm fell back to packing the
1825
+ whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
1826
+ listed **21 files** — 15 under `src/`, three of them unit tests
1827
+ (`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
1828
+ build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
1829
+ publish time rather than instead of it, so consumers were installing the
1830
+ TypeScript sources and the test suite alongside the artifact they asked for.
1831
+
1832
+ Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
1833
+ that already did. Nothing a consumer imports moves: every `main` / `types` /
1834
+ `exports` target in all 20 already resolved inside `dist/`, which the new
1835
+ `check:published-files` guard verifies rather than assumes. The visible change
1836
+ is a smaller install and a smaller dependency-scanning surface — `npm pack` on
1837
+ `@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.
1838
+
1839
+ The other half of the fix is the gate. Half the packages declaring `files` and
1840
+ half not was the #3786 shape — a hand-copied convention with nothing enforcing
1841
+ it, where whoever forgets the line gets no signal at all. `check:published-files`
1842
+ (new, wired into the always-required `lint` job) holds every non-private
1843
+ workspace package to four invariants: `files` is **declared**; it is
1844
+ **sufficient** (covers every entry point, so tightening a whitelist cannot ship
1845
+ a package that fails to resolve); it is **minimal** (admits no test, test-harness
1846
+ config or build script); and anything beyond `dist` + `README.md` is
1847
+ **registered** with a reason, reconciled in both directions so a stale exemption
1848
+ is an error rather than dead text. `@objectstack/spec` is the one package with
1849
+ registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
1850
+ `CHANGELOG.md` are product, not build input.
1851
+
1852
+ This also closes an assumption #4206 was resting on. Excluding `<pkg>/scripts/**`
1853
+ from the docs-drift implementation test is sound only while no package publishes
1854
+ `scripts/` as runtime code; that held, but it held because someone read all three
1855
+ offenders by hand. It is now checked on every PR.
1856
+
1857
+ - db48ad5: fix(security,approvals,metadata-core): restore batch routes on the eight objects the #3391 P1 companion fix missed (#3026)
1858
+
1859
+ The #3391 P1 contract made the bulk gate `bulk ∧ derived(child)`: a batch
1860
+ request is admitted only when the object grants the `bulk` **primitive** and the
1861
+ batched child operation is itself allowed. Before that, the `*Many` routes
1862
+ checked only the child verb, so a boilerplate CRUD-five whitelist
1863
+ (`['get','list','create','update','delete']`) batched fine.
1864
+
1865
+ The companion fix — adding the `bulk` primitive wherever an explicit whitelist
1866
+ survived — was applied only inside `platform-objects`. Eight objects carrying
1867
+ the same boilerplate live in other packages and kept the gap, so `/batch`,
1868
+ `createMany`, `updateMany` and `deleteMany` answered `405
1869
+ OBJECT_API_METHOD_NOT_ALLOWED` on objects whose single-record create/update/
1870
+ delete were wide open. `data-objectstack` rethrows that 405 without falling back
1871
+ to per-row writes, which surfaced as a hard error on multi-select delete in the
1872
+ Setup grids.
1873
+
1874
+ Objects reclaimed (whitelist now `['get','list','create','update','delete','bulk']`):
1875
+ `sys_capability`, `sys_permission_set`, `sys_position`,
1876
+ `sys_position_permission_set`, `sys_user_permission_set`, `sys_user_position`
1877
+ (plugin-security); `sys_approval_delegation` (plugin-approvals);
1878
+ `sys_view_definition` (metadata-core).
1879
+
1880
+ No new authority is granted: `bulk` only permits batching verbs each object
1881
+ already exposes one record at a time, and every batched row still passes the
1882
+ same row- and field-level permission checks. The whitelists stay explicit rather
1883
+ than being deleted — seven of the eight are `managedBy`, and
1884
+ `reconcileManagedApiMethods` (ADR-0103 D3) early-returns on a non-array
1885
+ `apiMethods`, so dropping the line would silently disable the managed-write
1886
+ backstop.
1887
+
1888
+ - dadd1ad: refactor(spec,plugin-sharing): retire the exported `SharingExecutionContext` type (#7218)
1889
+
1890
+ <!-- adr-0087: registered sharing-execution-context-retired -->
1891
+
1892
+ **BREAKING — public surface removal.** `SharingExecutionContext` is deleted from
1893
+ `@objectstack/spec` (`contracts/sharing-service`) and from
1894
+ `@objectstack/plugin-sharing`, which re-exported it. Both `api-surface/` and
1895
+ `export-origins/` snapshots are regenerated accordingly.
1896
+
1897
+ This is the deferred deletion recorded when #7070 split the convergence in two.
1898
+ #6523 / PR #7068 converged 36 contract signatures onto the full
1899
+ `resolveAuthzContext` envelope (`ExecutionContext`), applying the #6206 ruling —
1900
+ enforcement adjudicates on the whole envelope, never a per-site subset. The
1901
+ consumer halves then re-annotated the implementations: PR #7140 (identity:
1902
+ `plugin-sharing`, `plugin-audit`) and PR #7206 (services: `plugin-approvals`,
1903
+ `plugin-reports`). Both landed with the type still exported, because it is
1904
+ DEFINED in `packages/spec` and that package's retirement is the spec seat's to
1905
+ make. Nothing declares it any more, so it goes.
1906
+
1907
+ **Migration.** Anyone who imported `SharingExecutionContext` from either package
1908
+ should import `ExecutionContext` from `@objectstack/spec` instead — the type the
1909
+ contracts have declared since #7068. The old shape was six optional fields, all
1910
+ of which exist on the envelope with the same names and types, so a value that
1911
+ satisfied the retired type already satisfies `ExecutionContext`; only the
1912
+ spelling of the annotation changes.
1913
+
1914
+ **No runtime behaviour changes.** The type was erased at compile time and no
1915
+ signature's accepted shape moved: the contracts already took the wide envelope.
1916
+
1917
+ **What the retirement did NOT remove — the reason to read the pins.** Deleting
1918
+ the type does not make re-narrowing a compile error. Structural subtyping still
1919
+ accepts a six-field context where the envelope is expected, so the boundary is
1920
+ held by the declared parameter type plus the pins, exactly as before. The three
1921
+ `exec-context-annotation.pin.ts` files (`plugin-sharing`, `plugin-approvals`,
1922
+ `plugin-reports`) told their failure story as "the parameter narrows back to
1923
+ `SharingExecutionContext`", which a deletion would have quietly hollowed out.
1924
+ Each now keeps the retired six-field shape as a local, non-exported SPECIMEN
1925
+ type and refutes every enforcement parameter against it by type identity, so a
1926
+ re-narrowing under ANY name is red — alongside the fresh-literal
1927
+ excess-property checks they already carried. `sharing-service.test.ts` in
1928
+ `packages/spec` is re-anchored the same way, and its "twin unchanged in shape"
1929
+ case becomes a "twin stays retired" case. The narrative the retired type's doc
1930
+ block carried (the measured `(context as any).posture` specimen, and why tsc
1931
+ cannot police this) moves to the module doc of `contracts/sharing-service`,
1932
+ which the contracts and pins now point at.
1933
+
1934
+ - 2465133: fix(plugins): sweep the service-lookup erasures out of the plugin composition roots, and fix the two alias-only HTTP reads it exposed (#4251 B5)
1935
+
1936
+ Batch B5 of the #4251 sweep: the seven remaining `packages/plugins/*` composition
1937
+ roots. 35 lookup sites that had been erased to `any` now carry the slot's
1938
+ contract, so the compiler checks what each plugin actually calls on the service
1939
+ it resolved. The ratchet drops 143 sites / 32 files to 108 / 25.
1940
+
1941
+ **Two real defects, both of the shape this sweep exists to find.** Approvals'
1942
+ actionable-link pages (ADR-0043) and sharing's public share-link REST routes each
1943
+ read the HTTP server under `http-server` _only_ — the deprecated alias. The
1944
+ ledger records `http.server` as canonical and as the only name present on every
1945
+ provider path: `runtime.ts`'s `config.server` path registers no alias at all. On
1946
+ that path both lookups threw, the surrounding `catch` swallowed it, and the
1947
+ routes silently never mounted — approval e-mail action links 404'd and the
1948
+ share-link surface was absent, with nothing in the log to say so. Both reads are
1949
+ now canonical-first with the alias as fallback, each name in its own `try`
1950
+ because `getService` throws on an empty slot (so `a() ?? b()` inside one `try`
1951
+ never reaches `b` — the same correction #4393 made in metadata and
1952
+ cloud-connection).
1953
+
1954
+ Typing choices follow the batch method: pure data-plane consumers take the
1955
+ narrow contract (`IDataEngine` in reports), consumers that bind hook or
1956
+ middleware seams take the engine seen whole (`IObjectQLEngine` in approvals,
1957
+ sharing and pinyin-search), and slots with no contract get a **named** local
1958
+ surface rather than `any` — plugin-email's `MailSettingsSurface`, and the
1959
+ surfaces the consuming packages already declared (`ApprovalMessagingSurface`,
1960
+ `SharingSecurityProbe`, `ReportEmail`). A named surface that omits a member
1961
+ still makes the compiler name every call site; `any` says nothing.
1962
+
1963
+ No behaviour change beyond the two alias reads. No contract changes.
1964
+
1965
+ - 83c161f: feat(automation)!: a flow run with no trigger user may no longer touch data (#3760)
1966
+
1967
+ An effective `runAs:'user'` run that resolves **no trigger user** used to execute
1968
+ its data nodes **UNSCOPED** — it presented no principal, and the data security
1969
+ middleware skips when there is no principal, so the run read and wrote every row.
1970
+ `runAs:'user'` is an access-_narrowing_ declaration; failing to resolve it must
1971
+ never resolve to a grant (ADR-0049). It now **refuses** the operation
1972
+ (`UnscopedRunDataAccessError`), naming `runAs:'system'` as the fix.
1973
+
1974
+ **This was never really about schedules.** The docs, the spec, the runtime
1975
+ warning and the lint all described a schedule-shaped problem, and the lint only
1976
+ ever matched that shape. But the runtime predicate is "no user", and the
1977
+ commonest way to have no user is a **record-change flow fired by a write that
1978
+ carried none**: `isSystem` does _not_ suppress trigger dispatch — only
1979
+ `skipTriggers` does, and exactly three first-party paths set it — so every
1980
+ plugin/service system write, the approvals status mirror, and a `runAs:'system'`
1981
+ flow's own data node dispatched record-change flows with `userId: undefined`.
1982
+ Ordinary users reach those writes routinely (submitting for approval mirrors a
1983
+ status onto the target record), so the fail-open was reachable by unprivileged
1984
+ input and was the common case, not the rare one.
1985
+
1986
+ Deliberately **not** implemented as "inherit the triggering write's posture and
1987
+ run as `isSystem`". That reads like a relabel but is a privilege escalation: the
1988
+ security middleware's `isSystem` short-circuit fires _before_ its
1989
+ package-managed-row, system-row, audience-anchor and delegated-admin gates, all
1990
+ of which a principal-less context still has to clear. Such a run cannot write
1991
+ `sys_user_position` today; as `isSystem` it could. "Unscoped" was never
1992
+ equivalent to "system".
1993
+
1994
+ **Breaking — how to migrate.** A flow that reacts to system writes and needs to
1995
+ act beyond one user's grants declares `runAs: 'system'`, making the elevation
1996
+ explicit and audit-attributable. Otherwise ensure the trigger supplies a user.
1997
+ Flows that touch no data are unaffected (`runAs` is moot), and the failure is
1998
+ isolated: the trigger already swallows flow errors, so the originating write
1999
+ still succeeds. The engine warns at run _setup_, before any node executes.
2000
+
2001
+ **#3712's user-less provenance path is subsumed, not broken.** That fix let a
2002
+ run with no trigger user write its own approval-locked record by carrying a
2003
+ provenance-only ObjectQL context (the run id, nothing else). Such a run can no
2004
+ longer perform a data operation at all — presenting no principal is exactly what
2005
+ made the write unscoped — so it is refused before the lock is consulted. The
2006
+ capability survives via the explicit route: a schedule that must write records
2007
+ declares `runAs:'system'`, which the lock hook exempts on its own `isSystem`
2008
+ branch. The `flowRunId` exemption itself stays live and load-bearing for what
2009
+ #3703 built it for — a `runAs:'user'` run that _does_ have a user — where the
2010
+ exemption is still provenance rather than privilege.
2011
+
2012
+ Also in this change:
2013
+
2014
+ - **`flow-schedule-runas-unscoped` → `flow-runas-unscoped`, and it now fails the
2015
+ build.** It read as a gate and behaved as a comment — `os compile` documented
2016
+ that the flow lint "NEVER fails the build" — which is close to no net at all
2017
+ for the audience it protects, very often an AI generating flows in bulk. It now
2018
+ also covers the other provably user-less triggers (`time_relative`, `api`), per
2019
+ ADR-0073 D5. It still cannot cover `record_change`, which is undecidable at
2020
+ authoring time — that is exactly why the runtime refusal exists.
2021
+ - **Three seed writes stopped firing automation.** The seed loader's pass-2
2022
+ deferred-reference back-fill and both of `AppPlugin`'s basic-insert fallbacks
2023
+ inlined a bare `{ isSystem: true }` instead of the shared seed options, so they
2024
+ seeded with record-change automation live — the self-trigger vector
2025
+ `skipTriggers` exists to prevent, on the writes that skipped it.
2026
+ - **ADR-0073 amended.** Its severity rationale ("an unprivileged user cannot
2027
+ trigger a schedule, so there is no untrusted-input path") is falsified, and its
2028
+ rejection of fail-closed ("breaks legitimate scheduled CRUD — 2/3 example flows
2029
+ relied on the default") expired when those flows were fixed to declare
2030
+ `runAs:'system'`. Refusal is an interim posture, forward-compatible with the
2031
+ ADR's `automation` principal: when that lands, the refusal point becomes the
2032
+ place that resolves it.
2033
+
2034
+ - Updated dependencies [50616d9]
2035
+ - Updated dependencies [430dcc2]
2036
+ - Updated dependencies [690ccf2]
2037
+ - Updated dependencies [6a67d7a]
2038
+ - Updated dependencies [098f4bb]
2039
+ - Updated dependencies [333a374]
2040
+ - Updated dependencies [9fe9c1d]
2041
+ - Updated dependencies [3d5c090]
2042
+ - Updated dependencies [e5bd768]
2043
+ - Updated dependencies [08b5a3d]
2044
+ - Updated dependencies [e027b3e]
2045
+ - Updated dependencies [e6ac4bd]
2046
+ - Updated dependencies [c2429b0]
2047
+ - Updated dependencies [445a0c2]
2048
+ - Updated dependencies [d99aeb3]
2049
+ - Updated dependencies [f6609e6]
2050
+ - Updated dependencies [4727eb8]
2051
+ - Updated dependencies [a70358a]
2052
+ - Updated dependencies [0ecc656]
2053
+ - Updated dependencies [06772eb]
2054
+ - Updated dependencies [d4e0809]
2055
+ - Updated dependencies [80334c7]
2056
+ - Updated dependencies [f63cd09]
2057
+ - Updated dependencies [97e7e3c]
2058
+ - Updated dependencies [ce5242c]
2059
+ - Updated dependencies [a7163ea]
2060
+ - Updated dependencies [e6e9379]
2061
+ - Updated dependencies [5823d59]
2062
+ - Updated dependencies [3140f9c]
2063
+ - Updated dependencies [9500ba4]
2064
+ - Updated dependencies [fa3d0cf]
2065
+ - Updated dependencies [af5a224]
2066
+ - Updated dependencies [71f76e1]
2067
+ - Updated dependencies [37b1346]
2068
+ - Updated dependencies [99736a0]
2069
+ - Updated dependencies [fe67e34]
2070
+ - Updated dependencies [fdb4f50]
2071
+ - Updated dependencies [270650f]
2072
+ - Updated dependencies [3aef718]
2073
+ - Updated dependencies [1bd5652]
2074
+ - Updated dependencies [14252d3]
2075
+ - Updated dependencies [7fb436c]
2076
+ - Updated dependencies [879ea13]
2077
+ - Updated dependencies [8828b9e]
2078
+ - Updated dependencies [1ea6bce]
2079
+ - Updated dependencies [c1dcacd]
2080
+ - Updated dependencies [ad303ed]
2081
+ - Updated dependencies [32ccb23]
2082
+ - Updated dependencies [f5a4ef0]
2083
+ - Updated dependencies [2d3e255]
2084
+ - Updated dependencies [a8940e4]
2085
+ - Updated dependencies [7d7521f]
2086
+ - Updated dependencies [5dc4d02]
2087
+ - Updated dependencies [f724f69]
2088
+ - Updated dependencies [98877c9]
2089
+ - Updated dependencies [98877c9]
2090
+ - Updated dependencies [53068c1]
2091
+ - Updated dependencies [ee58392]
2092
+ - Updated dependencies [f16e54e]
2093
+ - Updated dependencies [c44dd5e]
2094
+ - Updated dependencies [06be54e]
2095
+ - Updated dependencies [28ad90e]
2096
+ - Updated dependencies [76d74ec]
2097
+ - Updated dependencies [201b31f]
2098
+ - Updated dependencies [e6b1b69]
2099
+ - Updated dependencies [259459d]
2100
+ - Updated dependencies [3f7f14e]
2101
+ - Updated dependencies [e2616e0]
2102
+ - Updated dependencies [6fdc5c6]
2103
+ - Updated dependencies [8b9d71e]
2104
+ - Updated dependencies [05154a1]
2105
+ - Updated dependencies [33f5e23]
2106
+ - Updated dependencies [259af21]
2107
+ - Updated dependencies [f8644c7]
2108
+ - Updated dependencies [306ca50]
2109
+ - Updated dependencies [840ee4b]
2110
+ - Updated dependencies [978fed2]
2111
+ - Updated dependencies [cfc293f]
2112
+ - Updated dependencies [587fc91]
2113
+ - Updated dependencies [de70b42]
2114
+ - Updated dependencies [9b6fe7c]
2115
+ - Updated dependencies [64cd010]
2116
+ - Updated dependencies [fb3d99b]
2117
+ - Updated dependencies [1986594]
2118
+ - Updated dependencies [6968885]
2119
+ - Updated dependencies [eaed61f]
2120
+ - Updated dependencies [52200b4]
2121
+ - Updated dependencies [cdfbee2]
2122
+ - Updated dependencies [ad4af62]
2123
+ - Updated dependencies [debe2f6]
2124
+ - Updated dependencies [d44dbfa]
2125
+ - Updated dependencies [29c6c9d]
2126
+ - Updated dependencies [d21c001]
2127
+ - Updated dependencies [ad047d2]
2128
+ - Updated dependencies [8c711fb]
2129
+ - Updated dependencies [f1cc3a3]
2130
+ - Updated dependencies [09e4547]
2131
+ - Updated dependencies [97b0798]
2132
+ - Updated dependencies [474fe39]
2133
+ - Updated dependencies [0bc685a]
2134
+ - Updated dependencies [b949059]
2135
+ - Updated dependencies [2826d1e]
2136
+ - Updated dependencies [be1c52c]
2137
+ - Updated dependencies [c5ff96d]
2138
+ - Updated dependencies [5a84d41]
2139
+ - Updated dependencies [84e7be9]
2140
+ - Updated dependencies [91f4c78]
2141
+ - Updated dependencies [ddc2527]
2142
+ - Updated dependencies [820eff9]
2143
+ - Updated dependencies [a6c3f38]
2144
+ - Updated dependencies [5fa04fb]
2145
+ - Updated dependencies [debc23a]
2146
+ - Updated dependencies [0f8ad09]
2147
+ - Updated dependencies [553a47f]
2148
+ - Updated dependencies [43a7a8d]
2149
+ - Updated dependencies [a98085f]
2150
+ - Updated dependencies [20b1a9e]
2151
+ - Updated dependencies [344a22a]
2152
+ - Updated dependencies [4827e91]
2153
+ - Updated dependencies [8d895ff]
2154
+ - Updated dependencies [86f7a20]
2155
+ - Updated dependencies [a3a884d]
2156
+ - Updated dependencies [cfed092]
2157
+ - Updated dependencies [203a449]
2158
+ - Updated dependencies [8f9689f]
2159
+ - Updated dependencies [73f69dc]
2160
+ - Updated dependencies [04c56aa]
2161
+ - Updated dependencies [f6472d7]
2162
+ - Updated dependencies [57a3bb3]
2163
+ - Updated dependencies [b3efeb7]
2164
+ - Updated dependencies [ddd075a]
2165
+ - Updated dependencies [88154be]
2166
+ - Updated dependencies [e8dc61e]
2167
+ - Updated dependencies [9c82146]
2168
+ - Updated dependencies [5f9a987]
2169
+ - Updated dependencies [744b8f5]
2170
+ - Updated dependencies [9f5cc79]
2171
+ - Updated dependencies [ac37fc6]
2172
+ - Updated dependencies [9f060e5]
2173
+ - Updated dependencies [bc17d39]
2174
+ - Updated dependencies [2f3e793]
2175
+ - Updated dependencies [4820f55]
2176
+ - Updated dependencies [462d9c4]
2177
+ - Updated dependencies [78caf51]
2178
+ - Updated dependencies [7d21581]
2179
+ - Updated dependencies [37785ed]
2180
+ - Updated dependencies [62a789b]
2181
+ - Updated dependencies [2e284b2]
2182
+ - Updated dependencies [d8e8d9c]
2183
+ - Updated dependencies [789ad63]
2184
+ - Updated dependencies [f2445c9]
2185
+ - Updated dependencies [94e749b]
2186
+ - Updated dependencies [ea1d916]
2187
+ - Updated dependencies [2af1988]
2188
+ - Updated dependencies [0af50a3]
2189
+ - Updated dependencies [1b49eaf]
2190
+ - Updated dependencies [ae31a19]
2191
+ - Updated dependencies [b230e5e]
2192
+ - Updated dependencies [5d24f4b]
2193
+ - Updated dependencies [29b94ed]
2194
+ - Updated dependencies [07c68b0]
2195
+ - Updated dependencies [f6cd635]
2196
+ - Updated dependencies [2e836de]
2197
+ - Updated dependencies [e0f300b]
2198
+ - Updated dependencies [0161c7f]
2199
+ - Updated dependencies [e900015]
2200
+ - Updated dependencies [db02d47]
2201
+ - Updated dependencies [b5bdf48]
2202
+ - Updated dependencies [23338c3]
2203
+ - Updated dependencies [12a19a8]
2204
+ - Updated dependencies [5b843fb]
2205
+ - Updated dependencies [62b6a2f]
2206
+ - Updated dependencies [7e5af5c]
2207
+ - Updated dependencies [5b4780b]
2208
+ - Updated dependencies [a933452]
2209
+ - Updated dependencies [9d1d9c7]
2210
+ - Updated dependencies [8140915]
2211
+ - Updated dependencies [a019e52]
2212
+ - Updated dependencies [e8f8f6c]
2213
+ - Updated dependencies [41dcda3]
2214
+ - Updated dependencies [7b48cf9]
2215
+ - Updated dependencies [b5404f4]
2216
+ - Updated dependencies [64fc6d5]
2217
+ - Updated dependencies [b746aa0]
2218
+ - Updated dependencies [b4487aa]
2219
+ - Updated dependencies [1007379]
2220
+ - Updated dependencies [65ca83a]
2221
+ - Updated dependencies [0bfdf46]
2222
+ - Updated dependencies [947d4f9]
2223
+ - Updated dependencies [f764691]
2224
+ - Updated dependencies [e120a5a]
2225
+ - Updated dependencies [e5bd2f6]
2226
+ - Updated dependencies [e9b5265]
2227
+ - Updated dependencies [e650d67]
2228
+ - Updated dependencies [121852d]
2229
+ - Updated dependencies [04476e7]
2230
+ - Updated dependencies [67bf2e2]
2231
+ - Updated dependencies [eaaf03c]
2232
+ - Updated dependencies [d17df80]
2233
+ - Updated dependencies [7d0e7b5]
2234
+ - Updated dependencies [c6d1cb4]
2235
+ - Updated dependencies [6513c17]
2236
+ - Updated dependencies [36030ff]
2237
+ - Updated dependencies [79228cd]
2238
+ - Updated dependencies [6117f7b]
2239
+ - Updated dependencies [87aca93]
2240
+ - Updated dependencies [e533b0b]
2241
+ - Updated dependencies [cdf4d9a]
2242
+ - Updated dependencies [aee1806]
2243
+ - Updated dependencies [c13350b]
2244
+ - Updated dependencies [c13350b]
2245
+ - Updated dependencies [2c1988c]
2246
+ - Updated dependencies [9ca2d85]
2247
+ - Updated dependencies [c13350b]
2248
+ - Updated dependencies [891d345]
2249
+ - Updated dependencies [c8124e5]
2250
+ - Updated dependencies [a52e2ef]
2251
+ - Updated dependencies [5293114]
2252
+ - Updated dependencies [376a061]
2253
+ - Updated dependencies [c142ced]
2254
+ - Updated dependencies [211abdb]
2255
+ - Updated dependencies [b3363e9]
2256
+ - Updated dependencies [eda599e]
2257
+ - Updated dependencies [a1a4140]
2258
+ - Updated dependencies [c20b875]
2259
+ - Updated dependencies [7c7e246]
2260
+ - Updated dependencies [2ef1807]
2261
+ - Updated dependencies [f35cdc5]
2262
+ - Updated dependencies [d03fe25]
2263
+ - Updated dependencies [2a37694]
2264
+ - Updated dependencies [217e2e6]
2265
+ - Updated dependencies [2672f85]
2266
+ - Updated dependencies [20bc357]
2267
+ - Updated dependencies [11066f6]
2268
+ - Updated dependencies [916af17]
2269
+ - Updated dependencies [84c86fb]
2270
+ - Updated dependencies [2a2a9fb]
2271
+ - Updated dependencies [86a71d1]
2272
+ - Updated dependencies [c001422]
2273
+ - Updated dependencies [77022a9]
2274
+ - Updated dependencies [d5c75e2]
2275
+ - Updated dependencies [03d26f7]
2276
+ - Updated dependencies [5966c2a]
2277
+ - Updated dependencies [2382580]
2278
+ - Updated dependencies [9ea2bc5]
2279
+ - Updated dependencies [a2e157c]
2280
+ - Updated dependencies [95c4227]
2281
+ - Updated dependencies [2a61116]
2282
+ - Updated dependencies [52760bf]
2283
+ - Updated dependencies [5543020]
2284
+ - Updated dependencies [880d343]
2285
+ - Updated dependencies [6e82972]
2286
+ - Updated dependencies [d4df105]
2287
+ - Updated dependencies [4615a18]
2288
+ - Updated dependencies [f505689]
2289
+ - Updated dependencies [d9fa683]
2290
+ - Updated dependencies [32d3800]
2291
+ - Updated dependencies [606d577]
2292
+ - Updated dependencies [4384921]
2293
+ - Updated dependencies [e2798fa]
2294
+ - Updated dependencies [3c628ce]
2295
+ - Updated dependencies [c2d9098]
2296
+ - Updated dependencies [0fd8556]
2297
+ - Updated dependencies [3c7bcc0]
2298
+ - Updated dependencies [4b6cac7]
2299
+ - Updated dependencies [7631964]
2300
+ - Updated dependencies [ac471a0]
2301
+ - Updated dependencies [60ae58e]
2302
+ - Updated dependencies [7f62706]
2303
+ - Updated dependencies [667fa44]
2304
+ - Updated dependencies [37e38d1]
2305
+ - Updated dependencies [e906126]
2306
+ - Updated dependencies [ce92674]
2307
+ - Updated dependencies [08363a0]
2308
+ - Updated dependencies [444de5b]
2309
+ - Updated dependencies [0f17114]
2310
+ - Updated dependencies [a227ed7]
2311
+ - Updated dependencies [7cb922e]
2312
+ - Updated dependencies [1d22114]
2313
+ - Updated dependencies [1eb13a0]
2314
+ - Updated dependencies [c52e608]
2315
+ - Updated dependencies [9613396]
2316
+ - Updated dependencies [3f7b4ff]
2317
+ - Updated dependencies [74155c7]
2318
+ - Updated dependencies [b5f9397]
2319
+ - Updated dependencies [db0d53c]
2320
+ - Updated dependencies [ed77493]
2321
+ - Updated dependencies [6908830]
2322
+ - Updated dependencies [8b06bba]
2323
+ - Updated dependencies [58a03d2]
2324
+ - Updated dependencies [2bacd1a]
2325
+ - Updated dependencies [e47b342]
2326
+ - Updated dependencies [4c54037]
2327
+ - Updated dependencies [dc530b4]
2328
+ - Updated dependencies [9f601e8]
2329
+ - Updated dependencies [6a9dec6]
2330
+ - Updated dependencies [0f7157b]
2331
+ - Updated dependencies [4dc1c7d]
2332
+ - Updated dependencies [d9bef45]
2333
+ - Updated dependencies [f598aa8]
2334
+ - Updated dependencies [4dfd002]
2335
+ - Updated dependencies [f549a0d]
2336
+ - Updated dependencies [51c5227]
2337
+ - Updated dependencies [82da264]
2338
+ - Updated dependencies [f586f1a]
2339
+ - Updated dependencies [77be690]
2340
+ - Updated dependencies [4ed7ed4]
2341
+ - Updated dependencies [9b9b70f]
2342
+ - Updated dependencies [f5a9bc2]
2343
+ - Updated dependencies [e59786e]
2344
+ - Updated dependencies [2fa4ca1]
2345
+ - Updated dependencies [bcf1112]
2346
+ - Updated dependencies [baeb4f0]
2347
+ - Updated dependencies [29488cc]
2348
+ - Updated dependencies [881a3cc]
2349
+ - Updated dependencies [f5a2320]
2350
+ - Updated dependencies [ad6317b]
2351
+ - Updated dependencies [811c30c]
2352
+ - Updated dependencies [a4a85c8]
2353
+ - Updated dependencies [859cb83]
2354
+ - Updated dependencies [07a4e26]
2355
+ - Updated dependencies [9774b78]
2356
+ - Updated dependencies [d5e9f6e]
2357
+ - Updated dependencies [8a88885]
2358
+ - Updated dependencies [deb538f]
2359
+ - Updated dependencies [b49ccfd]
2360
+ - Updated dependencies [5b89711]
2361
+ - Updated dependencies [85d95e7]
2362
+ - Updated dependencies [08cd163]
2363
+ - Updated dependencies [0c8a22f]
2364
+ - Updated dependencies [5f7669e]
2365
+ - Updated dependencies [becbe53]
2366
+ - Updated dependencies [b127c8b]
2367
+ - Updated dependencies [763931e]
2368
+ - Updated dependencies [ec975f1]
2369
+ - Updated dependencies [168f60f]
2370
+ - Updated dependencies [b07d829]
2371
+ - Updated dependencies [de9af8a]
2372
+ - Updated dependencies [eb4204b]
2373
+ - Updated dependencies [a80302a]
2374
+ - Updated dependencies [a648e96]
2375
+ - Updated dependencies [a47ac06]
2376
+ - Updated dependencies [e4c61a7]
2377
+ - Updated dependencies [cc60165]
2378
+ - Updated dependencies [474f131]
2379
+ - Updated dependencies [081aa6f]
2380
+ - Updated dependencies [91f4c78]
2381
+ - Updated dependencies [050cd82]
2382
+ - Updated dependencies [4d552af]
2383
+ - Updated dependencies [44d677c]
2384
+ - Updated dependencies [c32944d]
2385
+ - Updated dependencies [1dd780f]
2386
+ - Updated dependencies [e8d0c21]
2387
+ - Updated dependencies [244ca86]
2388
+ - Updated dependencies [546ab3c]
2389
+ - Updated dependencies [cafec0a]
2390
+ - Updated dependencies [58f3220]
2391
+ - Updated dependencies [07f1822]
2392
+ - Updated dependencies [c4df271]
2393
+ - Updated dependencies [c8d6f6e]
2394
+ - Updated dependencies [0b51bb6]
2395
+ - Updated dependencies [08f93bc]
2396
+ - Updated dependencies [d9971d3]
2397
+ - Updated dependencies [7dc1067]
2398
+ - Updated dependencies [4f13be2]
2399
+ - Updated dependencies [a41ba5c]
2400
+ - Updated dependencies [189854c]
2401
+ - Updated dependencies [0e3a226]
2402
+ - Updated dependencies [92a67f2]
2403
+ - Updated dependencies [9136327]
2404
+ - Updated dependencies [bf0ae99]
2405
+ - Updated dependencies [c7e7900]
2406
+ - Updated dependencies [eb3e650]
2407
+ - Updated dependencies [abeb375]
2408
+ - Updated dependencies [cb3b6cd]
2409
+ - Updated dependencies [73b7234]
2410
+ - Updated dependencies [d2b97c3]
2411
+ - Updated dependencies [61cc079]
2412
+ - Updated dependencies [45dc446]
2413
+ - Updated dependencies [0e96e46]
2414
+ - Updated dependencies [c1d44f7]
2415
+ - Updated dependencies [59b794f]
2416
+ - Updated dependencies [ef4efa8]
2417
+ - Updated dependencies [cbb6a5c]
2418
+ - Updated dependencies [fc3a36a]
2419
+ - Updated dependencies [ab9fb5c]
2420
+ - Updated dependencies [69787f0]
2421
+ - Updated dependencies [5d022a1]
2422
+ - Updated dependencies [042b9ee]
2423
+ - Updated dependencies [b25a116]
2424
+ - Updated dependencies [02dc076]
2425
+ - Updated dependencies [f985b3f]
2426
+ - Updated dependencies [795b6e1]
2427
+ - Updated dependencies [d52d4fe]
2428
+ - Updated dependencies [742cebb]
2429
+ - Updated dependencies [175d789]
2430
+ - Updated dependencies [f549a0d]
2431
+ - Updated dependencies [524151c]
2432
+ - Updated dependencies [427344c]
2433
+ - Updated dependencies [8af76ae]
2434
+ - Updated dependencies [1d4756e]
2435
+ - Updated dependencies [720c5ad]
2436
+ - Updated dependencies [a8d1e24]
2437
+ - Updated dependencies [b85cc54]
2438
+ - Updated dependencies [a36db28]
2439
+ - Updated dependencies [7a8476f]
2440
+ - Updated dependencies [518ca7a]
2441
+ - Updated dependencies [d1cabaa]
2442
+ - Updated dependencies [41642b0]
2443
+ - Updated dependencies [4cca74c]
2444
+ - Updated dependencies [88ef03e]
2445
+ - Updated dependencies [9a4932a]
2446
+ - Updated dependencies [3f8817a]
2447
+ - Updated dependencies [a2443e3]
2448
+ - Updated dependencies [e1554b1]
2449
+ - Updated dependencies [9e2caf3]
2450
+ - Updated dependencies [4856789]
2451
+ - Updated dependencies [81ce41a]
2452
+ - Updated dependencies [85e1e4e]
2453
+ - Updated dependencies [c3f4916]
2454
+ - Updated dependencies [55dbbba]
2455
+ - Updated dependencies [33e0385]
2456
+ - Updated dependencies [dac6a08]
2457
+ - Updated dependencies [72c3c86]
2458
+ - Updated dependencies [3670cf9]
2459
+ - Updated dependencies [2d8dba3]
2460
+ - Updated dependencies [7f1a635]
2461
+ - Updated dependencies [2205363]
2462
+ - Updated dependencies [09fe58d]
2463
+ - Updated dependencies [f9fc874]
2464
+ - Updated dependencies [d62f8eb]
2465
+ - Updated dependencies [d0a5ceb]
2466
+ - Updated dependencies [a7586cd]
2467
+ - Updated dependencies [4c5e80e]
2468
+ - Updated dependencies [4b5702a]
2469
+ - Updated dependencies [011b386]
2470
+ - Updated dependencies [e18a162]
2471
+ - Updated dependencies [e98fb14]
2472
+ - Updated dependencies [394b7a1]
2473
+ - Updated dependencies [ce92674]
2474
+ - Updated dependencies [0f2fdcd]
2475
+ - Updated dependencies [d6d1a50]
2476
+ - Updated dependencies [cf2c9b7]
2477
+ - Updated dependencies [8ffa8b9]
2478
+ - Updated dependencies [d127ff0]
2479
+ - Updated dependencies [674ac99]
2480
+ - Updated dependencies [833b512]
2481
+ - Updated dependencies [9881074]
2482
+ - Updated dependencies [1b9a53b]
2483
+ - Updated dependencies [36d90fc]
2484
+ - Updated dependencies [7777e8f]
2485
+ - Updated dependencies [9b86cf6]
2486
+ - Updated dependencies [d063a96]
2487
+ - Updated dependencies [8825a06]
2488
+ - Updated dependencies [5087ac6]
2489
+ - Updated dependencies [6965160]
2490
+ - Updated dependencies [677b591]
2491
+ - Updated dependencies [cf7c694]
2492
+ - Updated dependencies [ddd0f06]
2493
+ - Updated dependencies [d77d1b7]
2494
+ - Updated dependencies [0f9faa2]
2495
+ - Updated dependencies [2d1ddf0]
2496
+ - Updated dependencies [354b00f]
2497
+ - Updated dependencies [3de535b]
2498
+ - Updated dependencies [fe2e15a]
2499
+ - Updated dependencies [5b79a34]
2500
+ - Updated dependencies [502564d]
2501
+ - Updated dependencies [603cab8]
2502
+ - Updated dependencies [c757854]
2503
+ - Updated dependencies [471839d]
2504
+ - Updated dependencies [507b92a]
2505
+ - Updated dependencies [46365ab]
2506
+ - Updated dependencies [b508244]
2507
+ - Updated dependencies [df95346]
2508
+ - Updated dependencies [3dede58]
2509
+ - Updated dependencies [c6b6bb4]
2510
+ - Updated dependencies [594508e]
2511
+ - Updated dependencies [7cf42fe]
2512
+ - Updated dependencies [5966c2a]
2513
+ - Updated dependencies [59c544d]
2514
+ - Updated dependencies [0045682]
2515
+ - Updated dependencies [7309c81]
2516
+ - Updated dependencies [2f59da0]
2517
+ - Updated dependencies [7372d46]
2518
+ - Updated dependencies [5e247fd]
2519
+ - Updated dependencies [d56012f]
2520
+ - Updated dependencies [1a53a02]
2521
+ - Updated dependencies [f78dd83]
2522
+ - Updated dependencies [a2cd18a]
2523
+ - Updated dependencies [9051802]
2524
+ - Updated dependencies [20bc1ec]
2525
+ - Updated dependencies [1c625ca]
2526
+ - Updated dependencies [2f8328c]
2527
+ - Updated dependencies [a954634]
2528
+ - Updated dependencies [2a6c279]
2529
+ - Updated dependencies [9319586]
2530
+ - Updated dependencies [8c8f0df]
2531
+ - Updated dependencies [8ad609c]
2532
+ - Updated dependencies [bbee302]
2533
+ - Updated dependencies [90c2b15]
2534
+ - Updated dependencies [4638aaa]
2535
+ - Updated dependencies [0222d3c]
2536
+ - Updated dependencies [08863dd]
2537
+ - Updated dependencies [39eb01b]
2538
+ - Updated dependencies [071d0dc]
2539
+ - Updated dependencies [f293d45]
2540
+ - Updated dependencies [56664f5]
2541
+ - Updated dependencies [71f205d]
2542
+ - Updated dependencies [f067930]
2543
+ - Updated dependencies [414395b]
2544
+ - Updated dependencies [42eeb7d]
2545
+ - Updated dependencies [31cbe90]
2546
+ - Updated dependencies [6b7129a]
2547
+ - Updated dependencies [c5adfe1]
2548
+ - Updated dependencies [97ace2a]
2549
+ - Updated dependencies [26e1029]
2550
+ - Updated dependencies [0a936ea]
2551
+ - Updated dependencies [90bbf25]
2552
+ - Updated dependencies [023c00b]
2553
+ - Updated dependencies [eb91eba]
2554
+ - Updated dependencies [42da73d]
2555
+ - Updated dependencies [01e124d]
2556
+ - Updated dependencies [ef7b5ef]
2557
+ - Updated dependencies [9514767]
2558
+ - Updated dependencies [8f20201]
2559
+ - Updated dependencies [155507e]
2560
+ - Updated dependencies [643b7c7]
2561
+ - Updated dependencies [7bba90b]
2562
+ - Updated dependencies [8813b90]
2563
+ - Updated dependencies [108ba8d]
2564
+ - Updated dependencies [2a5f04a]
2565
+ - Updated dependencies [4f740b0]
2566
+ - Updated dependencies [030125b]
2567
+ - Updated dependencies [7ce02eb]
2568
+ - Updated dependencies [b4ad984]
2569
+ - Updated dependencies [e7a7506]
2570
+ - Updated dependencies [a9f32df]
2571
+ - Updated dependencies [aeb9b27]
2572
+ - Updated dependencies [7d27da0]
2573
+ - Updated dependencies [d0d5205]
2574
+ - Updated dependencies [1a15893]
2575
+ - Updated dependencies [b70e534]
2576
+ - Updated dependencies [7e05d8e]
2577
+ - Updated dependencies [8f1851e]
2578
+ - Updated dependencies [b4b2c7d]
2579
+ - Updated dependencies [fda61e4]
2580
+ - Updated dependencies [61ea810]
2581
+ - Updated dependencies [2233a85]
2582
+ - Updated dependencies [67452d1]
2583
+ - Updated dependencies [089767f]
2584
+ - Updated dependencies [a13827e]
2585
+ - Updated dependencies [66d99ec]
2586
+ - Updated dependencies [cb43296]
2587
+ - Updated dependencies [b61afc1]
2588
+ - Updated dependencies [79021fc]
2589
+ - Updated dependencies [7733604]
2590
+ - Updated dependencies [4921a95]
2591
+ - Updated dependencies [40e420f]
2592
+ - Updated dependencies [62dd69a]
2593
+ - Updated dependencies [d13004a]
2594
+ - Updated dependencies [be7360c]
2595
+ - Updated dependencies [e15e679]
2596
+ - Updated dependencies [2ab1257]
2597
+ - Updated dependencies [0fc6219]
2598
+ - Updated dependencies [061406d]
2599
+ - Updated dependencies [e4c8b6c]
2600
+ - Updated dependencies [acb10f6]
2601
+ - Updated dependencies [605e190]
2602
+ - Updated dependencies [c6c59f1]
2603
+ - Updated dependencies [b0e78a8]
2604
+ - Updated dependencies [f31cc8d]
2605
+ - Updated dependencies [f343dc4]
2606
+ - Updated dependencies [8269e32]
2607
+ - Updated dependencies [74f7339]
2608
+ - Updated dependencies [a6c35a2]
2609
+ - Updated dependencies [c2f1002]
2610
+ - Updated dependencies [4cc4fb7]
2611
+ - Updated dependencies [cc2de0e]
2612
+ - Updated dependencies [97b6658]
2613
+ - Updated dependencies [28d1eb7]
2614
+ - Updated dependencies [06770c0]
2615
+ - Updated dependencies [2c26040]
2616
+ - Updated dependencies [f758cec]
2617
+ - Updated dependencies [5b47ab5]
2618
+ - Updated dependencies [b09d8d9]
2619
+ - Updated dependencies [b09d8d9]
2620
+ - Updated dependencies [8675db6]
2621
+ - Updated dependencies [b09d8d9]
2622
+ - Updated dependencies [27358d5]
2623
+ - Updated dependencies [1c3da1f]
2624
+ - Updated dependencies [c1f344b]
2625
+ - Updated dependencies [db48ad5]
2626
+ - Updated dependencies [3eb1b2b]
2627
+ - Updated dependencies [9c93465]
2628
+ - Updated dependencies [a34fd2e]
2629
+ - Updated dependencies [ebb209c]
2630
+ - Updated dependencies [76bcb83]
2631
+ - Updated dependencies [59b85c0]
2632
+ - Updated dependencies [889ae47]
2633
+ - Updated dependencies [4f4c3fb]
2634
+ - Updated dependencies [78f0be8]
2635
+ - Updated dependencies [65f184b]
2636
+ - Updated dependencies [6e357ed]
2637
+ - Updated dependencies [d6938bf]
2638
+ - Updated dependencies [35f7fb4]
2639
+ - Updated dependencies [0410522]
2640
+ - Updated dependencies [63b33e6]
2641
+ - Updated dependencies [f163028]
2642
+ - Updated dependencies [814db6d]
2643
+ - Updated dependencies [a5302c7]
2644
+ - Updated dependencies [31e0be9]
2645
+ - Updated dependencies [4bfd455]
2646
+ - Updated dependencies [ffd2ce2]
2647
+ - Updated dependencies [2a44c1d]
2648
+ - Updated dependencies [7084313]
2649
+ - Updated dependencies [f07808c]
2650
+ - Updated dependencies [91cefb8]
2651
+ - Updated dependencies [7ffc3d3]
2652
+ - Updated dependencies [88346ba]
2653
+ - Updated dependencies [4631592]
2654
+ - Updated dependencies [62f8017]
2655
+ - Updated dependencies [32ff033]
2656
+ - Updated dependencies [a831df1]
2657
+ - Updated dependencies [f752ee3]
2658
+ - Updated dependencies [a1b61e0]
2659
+ - Updated dependencies [cd6b9f2]
2660
+ - Updated dependencies [2cb6d3c]
2661
+ - Updated dependencies [af2a095]
2662
+ - Updated dependencies [5ac93d4]
2663
+ - Updated dependencies [695cfbd]
2664
+ - Updated dependencies [0e043d8]
2665
+ - Updated dependencies [93f267f]
2666
+ - Updated dependencies [7445149]
2667
+ - Updated dependencies [ec796d5]
2668
+ - Updated dependencies [071d0dc]
2669
+ - Updated dependencies [0024abf]
2670
+ - Updated dependencies [8dd98bf]
2671
+ - Updated dependencies [e87fea1]
2672
+ - Updated dependencies [c65e529]
2673
+ - Updated dependencies [0848bea]
2674
+ - Updated dependencies [d51bed2]
2675
+ - Updated dependencies [dadd1ad]
2676
+ - Updated dependencies [acbf364]
2677
+ - Updated dependencies [3ca34c1]
2678
+ - Updated dependencies [7adc841]
2679
+ - Updated dependencies [239c3a3]
2680
+ - Updated dependencies [b8b3c64]
2681
+ - Updated dependencies [2f2e63c]
2682
+ - Updated dependencies [4845f85]
2683
+ - Updated dependencies [486d526]
2684
+ - Updated dependencies [94a0bbc]
2685
+ - Updated dependencies [bf1edef]
2686
+ - Updated dependencies [d6bfb3d]
2687
+ - Updated dependencies [8a9c079]
2688
+ - Updated dependencies [7b005b4]
2689
+ - Updated dependencies [cc3555e]
2690
+ - Updated dependencies [a2266a6]
2691
+ - Updated dependencies [d25a0ec]
2692
+ - Updated dependencies [89d7b35]
2693
+ - Updated dependencies [94f7b6a]
2694
+ - Updated dependencies [5c94f83]
2695
+ - Updated dependencies [ea936f3]
2696
+ - Updated dependencies [0c0fbd9]
2697
+ - Updated dependencies [667b83e]
2698
+ - Updated dependencies [f3141d8]
2699
+ - Updated dependencies [5487c20]
2700
+ - Updated dependencies [aa8b847]
2701
+ - Updated dependencies [7687f7b]
2702
+ - Updated dependencies [5a84d41]
2703
+ - Updated dependencies [fd3013a]
2704
+ - Updated dependencies [85ec26d]
2705
+ - Updated dependencies [73e576f]
2706
+ - Updated dependencies [f6476fc]
2707
+ - Updated dependencies [69ac82c]
2708
+ - Updated dependencies [4ac12ef]
2709
+ - Updated dependencies [833ed84]
2710
+ - Updated dependencies [a18abf3]
2711
+ - Updated dependencies [c6a4eeb]
2712
+ - Updated dependencies [1659072]
2713
+ - Updated dependencies [f450ae7]
2714
+ - Updated dependencies [abceb0d]
2715
+ - Updated dependencies [627b188]
2716
+ - Updated dependencies [8d4eae7]
2717
+ - Updated dependencies [c5a5996]
2718
+ - Updated dependencies [0c302a7]
2719
+ - Updated dependencies [b88f5e8]
2720
+ - Updated dependencies [857a6cf]
2721
+ - Updated dependencies [65a3a84]
2722
+ - Updated dependencies [6633337]
2723
+ - Updated dependencies [21676eb]
2724
+ - Updated dependencies [3f296bf]
2725
+ - Updated dependencies [e474853]
2726
+ - Updated dependencies [e9cb9ab]
2727
+ - Updated dependencies [42cc219]
2728
+ - Updated dependencies [d42a92f]
2729
+ - Updated dependencies [569611f]
2730
+ - Updated dependencies [51d74ad]
2731
+ - Updated dependencies [d7e0b42]
2732
+ - Updated dependencies [3510e4a]
2733
+ - Updated dependencies [d5749d7]
2734
+ - Updated dependencies [f00d8d4]
2735
+ - Updated dependencies [5326b36]
2736
+ - Updated dependencies [aa4b90d]
2737
+ - Updated dependencies [ccd9397]
2738
+ - Updated dependencies [503be86]
2739
+ - Updated dependencies [54299ca]
2740
+ - Updated dependencies [51a587d]
2741
+ - Updated dependencies [ae490ef]
2742
+ - Updated dependencies [e124711]
2743
+ - Updated dependencies [dc61def]
2744
+ - Updated dependencies [bca935b]
2745
+ - Updated dependencies [d92c72d]
2746
+ - Updated dependencies [c54c822]
2747
+ - Updated dependencies [8dcc0f5]
2748
+ - Updated dependencies [75b9e51]
2749
+ - Updated dependencies [f61c8cf]
2750
+ - Updated dependencies [e3ef52b]
2751
+ - Updated dependencies [0a2f233]
2752
+ - Updated dependencies [8621cdd]
2753
+ - Updated dependencies [251e888]
2754
+ - Updated dependencies [07f1822]
2755
+ - Updated dependencies [e336549]
2756
+ - Updated dependencies [3bb9340]
2757
+ - Updated dependencies [1e604c4]
2758
+ - Updated dependencies [04fab5e]
2759
+ - Updated dependencies [183b4c4]
2760
+ - Updated dependencies [7f713b6]
2761
+ - Updated dependencies [d40f43a]
2762
+ - Updated dependencies [2fdb36e]
2763
+ - Updated dependencies [e787608]
2764
+ - Updated dependencies [6f23667]
2765
+ - Updated dependencies [cde1975]
2766
+ - Updated dependencies [0bc685a]
2767
+ - Updated dependencies [20526f5]
2768
+ - Updated dependencies [efedd28]
2769
+ - Updated dependencies [5d21a48]
2770
+ - Updated dependencies [5278e11]
2771
+ - Updated dependencies [c5eef1d]
2772
+ - Updated dependencies [e5e7ee0]
2773
+ - Updated dependencies [23dba62]
2774
+ - Updated dependencies [e0f300b]
2775
+ - Updated dependencies [761a0ba]
2776
+ - Updated dependencies [c960170]
2777
+ - Updated dependencies [19365b7]
2778
+ - Updated dependencies [ba98e26]
2779
+ - Updated dependencies [b7ed26d]
2780
+ - Updated dependencies [a2ebea2]
2781
+ - Updated dependencies [800bdb0]
2782
+ - Updated dependencies [9d4dfc4]
2783
+ - Updated dependencies [1059965]
2784
+ - Updated dependencies [def5919]
2785
+ - Updated dependencies [ee264b2]
2786
+ - Updated dependencies [60b672e]
2787
+ - Updated dependencies [f104bab]
2788
+ - Updated dependencies [68dea0b]
2789
+ - Updated dependencies [6b441a8]
2790
+ - Updated dependencies [64f8cbe]
2791
+ - Updated dependencies [6cb81c7]
2792
+ - Updated dependencies [61282f9]
2793
+ - Updated dependencies [c073b8c]
2794
+ - Updated dependencies [ce0cfe9]
2795
+ - Updated dependencies [04f1182]
2796
+ - Updated dependencies [3a2dde7]
2797
+ - Updated dependencies [8c20f75]
2798
+ - Updated dependencies [be87153]
2799
+ - Updated dependencies [dd0f681]
2800
+ - Updated dependencies [60f0dd8]
2801
+ - Updated dependencies [a87c5cd]
2802
+ - Updated dependencies [a47f338]
2803
+ - Updated dependencies [b3a3d83]
2804
+ - Updated dependencies [7a55913]
2805
+ - Updated dependencies [078e28b]
2806
+ - Updated dependencies [35accbf]
2807
+ - Updated dependencies [6038de7]
2808
+ - Updated dependencies [fc5f536]
2809
+ - Updated dependencies [5647006]
2810
+ - Updated dependencies [e654bfd]
2811
+ - Updated dependencies [01a7337]
2812
+ - Updated dependencies [b45c71e]
2813
+ - Updated dependencies [d71ff32]
2814
+ - Updated dependencies [f8cfbb4]
2815
+ - Updated dependencies [6e6c872]
2816
+ - Updated dependencies [2598216]
2817
+ - Updated dependencies [11949fc]
2818
+ - Updated dependencies [2c7e62d]
2819
+ - Updated dependencies [eb95d97]
2820
+ - Updated dependencies [b098b0e]
2821
+ - Updated dependencies [4d00b13]
2822
+ - Updated dependencies [1363084]
2823
+ - Updated dependencies [fa5758e]
2824
+ - Updated dependencies [38f7e4f]
2825
+ - Updated dependencies [eb7613c]
2826
+ - Updated dependencies [c57f3cf]
2827
+ - Updated dependencies [ecc9110]
2828
+ - Updated dependencies [9aa5510]
2829
+ - Updated dependencies [e4c2dc8]
2830
+ - Updated dependencies [97faca3]
2831
+ - Updated dependencies [57bab76]
2832
+ - Updated dependencies [c89d18c]
2833
+ - Updated dependencies [1bd2795]
2834
+ - Updated dependencies [f7bd4e2]
2835
+ - Updated dependencies [694c350]
2836
+ - Updated dependencies [361bd5b]
2837
+ - Updated dependencies [aac90a5]
2838
+ - Updated dependencies [3da3da5]
2839
+ - Updated dependencies [1e6ab15]
2840
+ - Updated dependencies [b90086a]
2841
+ - Updated dependencies [129b378]
2842
+ - Updated dependencies [88f9d94]
2843
+ - Updated dependencies [8186a70]
2844
+ - Updated dependencies [a329cca]
2845
+ - Updated dependencies [c87ef70]
2846
+ - Updated dependencies [3cb0618]
2847
+ - Updated dependencies [32a0874]
2848
+ - Updated dependencies [6eec18c]
2849
+ - Updated dependencies [4d7bebf]
2850
+ - Updated dependencies [821ac7a]
2851
+ - Updated dependencies [8f81731]
2852
+ - Updated dependencies [7055c22]
2853
+ - Updated dependencies [785a748]
2854
+ - Updated dependencies [3af0354]
2855
+ - Updated dependencies [866ff16]
2856
+ - Updated dependencies [5a85e67]
2857
+ - Updated dependencies [4965bfa]
2858
+ - Updated dependencies [8b50cb3]
2859
+ - Updated dependencies [a0fdc56]
2860
+ - Updated dependencies [946a131]
2861
+ - Updated dependencies [b95577a]
2862
+ - Updated dependencies [0dcbc11]
2863
+ - Updated dependencies [d88f3e9]
2864
+ - Updated dependencies [ad5fe25]
2865
+ - Updated dependencies [c183a12]
2866
+ - Updated dependencies [83c161f]
2867
+ - Updated dependencies [d8c4957]
2868
+ - Updated dependencies [b9f930b]
2869
+ - Updated dependencies [f24cb83]
2870
+ - Updated dependencies [5dbbb92]
2871
+ - Updated dependencies [ea90179]
2872
+ - Updated dependencies [1818998]
2873
+ - Updated dependencies [ce92674]
2874
+ - Updated dependencies [5ef0b5b]
2875
+ - Updated dependencies [8c2db68]
2876
+ - Updated dependencies [22b5e54]
2877
+ - Updated dependencies [0166bd5]
2878
+ - Updated dependencies [3d4c545]
2879
+ - Updated dependencies [bb7cb41]
2880
+ - Updated dependencies [8064b07]
2881
+ - Updated dependencies [09ee21c]
2882
+ - Updated dependencies [4a56dbd]
2883
+ - Updated dependencies [289d04a]
2884
+ - Updated dependencies [f549a0d]
2885
+ - Updated dependencies [48fbacb]
2886
+ - Updated dependencies [06df4fa]
2887
+ - Updated dependencies [3fc2e48]
2888
+ - Updated dependencies [c9b809f]
2889
+ - Updated dependencies [e8f435c]
2890
+ - Updated dependencies [32386f8]
2891
+ - Updated dependencies [9b702dc]
2892
+ - Updated dependencies [ab16331]
2893
+ - Updated dependencies [41610f6]
2894
+ - Updated dependencies [69f1dfd]
2895
+ - Updated dependencies [bbe05de]
2896
+ - Updated dependencies [355e951]
2897
+ - Updated dependencies [a1dd1e4]
2898
+ - Updated dependencies [dadb43f]
2899
+ - Updated dependencies [3556b67]
2900
+ - @objectstack/spec@17.0.0
2901
+ - @objectstack/core@17.0.0
2902
+ - @objectstack/platform-objects@17.0.0
2903
+ - @objectstack/types@17.0.0
2904
+ - @objectstack/metadata-core@17.0.0
2905
+ - @objectstack/formula@17.0.0
2906
+
3
2907
  ## 17.0.0-rc.6
4
2908
 
5
2909
  ### Minor Changes