@object-ui/data-objectstack 17.5.0 → 17.7.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +2311 -0
  2. package/README.md +557 -11
  3. package/dist/index.cjs +1663 -463
  4. package/dist/index.d.cts +1780 -518
  5. package/dist/index.d.ts +1780 -518
  6. package/dist/index.js +1648 -457
  7. package/package.json +9 -9
  8. package/dist/tsconfig.tsbuildinfo +0 -1
  9. package/src/aggregate-capability.test.ts +0 -177
  10. package/src/apiAccessDenied.test.ts +0 -233
  11. package/src/appAccessProbe.test.ts +0 -147
  12. package/src/bulkDelete.test.ts +0 -75
  13. package/src/bulkUpdate.test.ts +0 -93
  14. package/src/cache/MetadataCache.test.ts +0 -477
  15. package/src/cache/MetadataCache.ts +0 -289
  16. package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
  17. package/src/connection.test.ts +0 -188
  18. package/src/contracts.ts +0 -115
  19. package/src/deleteView.homes.test.ts +0 -389
  20. package/src/deleteViewContract.types.test.ts +0 -246
  21. package/src/errors.test.ts +0 -426
  22. package/src/errors.ts +0 -293
  23. package/src/exportDownload.test.ts +0 -141
  24. package/src/filter-entry-translation.test.ts +0 -360
  25. package/src/filter-operator-ast-parity.test.ts +0 -135
  26. package/src/getObjectSchema.test.ts +0 -145
  27. package/src/httpFailureLogging.test.ts +0 -109
  28. package/src/importJob.test.ts +0 -71
  29. package/src/index.ts +0 -4442
  30. package/src/integration.ts +0 -200
  31. package/src/listViewOverrides.test.ts +0 -241
  32. package/src/listViews.test.ts +0 -175
  33. package/src/metadata-client.get-envelope.test.ts +0 -171
  34. package/src/metadata-client.saveAdvisories.test.ts +0 -226
  35. package/src/metadata-client.test.ts +0 -303
  36. package/src/metadata-client.ts +0 -1046
  37. package/src/occ.test.ts +0 -101
  38. package/src/onMutation.test.ts +0 -589
  39. package/src/onSaveAdvisory.test.ts +0 -311
  40. package/src/onWriteWarning.test.ts +0 -273
  41. package/src/orderby-serialization.test.ts +0 -115
  42. package/src/queryDataset.test.ts +0 -543
  43. package/src/searchAll.test.ts +0 -120
  44. package/src/security.ts +0 -243
  45. package/src/spec-symbol-batch6.test.ts +0 -326
  46. package/src/studio.ts +0 -152
  47. package/src/updateView.draft.test.ts +0 -242
  48. package/src/userState.test.ts +0 -315
  49. package/src/userState.ts +0 -238
  50. package/src/v3-compat.test.ts +0 -229
  51. package/src/validation-error.test.ts +0 -94
  52. package/src/viewCacheInvalidation.pin.test.ts +0 -260
  53. package/src/viewInvalidationSeam.pin.test.ts +0 -162
package/CHANGELOG.md CHANGED
@@ -1,5 +1,2316 @@
1
1
  # @object-ui/data-objectstack
2
2
 
