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