@objectstack/plugin-approvals 17.2.0 → 17.4.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,1149 @@
1
1
  # @objectstack/plugin-approvals
2
2
 
3
+ ## 17.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6530e04: A restored approval suspension can now be decided again, not only cancelled.
8
+
9
+ `AutomationEngine.restoreConsumedSuspension` re-arms the pause of a run that stranded mid-resume and tells the operator to *re-issue the continuation*. For an `approval` suspension nobody could: every approvals door that stamps the resume marker — `decide`, `recall`, `sendBack`, `resubmit` — guards on a `pending` request, and the row is terminal, written by the very call that stranded the run; and the generic engine door refuses an `approval` pause outright, because that node declares `resumeAuthority: 'service'`. The only remaining verb was `cancelRun`, which discards the branch's downstream work — so the advertised repair produced a run that looked resumable and was not decidable.
10
+
11
+ Measured against the real engine and the real decision door: the restored suspension lacks nothing. A `resumeAuthority`-marked resume walks the restored pause to completion. What was missing was an **issuer** on the approvals side, and that is what this adds.
12
+
13
+ - **`ApprovalService.continueRestoredRun(requestId, options?)`** re-issues the continuation the recorded outcome already produced once, against a pause an operator has re-armed. It reports which outcome it replayed, which edge it walked, and whether the signal was replayed exactly or rebuilt (`source: 'journal' | 'reconstructed'`).
14
+ - **The failing door now journals the signal it was carrying** on the repairable exit — the engine's own `status: 'stranded'` discriminator, the one exit that journals a repair snapshot — under `__strandedContinuation` in the request's `node_config_json`, beside the `__decisionOutputs` side-channel that was already there. Best-effort: it is awaited but can never replace the `RESUME_FAILED` throw the decision's caller is owed.
15
+ - **The continuation is tied to this request's own pause, by three guards.** A boolean "is this run suspended" is not enough: a run outlives any one request, so a terminal row's continuation could be issued against whatever pause the run happened to be sitting on. It now requires that the request is still the newest on its run, that a pause exists (strictly — an unreadable store throws rather than reading as "not suspended"), and that the pause is parked **where this request's recorded outcome was issued from**. That node is signal-aware, not simply the row's own: `approve`, `reject`, `revise` and `recall` are all issued at the request's own approval node, but a `resubmit` is only ever issued from the revise window the request's `revise` edge leads to, so its pause is re-armed there while the row still records the approval node. Comparing against the row's own node refused exactly that case, and told the operator the pause was not this request's when it was. The node check is fail-closed in every direction, including an engine that cannot report where a run is parked and a revise window this service cannot derive from the flow definition. This needs no new automation-engine surface: `listSuspendedRunsDurable` is already public, and the approvals-side resume interface simply declares it.
16
+ - **Runs stranded before this shipped are served too**, and where the signal cannot be proved the verb **refuses instead of guessing**. A status is not the same thing as a continuation, and three of the four terminal statuses have more than one writer or issuer: `approved` is unambiguous; `rejected` has two writers, discriminated by the `revise` action row that only ADR-0044's revision-limit auto-rejection leaves behind; `returned` has one writer but **two** issuers, discriminated by the `resubmit` action row whose sole writer is `resubmit` — without it a stranded resubmit was rebuilt as a send-back and walked the wrong edge, proceeding only through the engine's unmatched-label fallback with the wrong output; and `recalled` has two writers across **three** behaviours, two of which issue no continuation at all, so it is **refused on the rebuild path** with a message naming what an operator can do instead. Journal-recoverable is a **measured, named set** rather than a blanket claim: `approve`, `reject`, `resubmit` and `recall` continuations replay end to end through the verb, and `reject` and `resubmit` do so on the rebuild path as well. Two shapes are refused by design and stay refused — a `rejected` row that also carries a `revise` action, and a `recalled` row with no journal. NOT covered by a pin, and so not claimed: the `approve` rebuild path.
17
+
18
+ - **A journalled signal is checked against what the row's status can have issued, before it is replayed.** The journal records what the last FAILED resume was carrying, and nothing rewrites it when a later door moves the row on — so a signal can outlive the state that issued it. Measured, with no injected failure beyond the strand: a `resubmit` strands and journals `resubmit`; the submitter then recalls, a real `cancelRun` on an already-stranded run answers `false`, the row is marked `recalled` and the run stays parked; the restore re-arms the pause; and the stale `resubmit` was replayed, opening a fresh `pending` round on a request somebody deliberately withdrew. Every step an ordinary action answering ordinarily. A row is now replayable only for a continuation its own status can have issued — `approved`→`approve`, `rejected`→`reject`, `returned`→`revise` or `resubmit`, `recalled`→`recall`, and nothing at all for a status nobody has enumerated. ⛔ Clearing the journal after a successful replay does not close this and was measured not to: the offending replay is the FIRST replay of that journal, so a clear that fires afterwards can never run before the advance it would prevent.
19
+
20
+ ⛔ What this deliberately does not do, each pinned: it does not re-open or rewrite the request row — all four `pending` guards are untouched and no status, mirror field or audit row is written, so a decided request still cannot be decided again through the front door; it does not relax `resumeAuthority: 'service'`, since the resume still goes through the one call site that stamps the marker; and it does not change `ApprovalDecisionResult`, whose shape is the subject of an open ruling. It also grants no capability in-process code did not already have — `RESUME_AUTHORITY_SERVICE` is importable by any host — what it adds is the guarded form, and the guards are stated as what they actually check: that this request is still the newest on its run, that a pause exists at all, that it is parked where this outcome was issued from, and that the recorded signal is one the row's present status can have issued. ⛔ None of them checks that the pause was consumed and genuinely re-armed, and an earlier wording of this entry claimed one did: a `returned` row with a resubmit action row and a pause that was never consumed is admitted, with `restoreConsumedSuspension` itself answering *"already resumable — nothing to restore"*. That shape is benign — the recorded action is the submitter's own resubmit, so the step it walks was decided — but it is not what any guard tests. Like the engine verb it completes, it is an in-process operator repair: no REST route, and no entry in the spec `ApprovalService` contract.
21
+ - 4c31f02: The stranded-request inspection tells a repairable strand from a cascade-failed run — through a dedicated read-only engine member, not through the wire (#15358, ruling B′).
22
+
23
+ `ApprovalService.inspectStrandedRequests` keyed on `run.status === 'failed'`, which over-reports in one direction: a **cascade-failed** run — an ancestor `failAncestors` failed while it was parked at its `subflow` node, whose pause `failSuspendedRun` consumed and journalled nothing — has the same terminal `failed` row as the #13909 strand, so both came back `runState: 'failed'`, and `restoreConsumedSuspension` re-arms one and refuses the other (`NO_CONSUMED_SUSPENSION`). The engine's discriminator (the consumed-suspension snapshot on the durable `RunRecord`) is deliberately NOT on the `ExecutionLogEntry` that `getRun` answers, because `GET /automation/:name/runs/:runId` serves that object verbatim — so the plugin could not read it, and reading its absence as "not a strand" would have called the repairable row dead.
24
+
25
+ **`@objectstack/service-automation` — additive, `minor`.** `AutomationEngine.inspectConsumedSuspension(runId)` answers whether `restoreConsumedSuspension` would have a consumed suspension to put back, from the SAME two witnesses that verb reads (this process's hot journal and the durable row, reconciled by the same `rowSupersedesJournal` / `persisted` / drop-notice rules — the read is now one private method both call), and re-arms nothing. Four answers, none folded: `repairable: true` (with the pause it would re-arm and which witness answered); `SNAPSHOT_DROPPED` (the strand happened, the store could not persist the snapshot, and this process holds no hot copy — repairable only by the replica that stranded it, while it lives); `NO_CONSUMED_SUSPENSION` (cascade-failed or never paused); `RUN_SUSPENDED` (already resumable). It REJECTS when a store cannot be read — an outage is unknown, not "nothing to restore". The result type is exported as `ConsumedSuspensionInspection`. `restoreConsumedSuspension` behaves exactly as before; nothing on `ExecutionLogEntry`, the run-detail route, or `@objectstack/spec` changes.
26
+
27
+ **`@objectstack/plugin-approvals` — additive on two published types, `minor`.**
28
+
29
+ - `ApprovalResumeSurface` gains the optional `inspectConsumedSuspension?(runId)`, declared the way `listSuspendedRunsDurable` is: a method `AutomationEngine` already implements, widening no engine surface.
30
+ - `StrandedRunState` splits `'failed'` three ways and keeps `'missing'` untouched: `'repairable'` (the #13909 strand — restore, then `continueRestoredRun`), `'snapshot_dropped'` (its own class: as `repairable` it over-reports, as `unrepairable` it is a false negative), and `'unrepairable'` (the cascade-failed / never-paused run, #15222's shape — nothing re-arms it). **`'failed'` stays a member, on purpose**: it is what a `failed` row is reported as when the attached surface has no `inspectConsumedSuspension` (an engine build older than this plugin, or a host double). Absence of the discriminator is fail-closed for a report — the row is reported, undifferentiated, never labelled `unrepairable` and never dropped. A thrown read counts `undetermined`, as the other two oracles' do.
31
+
32
+ A consumer switching exhaustively over `StrandedRunState` gains three arms; nothing it matched before stops arriving. The inspection's summary log adds `runRepairable` / `runSnapshotDropped` / `runUnrepairable` beside the existing counts.
33
+ - 3d3f60e: An approval decision that lands while its flow run strands now says so in fields, not only in prose.
34
+
35
+ `POST /api/v1/approvals/requests/{id}/reject` — and its sibling decision doors — could produce three coexisting outcomes from one call: the caller read HTTP 500, the request row **was** in its terminal status and had left the pending inbox, and the workflow run was stranded. A caller reading 500 has one honest inference available — "the rejection did not happen" — and it was the wrong one, so scripts and operators retried or escalated against a decision that was already durable. The only carrier of the truth was English prose in `error`, so finding the affected run meant regexing a run id out of a sentence, and nothing said whether that run could be repaired at all.
36
+
37
+ The 500 stays. A recorded decision whose flow never advances is still a failure and is still reported as one; the door does not become atomic and no decision is ever rolled back. What changed is that it stops discarding what the engine already said:
38
+
39
+ - **The `RESUME_FAILED` body gains four fields**, additively — `finalized` (always `true`: the decision stands), `decision`, `runId`, and `repairable`. Existing consumers see the same `code`, the same `error` and the same status.
40
+ - **`repairable` carries the engine's own discriminator** — `AutomationResult.status === 'stranded'`, the state stamped on exactly the exit that journals a repair snapshot. `false` is the answer for every other failure, including a lost run: absence of the signal is not repairability, and a repair verb that would refuse is worse than no promise.
41
+ - **`serviceResume` carries `status`** through to the door. It previously read only `success` / `code` / `error`, and the stranded exit reports a `status` and no `code` at all — so the platform's own repairability signal died one line before the envelope was built.
42
+
43
+ `@objectstack/types` gains `strandedDecisionFailure` / `strandedDecisionDetails` and the `StrandedDecisionDetails` type — the constructor and its recogniser in one module, so the producing service and the REST door cannot drift. A `RESUME_FAILED` raised without that carrier answers exactly the body it always did; the door never synthesises the envelope.
44
+
45
+ ### Patch Changes
46
+
47
+ - ea03c7c: Fix: a `department` approver on a seeded business unit no longer routes the approval to another organization's members.
48
+
49
+ `ApprovalService.expandBusinessUnitUsers` screened the `sys_business_unit` rows with the null-inclusive tenant predicate (#3807 — a seeded unit carries no organization and is admitted on purpose) but read `sys_business_unit_member` with no organization predicate at all, under a system context that carries no tenant either. A seeded unit id exists identically in every tenant, so a `department:<id>` approver on tenant A's request resolved the shared unit and then collected every tenant's membership rows hanging off it — approval authority over A's record, routed to B's users. The member read now carries a strict `organization_id` equality against the directory organization the approver resolves in: the same screen `plugin-sharing` applies to these rows, and the same posture this package already takes for `sys_team_member` and `sys_user_position`.
50
+
51
+ The screen is strict rather than null-inclusive on purpose. `sys_business_unit_member.organization_id` is filled by REST/session writes but left NULL by seed replay and by elevated system-context writes (tracked in #14570), so a NULL on a membership row means unknown tenancy, not "platform-global", and routing fails closed on it. Declared cost: on a deployment whose membership rows (not merely its units) were seeded or system-written, a `department` approver on a request that carries an organization now expands to nobody — the slot falls to the `department:<id>` literal, the existing `expanded to nobody` warning (#3807) names it, and `onEmptyApprovers` governs the request as for any unstaffed target. The repair is to stamp those membership rows. A request that carries no organization is unchanged, and so is every unit-level screen.
52
+ - be92d46: These fourteen packages now declare `repository.directory`, so their npm pages carry a working "source" deep link to their own directory in the monorepo.
53
+
54
+ npm renders that field by concatenating it onto `repository.url`. None of these fourteen manifests carried a `repository` block at all, so every one of their npm pages offered no route from the package back to its code — not a broken link, no link. That is what this publishes: the block those pages read, naming each package's own directory.
55
+
56
+ Nothing else about these packages changes. No export, no runtime behaviour, no dependency and no file in the tarball other than the manifest's own `repository` key. The version bump exists because the fix is only real once it is published: the field lives in the manifest npm serves, so a corrected manifest sitting in the repository leaves the package page exactly as wrong as it was.
57
+
58
+ The rule behind it is now mechanical rather than remembered — `check:manifest-repository-directory` makes a publishable (non-private) workspace manifest declare the field naming its own directory, so a package added or moved after this cannot quietly go back to having no source link.
59
+ - ac6213e: Four server-side authorization sites stop deriving platform-operator authority from a NAME in `ExecutionContext.positions`, and read the ADR-0095 posture rung instead.
60
+
61
+ `positions[]` is the security axis, so it carries ADR-0057 D4 `sys_user_position` names alongside the built-ins. `sys_user_position` is `apiEnabled` and its `position` values are unconstrained, so a tenant could mint a row spelling `platform_admin` for one of their own users: `resolveUserAuthzGrants` pushed that name straight onto `grants.positions`, while `grants.posture` — derived from the unscoped `admin_full_access` grant and nothing else — correctly stayed `MEMBER`. Every reader of the name therefore answered `true` for a principal enforcement treats as an ordinary member. `resolve-authz-context.ts` states the rule at `hasPlatformAdminStanding` ("read the RUNG — never `positions.includes(...)`"), but a comment is not a gate and these four had not followed it.
62
+
63
+ Each site now tests `posture === 'PLATFORM_ADMIN'`, byte-for-byte what `hasPlatformAdminStanding` returns:
64
+
65
+ - **`plugin-sharing`** — `hasPlatformAuthority`. The minted row satisfied `assertResolvableAdminScope`, so an org-less caller holding only the ORG-scoped `manage_sharing` capability was answered with **every tenant's** sharing rules, and could delete platform-global rules. The `manage_platform_settings` capability spelling is unchanged.
66
+ - **`plugin-approvals`** — `isOverrideActor`. This predicate already read the rung and then ORed the name onto it, which is no protection: an OR is only as strong as its weakest arm. Because the platform arm deliberately crosses the tenant wall, the minted row let a member of one organization approve, reject or recall a **different organization's** pending request while holding no slot in its slate. The `ADMIN_FULL_ACCESS` capability arm and both TENANT_ADMIN arms are unchanged.
67
+ - **`runtime`** — the ADR-0126 §5 activation gate. Under a `group` or `isolated` posture this gate is the only thing between a tenant org admin and the **install-wide** `sys_metadata_activation` row, so the minted row reopened #10243 with a durable row behind it.
68
+ - **`plugin-security`** — `derivePosture` in the explain engine. Narrower than the other three, and stated precisely rather than overclaimed: the name-read sat behind an early `ctx.posture` return that `buildContextForUser` always populates, so the shipping path was already gated and a D4 row never moved it. What the read did reach was a posture-less hand-built context, where it made the panel **report** `PLATFORM_ADMIN` for a principal enforcement treats as a MEMBER — a misreport rather than an admission, but in the one tool an administrator opens to check exactly this.
69
+
70
+ No behaviour changes for a genuine platform operator: their resolved context carries the rung, and the built-in position is still projected onto `positions[]` for display and predicate use. What changes is that the name alone no longer answers the authorization question.
71
+
72
+ Graded `patch` on the surface it moves: no exported type, signature or contract changes, and no authorable metadata is added, removed or renamed. The only observable difference is that a principal who never held the capability grant stops being admitted — which is the defect, not a feature anyone could have depended on.
73
+ - 455d037: Documentation: `ApprovalService.recall`'s docblock summary line no longer claims the submitter is the only actor.
74
+
75
+ The block opened with "Withdraw a pending request (submitter only)" and then, three paragraphs down, stated the #3424 privileged override correctly — "The #3424 privileged override reaches a PENDING request only (#12775, maintainer ruling 2026-09-02)". Both cannot be true, and the code settles it in the paragraph's favour: `overrideAdmits` short-circuits the non-submitter guard on a `pending` request. A reader who finishes the block is not misled, but the summary line is the one an editor shows on hover and the one any single-line extraction takes.
76
+
77
+ The summary line now reads "Withdraw an undecided request." — status is the axis and the actor rules are left to the paragraphs that already state them correctly, the same structural move the `IApprovalService.recall` docstring makes on the spec side.
78
+
79
+ Prose only: no guard, no branch and no signature changed. It earns a changeset rather than `skip-changeset` because `@objectstack/plugin-approvals` publishes `dist/`, and this text ships inside the published `dist/index.d.ts` for `ApprovalService.recall`.
80
+ - 8c7cca1: `inspectStrandedRequests` no longer drops a row it could not differentiate, and no longer lets one misbehaving host abort the whole scan (#16709, items 2 and 3).
81
+
82
+ Both are the same mistake at two altitudes: the method exists to **enumerate** the terminal approval requests whose flow run cannot advance, so a failure to read the #15358 third oracle must never remove a row from the answer — and never remove the *other* rows either.
83
+
84
+ - **A thrown third read now leaves its row in `stranded`, as `'failed'`.** It used to be counted `undetermined` and skipped, exactly as a thrown `hasSuspendedRun` or `getRun` is. Those two are not the same question: a throw from either leaves it unknown *whether* the row is stranded at all, and a storage outage must not be published as a lost run. By the time the third oracle is asked, both have answered — no live pause, terminal `failed` — and it is asked only *which* of the three shapes the row is. A read that could not be made is therefore the textbook "could not differentiate", which is what `'failed'` already means (`StrandedRunState`, #15358 ruling item 1). Dropping the row let `stranded: []` read as "nothing stranded" while a row was in fact stuck, with a log line as its only trace; for a report, fail-closed means showing the row.
85
+ - **A host that violates `ApprovalResumeSurface` no longer aborts the scan.** `refineFailedRunState(verdict)` ran outside the `try` that wrapped the read, so an implementation resolving `undefined` where a verdict is declared threw a `TypeError` out of `inspectStrandedRequests` itself and the scan enumerated **nothing**. The refinement now runs inside that `try`; a malformed verdict costs its own row the differentiation, is counted `undetermined`, and costs every other row nothing.
86
+
87
+ ⛔ No new `StrandedRunState` member and no widened export: both cases map onto the existing undifferentiated `'failed'`. The `undetermined` counter is kept as telemetry and now **overlaps** `stranded` by design — a row can be both reported and counted — so neither number alone sizes the scan's blind spot.
88
+ - Updated dependencies [fe0d9a4]
89
+ - Updated dependencies [ecd2158]
90
+ - Updated dependencies [f2b5e46]
91
+ - Updated dependencies [2ed6be6]
92
+ - Updated dependencies [ed7243d]
93
+ - Updated dependencies [6ba0db4]
94
+ - Updated dependencies [625b0c3]
95
+ - Updated dependencies [233222e]
96
+ - Updated dependencies [07f40e5]
97
+ - Updated dependencies [ceb4877]
98
+ - Updated dependencies [e9fcd6b]
99
+ - Updated dependencies [90e7e6d]
100
+ - Updated dependencies [2bdabe6]
101
+ - Updated dependencies [ca326b5]
102
+ - Updated dependencies [8f404a5]
103
+ - Updated dependencies [159dbad]
104
+ - Updated dependencies [68437d4]
105
+ - Updated dependencies [abb140c]
106
+ - Updated dependencies [8333a6c]
107
+ - Updated dependencies [3e3ecb0]
108
+ - Updated dependencies [3030369]
109
+ - Updated dependencies [d5d8d50]
110
+ - Updated dependencies [e08892d]
111
+ - Updated dependencies [ae05f2e]
112
+ - Updated dependencies [b548e43]
113
+ - Updated dependencies [c463d03]
114
+ - Updated dependencies [64bd6a3]
115
+ - Updated dependencies [13c48c2]
116
+ - Updated dependencies [b0529e1]
117
+ - Updated dependencies [66dc6ab]
118
+ - Updated dependencies [6f94458]
119
+ - Updated dependencies [6e67b86]
120
+ - Updated dependencies [132742f]
121
+ - Updated dependencies [85a2459]
122
+ - Updated dependencies [50dc214]
123
+ - Updated dependencies [e89fa92]
124
+ - Updated dependencies [e9fcd6b]
125
+ - Updated dependencies [8976ea1]
126
+ - Updated dependencies [56fe8c2]
127
+ - Updated dependencies [acabd24]
128
+ - Updated dependencies [ab50c8f]
129
+ - Updated dependencies [6491463]
130
+ - Updated dependencies [89cf4d6]
131
+ - Updated dependencies [21c5dcb]
132
+ - Updated dependencies [6d4d5d3]
133
+ - Updated dependencies [ed5d557]
134
+ - Updated dependencies [bca21f7]
135
+ - Updated dependencies [e9fcd6b]
136
+ - Updated dependencies [2025b1f]
137
+ - Updated dependencies [1a7a7c9]
138
+ - Updated dependencies [e9fcd6b]
139
+ - Updated dependencies [cbca47d]
140
+ - Updated dependencies [acf4d38]
141
+ - Updated dependencies [ef3a138]
142
+ - Updated dependencies [68d5dfd]
143
+ - Updated dependencies [3e21cf0]
144
+ - Updated dependencies [4cfc93b]
145
+ - Updated dependencies [098cbb7]
146
+ - Updated dependencies [efd6b43]
147
+ - Updated dependencies [859ded3]
148
+ - Updated dependencies [fa125f3]
149
+ - Updated dependencies [74628d9]
150
+ - Updated dependencies [a646120]
151
+ - Updated dependencies [6f1ce7d]
152
+ - Updated dependencies [7778115]
153
+ - Updated dependencies [86c75f4]
154
+ - Updated dependencies [2c753fe]
155
+ - Updated dependencies [52804cd]
156
+ - Updated dependencies [3f89967]
157
+ - Updated dependencies [53cf263]
158
+ - Updated dependencies [21aabbc]
159
+ - Updated dependencies [9c270bb]
160
+ - Updated dependencies [76c8c5a]
161
+ - Updated dependencies [088f761]
162
+ - Updated dependencies [a84e1ce]
163
+ - Updated dependencies [bf1054a]
164
+ - Updated dependencies [d8d2776]
165
+ - Updated dependencies [222dc0f]
166
+ - Updated dependencies [e9fcd6b]
167
+ - Updated dependencies [32c917d]
168
+ - Updated dependencies [f9a3c32]
169
+ - Updated dependencies [f502898]
170
+ - Updated dependencies [51ae731]
171
+ - Updated dependencies [af7edfe]
172
+ - Updated dependencies [b60f48b]
173
+ - Updated dependencies [c78c918]
174
+ - Updated dependencies [4ca358d]
175
+ - Updated dependencies [cf9bda4]
176
+ - Updated dependencies [784cb92]
177
+ - Updated dependencies [7629f4d]
178
+ - Updated dependencies [51df9fd]
179
+ - Updated dependencies [a7da4de]
180
+ - Updated dependencies [de0bcdd]
181
+ - Updated dependencies [70f7d6d]
182
+ - Updated dependencies [c677cda]
183
+ - Updated dependencies [6acb37e]
184
+ - Updated dependencies [7797102]
185
+ - Updated dependencies [554a160]
186
+ - Updated dependencies [f7da71e]
187
+ - Updated dependencies [7f745c3]
188
+ - Updated dependencies [0a038cc]
189
+ - Updated dependencies [e9fcd6b]
190
+ - Updated dependencies [97adce2]
191
+ - Updated dependencies [a83482c]
192
+ - Updated dependencies [5eb24f8]
193
+ - Updated dependencies [2a3decc]
194
+ - Updated dependencies [cc00df2]
195
+ - Updated dependencies [cc00df2]
196
+ - Updated dependencies [f4e6adf]
197
+ - Updated dependencies [ee4a59b]
198
+ - Updated dependencies [4db3c61]
199
+ - Updated dependencies [5ca314a]
200
+ - Updated dependencies [e0af1a8]
201
+ - Updated dependencies [4771bd9]
202
+ - Updated dependencies [414c1fc]
203
+ - Updated dependencies [22c0279]
204
+ - Updated dependencies [c930f85]
205
+ - Updated dependencies [0db2947]
206
+ - Updated dependencies [92b5d7f]
207
+ - Updated dependencies [613bfbd]
208
+ - Updated dependencies [abae16a]
209
+ - Updated dependencies [094b8fd]
210
+ - Updated dependencies [c7aca0d]
211
+ - Updated dependencies [c1d8f98]
212
+ - Updated dependencies [8e0b297]
213
+ - Updated dependencies [d4f9b2a]
214
+ - Updated dependencies [5f7fa1d]
215
+ - Updated dependencies [87f0ccc]
216
+ - Updated dependencies [aedbaef]
217
+ - Updated dependencies [a727043]
218
+ - Updated dependencies [c5d6803]
219
+ - Updated dependencies [10d05bb]
220
+ - Updated dependencies [69602e5]
221
+ - Updated dependencies [c3ce76c]
222
+ - Updated dependencies [7936b29]
223
+ - Updated dependencies [46803fa]
224
+ - Updated dependencies [c2a336c]
225
+ - Updated dependencies [9f890d3]
226
+ - Updated dependencies [0bb2318]
227
+ - Updated dependencies [f7db8f4]
228
+ - Updated dependencies [1ecee3e]
229
+ - Updated dependencies [9408b7f]
230
+ - Updated dependencies [2bb0614]
231
+ - Updated dependencies [b3820c3]
232
+ - Updated dependencies [e9fcd6b]
233
+ - Updated dependencies [4df2a98]
234
+ - Updated dependencies [9bcd9be]
235
+ - Updated dependencies [b398ad2]
236
+ - Updated dependencies [99261a7]
237
+ - Updated dependencies [81b426f]
238
+ - Updated dependencies [001af1c]
239
+ - Updated dependencies [fb77aa5]
240
+ - Updated dependencies [3d3f60e]
241
+ - Updated dependencies [581d8f8]
242
+ - Updated dependencies [f81afe3]
243
+ - Updated dependencies [40a44b9]
244
+ - Updated dependencies [f89812e]
245
+ - Updated dependencies [7a7fb03]
246
+ - Updated dependencies [8fd246d]
247
+ - Updated dependencies [021a735]
248
+ - Updated dependencies [7bdb163]
249
+ - @objectstack/spec@17.4.0
250
+ - @objectstack/core@17.4.0
251
+ - @objectstack/platform-objects@17.4.0
252
+ - @objectstack/formula@17.4.0
253
+ - @objectstack/types@17.4.0
254
+ - @objectstack/metadata-core@17.4.0
255
+
256
+ ## 17.3.0
257
+
258
+ ### Minor Changes
259
+
260
+ - dda969c: fix(approvals): a deleted record's pending approvals auto-cancel instead of stranding in the inbox (#13568)
261
+
262
+ Deleting a record left every `pending` approval it had opened sitting in the
263
+ approvers' inbox — counted in the pending total, openable, and pointing at a
264
+ `record_id` that resolves to nothing. Nothing about it was module-specific:
265
+ an approval node that declares `lockRecord` blocks the EDIT, so "delete and
266
+ recreate" is the only route left to an author who needs to fix a submitted
267
+ record, and every such delete added another orphan. Maintainer ruling
268
+ 2026-08-31 (`总监席第 5 场决裁批 #5`, verbatim 「同意」): pending requests
269
+ auto-cancel on record delete — status `cancelled` plus a machine-readable
270
+ reason, rows KEPT for audit, out of the pending count and the inbox's default
271
+ view.
272
+
273
+ **Graded `minor`, and deliberately not `patch`.** The repair itself is a
274
+ defect fix, but it lands by WIDENING two published vocabularies and adding a
275
+ declared column, and this repo's convention grades a shipped service's
276
+ accept-set/behaviour move as `minor`. **No `BREAKING` banner**: nothing is
277
+ narrowed and no metadata that used to be accepted is now refused — the one
278
+ consequence a consumer can feel is that `ApprovalStatus` and
279
+ `ApprovalActionKind` each gained a member, so an exhaustive `switch` with no
280
+ default, or a `satisfies Record<ApprovalStatus, …>` map outside this repo,
281
+ now has a case to add. That is the same shape `returned` had when ADR-0044
282
+ landed it.
283
+
284
+ **Spec (`@objectstack/spec/contracts/approval-service`)**
285
+
286
+ - `APPROVAL_STATUSES` gains `cancelled` (+ its `APPROVAL_STATUS_LABELS`
287
+ entry). Its own terminal state rather than a re-use of `recalled`: a recall
288
+ is an ACT by the submitter, and filing a platform-initiated void as one
289
+ attributes a withdrawal to a person who never performed it.
290
+ - New `APPROVAL_CANCEL_REASONS` / `ApprovalCancelReason` /
291
+ `APPROVAL_CANCEL_REASON_LABELS`, single entry `record_deleted`. A
292
+ VOCABULARY, not free text, because the reason has a non-human consumer (the
293
+ inbox and the tombstone presentation branch on it) — and a CLASS, per the
294
+ ruling's wording, so the next platform-initiated cancellation cause extends
295
+ this list instead of minting a second terminal status for itself.
296
+ - `APPROVAL_ACTION_KINDS` gains `cancel` — the only kind with no human actor,
297
+ by construction.
298
+ - `ApprovalRequestRow.cancel_reason` declared, optional-nullable.
299
+
300
+ **Plugin (`@objectstack/plugin-approvals`)**
301
+
302
+ - `sys_approval_request.cancel_reason`, a select derived from the contract
303
+ vocabulary and never re-typed (the #3786 rule the `status` column already
304
+ follows). On the row rather than on the audit entry, so a plain list view
305
+ can read WHY without joining the append-only action log.
306
+ - `bindRecordDeleteCancelHook` — a GLOBAL `afterDelete` registration beside
307
+ the existing global record-lock hook, so one platform-level linkage covers
308
+ every "approval + `lockRecord`" object at once. It needs no row-set
309
+ plumbing: the engine binds the deleted row's pre-image on the by-id path
310
+ and fans `afterDelete` out per matched row on a predicate delete, so a bulk
311
+ delete is covered by the same handler. The approvals tables are excluded at
312
+ registration, so they do not pay the delete-side pre-image read.
313
+ - `ApprovalService.cancelForDeletedRecord` writes the transition: one
314
+ append-only `sys_approval_action` row (`action: 'cancel'`, no actor),
315
+ `status: 'cancelled'` + `cancel_reason: 'record_deleted'` +
316
+ `completed_at`, and a `sys_approval_approver` index clear — that last one
317
+ is not optional garnish, it is what actually empties the inbox, because the
318
+ approver filter resolves through that index rather than through `status`.
319
+ - The `Completed` list view now includes `cancelled`, so a kept audit row is
320
+ visible in the one curated terminal view rather than only under `All`.
321
+ - ⛔ **No flow resume and no status mirror-back.** A cancellation is a status
322
+ write plus a reason, not a decision, so there is no branch to resume down.
323
+ The mirror-back is skipped by construction rather than by a swallowed
324
+ error: it is an `update_record` against the row that was just deleted — the
325
+ exact write this card's forensics caught failing elsewhere. The suspended
326
+ run the request gated is reported at `warn` with its id and otherwise left
327
+ alone; what becomes of it belongs to the automation service.
328
+ - ⛔ **The delete is never refused.** The "forbid delete while an approval is
329
+ pending" direction was vetoed in the same ruling — `lockRecord` already
330
+ blocks the edit, and blocking the delete too locks an author onto a record
331
+ they cannot fix. Nothing in the hook throws; a failure degrades to the
332
+ pre-existing state (the stale row) and is logged.
333
+ - Terminal rows are untouched. `approved` / `rejected` / `recalled` /
334
+ `returned` requests about the deleted record keep their recorded outcome —
335
+ history stays history, and rendering their now-dead record reference is a
336
+ separate console-side change.
337
+
338
+ zh-CN / ja-JP / es-ES bundles carry authored translations for the new leaves
339
+ (已作废 / 無効化済み / Anulada), not source fills.
340
+ - 277948f: feat(spec,plugin-approvals): `escalation.enabled` defaults to `true` and the SLA sweep finally reads it (#12278)
341
+
342
+ **BREAKING** semantic default flip on a published authorable key, shipped as
343
+ `minor` under the repo's launch-window convention for breaking changes.
344
+ Maintainer ruling 2026-08-27 (Option C), explicitly reversing the 2026-08-26
345
+ "spec stays as declared" ruling with fresh analysis.
346
+
347
+ `ApprovalEscalationSchema.enabled` declared `default(false)` while the
348
+ plugin-approvals escalation sweep never read the key: any escalation block
349
+ with a positive `timeoutHours` escalated, and with `action: 'auto_approve'`
350
+ that silently approved requests their author had declared off the clock —
351
+ the ADR-0049 declared-but-unenforced shape, failing open. Worse, the
352
+ approval-node executor parses node config through the schema before
353
+ snapshotting it onto the request row, so the old default **materialized**
354
+ `enabled: false` into storage for every author who omitted the key, making
355
+ "authored off" and "defaulted off" byte-identical at the sweep site.
356
+
357
+ One change, both halves:
358
+
359
+ - **spec**: `enabled` now defaults to `true` (stays `z.boolean()`; no
360
+ tri-state). The feature-level switch is whether an `escalation` block
361
+ exists at all; within a block carrying `timeoutHours`, escalation is on
362
+ unless explicitly turned off — which is what the runtime, its eleven
363
+ behaviour tests, and every teaching surface have always meant. Declared in
364
+ `DEFAULT_CHANGES_BY_MAJOR` (17) and registered as the
365
+ `approval-escalation-enabled-default-flip` semantic migration entry.
366
+ - **runtime**: `runEscalations` skips a request whose snapshot carries an
367
+ explicit `escalation.enabled === false` — the declared switch is enforced.
368
+ Request snapshots created **before** the flip cutoff
369
+ (`ESCALATION_ENABLED_FLIP_CUTOFF_MS`, 2026-08-28T00:00:00Z) ride a
370
+ read-side legacy window and keep escalating exactly as they do today: their
371
+ stored `false` is overwhelmingly the old schema default materialized onto an
372
+ author who never wrote the key, every such stored row is escalating today,
373
+ and the window retires itself as pending requests drain — zero tenant rows
374
+ rewritten.
375
+
376
+ Deployed metadata that omits `enabled` does not change behaviour (it
377
+ escalated before, it escalates after). What changes is that writing
378
+ `enabled: false` finally binds for newly opened requests — the declared
379
+ intent being honoured. To keep an SLA off, write `enabled: false`; to
380
+ escalate on timeout, an `escalation` block with `timeoutHours` is enough.
381
+
382
+ <!-- adr-0087: registered approval-escalation-enabled-default-flip -->
383
+ - 3690922: fix(plugin-approvals): the non-submitter recall refusal renders through the
384
+ Operation Message Catalog instead of a hardcoded English sentence (#11993, the
385
+ services-side half of the shape-A ruling)
386
+
387
+ A user who opened a record someone else had submitted for approval, clicked
388
+ Recall and was correctly refused read the reason in English regardless of their
389
+ own locale. `@objectstack/rest`'s `handleApprovalError` ships this service's
390
+ thrown reason as the 403 body's human-readable `error`, and Console splices it
391
+ under its own localized label — so an operator in a fully Chinese deployment
392
+ read a Chinese prefix glued onto an English sentence they could not act on
393
+ (`撤回审批失败: <English>`).
394
+
395
+ The refusal now renders through the shared Operation Message Catalog in
396
+ `@objectstack/spec/system` under the key `approval_recall_not_submitter` that
397
+ #12493 landed for it — the same mechanism `plugin-security`'s denial gates
398
+ already use, with the same resolution ladder (deployment override → the
399
+ caller's locale → `en` → the key) and the same guarantee that a misbehaving
400
+ i18n service cannot turn a 403 into a 500. All four platform locales (`en`,
401
+ `zh-CN`, `ja-JP`, `es-ES`) ship copy that names who *can* recall, rather than
402
+ dead-ending the reader.
403
+
404
+ `ApprovalServiceOptions` gains an optional `messageTranslator` — a lazily
405
+ resolved, `II18nService.t`-compatible lookup, wired by `ApprovalsServicePlugin`
406
+ the same way `tenancyPosture` and the field-visibility source are, because the
407
+ i18n service is contributed by another plugin and may start later. It is what
408
+ makes the override address the catalog documents,
409
+ `errors.approval_recall_not_submitter`, actually take effect for this emitter;
410
+ a stack without an i18n service still renders the built-in catalog in the
411
+ caller's locale.
412
+
413
+ **Not changed: who may recall an approval.** The gate is byte-identical — the
414
+ submitter, or a privileged admin releasing a stuck record (#3424). Only the
415
+ sentence the refusal carries is different, and the `FORBIDDEN:` code prefix
416
+ that the REST layer maps to 403 is untouched.
417
+
418
+ The button-visibility half of #11993 — a non-submitter seeing a live recall
419
+ button at all — is not addressed here; see the issue for the measurement.
420
+ - 599515d: Four more system objects declare their polymorphic pointer pair (#11386, ADR-0052 §5, adopting the carrier #11339 landed): `sys_audit_log.record_id`, `sys_approval_request.record_id`, `sys_record_share.record_id` and `sys_share_link.record_id` now carry `referenceVia: 'object_name'`. A seed row addressing one of these by the target's natural key resolves against the object its sibling column names, per row — so a packaged app can ship audit history, pending approvals, record grants and share links that actually attach to the records they are about, and the queries that give each row its meaning (the `{object_name, record_id}` index, the pending-request lock, the sharing middleware's grant lookup, the share link's fail-closed record-existence gate) match on the target's real id.
421
+
422
+ The accept/reject contract changes with it on those four objects, deliberately and in the already-ruled direction: an unresolvable pointer on a DECLARED pair is a loud, counted failure instead of the old silent verbatim store. On a grant table that is the sharper win — a share whose `record_id` stayed a natural key enforced nothing while displaying as a grant, and was then deleted by the orphan sweep for describing a record that does not exist. Internal-id-shaped values still pass through verbatim, so a demo row about an already-deleted record (an `action: 'delete'` audit row) stays authorable. Undeclared text columns are untouched.
423
+
424
+ The fifth object surveyed, `sys_automation_run` (`trigger_object` / `trigger_record_id`), deliberately STAYS UNDECLARED. Its pair has the same shape but its rows are not content about a record: a `paused` row is a live continuation the engine rehydrates on boot, terminal rows are telemetry under a 30-day sweep, and the object has no natural key to address rows by. The verdict, its reasons, and what would have to change to flip it are recorded on the field itself and pinned by a test.
425
+
426
+ ### Patch Changes
427
+
428
+ - 1007134: fix(approvals): the #3424 override reaches a `pending` request only — override-recall of a `returned` request is now refused (#12775)
429
+
430
+ **Behaviour change, declared explicitly (maintainer ruling 2026-09-02) so that
431
+ one revert of this changeset's PR restores the previous behaviour.**
432
+
433
+ `ApprovalService.recall` admits two callers: the submitter, and a platform or
434
+ tenant admin releasing a stuck request (the #3424 privileged override). Recall
435
+ is also valid on the LATEST `returned` request of a run — the ADR-0044 revise
436
+ window, where the submitter abandons the revision instead of resubmitting.
437
+ Those two rules met above the state check: the override short-circuit carried
438
+ no status test of its own, so an override actor could recall a `returned`
439
+ request too. Nothing else on the platform said so — `isOverrideActor`'s doc
440
+ block names a PENDING request, `attachViewers` computes
441
+ `viewer.can_override` as `status === 'pending' && isOverrideActor(...)`, and
442
+ the `approval_recall` action's override arm reads that flag — so the reach was
443
+ API-only, never offered by any UI, and pinned by nothing.
444
+
445
+ What changes:
446
+
447
+ - **Override-recall of a `returned` request is refused.** The override
448
+ short-circuit in `recall` now applies only while the request is `pending`,
449
+ spelled exactly as the viewer flag is computed. On `returned` an override
450
+ actor is judged exactly as any other non-submitter and receives the existing
451
+ refusal: `403 FORBIDDEN` over REST, with the operation catalog's
452
+ `approval_recall_not_submitter` sentence. No new error code, no new envelope.
453
+ - **Only `pending` requests are override-recallable** — the same scope as the
454
+ other three override levers (approve / reject / reassign are pending-only at
455
+ their endpoints), and the same scope the viewer flag has always declared.
456
+
457
+ What does not change:
458
+
459
+ - The submitter's own recall of a `returned` request (the ADR-0044 revise
460
+ window) is untouched; so is the submitter's recall of a `pending` one.
461
+ - The override actor's recall of a `pending` request — the #3424 rescue of a
462
+ request routed to an unstaffed position — is untouched, lock release and all.
463
+ - The refusal's message, wire code and developer log line keep their shape; the
464
+ log line now also names the request status it refused on.
465
+
466
+ Why: the gate now agrees with the viewer flag and the documented contract at
467
+ one point instead of disagreeing with both. If a real operator workflow
468
+ depended on override-recalling a `returned` request, this is the change to
469
+ revert; the `returned` record lock is already released, so the stuck-record
470
+ rescue motive that justifies the override does not apply on that status.
471
+ - b579b03: fix(approvals): refuse loudly when a successful mutation's read-back is org-filtered out (#12769)
472
+
473
+ Ten ApprovalService result sites (decide/decideNode, recall, sendBack, resubmit,
474
+ reassign, remind, requestInfo, comment) read the row they just mutated back
475
+ through the caller's organization narrowing and asserted the result non-null
476
+ (`fresh!`). For an org-less request row — produced by construction on every
477
+ schedule / time-relative / api trigger run (#10131; pinned rather than repaired
478
+ by #9132) — an org-scoped caller's read-back matches nothing, so a call that
479
+ SUCCEEDED shipped a well-formed success envelope whose declared-non-null
480
+ `request` was `null` (HTTP 200 with `"request": null` through the REST
481
+ pass-through), and a client dereferencing `request.status` crashed.
482
+
483
+ The read-back now throws `READ_BACK_FAILED: …` instead: the write is recorded
484
+ and NOT rolled back; only the result echo is refused, loudly. Over REST the
485
+ error surfaces through each route's existing 500 arm (`APPROVAL_RECALL_FAILED`
486
+ and siblings — already-registered codes) with the `READ_BACK_FAILED:` message
487
+ in the body. No declared result type changed — the declared-non-null `request`
488
+ is now always true because a result that cannot be built is never returned. The
489
+ caller-org narrowing in `loadRequest` (tenancy wall) is deliberately untouched.
490
+ - 2a05851: fix(plugin-approvals): the `approval_recall` action shows for the #3424 admin override (#12716)
491
+
492
+ `ApprovalService.recall` has admitted two callers since #3424 — the submitter,
493
+ and a platform/tenant admin releasing a stuck request — and `isOverrideActor`'s
494
+ own doc block names recall as one of the four override levers in so many words.
495
+ The declared action that reaches that endpoint did not agree: `approval_recall`'s
496
+ `visible` predicate was submitter-only, while its three siblings
497
+ (`approval_approve` / `approval_reject` / `approval_reassign`) each OR in
498
+ `record.viewer.can_override`.
499
+
500
+ So recall was the one lever the override covers whose button never appeared. An
501
+ admin rescuing an approval routed to an unstaffed position could approve or
502
+ reject their way out — writing a decision nobody made — or reassign it, but
503
+ could not simply withdraw it. This is declared-vs-enforced drift in the less
504
+ usual direction: a capability the server grants that no UI entry exposed.
505
+
506
+ `approval_recall`'s `visible` now ORs in `record.viewer.can_override`, spelled
507
+ byte-identically to the three siblings.
508
+
509
+ Not a permission change: the service's authorisation set is untouched, and
510
+ `can_override` was already computed server-side for every viewer.
511
+
512
+ **Pending-only, and enforced rather than asserted.** The new arm carries no
513
+ status test of its own — neither do the siblings — because the flag is already
514
+ status-scoped where it is computed: `attachViewers` sets
515
+ `can_override: row.status === 'pending' && isOverrideActor(...)`, ANDed, so the
516
+ flag can never be true off `pending` and the arm is pending-only in effect
517
+ however CEL groups the expression. The submitter's own `returned` (revise
518
+ window) arm is unchanged. Pinned in both directions, with the flag's own scoping
519
+ pinned against the real service on a genuinely `returned` row.
520
+ - 9bc403e: Correct what `sys_approval_request.payload_json` is documented to be FOR — it is audit evidence served redacted per reader, not a notification source
521
+
522
+ The object's module docstring — which ships to consumers in the package's type
523
+ declarations — justified the snapshot column with: *"used by notifications so
524
+ they can render before the record is locked or changed."* That consumer does
525
+ not exist. Measured against every `this.notify(...)` call site in
526
+ `approval-service.ts`, all **12** of them, each passes a payload of
527
+ `{ title, message, actionUrl }` (two also carry `actions`), built from
528
+ `object_name` / `record_id` and the caller's own comment. **None** reads
529
+ `payload_json` or the parsed `payload`.
530
+
531
+ This is more than tidiness: that sentence was the only documented
532
+ justification for the column holding a *full* row, and it was cited as such
533
+ during the #10749 consumer inventory before anyone checked it. The docstring
534
+ now states the real reason — the snapshot is retained as **audit evidence of
535
+ what was actually submitted**, so the column stays whole at rest, and is served
536
+ **redacted per reader** by the subject object's field-level read controls via
537
+ `getReadableFields`, on the approvals-inbox door and the generic data door
538
+ alike (#11039).
539
+
540
+ The field's own `description` is deliberately unchanged: `Record snapshot at
541
+ submission time` is accurate, and it — unlike the JSDoc — is the string
542
+ extracted into the four generated i18n bundles, so no translation leaf moves
543
+ and no locale is left holding an English seed.
544
+
545
+ Also carried in the same pass, the residual documentation the #10749 closure
546
+ assigned to the next docs touch in this lane: `payload-redaction.ts` recorded
547
+ `hidden`-vs-serialization as an **open** `packages/spec` question, and it has
548
+ since been ruled (maintainer, 2026-08-24, applying the 2026-08-12 lineage).
549
+ That paragraph now states the ruling — **`hidden: true` stays UI-only;
550
+ `internal: true` is the serialization primitive** — so an author who needs a
551
+ field kept out of read results is pointed at `internal: true` (#7728,
552
+ ADR-0049) rather than at `hidden`, which never governed serialization.
553
+
554
+ Documentation only: no runtime behaviour, no schema field, and no public type
555
+ signature changes.
556
+ - 59c0891: fix(plugin-approvals): the stranded-request inspection now sees a run that FAILED mid-resume (#13909)
557
+
558
+ `ApprovalService.inspectStrandedRequests` was structurally blind to the shape
559
+ #13909 owns, and reported `0` for it — the one shape an operator most needs to
560
+ see.
561
+
562
+ **The mechanism.** `AutomationEngine.resumeInternal` consumes the suspension
563
+ *before* running the downstream nodes: `forgetSuspendedRun(run, 'resumed')`
564
+ precedes `traverseNext`. A downstream node that merely THREW therefore threw
565
+ with the pause already gone, the catch arm recorded the run `failed`, and
566
+ nothing can resume it again (`resume` answers `RUN_NOT_FOUND`, `cancelRun` is a
567
+ no-op). The decision is durable and the flow stopped half-way.
568
+
569
+ **Why the inspection could not see it.** Its second oracle was
570
+ `if (terminal) continue` — the existence of ANY run-history row ended the check,
571
+ on the reading "the run ran to a terminal state, it is not dangling". But the
572
+ terminal row here is written BY the failure that stranded the request, so the
573
+ evidence of the defect was read as evidence of health. `releaseDeadRunRequests`
574
+ cannot see it either: it scans `status: 'pending'`, and the decision is what
575
+ took the row out of `pending`.
576
+
577
+ **The widening, and its limits.** The second oracle now classifies the run
578
+ instead of merely detecting it. A `failed` run is reported; `completed`,
579
+ `cancelled` and `paused` are each still skipped, one named reason at a time, and
580
+ a status this code does not recognise is skipped too — the spec's
581
+ `ExecutionStatus` vocabulary is wider than the four statuses the engine writes,
582
+ and a future status must not become a silent false positive. `paused` in
583
+ particular stays skipped because "the suspension is gone but no terminal row is
584
+ written yet" is exactly what a resume IN FLIGHT looks like. The first oracle is
585
+ unchanged: a run the suspension store still holds is alive, and an unreadable
586
+ store is still counted `undetermined`, never condemned.
587
+
588
+ Reported rows now carry `runState: 'missing' | 'failed'` (new exported type
589
+ `StrandedRunState`), because the two shapes need different remedies: a `missing`
590
+ run has no history to read, a `failed` one has a step log and an error naming
591
+ the node that threw. The sweep's own warning splits its counts the same way.
592
+ `StrandedApprovalRequest` is an output-only reporting shape the service
593
+ produces; the added field is not constructed by any caller in this repo.
594
+
595
+ **Still read-only, and still not a census.** No status is changed and no run is
596
+ cancelled — the decision genuinely happened. This makes the condition *visible*
597
+ in a deployment; how many runs are already in it can only be answered against
598
+ that deployment's own tables. Nothing here changes the resume ordering, which
599
+ is #13909's own next slice.
600
+ - 4bb09e8: feat(service-automation): an operator can put back a suspension a failed resume consumed (#13909)
601
+
602
+ A run that was resumed and whose downstream node merely **threw** was
603
+ terminally unresumable, and nothing anywhere could move it. The engine consumes
604
+ the suspension *before* running downstream nodes, so such a node throws with
605
+ the pause already gone and the catch arm records the run `failed`: `resume`
606
+ then answers `RUN_NOT_FOUND`, `cancelRun` is a no-op, and none of the engine's
607
+ other public methods takes the run anywhere. A deployment could enter that
608
+ state and never leave it.
609
+
610
+ `AutomationEngine.restoreConsumedSuspension(runId, { requestedBy, reason })`
611
+ is the exit. It puts the consumed suspension back — verbatim, as it stood at
612
+ the pause — so the run is resumable again through an ordinary `resume`, with
613
+ the same authority gate, the same screen validation and the same idempotency
614
+ guard as any other.
615
+
616
+ - **Deliberate, never automatic.** Nothing calls it on its own: no retry, no
617
+ sweeper. An operator asks for one run, by id.
618
+ - **Safe to refuse, with the reason named.** A run that is still suspended, one
619
+ whose resume is *in flight*, one that completed, one that was cancelled, one
620
+ that never suspended, and an unknown id each get their own refusal — as does
621
+ an unreadable store, which is refused rather than guessed at.
622
+ - **Idempotent.** A suspension is keyed by run id, so however many operators
623
+ ask there is one resumable pause and no extra traversal — the verb re-arms
624
+ and stops. Two racing callers in one process get one restore and one refusal.
625
+ - **It leaves a trace.** The restore is logged with the run, flow, node, when
626
+ the suspension was consumed, who asked and why, and the run is recorded
627
+ `paused` again so the repair is not invisible. Across a restart the exit
628
+ still works: the consumed suspension rides the run's own terminal history row
629
+ (in `sys_automation_run` columns that already existed), and a run that is
630
+ restored and then finishes clears it.
631
+
632
+ ⚠️ A repair, not a prevention. The failed attempt's side effects are **not**
633
+ undone and the original resume signal is **not** replayed — the continuation
634
+ must be re-issued. Whether the pause should survive a downstream throw at all
635
+ is a separate, unruled decision (#13937); this change leaves the resume
636
+ ordering, `forgetSuspendedRun` and `traverseNext` exactly as they are, mints no
637
+ new run status, and works whichever way that is ruled — the runs already stuck
638
+ today are not released by changing what future resumes do.
639
+
640
+ `plugin-approvals` carries a comment correction only: its organization backfill
641
+ documented `context_json` as never written on terminal rows, which this change
642
+ makes false for that one class of row. No behaviour change there.
643
+ - 5a916c4: **Ops:** a one-off, idempotent backfill for the platform rows the pre-#10101 writers stranded with no organization — dry run first (#11308).
644
+
645
+ #10101 fixed the WRITERS: a `sys_approval_request` and a `sys_automation_run` are now stamped from the SUBJECT record's organization, with the acting context as the ruled fallback. It wrote nothing to existing rows, so the population produced before it persists — a **pending** org-less approval request LOCKS the record it is about while being invisible in every organization-scoped inbox, its own owner's included, and automation-run history stays unattributed. This is the repair for those rows, on the maintainer's 2026-08-23 ruling (direction 3).
646
+
647
+ `packages/plugins/plugin-approvals/src/backfill-platform-row-organizations.ts` sweeps `sys_approval_request` (with its `sys_approval_action` / `sys_approval_approver` children, which move with their request) and `sys_automation_run`. It scans only rows whose organization column is unset, re-reads each row's subject at repair time — live record first, the write-time snapshot (`payload_json` / `context_json`'s `record`) second for a subject that has since been deleted — and stamps the platform row with the subject's own organization.
648
+
649
+ **Dry run first, and by default.** `planPlatformRowOrganizationBackfill(engine)` reads only and returns a per-object report naming every row it would touch; `runPlatformRowOrganizationBackfill(engine, { dryRun: false })` writes. Nothing runs at boot and nothing is scheduled: this is an operator-invoked module, run once against an affected install.
650
+
651
+ **Rows whose subject is equally org-less are counted and named, never written.** The acting-context fallback the writers apply is not available to a repair — the acting context is gone — and inventing one stays vetoed. Those ids are reported so the population is checkable and stays visible.
652
+
653
+ **`sys_api_key`'s divergence is preserved, not flattened.** Both the column read on a subject and the column written on a platform row are resolved from the registered schema through the shared `createRecordOrganizationResolver` (`@objectstack/metadata-core`), so a platform row ABOUT an API key is repaired from that object's stamp-only `active_organization_id` (limb 0, #8778) and the credential table itself is never written to.
654
+
655
+ **Idempotent, and asserted rather than claimed.** Every scan is `WHERE <organization column> IS NULL` and every write fills that column, so a repaired row cannot match again; the test suite runs the sweep twice and pins the second run at zero writes.
656
+
657
+ Publishes no runtime code: the module is not exported from the package index and not bundled into `dist` (`tsup` builds `src/index.ts`). It is graded rather than skipped because the release notes are where an operator of an affected install learns the repair exists, what it will and will not touch, and that the dry run comes first.
658
+ - 30928a6: fix(i18n): read the provenance companion at serving time, not only record it (#12642)
659
+
660
+ Maintainer ruling #12069 Option A (#11671) landed translation provenance as
661
+ **two** halves: `os i18n extract --source-hashes` RECORDS which source revision
662
+ a generated leaf is still a byte copy of, and `withSourceFallback` READS those
663
+ records at serving time and substitutes the current source for a leaf whose
664
+ source has moved underneath it. The recording half was then rolled out to every
665
+ bundle set. The reading half was not — measured on `main`: provenance
666
+ **recorded in 9 of 9** bundle sets and **read at serving time in 1**.
667
+
668
+ The other eight assembled their `TranslationBundle` straight from the raw
669
+ generated modules and never consulted the companion sitting beside them, so
670
+ they recorded the drift and went on serving the superseded draft. Nothing said
671
+ so: `check:i18n` compares key sets and they still matched, `check:i18n-coverage`
672
+ counts a present leaf as translated, and `check:i18n-stale-fill`'s cross-locale
673
+ rule needs a SECOND locale holding the same stale bytes before it can testify.
674
+ The measured case had one locale and no second witness.
675
+
676
+ All eight are wired here, in the shape `@objectstack/platform-objects`'s own
677
+ `metadata-translations/index.ts` uses — the committed
678
+ `<locale>.source-hashes.generated.ts` passed as the fourth argument, the third
679
+ left `undefined` because these sets have no hand-authored sections. Provenance
680
+ is now recorded in 9 of 9 sets and served in 9 of 9.
681
+
682
+ `@objectstack/plugin-webhooks` was the last of them and is the only one whose
683
+ manifest changed: `withSourceFallback` lives in `@objectstack/platform-objects`,
684
+ which that package did not declare. It was **already in that package's install
685
+ closure** through `@objectstack/service-messaging`, so the edge declares a
686
+ resolution that already resolved rather than adding a package to the graph —
687
+ and relying on it undeclared would have been a phantom dependency under this
688
+ repo's strict package manager.
689
+
690
+ `check:i18n-stale-fill` gains a second verdict, **UNSERVED PROVENANCE**, so this
691
+ cannot silently come apart again: a bundle set that commits a companion and
692
+ does not consult it at serving time now fails the build, including a tenth set
693
+ that lands tomorrow.
694
+
695
+ **Graded `patch`, and the grade is the interesting part.** No API changes, no
696
+ new exported surface, and no key set moves — substitution was chosen over
697
+ deletion precisely so key-set claims stay put (ruling #8765 Option B). What
698
+ changes is which STRING a stale leaf serves. On this tree that is **zero
699
+ leaves**: a record is only ever written for a leaf that IS a byte copy of the
700
+ current source, so the companions arrive 0-stale by construction. The change is
701
+ in what happens the next time a source string moves — the reader sees the
702
+ English source rather than a superseded draft of it, which is the same
703
+ degradation an untranslated key already produces and not a new state.
704
+ - de47336: chore(i18n): roll the generated-leaf provenance companion out to the remaining bundle sets (#12559)
705
+
706
+ `os i18n extract --source-hashes` (#11671, maintainer ruling #12069 Option A)
707
+ records, per generated translation leaf, the digest of the source revision that
708
+ leaf is **still a byte copy of** — the one signal that tells a stale fill from a
709
+ real translation once the source has moved and the two stopped being
710
+ distinguishable by value. It shipped opt-in, and exactly one of the nine i18n
711
+ bundle sets opted in. A landed detector, a changeset announcing it and a green
712
+ gate read together as *"generated translation staleness is now caught"*; for
713
+ eight of nine sets it was not, and the thing making it not caught was a single
714
+ absent flag in an extract config — invisible from all three of those surfaces.
715
+
716
+ **All eight remaining sets now opt in** — `plugin-approvals`, `plugin-audit`,
717
+ `plugin-security`, `plugin-sharing`, `plugin-webhooks`, `service-messaging`,
718
+ `service-realtime`, `service-storage`. Each documents `source-hashes` in its
719
+ extract config and commits three `<locale>.source-hashes.generated.ts`
720
+ companions, produced by the same extract run as the bundles they sit beside
721
+ (`check:i18n` compares them byte-for-byte, so they cannot be written by hand).
722
+ `check:i18n` now reports 7 bundles per set where it reported 4, and 11 for
723
+ `platform-objects` where it reported 8.
724
+
725
+ **Records count what is currently RECORDABLE, never what is covered.** A record
726
+ is written only for a leaf that *is* right now a byte copy of the current
727
+ source, so a fully translated locale starts with an empty table — which is the
728
+ instrument armed, not an instrument that measures nothing: the entry appears by
729
+ itself on the first extract after a leaf becomes a fill. Measured at this
730
+ commit, per set over its three translated locales: `service-messaging` 289,
731
+ `plugin-approvals` 61, `plugin-security` 33, `plugin-webhooks` 20,
732
+ `plugin-audit` 8, `service-storage` 7, `plugin-sharing` 1 (es-ES only; zh-CN and
733
+ ja-JP are fully translated and start empty), `service-realtime` 0 (all three
734
+ locales fully translated). **419 records written across the eight sets, 0
735
+ stale.**
736
+
737
+ **One extractor fix the rollout forced.** `--source-hashes` had one user, and
738
+ that user commits both generated sections, so the interaction with
739
+ `--no-metadata-forms` had never been exercised. The provenance table is computed
740
+ over every generated section the extractor builds; the eight sets here commit no
741
+ metadata-forms bundle, and their `metadataForms` subtree — absent from their
742
+ merge baseline — arrives as a fresh `--fill=default` copy of `en`, so every leaf
743
+ of it was recordable. First measured on `plugin-audit`: **763 records, of which
744
+ 2 were its own objects and 761 were digests of the Studio metadata-form baseline
745
+ `@objectstack/platform-objects` owns.** Those records are unreadable in the
746
+ package holding them and would have rewritten all 24 companions on any unrelated
747
+ `*.form.ts` change in `packages/spec` — the cross-package coupling ADR-0029 D8
748
+ and every `bundle-ownership.test.ts` keep out of committed bundles. The
749
+ companion now covers exactly the sections a run commits, decided by the same two
750
+ predicates that decide the bundle files. `platform-objects` commits both, so its
751
+ three committed companions are byte-for-byte unchanged.
752
+
753
+ **Grade: `patch`, and behaviour on the day it lands is unchanged for every
754
+ leaf.** A record is written only where a leaf is currently a byte copy of the
755
+ **current** source, so every record written equals the current digest and none
756
+ of them can be stale; the mechanism cannot arrive red. No committed translation
757
+ bundle changed a byte, no public API moved, and no leaf's rendered text changed.
758
+ `narrowToCommittedSections` is new but internal to `@objectstack/cli` — the
759
+ package's entrypoint does not re-export the extractor utils.
760
+
761
+ **What this does not do**, stated so the boundary is not inferred wrongly a
762
+ second time: these eight sets now *record* provenance. Reading it at serving
763
+ time is `withSourceFallback`, and that is still wired in
764
+ `@objectstack/platform-objects` alone — so a stale fill in one of the eight is
765
+ now recorded and reportable, but not yet substituted at runtime. Tracked
766
+ separately.
767
+ - ad54eb3: feat(tooling): onboard all 14 `packages/plugins/**` packages into `check:test-typecheck` (#14062)
768
+
769
+ Every plugin package now has a `tsconfig.test.json` compiled by the shared
770
+ `check:test-typecheck` gate, and its `typecheck` script names it. Before this,
771
+ the shrink-only `test-typecheck-debt.json` ratchet said **nothing** about a
772
+ third of the repo's runtime surface: 14 packages, 1 `tsconfig.test.json`
773
+ (`plugin-security`, wired directly to `tsc` rather than to the instrument), and
774
+ 0 `check:test-typecheck` scripts.
775
+
776
+ Onboarded as a family by the director ruling of 2026-09-01 on #14062
777
+ (maintainer verbatim: 「同意」), which also carries the #5286 maintainer
778
+ authority the starting ledgers need. The smaller branch triage recommended —
779
+ declare the instrument's scope and re-site the two compile-time pins — was
780
+ recorded as considered and not taken: an instrument silent over a third of the
781
+ runtime surface is a hole readers generalise across, and that costs more than
782
+ fourteen tsconfigs.
783
+
784
+ **Measured, not assumed** (at `e80889095`, workspace closure built first). Four
785
+ packages carry residue and therefore a starting ledger — plugin-approvals 324
786
+ over 8 files, plugin-auth 94 over 10, plugin-sharing 3 over 2,
787
+ knowledge-ragflow 3 over 1. The other ten measure **zero** and deliberately get
788
+ no ledger file at all: the gate reads a missing ledger as `{ entries: {} }`, so
789
+ any error there is red immediately with no entry to be added to — strictly
790
+ stronger than a ledger holding nothing, and the call `plugin-security` had
791
+ already recorded for itself.
792
+
793
+ ⛔ **This does not repair 345 type errors.** Per ruling item 3 it makes the
794
+ ratchet able to *see* them; paydown follows the ratchet's own shrink-only
795
+ discipline on its own cards. No test file is edited here.
796
+
797
+ Two corrections to the finding's own prose, both measured: the exclusion is
798
+ narrower than "no plugin package compiles its tests" — 9 of the 14 already
799
+ compiled their tests inside the `typecheck`-invoked build config, at zero
800
+ errors — and `exec-context-annotation.pin.ts` is a `.pin.ts`, which
801
+ `**/*.test.ts` never excluded, so its directives were already live. The pin
802
+ this change genuinely makes real is
803
+ `plugin-approvals/src/manager-org-screen-parity.contract.test.ts`, which no tsc
804
+ program had ever read.
805
+ - 1272f0a: Promote `resolveRecordOrganizationField` to the shared platform-row organization resolver (the cloud#1395 Option A ruling): a platform row's organization is the SUBJECT record's organization; actor context is the fallback, never the primary.
806
+
807
+ - `@objectstack/metadata-core` now owns the resolver (`resolveRecordOrganizationField`, `createFieldPresenceProbe`, and the new memoized `createRecordOrganizationResolver` factory) so all three sanctioned writers share one precedence.
808
+ - `@objectstack/plugin-approvals`: `openNodeRequest` stamps `sys_approval_request`, `sys_approval_action` and the `sys_approval_approver` index from the subject record's organization (acting context as fallback). Fixes the measured defect where every schedule / time-relative / api triggered approval persisted `organization_id = NULL` — locking the record it was about while being invisible in every inbox, its owner's included.
809
+ - `@objectstack/service-automation`: `sys_automation_run` rows (paused and terminal) resolve their organization from the trigger-record snapshot, with the acting tenant as fallback. Terminal rows previously never carried an organization at all.
810
+ - `@objectstack/plugin-audit`: the resolver moved out; the package re-exports it from the original paths, behavior unchanged.
811
+
812
+ The `sys_api_key` divergence is preserved and pinned: `tenancy.organizationField` (who a row is ABOUT) still wins over the tenant wall answer, and the credential table stays unwalled.
813
+ - Updated dependencies [809d417]
814
+ - Updated dependencies [387e231]
815
+ - Updated dependencies [f794e4e]
816
+ - Updated dependencies [cae2169]
817
+ - Updated dependencies [b812a54]
818
+ - Updated dependencies [2d4fa75]
819
+ - Updated dependencies [0e4e51b]
820
+ - Updated dependencies [e84bbf6]
821
+ - Updated dependencies [effae80]
822
+ - Updated dependencies [efb3513]
823
+ - Updated dependencies [d62f990]
824
+ - Updated dependencies [c45d8e6]
825
+ - Updated dependencies [2e3e8c7]
826
+ - Updated dependencies [e621291]
827
+ - Updated dependencies [655b106]
828
+ - Updated dependencies [40a93b5]
829
+ - Updated dependencies [101ad2c]
830
+ - Updated dependencies [d5b330d]
831
+ - Updated dependencies [dda969c]
832
+ - Updated dependencies [1f45690]
833
+ - Updated dependencies [277948f]
834
+ - Updated dependencies [8bdd955]
835
+ - Updated dependencies [54e2d36]
836
+ - Updated dependencies [b745157]
837
+ - Updated dependencies [f3bbbef]
838
+ - Updated dependencies [4f24e9d]
839
+ - Updated dependencies [e27583e]
840
+ - Updated dependencies [4bd6faa]
841
+ - Updated dependencies [86cbe37]
842
+ - Updated dependencies [6a180e4]
843
+ - Updated dependencies [474242f]
844
+ - Updated dependencies [63cd487]
845
+ - Updated dependencies [bd4aa4e]
846
+ - Updated dependencies [803eaab]
847
+ - Updated dependencies [f8e8f03]
848
+ - Updated dependencies [983edf1]
849
+ - Updated dependencies [eae824e]
850
+ - Updated dependencies [f6fa22c]
851
+ - Updated dependencies [8a483b3]
852
+ - Updated dependencies [3bc2e38]
853
+ - Updated dependencies [97bcd99]
854
+ - Updated dependencies [df59de0]
855
+ - Updated dependencies [96e25a8]
856
+ - Updated dependencies [713f83f]
857
+ - Updated dependencies [77d4b3c]
858
+ - Updated dependencies [f75a38a]
859
+ - Updated dependencies [7a25e7d]
860
+ - Updated dependencies [1fa05a6]
861
+ - Updated dependencies [c85a265]
862
+ - Updated dependencies [dcb10a5]
863
+ - Updated dependencies [773a999]
864
+ - Updated dependencies [35dffea]
865
+ - Updated dependencies [d8024f0]
866
+ - Updated dependencies [8120808]
867
+ - Updated dependencies [776a098]
868
+ - Updated dependencies [5060877]
869
+ - Updated dependencies [4f6325d]
870
+ - Updated dependencies [52954c0]
871
+ - Updated dependencies [2aa8456]
872
+ - Updated dependencies [d23ebb9]
873
+ - Updated dependencies [93809a3]
874
+ - Updated dependencies [7c0d0c3]
875
+ - Updated dependencies [daae7aa]
876
+ - Updated dependencies [8dc22d6]
877
+ - Updated dependencies [fa5d137]
878
+ - Updated dependencies [a392dbf]
879
+ - Updated dependencies [279431e]
880
+ - Updated dependencies [948dd6b]
881
+ - Updated dependencies [3b4c56c]
882
+ - Updated dependencies [ae8edd2]
883
+ - Updated dependencies [e25403c]
884
+ - Updated dependencies [a81aa9d]
885
+ - Updated dependencies [64baa68]
886
+ - Updated dependencies [9fa70d7]
887
+ - Updated dependencies [09db64a]
888
+ - Updated dependencies [92916e7]
889
+ - Updated dependencies [a84f3ea]
890
+ - Updated dependencies [f2eaae8]
891
+ - Updated dependencies [56c093c]
892
+ - Updated dependencies [c09451b]
893
+ - Updated dependencies [ba64877]
894
+ - Updated dependencies [e7191ce]
895
+ - Updated dependencies [7345308]
896
+ - Updated dependencies [79b6a22]
897
+ - Updated dependencies [30d96ab]
898
+ - Updated dependencies [f658793]
899
+ - Updated dependencies [0fd4899]
900
+ - Updated dependencies [c95ad19]
901
+ - Updated dependencies [e58ea8b]
902
+ - Updated dependencies [4a17645]
903
+ - Updated dependencies [3795c5f]
904
+ - Updated dependencies [8ab926b]
905
+ - Updated dependencies [7317cf2]
906
+ - Updated dependencies [e25e839]
907
+ - Updated dependencies [5997207]
908
+ - Updated dependencies [8b13cc8]
909
+ - Updated dependencies [00d8f65]
910
+ - Updated dependencies [4a4a35d]
911
+ - Updated dependencies [4a4a35d]
912
+ - Updated dependencies [86e765a]
913
+ - Updated dependencies [1d7e76a]
914
+ - Updated dependencies [53dc739]
915
+ - Updated dependencies [fd289be]
916
+ - Updated dependencies [03bf7b1]
917
+ - Updated dependencies [f90e820]
918
+ - Updated dependencies [18d816a]
919
+ - Updated dependencies [e8bd715]
920
+ - Updated dependencies [b91c351]
921
+ - Updated dependencies [a28a3c0]
922
+ - Updated dependencies [200d255]
923
+ - Updated dependencies [2852acc]
924
+ - Updated dependencies [daeaaf9]
925
+ - Updated dependencies [c459da6]
926
+ - Updated dependencies [e914733]
927
+ - Updated dependencies [1d8ad0f]
928
+ - Updated dependencies [9738c35]
929
+ - Updated dependencies [f887e52]
930
+ - Updated dependencies [881f8d8]
931
+ - Updated dependencies [3bfa1e6]
932
+ - Updated dependencies [0a8ebf3]
933
+ - Updated dependencies [901355c]
934
+ - Updated dependencies [34ce8e7]
935
+ - Updated dependencies [33681ea]
936
+ - Updated dependencies [bfe13c8]
937
+ - Updated dependencies [0fb3044]
938
+ - Updated dependencies [4635f3e]
939
+ - Updated dependencies [fd289be]
940
+ - Updated dependencies [ee3595c]
941
+ - Updated dependencies [09b4f4e]
942
+ - Updated dependencies [b2eab95]
943
+ - Updated dependencies [93940d4]
944
+ - Updated dependencies [3a04b01]
945
+ - Updated dependencies [45b9051]
946
+ - Updated dependencies [3954fb7]
947
+ - Updated dependencies [4805b56]
948
+ - Updated dependencies [b9e9227]
949
+ - Updated dependencies [d395692]
950
+ - Updated dependencies [5894d30]
951
+ - Updated dependencies [a3765f6]
952
+ - Updated dependencies [2d5cee3]
953
+ - Updated dependencies [e22158f]
954
+ - Updated dependencies [7404925]
955
+ - Updated dependencies [0c2334f]
956
+ - Updated dependencies [778c59f]
957
+ - Updated dependencies [d2619fd]
958
+ - Updated dependencies [af56546]
959
+ - Updated dependencies [6acb11a]
960
+ - Updated dependencies [33c5fd3]
961
+ - Updated dependencies [20b0fdb]
962
+ - Updated dependencies [905019b]
963
+ - Updated dependencies [a286411]
964
+ - Updated dependencies [98c0d33]
965
+ - Updated dependencies [368a82e]
966
+ - Updated dependencies [a3d5724]
967
+ - Updated dependencies [93ea19b]
968
+ - Updated dependencies [9ee2dcf]
969
+ - Updated dependencies [8cb96ec]
970
+ - Updated dependencies [8f10a79]
971
+ - Updated dependencies [6269a55]
972
+ - Updated dependencies [a17da05]
973
+ - Updated dependencies [a8c00e2]
974
+ - Updated dependencies [22e5236]
975
+ - Updated dependencies [0fb8760]
976
+ - Updated dependencies [e5ce2ed]
977
+ - Updated dependencies [be21955]
978
+ - Updated dependencies [bc56e18]
979
+ - Updated dependencies [be21955]
980
+ - Updated dependencies [a9ee989]
981
+ - Updated dependencies [4d0d944]
982
+ - Updated dependencies [15d58db]
983
+ - Updated dependencies [d63b014]
984
+ - Updated dependencies [9abe4e4]
985
+ - Updated dependencies [2cc7122]
986
+ - Updated dependencies [50d6c92]
987
+ - Updated dependencies [15d55fb]
988
+ - Updated dependencies [9e0ba21]
989
+ - Updated dependencies [311433f]
990
+ - Updated dependencies [3e5ad08]
991
+ - Updated dependencies [9abe4e4]
992
+ - Updated dependencies [b7131f3]
993
+ - Updated dependencies [e5812fa]
994
+ - Updated dependencies [7085f90]
995
+ - Updated dependencies [dee4dd4]
996
+ - Updated dependencies [ce7e497]
997
+ - Updated dependencies [51ecb2f]
998
+ - Updated dependencies [9086761]
999
+ - Updated dependencies [f6344e7]
1000
+ - Updated dependencies [42a117b]
1001
+ - Updated dependencies [1401ae7]
1002
+ - Updated dependencies [4297fe7]
1003
+ - Updated dependencies [e398863]
1004
+ - Updated dependencies [d16df74]
1005
+ - Updated dependencies [d79c602]
1006
+ - Updated dependencies [f11fc61]
1007
+ - Updated dependencies [e808890]
1008
+ - Updated dependencies [8f79379]
1009
+ - Updated dependencies [e6ca40e]
1010
+ - Updated dependencies [0c77ea4]
1011
+ - Updated dependencies [52954c0]
1012
+ - Updated dependencies [89eb997]
1013
+ - Updated dependencies [7131f12]
1014
+ - Updated dependencies [aa5994e]
1015
+ - Updated dependencies [be93457]
1016
+ - Updated dependencies [a65db76]
1017
+ - Updated dependencies [2cf5a96]
1018
+ - Updated dependencies [15eb2c9]
1019
+ - Updated dependencies [5691b07]
1020
+ - Updated dependencies [2a6122b]
1021
+ - Updated dependencies [225e769]
1022
+ - Updated dependencies [8af88dd]
1023
+ - Updated dependencies [fb5fbb8]
1024
+ - Updated dependencies [d7b3963]
1025
+ - Updated dependencies [33184fd]
1026
+ - Updated dependencies [7c41693]
1027
+ - Updated dependencies [b72db01]
1028
+ - Updated dependencies [dce5cd4]
1029
+ - Updated dependencies [9688f58]
1030
+ - Updated dependencies [556ebc1]
1031
+ - Updated dependencies [177ebdc]
1032
+ - Updated dependencies [8d237b4]
1033
+ - Updated dependencies [2d2e6f0]
1034
+ - Updated dependencies [2d8dd8d]
1035
+ - Updated dependencies [22d573e]
1036
+ - Updated dependencies [b5a2398]
1037
+ - Updated dependencies [348860c]
1038
+ - Updated dependencies [5383fa6]
1039
+ - Updated dependencies [5b3ff63]
1040
+ - Updated dependencies [1a6a19c]
1041
+ - Updated dependencies [064d484]
1042
+ - Updated dependencies [527e050]
1043
+ - Updated dependencies [dd33bf9]
1044
+ - Updated dependencies [4cb2a90]
1045
+ - Updated dependencies [74a7804]
1046
+ - Updated dependencies [53d3689]
1047
+ - Updated dependencies [b3a63d3]
1048
+ - Updated dependencies [49f0dcf]
1049
+ - Updated dependencies [033a34c]
1050
+ - Updated dependencies [4d25d22]
1051
+ - Updated dependencies [1ffee51]
1052
+ - Updated dependencies [5ae4303]
1053
+ - Updated dependencies [ece4dad]
1054
+ - Updated dependencies [e9b377e]
1055
+ - Updated dependencies [146f448]
1056
+ - Updated dependencies [735f5c7]
1057
+ - Updated dependencies [a7e18de]
1058
+ - Updated dependencies [366f895]
1059
+ - Updated dependencies [dc75ba8]
1060
+ - Updated dependencies [cce0aa9]
1061
+ - Updated dependencies [e764507]
1062
+ - Updated dependencies [cff17af]
1063
+ - Updated dependencies [39404f3]
1064
+ - Updated dependencies [ca1965f]
1065
+ - Updated dependencies [8619f95]
1066
+ - Updated dependencies [b706af9]
1067
+ - Updated dependencies [db8c288]
1068
+ - Updated dependencies [0e5fe7f]
1069
+ - Updated dependencies [add4360]
1070
+ - Updated dependencies [e0abc38]
1071
+ - Updated dependencies [fc9ba76]
1072
+ - Updated dependencies [1272f0a]
1073
+ - Updated dependencies [0f94cc7]
1074
+ - Updated dependencies [a11c1a5]
1075
+ - Updated dependencies [71f9cd1]
1076
+ - Updated dependencies [ee17d86]
1077
+ - Updated dependencies [cdbd920]
1078
+ - Updated dependencies [18c432e]
1079
+ - Updated dependencies [3c418c4]
1080
+ - Updated dependencies [fa8715a]
1081
+ - Updated dependencies [a933ed7]
1082
+ - Updated dependencies [b3ca463]
1083
+ - Updated dependencies [a933ed7]
1084
+ - Updated dependencies [0d4a6a8]
1085
+ - Updated dependencies [518d5e5]
1086
+ - Updated dependencies [6643ba1]
1087
+ - Updated dependencies [eeba2ef]
1088
+ - Updated dependencies [ec4c4d2]
1089
+ - Updated dependencies [424f73c]
1090
+ - Updated dependencies [cccbe51]
1091
+ - Updated dependencies [a8d6b1d]
1092
+ - Updated dependencies [e4a7695]
1093
+ - Updated dependencies [87075b1]
1094
+ - Updated dependencies [fc58a99]
1095
+ - Updated dependencies [14cfc00]
1096
+ - Updated dependencies [1c6f7b4]
1097
+ - Updated dependencies [e854a53]
1098
+ - Updated dependencies [dfebfc8]
1099
+ - Updated dependencies [598b7ec]
1100
+ - Updated dependencies [d028b37]
1101
+ - Updated dependencies [f7b25c5]
1102
+ - Updated dependencies [122ef38]
1103
+ - Updated dependencies [4a37870]
1104
+ - Updated dependencies [428f9b2]
1105
+ - Updated dependencies [aa7ff56]
1106
+ - Updated dependencies [811a3c2]
1107
+ - Updated dependencies [1401ae7]
1108
+ - Updated dependencies [2fd3f1c]
1109
+ - Updated dependencies [c41b42e]
1110
+ - Updated dependencies [d41d166]
1111
+ - Updated dependencies [c4db311]
1112
+ - Updated dependencies [750fff5]
1113
+ - Updated dependencies [c19035e]
1114
+ - Updated dependencies [ececf7a]
1115
+ - Updated dependencies [d173125]
1116
+ - Updated dependencies [8eeca27]
1117
+ - Updated dependencies [8425c17]
1118
+ - Updated dependencies [a5ef1d8]
1119
+ - Updated dependencies [87ad30c]
1120
+ - Updated dependencies [772d5de]
1121
+ - Updated dependencies [ce80ec2]
1122
+ - Updated dependencies [b372318]
1123
+ - Updated dependencies [97a2263]
1124
+ - Updated dependencies [29d0676]
1125
+ - Updated dependencies [0169d49]
1126
+ - Updated dependencies [6bd3231]
1127
+ - Updated dependencies [d2b5ba8]
1128
+ - Updated dependencies [b799ac5]
1129
+ - Updated dependencies [8f74307]
1130
+ - Updated dependencies [d23dc08]
1131
+ - Updated dependencies [038f333]
1132
+ - Updated dependencies [644ad50]
1133
+ - Updated dependencies [9735662]
1134
+ - Updated dependencies [4d5b4f8]
1135
+ - Updated dependencies [5d16379]
1136
+ - Updated dependencies [0da7cd2]
1137
+ - Updated dependencies [28a5c3e]
1138
+ - Updated dependencies [4bc18e5]
1139
+ - Updated dependencies [9f57f1e]
1140
+ - @objectstack/spec@17.3.0
1141
+ - @objectstack/platform-objects@17.3.0
1142
+ - @objectstack/core@17.3.0
1143
+ - @objectstack/types@17.3.0
1144
+ - @objectstack/metadata-core@17.3.0
1145
+ - @objectstack/formula@17.3.0
1146
+
3
1147
  ## 17.2.0
4
1148
 
5
1149
  ### Minor Changes