3
+ ## 17.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7b395d8: **BREAKING** — three more record ids that the objectui#9511 ruling's enumeration left out are
8
+ strings now (objectui#10078). `CommentSearchResult.recordId`, `RecordSubscription.recordId`
9
+ and `DataSourceMutationEvent.id` narrow **FROM** `string | number` **TO** `string`. ⚠️ Other
10
+ record ids in `@object-ui/types` still admit a number and are not touched here, among them
11
+ `DetailViewSchema.recordNavigation` (`recordIds`, `onNavigate`), `FeedItem.sourceId`, and the
12
+ `onNavigate` slots on `ObjectGridSchema`, `ObjectViewSchema` and `ListViewRuntimeProps`.
13
+
14
+ ```ts
15
+ // before — compiled
16
+ const bell: RecordSubscription = { recordId: 42, subscribed: true };
17
+ // after — refused by the compiler; write the id the protocol carries
18
+ const bell: RecordSubscription = { recordId: '42', subscribed: true };
19
+ ```
20
+
21
+ **Why.** objectui#9511 (director batch #195 item 1, letter A) ruled that a record id is a
22
+ string wherever **metadata** names one, and extended that, as 「the same principle」, to
23
+ `CommentEntry.recordId` and `MentionNotification.recordId`. It did not name these three, which
24
+ are runtime and API shapes rather than metadata. Carrying the rule to them is the `domain:spec`
25
+ seat's inheritance decision, recorded in the claim on objectui#10078: they hold the same id and
26
+ were left with the old spelling. `CommentSearchResult.recordId` is copied straight from
27
+ `CommentEntry.recordId`, `RecordSubscription.recordId` names the record a notification bell is
28
+ for, and `DataSourceMutationEvent.id` is the id the `update` / `delete` doors already take as a
29
+ string.
30
+
31
+ **What changes at runtime.** `ValueDataSource` and `ObjectStackAdapter` now emit
32
+ `onMutation` events whose `id` is always a string. Both methods still accept a numeric id
33
+ through their own wider parameter, and each converts it at its own boundary before emitting —
34
+ the one place the rule says the conversion lives. An in-memory `value` source whose items
35
+ carry numeric keys therefore announces `'42'`, not `42`. A subscriber that compared
36
+ `event.id` against a number must compare against the string. The two in-repo subscribers
37
+ that read `event.id` already pass it through `String(...)`, so they see no difference.
38
+
39
+ **Packages that narrow without a source change.** `@object-ui/collaboration` (`useCommentSearch`
40
+ returns `CommentSearchResult[]`) and `@object-ui/plugin-detail` (`SubscriptionToggleProps`
41
+ carries a `RecordSubscription`) publish the narrower type through their own `.d.ts`.
42
+
43
+ **What a host has to do.** Code that builds a `RecordSubscription`, reads
44
+ `CommentSearchResult.recordId`, or implements `DataSource.onMutation` with numeric keys
45
+ converts once, where the number enters (its own adapter or data boundary), and passes the
46
+ string from there on. ⛔ Nothing converts silently on the reading side.
47
+
48
+ **Disposition: ADR-0087 D7 — a published runtime TypeScript interface with no metadata
49
+ surface is carried by the compiler and needs no ledger entry; ⛔ no conversion entry, ⛔ not
50
+ a tombstone, ⛔ not an npm `major`.** None of the three has a zod mirror, none is authored into JSON, and no schema
51
+ references them, so there is no parse-time door and nothing for a conversion table to
52
+ rewrite: the compiler error at the numeric site is the delivery channel. The level is `minor`
53
+ because objectui marks its own breaking changes `minor` with this banner (AGENTS.md, version
54
+ alignment); `major` is refused outright because every publishable package sits in one `fixed`
55
+ group pinned to the `@objectstack` major (`scripts/check-changeset-no-major.mjs`).
56
+
57
+ **Pin.** `record-id-string-survivors-10078.test.ts` in `@object-ui/types` asserts all three
58
+ members read `string` (not `any`), refuses a numeric literal at each, and keeps a
59
+ source-text census of the three declarations with controls that prove the census can fire.
60
+ - 1563d3e: The object-metadata write guard now holds a `select` / `radio` field that has no option source.
61
+
62
+ This is the objectui half of the maintainer's ruling A on objectstack#20827, which refuses a
63
+ `select` / `radio` with neither `options` nor `picklist` at the `FieldSchema` door and has objectui
64
+ stop sending one first. The rule lives in the existing door guard, so `MetadataClient.save`,
65
+ `importObjectDraft` and `MetadataService` all apply it, for every writer behind them.
66
+
67
+ BREAKING (behaviour, released as `minor` per this repository's version policy):
68
+ `assertObjectMetadataWritable('object', …)`, and so `MetadataClient.save('object', …)`, now throws
69
+ BEFORE issuing the request when a `select` or `radio` field carries neither a non-empty `options`
70
+ list nor a `picklist` string. `options: []` counts as none, as the contract's own completeness rule
71
+ reads it. Unlike the relationship rule, this refuses a document the installed server still accepts:
72
+ the ruling orders the client first on purpose, and the door follows. The message names the field
73
+ and asks for an option, a shared `picklist`, or a non-choice type. A field that names a `picklist`
74
+ is never refused by this rule.
75
+
76
+ The Studio data page and the metadata-admin object editor already surface the refusal in their
77
+ error banner and send the next autosave once an option exists, including for an object that already
78
+ stores such a field.
79
+
80
+ New export: `CHOICE_TYPES_REQUIRING_OPTIONS`, derived by this package's pin from the installed
81
+ `@objectstack/spec`'s `field/choice-without-options` completeness rule.
82
+ - d893290: `formatMetadataError` and `formatMetadataIssue` are exported from `@object-ui/data-objectstack`: the one reader of a failed metadata save (objectui#11302).
83
+
84
+ A save the spec refuses answers `422 INVALID_METADATA` with a headline message (a count plus `path [code]` locators) and the author's prescription in structured issues, which `MetadataClient` puts on `MetadataError.issues`. `formatMetadataError(err)` lists those issues one field per line (`• fields.amount.type — Required`) and falls back to the error's message when there are none; `formatMetadataIssue(issue)` is that one-line grammar, for callers that format a list of failures. Render the result with a whitespace class that keeps newlines.
85
+
86
+ The reader moved here from `@object-ui/app-shell`, where it was internal, so every surface that saves through `MetadataClient` can render a refusal the same way. Additive: nothing existing changed.
87
+ - 993f312: Parse a write-strip's `reason` against the spec enum at the boundary
88
+ (objectui#4934).
89
+
90
+ `notifyDroppedFields` filtered a create/update response's `droppedFields` on
91
+ SHAPE alone — a hand-written `e is DroppedFieldsEvent` guard that checked
92
+ `Array.isArray(fields)` and nothing else — so a `reason` outside
93
+ `'readonly' | 'readonly_when' | 'primary_key'` reached every subscriber typed as
94
+ though it were inside the union. A deployed client normally runs BEHIND the
95
+ server it talks to, so a reason from the future is the expected skew direction,
96
+ not a corrupt payload; the interior was typed to trust a union no one had
97
+ checked, and nothing in the repo could say so. `notifyBatchDroppedFields` did
98
+ the same through its `entry as DroppedFieldsEvent & { index?: number }` cast.
99
+
100
+ Both paths now read `reason` against `DroppedFieldsEventSchema.shape.reason` —
101
+ the enum the installed pin declares, derived rather than restated, so a pin bump
102
+ that adds an arm widens the accept set on its own:
103
+
104
+ - **Every entry is kept.** Dropping the unparsable ones would tell the user
105
+ nothing about fields the server really did strip, which is exactly the silence
106
+ objectui#3484 removed.
107
+ - An unrecognized `reason` arrives on a named skew arm,
108
+ `UnrecognizedDropReasonEvent`, carrying `UNRECOGNIZED_DROP_REASON` plus the
109
+ wire value **verbatim** in `unrecognizedReason` — never coerced onto a known
110
+ arm, because claiming `readonly` for a reason we cannot name is a false
111
+ statement about the user's data.
112
+ - `WriteWarningEvent['droppedFields']` is therefore the two-arm
113
+ `DroppedFieldsNotice`. The spec type stays the canonical arm and is not
114
+ widened to `string` (objectui#3160): the skew arm is not assignable to
115
+ `DroppedFieldsEvent`, so a consumer branching on `reason` now hears about
116
+ server skew from `tsc` instead of from a per-consumer discipline.
117
+
118
+ Runtime wording is unchanged: the one reader, the app shell's write-warning
119
+ toast, already answered an unrecognized reason with its cause-free line.
120
+
121
+ **Blast radius — the compile error IS the intended signal, not a regression.** A
122
+ consumer that branches exhaustively on `reason` — a parameter, a `Map` key or a
123
+ `Record` annotated `DroppedFieldsEvent['reason']` — stops compiling against this
124
+ release, with a `TS2345` at each such site. That error is the notification, and
125
+ the only one: the skew arm is deliberately NOT assignable to the spec union, so
126
+ `tsc` reports server skew at the one place the wire is read rather than leaving
127
+ it to a per-consumer discipline. Do not cast it away. Widen the annotation to
128
+ `DroppedFieldsNotice['reason']`, and where the two arms have to be told apart,
129
+ narrow with `entry.reason === UNRECOGNIZED_DROP_REASON` and read the wire value
130
+ verbatim from `unrecognizedReason`.
131
+
132
+ Widen the LOOKUPS, not the table. A `Record` that must stay exhaustive over the
133
+ SPEC arms keeps its `DroppedFieldsEvent['reason']` key: widening that one would
134
+ trade away the guarantee that a newly pinned spec reason fails `type-check`
135
+ unworded (objectui#3935).
136
+
137
+ In this repo the entire blast radius is the app shell's write-warning toast —
138
+ two type annotations, no runtime change. Its executable JavaScript is byte-identical
139
+ and its wording tests pass unchanged, because the file was already written for
140
+ this value: its own docstring says the runtime `reason` may sit outside the spec
141
+ union and that the cause-free fallback is reachable, not dead. Only the parameter
142
+ and the `Map` key had been left narrower than that documented contract.
143
+ - 41b7ce3: **View configuration is explicitly org-wide, and its write path is now gated (objectstack#7494's
144
+ ruling, maintainer 2026-08-12).** The `sort` / `hiddenFields` / `columnState` / `rowHeight` that a
145
+ list toolbar persists were never per-user: they are one shared row on the view, so an ordinary user
146
+ dragging a column or cycling density was re-styling that view for the entire organization. Nothing
147
+ in the console said so, and nothing stopped it. A per-user scope stays parked (objectstack#7611,
148
+ v18) and is deliberately not built here — which is precisely why the write has to be gated rather
149
+ than narrowed: there is no second, private store for it to fall back to.
150
+
151
+ `ObjectStackAdapter.updateViewConfig` now refuses when the session's **reported** ADR-0066 capability
152
+ set does not contain `manage_metadata`, throwing the new `ViewConfigPermissionDeniedError`
153
+ (`VIEW_CONFIG_PERMISSION_DENIED`, with `isViewConfigPermissionDeniedError` and the
154
+ `VIEW_CONFIG_CAPABILITY` constant alongside it). The gate is the **first** statement in the method —
155
+ before `connect()`, before the payload is assembled — so a refused call puts nothing on the wire.
156
+ It is on the write rather than on the toolbar button on purpose: withholding the affordance would
157
+ leave the method still accepting the call from anything else holding the adapter, whereas a gate on
158
+ the write is inherited by every caller, present and future.
159
+
160
+ `manage_metadata` is not a newly minted name. It is the capability this repo already treats as
161
+ metadata-authoring authority — `HomePage`'s `AUTHORING_CAPABILITY`, the one the server itself
162
+ refuses metadata writes without — and the gated write goes through `client.meta.saveItem`, the very
163
+ same ADR-0005 metadata door, so this applies the authority the server is already applying instead of
164
+ inventing a parallel one.
165
+
166
+ **Unknown fails open, by doctrine.** A capability set that was never reported (a backend predating
167
+ ADR-0066, or no permission provider mounted) is not a denial: the server enforces regardless, so a
168
+ client-side refusal on missing data cannot protect anything and can only break a permitted user. A
169
+ *reported* empty grant gates strictly. Hosts push the session's capabilities in with the new
170
+ `setSystemCapabilities`; `ObjectView` wires it from `usePermissions()`.
171
+
172
+ The refusal is also **said out loud**. `ObjectView`'s persist path previously swallowed every failure
173
+ into `console.error`, which for a debounced toggle whose UI has already moved would have left the
174
+ operator looking at a density they did not get; a denied write now raises a toast. And the "View
175
+ settings" popover — where density and field visibility are actually changed — now states the scope
176
+ before the operator acts: *"Grouping, color, density, and visible fields. Applies to everyone who
177
+ uses this view."*, translated in all ten packs.
178
+ - 94e2fa7: `MetadataClient.layered()` validates the ADR-0010 protection envelope against the
179
+ producer's own schema at the boundary, instead of casting ten wire fields through
180
+ unchecked (objectui#5676, triage adjudication 2026-08-22).
181
+
182
+ The envelope arrived by ten `as` assertions over a raw `res.json()` body — no parse, no
183
+ allowlist, no default. The consumer that reads it opens the metadata lock banner on
184
+ `layered?.lock && layered.lock !== 'none'`, true for **any** non-`none` value, so a server
185
+ sending a lock state this console had never heard of opened the amber box, drew the padlock
186
+ and the border, and rendered an empty title. No fifth state ever had to be added to this
187
+ repo for that to happen: a union types what this repo writes and constrains nothing about
188
+ what a server sends.
189
+
190
+ The boundary now runs `GetMetaItemLayeredResponseSchema.safeParse`. On the conforming path
191
+ every value is the producer's schema output and the ten assertions are gone. `safeParse`
192
+ and never `parse`: a metadata console that rejected every dialect it had not been compiled
193
+ against would answer a newer server with a blank page, which is strictly worse than the
194
+ wrong render being fixed. Values the schema rejects are still **forwarded** — dropping them
195
+ would be that same refused rejection wearing different clothes — and are named in a new
196
+ optional `MetadataLayered._unrecognized`, absent whenever everything parsed. This extends
197
+ to the whole envelope the "pass through and label" treatment objectui#5672 chose for `lock`
198
+ alone; the banner's existing unrecognised-token title is unchanged and needed no edit.
199
+
200
+ The labelling is per field, which is the part that makes it a degrade rather than a subtler
201
+ version of the same bug. Measured on the installed spec (17.2.0):
202
+ `GetMetaItemLayeredResponseSchema.safeParse(body)` is all-or-nothing — one unknown `lock`
203
+ returns `success: false` with `data` undefined — so the failure branch re-checks each key
204
+ against that schema's own `shape[key]`, where only the offending field fails and the other
205
+ six still arrive typed. Absence is never "unrecognised": the four resolved verdicts are
206
+ required upstream on this path, so a pre-ADR-0010 backend takes the failure branch with
207
+ nothing flagged and behaves exactly as before.
208
+
209
+ One consequence of the same ruling, fixed alongside because it defeats it: a 200 answer
210
+ whose body was a bare JSON string or number **rejected** the promise with a
211
+ `TypeError: Cannot use 'in' operator`, from the envelope-detection guard's bare truthiness
212
+ check. A malformed body must degrade, never throw.
213
+ - f07b976: `aggregate()`'s spec-shape branch now REFUSES an unlowered `where` instead of
214
+ posting it (objectui#6825, maintainer ruling 2026-08-30 — option A).
215
+
216
+ **Breaking for callers that were already broken, so read this if you call
217
+ `aggregate()` with a `where`.** `aggregate()` has two branches. The analytics
218
+ branch takes `filter` and lowers a rule-shaped array before the wire (#6302).
219
+ The spec-shape branch — entered when `params` carries an array `groupBy`, an
220
+ array `aggregations`, or ANY `where` key — takes `where` and posts it to
221
+ `POST /data/:object/query` verbatim. It never lowered, and it still does not:
222
+ it now says so.
223
+
224
+ **What now throws that previously went through.** A `where` that is an ARRAY
225
+ the spec's own `isFilterAST` gate rejects — an unlowered
226
+ `[{ field, operator, value }, ...]` above all, plus the infix join dialect
227
+ (`[condA, 'or', condB]`), a tuple whose operator is outside the AST vocabulary,
228
+ `['and']` with nothing to join, and an element that is not a condition. The
229
+ throw is an `UnloweredAggregateWhereError` (exported), carrying the same
230
+ `code: 'INVALID_FILTER'` / `httpStatus: 400` pair as `MalformedFilterError`, so
231
+ `isMalformedFilterError()` recognises it and a failed widget renders "this
232
+ filter is malformed" rather than "check your connection".
233
+
234
+ **This adds no new failure — it relocates one.** Every shape now refused is one
235
+ the receiving engine already refused (`is not a filter`, 400 `INVALID_FILTER`,
236
+ before the store is touched). What changed is WHERE you find out and whether you
237
+ can act on it: previously the predicate was lost on the wire — or dropped
238
+ outright, leaving a chart rendering confident, wrong numbers with no signal to
239
+ its author. The refusal is now raised at the producer, names the value it
240
+ received, names the shape expected and where the spec declares it, and says
241
+ nothing was sent.
242
+
243
+ **What an affected caller should send instead.** Lower the rules to a filter AST
244
+ BEFORE calling `aggregate()`:
245
+
246
+ ```diff
247
+ - adapter.aggregate('opportunity', {
248
+ - groupBy: ['stage'], aggregations: [...],
249
+ - where: [{ field: 'stage', operator: 'equals', value: 'won' }],
250
+ - })
251
+ + adapter.aggregate('opportunity', {
252
+ + groupBy: ['stage'], aggregations: [...],
253
+ + where: ['stage', '=', 'won'],
254
+ + })
255
+ ```
256
+
257
+ Or keep using the analytics branch, which lowers for you: pass the rules under
258
+ `filter` with the legacy `field` / `function` / `groupBy` params.
259
+
260
+ **Unchanged, deliberately.** The analytics branch still lowers `filter` exactly
261
+ as #6302 left it. On the spec-shape branch a `FilterCondition` object
262
+ (`{ stage: 'won' }` — what `QuerySchema.where` actually declares), an empty
263
+ array (`[]` is "no filter", and the engine agrees), and every already-valid
264
+ filter AST all reach `client.data.query` byte-unchanged.
265
+ - 503cd8b: `aggregate()`'s spec-shape branch now REFUSES the analytics branch's `filter` /
266
+ `field` / `function` instead of dropping them (extending the
267
+ maintainer ruling of 2026-08-30 on objectui#6825 — option A, refuse at the
268
+ producer).
269
+
270
+ **Breaking for callers that were already broken, so read this if you call
271
+ `aggregate()` with an ARRAY `groupBy`.** The spec-shape branch — entered when
272
+ `params` carries an array `groupBy`, an array `aggregations`, or any `where`
273
+ key — builds its request from exactly four keys: `groupBy`, `aggregations`,
274
+ `where`, `limit`. `filter`, `field` and `function` are the OTHER branch's
275
+ parameters, and they were neither read, nor refused, nor warned about: they
276
+ were simply absent from the body that went to `POST /data/:object/query`.
277
+
278
+ **Why that was worse than the `where` half #6825 fixed.** `field` + `function`
279
+ are the analytics branch's whole measure, and this branch takes a measure only
280
+ out of `aggregations`. So the legacy shape `{ field, function, groupBy, filter }`
281
+ whose `groupBy` happened to be an ARRAY produced a query carrying a `groupBy`
282
+ and **no aggregations at all** — a grouping with no measure — with the author's
283
+ filter gone as well. The chart rendered, the numbers were wrong, and there was
284
+ nothing on screen or on the wire to look at.
285
+
286
+ **What now throws that previously went through.** A spec-shape call carrying a
287
+ non-nullish `filter`, `field` or `function` throws the new
288
+ `AnalyticsKeysOnSpecShapeError`. It carries the `INVALID_FILTER` / 400 pair its
289
+ siblings carry (so `isMalformedFilterError()` recognises it and a failed widget
290
+ renders "this filter is malformed" rather than "check your connection"), plus
291
+ `keys` — the offending key names — and `received`, what each one carried. The
292
+ message names each key, says what its spec-shape equivalent is, states which
293
+ `looksLikeSpecShape` disjunct put the call on this branch, and says outright
294
+ when the resulting query would have had no measure. Nothing is sent to the
295
+ server, so no unfiltered numbers come back.
296
+
297
+ **What is deliberately NOT refused.** The legacy analytics shape (a STRING
298
+ `groupBy`) is untouched and still lowers `filter` and still fuses
299
+ `field` + `function` into its measure — the refusal lives inside the spec-shape
300
+ branch only. A key that is present but nullish (`filter: undefined`) carries
301
+ nothing to drop and passes, so params built by spreading possibly-absent
302
+ authored values keep working. And keys outside those three (`orderBy`, a future
303
+ spec key, any host extra) are not refused: the gate names three keys, and only
304
+ those.
305
+
306
+ **Migration.** Pick one shape per call. Spec-shape: `{ groupBy: GroupByNode[],
307
+ aggregations: AggregationNode[], where?, limit? }`, with `where` already lowered.
308
+ Analytics: `{ field, function, groupBy: string, filter? }`, which lowers `filter`
309
+ for you. `AnalyticsKeysOnSpecShapeError` is exported from
310
+ `@object-ui/data-objectstack`.
311
+ - ce986aa: Studio's "publish whole app" reports the runtime authoring gate's per-draft
312
+ advisories (server half objectstack#9343).
313
+
314
+ `POST /packages/:id/publish-drafts` began answering `advisories` on each
315
+ `published[]` element when objectstack#9343 landed, but the author publishing a
316
+ whole app was still told nothing: both client call sites bypassed the data-layer
317
+ seam — a bare `fetch` in `usePublishAllDrafts` and the page-private `apiJson` in
318
+ `PackagesPage`, whose declared response type held two counts and `failed[]`, with
319
+ no `published[]` at all. The same button's own client-side capability lint was
320
+ raising a toast the whole time, so a finding from the server was the one thing
321
+ that could not reach the person pressing it.
322
+
323
+ - `MetadataClient.publishPackageDrafts(packageId)` expresses the route and emits
324
+ one advisory event per advised `published[]` element — each naming that
325
+ element's own `type` / `name` — into the sink, event and renderer the save and
326
+ single-item publish doors already use. Both call sites go through it.
327
+ - The batch door reports `door: 'publish'` rather than a third discriminator
328
+ value: every item the event names really was published, and the renderer's
329
+ only door-dependent output is that verb. The per-item identity the author
330
+ needs rides `type` / `name`, one event per item.
331
+ - It renders only what the server sent where `PublishPackageDraftsResponseSchema`
332
+ declares it. A half-shaped finding, an element that cannot name its item, and a
333
+ top-level `advisories` the ruled shape does not put there all report nothing —
334
+ pinned, alongside the presence, in `metadata-client.publishAdvisories.test.ts`,
335
+ whose absence pin this flips.
336
+ - `publishPackageDrafts` returns the batch body derived from the spec schema, so
337
+ a caller reading `failed[]` / `publishedCount` reads a declared shape. Non-2xx
338
+ raises the usual `MetadataError`; the 2xx batch verdict stays the caller's to
339
+ judge, because `success: false` is not a refusal on this route.
340
+ - 512bc90: `aggregate()` reads the analytics answer in ONE spelling: `rows` on the `AnalyticsResult` that `client.analytics.query` resolves to (objectui#7028). The `{ success, data: { rows } }` envelope is no longer accepted at this site.
341
+
342
+ `@objectstack/client` 17.3.0 converged `analytics.query` on `unwrapResponse` (objectstack#13079): the method resolves to the payload, and the caller reads `result.rows`. The maintainer's ruling on that card ordered this adapter's tolerant row ladder tightened in the same wave. The ladder read five spellings (a bare array, `rows`, `data` as an array, `data.data.rows` and `results`) and answered any other shape with `[]`, which a chart renders as "no data" and a KPI as a confident zero.
343
+
344
+ What changes:
345
+
346
+ - **Only `rows` is read.** Any other value now throws the new `AnalyticsResultShapeError` (`code: 'ANALYTICS_RESULT_SHAPE_INVALID'`, with `envelope: true` when the value is the pre-17.3.0 envelope). It is not answered by the client-side `find()` fallback, because analytics did answer: degrading would put plausible numbers from a different code path over a contract violation.
347
+ - **Minimum version: `@objectstack/client` 17.3.0.** The dependency range moves from `^17.0.0` to `^17.3.0`, the first release that unwraps this method. A client older than that resolved `analytics.query` to the whole envelope, which this site no longer reads. The console's own `@objectstack/client` range moves with it, so the workspace declares one client floor.
348
+ - **No server is dropped.** The envelope has only ever been a client-side question: every `@objectstack` 17.x server answers `POST /analytics/query` with the same `{ success, data }` envelope, and the client decides whether it is unwrapped. The server version is not the variable here.
349
+ - The measure-missing fallback is unchanged: rows that come back without the requested measure are still re-aggregated client-side from a scoped `find()`.
350
+
351
+ The objectui#7122 changeset recorded these branches as kept "rather than deleted" and left the compatibility question open. The objectstack#13079 ruling answers it, and this change removes them.
352
+ - e2b3826: Grid grouping is now **server-side** (objectui#7189, maintainer ruling A): the
353
+ set of groups and every number in a group header — the count and any per-group
354
+ aggregation — come from the query, and the rows inside a group are paged by the
355
+ server.
356
+
357
+ Before, a grouped grid fetched one window and bucketed it in the browser, so
358
+ both the group set and every header number were properties of the page. On a
359
+ store of 186 records over five business units sized 86 / 61 / 31 / 7 / 1 with a
360
+ 100-row page it rendered **two** headers (86, 14) when the rows were stored
361
+ contiguously and five page slices (31 / 31 / 30 / 7 / 1) when interleaved — and
362
+ the records past the window were not on a second page, they were unreachable.
363
+ The same store now renders five headers reading 86 / 61 / 31 / 7 / 1 in either
364
+ order, and every record is reachable through its group's own pager.
365
+
366
+ - **`@object-ui/types`** — `DataSource` gains an optional
367
+ `queryGroupHeaders(resource, query)`: it answers the group header query
368
+ `@objectstack/spec/ui`'s `compileListViewGroupQuery` compiles (an
369
+ `EngineAggregateOptions`) with one header row per group. Presence is the
370
+ capability; a source that cannot answer it leaves it undeclared.
371
+ - **`@object-ui/data-objectstack`** — `ObjectStackAdapter.queryGroupHeaders`
372
+ posts the compiled query verbatim to the existing `POST /data/:object/query`
373
+ door and answers its `records`. It never degrades: a refusal throws, and a
374
+ body without `records` is refused rather than read as "no groups".
375
+ - **`@object-ui/plugin-grid`** — a grouped grid that fetches its own rows from a
376
+ source declaring `queryGroupHeaders` asks for its headers (one compiled query
377
+ per grouping level) and pages each **open** group's rows with the group's
378
+ own compiled row query (`compileListViewGroupRowsQuery`: the view's filter AND
379
+ the group key, `limit` / `offset` per group); a collapsed group costs no row
380
+ query, `aggregations` are the header query's numbers, and a reference-typed
381
+ grouping key is labelled from the referenced record. Such a grid's column
382
+ sort is the server's, and it no longer shows the `Partial` marker: its counts
383
+ are the query's own. Rows handed in whole are still grouped in the browser
384
+ (exact), and a source with no header query still groups the page it fetched
385
+ and still marks it partial.
386
+ - **`@object-ui/plugin-list`** — `ListView` hands a grouped grid its own fetch,
387
+ with the view's effective filter, when the data source can answer the header
388
+ query, and draws no record-count bar over it. While a toolbar search is
389
+ active it keeps hosting the rows as before: `$search` has no counterpart on
390
+ the header query.
391
+
392
+ **Breaking semantics (declared `minor` per this repo's version policy):**
393
+
394
+ - A grouped grid over a capable source sends different requests: one header
395
+ query per grouping level plus one row query per open group, instead of one
396
+ window. A host that counted or mocked the single window sees the new shape.
397
+ - `useGroupedData` takes an optional fifth argument (the server's header rows).
398
+ - `GroupEntry` gains two REQUIRED members: `count` (the group's size — use it
399
+ instead of `rows.length`, which is empty on a server-grouped grid) and
400
+ `keyValues`. Code that only READS a `GroupEntry` is unaffected; code that
401
+ CONSTRUCTS a `GroupEntry` literal no longer type-checks until it supplies
402
+ both.
403
+ - `AggregationResult.value` is `number | null`, `null` being what the server's
404
+ aggregate answered over no values (rendered as a dash, never invented as 0).
405
+ - `@object-ui/plugin-grid` and `@object-ui/data-objectstack` now require
406
+ `@objectstack/spec` `^17.4.0`, the first release that ships the group-query
407
+ compilers.
408
+
409
+ This supersedes the last paragraph of the pending
410
+ `7189-grouped-grid-partial-disclosure` changeset, which said server-side
411
+ grouping was not built: it now is, and the `Partial` marker described there
412
+ remains only for the data sources that cannot answer the header query.
413
+
414
+ ⚠️ **Dated note, 2026-09-28 — a source with no header query now refuses
415
+ grouping — objectui#10881.** Later in this same release two sentences above
416
+ stopped describing the grid: that a source with no header query still groups
417
+ the page it fetched and still marks it partial, and that the `Partial` marker
418
+ remains for the data sources that cannot answer the header query. Maintainer
419
+ ruling F retired both. Over such a source a grouped grid that fetches its own
420
+ rows, and a `ListView` hosting a grouped grid, refuse grouping with an error
421
+ naming `queryGroupHeaders`, and the marker is gone everywhere. Rows handed in
422
+ whole are still grouped in the browser. The rest of this entry is kept as the
423
+ reading of this change; the `10881-grouping-needs-header-query` entry states
424
+ what ships.
425
+ - 6a99bb2: `createObjectStackAdapter` declares the adapter it returns, not the shared `DataSource`
426
+ interface (objectui#7323).
427
+
428
+ The factory returned `new ObjectStackAdapter(config)` while declaring `DataSource<T>`.
429
+ A wider value is assignable to a narrower annotation, so nothing ever failed to compile
430
+ — the loss was entirely on the reading side. Measured against the shipped
431
+ `dist/index.d.ts` with the doc-snippet gate's own compiler options, nine reads through
432
+ `ReturnType<typeof createObjectStackAdapter>` failed with TS2339: `getClient`,
433
+ `getCacheStats`, `invalidateCache`, `clearCache`, `getConnectionState`, `isConnected`,
434
+ `onConnectionStateChange`, `onBatchProgress` and `setSystemCapabilities`. Eight of those
435
+ nine reads are on this package's README API Reference list, and four whole README
436
+ sections are built on them; the ninth measured read is the one the factory's own JSDoc
437
+ links to (`[ADR-0066] See {@link ObjectStackAdapter.setSystemCapabilities}`). The README
438
+ list is itself **nine** adapter-only members, not eight — `connect()` is adapter-only
439
+ too and was documented all along; it simply was not one of the reads the card's
440
+ reproduction measured. So the file's own doc comment pointed the reader at a method its
441
+ declared return hid, and the two documented ways to obtain the same object — the factory
442
+ and `new ObjectStackAdapter(…)` — handed back different type surfaces.
443
+
444
+ **What the declared return now is: the whole class, not those nine reads.** The nine
445
+ above are what the reproduction measured, not the size of this change. The factory's
446
+ declared return is now `ObjectStackAdapter<T>` itself, so **every public member of the
447
+ class** is part of what the factory promises. Against `DataSource` that is **20**
448
+ members, not nine — `tsc`-computed as
449
+ `Exclude<keyof ObjectStackAdapter<unknown>, keyof DataSource<unknown>>`: `clearCache`,
450
+ `connect`, `getCacheStats`, `getCached`, `getClient`, `getConnectionState`,
451
+ `getDiscovery`, `getItems`, `invalidateCache`, `invalidateViewKeys`, `isConnected`,
452
+ `listImportMappings`, `onBatchProgress`, `onConnectionStateChange`, `onSaveAdvisory`,
453
+ `onWriteWarning`, `probeAppAccess`, `queryDataset`, `setSystemCapabilities`,
454
+ `updateDashboard`. The eleven past the documented nine were already in the shipped class
455
+ type — none is `@internal` or `@deprecated`, `stripInternal` is not set, and all were
456
+ already reachable through `new ObjectStackAdapter(…)` and through every
457
+ `ObjectStackAdapter`-typed seam in `@object-ui/react` and `app-shell` — so what widens
458
+ here is what the **factory declares**, not what the package ships. Two are escape-hatch
459
+ shaped and worth knowing before building on them: `getCached(key)` is a raw cache read,
460
+ and `getDiscovery()` reaches an internal property of the underlying `ObjectStackClient`.
461
+
462
+ **Branch taken: A (widen the factory's declared return), and why.** The card offered
463
+ three. B — moving caching, connection state and batch progress onto `DataSource` — was
464
+ rejected because those are this adapter's concerns, not every data source's; every other
465
+ `DataSource` implementation would then declare members it does not have. C — documenting
466
+ a cast — teaches a cast around a declaration that is merely narrower than the value,
467
+ which is the opposite of `declared = enforced`. A is one line and makes declared match
468
+ shipped for every documented member at once.
469
+
470
+ Two questions decided the shape and both were answered from the code before the diff.
471
+ `ObjectStackAdapter` was **already** exported from the package's only entry
472
+ (`src/index.ts`, tsup's single entry; the class is in the shipped `dist/index.d.ts`
473
+ export list, two pin tests assert the exported spelling, and `apps/console` re-exports it
474
+ by name) — so widening the return exports nothing by implication. And the narrow return
475
+ was **not** a deliberate swappability guarantee: no comment, ADR or test pinned it, and
476
+ the commit that added `autoReconnect` / `maxReconnectAttempts` / `reconnectDelay` to the
477
+ factory's own config bag left the members that observe those features off the factory's
478
+ declared return in the same change.
479
+
480
+ **One caller shape breaks: a structural stand-in for the factory's return.** A
481
+ hand-written object literal annotated `ReturnType<typeof createObjectStackAdapter>` no
482
+ longer satisfies that type, because it is now a class with private members (TS2740) —
483
+ annotate such a fake as `DataSource` instead, which is what it was standing in for.
484
+ Nothing else moves: a wider return is assignable to the narrower annotation, so
485
+ `const ds: DataSource = createObjectStackAdapter(…)` keeps compiling and keeps giving
486
+ the narrow surface to anyone who wants it.
487
+
488
+ The README's note saying the page could not yet teach the factory's shape is removed, and
489
+ the four sections built on the adapter-only members (Metadata Caching, Connection State
490
+ Monitoring, Batch Operation Progress, Troubleshooting → Cache Issues) now continue from
491
+ Basic Setup's `createObjectStackAdapter(…)` call instead of declaring the class by hand.
492
+ The docs-site page `content/docs/utilities/data-objectstack.mdx` is corrected the same
493
+ way: its prose, its factory signature fragment and its "hold the class type to reach
494
+ these" section described the old narrow return, and its Mutations and Troubleshooting
495
+ examples told the reader to construct the class by hand to reach members the factory now
496
+ declares. `src/adapterFactoryReturn.types.test.ts` pins the card's TS2339 reproduction
497
+ inverted,
498
+ with two controls: the adapter-only members stay absent from `DataSource` (fires on
499
+ option B), and the widened return stays assignable to `DataSource` (swappability kept).
500
+ - a915064: `listImportMappings` no longer renders a refused door as "no mapping is registered"
501
+ (objectui#7741).
502
+
503
+ `ObjectStackAdapter.listImportMappings` degrades every failure to an empty list, and the
504
+ import wizard hides its saved-mapping selector on an empty list. So "the server served
505
+ zero mappings" and "the server refused, or broke" produced the identical UI on every
506
+ deployment — the feature simply absent — with a `console.warn` as the only
507
+ discriminator, in the browser console, with nothing pointing at it. That silence did not
508
+ merely hide a fault: it produced a confident WRONG diagnosis in a careful reporter
509
+ (objectstack#14026 was filed, routed and worked by two seats against a wizard that had
510
+ been correct since `@object-ui/data-objectstack@17.1.0`).
511
+
512
+ **The empty-list return is unchanged.** `listImportMappings` still answers
513
+ `Promise<any[]>` and still never throws, on every arm including the loud ones — this is
514
+ a channel added ALONGSIDE that contract, not a change to it.
515
+
516
+ - **New: `ObjectStackAdapter.onMetadataReadWarning(cb)`** — a subscribe/unsubscribe
517
+ channel, sibling in shape to `onWriteWarning` and `onSaveAdvisory`. It fires when a
518
+ metadata read failed in a way that is NOT the supported "this deployment does not
519
+ serve that kind" shape, carrying `MetadataReadWarningEvent`: which read it was, the
520
+ object, whether the server `refused` this caller or the answer was `unreadable`, and
521
+ the server's own ADR-0112 code, HTTP status and message.
522
+ - **New: `classifyImportMappingsFailure(err)`** and `ImportMappingsFailureKind`, exported
523
+ so a consumer can apply the same verdict. It reads the ERROR — the ADR-0112 `code`
524
+ first, the status only where no code was declared — and never "is the result an empty
525
+ array", which is what both conditions produce and so can never tell them apart.
526
+ - **The older-server case stays quiet.** A deployment that does not serve the `mapping`
527
+ kind (404/501 with no route, `ROUTE_NOT_FOUND`, `NOT_IMPLEMENTED`, or the metadata list
528
+ door's 400 `INVALID_REQUEST`) still degrades to an empty list with no selector and no
529
+ event. That is a real, supported deployment shape and it must not become a visible
530
+ fault.
531
+ - **The console now says so.** `AdapterProvider` subscribes to the new channel and
532
+ renders a warning toast naming the object, the remedy and the server's own words, so a
533
+ user without devtools open can tell "there are none" from "we could not find out".
534
+ Three new `console.importMappings*` keys ship in all ten locale packs.
535
+
536
+ This applies framework objectstack#13906 decision 1 option A — *a thing that could not be READ is
537
+ not a thing that is ABSENT* — at this seam. It is an already-adopted discrimination, not
538
+ a new principle.
539
+ - 53ded82: Array filters on analytics aggregates were posted un-lowered and refused by the runtime with 400; they are now lowered to the canonical `FilterCondition` before the wire.
540
+
541
+ An `element:number` or an `object-metric` whose filter is authored as an array (`[{ field, operator, value }, ...]`, a comparison tuple, or an `and`/`or` group) reached `POST /analytics/query` as an array. That route parses the body with `AnalyticsQueryRequestSchema` first, and its `where` is a `FilterCondition`, so the widget answered `400 Invalid AnalyticsQuery body: where: ...` instead of its number — leaving the MongoDB-style record as the only authoring form that still worked.
542
+
543
+ `aggregate()` now lowers the array through `parseFilterAST`, the single sink `@objectstack/spec` names for turning a `FilterArray` into a `FilterCondition`, so the posted `where` is the shape the wire declares. An empty array posts no `where` at all, a record-shaped filter is unchanged, and an array the sink cannot lower — an infix join such as `[condA, 'or', condB]` — is refused with this adapter's `INVALID_FILTER` / 400 error rather than posted or silently dropped into an unfiltered aggregate.
544
+ - 9662aca: Apply the object-metadata write invariant at the write DOORS instead of at the writers.
545
+
546
+ objectui#7714 ruled that a half-filled relationship stays client-side and the PUT body never
547
+ carries one without a non-empty `reference`, and implemented that ruling by naming the two
548
+ writers it knew of. The identical defect was then reproduced on a third (fixed in `9073cf018`); a
549
+ sweep found nine more. The doors — the three places in this repo that actually PUT `/meta/:type/:name` —
550
+ now apply the invariant themselves, so every writer is covered without any list of writers
551
+ existing anywhere, and a new door is caught by a gate that derives the door set from the
552
+ tree rather than restating it.
553
+
554
+ Behaviour change for consumers: `MetadataClient.save('object', …)` now throws BEFORE issuing
555
+ the request when the body carries a relationship field with a missing, empty or whitespace-only
556
+ `reference`. The same document is refused by the server with a 422 on `fields.NAME.reference`,
557
+ so nothing that previously succeeded now fails — the refusal moves earlier, names the field,
558
+ and leaves the draft in the client instead of wedging every later save of that object. Writes
559
+ of every other metadata type are untouched.
560
+
561
+ New export from `@object-ui/data-objectstack`: `assertObjectMetadataWritable`,
562
+ `RELATIONSHIP_TYPES_REQUIRING_REFERENCE` and `OBJECT_METADATA_TYPE`.
563
+ - abf710d: Lower the TRUE-identity filter combinators to "no constraint" instead of handing the
564
+ caller's own object back (objectui#8770).
565
+
566
+ `convertFiltersToAST({ $and: [] })`, `{ $or: [{}] }` and `{ $and: [{}] }` returned the
567
+ INPUT OBJECT unchanged. `lowerLogicalGroup` correctly answers `undefined` for a group
568
+ that reduces to the TRUE identity — objectstack#5322 rules all three "every row", and a
569
+ childless `['and']` would be `isFilterAST` FALSE — but when such a group was the only
570
+ thing in the filter, that `undefined` fell through to the general tail
571
+ (`if (conditions.length === 0) return filter`) and the group reappeared one level up, in
572
+ the `$` dialect, in the slot the AST occupies. The same function already lowered the
573
+ fourth identity, `{ $or: [] }`, correctly, so this was an internal inconsistency rather
574
+ than an open question; the consumer half was settled by objectui#8513.
575
+
576
+ **This widens what those three filters return, and that is the point.** Measured against
577
+ `@objectstack/spec` 17.4.0 and `@objectstack/client` 17.4.0 before the change: the
578
+ returned object is not sent as a filter and refused — `client.data.find()` tests the
579
+ value with `isFilterAST` and its else branch spreads a plain object's entries as query
580
+ parameters, so `{ $and: [] }` left as `?$and=` with **no `filter` parameter at all**, and
581
+ the server answered `400 UNSUPPORTED_QUERY_PARAM` for the unknown `$`-prefixed
582
+ parameter. A filter whose ruled answer is EVERY ROW was a **failed list**, not a narrowed
583
+ one — so nothing could have been relying on it to scope data. On the sibling
584
+ `$expand` / `$search` route the same object travelled as `filter={"$and":[]}`, which the
585
+ server accepts as a `FilterCondition` and already answers with every row; the two routes
586
+ disagreed about one filter and now agree.
587
+
588
+ **`@object-ui/core`.** `convertFiltersToAST`'s declared return type gains `undefined`,
589
+ which is what `toFilterNode`, `mergeFilterNodes` and `data-objectstack`'s
590
+ `translateFilterToAST` already mean by "no filter, skip the slot". Every call site
591
+ already acted on it. The fold is scoped to a filter whose EVERY key is such a group:
592
+ the same tail also serves `{}`, an all-null filter and an empty operator map, and those
593
+ keep the object they always returned — a null-valued key is this converter's own
594
+ tolerance rather than a ruled identity, and the object it hands back reaches the server
595
+ as a REAL `a IS NULL` predicate on the `$expand` route, so folding it in would return
596
+ more rows on a path the ruling said nothing about.
597
+
598
+ `{ $or: [] }` is untouched: FALSE is not "no constraint", the AST has no contradiction
599
+ literal, and its `['$or', '=', []]` leaf answers FALSE at both consumers.
600
+
601
+ **`@object-ui/data-objectstack`.** `convertQueryParams` skips the `filters` slot when the
602
+ lowering answers `undefined`, the same answer the raw-GET route's
603
+ `if (translated !== undefined)` already gave, so the two `find()` routes cannot disagree
604
+ about one filter.
605
+
606
+ **Migration.** A TypeScript caller that stored `convertFiltersToAST(...)` in a
607
+ `FilterNode | Record<string, any>` slot must widen it with `| undefined` and skip the
608
+ filter when it is absent — the same handling `toFilterNode` has always needed. At
609
+ runtime, a filter that is nothing but TRUE-identity combinators now returns every row
610
+ (what objectstack#5322 rules) instead of failing the request.
611
+
612
+ ⚠️ **Dated note, 2026-09-27 — an empty operator map no longer keeps the object — objectui#9164.**
613
+ Later in this same release `convertFiltersToAST` refuses an empty operator map that is all
614
+ a filter says (`{ a: {} }`, `{ a: {}, b: undefined }`, `{ $and: [], a: {} }`) with a
615
+ `FilterOperatorError` instead of returning the object, so the sentence above that it
616
+ "keeps the object" no longer holds for it; an all-null filter had already moved to
617
+ `undefined` with objectui#9020. Only `{}` still comes back as itself. The rest of this
618
+ entry is kept as the reading of this change; the objectui#9164 entry states what an empty
619
+ operator map now answers.
620
+ - 2bf34f7: The console's "app not available" screen now says what it measured, and the by-name app
621
+ probe stopped folding four answers into one (objectui#9262).
622
+
623
+ **BREAKING for consumers of the published `AppAccessVerdict` type.** `probeAppAccess`
624
+ widens by two members ruled by the maintainer: `granted | denied | unknown` becomes
625
+ `granted | denied | not_found | unreachable | unknown`. A consumer that exhaustively
626
+ switches on the verdict gains two cases. The meaning of `unknown` also narrows — it no
627
+ longer means "an absent app, an unreachable server, or an adapter that cannot ask", it
628
+ means only that nothing was asked (a host DataSource without this probe). Code that read
629
+ `unknown` as a positive absence must move to `not_found`.
630
+
631
+ **What users see.** The sentence `This app is not available yet — it may still be
632
+ publishing. Try again in a moment.` is retired from all ten locale packs. It was asserted
633
+ for seven distinct causes, only one of which is a publish, and it is the screen an author
634
+ lands on after any of them — the same defect objectui#4252 split `denied` off this screen
635
+ for. In its place: `This app can't be opened` for a measured absence, `Couldn't reach the
636
+ server` for a probe that got no answer, the unchanged denial screen for a permission
637
+ refusal, and a neutral `App not available` when nothing was measured. All but the denial
638
+ keep the Retry button, because all but the denial can change on their own. A genuine
639
+ post-publish lag reads as an absence, and the one forced metadata refresh that gives that
640
+ lag its chance to resolve is unchanged.
641
+
642
+ **Measured before the branches were written.** On a real server the by-name route answers
643
+ `200` with the declared envelope MINUS its `item` for a name that does not exist — not the
644
+ `404` this code assumed — so `probeAppAccess` returned `granted` for every typo, every
645
+ never-created app and every unpublished draft. `granted` fell through to the same screen as
646
+ `unknown`, which is why nothing showed. Absence is therefore read from the envelope as well
647
+ as from the `404` code; a `not_found` branch keyed on the status alone would have been dead
648
+ for the commonest case.
649
+ - b253c4e: The import wizard's "Download template" button now downloads the server's import template, an Excel workbook, instead of building a CSV of every field (objectui#9600).
650
+
651
+ The old CSV listed every field the wizard was handed, including the system and read-only columns every object carries (created by, owner, and so on). The server strips those columns on import without an error, so a user who filled them in lost that data silently. The server's template (`GET /api/v1/data/:object/export?template=true`, objectstack 17.6.0) lists only the columns this caller can import. It marks required ones with `*`, adds dropdowns for select and boolean columns, and includes an instructions sheet.
652
+
653
+ - **`@object-ui/types`: new optional `DataSource.downloadImportTemplate(resource)`**, resolving to the template file as a `Blob`. Additive. When a data source lacks it, no template is offered: there is no client-side fallback.
654
+ - **`@object-ui/data-objectstack`: `ObjectStackAdapter.downloadImportTemplate(resource)`** sends `template=true` on the export route, on the same request path as `exportDownload`. When the client has a locale set (`getClient().setLocale`), the request carries it as `Accept-Language`, the way the import request does. The template's labels are written in the request's locale, and the import accepts a translated label only in its own request's locale, so the two requests must match.
655
+ - **`@object-ui/plugin-grid`: `ImportWizard` offers the template** when its data source has `downloadImportTemplate` and the user can create records of the object (`usePermissions().can(object, 'create')`, the server's own gate). A 405 (object not open for import), a 403 (no create permission) and any other failure each show a message on the upload step, and no file is saved. The client-built CSV is deleted, with its helpers (`buildImportTemplateCsv`, `exampleForField`, `firstOptionValue`, `downloadTextFile`). The column mapping step is unchanged.
656
+ - **`@object-ui/app-shell`**: the identity-import data source (`sys_user`) withholds `downloadImportTemplate`, because the server's template describes the generic import door, not the identity pipeline.
657
+ - **`@object-ui/i18n`**: `grid.import.downloadTemplateHint` describes the Excel template in all ten packs, and `grid.import.templateDownloadFailed` and `grid.import.templateNotPermitted` are new.
658
+ - d5c1f52: `classifyAnalyticsFailure` now reads a 400 as a refusal of the query body we
659
+ sent regardless of which ADR-0112 `code` it carries, so `aggregate()` no
660
+ longer answers a rejected filter with client-side numbers from a different
661
+ door (objectui#7755).
662
+
663
+ Before this fix, only 400 `VALIDATION_FAILED` (and a code-less 400) threw
664
+ `AnalyticsQueryRejectedError`. Any OTHER coded 400 — `service-analytics` ships
665
+ its own 400 `INVALID_FILTER` on a filter shape it refuses — matched none of
666
+ `classifyAnalyticsFailure`'s branches and fell through to `unknown`, which
667
+ `aggregate()`'s catch has no arm for, so it silently degraded to
668
+ `aggregateViaFind`: a re-read through `find()`'s `$filter` query-string
669
+ contract, which accepts array shapes the analytics request body does not. A
670
+ filter the analytics route refused could still be answered — with a
671
+ plausible, wrong number, and no sign the request had a defect.
672
+
673
+ The fix is a floor UNDER the existing code branches, not a replacement for
674
+ them: `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` still win `not-installed`,
675
+ `VALIDATION_FAILED` / `UNAUTHENTICATED` / `CUBE_NOT_FOUND` still win their own
676
+ outcomes first (objectui#5721). Only a 400 that none of those four already
677
+ claimed now falls to the new floor instead of past it. An unmatched NON-400
678
+ coded error (e.g. a coded 5xx) is unaffected and keeps degrading exactly as
679
+ before — this fix is scoped to the 400 case only.
680
+ - b2065e7: Classify `/analytics/query` failures by their ADR-0112 `code` rather than their HTTP status, so a chart is no longer answered from a different code path behind the wrong explanation.
681
+
682
+ `classifyAnalyticsFailure` tested `status === 404 || status === 501` before the code operands on the same line, so the status short-circuited every one of them: any 404 on this face was classified "the analytics capability is not installed" whatever code it carried, and `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` were unreachable for the conditions they name. Three unrelated conditions answer 404 on this url, so the status cannot tell them apart — the `code` is the contract.
683
+
684
+ Two conditions change behaviour:
685
+
686
+ - **404 `CUBE_NOT_FOUND`** (a misspelled or unregistered cube — an authoring mistake) now **throws** the server's own error verbatim, keeping `code` and the producer's repair instructions. It previously warned "install `@objectstack/service-analytics`" and silently degraded to `find()` + client-side aggregation — which cannot answer it anyway, because the fallback re-reads the same name through `/data`, where an unregistered object is a 404 `OBJECT_NOT_FOUND`.
687
+ - **401 `UNAUTHENTICATED`** (an anonymous or lapsed session) now **throws** `AnalyticsUnauthenticatedError` instead of degrading silently behind a `find()` that is about to be refused the same way.
688
+
689
+ Unchanged: `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` and code-less 404/501 answers still degrade loudly to the client-side fallback, 400 `VALIDATION_FAILED` still throws, and 5xx / network failures still degrade silently.
690
+ - b470e91: The metadata designer states its package on the publish step, not only on the save
691
+
692
+ Studio's designer save→publish loop bound the draft to a software package on the
693
+ save (`PUT ?mode=draft&package=<id>`) and then sealed it with a publish that named
694
+ no package at all. objectstack `9e04c3e35` (shipped in `@objectstack/rest` 17.2.0) taught
695
+ `POST /meta/:type/:name/publish` to accept `?package=<id>`, so the second call can now
696
+ state the same binding the first one already states.
697
+
698
+ - `MetadataClient.publish()` accepts `packageId` and sends `?package=<id>`, the same
699
+ wire spelling and the same `encodeURIComponent` treatment `save()` gives it.
700
+ - `MetadataResourceEditPage` reads the binding for BOTH steps from one derivation
701
+ (`readActivePackageBinding`), so the two calls of one loop cannot drift apart. The
702
+ `?package=all` "show everything" scope keeps folding to "no package".
703
+
704
+ The parameter is **omitted**, never sent empty, when the designer holds no binding.
705
+ Empty and absent are the same to the framework's normaliser today, but absent is the
706
+ shape the save door already followed, and the framework's promotion path branches on
707
+ the key being present downstream.
708
+
709
+ What this buys is **reachability**, not speed: it lets `#9612`'s package-closure
710
+ narrowing at the runtime publish gate fire on an HTTP-driven promotion at all. That
711
+ narrowing has a second, independent gate this does not touch — objects carrying no
712
+ `_packageId` provenance are kept unconditionally — so on a tenant-authored overlay
713
+ corpus stating the package still narrows nothing.
714
+
715
+ ⚠️ **Dated note, 2026-09-29 — the publish-gate narrowing is an objectstack card — objectui#11016.** The last paragraph
716
+ names the package-closure narrowing by a bare number, which in this repository
717
+ resolves to an unrelated objectui pull request. It is
718
+ objectstack-ai/objectstack#9612, the runtime publish gate's rule snapshot. The text above is kept as the reading of this change.
719
+ - fffa30d: `listViews` no longer renders a refused metadata read as "this object has no saved views"
720
+ (objectui#8151).
721
+
722
+ `ObjectStackAdapter.listViews` degrades every failure to an empty list, and every consumer
723
+ reads only the return. So "the server served zero saved views" and "the server refused, or
724
+ broke" produced the identical UI — with a `console.warn` as the only discriminator, in the
725
+ browser console, with nothing pointing at it. It is the same defect objectui#7741 removed
726
+ from `listImportMappings` one method over, and the user-visible cost is the higher one: an
727
+ empty `listViews` is an object's **view switcher**, so a user whose token lapsed
728
+ mid-session could be shown an object that appears to have no saved views at all —
729
+ including views they created themselves.
730
+
731
+ **The empty-list return is unchanged.** `listViews` still answers `Promise<any[]>` and
732
+ still never throws, on every arm including the loud ones — this is a channel added
733
+ ALONGSIDE that contract, not a change to it. `listImportMappings` is likewise unchanged,
734
+ down to its wording.
735
+
736
+ - **`ObjectStackAdapter.listViews` now emits on `onMetadataReadWarning`** — the channel
737
+ objectui#7741 added — when the read failed in a way that is not the supported "this host
738
+ mounted no metadata door" shape. The event carries the object, whether the server
739
+ `refused` this caller or the answer was `unreadable`, and the server's own ADR-0112
740
+ code, HTTP status and message.
741
+ - **New: `classifyViewsFailure(err)`.** A SEPARATE reading, deliberately not a second
742
+ caller of `classifyImportMappingsFailure`: `view`'s quiet set is strictly smaller. The
743
+ arm the mapping classifier is built around — 400 `INVALID_REQUEST`, the metadata list
744
+ door's "this deployment carries no such kind" — is unreachable for `view`, which is in
745
+ the platform's static spelling contract, so reading it as kind-absence would swallow a
746
+ real refusal. On `view`, only a host with no `/meta` door at all stays quiet.
747
+ - **`MetadataReadWarningEvent`'s `operation` and `kind` gain their second members**
748
+ (`'listViews'` / `'view'`). This is the additive, reviewed widening the single-member
749
+ unions were designed for, and it worked as designed: the consumer that renders these
750
+ events had a `switch` naming one operation, so the widening turned "a views failure is
751
+ toasted as an import-mapping failure" into a compile error rather than a runtime lie.
752
+ - **New: `MetadataReadFailureKind`**, the neutral spelling of the three verdicts.
753
+ `ImportMappingsFailureKind` is now an alias of it — identical members, so existing
754
+ consumers are unaffected in both directions.
755
+ - **The console says which list it was.** `metadataReadWarningToast` picks its title and
756
+ its remedy by `operation`, so a failed view read reads *"Saved views for {{object}}
757
+ could not be loaded … not because this object has no saved views"*. Three new
758
+ `console.savedViews*` keys ship in all ten locale packs; the `console.importMappings*`
759
+ copy is untouched.
760
+
761
+ This applies framework objectstack#13906 decision 1 option A — *a thing that could not be READ is not
762
+ a thing that is ABSENT* — at the second seam that needed it.
763
+ - 8e00bfd: **Breaking (published surface):** remove `options.actor` from `MetadataClient`'s
764
+ `save`, `reset`, `publish` and `rollback`, and stop emitting the `X-Actor`
765
+ request header.
766
+
767
+ The server stopped honouring that header. objectstack#7941 ruled that the
768
+ recorded actor is the identity the request was authorized as, and removed the
769
+ header limb from the `/meta` write resolver — attribution cannot drift from
770
+ authorization. The option therefore typed cleanly, sent a header, and could not
771
+ influence the audit or history row it appeared to address: a false affordance
772
+ that promised attribution and silently failed to deliver it.
773
+
774
+ Three declarations go: `MetadataClientSaveOptions.actor` (inherited by
775
+ `MetadataDeleteOptions` via `extends`, so it served both `save` and `reset`),
776
+ and the inline `{ actor?: string }` on each of `publish` and `rollback`.
777
+ `MetadataAuditEntry.actor` is unaffected — that is the server's read-back of
778
+ who acted, and it remains the way to see attribution.
779
+
780
+ Marked `minor` rather than `major` per this repo's version-alignment policy
781
+ (the fixed group's major tracks `@objectstack`, and `major` in a changeset
782
+ would drag all 39 packages off that cadence).
783
+
784
+ No caller in this repo passed `actor`; the census found the only in-repo
785
+ occurrence was the client's own unit test. Callers outside this repo that still
786
+ pass it are unaffected at runtime beyond losing a header the server already
787
+ ignored — the property is dropped rather than forwarded, pinned by
788
+ `metadata-actor-retired-4834.pin.test.ts`.
789
+ - fd8dace: Studio surfaces the runtime authoring gate's advisory findings after a **publish**, not only after a save
790
+
791
+ objectui#4133 / PR #4236 wired the gate's advisories to the save door and recorded, honestly, what that left unsurfaced: Studio's designer stages every edit as a `mode: 'draft'` save, drafts are never gated (the framework returns at its D1 early-return before a single rule runs), and the publish step that *is* gated returned no `advisories` field at all. So on the flow most tenants actually use, the author was told nothing at either door — for two different reasons, only one of which was objectui's.
792
+
793
+ The second reason has expired. `PublishMetaItemResponseSchema` now declares the same optional, omitted-when-empty `advisories` key that `SaveMetaItemResponseSchema` has carried since #4717, and `publishMetaItem` populates it. Measured against the installed `@objectstack/spec` (17.2.0) rather than inferred from the version number: the key survives a `safeParse`, a half-shaped finding is rejected, and a clean publish omits the key entirely. That reading is now a test rather than a note, so a spec drift fails CI instead of silently re-muting the door.
794
+
795
+ ⚠️ **Dated note, 2026-09-29 — the save response's advisories key came from an objectstack card — objectui#11016.** The
796
+ paragraph above dates the save response's `advisories` key by a bare number,
797
+ which in this repository resolves to an unrelated objectui item. It is
798
+ objectstack-ai/objectstack#4717, the card that put the runtime authoring gate's
799
+ advisory findings on the response. The text above is kept as the reading of this change.
800
+
801
+ `MetadataClient.publish` and `MetadataClient.publishDraft` — the two methods over the single-item publish route `POST /meta/:type/:name/publish` — now report through the **same** sink, the same event and the same renderer the save door already used. No new UI shape: same warning tier, same 10s duration, same per-finding `rule` + `message` + `hint` formatting, findings still rendered verbatim as server prose. The wiring lands in the data layer rather than at the call sites, so `ResourceEditPage`'s Publish button and the runtime `RuntimeDraftBar` promotion (ObjectView / ReportView / DashboardView) are covered by one change, as are future ones.
802
+
803
+ One thing had to differ, and it is the frame's verb. Save and Publish are two different buttons in this product, so a toast that says "Saved" after a Publish tells the author their change is still a draft — the opposite of what happened. `MetadataSaveAdvisoryEvent` therefore gains a required `door: 'save' | 'publish'` and the renderer picks `console.publishAdvisoryTitle` (added to all ten locale packs) accordingly. `door` exists because `mode` cannot answer this: a direct active save and a draft promotion both report `mode: 'publish'`, since both land the body in the active overlay. It is required rather than optional so a future third door cannot be wired without saying which one it is, and the renderer branches on it through an exhaustive switch with a `never` check, so adding a third member is a compile error rather than a silently wrong verb.
804
+
805
+ **BREAKING for event constructors — `MetadataSaveAdvisoryEvent.door` is required.** Reading the event is unaffected: a listener that ignores `door` behaves exactly as before, and every other member is unchanged. Constructing one is a compile break — a door-less event literal that type-checked before now fails with TS2741, `Property 'door' is missing`. Measured on the emitted `dist/index.d.ts` of `@object-ui/data-objectstack` on both sides: that single required member is the entire non-comment delta of the package's published surface. **Migration:** add `door: 'save'` or `door: 'publish'` to the literal, whichever write it models — `'save'` for `PUT /meta/:type/:name`, `'publish'` for `POST /meta/:type/:name/publish`. Scored `minor` rather than `major` per the repo's version policy: objectui's major is pinned to `@objectstack`'s so that "same major means compatible" holds across the two repos, so objectui's own breaking changes ship as `minor` with the break named here (`scripts/check-changeset-no-major.mjs`). Every publishable package sits in one `fixed` group, so this entry carries the group.
806
+
807
+ Unchanged, deliberately: the **batch** door. "Publish whole app" (`POST /packages/:id/publish-drafts`) discarded per-draft advisories server-side when this change was written — objectstack#9343, open and unruled at the time — and nothing here compensated for that from the client side. A test pinned the absence, so a later traversal of a batch-shaped `published[]` could not be added without turning it red.
808
+
809
+ *Corrected before release (`ce986aafc`): both present-tense claims in the paragraph above went false after it was written — objectstack#9343 landed, the batch response now carries per-draft advisories, and `ce986aafc` routes that door through the same seam and flips the absence pin to a presence pin. The paragraph is kept in the past tense as the record of what this change did and did not do; this note is what the CHANGELOG publishes instead of a sentence that was true only while it sat here.*
810
+ - 2d36552: Pins `@objectstack/spec`, `@objectstack/client`, `@objectstack/formula` and `@objectstack/lint` to `17.1.0`, and adapts the two consumer surfaces the new build moves.
811
+
812
+ The pin itself is a lockfile refresh — every manifest already declared `^17.0.0`, which admits `17.1.0`, so no dependency range changed. All four move together: a split resolution is what produced the dual-version spec graph that reddened `check:spec-symbols` in this repo's history.
813
+
814
+ **A `icontains` filter now reaches the driver as a filter.** `icontains` is a canonical `VIEW_FILTER_OPERATORS` member as of `17.1.0`, so an author can declare it on a `ViewFilterRule` and the spec validates it — but `@object-ui/data-objectstack`'s alias table had no row for it, and an unmapped operator is how this adapter shipped an unfiltered query before (objectstack#3948). It is an identity row like `contains`: `icontains` is itself a member of `VALID_AST_OPERATORS`, so the spelling the author writes is the spelling the AST takes, and no case-sensitivity is translated away. Declared rather than left to the table's `?? op` fall-through, on the rule its own parity test states — the AST gate accepting a spelling is not the driver compiling it into a `WHERE` clause.
815
+
816
+ The same operator reaches the list view's own bridge: `@object-ui/plugin-list`'s `mapOperator` gains an explicit `icontains` arm. The emitted spelling is identical to the input, but the arm is written out rather than left to the `default` passthrough — `icontains` is its own member of `VALID_AST_OPERATORS`, so a raw passthrough is accepted *today*, and depending on that coincidence is what the bridge's own parity test records as how it once stopped discriminating.
817
+
818
+ `@object-ui/core` adds `onSuccess` to its spec key inventory, so an author writing the key `17.1.0` now declares is no longer warned that it is unknown. That is a diagnostic statement only — the four declared action surfaces still drop the key before it reaches the runner, which is tracked separately.
819
+
820
+ **A stored view filtering case-insensitively still shows that operator when it is reopened.** `@object-ui/plugin-view`'s canonical-to-builder table is keyed by `ViewFilterOperator`, so `17.1.0` adding `icontains` failed to compile rather than letting the operator reach the FilterBuilder as a raw spelling its dropdown cannot select. It maps to the builder's `containsCaseInsensitive` — the id that authors the spec's `$icontains` — and deliberately not to `contains`, which would quietly rewrite a case-insensitive filter into a case-sensitive one the next time the view was saved.
821
+
822
+ **The page-editor palette keeps one entry per renderer.** `17.1.0` retires `element:filter` from `PageComponentType` and adds `record:discussion`, leaving the member count at 34 either side — so the swap is invisible to any count-based reading. The stale `element:filter` exclusion is dropped, and `record:discussion` is excluded because it is the *same renderer* as the already-offered `record:chatter`, not because it is unauthorable. Nothing the palette offers changes.
823
+
824
+ **The console eager-closure ceiling is re-baselined, by maintainer ruling.** The release is roughly 930 KB larger uncompressed and nearly all of it lands in `vendor-objectstack-*.js`, which put the closure past a ceiling that was deliberately sized to catch a 89 KiB regression — the gate refused the bump, correctly. Raising it was escalated rather than taken locally, because gate-strength policy had been ruled the maintainer's; the ruling on objectui#5531 authorised the raise. `MAX_EAGER_CLOSURE_GZIP_BYTES` and the `BASELINE` it is derived from move together in one commit, keeping headroom at 2.00% and below the 91,136-byte regression size the gate must still catch. The gate's *sensitivity* is untouched: a repeat of that regression from the new baseline still fails. No behaviour ships from this file — it is CI policy, recorded here because the version it governs is the one this changeset publishes.
825
+
826
+ ### Patch Changes
827
+
828
+ - 9ec8ceb: Views that an earlier "Edit view config → Save" made read-only recover on read, and keep
829
+ their edits (objectui#10210).
830
+
831
+ A view row now counts as a personalization overlay only when it carries the `_isOverride`
832
+ marker. The view list used to treat a flat row carrying `viewKind: 'list'` as an overlay
833
+ too, a shape guess kept for toolbar rows written before the marker existed. A config save
834
+ on a code-defined view stored exactly that shape, so the view dropped out of the saved
835
+ views and its tab turned read-only, for good once the draft was published. With the guess
836
+ retired, such a row reads back as the saved view it is: its tab is editable again and shows
837
+ what the save stored. Nothing is rewritten at rest; the next read is enough.
838
+
839
+ ⚠️ This exposes one class of row: overlay rows written before the marker
840
+ (objectui#4227, closed 2026-08-15) and never touched since. They have the same shape, so
841
+ they now also read as plain rows, and their frozen label, columns and filter copy covers
842
+ the code definition again. No deployment is named as holding one.
843
+ - f812f37: fix(data-objectstack): the object-metadata write refusal no longer points at an issue that answers 404
844
+
845
+ `assertObjectMetadataWritable` throws when an object-metadata write carries a relationship
846
+ field with no usable target, and a host surfaces that message on screen or in a log. Its
847
+ reason ended with two issue pointers, and one of them answers 404, so a reader who followed
848
+ it found nothing. The message now cites only objectui#7714, the ruling the refusal enforces.
849
+
850
+ Nothing else in the message moves: the door name it opens with, the field it names, the
851
+ target state it describes and the advice it closes with are unchanged, and the guard refuses
852
+ exactly the writes it refused before.
853
+ - ac15833: fix(data-objectstack, plugin-dashboard): a dataset tile the viewer may not read shows a localized "no access" state (objectui#10899)
854
+
855
+ When the analytics read admission refused a dashboard dataset query — `403`
856
+ with ADR-0112 `PERMISSION_DENIED` — `queryDataset` had no branch for that code,
857
+ so it threw the generic `Dataset query failed: 403 Forbidden — [Analytics]
858
+ Access denied: …` string, and `DatasetWidget` printed it verbatim in a red
859
+ alert. The list view over the same object already says 「无权访问」.
860
+
861
+ - `queryDataset` now throws a typed `AnalyticsForbiddenError` for that code,
862
+ carrying `httpStatus: 403` and `code: 'PERMISSION_DENIED'` (plus the server's
863
+ code and message for diagnostics). A code-less 403 — no ObjectStack route
864
+ wrote it — keeps the generic error.
865
+ - `DatasetWidget` classifies a failed query with the shared `classifyLoadError`
866
+ and, for `forbidden`, renders a localized no-access state
867
+ (`dashboard.widgetForbiddenTitle` / `dashboard.widgetForbiddenMessage`, new in
868
+ all ten packs) instead of the exception text. Every other failure keeps the
869
+ detailed alert.
870
+ - 3c13675: **The console reads a saved view by the spellings `@objectstack/spec` declares, and stops reading the keys nothing writes (objectui#11013).** This is the console end of the ruling on objectstack#20051: the spec now declares the keys the console writes onto a stored view and reads back (`VIEW_CONSOLE_ROUND_TRIP_KEYS`), and the console reads a stored view under those spellings.
871
+
872
+ **A saved view keeps its switcher state across a reload.** A view saved as a record (the "+" tab's create, or "Edit view config → Save") used to come back from `listViews()` with only its configuration, name, label and default flag. It now also comes back with its pin (`isPinned`), its position among saved views (`sortOrder`), its switcher group (`visibility`), its column layout (`columnState`) and its bound `object`. Visible effect: a saved view you dragged to a new position keeps that position after a reload, including in a browser that never saw the drag. The carried keys are read off the spec's record, not hand-listed.
873
+
874
+ **Keys a saved view no longer steers.** No console surface writes any of these onto a view, no view in this repository authors one, and the spec's view schema refuses each by name. A stored view that still carries one now behaves as if it did not:
875
+
876
+ | key on the stored view | what changes for the user |
877
+ | --- | --- |
878
+ | `allowExport` | A view carrying `allowExport: false` no longer hides the list's export control or drops its `exportOptions`. Whether a list offers export is the page's setting (the `list-view` / `object-view` node's `allowExport`); which formats it offers is the view's `exportOptions.formats`. |
879
+ | `wrapHeaders` | Column-header wrapping follows the page's setting, not the view's. |
880
+ | `editRecordsInline` | On the object page this was read as a second spelling of `inlineEdit`. A view carrying only it no longer turns inline editing on; write `inlineEdit`. |
881
+ | `clickIntoRecordDetails`, `addRecordViaForm`, `addDeleteRecordsInline`, `collapseAllByDefault`, `fieldTextColor`, `prefixField` | Relayed into the list, which drew nothing from them. No visible change. |
882
+
883
+ The same keys authored on the `object-view` node itself are unchanged: that node's own values still reach the list.
884
+
885
+ **The toolbar policy is read as `userActions`.** The `object-view` node the object page builds, and `@object-ui/plugin-view`'s own kanban / calendar / gallery / timeline / gantt / map route (a host that renders the `object-view` node without `renderListView`), read a view's search, sort and filter toggles from `userActions.search`, `.sort` and `.filter`, where they used to read the bare `showSearch`, `showSort` and `showFilters` flags. On that plugin route, a view that declared `userActions: { search: false }` now hides the search control; the object page's list already honoured it. A stored view that still carries a bare flag keeps its answer: `normalizeListViewSchema` folds it onto `userActions`.
886
+
887
+ **A stored view is bound and identified by its declared keys only.** A view row is matched to its object by `object` (or its configuration's `data.object`), and no longer by `objectName`; it is identified by `name`, and no longer by a top-level `id` or `_id`. This holds in the view switcher (`listViews()` / `listViewOverrides()`), in Studio's view picker for `interfaceConfig.sourceView`, and in Studio's view preview. Every console write stamps both `object` and `name`, and the metadata door refuses a view record or overlay row bound by `objectName` alone, so a row the console wrote is unaffected. The console also no longer stamps an undeclared `objectName` onto the rows it reads, which a saved view's toolbar save used to write back into the stored row.
888
+
889
+ **BREAKING (TypeScript only) — `NamedListView.allowExport` is retired.** It is now a `?: never` tombstone, like the seventeen members objectui#7924 retired: a TypeScript author who writes it gets a compile error there. Ruling A on objectui#7924 had kept it declared only because both relays read it off a view, and those reads are gone.
890
+ - f61dab1: `reference` is now the only spelling ObjectUI writes or reads for a relational field's target object (objectui#11070, round 4, under the objectui#6837 ruling: 「objectui不是前端的项目吗?后端的元数据只要对,前端按协议执行就行了呀」).
891
+
892
+ - **Types.** `LookupFieldMetadata`, `MasterDetailFieldMetadata` and `DetailViewField` (with its zod mirror `DetailViewFieldSchema`) declare `reference` and no longer declare `reference_to`. On the two field metadata types the member is typed by reference to `@objectstack/spec`'s `FieldSchema.reference`. `@object-ui/plugin-form`'s `FieldDefaultsSchemaLike` drops its `reference_to` member the same way.
893
+ - **Readers.** `LookupField`, `UserField`, `LookupCellRenderer`, `UserCellRenderer`, the inline editor's reference fallback, the form's `current_user` seeding and the inline-subform parent lookup read `reference` alone.
894
+ - **Emitters.** Every in-repo producer that builds a field definition or a widget `field` prop writes `reference`: the action-param dialog (`paramToField`), the bulk-action dialog, the record detail page, drawer, footer, related list and synthesised page, the gallery card, the form's section-field override and the flow designer's reference picker. The grid's and the dashboard's relational copy sets carry `reference` and no longer copy `reference_to`.
895
+ - **Ingestion.** `normalizeFieldReferenceKeys` (behind `ObjectStackAdapter.getObjectSchema` and `MetadataProvider`) still folds a legacy `reference_to` / `referenceTo` onto `reference` when `reference` is absent, and still warns in dev. It no longer stamps `reference_to` onto every relational definition, and it still never drops or overwrites a key.
896
+
897
+ `@objectstack/spec`'s `FieldSchema` refuses `reference_to` by name, and objectstack#13847 rewrites stored ones on the serve path and in `os migrate meta`, so a definition served by an ObjectStack backend is unaffected.
898
+
899
+ ## ⚠️ BREAKING for a host that hands `reference_to` to the widgets directly
900
+
901
+ The type change is a compile error for TypeScript that writes `reference_to` on `LookupFieldMetadata`, `MasterDetailFieldMetadata` or `DetailViewField`: rename it to `reference`.
902
+
903
+ At runtime the break reaches exactly one kind of host: one that serves object definitions spelling the target only as `reference_to` through a `DataSource` other than `ObjectStackAdapter`, or that passes such a definition straight into `LookupField`, `UserField` or a cell renderer. Those definitions never pass the ingestion fold. Measured with an object-bound lookup field on `ObjectForm` and a lookup column on `ObjectGrid`, both fed by a hand-written `DataSource` whose `getObjectSchema` returns `{ type: 'lookup', reference_to: 'account' }`:
904
+
905
+ - before this change, opening the picker queried `account` and the cell resolved the record's name;
906
+ - after it, the picker has no object to query (no `find` call is made) and the cell shows the raw id beside the unresolved-reference marker.
907
+
908
+ The same definition served through `ObjectStackAdapter.getObjectSchema` still works, before and after: the fold adds `reference`, the picker queries `account`, the cell resolves the name, and the dev warning names the field. **Fix:** spell the target `reference` in the definition your `DataSource` serves.
909
+
910
+ ⚠️ **Dated note, 2026-09-30 — the break reaches more than the paragraph above states — objectui#11070.** Two corrections from the contract review of this change, appended rather than edited in; neither changes what ships.
911
+
912
+ - "The break reaches exactly one kind of host" above is too narrow. Host code that read `reference_to` off a definition after ingestion — from `ObjectStackAdapter.getObjectSchema()` or `useMetadata().objects` — also loses that key, because the ingestion pass no longer stamps it (the **Ingestion** bullet above states the fact, and `reference_to` was never a declared member of those definitions). **Fix:** read `reference`.
913
+ - The measured break above is a `lookup` field's. A `user` field handed a `reference_to`-only definition directly does not end at "no query": `UserField` falls back to `sys_user`, so its picker queries `sys_user` rather than the object `reference_to` named. **Fix:** spell the target `reference` there too.
914
+
915
+ The text above is kept as the reading of this change.
916
+ - 81f8498: objectui now resolves `@objectstack/*` 17.5.0 and `zod` 4.6.5, and follows every contract move that release makes (objectui#11073). `@objectstack/spec` 17.5.0 and `@objectstack/core` 17.5.0 require `zod ^4.6.1`, so objectui's own `zod` resolves to the same 4.6.5: two zod minors do not type-check against each other.
917
+
918
+ ⚠️ Breaking in places, marked `minor` under this repo's version-alignment rule (a `major` in the fixed group would move all of it off the `@objectstack` major). Every narrowing below is the spec's own, and already reaches any consumer that resolves `@objectstack/spec ^17.5`.
919
+
920
+ - `@object-ui/types`: declares `@objectstack/spec ^17.5.0` and `zod ^4.6.1`. A named list view's `pageName` and `tabs` are retired, as the spec retired them: typed `never` and refused by name at parse. `DashboardWidgetSchema` attaches the spec's two new checks (`checkDashboardWidgetStageOrder`, `checkDashboardWidgetMetricMeasureArity`), so a widget the spec refuses is refused here too. Where a strict object is an arm of a plain union, its unknown-key refusal is terminal again (the spec's own mechanism, needed under zod 4.6): the union answers with every arm instead of the one that failed only on unknown keys. That changes the error shape, never the verdict (measured on every objectui union). Two by-name named-view checks that can no longer run are retired; the spec's refusal answers those documents.
921
+ - `@object-ui/core`: declares `@objectstack/spec ^17.5.0`. `page` leaves the list-view kinds the normalizer knows, as the spec removed `type: 'page'`. `isRefusedTextComparand` and `textComparandRefusalReason` are re-exported from `@objectstack/spec/data`, whose copies are byte-identical. `SPEC_ACTION_KEYS` lists `execution`, which `ActionSchema` now declares.
922
+ - `@object-ui/data-objectstack`: declares `@objectstack/spec ^17.5.0`, up from `^17.4.0`. Its bundle carries `@object-ui/core`'s filter converter, which now takes `isRefusedTextComparand` and `textComparandRefusalReason` from `@objectstack/spec/data`. `@objectstack/spec` 17.4.0 exports neither, so the old range admitted a spec under which the shipped module names exports that are not there (objectui#5793's floor gate). Nothing moves at runtime in this repository, because the lockfile already resolves 17.5.0. `patch`, as the floor raise of objectui#10864 was.
923
+ - `@object-ui/fields`: the percent and number `scale` clamp is gone, at the sunset its own test named. The spec now refuses a `scale` above 100 at the declaration, so the width a formatter cannot render no longer arrives, and every face passes the declared width through as it did before the clamp.
924
+ - `@object-ui/components`: two module-local prop interfaces of the action renderers are renamed (`ActionButtonRendererProps`, `ActionIconRendererProps`), because the spec now exports the old names for its authored props. Neither is reachable through the package's exports map; only the shipped per-file declarations change.
925
+ - `@object-ui/plugin-list`: the README's view-type example no longer lists the retired `page` kind.
926
+ - `@object-ui/app-shell`: the flow designer follows 17.5.0 (maintainer ruling on objectui#11088, decision 2). A new `wait` node is seeded `waitEventConfig: { eventType: 'timer', timerDuration: 'PT1H' }`, because the spec now refuses a timer wait with no duration; the inspector shows the Duration at once and it can be changed. The decision form offers the spec's new `mode` (exclusive or inclusive), with no default declared because the spec applies none. The screen form's `mode` states `create` again, because the spec now applies that default. `minor` because the decision form gains a control.
927
+ - `@object-ui/app-shell`: declares `@objectstack/spec ^17.5.0`, up from `^17.4.0`, and `zod ^4.6.1`, up from `^4.4.3`. The designer now states 17.5.0 behaviour. The screen form's declared `create` default equals a default the spec applies only since 17.5.0, and objectui#9109 requires the two to be equal, so a range that admitted 17.4.0 claimed more than the designer holds to. The resolved versions do not move.
928
+ - `@object-ui/console`: the bundle inlines the 17.5.0 packages, so its client-side validation answers as the published 17.5.0 contract does. The flow preview sample's script nodes use the 17.5.0 script contract. The eager-closure budget is re-baselined for the release's growth, under the maintainer ruling on objectui#11088 (decision 1).
929
+ - 490d9a9: Grid headers offer a sort click only on columns the PLATFORM says it will order by
930
+ (objectui#5729 — the consumer leg of objectstack#10235, maintainer ruling A, 2026-08-23:
931
+ the platform serves an explicit per-column sortability signal and the grid reads it,
932
+ rather than re-deriving "virtual ⇒ unsortable" from field type).
933
+
934
+ `GET /api/v1/meta/object/:name` now answers with a `sortability` projection on its
935
+ ENVELOPE — `{ fields: { [name]: { sortable, reason?, caveat? } } }`, computed at serve
936
+ time from the platform's own storage predicates, deliberately beside `item` rather than
937
+ inside it so the key stays un-authorable. The signal was reaching the browser and being
938
+ discarded one line before its only consumer: `ObjectStackAdapter.getObjectSchema` unwraps
939
+ the envelope to `item`, so every UI reader saw a document with no signal on it. It now
940
+ survives that unwrap, carried on the schema under a symbol key — invisible to
941
+ `JSON.stringify`, to `Object.keys` and to a spread, so a schema handed back at a metadata
942
+ write endpoint can never take it into a body the server parses strictly.
943
+
944
+ `@object-ui/core` gains the one spelling of the consumer contract:
945
+ `isPlatformSortableField(projection, name)` is `true` iff an entry EXISTS for the name and
946
+ says `sortable: true`. Absence is a refusal — it is how the platform encodes an unknown
947
+ name, a dotted path and an unprovisioned audit column, all three of which the runtime
948
+ doors reject — so the `!== false` spelling every other optional flag in this repo uses
949
+ would get exactly that family backwards. A projection that is absent ALTOGETHER is a
950
+ different question with a different answer (`undefined`: no signal was served) and is
951
+ typed apart from an empty one, so a deployment older than the upstream change keeps the
952
+ behaviour it had rather than being told, falsely, that nothing on the object is sortable.
953
+
954
+ Three things follow in the grid. The header click on a refused column ceases to exist, so
955
+ neither the old silent-unordered result nor the `400 INVALID_SORT` that replaced it is
956
+ reachable from it. A sort PERSISTED before the signal existed is filtered out of both what
957
+ the grid renders and what it emits, so a restored personalization cannot ride back into
958
+ the next `persistViewPatch({ sort })` — the half-fix where the affordance is gone and the
959
+ PUT still fires. And the relational carve-out is untouched and deliberately not delegated
960
+ to this signal: the platform answers `sortable: true` for a `lookup` (it has a stored
961
+ foreign key and both runtime doors accept ordering by it), while the grid withholds that
962
+ header for a different reason — a column of names ordered by an invisible id.
963
+
964
+ Columns carrying `caveat: 'unprovisioned-anchor'` keep their click. The runtime accepts
965
+ those sorts; refusing what the platform does not refuse would recreate declared-≠-enforced
966
+ drift in mirror image.
967
+ - a26b9e4: `packages/core/src/adapters/README.md` now documents the adapters that are actually in that
968
+ directory, and the ObjectStack material it carried moved to the package that owns the behaviour
969
+ (objectui#6213). Both files ship to consumers — `@object-ui/core` publishes its `src/`, and a
970
+ README rides every tarball — so this was published documentation describing the wrong package.
971
+
972
+ The page had been left behind when the ObjectStack adapter moved out to
973
+ `@object-ui/data-objectstack`: its headings, feature list, filter-operator table and
974
+ query-parameter table were all about that adapter, and its one-entry "Available Adapters" list
975
+ told a reader Object UI has exactly one adapter and that it comes from `@object-ui/core`.
976
+ `ApiDataSource`, `ValueDataSource`, `resolveDataSource`, `runBatchTransaction` and
977
+ `emulateBatchTransaction` — the five exports that directory really ships — were named nowhere.
978
+
979
+ - **`@object-ui/core`**: the page now opens with what the directory holds, gives each export a
980
+ usage snippet and a `provider` mapping, and points at `@object-ui/data-objectstack` for the
981
+ ObjectStack adapter. `## Creating Custom Adapters` is unchanged — it is the one section that was
982
+ always about this directory.
983
+ - **`@object-ui/data-objectstack`**: gains a `## Query Translation` section carrying the
984
+ filter-operator and query-parameter mapping tables, the AST conversion example and the sorting
985
+ example. That material existed **only** in the `core` copy — this package's README documented
986
+ query translation as a single feature bullet — so it is ported, not dropped.
987
+
988
+ No runtime behaviour changes; the duplicate copy of one package's documentation living under
989
+ another package is what goes away.
990
+ - 5127378: `ObjectStackAdapter.aggregate()` lowers rule-shaped filter arrays before the
991
+ analytics wire, reusing the lowering `find()` already runs (objectui#6302).
992
+
993
+ `find()` has translated `[{ field, operator, value }, ...]` into the server's
994
+ filter AST for as long as `convertQueryParams` has existed. The analytics path
995
+ did not: `aggregate()` assigned `payload.where = params.filter` verbatim and
996
+ posted it to `/analytics/query`.
997
+
998
+ The two doors are not equally forgiving, so the gap had a user-visible end.
999
+ `lowerAnalyticsWhere` in `@objectstack/service-analytics` — shared by both
1000
+ aggregation strategies — accepts AST tuples and throws on an array of rule
1001
+ objects. A stored `ViewFilterRule[]` that a LIST renders correctly therefore
1002
+ rendered `element:number` into its error state on every analytics-capable
1003
+ deployment, which is the default one because the CLI always loads analytics.
1004
+
1005
+ An array filter now goes through the same `translateFilterArray` the `find()`
1006
+ path uses — one lowering, so the two paths cannot disagree about one stored
1007
+ filter. Rules spread into a logical node (`['and', ...rules, ...tuples]`, the
1008
+ commonest composite there is) are lowered at depth, as they already were on
1009
+ `find()`. Non-array filters are untouched: the MongoDB-style object this branch
1010
+ was written for is what `/analytics/query` already accepts, and translating it
1011
+ would be a semantic change this fix does not make. Already-AST arrays,
1012
+ record-shaped filters, and the no-filter case are byte-unchanged.
1013
+ - 5961030: `@object-ui/core` and `@object-ui/data-objectstack` now declare
1014
+ `"@objectstack/spec": "^17.2.0"` rather than `^17.0.0`, which is the lowest published
1015
+ spec that carries every symbol each package's own build output references
1016
+ (objectui#6361).
1017
+
1018
+ `packages/core/dist/utils/column-sortability.d.ts` references
1019
+ `FIELD_SORTABLE_UNPROVISIONED_ANCHOR`, `FIELD_UNSORTABLE_VIRTUAL_TYPE`,
1020
+ `FieldSortability` and `ObjectSortability` from `@objectstack/spec/api`, and
1021
+ `packages/data-objectstack/dist/index.js` references the first two — none of which
1022
+ `@objectstack/spec@17.0.0` exports. Measured against the published tarballs rather than
1023
+ the installed tree, by `scripts/check-spec-range-floors.mjs`: six `floor-too-low`
1024
+ findings across the two packages, and `^17.2.0` is that gate's own computed answer for
1025
+ both. So the old range was a claim neither package could honour: any consumer
1026
+ resolution that lands 17.0.0 — a sibling pinning it exactly, an `overrides` entry, a
1027
+ mirror two minors behind — satisfied `^17.0.0` and got a dangling reference.
1028
+
1029
+ Nothing a consumer installs today changes: normal resolution already picks the newest
1030
+ 17.x, and `pnpm-lock.yaml` still resolves `17.2.0` on both edges after the bump — only
1031
+ the recorded `specifier:` moves. No source and no behaviour changes, which is why this
1032
+ is scored `patch`, on the reasoning `111741454` used for the same remediation on
1033
+ `@object-ui/plugin-detail`.
1034
+
1035
+ The bump is release-blocking rather than cosmetic. `check:spec-floors` is deliberately
1036
+ not a `pull_request` job, so every PR stayed green while its blocking copy on the
1037
+ publish path — `pnpm changeset:publish` runs it before a single tarball reaches npm —
1038
+ would have cancelled the next release.
1039
+ - 639114c: Reconcile `@object-ui/data-objectstack` with the `@objectstack/*` family at
1040
+ 17.3.0 (objectui#7122).
1041
+
1042
+ `@objectstack/client`, `core`, `formula` and `lint` each pin `@objectstack/spec`
1043
+ EXACTLY, so resolving the spec alone to 17.3.0 left the console bundling TWO
1044
+ copies of it. Moving the family with it in `pnpm-lock.yaml` collapses the
1045
+ duplicate; every declared range already admitted 17.3.0, so no manifest moved.
1046
+
1047
+ The one source change the family bump forces is a type reconciliation, not a
1048
+ behaviour change. `client.analytics.query` resolved to `Promise<any>` at 17.2.0
1049
+ and resolves to `Promise<AnalyticsResult>` at 17.3.0, so the pre-envelope
1050
+ branches of `aggregate`'s row-shape fallback stopped type-checking. Those
1051
+ branches are read through a widened alias rather than deleted: the client's own
1052
+ docblock records the runtime change behind the narrower type ("BREAKING since
1053
+ objectstack#13079 — read `result.rows`, not `result.data.rows`"), and deleting
1054
+ them is a runtime compatibility decision about servers older than that, not a
1055
+ type repair. The alias restores exactly the compile-time latitude 17.2.0 gave
1056
+ the same expression and changes no runtime byte of it.
1057
+ - f57ca75: fix(studio): one draft-envelope reader, and it strips the framework's read decorations
1058
+
1059
+ `client.getDraft()` serves a DECORATED body — the draft branch stamps
1060
+ `_draft: true` and then `decorateMetadataItem` attaches `_diagnostics` for any
1061
+ type with a registered Zod schema. The spec names both READ-TIME decorations
1062
+ precisely because a served body "is NOT a valid input to the schema that
1063
+ produced it until these are removed" (`METADATA_READ_DECORATIONS`).
1064
+
1065
+ objectui#7603 taught `ResourceEditPage` to strip them. It could only teach one
1066
+ site, because `extractDraftBody` existed **four times** — three verbatim copies
1067
+ plus a hand-rolled one in `ObjectHooksPanel` — and six more consumers unwrapped
1068
+ the envelope inline. Ten readers, one of which knew the rule.
1069
+
1070
+ **The user-visible half.** The pending-changes sheet's per-entry diff compares
1071
+ the published body against the draft body key by key. Those two reads are
1072
+ decorated ASYMMETRICALLY — only the draft branch stamps `_draft` — so the sheet
1073
+ listed `_draft` under "Also changed:" on every entry that has a published
1074
+ counterpart, and `_diagnostics` alongside it whenever the two read-time verdicts
1075
+ differed. Framework-internal keys were being presented to the author as their
1076
+ own edits, on the screen where they decide whether to publish.
1077
+
1078
+ **The rest.** Six sites merged a decorated body into a document they then wrote
1079
+ back through `save(..., { mode: 'draft' })` — the Studio app / page / object /
1080
+ flow surfaces, the package OWD panel, the object hooks panel, and the
1081
+ adapter's `updateView`. Today's server absorbs that (it strips read decorations
1082
+ on ingress, before its own schema gate), so nothing 400s; this is still a client
1083
+ emitting a body its own spec calls invalid, and the fix belongs at the producer.
1084
+
1085
+ The cure is one function rather than ten strips: `extractDraftBody` is now
1086
+ exported from `@object-ui/data-objectstack`, beside the `getDraft` whose
1087
+ envelope it decodes. The key list is the spec's exported
1088
+ `stripReadDecorations` — never a second hand-maintained copy in this repo. The
1089
+ presence verdict still runs BEFORE the strip, so removing our own annotations
1090
+ can never turn a served draft into "nothing pending", and the ADR-0010
1091
+ protection envelope (`_lock`, `_provenance`, `_packageId`, `_packageVersion`)
1092
+ is deliberately untouched: those keys are declared by the closed schemas.
1093
+
1094
+ No schema was loosened, and no gate was taught to tolerate `_diagnostics`.
1095
+ - 0b8c638: fix(data-objectstack): the rule-entry filter form refuses an empty or non-string `icontains` comparand
1096
+
1097
+ `@objectstack/spec`'s `FILTER_TEXT_CASES` declares two shapes REFUSED for the
1098
+ case-insensitive contains operator: an empty comparand, and one that is not a
1099
+ string. This adapter already refused both when a filter arrived as an object
1100
+ (`{ name: { $icontains: '' } }`), but a filter in the rule-entry form
1101
+ (`[{ field: 'name', operator: 'icontains', value: '' }]`) was lowered to
1102
+ `['name', 'icontains', '']` and sent. So the same condition was refused or sent
1103
+ depending on which form it was written in. An empty comparand matches every
1104
+ row, so the entry form returned the whole table where the author asked for a
1105
+ subset.
1106
+
1107
+ The entry form now asks the spec's own `isRefusedTextComparand` after the
1108
+ operator alias fold, so a case variant such as `ICONTAINS`, or the operator
1109
+ given under the `op` key, is judged too. It throws `MalformedFilterError`
1110
+ (`INVALID_FILTER` / 400) carrying the spec's `textComparandRefusalReason`
1111
+ unchanged, before any request is sent, on `find()` (both routes) and on
1112
+ `aggregate()`, and for a rule nested under a logical node as well as at the top
1113
+ level. `convertFiltersToAST` and `ValueDataSource` read the same two functions,
1114
+ so all three now refuse the same set: an empty string and any non-string,
1115
+ including a missing `value`. A missing value used to go out as JSON `null`.
1116
+
1117
+ `MalformedFilterError`'s public signature is unchanged: the refusal sentence is
1118
+ seated inside this module. Building its message no longer throws for an entry
1119
+ that carries a BigInt (such a value is shown as its literal, e.g. `3n`), and its
1120
+ text is otherwise the same as before. The README's "Rule-shaped arrays" section
1121
+ documents the refusal. The case-sensitive `contains` family is untouched: the
1122
+ table declares no such row for it.
1123
+ - 72d6587: A record id is a `string` everywhere in the published types, as
1124
+ `@objectstack/spec` has always declared it. Three published declarations that
1125
+ admitted `number` no longer do.
1126
+
1127
+ ⚠️ **BREAKING if your code hands a numeric primary key to any of these three:**
1128
+
1129
+ - **`RecordContextValue.recordId`** (`@object-ui/react`) — the value
1130
+ `useRecordContext()` gives you is now a `string`, never a `number`.
1131
+ - **`DataSource.update`'s `id` parameter** (`@object-ui/types`) — a call that
1132
+ passes a `string | number` is now a type error. Adapters that *implement*
1133
+ `DataSource` are unaffected (see Migration).
1134
+ - **`TransactionOperation.id`** (`@object-ui/core`) — the operation record you
1135
+ hand to `TransactionManager.recordOperation()` must carry a `string` id. If
1136
+ you build that object from a numeric key, convert it where you build it. This
1137
+ type is exported from the package root, so this is a breaking change for
1138
+ `@object-ui/core` consumers in its own right, not just a knock-on.
1139
+
1140
+ Ships as `minor` per the launch-window convention: objectui's
1141
+ `major` is a cross-repo pin to `@objectstack`'s so that "same major means
1142
+ compatible" holds across the two repos
1143
+ (`scripts/check-changeset-no-major.mjs`), and objectui's own breaking changes
1144
+ ship as `minor` with the break named where it lands — this entry is the channel
1145
+ that carries it.
1146
+
1147
+ ## What changed
1148
+
1149
+ - `RecordContextValue.recordId` (`@object-ui/react`) was
1150
+ `string | number | null | undefined`; it is now `string | null | undefined`.
1151
+ - `DataSource.update`'s `id` parameter (`@object-ui/types`) was
1152
+ `string | number`; it is now `string`.
1153
+ - `TransactionOperation.id` (`@object-ui/core`) was `string | number`; it is now
1154
+ `string`. Its sibling `BatchTransactionOperation.id` was already a `string`,
1155
+ so the two operation records finally agree.
1156
+ - `LineItemsPanel` (`@object-ui/plugin-form`) drops the type assertion
1157
+ objectui#9304 left on its parent id. That assertion was the only thing making
1158
+ the context declaration and `buildMasterDetailEditBatch(parentId: string)`
1159
+ meet; the declaration now does it, so the evidence is discharged.
1160
+
1161
+ ## Why the protocol, and not a wider consumer type
1162
+
1163
+ `@objectstack/spec` declares a record id as `z.string()` on every record door —
1164
+ get, update, delete and the batch operation. A consumer type may not be wider
1165
+ than the protocol: a declaration that admits `number` promises callers something
1166
+ the wire never carries, and the promise is kept only by an assertion at the far
1167
+ end, which is what this card was filed about.
1168
+
1169
+ ## Internal consumers repaired at the same time (no public contract moves)
1170
+
1171
+ Narrowing an interface **parameter** never reaches implementors — TypeScript
1172
+ compares method parameters bivariantly, so an adapter that still declares
1173
+ `id: string | number` keeps satisfying `DataSource`. It reaches **callers**. A
1174
+ full local type-check of every workspace type-check program found exactly six,
1175
+ in three packages, and each was red because a further declaration one layer in
1176
+ was itself wider than the protocol. All three are narrowed here, types only, with
1177
+ no runtime change and no coercion added at any call site:
1178
+
1179
+ - `UserPreferenceRecord.id` and the `cachedRowId` it feeds
1180
+ (`@object-ui/data-objectstack`) are `string`. Module-local, not published —
1181
+ these rows are read back off the protocol, so the union was a claim the wire
1182
+ never makes.
1183
+ - `resolveRecordId`'s return type (`@object-ui/plugin-grid`) is
1184
+ `string | undefined`. Module-local, not published — it annotates `any`-typed
1185
+ row data, so the union was an assertion rather than a measurement.
1186
+
1187
+ ## Migration — no `String(...)` at your call sites
1188
+
1189
+ `RecordContextProvider` still **accepts** `string | number | null | undefined`
1190
+ and narrows it once, itself. A host that mounts a record with a numeric primary
1191
+ key therefore changes nothing: the conversion is paid at that injection
1192
+ boundary, typed, in one place. Consumers of `useRecordContext()` read a
1193
+ `string`.
1194
+
1195
+ `DataSource` implementors are unaffected — TypeScript compares method parameters
1196
+ bivariantly, so an adapter that still declares `id: string | number` continues
1197
+ to satisfy the interface. What changes is the **caller** side: a call that passes
1198
+ a `string | number` to `dataSource.update` is now a type error. A backend whose
1199
+ primary keys are numeric maps them at its own adapter boundary rather than
1200
+ pushing the union through every caller.
1201
+
1202
+ For `TransactionOperation`, the same rule applies one level up: build the
1203
+ operation record with a `string` id. If the id arrives from a numeric-keyed
1204
+ backend, convert it in your adapter — the one place that knows the backend's key
1205
+ type — rather than at each `recordOperation()` call. Nothing about this change
1206
+ alters what is sent over the wire; only the declarations moved.
1207
+ - 269880a: Read the flat REST error envelope in `ObjectStackAdapter`, so a denial that names the
1208
+ missing grant stops arriving as a bare HTTP status word (objectui#9594).
1209
+
1210
+ A console operator who clicked "export to CSV" without the `allowExport` grant was
1211
+ shown one word: `Forbidden`. The server had answered a full sentence — which object,
1212
+ which user, which axis — and the adapter threw it away.
1213
+
1214
+ **Why it was thrown away.** `exportDownload` read its failure through a ladder that
1215
+ knew two envelope dialects: the ADR-0112 nested `error: { message }`, and the flat shape
1216
+ that carries a `message` key. The REST export gate writes a third one — flat, with the
1217
+ sentence in a bare string `error` and no `message` key anywhere — so every rung missed
1218
+ and the ladder fell through to `res.statusText`. The same ladder had been hand-copied at
1219
+ four sites in the module (`searchAll`, `rawFindWithPopulate`, `exportDownload`,
1220
+ `fetchObjectSchemaFresh`), and the two upload paths (`uploadFile`, `uploadFiles`) read a
1221
+ narrower version of it, so all six were blind to the same dialect.
1222
+
1223
+ **The fix is one reader, not six patches.** A single internal `readErrorEnvelope` now
1224
+ holds the dialect knowledge and every one of those six sites routes through it. It is
1225
+ deliberately **not** exported from the package entry point — the published face of
1226
+ `@object-ui/data-objectstack` is unchanged by this release.
1227
+
1228
+ **The rung order is load-bearing.** `message` is consulted before a string-valued
1229
+ `error`, because the REST 401 body is flat *and* carries both keys, with the code word in
1230
+ `error` and the sentence in `message`. Reading `error` first would make an
1231
+ unauthenticated response render the word `UNAUTHENTICATED` instead of its sentence. The
1232
+ 401/403 pair is pinned as a control for exactly that inversion.
1233
+
1234
+ **The machine code now reaches the caller too.** The four `res.ok` ladders attach the
1235
+ server's `code` to the thrown error (`EXPORT_NOT_PERMITTED`, `PERMISSION_DENIED`, …), so
1236
+ a surface has something to discriminate on and an operator has something to search for.
1237
+ `rawFindWithPopulate` already did this; the other three now match it rather than drifting
1238
+ from it. The code is read from `code` or a nested `error.code` and **never** promoted out
1239
+ of a string `error` — a sentence is not a code, and `@objectstack/core`'s own guidance
1240
+ names that chain as where an envelope regression hides. The upload paths keep their
1241
+ declared `UPLOAD_ERROR` code and their own fallback text; only the dialect reading is
1242
+ shared, because their envelope is genuinely a different shape.
1243
+
1244
+ **Not a lenient fallback.** ADR-0112's 2026-07-30 amendment records the flat and the
1245
+ wrapped envelopes as the two live, sanctioned shapes, and D5 leaves the flat one's
1246
+ permanent position an open maintainer question. Every site routed through the reader calls
1247
+ a `/data` route, whose declared answer is the flat family, so reading it is conformance
1248
+ rather than tolerance. The server envelope is untouched — that belongs to the producer's
1249
+ ratchet card in the `objectstack` repo.
1250
+
1251
+ **Blast radius.** Not one route: the REST package answers a wide family of these
1252
+ `{ code, error }` bodies, and `VALIDATION_ERROR` / `PERMISSION_DENIED` / `INVALID_FILTER`
1253
+ arriving as `Bad Request` / `Forbidden` is the same loss with a much wider audience than
1254
+ the export button. The affected-code count quoted on objectui#9594 is a reading taken
1255
+ against one published `@objectstack/rest` version by the scan that card records; nothing
1256
+ in this repo re-derives it, so it is not restated here as a live figure.
1257
+ - b2e85a9: `ObjectStackAdapter.getApp` and `getPage` now address the `app` / `page` metadata
1258
+ types in the singular, matching the other twelve `client.meta.*` call sites in this
1259
+ file (objectui#4940).
1260
+
1261
+ `getApp` (`getItem('apps', …)`) and `probeAppAccess` (`getItem('app', …)`) addressed
1262
+ the same metadata type sixty lines apart, and only `probeAppAccess`'s comment argued
1263
+ its singular spelling was deliberate — the plural site was silent. Both plural sites
1264
+ resolved today only because the server folds plural → singular
1265
+ (`RestServer.metaTypeSingular` via `PLURAL_TO_SINGULAR` from `@objectstack/spec/shared`,
1266
+ confirmed by reading both the mapping and the by-name route handler that calls it), so
1267
+ this is consistency restoration rather than a behavior change — nothing a user hits was
1268
+ broken, and nothing a user hits changes.
1269
+
1270
+ `appAccessProbe.test.ts` (objectui#4252's local pin for this same spelling) is extended
1271
+ with two new cases asserting `getApp`/`getPage` pass the singular type to
1272
+ `client.meta.getItem`, so a future revert to the plural spelling fails a test instead of
1273
+ depending on the server-side fold staying in place.
1274
+ - c7cd2b6: `ObjectStackAdapter.queryDataset` now maps a failed dataset query by the server's
1275
+ ADR-0112 error `code`, not by the HTTP status, so an unknown dataset and an
1276
+ unauthenticated session stop being reported as a missing analytics capability
1277
+ (objectui#5663).
1278
+
1279
+ Two unrelated conditions answer **404** on `POST /api/v1/analytics/dataset/query`:
1280
+ the runtime dispatcher's `ROUTE_NOT_FOUND` when the route was never mounted, and
1281
+ the route's own `NOT_FOUND` when `body.datasetName` matches no saved dataset. The
1282
+ mapping tested `res.status === 501 || res.status === 404` and called all of it
1283
+ "the analytics capability is not installed", so every unknown dataset produced a
1284
+ banner telling the operator to install `@objectstack/service-analytics` and mount
1285
+ `AnalyticsServicePlugin`. Measured live on a prod tenant, that banner was shown on
1286
+ four HotCRM Executive Overview widgets while the analytics service was installed
1287
+ and answering — the real condition was an installed `app.objectstack.hotcrm` at
1288
+ 1.3.0 whose datasets ship in 2.2.2, i.e. a package upgrade, the opposite corner of
1289
+ the system from the remedy the banner named.
1290
+
1291
+ Three conditions now get three answers, each keyed on the code the framework
1292
+ declares for it:
1293
+
1294
+ - `NOT_IMPLEMENTED` (501, route mounted with no analytics service) and
1295
+ `ROUTE_NOT_FOUND` (404, route not mounted) keep the existing
1296
+ `AnalyticsNotInstalledError` and its copy — one remedy, one message.
1297
+ - `NOT_FOUND` (404, unknown `datasetName`) throws the new
1298
+ `AnalyticsDatasetNotFoundError` (`ANALYTICS_DATASET_NOT_FOUND`), naming the
1299
+ dataset and pointing at the installed app's version rather than at the server.
1300
+ - `UNAUTHENTICATED` (401, `enforceAuth`) throws the new
1301
+ `AnalyticsUnauthenticatedError` (`ANALYTICS_UNAUTHENTICATED`), which says the
1302
+ request was refused before it ran and therefore says nothing about the
1303
+ capability.
1304
+
1305
+ The banner also used to print the server's own message in a parenthetical while
1306
+ contradicting it in the headline — it quoted `Dataset "opportunity_metrics" not
1307
+ found.` under a headline claiming a missing capability. That is now structurally
1308
+ impossible rather than merely fixed: the headline is a pure function of `code` and
1309
+ the parenthetical is a verbatim quote of `message`, both read off the same
1310
+ response, and a test walks every branch asserting each message carries its own
1311
+ headline and none of the others'.
1312
+
1313
+ Additive only. `AnalyticsNotInstalledError` keeps its `code`, its copy and its
1314
+ constructor signature (it gains an optional third `serverCode` argument and a
1315
+ `serverCode` field), so consumers matching `ANALYTICS_NOT_INSTALLED` — including
1316
+ the metadata-admin dataset preview — are unaffected. A 404 carrying a code this
1317
+ client does not recognise, such as the analytics cube gate's `CUBE_NOT_FOUND`, now
1318
+ keeps its server detail instead of being relabelled as a missing capability; a 404
1319
+ or 501 carrying no code at all is still read as the capability being absent, since
1320
+ the route's own `NOT_FOUND` always ships a code.
1321
+ - 617707a: `convertFiltersToAST` lowers the `$and` / `$or` combinators to real ObjectQL AST
1322
+ group nodes (objectui#6948).
1323
+
1324
+ `FilterCondition` declares `$and` / `$or` / `$not`, and this repo's one lowering
1325
+ had no branch for any of them. `$and` / `$or` fell through to the
1326
+ simple-equality branch and became a leaf naming a field literally called `$and`
1327
+ / `$or`. That leaf reached the server intact — `parseFilterAST` reads
1328
+ `['$or', '=', [...]]` back as a real `$or`, so the wire condition was correct
1329
+ and is unchanged by this release — but it is a well-formed *comparison* node, so
1330
+ every AST evaluator in this repo read `$or` as a field name, found no such key
1331
+ on any record, and returned an EMPTY list with no error. Producers that reach
1332
+ this today include `mergeFilters` (dashboard scope broadcast, dataset report
1333
+ blocks), `FilterConditionField`, and `Field.relatedListFilter`.
1334
+
1335
+ `minor` rather than `patch`: shipped results move. A list filtered by a
1336
+ combinator through any in-process data source went from zero rows to the rows
1337
+ the author asked for, and an unknown or refused operator *inside* a combinator
1338
+ branch — which used to travel to the wire unchecked inside the leaf's value slot
1339
+ — is now refused at the same door as every other operator.
1340
+
1341
+ `$not` is refused with an accurate message instead of translated: the AST has no
1342
+ negation keyword (`FILTER_ARRAY_LOGIC_KEYWORDS` is `['and', 'or']`) and several
1343
+ operators it carries have no negated counterpart, so a rewrite would be silently
1344
+ partial. It threw before this change too, naming the author's own nested field
1345
+ as a bogus operator; the verdict is unchanged, only the diagnostic.
1346
+ - 8d37efb: The metadata lock banner can no longer render an amber, padlocked box with no
1347
+ title, and the ADR-0010 §3.6 lock vocabulary is declared once instead of three
1348
+ times (objectui#5024).
1349
+
1350
+ `MetadataLayered.lock` and `MetadataAuditEntry.lockState` each spelled the four
1351
+ states out by hand, 42 lines apart in one file, compared by no gate. They are now
1352
+ one exported `MetadataLockState` — derived from `GetMetaItemLayeredResponseSchema`'s
1353
+ `z.enum` in `@objectstack/spec`, which already owns this vocabulary, so the copies
1354
+ were restating a schema rather than filling a gap.
1355
+
1356
+ The user-visible half is the banner. Its title was three independent `&&` branches
1357
+ with no fallback, while the switch that opens the banner is true for any non-`none`
1358
+ value — so a lock state outside the four opened the box and left the headline
1359
+ empty. That is reachable without a fifth state ever being added here:
1360
+ `MetadataClient.layered()` casts the wire value through unchecked, so a newer
1361
+ server reaches this banner as-is. Measured, not assumed — feeding `no-publish`
1362
+ through the page rendered the padlock, the border and an empty title. The title is
1363
+ now a keyed lookup with a loud fallback that names the unrecognised token, so a
1364
+ fifth state fails `type-check` here and, if one arrives from a server anyway, the
1365
+ operator reads a sentence instead of a blank box.
1366
+ - 9118a31: Preserve the producer's `userMessage` marking when `normaliseClientError` re-wraps a refusal.
1367
+
1368
+ `ApiErrorSchema.userMessage` (objectstack `79c46da90`) is the opt-in channel an application author
1369
+ sets at throw time to say "this text is for the end user", and the contract states it
1370
+ status-agnostic — any refusal status may carry it. Both of the shapes this adapter re-wraps
1371
+ into typed errors dropped the marking: a hook that refused a write with `VALIDATION_FAILED`
1372
+ or `CONCURRENT_UPDATE` and marked its own sentence had that sentence discarded at the
1373
+ adapter boundary, before any surface could render it. Nothing threw and the typed error was
1374
+ otherwise correct, so the only symptom was the user reading a generic string instead of the
1375
+ sentence their administrator wrote.
1376
+
1377
+ The marking now rides both re-wraps, in the form the shared reader (`declaredUserMessage`)
1378
+ already looks for: on the details bag for `DataApiValidationError`, exactly the way `fields`
1379
+ already survives, and on a new readonly `userMessage` member for `ConcurrentUpdateError`,
1380
+ which has no details bag. Unmarked refusals are untouched — they carry no key and still read
1381
+ as `null`, so nothing a producer did not opt into can reach a user.
1382
+ - 7b43319: **Fix:** `MetadataClient.publishDraft` no longer unwraps a `{ success, data }`
1383
+ envelope, so it and `MetadataClient.publish` hold ONE belief about the route
1384
+ they share.
1385
+
1386
+ The two methods sit ~250 lines apart in `metadata-client.ts` and both POST
1387
+ `/api/v1/meta/:type/:name/publish`. `publishDraft` tolerated a dispatcher-shaped
1388
+ envelope and returned the inner object; `publish` returned the body as parsed.
1389
+ Nothing said which was right, and the card explicitly refused to settle it from
1390
+ `PublishMetaItemResponseSchema` alone — an inference from a declaration is not a
1391
+ measurement of the server.
1392
+
1393
+ So the producer was read instead, in the framework checkout:
1394
+
1395
+ - `POST /api/v1/meta/:type/:name/publish` has exactly ONE mount,
1396
+ `packages/rest/src/rest-server.ts`, whose handler ends
1397
+ `res.json(await p.publishMetaItem(publishRequest))` — the protocol object
1398
+ verbatim, no envelope branch on any arm. The ADR-0006 project-scoped base
1399
+ re-mounts that same handler.
1400
+ - The `{ success, data }` envelope has ONE producer, `HttpDispatcher.success()`,
1401
+ and it does not serve this route: no publish branch in `runtime`'s `/meta`
1402
+ domain (its three-segment arm is `/published`, GET only), no row in the
1403
+ dispatcher's route ledger, and a three-segment `/meta` path that is not
1404
+ `/published` terminates in a located `routeNotFound`. That holds for the
1405
+ dispatcher-only Hono adapter as much as for a full `rest` +
1406
+ `plugin-hono-server` boot, where the REST mount shadows the catch-all.
1407
+ - `packages/spec` declares the split in so many words:
1408
+ `PublishMetaItemResponseSchema` documents "the FULL body" of this route and
1409
+ records that the REST route hands the producer's object to `res.json()`
1410
+ verbatim, while the batch sibling `PublishPackageDraftsResponseSchema`
1411
+ documents a body answered "inside the dispatcher's `{ success, data }`
1412
+ envelope".
1413
+
1414
+ The route never envelopes, so the tolerance was a dialect with no producer —
1415
+ Commandment #0.1's lenient fallback, and the kind that fails in the direction
1416
+ that hides the problem: a body arriving enveloped is one this door did not
1417
+ serve, and unwrapping it presents that as a successful promotion.
1418
+
1419
+ **Behaviour delta, stated plainly.** Handed an enveloped body, `publishDraft`
1420
+ used to return the inner object and now returns the body as-is. Graded `patch`
1421
+ because that input is not one any measured configuration emits: on a conformant
1422
+ response — which has no `data` member at all — the removed branch was already a
1423
+ no-op, so `seedApplied`, `version` and `seq` read off both methods exactly as
1424
+ before. Sibling tolerances in the same file are untouched and still correct:
1425
+ `listDrafts` reads `data?.data?.drafts` because `GET /meta/_drafts` really is a
1426
+ dispatcher route, and `publishHealthFromResponse` unwraps because the batch
1427
+ publish door really is enveloped. The rule is per-route, not per-file.
1428
+ - f75810e: Parse a `droppedFields` wire entry's `fields` elements and its `object` at the
1429
+ write-warning boundary instead of asserting them.
1430
+
1431
+ The structural gate checked `Array.isArray(fields) && fields.length > 0` and then
1432
+ asserted the entry into a type declaring `fields: string[]` and a required
1433
+ `object: string` — reading neither. A response carrying `fields: [42]` reached
1434
+ `onWriteWarning` subscribers typed as a field name (the shell rendered it as the
1435
+ label `42`), and an entry that omitted `object` arrived claiming a string that was
1436
+ not there.
1437
+
1438
+ Now the wire type declares only what the gate establishes, and the notice is
1439
+ parsed: non-string `fields` elements are refused, an entry naming no field at all
1440
+ is dropped as `fields: []` already was, and a missing or non-string `object` is
1441
+ healed from the object the write targeted. Warnings are never silenced for a
1442
+ field the server really did name. No published type changes — `WriteWarningEvent`
1443
+ and `DroppedFieldsNotice` keep their shapes, and a subscriber's `fields: string[]`
1444
+ is now true rather than asserted.
1445
+ - Updated dependencies [97abedc]
1446
+ - Updated dependencies [b46c58f]
1447
+ - Updated dependencies [ad694ac]
1448
+ - Updated dependencies [6f96fca]
1449
+ - Updated dependencies [c131d9e]
1450
+ - Updated dependencies [5f00ff4]
1451
+ - Updated dependencies [c9e073a]
1452
+ - Updated dependencies [7b395d8]
1453
+ - Updated dependencies [0879812]
1454
+ - Updated dependencies [8cedb0d]
1455
+ - Updated dependencies [6cc910b]
1456
+ - Updated dependencies [061f5e8]
1457
+ - Updated dependencies [2dd4d3f]
1458
+ - Updated dependencies [4ab4f1b]
1459
+ - Updated dependencies [e3ea4f9]
1460
+ - Updated dependencies [8b1f066]
1461
+ - Updated dependencies [af243c1]
1462
+ - Updated dependencies [961ceaa]
1463
+ - Updated dependencies [f3f4e4c]
1464
+ - Updated dependencies [a05c350]
1465
+ - Updated dependencies [8c10f4f]
1466
+ - Updated dependencies [90dac98]
1467
+ - Updated dependencies [6096f20]
1468
+ - Updated dependencies [544aca2]
1469
+ - Updated dependencies [ea02938]
1470
+ - Updated dependencies [a14fb23]
1471
+ - Updated dependencies [ae98f1d]
1472
+ - Updated dependencies [f98eddf]
1473
+ - Updated dependencies [ce6bd99]
1474
+ - Updated dependencies [a5b08c9]
1475
+ - Updated dependencies [86982ac]
1476
+ - Updated dependencies [ff14e29]
1477
+ - Updated dependencies [9b28151]
1478
+ - Updated dependencies [64563a9]
1479
+ - Updated dependencies [1a5003f]
1480
+ - Updated dependencies [8acc51b]
1481
+ - Updated dependencies [3261e64]
1482
+ - Updated dependencies [d22b37b]
1483
+ - Updated dependencies [1daf477]
1484
+ - Updated dependencies [6c2f3c5]
1485
+ - Updated dependencies [fb13e85]
1486
+ - Updated dependencies [c2d8659]
1487
+ - Updated dependencies [e0f8202]
1488
+ - Updated dependencies [c3a26cc]
1489
+ - Updated dependencies [a66e58e]
1490
+ - Updated dependencies [d89492c]
1491
+ - Updated dependencies [9a5f998]
1492
+ - Updated dependencies [9327397]
1493
+ - Updated dependencies [17cc3a3]
1494
+ - Updated dependencies [4758b33]
1495
+ - Updated dependencies [9c78ebe]
1496
+ - Updated dependencies [12809a5]
1497
+ - Updated dependencies [f9c06ef]
1498
+ - Updated dependencies [5ad3b88]
1499
+ - Updated dependencies [f9d772b]
1500
+ - Updated dependencies [97b6c21]
1501
+ - Updated dependencies [baac95a]
1502
+ - Updated dependencies [29b45f6]
1503
+ - Updated dependencies [39b8d51]
1504
+ - Updated dependencies [17b323e]
1505
+ - Updated dependencies [b956e69]
1506
+ - Updated dependencies [33e58d8]
1507
+ - Updated dependencies [256b4c9]
1508
+ - Updated dependencies [fec3b1a]
1509
+ - Updated dependencies [b8e0941]
1510
+ - Updated dependencies [0c50f18]
1511
+ - Updated dependencies [1dae95a]
1512
+ - Updated dependencies [e32dae1]
1513
+ - Updated dependencies [30b11ad]
1514
+ - Updated dependencies [4aebea0]
1515
+ - Updated dependencies [f976774]
1516
+ - Updated dependencies [3d6badf]
1517
+ - Updated dependencies [25cb364]
1518
+ - Updated dependencies [c6678b1]
1519
+ - Updated dependencies [0638322]
1520
+ - Updated dependencies [e3782d2]
1521
+ - Updated dependencies [db0beb2]
1522
+ - Updated dependencies [997ce38]
1523
+ - Updated dependencies [ae0b9d3]
1524
+ - Updated dependencies [3b469c8]
1525
+ - Updated dependencies [990a2d6]
1526
+ - Updated dependencies [6650259]
1527
+ - Updated dependencies [4f8b7f8]
1528
+ - Updated dependencies [9e6619f]
1529
+ - Updated dependencies [f6ae5e2]
1530
+ - Updated dependencies [7343376]
1531
+ - Updated dependencies [b2683a2]
1532
+ - Updated dependencies [dded788]
1533
+ - Updated dependencies [b45d463]
1534
+ - Updated dependencies [54a7830]
1535
+ - Updated dependencies [f3135a4]
1536
+ - Updated dependencies [b5696d3]
1537
+ - Updated dependencies [3f9d926]
1538
+ - Updated dependencies [e978ed5]
1539
+ - Updated dependencies [6a7f24e]
1540
+ - Updated dependencies [b3c96d6]
1541
+ - Updated dependencies [8d0ca91]
1542
+ - Updated dependencies [c30c8dd]
1543
+ - Updated dependencies [328abeb]
1544
+ - Updated dependencies [24d3e65]
1545
+ - Updated dependencies [95a7c8d]
1546
+ - Updated dependencies [e227156]
1547
+ - Updated dependencies [cc4e476]
1548
+ - Updated dependencies [92970c4]
1549
+ - Updated dependencies [d570eaa]
1550
+ - Updated dependencies [42687ba]
1551
+ - Updated dependencies [6cd8f66]
1552
+ - Updated dependencies [24a0f14]
1553
+ - Updated dependencies [797a30f]
1554
+ - Updated dependencies [b4075c0]
1555
+ - Updated dependencies [9b85600]
1556
+ - Updated dependencies [99878d8]
1557
+ - Updated dependencies [3c13675]
1558
+ - Updated dependencies [63ab761]
1559
+ - Updated dependencies [0eb9f36]
1560
+ - Updated dependencies [ae582b7]
1561
+ - Updated dependencies [db11afd]
1562
+ - Updated dependencies [154075a]
1563
+ - Updated dependencies [582edef]
1564
+ - Updated dependencies [19f484f]
1565
+ - Updated dependencies [0a78a20]
1566
+ - Updated dependencies [615346d]
1567
+ - Updated dependencies [75dcc81]
1568
+ - Updated dependencies [55a12a8]
1569
+ - Updated dependencies [edfcf5a]
1570
+ - Updated dependencies [0a3e540]
1571
+ - Updated dependencies [f61dab1]
1572
+ - Updated dependencies [b0a05dd]
1573
+ - Updated dependencies [dd5ff19]
1574
+ - Updated dependencies [81f8498]
1575
+ - Updated dependencies [a782fa7]
1576
+ - Updated dependencies [76e9df0]
1577
+ - Updated dependencies [6158e4c]
1578
+ - Updated dependencies [c27b575]
1579
+ - Updated dependencies [0e6e76b]
1580
+ - Updated dependencies [bf43afa]
1581
+ - Updated dependencies [858eafb]
1582
+ - Updated dependencies [cd5b19a]
1583
+ - Updated dependencies [17dc167]
1584
+ - Updated dependencies [20d23be]
1585
+ - Updated dependencies [20d23be]
1586
+ - Updated dependencies [e6bc087]
1587
+ - Updated dependencies [a7557a7]
1588
+ - Updated dependencies [7d074ba]
1589
+ - Updated dependencies [6158e4c]
1590
+ - Updated dependencies [6158e4c]
1591
+ - Updated dependencies [52aad5c]
1592
+ - Updated dependencies [58da8ae]
1593
+ - Updated dependencies [138ad45]
1594
+ - Updated dependencies [138ad45]
1595
+ - Updated dependencies [5262f7d]
1596
+ - Updated dependencies [6aa029b]
1597
+ - Updated dependencies [770cc5b]
1598
+ - Updated dependencies [1a88ce2]
1599
+ - Updated dependencies [a1a44d6]
1600
+ - Updated dependencies [e0a9c67]
1601
+ - Updated dependencies [5638529]
1602
+ - Updated dependencies [5638529]
1603
+ - Updated dependencies [c476be0]
1604
+ - Updated dependencies [c82ff39]
1605
+ - Updated dependencies [6c3da53]
1606
+ - Updated dependencies [31987bd]
1607
+ - Updated dependencies [3c3ce15]
1608
+ - Updated dependencies [063119f]
1609
+ - Updated dependencies [e100589]
1610
+ - Updated dependencies [304f611]
1611
+ - Updated dependencies [e46ee77]
1612
+ - Updated dependencies [f9c8c4e]
1613
+ - Updated dependencies [6e9c8d2]
1614
+ - Updated dependencies [9547063]
1615
+ - Updated dependencies [3f6efd6]
1616
+ - Updated dependencies [c4ab6d0]
1617
+ - Updated dependencies [0e9058b]
1618
+ - Updated dependencies [5988b6b]
1619
+ - Updated dependencies [00ccdf7]
1620
+ - Updated dependencies [9d9ed54]
1621
+ - Updated dependencies [ca3de72]
1622
+ - Updated dependencies [83e3f83]
1623
+ - Updated dependencies [401611b]
1624
+ - Updated dependencies [2c0ddf2]
1625
+ - Updated dependencies [4abc0aa]
1626
+ - Updated dependencies [f560ded]
1627
+ - Updated dependencies [2b188fa]
1628
+ - Updated dependencies [f68e0a0]
1629
+ - Updated dependencies [aea682a]
1630
+ - Updated dependencies [fcdc8ec]
1631
+ - Updated dependencies [2d576e4]
1632
+ - Updated dependencies [8366acc]
1633
+ - Updated dependencies [95e58a3]
1634
+ - Updated dependencies [9d7419b]
1635
+ - Updated dependencies [fc7db05]
1636
+ - Updated dependencies [9ed8d0f]
1637
+ - Updated dependencies [c73cdb5]
1638
+ - Updated dependencies [6f5719e]
1639
+ - Updated dependencies [64dae8e]
1640
+ - Updated dependencies [06a8af5]
1641
+ - Updated dependencies [6a91586]
1642
+ - Updated dependencies [a04d7c6]
1643
+ - Updated dependencies [f3c2bb0]
1644
+ - Updated dependencies [9801765]
1645
+ - Updated dependencies [9cebfca]
1646
+ - Updated dependencies [460575f]
1647
+ - Updated dependencies [d88e20f]
1648
+ - Updated dependencies [2d7304d]
1649
+ - Updated dependencies [636b236]
1650
+ - Updated dependencies [d6d8fb9]
1651
+ - Updated dependencies [64d624d]
1652
+ - Updated dependencies [053fdc8]
1653
+ - Updated dependencies [ae476b8]
1654
+ - Updated dependencies [95bad12]
1655
+ - Updated dependencies [d2fb6ef]
1656
+ - Updated dependencies [fda49e5]
1657
+ - Updated dependencies [490d9a9]
1658
+ - Updated dependencies [fc62bb4]
1659
+ - Updated dependencies [41df893]
1660
+ - Updated dependencies [0cba1b7]
1661
+ - Updated dependencies [00f3eb5]
1662
+ - Updated dependencies [1ec291c]
1663
+ - Updated dependencies [453dbaa]
1664
+ - Updated dependencies [69a2163]
1665
+ - Updated dependencies [24e027e]
1666
+ - Updated dependencies [2c3cd1b]
1667
+ - Updated dependencies [90665e0]
1668
+ - Updated dependencies [7e19d03]
1669
+ - Updated dependencies [1e946c9]
1670
+ - Updated dependencies [546ddf7]
1671
+ - Updated dependencies [864154e]
1672
+ - Updated dependencies [b023625]
1673
+ - Updated dependencies [75bd83d]
1674
+ - Updated dependencies [44d075b]
1675
+ - Updated dependencies [40c479a]
1676
+ - Updated dependencies [971d387]
1677
+ - Updated dependencies [ee851c3]
1678
+ - Updated dependencies [6414dfd]
1679
+ - Updated dependencies [a8d5c71]
1680
+ - Updated dependencies [905b21f]
1681
+ - Updated dependencies [88e9109]
1682
+ - Updated dependencies [2c45966]
1683
+ - Updated dependencies [db3a600]
1684
+ - Updated dependencies [3a3db76]
1685
+ - Updated dependencies [0d723a3]
1686
+ - Updated dependencies [0c95d3d]
1687
+ - Updated dependencies [3e4fa2c]
1688
+ - Updated dependencies [b5b928a]
1689
+ - Updated dependencies [52a43de]
1690
+ - Updated dependencies [195052f]
1691
+ - Updated dependencies [e4559d1]
1692
+ - Updated dependencies [2c71482]
1693
+ - Updated dependencies [a26b9e4]
1694
+ - Updated dependencies [5ef9c4f]
1695
+ - Updated dependencies [46f0bb4]
1696
+ - Updated dependencies [06b82b8]
1697
+ - Updated dependencies [6f81384]
1698
+ - Updated dependencies [8f1d995]
1699
+ - Updated dependencies [dddb942]
1700
+ - Updated dependencies [29754cf]
1701
+ - Updated dependencies [3c2b6f7]
1702
+ - Updated dependencies [b84dc18]
1703
+ - Updated dependencies [ac8abb0]
1704
+ - Updated dependencies [9d86e1d]
1705
+ - Updated dependencies [3a5817f]
1706
+ - Updated dependencies [99a3c2d]
1707
+ - Updated dependencies [5961030]
1708
+ - Updated dependencies [c8ea8af]
1709
+ - Updated dependencies [3190414]
1710
+ - Updated dependencies [4e480f5]
1711
+ - Updated dependencies [38a123c]
1712
+ - Updated dependencies [299102e]
1713
+ - Updated dependencies [d7acad6]
1714
+ - Updated dependencies [45a9aeb]
1715
+ - Updated dependencies [713db46]
1716
+ - Updated dependencies [bf3a03c]
1717
+ - Updated dependencies [cb55718]
1718
+ - Updated dependencies [831be72]
1719
+ - Updated dependencies [29cb85b]
1720
+ - Updated dependencies [3e028c8]
1721
+ - Updated dependencies [d0889e2]
1722
+ - Updated dependencies [ce503e5]
1723
+ - Updated dependencies [f20dcf0]
1724
+ - Updated dependencies [4ca30d0]
1725
+ - Updated dependencies [7a5da14]
1726
+ - Updated dependencies [2c1c967]
1727
+ - Updated dependencies [4d5f9b4]
1728
+ - Updated dependencies [d6ceb8d]
1729
+ - Updated dependencies [7977ff9]
1730
+ - Updated dependencies [3beef6d]
1731
+ - Updated dependencies [2acd8e1]
1732
+ - Updated dependencies [045d20b]
1733
+ - Updated dependencies [a2d2515]
1734
+ - Updated dependencies [adb2a86]
1735
+ - Updated dependencies [3619792]
1736
+ - Updated dependencies [3561bd2]
1737
+ - Updated dependencies [bf97b98]
1738
+ - Updated dependencies [b0d308d]
1739
+ - Updated dependencies [40f34b4]
1740
+ - Updated dependencies [8063bcb]
1741
+ - Updated dependencies [b74a859]
1742
+ - Updated dependencies [d4493fd]
1743
+ - Updated dependencies [240b80f]
1744
+ - Updated dependencies [77cb489]
1745
+ - Updated dependencies [bfaa158]
1746
+ - Updated dependencies [777e5c6]
1747
+ - Updated dependencies [0c386dd]
1748
+ - Updated dependencies [9e37d9b]
1749
+ - Updated dependencies [5ad86dd]
1750
+ - Updated dependencies [16a725f]
1751
+ - Updated dependencies [4dfdcc3]
1752
+ - Updated dependencies [446d93d]
1753
+ - Updated dependencies [ecd9cb2]
1754
+ - Updated dependencies [98d4108]
1755
+ - Updated dependencies [0e3b3be]
1756
+ - Updated dependencies [a29ae2d]
1757
+ - Updated dependencies [4388f71]
1758
+ - Updated dependencies [0b1ac58]
1759
+ - Updated dependencies [c93b4d5]
1760
+ - Updated dependencies [c1fe272]
1761
+ - Updated dependencies [8ad218d]
1762
+ - Updated dependencies [3e41187]
1763
+ - Updated dependencies [5f78953]
1764
+ - Updated dependencies [639114c]
1765
+ - Updated dependencies [1f31d3a]
1766
+ - Updated dependencies [351eb31]
1767
+ - Updated dependencies [20c04b2]
1768
+ - Updated dependencies [48c19bd]
1769
+ - Updated dependencies [a6d8b8d]
1770
+ - Updated dependencies [b652514]
1771
+ - Updated dependencies [adbda1b]
1772
+ - Updated dependencies [e2b3826]
1773
+ - Updated dependencies [2e32ed4]
1774
+ - Updated dependencies [e75f4c9]
1775
+ - Updated dependencies [19f1639]
1776
+ - Updated dependencies [47547d0]
1777
+ - Updated dependencies [1bee5d0]
1778
+ - Updated dependencies [858cd72]
1779
+ - Updated dependencies [cfc9b6d]
1780
+ - Updated dependencies [554f2b6]
1781
+ - Updated dependencies [669d71b]
1782
+ - Updated dependencies [ed27d7c]
1783
+ - Updated dependencies [52c8cf7]
1784
+ - Updated dependencies [2ceb43a]
1785
+ - Updated dependencies [7cdd2b9]
1786
+ - Updated dependencies [52c8cf7]
1787
+ - Updated dependencies [81a2eb1]
1788
+ - Updated dependencies [caa0cd3]
1789
+ - Updated dependencies [25c7d58]
1790
+ - Updated dependencies [00d2fa6]
1791
+ - Updated dependencies [c6198c2]
1792
+ - Updated dependencies [721d1e0]
1793
+ - Updated dependencies [1237ae4]
1794
+ - Updated dependencies [51eb515]
1795
+ - Updated dependencies [c354ce5]
1796
+ - Updated dependencies [8fe8e5c]
1797
+ - Updated dependencies [feac439]
1798
+ - Updated dependencies [efbd566]
1799
+ - Updated dependencies [9587fc9]
1800
+ - Updated dependencies [e62c44e]
1801
+ - Updated dependencies [5d0876c]
1802
+ - Updated dependencies [b041b9c]
1803
+ - Updated dependencies [ce2aaef]
1804
+ - Updated dependencies [544ecba]
1805
+ - Updated dependencies [bc640ec]
1806
+ - Updated dependencies [1e215c4]
1807
+ - Updated dependencies [da6e191]
1808
+ - Updated dependencies [3e377c9]
1809
+ - Updated dependencies [a3eb5d0]
1810
+ - Updated dependencies [4ce14f1]
1811
+ - Updated dependencies [aef97e5]
1812
+ - Updated dependencies [2af1fa7]
1813
+ - Updated dependencies [a137d0c]
1814
+ - Updated dependencies [caf477f]
1815
+ - Updated dependencies [f6375da]
1816
+ - Updated dependencies [967e5d8]
1817
+ - Updated dependencies [a4611b3]
1818
+ - Updated dependencies [20316ba]
1819
+ - Updated dependencies [d3499b3]
1820
+ - Updated dependencies [309c75e]
1821
+ - Updated dependencies [c9f9bae]
1822
+ - Updated dependencies [18897a4]
1823
+ - Updated dependencies [8b7ea39]
1824
+ - Updated dependencies [dcbf0b2]
1825
+ - Updated dependencies [52cac38]
1826
+ - Updated dependencies [93fea2e]
1827
+ - Updated dependencies [1422a92]
1828
+ - Updated dependencies [d05fe17]
1829
+ - Updated dependencies [a480f79]
1830
+ - Updated dependencies [f08d1a8]
1831
+ - Updated dependencies [64a252d]
1832
+ - Updated dependencies [786bc91]
1833
+ - Updated dependencies [75fca96]
1834
+ - Updated dependencies [7ca6ddd]
1835
+ - Updated dependencies [f1cd290]
1836
+ - Updated dependencies [5a41ce7]
1837
+ - Updated dependencies [8d50bc2]
1838
+ - Updated dependencies [604476d]
1839
+ - Updated dependencies [335abea]
1840
+ - Updated dependencies [0f5cadf]
1841
+ - Updated dependencies [4f9f1ee]
1842
+ - Updated dependencies [66e8b2a]
1843
+ - Updated dependencies [aa083cd]
1844
+ - Updated dependencies [12b5992]
1845
+ - Updated dependencies [b93e245]
1846
+ - Updated dependencies [c842594]
1847
+ - Updated dependencies [290de37]
1848
+ - Updated dependencies [8c8da45]
1849
+ - Updated dependencies [8cd8eb5]
1850
+ - Updated dependencies [cf1d29e]
1851
+ - Updated dependencies [1bd79c8]
1852
+ - Updated dependencies [af9e957]
1853
+ - Updated dependencies [c974edf]
1854
+ - Updated dependencies [ad852b6]
1855
+ - Updated dependencies [ee4d19f]
1856
+ - Updated dependencies [496d31d]
1857
+ - Updated dependencies [0ea7054]
1858
+ - Updated dependencies [9a853f2]
1859
+ - Updated dependencies [cb847fd]
1860
+ - Updated dependencies [ee70287]
1861
+ - Updated dependencies [3e98e13]
1862
+ - Updated dependencies [fc32921]
1863
+ - Updated dependencies [4eaa835]
1864
+ - Updated dependencies [8f9d87a]
1865
+ - Updated dependencies [b1777ae]
1866
+ - Updated dependencies [24845c4]
1867
+ - Updated dependencies [6f864cf]
1868
+ - Updated dependencies [24d1edd]
1869
+ - Updated dependencies [645087c]
1870
+ - Updated dependencies [33f4a19]
1871
+ - Updated dependencies [5323168]
1872
+ - Updated dependencies [841dd2b]
1873
+ - Updated dependencies [3014fc0]
1874
+ - Updated dependencies [dacb402]
1875
+ - Updated dependencies [846cec0]
1876
+ - Updated dependencies [91facae]
1877
+ - Updated dependencies [b38014e]
1878
+ - Updated dependencies [474797d]
1879
+ - Updated dependencies [704e695]
1880
+ - Updated dependencies [a407bd6]
1881
+ - Updated dependencies [317dbce]
1882
+ - Updated dependencies [3a43a15]
1883
+ - Updated dependencies [f76f436]
1884
+ - Updated dependencies [ce45a03]
1885
+ - Updated dependencies [421544b]
1886
+ - Updated dependencies [fb01022]
1887
+ - Updated dependencies [e9d9212]
1888
+ - Updated dependencies [ecfb693]
1889
+ - Updated dependencies [abc1b18]
1890
+ - Updated dependencies [81a51db]
1891
+ - Updated dependencies [67749c7]
1892
+ - Updated dependencies [507b61b]
1893
+ - Updated dependencies [512c84b]
1894
+ - Updated dependencies [fb3a101]
1895
+ - Updated dependencies [d4733f2]
1896
+ - Updated dependencies [f391ede]
1897
+ - Updated dependencies [f5cfbbd]
1898
+ - Updated dependencies [f5cfbbd]
1899
+ - Updated dependencies [8b532cb]
1900
+ - Updated dependencies [64c3cdd]
1901
+ - Updated dependencies [4d65991]
1902
+ - Updated dependencies [c42554e]
1903
+ - Updated dependencies [555b4ec]
1904
+ - Updated dependencies [1ccfc23]
1905
+ - Updated dependencies [542718f]
1906
+ - Updated dependencies [7f27bc5]
1907
+ - Updated dependencies [0a174f3]
1908
+ - Updated dependencies [f95b140]
1909
+ - Updated dependencies [541ce4e]
1910
+ - Updated dependencies [6479086]
1911
+ - Updated dependencies [d79f525]
1912
+ - Updated dependencies [d1865d2]
1913
+ - Updated dependencies [f1190b0]
1914
+ - Updated dependencies [561abef]
1915
+ - Updated dependencies [6a4680b]
1916
+ - Updated dependencies [c3a4273]
1917
+ - Updated dependencies [abf710d]
1918
+ - Updated dependencies [093af32]
1919
+ - Updated dependencies [1bd1be7]
1920
+ - Updated dependencies [d234fa9]
1921
+ - Updated dependencies [adf5812]
1922
+ - Updated dependencies [5058336]
1923
+ - Updated dependencies [2f6b2bf]
1924
+ - Updated dependencies [2028b31]
1925
+ - Updated dependencies [63601ab]
1926
+ - Updated dependencies [c372b29]
1927
+ - Updated dependencies [8693b85]
1928
+ - Updated dependencies [58b7b3d]
1929
+ - Updated dependencies [84defab]
1930
+ - Updated dependencies [681d3f1]
1931
+ - Updated dependencies [969d4f2]
1932
+ - Updated dependencies [f3bc481]
1933
+ - Updated dependencies [b79aac2]
1934
+ - Updated dependencies [93fc0e7]
1935
+ - Updated dependencies [a4b723f]
1936
+ - Updated dependencies [2b10ca0]
1937
+ - Updated dependencies [7db4a81]
1938
+ - Updated dependencies [19a0b0e]
1939
+ - Updated dependencies [526fc11]
1940
+ - Updated dependencies [f8e3e9a]
1941
+ - Updated dependencies [3b6bc69]
1942
+ - Updated dependencies [6732df4]
1943
+ - Updated dependencies [fe9e0d0]
1944
+ - Updated dependencies [63fb72c]
1945
+ - Updated dependencies [804831c]
1946
+ - Updated dependencies [279e48e]
1947
+ - Updated dependencies [8700d6d]
1948
+ - Updated dependencies [8db2a0f]
1949
+ - Updated dependencies [30443fb]
1950
+ - Updated dependencies [da45e6b]
1951
+ - Updated dependencies [835f0f3]
1952
+ - Updated dependencies [ed35b44]
1953
+ - Updated dependencies [729e851]
1954
+ - Updated dependencies [96919a4]
1955
+ - Updated dependencies [20b507a]
1956
+ - Updated dependencies [2e471dc]
1957
+ - Updated dependencies [be50942]
1958
+ - Updated dependencies [775e079]
1959
+ - Updated dependencies [7e8b3c0]
1960
+ - Updated dependencies [53374dc]
1961
+ - Updated dependencies [f6fb83f]
1962
+ - Updated dependencies [2049b03]
1963
+ - Updated dependencies [7cbc724]
1964
+ - Updated dependencies [fb91ac9]
1965
+ - Updated dependencies [8524372]
1966
+ - Updated dependencies [7cbefa5]
1967
+ - Updated dependencies [72d6587]
1968
+ - Updated dependencies [a272a4f]
1969
+ - Updated dependencies [55f39ee]
1970
+ - Updated dependencies [0970a0e]
1971
+ - Updated dependencies [e427e9c]
1972
+ - Updated dependencies [bbc9dc3]
1973
+ - Updated dependencies [02f1813]
1974
+ - Updated dependencies [ba0b61a]
1975
+ - Updated dependencies [ac716ff]
1976
+ - Updated dependencies [f0f3cd5]
1977
+ - Updated dependencies [ab856ed]
1978
+ - Updated dependencies [20f3e65]
1979
+ - Updated dependencies [bbba098]
1980
+ - Updated dependencies [87af769]
1981
+ - Updated dependencies [3be720e]
1982
+ - Updated dependencies [43c0d17]
1983
+ - Updated dependencies [c3df43a]
1984
+ - Updated dependencies [d16d0e9]
1985
+ - Updated dependencies [bbe57fd]
1986
+ - Updated dependencies [272a530]
1987
+ - Updated dependencies [1779e8d]
1988
+ - Updated dependencies [f0f4d6c]
1989
+ - Updated dependencies [4128188]
1990
+ - Updated dependencies [b253c4e]
1991
+ - Updated dependencies [78a9c67]
1992
+ - Updated dependencies [4a7ef0d]
1993
+ - Updated dependencies [dea17b4]
1994
+ - Updated dependencies [89bb77a]
1995
+ - Updated dependencies [06611e4]
1996
+ - Updated dependencies [dc3893d]
1997
+ - Updated dependencies [1bbaa16]
1998
+ - Updated dependencies [6ee259a]
1999
+ - Updated dependencies [e708426]
2000
+ - Updated dependencies [3b6d53b]
2001
+ - Updated dependencies [276d174]
2002
+ - Updated dependencies [2982ed9]
2003
+ - Updated dependencies [a8198de]
2004
+ - Updated dependencies [05a49f2]
2005
+ - Updated dependencies [a78cd37]
2006
+ - Updated dependencies [5ea623e]
2007
+ - Updated dependencies [5eabe86]
2008
+ - Updated dependencies [ca5d671]
2009
+ - Updated dependencies [32bf2d6]
2010
+ - Updated dependencies [af4fb29]
2011
+ - Updated dependencies [ff5ef1c]
2012
+ - Updated dependencies [9a97800]
2013
+ - Updated dependencies [6bca0e4]
2014
+ - Updated dependencies [81c0bc4]
2015
+ - Updated dependencies [60500cb]
2016
+ - Updated dependencies [2fcefb9]
2017
+ - Updated dependencies [b55a346]
2018
+ - Updated dependencies [065bba7]
2019
+ - Updated dependencies [6791717]
2020
+ - Updated dependencies [100547e]
2021
+ - Updated dependencies [6d1c155]
2022
+ - Updated dependencies [d7573b3]
2023
+ - Updated dependencies [bf3edfe]
2024
+ - Updated dependencies [0e05aac]
2025
+ - Updated dependencies [5aed9e4]
2026
+ - Updated dependencies [83c77dc]
2027
+ - Updated dependencies [18a8e7d]
2028
+ - Updated dependencies [e7957ab]
2029
+ - Updated dependencies [f7e34ca]
2030
+ - Updated dependencies [e719ebd]
2031
+ - Updated dependencies [516583b]
2032
+ - Updated dependencies [f9e4f91]
2033
+ - Updated dependencies [6ef48b1]
2034
+ - Updated dependencies [fa429cf]
2035
+ - Updated dependencies [ed8df3e]
2036
+ - Updated dependencies [8b446f5]
2037
+ - Updated dependencies [8e74b27]
2038
+ - Updated dependencies [8ebd57f]
2039
+ - Updated dependencies [617707a]
2040
+ - Updated dependencies [7357447]
2041
+ - Updated dependencies [199d31b]
2042
+ - Updated dependencies [3e01cb5]
2043
+ - Updated dependencies [7138bc1]
2044
+ - Updated dependencies [cef27e2]
2045
+ - Updated dependencies [4e8622b]
2046
+ - Updated dependencies [dffd752]
2047
+ - Updated dependencies [06973aa]
2048
+ - Updated dependencies [50798f3]
2049
+ - Updated dependencies [105f3c5]
2050
+ - Updated dependencies [3ccd9e8]
2051
+ - Updated dependencies [689b979]
2052
+ - Updated dependencies [e546222]
2053
+ - Updated dependencies [fd13f52]
2054
+ - Updated dependencies [fb336df]
2055
+ - Updated dependencies [0fce2ef]
2056
+ - Updated dependencies [42df928]
2057
+ - Updated dependencies [0e2ddd4]
2058
+ - Updated dependencies [b7479ab]
2059
+ - Updated dependencies [b2ea297]
2060
+ - Updated dependencies [5b5a5c3]
2061
+ - Updated dependencies [14582b8]
2062
+ - Updated dependencies [51e144e]
2063
+ - Updated dependencies [a691c0b]
2064
+ - Updated dependencies [af3861f]
2065
+ - Updated dependencies [515f171]
2066
+ - Updated dependencies [1f4e029]
2067
+ - Updated dependencies [258d264]
2068
+ - Updated dependencies [c00bf28]
2069
+ - Updated dependencies [93127bd]
2070
+ - Updated dependencies [f2158ec]
2071
+ - Updated dependencies [759606e]
2072
+ - Updated dependencies [a51fa0c]
2073
+ - Updated dependencies [51f3d8d]
2074
+ - Updated dependencies [78cbdb5]
2075
+ - Updated dependencies [b7543a9]
2076
+ - Updated dependencies [6c6cee7]
2077
+ - Updated dependencies [83fe6e7]
2078
+ - Updated dependencies [d1ab06f]
2079
+ - Updated dependencies [91783c4]
2080
+ - Updated dependencies [ca39427]
2081
+ - Updated dependencies [2d36552]
2082
+ - Updated dependencies [c9327c9]
2083
+ - Updated dependencies [920165d]
2084
+ - Updated dependencies [968dc1e]
2085
+ - Updated dependencies [3c73d99]
2086
+ - Updated dependencies [ed71d9e]
2087
+ - Updated dependencies [7776fc2]
2088
+ - Updated dependencies [e76634c]
2089
+ - Updated dependencies [1170ed1]
2090
+ - Updated dependencies [92814db]
2091
+ - Updated dependencies [4d73b07]
2092
+ - @object-ui/core@17.7.0
2093
+ - @object-ui/types@17.7.0
2094
+
2095
+ ## 17.6.0
2096
+
2097
+ ### Minor Changes
2098
+
2099
+ - a8411ad: data-objectstack: retire the four remaining `v3.0.0 Deep Integration` modules — `IntegrationManager`, `SecurityManager`, the studio canvas helpers (`createDefaultCanvasConfig` / `snapToGrid` / `calculateAutoLayout`), and the contract helpers (`validatePluginContract` / `generateContractManifest`) — for having zero code consumers outside this package
2100
+
2101
+ `src/index.ts`'s `// v3.0.0 Deep Integration modules` banner introduced five
2102
+ modules. objectui#4152 / PR #4239 already retired the first,
2103
+ `CloudOperations`, for fabricating a plausible success against a client
2104
+ namespace that does not exist. This closes out the other four:
2105
+ `contracts.ts`, `integration.ts`, `security.ts`, `studio.ts`.
2106
+
2107
+ **Not a repeat of #4152's urgency limb.** None of these four fabricated
2108
+ anything — `SecurityManager.generateCSPHeader()` really composes a header,
2109
+ `snapToGrid` really snaps, `validatePluginContract` really validates. What
2110
+ they shared with `CloudOperations` was the other limb: published surface of
2111
+ `@object-ui/data-objectstack` with a measured **zero** code consumers outside
2112
+ this package, across `packages/`, `apps/` and `examples/` (`.ts`/`.tsx`,
2113
+ excluding `node_modules`). The two apparent hits on re-measurement were
2114
+ homonyms, not consumers — `packages/plugin-designer/src/PageDesigner.tsx`
2115
+ declares its own local `snapToGrid` callback with no import from this
2116
+ package, and the `SecurityManager` hits outside this file are prose in
2117
+ `CHANGELOG.md`. Under the startup-focus principle a declared capability with
2118
+ no producer, no consumer and no business pull is retired, not kept on the
2119
+ chance it becomes useful.
2120
+
2121
+ **Breaking, in FROM → TO form.** The following are no longer exported from
2122
+ `@object-ui/data-objectstack`:
2123
+
2124
+ - `IntegrationManager` and its types (`IntegrationConfig`, `IntegrationTrigger`,
2125
+ `IntegrationProvider`, `SlackIntegrationConfig`, `EmailIntegrationConfig`,
2126
+ `WebhookIntegrationConfig`)
2127
+ - `SecurityManager` and its types (`SecurityManagerPolicy`, `CSPConfig`,
2128
+ `AuditLogConfig`, `AuditEventType`, `DataMaskingConfig`, `DataMaskingRule`,
2129
+ `AuditLogEntry`)
2130
+ - `createDefaultCanvasConfig`, `snapToGrid`, `calculateAutoLayout` and their
2131
+ types (`StudioCanvasConfig`, `StudioPropertyEditor`,
2132
+ `StudioThemeBuilderConfig`, `StudioColorPalette`, `StudioTypographyPreset`,
2133
+ `StudioShadowPreset`)
2134
+ - `validatePluginContract`, `generateContractManifest` and their types
2135
+ (`PluginContract`, `PluginExport`, `PluginAPIContract`,
2136
+ `ContractValidationResult`, `ContractValidationError`)
2137
+
2138
+ It is a `minor` under this repo's version policy (objectui's own breaking
2139
+ changes never declare `major`). Nothing broke that was working: the only
2140
+ in-repo construction sites were this package's own `v3-compat.test.ts` (which
2141
+ exercised the modules directly) and `spec-symbol-batch6.test.ts` (which only
2142
+ guarded `SecurityManagerPolicy`'s name against colliding with the spec's
2143
+ unrelated `SecurityPolicy` — that guard is removed along with its subject).
2144
+
2145
+ **No compile-compat stub was left**, for the same reason #4152 left none: with
2146
+ no consumer to keep compiling, a stub would be a second phantom surface
2147
+ guarding the first.
2148
+
2149
+ **The banner and the compat-test title stop claiming a v3.** `index.ts`'s
2150
+ `// v3.0.0 Deep Integration modules` banner had nothing left under it once
2151
+ these four went, so it is removed rather than retitled.
2152
+ `v3-compat.test.ts` — titled "v3.0.0 compatibility tests for @objectstack
2153
+ dependencies" against a resolved `@objectstack` family of `17.0.0-rc.6` even
2154
+ before this change — is not an empty shell (one block, `PaginatedResult API`,
2155
+ never depended on any of the five retired modules), so it stays and is
2156
+ retitled instead of deleted.
2157
+
2158
+ A negative pin
2159
+ (`src/v3-deep-integration-retired-4241.pin.test.ts`) replaces the retired
2160
+ `v3-compat.test.ts` cases and fails if any of the thirty retired names
2161
+ returns — reading both the runtime export list (which catches the seven
2162
+ class/function exports) and `index.ts`'s source text (the only instrument
2163
+ that can catch a returning `export type`).
2164
+
2165
+ ### Patch Changes
2166
+
2167
+ - 1ef236e: `@object-ui/data-objectstack` stops publishing its `src/` tree
2168
+
2169
+ The manifest's `files` array listed `src` alongside `dist`, so every published tarball carried all 43 source files — 38 of them `*.test.ts`. It had been that way since the package's first commit (`780a1b993`), never added for a consumer, and objectui#4006 recorded the same shape without acting on it: its scope was the `*.test.d.ts` half that the build program emitted into `dist`, and its own triage note graded this half as tarball weight rather than a break.
2170
+
2171
+ Nothing in the published surface reached those files, which is why no consumer changes in either direction. Measured on a cleanly rebuilt `dist`, all four ways in are closed: the `exports` map has one entry (`.`) and every condition under it targets `dist`; `main` / `module` / `types` are `./dist/index.js`, `./dist/index.js`, `./dist/index.d.ts`; the repo and the docs teach only the root specifier, and no `@object-ui/data-objectstack/src/...` deep import exists anywhere (the `src` paths in sibling `vite.config.ts` / `vitest.config.mts` files are workspace aliases resolved through `path.resolve()` against the source tree, which no `files` array shapes); and the tarball holds no sourcemap that could point back at `src`, since `tsup.config.ts` sets `sourcemap: false` and its bundled `dts` writes no `.d.ts.map` — the built `dist` contains four files, zero `.map` among them, and zero occurrences of `sourceMappingURL` or `../src/`.
2172
+
2173
+ `npm pack --dry-run` across the change, on the same `dist`:
2174
+
2175
+ | | before | after |
2176
+ | --- | --- | --- |
2177
+ | entries | 51 | 8 |
2178
+ | unpacked | 1356830 B | 719876 B |
2179
+ | tarball | 393379 B | 222157 B |
2180
+
2181
+ 43 files leave, none arrives, and every surviving entry is byte-identical apart from the edited `package.json`: `dist/index.{js,cjs,d.ts,d.cts}`, `README.md`, `CHANGELOG.md`, `LICENSE`, `package.json`. The 43 are the 38 tests plus the five modules they cover (`index.ts`, `errors.ts`, `metadata-client.ts`, `userState.ts`, `cache/MetadataCache.ts`), whose published form remains the bundled `dist/index.js`.
2182
+ - cf4f8a6: `MetadataClient.layered()` now reads the three-layer view from its declared path,
2183
+ `GET /meta/:type/:name/layers`, instead of flagging the ordinary item read.
2184
+
2185
+ The consumer half of objectstack#5882 (ruled B by the maintainer; the server half
2186
+ landed in objectstack#6596 and shipped in `@objectstack/spec@17.0.0`). The layered
2187
+ projection — packaged baseline vs tenant overlay vs merged effective, which is
2188
+ what the Studio metadata editor's comparison tabs render — used to be reached by
2189
+ hanging a query flag on `GET /meta/:type/:name`. One route therefore answered two
2190
+ unrelated representations chosen by a query parameter, while `packages/spec`
2191
+ declared only the unflagged one: anything generating a client from the route
2192
+ table produced a parser that was simply wrong for the flagged call. The
2193
+ projection now has a path of its own and a response schema of its own
2194
+ (`GetMetaItemLayeredResponseSchema`).
2195
+
2196
+ Same body, same envelope, so nothing in the editor changes shape: `code`,
2197
+ `overlay`, `overlayScope`, `effective`, the load-time `_diagnostics` and the full
2198
+ ADR-0010 protection envelope all still arrive on one round trip, and `?package=`
2199
+ (ADR-0048) is still threaded — the two entry points are served by ONE handler
2200
+ upstream precisely so the deprecation window's promise holds. The retired
2201
+ spelling still answers during that window, marked with RFC 9745 `Deprecation` and
2202
+ an RFC 8288 `Link: rel="successor-version"` pointing here, so this migration is
2203
+ safe against a lagging backend for as long as the window stays open, and it is
2204
+ what lets the maintainer close it.
2205
+
2206
+ One behaviour delta rides along, and it is the server's design rather than a
2207
+ choice made here: the retired flag FELL THROUGH to the plain item read when the
2208
+ backend's protocol implementation had no layered support, answering the
2209
+ `{ type, name, item }` envelope. A dedicated path refuses to answer a different
2210
+ resource under this one's declared shape and returns 501 `NOT_IMPLEMENTED`, which
2211
+ surfaces as a failed read instead of a comparison view whose `code` and `overlay`
2212
+ are silently blank.
2213
+
2214
+ The request is built in this package rather than delegated to
2215
+ `@objectstack/client` because the SDK expresses no layered read in either
2216
+ spelling — the framework's REST route ledger records the route as `server-only`,
2217
+ "consumed by objectui over plain HTTP", and whether the SDK should express it is
2218
+ an open upstream product call. The new path expectation is derived from the
2219
+ installed `@objectstack/spec` route table, and a ratchet keeps any shipped source
2220
+ file or skills guide from reaching the projection by query flag again.
2221
+ - 3d053bb: The Studio's overlay-layer badge stops printing the producer's raw scope value
2222
+
2223
+ objectui#4982. `MetadataLayered.overlayScope` was typed `string | null` under a comment naming its
2224
+ vocabulary as `organization | environment | package` — three spellings the producer has never
2225
+ emitted. The real vocabulary lives in `@objectstack/spec`'s `GetMetaItemLayeredResponseSchema`
2226
+ (`z.enum(['org', 'env']).nullable()`), and the framework's two assignment sites write `'org'` /
2227
+ `'env'`. Because the declared type was `string`, no compiler anywhere had an opinion, so the wrong
2228
+ comment was the only description of the field a reader had.
2229
+
2230
+ `overlayScope` is now the spec union, derived by indexing the published response type rather than
2231
+ restated locally (a restatement is the fork `check:spec-symbol-derivation` rejects); the alias ships
2232
+ as `MetadataOverlayScope`.
2233
+
2234
+ User-visible half: `LayeredDiff`'s overlay badge rendered that value straight to screen while the
2235
+ sibling artifact / none / merged badges all went through `translateConsoleValue`, and
2236
+ `CONSOLE_VALUE_ZH.layer` had no entry for either value the field can hold. One badge therefore had
2237
+ two languages depending on the data — a zh-CN admin opening any overlaid metadata item read `org` /
2238
+ `env`, while an un-overlaid one read 「已设」. The badge now translates like its three siblings, with
2239
+ 「组织」/「环境」 added to the layer table. That table's overlay-scope half is keyed by the spec union,
2240
+ so a scope the spec adds later fails `type-check` until it has a label instead of quietly reaching a
2241
+ badge in English. `translateConsoleValue` remains zh-only for every group, as before — extending it
2242
+ to the other locale packs is a separate decision and not part of this change.
2243
+ - d871f8e: A view personalization overlay no longer freezes the view it was laid over. `ObjectView`'s
2244
+ `persistViewPatch` sends `{ ...baseViewDef, ...patch }`, so a row written by a mere column
2245
+ drag or sort change stored the view's whole body — its effective `filter`, `columns`,
2246
+ `label`, `type`, `isDefault` — as of that moment, and the display merge
2247
+ (`{ ...source, ...override }`) then let that snapshot outrank the source view indefinitely:
2248
+ an admin edited a view's filter and everyone who had ever resized a column silently kept the
2249
+ old filter, with nothing reporting it.
2250
+
2251
+ An overlay now contributes only the keys it owns — `rowHeight`, `sort`, `hiddenFields`,
2252
+ `columnState`, `inlineEdit` (`VIEW_OVERLAY_OWNED_KEYS`, new export from
2253
+ `@object-ui/data-objectstack` alongside `narrowPersonalizationOverlay`) — so a later change
2254
+ to the source view reaches every user, including those whose stored row still carries the
2255
+ old snapshot: rows written before this change stop shadowing the source on the next read,
2256
+ with no migration to run and nothing rewritten at rest. A genuine saved view's own body is
2257
+ untouched — it is classified by the same predicate `listViews()` already excludes overlay
2258
+ rows by, so a row cannot be an overlay for one reader and a saved view for the other
2259
+ (objectui#5233, ruled on objectstack#7494).
2260
+ - a0b9e91: A system (code-defined) view's personalization overlay row no longer masquerades as a user-created saved view.
2261
+
2262
+ Toggling density / sort / hidden columns / column widths / inline-edit on a code-defined view persists a row under the same `type='view'` metadata namespace a genuinely saved view lives in, keyed by the same id (`ObjectStackAdapter.updateViewConfig`). `listViews()` previously returned that row indistinguishably from a real saved view, so `ObjectView`'s `isSystem = !saved` check flipped to `false` and the tab gained Rename / Delete / Set-default / Pin against a view that lives in code — `handleDeleteView` would even call `dataSource.deleteView` on it.
2263
+
2264
+ Two layers now keep the two kinds of rows apart:
2265
+
2266
+ - **Write side**: `updateViewConfig` — the only production writer of personalization overlays — stamps an explicit `_isOverride: true` discriminant on every row it saves, UNLESS the write targets an already-saved view's own row (see below).
2267
+ - **Read side**: `listViews()` excludes any row carrying that marker, and (for rows already persisted before this fix shipped) a best-effort legacy shape: a flat body with a `viewKind` the platform can only have server-side-backfilled from a registry (code-defined) baseline — a genuine runtime-created saved view never has one.
2268
+
2269
+ `listViewOverrides()` (the reader `ObjectView` uses to merge these settings back into the live view for display) is unchanged — it is supposed to keep seeing overlay rows.
2270
+
2271
+ The overlay this stores is **org-wide shared view settings**, not a per-user preference (a true per-user scope is a parked platform-side v18 direction) — comments describing it as "personal" have been corrected to say so.
2272
+
2273
+ **Follow-up fix (same card, post-review):** `updateViewConfig`'s ONE call site (`ObjectView`'s toolbar-driven toggle) fires for a toggle on EITHER a system view OR an already-saved view — a saved view whose own toolbar the user toggles writes to that same view's own row. Stamping the overlay marker unconditionally there would flag the user's own saved view as an overlay and make `listViews()` exclude it on the very next read, i.e. the saved view would vanish from the switcher the moment its density was adjusted. `updateViewConfig` gains an optional `opts.isSavedView` parameter (also added to the `DataSource` interface in `@object-ui/types`); `ObjectView` passes it from the same `isSavedViewId` classification its readonly gate and mutating handlers already use, and the marker is withheld when it's true.
2274
+ - Updated dependencies [88085e3]
2275
+ - Updated dependencies [2533ec5]
2276
+ - Updated dependencies [bbe8b86]
2277
+ - Updated dependencies [8477be5]
2278
+ - Updated dependencies [279fb13]
2279
+ - Updated dependencies [ad07b65]
2280
+ - Updated dependencies [41f498b]
2281
+ - Updated dependencies [e1d4251]
2282
+ - Updated dependencies [1184192]
2283
+ - Updated dependencies [a2a9747]
2284
+ - Updated dependencies [ac600e5]
2285
+ - Updated dependencies [c1ef923]
2286
+ - Updated dependencies [af5e292]
2287
+ - Updated dependencies [7f96b10]
2288
+ - Updated dependencies [167ec42]
2289
+ - Updated dependencies [f1d4748]
2290
+ - Updated dependencies [b1119ec]
2291
+ - Updated dependencies [9f23d2b]
2292
+ - Updated dependencies [578e025]
2293
+ - Updated dependencies [af025ee]
2294
+ - Updated dependencies [598c89a]
2295
+ - Updated dependencies [b8b9af4]
2296
+ - Updated dependencies [31676be]
2297
+ - Updated dependencies [9ce096f]
2298
+ - Updated dependencies [e05db88]
2299
+ - Updated dependencies [5ffcc14]
2300
+ - Updated dependencies [d971e51]
2301
+ - Updated dependencies [97abb24]
2302
+ - Updated dependencies [deb157a]
2303
+ - Updated dependencies [d2ce342]
2304
+ - Updated dependencies [9695da7]
2305
+ - Updated dependencies [58b8346]
2306
+ - Updated dependencies [dfc6975]
2307
+ - Updated dependencies [3cf4de0]
2308
+ - Updated dependencies [c9dc811]
2309
+ - Updated dependencies [a0b9e91]
2310
+ - Updated dependencies [99bd015]
2311
+ - @object-ui/types@17.6.0
2312
+ - @object-ui/core@17.6.0
2313
+
3
2314
  ## 17.5.0
4
2315
 
5
2316
  ### Minor Changes