@object-ui/data-objectstack 17.6.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,62 +1,171 @@
1
1
  import { ObjectStackClient } from '@objectstack/client';
2
- import { DroppedFieldsEvent } from '@objectstack/spec/data';
2
+ import { DroppedFieldsEvent, EngineAggregateOptions } from '@objectstack/spec/data';
3
3
  export { DroppedFieldsEvent } from '@objectstack/spec/data';
4
- import { GetMetaItemLayeredResponse, RuntimeAuthoringIssue } from '@objectstack/spec/api';
4
+ import { ListViewGroupHeaderRow } from '@objectstack/spec/ui';
5
+ import { GetMetaItemResponse, GetMetaItemLayeredResponse, ListDraftsResponse, RuntimeAuthoringIssue, GetMetaItemsResponse, PublishPackageDraftsResponse, ValidateDataIssue } from '@objectstack/spec/api';
5
6
  export { RuntimeAuthoringIssue } from '@objectstack/spec/api';
6
7
  import { DatasetSelection, AnalyticsResult } from '@objectstack/spec/contracts';
7
- import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types';
8
- export { DeleteViewResult, FileUploadResult, ViewHomeDeleteOutcome } from '@object-ui/types';
8
+ import { DataSource, ConnectionState, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types';
9
+ export { ConnectionState, DeleteViewResult, FileUploadResult, ViewHomeDeleteOutcome } from '@object-ui/types';
9
10
  import { DatasetDrillRange } from '@object-ui/core';
10
11
 
11
12
  /**
12
- * ObjectUI
13
- * Copyright (c) 2024-present ObjectStack Inc.
13
+ * Take the body out of a served draft envelope (`{ type, name, item }`) and
14
+ * remove the framework's own READ DECORATIONS from it — or answer `null` when
15
+ * there is nothing pending.
14
16
  *
15
- * This source code is licensed under the MIT license found in the
16
- * LICENSE file in the root directory of this source tree.
17
+ * ## Why this function exists at all (objectui#8181)
18
+ *
19
+ * It was implemented three separate times — once in `ResourceEditPage`, once in
20
+ * `StudioDesignSurface`, once in `PackageOwdOverviewPanel` — plus a fourth
21
+ * hand-rolled copy in `ObjectHooksPanel` under a different name. objectui#7603
22
+ * taught exactly ONE of those copies to strip. That is the defect this file
23
+ * closes, and it is a different defect from "N consumers forgot": four
24
+ * byte-identical copies of a rule mean the next copy is free to omit it again,
25
+ * so the cure is one function rather than four strips.
26
+ *
27
+ * ## Why it lives HERE and not in `app-shell`
28
+ *
29
+ * Next to the {@link MetadataClient.getDraft} that produces the envelope this
30
+ * decodes. `getDraft` deliberately hands back the wire envelope rather than the
31
+ * body (objectui#4271), so the unwrap is part of that method's contract, not a
32
+ * detail of any one view. `app-shell` already depends on this package, and the
33
+ * one non-app-shell consumer (`updateView`, in this package) is a caller too.
34
+ *
35
+ * ## The verdict runs BEFORE the strip, never after
36
+ *
37
+ * What counts as a pending draft is `getDraft`'s answer. A draft whose only
38
+ * keys are decorations is still a served draft, so the emptiness verdict is
39
+ * taken on the body as it arrived; the strip may return an EMPTY object, but it
40
+ * may never turn a served draft into "no draft". Pinned in
41
+ * `draft-envelope.test.ts`.
42
+ *
43
+ * ## Which keys, and which keys deliberately survive
44
+ *
45
+ * The list is the SPEC'S (`METADATA_READ_DECORATIONS`), reached through its own
46
+ * exported helper — the same one `MetadataService.saveFields` uses on the write
47
+ * side. Never a second hand-maintained `['_diagnostics', '_draft']` in this
48
+ * repo: a local copy goes stale the next time the framework adds a decoration,
49
+ * and a decoration this code does not know to remove is precisely the defect.
50
+ *
51
+ * The ADR-0010 protection envelope (`_lock`, `_provenance`, `_packageId`,
52
+ * `_packageVersion`, ...) shares the underscore spelling and is deliberately
53
+ * NOT on that list: those keys are declared by the closed schemas so provenance
54
+ * survives a re-parse. This strip leaves them alone, and doing otherwise would
55
+ * be the same "drop whatever looks internal" pass AGENTS.md #0.1 bans.
56
+ *
57
+ * ## Not a lenient fallback
58
+ *
59
+ * It removes exactly the two keys the framework ADDS AT READ TIME and never
60
+ * stores. A genuinely unrecognized key still fails loudly at whichever gate
61
+ * sees it next.
62
+ *
63
+ * @param resp the value `MetadataClient.getDraft()` resolved to.
64
+ * @returns the decoration-free draft body, or `null` for "nothing pending".
65
+ */
66
+ declare function extractDraftBody(resp: unknown): Record<string, unknown> | null;
67
+ /**
68
+ * One entry of a pending draft's per-key provenance (objectui#12725): which
69
+ * authoring surface last changed one key path of the draft body, under which
70
+ * actor, and when. DERIVED from `@objectstack/spec`'s
71
+ * `GetMetaItemResponseSchema.draftChanges` (objectstack#24298), never
72
+ * restated: `path` is the key path inside `item` (an array value is one leaf,
73
+ * `[]` the whole body), `by` is `copilot | studio | api`, `at` an ISO instant.
17
74
  */
75
+ type DraftChange = NonNullable<GetMetaItemResponse['draftChanges']>[number];
18
76
  /**
19
- * MetadataClient
77
+ * The `draftChanges` of a served draft envelope — the value
78
+ * {@link MetadataClient.getDraft} resolved to — or `null` when the read
79
+ * recorded none.
20
80
  *
21
- * Thin, framework-agnostic HTTP client for the ObjectStack metadata
22
- * API (`/api/v1/meta/*`). Used by the platform Setup app surfaces in
23
- * `@object-ui/plugin-designer` (Object Manager, Field Designer, etc.)
24
- * so they can read and write protocol metadata without depending on
25
- * `ObjectStackAdapter` (which is a generic data-source binding and
26
- * carries a lot of view/dashboard-specific behaviour we don't need
27
- * here).
81
+ * The strict draft read (`?state=draft`) serves the key beside `item`, and
82
+ * leaves it ABSENT, never `[]`, when nothing was recorded (a draft row saved
83
+ * before the framework's writer, objectstack#24395). So `null` here means
84
+ * "not recorded", and it is never folded into an empty list a caller would read
85
+ * as "nothing changed".
86
+ *
87
+ * @param resp the value `MetadataClient.getDraft()` resolved to.
88
+ */
89
+ declare function readDraftChanges(resp: unknown): DraftChange[] | null;
90
+ /**
91
+ * The entry that answers for the key at `path`: of the entries whose path is
92
+ * `path` or one of its ancestors, the LONGEST — the framework's own rule, since
93
+ * a draft save replaces every entry at or under each path it changes, so the
94
+ * deepest recorded ancestor is the last write that changed this key. `[]` (the
95
+ * whole body) is everyone's ancestor. `null` when no entry is an ancestor, and
96
+ * when `changes` is `null` ("not recorded").
28
97
  *
29
- * Endpoints (see `packages/rest/src/rest-server.ts`):
30
- * GET /api/v1/meta — list all metadata types
31
- * GET /api/v1/meta/:type — list items of a type
32
- * GET /api/v1/meta/:type/:name — get one item (returns the
33
- * unwrapped item content)
34
- * PUT /api/v1/meta/:type/:name — save (overlay) one item
35
- * honours `If-Match` for OCC
36
- * DELETE /api/v1/meta/:type/:name — reset overlay to artifact
37
- * GET /api/v1/meta/:type/:name/history — durable change log
98
+ * Paths compare segment by segment, never as a joined string: a segment may
99
+ * itself contain a dot (a permission set's `fields` map is keyed
100
+ * `object.field`).
101
+ */
102
+ declare function governingDraftChange(changes: readonly DraftChange[] | null | undefined, path: readonly string[]): DraftChange | null;
103
+ /**
104
+ * The entries strictly UNDER `path`: changes to keys inside the value at
105
+ * `path` (`["fields", "x", "label"]` under `["fields", "x"]`). The governing
106
+ * entry ({@link governingDraftChange}) answers for the value as a whole; these
107
+ * answer for its parts, which a later write may have changed through another
108
+ * surface.
109
+ */
110
+ declare function draftChangesUnder(changes: readonly DraftChange[] | null | undefined, path: readonly string[]): DraftChange[];
111
+
112
+ /**
113
+ * ObjectUI
114
+ * Copyright (c) 2024-present ObjectStack Inc.
38
115
  *
39
- * The client deliberately keeps the response shape opaque (typed as
40
- * `unknown`/generic `T`) so callers explicitly narrow per-metadata-
41
- * type, mirroring the framework's "single Zod source per type" rule.
116
+ * This source code is licensed under the MIT license found in the
117
+ * LICENSE file in the root directory of this source tree.
42
118
  */
43
119
 
44
120
  /**
45
- * Emitted after a {@link MetadataClient.save} whose response carried a
46
- * non-empty `advisories` array. The save SUCCEEDED — the row persisted and the
47
- * server returned 200 — so this is advisory, never a failure.
121
+ * Emitted after a metadata WRITE whose response carried a non-empty
122
+ * `advisories` array. The write SUCCEEDED — the row persisted and the server
123
+ * returned 200 — so this is advisory, never a failure.
48
124
  *
49
125
  * Deliberately the same shape of seam as `ObjectStackAdapter.onWriteWarning`
50
- * (#3431/#3455): a successful write whose response carries something the author
126
+ * (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455): a successful write whose response carries something the author
51
127
  * needs to be told, surfaced to the shell as an event so the data layer never
52
128
  * imports a toaster. The difference is only which door produced it — that one
53
- * is record CRUD, this one is the metadata save door.
129
+ * is record CRUD, these are the metadata write doors.
130
+ *
131
+ * ## Both write doors report (#5026)
132
+ *
133
+ * The objectstack-ai/objectstack#4463 runtime authoring gate runs on BOTH metadata write doors by its D1
134
+ * ruling — a draft→active promotion is gated exactly as a direct active save —
135
+ * and its non-blocking findings ride the 2xx of whichever door earned them.
136
+ * `SaveMetaItemResponseSchema` has carried the key since objectstack-ai/objectstack#4717 and
137
+ * `PublishMetaItemResponseSchema` since objectstack#9176; both declare it at
138
+ * the response's TOP level, under the same name and with the same
139
+ * `RuntimeAuthoringIssueSchema` element type, which is what lets one reader and
140
+ * one event serve both. Measured against the installed `@objectstack/spec`
141
+ * rather than assumed — see the PR for the probe.
142
+ *
143
+ * ## And the BATCH publish door (`ce986aafc`)
144
+ *
145
+ * `PublishPackageDraftsResponseSchema` declares the same key with the same
146
+ * element type, riding EACH `published[]` element rather than a parallel
147
+ * top-level map (objectstack#9343's ruled shape). So the reader is still one
148
+ * reader — the object it is handed is one `published[]` element instead of a
149
+ * whole response — and the event is still one event per promoted item.
150
+ *
151
+ * The name keeps its `Save` prefix because it is public API of this package and
152
+ * renaming it would break consumers for no behavioural gain; {@link door} is
153
+ * what says which write produced the event.
54
154
  */
55
155
  interface MetadataSaveAdvisoryEvent {
56
156
  /** Metadata type saved (e.g. `'flow'`). */
57
157
  type: string;
58
158
  /** Item name saved. */
59
159
  name: string;
160
+ /**
161
+ * The bundle member the write named (objectui#12413), when it named one:
162
+ * set by {@link MetadataClient.publishDraft} from its `options.member`, so
163
+ * an advisory raised on one language's publish of an `email_template` is
164
+ * attributed to that member and not to the whole template. Absent for every
165
+ * write that names no member. Typed from {@link MetadataDraftHeader}'s
166
+ * `member`, the value the caller read off the draft row.
167
+ */
168
+ member?: MetadataDraftHeader['member'];
60
169
  /**
61
170
  * The save mode the call used. `'draft'` writes are **never gated** — the
62
171
  * framework returns at `runtime-authoring-gate.ts`'s D1 early-return
@@ -65,15 +174,65 @@ interface MetadataSaveAdvisoryEvent {
65
174
  * consumer reading the event can tell which door it came through.
66
175
  */
67
176
  mode: 'draft' | 'publish';
177
+ /**
178
+ * Which write door produced this event (#5026).
179
+ *
180
+ * - `'save'` — `PUT /meta/:type/:name` ({@link MetadataClient.save}, and the
181
+ * SDK's `meta.saveItem` behind `ObjectStackAdapter`).
182
+ * - `'publish'` — the promotion doors: `POST /meta/:type/:name/publish`
183
+ * ({@link MetadataClient.publish} and {@link MetadataClient.publishDraft}),
184
+ * and the batch `POST /packages/:id/publish-drafts`
185
+ * ({@link MetadataClient.publishPackageDrafts}), which emits ONE event per
186
+ * `published[]` element carrying that element's own `type` / `name`.
187
+ *
188
+ * ## Why the batch route is not a third value (`ce986aafc`)
189
+ *
190
+ * The batch door promotes drafts to active — the author pressed Publish, and
191
+ * every item this event names really was published. What `door` decides is
192
+ * the frame's VERB, and "Published" is the true one for all three routes; a
193
+ * third value would have to render the same word. The per-item identity the
194
+ * author needs to act on is already carried by {@link type} / {@link name},
195
+ * one event per item, so nothing about the batch is lost by sharing the
196
+ * value. ⛔ It is NOT a claim that "one call = one event": the batch emits as
197
+ * many events as it has advised items, which is why each one names its own.
198
+ *
199
+ * Distinct from {@link mode}, which cannot answer this: a direct active save
200
+ * and a draft promotion both report `mode: 'publish'` because both land the
201
+ * body in the active overlay. The renderer needs the DOOR, because the author
202
+ * pressed either Save or Publish and a toast that says "Saved" after a
203
+ * Publish tells them their change is still a draft — the opposite of what
204
+ * happened, and in this product Save and Publish are two different buttons
205
+ * with two different meanings.
206
+ *
207
+ * REQUIRED rather than optional-with-a-default so a future third door cannot
208
+ * be wired without stating which one it is: an omitted discriminator would
209
+ * silently render the save wording.
210
+ */
211
+ door: 'save' | 'publish';
68
212
  /** The findings. Never empty — the event is not emitted otherwise. */
69
213
  advisories: RuntimeAuthoringIssue[];
70
214
  }
71
215
  /** Event listener type for save-advisory events. */
72
216
  type MetadataSaveAdvisoryListener = (event: MetadataSaveAdvisoryEvent) => void;
73
217
  /**
74
- * Read the `advisories` array off a save response, defensively.
218
+ * Read the `advisories` array off a metadata write response, defensively.
75
219
  *
76
- * The server omits the key entirely on a clean save, so `undefined` is the
220
+ * Serves EVERY write door unchanged (#5026, `ce986aafc`):
221
+ * `SaveMetaItemResponseSchema` and `PublishMetaItemResponseSchema` declare the
222
+ * key at the same top level, and `PublishPackageDraftsResponseSchema` declares
223
+ * it on each `published[]` element — under the same name, with the same element
224
+ * schema. So there is one reader, not a per-door copy that could drift; the
225
+ * batch caller hands it one element, which is the object that carries the key
226
+ * there.
227
+ *
228
+ * ⛔ It reads the `advisories` key of the object it is GIVEN and never
229
+ * traverses. Choosing the object — a response body at the single-item doors,
230
+ * one `published[]` element at the batch door — is the caller's job precisely
231
+ * because the place is declared per route: a reader that went hunting for
232
+ * findings wherever they might be would be inventing a contract the server
233
+ * never stated.
234
+ *
235
+ * The server omits the key entirely on a clean write, so `undefined` is the
77
236
  * common case and means "nothing to say". Anything that is not an array of
78
237
  * objects carrying the six required keys is dropped rather than rendered: a
79
238
  * half-shaped finding would print blanks at the author, and this channel must
@@ -105,9 +264,13 @@ interface MetadataClientConfig {
105
264
  previewDrafts?: boolean;
106
265
  /**
107
266
  * Called after a {@link MetadataClient.save} whose 2xx response carried a
108
- * non-empty `advisories` array (objectstack#7435). The save already
109
- * succeeded; this is how the shell learns there is something to tell the
110
- * author instead of the findings being discarded client-side.
267
+ * non-empty `advisories` array (objectstack#7435). Since #5026 the SAME sink
268
+ * also receives the publish door's findings (objectstack#9176), and since
269
+ * `ce986aafc` the batch publish door's too, one event per advised
270
+ * `published[]` element (objectstack#9343) — read `event.door` to tell a
271
+ * save from a promotion, and `event.type` / `event.name` for which item. The
272
+ * write already succeeded; this is how the shell learns there is something to
273
+ * tell the author instead of the findings being discarded client-side.
111
274
  *
112
275
  * Set on the CONFIG rather than exposed as a `subscribe()` method on purpose:
113
276
  * console metadata clients are minted per-component by `useMetadataClient`,
@@ -124,18 +287,92 @@ interface MetadataListOptions {
124
287
  /** Filter by source package id (matches the `package` query param). */
125
288
  packageId?: string;
126
289
  }
290
+ /**
291
+ * A list read as {@link MetadataClient.listResponse} returns it: the items, and
292
+ * what the server serves BESIDE them, typed by the spec's
293
+ * `GetMetaItemsResponse` rather than restated (its `type` key is not kept).
294
+ *
295
+ * `registrations` is the automation engine's verdict on each listed flow,
296
+ * keyed by the item's `name` (`FlowRegistrationSchema`, objectstack#23316):
297
+ * `registered`, `refused` with the engine's `reason` verbatim, or
298
+ * `unregistered`. It is absent when the server sent none — a type other than
299
+ * `flow`, a server with no automation service, or one that predates the key —
300
+ * and absence means no engine judged the flow, ⛔ never `registered`. A name
301
+ * the map has no entry for is unjudged in the same way.
302
+ */
303
+ type MetadataListAnswer<T = unknown> = {
304
+ items: T[];
305
+ } & Pick<GetMetaItemsResponse, 'registrations'>;
127
306
  /**
128
307
  * A pending DRAFT metadata item (ADR-0033), as returned by {@link MetadataClient.listDrafts}.
129
308
  * Light header — no body — carrying the owning package so the console can group
130
309
  * pending changes by app package.
310
+ *
311
+ * DERIVED from the spec's row, `ListDraftsResponseSchema.drafts[]`, rather than
312
+ * re-spelled (objectui#12413): the key names and their types come from the
313
+ * spec symbol, so a key the spec renames or retypes fails the type-check here
314
+ * instead of drifting past it.
315
+ *
316
+ * - `member` — the bundle member this draft is (objectstack#23073): present on
317
+ * every draft of a type whose identity is a pair (today only
318
+ * `email_template`, `(name, locale)`), absent for a type identified by its
319
+ * name alone. It is the value the by-name doors take back as `?member=`
320
+ * ({@link MetadataClient.publishDraft}'s `options.member`).
321
+ *
322
+ * ⛔ The pick is deliberate: the five keys this interface declared before keep
323
+ * their exact types, and `member` is the one key added. The spec row's `label`
324
+ * is not picked here: it is declared REQUIRED there, and this client talks to
325
+ * runtimes of several vintages, so claiming it would be this client asserting
326
+ * a server version it cannot check.
131
327
  */
132
- interface MetadataDraftHeader {
133
- type: string;
134
- name: string;
135
- packageId: string | null;
136
- updatedAt: string | null;
137
- updatedBy: string | null;
328
+ interface MetadataDraftHeader extends Pick<ListDraftsRow, 'type' | 'name' | 'packageId' | 'updatedAt' | 'updatedBy'> {
329
+ /** The bundle member, typed by the spec row's own `member` (see above). */
330
+ member?: ListDraftsRow['member'];
138
331
  }
332
+ /** One element of `ListDraftsResponseSchema.drafts`, the spec row `MetadataDraftHeader` derives from. */
333
+ type ListDraftsRow = ListDraftsResponse['drafts'][number];
334
+ /**
335
+ * What {@link MetadataClient.publishPackageDrafts} resolves with — the "publish
336
+ * whole app" body, DERIVED from `PublishPackageDraftsResponseSchema` rather
337
+ * than re-spelled (`ce986aafc`).
338
+ *
339
+ * Derived, and then widened in exactly two ways, each for a measured reason:
340
+ *
341
+ * - `Partial<…>` because this client talks to runtimes of several vintages and
342
+ * the spec's six required keys are what TODAY's producer sets. Declaring them
343
+ * required would be this client asserting a server version it cannot check —
344
+ * and an older runtime that omits one would be a type lie, not a compile
345
+ * error. Every consumer here already reads these keys defensively.
346
+ * - The index signature because the REST door adds keys on the way out
347
+ * (ADR-0045 visibility receipts, the `metadata:reloaded` announce receipt),
348
+ * and because a body this client hands back must stay readable by a caller
349
+ * that knows about a key this package has never heard of.
350
+ *
351
+ * ⛔ What it is NOT is a second definition of the batch response: the key names
352
+ * and element shapes come from the spec symbol, so a key the spec adds, renames
353
+ * or retypes arrives here with no edit.
354
+ */
355
+ type MetadataPublishPackageDraftsResult = Partial<PublishPackageDraftsResponse> & Record<string, unknown>;
356
+ /**
357
+ * What {@link MetadataClient.publishPackageDrafts} may ask of the batch door
358
+ * beyond the package id (objectui#12929): today the protocol's opt-in
359
+ * `backfill` (objectstack#24253), and nothing else.
360
+ *
361
+ * Picked off `@objectstack/client`'s own `packages.publishDrafts` signature
362
+ * rather than re-spelled, so the element shape (`{ object, field }`) is the
363
+ * client's declaration. The request has no spec schema of its own; the
364
+ * client's signature is where the protocol declares it. `actor` is not
365
+ * picked: the door writes as the signed-in caller, and no console surface
366
+ * names another.
367
+ *
368
+ * `backfill` names fields this publish ADDS whose literal `defaultValue` the
369
+ * server writes into the object's existing rows, as the caller, after the
370
+ * batch goes live; the receipt comes back as `backfillApplied`. A field that
371
+ * is not new, or declares no literal default, refuses the WHOLE request with a
372
+ * 400 before anything is published, so a caller offers only fields it has
373
+ * judged by the same rule.
374
+ */
375
+ type MetadataPublishPackageDraftsOptions = Pick<NonNullable<Parameters<ObjectStackClient['packages']['publishDrafts']>[1]>, 'backfill'>;
139
376
  /**
140
377
  * Options for {@link MetadataClient.save} — a WRITE OVER HTTP to
141
378
  * `/api/v1/meta/:type/:name`.
@@ -146,17 +383,41 @@ interface MetadataDraftHeader {
146
383
  * metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
147
384
  * `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
148
385
  * exists here, and not one of these exists there: this is the REST client's
149
- * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), the
150
- * destructive-change override, the ADR-0033 draft/publish mode, and the
151
- * owning package. Same words, different layer.
386
+ * request envelope — optimistic concurrency (`ifMatch` → `If-Match`,
387
+ * `ifNoneMatch` → `If-None-Match`), the destructive-change override, the
388
+ * ADR-0033 draft/publish mode, and the owning package. Same words, different
389
+ * layer.
152
390
  */
153
391
  interface MetadataClientSaveOptions {
154
392
  /**
155
- * Optimistic concurrency token (the `checksum` returned by the last
156
- * read). When present, sent as the `If-Match` header so concurrent
157
- * edits get a 409 instead of overwriting each other.
393
+ * Optimistic concurrency (ADR-0008): the `version` token of the stored row
394
+ * this write was built on, sent as the `If-Match` header, so a write over a
395
+ * row that has moved since is refused `409 METADATA_CONFLICT` instead of
396
+ * overwriting it.
397
+ *
398
+ * The `/meta` doors serve that token in three places, each naming the row a
399
+ * save to the SAME address (type, name, `?package=`, lifecycle) compares
400
+ * against: the item read's `version` (`?state=draft` for the draft row,
401
+ * declared on `GetMetaItemResponseSchema`), a save receipt's `version`, and
402
+ * a version conflict's `currentVersion` (`MetadataConflictErrorSchema`). A
403
+ * served `null` means no such row: pin with {@link ifNoneMatch} instead.
404
+ *
405
+ * ⛔ Never together with {@link ifNoneMatch}: {@link MetadataClient.save}
406
+ * throws before sending a request that carries both.
158
407
  */
159
408
  ifMatch?: string;
409
+ /**
410
+ * The first-write pin, sent as `If-None-Match: *`: write only where no row of
411
+ * this write's lifecycle exists at its address (the draft row, for
412
+ * `mode: 'draft'`). A create sends it, and so does the first draft save of an
413
+ * item with no pending draft; the door refuses it `409 METADATA_CONFLICT`,
414
+ * carrying the existing row's `currentVersion`, when such a row is there.
415
+ *
416
+ * `'*'` alone: the door reads no other value. ⛔ Never together with
417
+ * {@link ifMatch} — the door refuses that pair `400`, and
418
+ * {@link MetadataClient.save} throws before sending it.
419
+ */
420
+ ifNoneMatch?: '*';
160
421
  /**
161
422
  * Bypass destructive-change protection (Phase 3a). The server returns
162
423
  * `409 destructive_change` with `issues[]` if a write would drop or
@@ -177,6 +438,19 @@ interface MetadataClientSaveOptions {
177
438
  * Studio package workspace. Omit for an env-local overlay.
178
439
  */
179
440
  packageId?: string;
441
+ /**
442
+ * The authoring surface this save is made through (objectui#12725), sent as
443
+ * `?surface=`. A draft save stamps it as `by` on every key path it changes,
444
+ * the entries the strict draft read serves as `draftChanges`; absent, the
445
+ * framework stamps `api`. Studio's own saves send `'studio'`, so a person's
446
+ * edit there is told apart from the AI copilot's (objectstack#24395).
447
+ *
448
+ * DERIVED from the spec's authoring-surface set (the one `draftChanges[].by`
449
+ * reports, `MetadataAuthoringSurface`), minus `'copilot'`: the HTTP doors
450
+ * refuse `?surface=copilot` `400` (only an in-process tool may state it), so
451
+ * this client cannot send it. Omit for any other caller.
452
+ */
453
+ surface?: Exclude<DraftChange['by'], 'copilot'>;
180
454
  }
181
455
  interface MetadataGetOptions {
182
456
  /**
@@ -193,7 +467,7 @@ interface MetadataGetOptions {
193
467
  */
194
468
  packageId?: string;
195
469
  }
196
- interface MetadataDeleteOptions extends MetadataClientSaveOptions {
470
+ interface MetadataDeleteOptions extends Omit<MetadataClientSaveOptions, 'surface'> {
197
471
  /**
198
472
  * Target state. `'draft'` discards the pending draft (keeps the
199
473
  * published overlay intact). Omit to reset the active overlay back
@@ -202,12 +476,13 @@ interface MetadataDeleteOptions extends MetadataClientSaveOptions {
202
476
  state?: 'active' | 'draft';
203
477
  }
204
478
  /**
205
- * Which layer an overlay was saved at — `'org'` for a tenant overlay, `'env'`
206
- * for an environment-level one, `null` exactly when `overlay` is null.
479
+ * Which layer an overlay was saved at — `'env'` for an environment-level one,
480
+ * `null` exactly when `overlay` is null. Every metadata read is environment →
481
+ * code since objectstack#15206 S5, which retired the `'org'` tenant scope.
207
482
  *
208
483
  * DERIVED from `@objectstack/spec`, not restated: the vocabulary lives once, in
209
484
  * `GetMetaItemLayeredResponseSchema`'s `overlayScope`
210
- * (`z.enum(['org', 'env']).nullable()`), and this alias indexes the published
485
+ * (`z.enum(['env']).nullable()`), and this alias indexes the published
211
486
  * response type so a scope the spec adds arrives here with no edit. Restating
212
487
  * the union locally is the fork `scripts/check-spec-symbol-derivation.mjs`
213
488
  * exists to reject, and the mirror-image mistake — a consumer re-spelling a
@@ -246,9 +521,13 @@ type MetadataOverlayScope = GetMetaItemLayeredResponse['overlayScope'];
246
521
  * the cross-repo half of the drift, not just the in-repo half.
247
522
  *
248
523
  * ⚠️ This types what this repo may WRITE. It does NOT constrain what a server
249
- * may SEND: {@link MetadataClient.layered} casts the wire value through
250
- * unchecked, so every reader must still handle a value outside these four. The
251
- * lock banner in `ResourceEditPage` is the worked example.
524
+ * may SEND, and it never will — {@link MetadataClient.layered} forwards the
525
+ * wire value rather than dropping it, so every reader must still handle a value
526
+ * outside these four. What changed in objectui#5676 is that the value is no
527
+ * longer forwarded *silently*: the boundary validates it against the producer's
528
+ * own schema and names the field in {@link MetadataLayered._unrecognized} when
529
+ * it does not match. The lock banner in `ResourceEditPage` is the worked
530
+ * example of a reader that labels such a value instead of rendering it blank.
252
531
  */
253
532
  type MetadataLockState = GetMetaItemLayeredResponse['lock'];
254
533
  /**
@@ -299,6 +578,21 @@ interface MetadataLayered<T = unknown> {
299
578
  deletable?: boolean;
300
579
  /** True when "Reset to package default" applies (has overlay + artifact). */
301
580
  resettable?: boolean;
581
+ /**
582
+ * Protection-envelope keys whose wire value did NOT match `packages/spec`'s
583
+ * schema for them (objectui#5676). Computed HERE, at the boundary — the
584
+ * server never sends this key, and {@link MetadataClient.layered} builds the
585
+ * returned object field by field, so a server that tried could not smuggle
586
+ * one in.
587
+ *
588
+ * Absent when everything the server sent parsed, so its presence is the
589
+ * signal. The offending values are still forwarded on the fields themselves:
590
+ * this is "pass through and label", the treatment objectui#5672 chose for
591
+ * `lock` alone, applied to the whole ADR-0010 envelope. Dropping them would
592
+ * be the reject semantics that card refused — turning a wrong render into no
593
+ * render is a behaviour change for every consumer of this client.
594
+ */
595
+ _unrecognized?: readonly string[];
302
596
  }
303
597
  /**
304
598
  * One row in the metadata protection-audit trail
@@ -443,7 +737,7 @@ declare class MetadataClient {
443
737
  private readonly headers;
444
738
  /** ADR-0037: when true, reads render the draft-overlaid world. */
445
739
  readonly previewDrafts: boolean;
446
- /** #4133 — sink for post-save advisory findings; see the config field. */
740
+ /** #4133 / #5026 — sink for every write door's advisory findings; see the config field. */
447
741
  private readonly onSaveAdvisory;
448
742
  constructor(config: MetadataClientConfig);
449
743
  /** Update the client's environment scope at runtime. */
@@ -454,10 +748,51 @@ declare class MetadataClient {
454
748
  * renderer tree into ADR-0037 preview mode off one URL flag.
455
749
  */
456
750
  withPreviewDrafts(previewDrafts: boolean): MetadataClient;
751
+ /**
752
+ * Share one in-flight GET among the callers asking the same method for the
753
+ * same URL with the same headers through the same transport (objectui#11797).
754
+ *
755
+ * Opening a Studio package mounts several readers that ask the same
756
+ * question at once, and each one sent its own request. The first call for a
757
+ * key sends `read()`; a call that arrives while it is pending waits for that
758
+ * request instead of sending another.
759
+ *
760
+ * ⛔ Not a response cache, deliberately — the rules `findOne` keeps on the
761
+ * data adapter (objectui#11699):
762
+ * - the entry goes the moment the read settles, so a call made after an
763
+ * answer always reaches the network (there is no reuse window: a client
764
+ * cannot see every write that changes metadata, an agent's server-side
765
+ * edits among them, and a window would answer the read after such a write
766
+ * with the state from before it);
767
+ * - a rejection reaches every caller already waiting and is not remembered;
768
+ * - a write sent through the same transport drops every pending read when
769
+ * it lands (see {@link sendWrite}), so a read asked after a save or a
770
+ * publish is never answered by a request sent before it;
771
+ * - every caller, the first one included, gets an answer no other caller
772
+ * holds (see {@link handOut}).
773
+ */
774
+ private shareRead;
775
+ /**
776
+ * Send a write, then drop every read still pending on this transport
777
+ * (objectui#11797): a read on the wire when the write lands was asked before
778
+ * it, so a caller asking after the write must not join it. Dropped however
779
+ * the write ends — a refused or interrupted write may still have changed
780
+ * something. The callers already holding a pending read keep its answer.
781
+ */
782
+ private sendWrite;
457
783
  /** List all registered metadata types (returns the registry rows). */
458
784
  listTypes(): Promise<unknown[]>;
459
785
  /** List items of a metadata type (e.g. `object`, `field`, `view`). */
460
786
  list<T = unknown>(type: string, options?: MetadataListOptions): Promise<T[]>;
787
+ /**
788
+ * The same list read as {@link list}, with what the server serves beside
789
+ * the items (objectui#12349): today that is the automation engine's
790
+ * `registrations` on a `flow` list — see {@link MetadataListAnswer}.
791
+ *
792
+ * One request for both methods: `list` is this read with the items picked
793
+ * out, so a caller asking both at once shares one in-flight GET.
794
+ */
795
+ listResponse<T = unknown>(type: string, options?: MetadataListOptions): Promise<MetadataListAnswer<T>>;
461
796
  /**
462
797
  * List pending DRAFT items (ADR-0033) — what an AI authored but nobody
463
798
  * published yet. `list()` only sees published/active metadata, so a
@@ -517,12 +852,71 @@ declare class MetadataClient {
517
852
  * {@link get} is deliberate and long-standing (the draft envelope's identity
518
853
  * and protection carriers are part of what a draft reader inspects), so it
519
854
  * is preserved by reading the transport directly instead of going through
520
- * `get()`'s unwrap. `unwrapDraftBody` (app-shell) and `unwrapViewDraft`
521
- * (this package) are the shared helpers for taking the body out.
855
+ * `get()`'s unwrap.
856
+ *
857
+ * ⛔ Do NOT hand `.item` to a gate or a write yourself: the body this serves
858
+ * is DECORATED (`_draft`, then `_diagnostics` from `decorateMetadataItem`),
859
+ * and the spec says a served body is not a valid input to the schema that
860
+ * produced it until those come off. {@link extractDraftBody} is the one
861
+ * reader for this method's answer — it takes the presence verdict and then
862
+ * the strip, in that order. That function used to be copy-pasted into four
863
+ * views and only one copy knew the rule (objectui#8181).
864
+ *
865
+ * Its tolerant siblings exist for the OTHER client method: `unwrapDraftBody`
866
+ * (app-shell) and `unwrapViewDraft` (this package) read a draft that arrived
867
+ * through {@link get}, which already unwrapped the envelope. They split on
868
+ * which method feeds them, not on what they mean — all three strip.
869
+ *
870
+ * objectui#11799 — whether a draft exists is answered by the pending-drafts
871
+ * ledger first (see {@link draftLedgerMayHold}), and `?state=draft` is sent
872
+ * only when the ledger lists the name. The item read answers 404 when there
873
+ * is no draft, so opening every item in Studio used to log one error per
874
+ * item for an answer the ledger gives with a 200.
522
875
  */
523
876
  getDraft<T = unknown>(type: string, name: string, options?: {
524
877
  packageId?: string;
525
878
  }): Promise<T | null>;
879
+ /**
880
+ * Whether the pending-drafts ledger (`GET /meta/_drafts`) may hold a draft
881
+ * named `name` (objectui#11799).
882
+ *
883
+ * Read through {@link listDrafts}, the shared read objectui#11797 made: a
884
+ * question asked while a ledger read is pending waits for it, and a write
885
+ * through this transport drops it when the write lands ({@link sendWrite}),
886
+ * so a draft saved a moment ago is never reported absent by a read sent
887
+ * before the save.
888
+ *
889
+ * The WHOLE ledger, matched by name. The `?state=draft` read finds a row
890
+ * under either spelling of the type, and, when it names a package, that
891
+ * package's row or the package-less one. The ledger's `type` and `packageId`
892
+ * filters are exact, so a filtered ledger could miss a draft the item read
893
+ * would serve, and the editor would open the published body over a pending
894
+ * draft. Unfiltered, every miss here is a real absence; a same-name row of
895
+ * another type or package only costs the item read that was always sent.
896
+ *
897
+ * A ledger that cannot be read (403 without an authoring capability, 501 on
898
+ * a kernel without drafts, a network fault) is UNKNOWN, not empty: the item
899
+ * read is sent, as before.
900
+ */
901
+ private draftLedgerMayHold;
902
+ /**
903
+ * Report the runtime authoring gate's advisory findings for a write the
904
+ * server has ALREADY committed (#4133 for the save door, #5026 for the
905
+ * publish door).
906
+ *
907
+ * Everything here is best-effort by construction, and that is the whole
908
+ * contract: the row is committed server-side before this runs, so neither a
909
+ * malformed body, nor a missing key, nor a throwing sink may change what the
910
+ * calling method returns or whether it throws. The server emits `advisories`
911
+ * ONLY when non-empty, so a clean write costs one absent-key check.
912
+ *
913
+ * One helper rather than a copy per door: every door's response declares the
914
+ * key identically, so a second inline copy could only ever drift. The batch
915
+ * door (`ce986aafc`) calls it once per `published[]` element, handing it
916
+ * the element — the object that carries the key there — and the same
917
+ * best-effort contract covers that loop unchanged.
918
+ */
919
+ private emitAdvisories;
526
920
  /**
527
921
  * Save (PUT) a metadata item. The framework accepts both the bare
528
922
  * item payload and the `{ item: ... }` / `{ metadata: ... }`
@@ -541,8 +935,77 @@ declare class MetadataClient {
541
935
  * the rows and reports under `seedApplied` — a data problem never fails the
542
936
  * publish, so callers should check `seedApplied?.success` and warn the user
543
937
  * rather than assume the data went live.
938
+ *
939
+ * Same door as {@link publish} (`POST /meta/:type/:name/publish`), so it
940
+ * reports the gate's advisories the same way (#5026).
941
+ *
942
+ * The BATCH door (`POST /packages/:id/publish-drafts`) is a different route
943
+ * with a method of its own — {@link publishPackageDrafts} — and it reports
944
+ * too. It did not when this paragraph was first written: it discarded
945
+ * per-draft advisories server-side, which was objectstack#9343, and that card
946
+ * has since landed with a ruling that each `published[]` element carries
947
+ * them (`ce986aafc` is the client half).
948
+ *
949
+ * What survives that change is the READING RULE, which is about this method
950
+ * rather than about the other route: it reads the top level of the
951
+ * single-item body and nothing else. A batch-shaped body arriving HERE still
952
+ * reports nothing, because `PublishMetaItemResponse` declares no
953
+ * `published[]` and a client that went looking for findings wherever they
954
+ * might be would be inventing a contract instead of reading one.
955
+ *
956
+ * ## Why there is no `{ success, data }` unwrapping here (`7b433197d`)
957
+ *
958
+ * This method used to unwrap a dispatcher-shaped envelope before returning,
959
+ * while {@link publish} — the same route, a couple of hundred lines down —
960
+ * did not. Two spellings of one wire contract, and nothing said which was
961
+ * right. The tolerance is gone because the route was MEASURED, at the
962
+ * producer rather than inferred from the response schema:
963
+ *
964
+ * - The only mount of `POST /api/v1/meta/:type/:name/publish` is the REST
965
+ * server's own route-manager registration, which answers
966
+ * `res.json(await protocol.publishMetaItem(...))` — the protocol's object
967
+ * verbatim, with no envelope branch. The project-scoped mirror
968
+ * (`/api/v1/environments/:environmentId/...`) re-mounts that same handler.
969
+ * - The `{ success, data }` envelope has exactly one producer, the runtime
970
+ * `HttpDispatcher`, and it does not serve this route: its `/meta` domain
971
+ * has no publish branch, the dispatcher's own route ledger has no row for
972
+ * it, and a three-segment `/meta` path that is not `/published` terminates
973
+ * in a located `routeNotFound`. So no composition — REST server, the
974
+ * dispatcher-only Hono adapter, or both — puts an envelope on this body.
975
+ * - `packages/spec` states the same split in the two docstrings side by
976
+ * side: `PublishMetaItemResponseSchema` describes "the FULL body" this
977
+ * route returns and records that the REST route hands the producer's
978
+ * object to `res.json()` verbatim, while its batch sibling
979
+ * `PublishPackageDraftsResponseSchema` describes a body answered
980
+ * explicitly "inside the dispatcher's `{ success, data }` envelope".
981
+ *
982
+ * ⛔ So an enveloped body on THIS route is not a shape the platform emits,
983
+ * and unwrapping one is not a kindness: it is Commandment #0.1's lenient
984
+ * fallback, a second de-facto contract for a route that declares one. It
985
+ * also fails in the direction that hides the problem — a body that arrived
986
+ * enveloped is one this door did not actually serve, and unwrapping it
987
+ * would present that as a successful promotion.
988
+ *
989
+ * ⚠️ Sibling tolerances in this file are NOT this defect and must stay:
990
+ * {@link listDrafts} reads `data?.data?.drafts` because `GET /meta/_drafts`
991
+ * IS a dispatcher route, and `publishHealthFromResponse` unwraps because the
992
+ * batch door really is enveloped. The rule is per-route, not per-file.
993
+ *
994
+ * ## The bundle member (objectui#12402)
995
+ *
996
+ * Pass `member` to publish one member of a type whose identity is a pair
997
+ * (today only `email_template`, `(name, locale)`), sent as `?member=` — the
998
+ * selector the by-name doors read since objectstack `038d4c958`, and the
999
+ * value a `GET /meta/_drafts` row carries as `member`. A publish that names
1000
+ * none promotes the CANONICAL member's draft, so two languages of one
1001
+ * template drafted outside a package cannot both be published by name
1002
+ * without it. The parameter is OMITTED, never sent empty, when the caller
1003
+ * has no member: the door refuses a blank selector, and refuses any
1004
+ * selector on a type with no members.
544
1005
  */
545
- publishDraft(type: string, name: string): Promise<{
1006
+ publishDraft(type: string, name: string, options?: {
1007
+ member?: string;
1008
+ }): Promise<{
546
1009
  success?: boolean;
547
1010
  seedApplied?: {
548
1011
  success: boolean;
@@ -552,6 +1015,63 @@ declare class MetadataClient {
552
1015
  errors?: unknown[];
553
1016
  };
554
1017
  } & Record<string, unknown>>;
1018
+ /**
1019
+ * Publish EVERY pending draft bound to one package — Studio's "publish whole
1020
+ * app" door, `POST /api/v1/packages/:id/publish-drafts` (ADR-0033; the route
1021
+ * that orders structure-before-seeds server-side and runs the ADR-0038 L3
1022
+ * runtime probes).
1023
+ *
1024
+ * ## Why it is expressed here at all (`ce986aafc`)
1025
+ *
1026
+ * It was not, and that was the defect. Two app-shell call sites fired this
1027
+ * route with a bare `fetch` / a page-private `apiJson`, outside the seam that
1028
+ * covers every other metadata write — so when objectstack#9343 landed and the
1029
+ * server began sending per-draft advisories, the author publishing a whole
1030
+ * app was told nothing, while the SAME button's client-side capability lint
1031
+ * still raised a toast. A door that cannot report is not a door the gate can
1032
+ * reach, however loudly the server speaks.
1033
+ *
1034
+ * ## What it reports, and what it refuses to invent
1035
+ *
1036
+ * `PublishPackageDraftsResponseSchema` declares `advisories` on EACH
1037
+ * `published[]` element — objectstack#9343's ruled shape, the same element
1038
+ * type and the same omitted-when-empty discipline as the single-item door,
1039
+ * and explicitly not a parallel top-level map. So this method emits one
1040
+ * {@link MetadataSaveAdvisoryEvent} per advised element, each naming that
1041
+ * element's own `type` / `name`, through the same sink, event and renderer
1042
+ * the save and single-item publish doors use.
1043
+ *
1044
+ * ⛔ It renders only what the server sent where the schema says it sits:
1045
+ *
1046
+ * - `advisories` is read off the element, by the one shared reader, which
1047
+ * drops anything that is not a complete finding.
1048
+ * - An element that does not carry the `type` and `name` the schema states as
1049
+ * REQUIRED is skipped rather than reported under invented identity — the
1050
+ * event names the item the author has to go fix, and an event that cannot
1051
+ * name it truthfully is worse than silence.
1052
+ * - Nothing is derived, counted or summarised from the batch: no advisory
1053
+ * exists here that the server did not put in the body.
1054
+ *
1055
+ * ## The failure shapes stay the callers'
1056
+ *
1057
+ * Non-2xx throws {@link MetadataError}, like every other method. A 2xx is
1058
+ * RETURNED unexamined — `success: false` is not a failure on this route
1059
+ * (`outcome: 'nothing_to_publish'` answers it too, and the spec's own text
1060
+ * says to read `outcome`, not the boolean), and the two callers have their
1061
+ * own, different rules for the refusal and rolled-back cases. Deciding that
1062
+ * here would change behaviour this card is not about.
1063
+ *
1064
+ * ## The opt-in backfill (objectui#12929)
1065
+ *
1066
+ * `options.backfill` is sent as the body's `backfill` key, as given, and only
1067
+ * when the caller passes one ({@link MetadataPublishPackageDraftsOptions}).
1068
+ * Without it the body is `{}`, byte for byte what every caller sent before,
1069
+ * so a caller that names nothing writes nothing into existing rows. The
1070
+ * receipt is `backfillApplied` on the returned body, unexamined like the rest
1071
+ * of a 2xx: one element per named field with the rows written, and `error`
1072
+ * on a field that stopped short (the publish itself stands).
1073
+ */
1074
+ publishPackageDrafts(packageId: string, options?: MetadataPublishPackageDraftsOptions): Promise<MetadataPublishPackageDraftsResult>;
555
1075
  /**
556
1076
  * Get the 3-state layered view of a metadata item: `code` (the packaged
557
1077
  * artifact baseline), `overlay` (the tenant customisation row alone) and
@@ -607,6 +1127,12 @@ declare class MetadataClient {
607
1127
  * Idempotent: returns the result even when no overlay row existed.
608
1128
  * Pass `state: 'draft'` to discard the pending draft only (keeps the
609
1129
  * published overlay intact) — useful for a Studio "Discard draft" button.
1130
+ *
1131
+ * Pass `packageId` to name the package whose row the delete removes, sent
1132
+ * as `?package=`. A delete that names none addresses the one package that
1133
+ * holds the item; when two or more packages hold it the door refuses it
1134
+ * `409 RESOURCE_CONFLICT`, deletes nothing, and names `?package=` as the
1135
+ * remedy. That refusal is thrown like any other {@link MetadataError}.
610
1136
  */
611
1137
  reset<T = unknown>(type: string, name: string, options?: MetadataDeleteOptions): Promise<T>;
612
1138
  /**
@@ -615,9 +1141,16 @@ declare class MetadataClient {
615
1141
  * `{ success, version, seq, message }`. Throws a `404 no_draft` if
616
1142
  * nothing is pending and `409 metadata_conflict` if the published
617
1143
  * overlay moved while the draft was sitting.
1144
+ *
1145
+ * Reports the runtime authoring gate's advisory findings through
1146
+ * {@link MetadataClientConfig.onSaveAdvisory} when the response carries them
1147
+ * (#5026 / objectstack#9176), with `door: 'publish'`. This is the door the
1148
+ * Studio designer takes on every edit, so it is the one whose findings an
1149
+ * author actually sees.
618
1150
  */
619
1151
  publish<T = unknown>(type: string, name: string, options?: {
620
1152
  message?: string;
1153
+ packageId?: string;
621
1154
  }): Promise<T>;
622
1155
  /**
623
1156
  * Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
@@ -877,6 +1410,35 @@ interface MetadataCacheStats {
877
1410
  * As soon as the backend supports `sys_user_preference`, persistence
878
1411
  * "lights up" with no code change.
879
1412
  *
1413
+ * ## One read per load (objectui#12187)
1414
+ *
1415
+ * The console builds one adapter per slot (favorites, recent items, flow
1416
+ * palette recents) over the same data source and user, and their providers
1417
+ * call `load()` in the same effects pass. Those loads share ONE request: the
1418
+ * first `load()` opens a read for its (data source, resource, user), every
1419
+ * `load()` of that scope issued before the next microtask adds its key to it,
1420
+ * and the read is then sent once and each adapter takes its own row from the
1421
+ * answer. A read that collected one key sends the per-key query this adapter
1422
+ * always sent; several keys go as one `key in (...)` query.
1423
+ *
1424
+ * Nothing is kept past that: the read closes the moment it is sent, so a later
1425
+ * `load()` (a new adapter after a language or account switch) asks the server
1426
+ * again, and a different user or data source never joins another's read.
1427
+ * `save()` does not use it: a write still finds and writes its own row.
1428
+ *
1429
+ * ## A family of keys in the same read (objectui#12418)
1430
+ *
1431
+ * Some state is one row per subject the console cannot name at load time: the
1432
+ * list state of every object a user has opened is a row per object,
1433
+ * `ui.list.<object>`, and which objects those are is only known from the rows
1434
+ * themselves. {@link createObjectStackUserPreferenceFamily} loads such a family
1435
+ * by its key PREFIX, and joins the same shared read: the read then asks for its
1436
+ * keys OR any key starting with the prefix, still one request. A prefix bounds
1437
+ * nothing the way the unique index bounds a key, so the read asks for at most
1438
+ * {@link PREFERENCE_FAMILY_ROW_LIMIT} family rows on top of its keys; an
1439
+ * answer that fills that limit may have left a key's row out, and such a key
1440
+ * asks for its own row instead (the same fallback an untagged row takes).
1441
+ *
880
1442
  * @module
881
1443
  */
882
1444
 
@@ -899,10 +1461,31 @@ interface ObjectStackUserStateAdapterOptions {
899
1461
  */
900
1462
  onError?: (where: 'load' | 'save', error: unknown) => void;
901
1463
  }
1464
+ /**
1465
+ * Generic persistence adapter for a user-scoped list.
1466
+ *
1467
+ * Implementations must be safe to call concurrently and should *never*
1468
+ * throw — failures should be swallowed so the hosting provider can degrade
1469
+ * to localStorage-only mode without crashing the UI.
1470
+ *
1471
+ * This is the one declaration of the name: `@object-ui/app-shell`'s
1472
+ * `UserStateAdaptersProvider` injects adapters of this type and re-exports it
1473
+ * (objectui#6349, batch 9).
1474
+ */
902
1475
  interface UserDataAdapter<T> {
1476
+ /** Load the persisted list for the current user. Resolve to [] when absent. */
903
1477
  load(): Promise<T[]>;
1478
+ /** Persist the full list (debounced upstream). Errors are silently ignored. */
904
1479
  save(items: T[]): Promise<void>;
905
1480
  }
1481
+ /**
1482
+ * How many rows of one key family a shared read asks for (objectui#12418).
1483
+ * The unique index bounds a KEY to one row; nothing bounds a prefix, so the
1484
+ * read's `$top` is its key count plus this many rows per family. An answer
1485
+ * that reaches its `$top` is read as possibly cut short: see
1486
+ * {@link SharedAnswer.saturated}.
1487
+ */
1488
+ declare const PREFERENCE_FAMILY_ROW_LIMIT = 200;
906
1489
  /**
907
1490
  * Build a `UserDataAdapter<T>` backed by ObjectStack.
908
1491
  *
@@ -910,6 +1493,290 @@ interface UserDataAdapter<T> {
910
1493
  * one per slot you want to persist.
911
1494
  */
912
1495
  declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
1496
+ interface ObjectStackUserPreferenceFamilyOptions {
1497
+ /** Connected data source (usually the one provided by `<AdapterProvider>`). */
1498
+ dataSource: DataSource<any>;
1499
+ /** Authenticated user id. */
1500
+ userId: string;
1501
+ /**
1502
+ * The family's key prefix, dotted and ending in a dot, e.g. `ui.list.`: the
1503
+ * family is every key that starts with it, one row per subject
1504
+ * (`ui.list.account`, `ui.list.contact`, …).
1505
+ */
1506
+ prefix: string;
1507
+ /** Override the storage object name. Defaults to `"sys_user_preference"`. */
1508
+ resource?: string;
1509
+ /** Optional diagnostics hook; defaults to noop. */
1510
+ onError?: (where: 'load' | 'save', error: unknown) => void;
1511
+ }
1512
+ /**
1513
+ * A user's rows of one key family: every `sys_user_preference` row whose key
1514
+ * starts with the family's prefix, each holding one JSON object.
1515
+ *
1516
+ * Same contract as {@link UserDataAdapter}: never throws; a failed read loads
1517
+ * as no rows and a failed write is reported to `onError` and swallowed, so
1518
+ * the hosting provider keeps its localStorage state.
1519
+ */
1520
+ interface UserPreferenceFamily<T> {
1521
+ /** The key prefix this family covers. */
1522
+ readonly prefix: string;
1523
+ /**
1524
+ * Load every row of the family, joining the shared read of the load
1525
+ * (one request with the slots' keys). Resolves to `{ [key]: value }`, with
1526
+ * only the rows whose value is a JSON object; `{}` when there are none.
1527
+ */
1528
+ load(): Promise<Record<string, T>>;
1529
+ /**
1530
+ * Upsert one key's row. A key outside the family is refused (reported to
1531
+ * `onError`, nothing written). Writes to one key run in order.
1532
+ */
1533
+ save(key: string, value: T): Promise<void>;
1534
+ }
1535
+ /**
1536
+ * Build a {@link UserPreferenceFamily} backed by ObjectStack: one row per key,
1537
+ * loaded together by prefix in the shared read (see the module header), each
1538
+ * written with the same upsert a single-key adapter uses.
1539
+ */
1540
+ declare function createObjectStackUserPreferenceFamily<T extends object = Record<string, unknown>>(options: ObjectStackUserPreferenceFamilyOptions): UserPreferenceFamily<T>;
1541
+
1542
+ /**
1543
+ * The served form of a picklist-bound field, turned back into the authored
1544
+ * form before a writer that seeded its body from a served read PUTs it
1545
+ * (objectui#10202, objectui#11692).
1546
+ *
1547
+ * ## Why the writer has to do this
1548
+ *
1549
+ * A select field may name a shared list instead of carrying its own options:
1550
+ * `picklist: 'industry'` (`FieldSchema.picklist`, `@objectstack/spec`
1551
+ * `data/picklist.zod.ts`). The runtime serves such a field with BOTH keys:
1552
+ * `picklist`, still naming the list, and `options`, RESOLVED from that list and
1553
+ * from every `picklistExtensions` entry other packages add to it
1554
+ * (`PicklistServedFieldSchema`). Every object read carries that served form —
1555
+ * the object list `GET /meta/object`, `GET /meta/object/NAME`, and the
1556
+ * `effective` layer of `GET /meta/object/NAME/layers` alike, because the
1557
+ * runtime resolves the binding in the one fold all three share.
1558
+ *
1559
+ * The authoring door refuses the two keys together: `FieldSchema` adds an issue
1560
+ * at `fields.FIELD.options` whose prescription is "Keep `picklist` and delete
1561
+ * `options`". So a writer that sends its seed back unchanged gets
1562
+ * `422 INVALID_METADATA` for the whole object, whatever the author edited —
1563
+ * an OWD batch edit, a relabel, a field reorder, anything.
1564
+ * The resolved list belongs to the picklist, not to the field: the producer of
1565
+ * the PUT drops it (objectstack's producer obligation, pinned server-side in
1566
+ * `protocol-picklist-served-roundtrip.test.ts`).
1567
+ *
1568
+ * ## What this changes, and what it never touches
1569
+ *
1570
+ * EVERY field that names a picklist leaves without `options` — not only the one
1571
+ * the author edited, because the whole document is re-sent and any bound field
1572
+ * in it is refused. A field that names no picklist is not touched at all: its
1573
+ * inline `options` stay byte-identical. No other key of any field, and no key of
1574
+ * the document, is touched.
1575
+ *
1576
+ * "Names a picklist" is the spec's own test, `picklist !== undefined` (the
1577
+ * `FieldSchema` refinement), so this drops `options` on exactly the fields the
1578
+ * door would refuse for carrying both.
1579
+ *
1580
+ * Both `fields` shapes a document can carry are handled: the spec's record keyed
1581
+ * by field name, and the array `readFields` / `writeFields` round-trip for a
1582
+ * document that arrived that way.
1583
+ *
1584
+ * ⛔ Not at the transport. `MetadataClient.save` and the other metadata-write
1585
+ * doors cannot tell a served copy from an author who wrote both keys, and the
1586
+ * second must stay a loud refusal with its prescription. The writer can tell:
1587
+ * it seeded its body from the served read. So this is exported for WRITERS —
1588
+ * the opposite of `assertObjectMetadataWritable`, which is exported for doors —
1589
+ * and the rule each writer applies is one sentence: a body seeded from a served
1590
+ * object read goes through this function on its way to the PUT; a body built
1591
+ * from the author's own input does not.
1592
+ *
1593
+ * ## Why it lives in this package (objectui#11692)
1594
+ *
1595
+ * The writers that seed from a served read are not all in one package: the
1596
+ * Studio and metadata-admin surfaces live in `@object-ui/app-shell`, and the
1597
+ * Setup fields and objects pages in `@object-ui/plugin-designer`, which does
1598
+ * not depend on `app-shell`. Both depend on this package, which also owns the
1599
+ * client that serves the read and carries the write — the same reason
1600
+ * `extractDraftBody` lives here. One function, not one copy per package: a
1601
+ * second copy is free to drift from the spec's served shape on its own.
1602
+ *
1603
+ * Pure: the input is never mutated, and a document with nothing to drop comes
1604
+ * back BY REFERENCE, so a caller can tell "unchanged" by identity.
1605
+ */
1606
+ declare function dropServedPicklistOptions<T extends Record<string, unknown>>(body: T): T;
1607
+
1608
+ /**
1609
+ * One issue → a single field-anchored line: `• fields.amount.type — Required`.
1610
+ *
1611
+ * Exported so a caller that formats a LIST of failures — `app-shell`'s publish
1612
+ * formatter, whose `failed[]` entries carry their own `issues` — writes each
1613
+ * issue in this same grammar instead of keeping a second copy of it.
1614
+ */
1615
+ declare function formatMetadataIssue(i: MetadataValidationIssue): string;
1616
+ /**
1617
+ * Format a caught save/publish error for a banner/toast. When the error carries
1618
+ * spec-validation issues, list them (one field per line) so the user sees the
1619
+ * offending fields and what to do about them; otherwise fall back to the plain
1620
+ * message.
1621
+ */
1622
+ declare function formatMetadataError(e: unknown): string;
1623
+
1624
+ /**
1625
+ * The object-metadata write invariant, placed at the DOOR rather than at the
1626
+ * writers (objectui#8676).
1627
+ *
1628
+ * ## Why this module exists — the enumeration, not the count
1629
+ *
1630
+ * objectui#7714 ruled one client behaviour: *a half-filled relationship stays
1631
+ * client-side and the PUT body never carries a `lookup` without a non-empty
1632
+ * `reference`*. Its PR implemented that ruling by naming the writers it knew
1633
+ * about — two of them — and putting the assertion inside each one's array-to-map
1634
+ * conversion (`toFieldsMap`). The very failure the ruling was written against
1635
+ * was then reproduced on a THIRD writer neither conversion covers, in that
1636
+ * card's own required dogfood (fixed in `9073cf018`). objectui#8676 swept and
1637
+ * found nine more.
1638
+ *
1639
+ * The durable half of that card is a sentence about method, not a number:
1640
+ *
1641
+ * > a ruling that enumerates writers is only as good as the enumeration.
1642
+ *
1643
+ * A hand list of writers is stale the next time somebody adds one, and nothing
1644
+ * says so. So this invariant is not attached to writers at all. It is attached
1645
+ * to the DOORS — the places that put bytes on the wire — because the doors are a
1646
+ * CLOSED set that this repository owns, while the writers are an OPEN set it
1647
+ * does not. Three in-repo transports can PUT `/meta/:type/:name`, and
1648
+ * `scripts/check-object-metadata-write-doors.mjs` derives that set on every run
1649
+ * rather than restating it:
1650
+ *
1651
+ * 1. `MetadataClient.save` `packages/data-objectstack/src/metadata-client.ts`
1652
+ * 2. `importObjectDraft` `packages/app-shell/src/views/metadata-admin/external/api.ts`
1653
+ * 3. the `@objectstack/client` SDK's `meta.saveItem`, reached in this repo
1654
+ * through `MetadataService`'s one seam
1655
+ *
1656
+ * Every writer in the repo reaches the server through one of those three. Guard
1657
+ * them and the writer count stops mattering; add a fourth transport and the gate
1658
+ * turns red naming it. That is the whole design: ⛔ do not re-open this by
1659
+ * adding a writer list anywhere.
1660
+ *
1661
+ * ## What it asserts, and the two things it deliberately does NOT
1662
+ *
1663
+ * It asserts two field-level invariants, and nothing else:
1664
+ *
1665
+ * - objectui#7714's: a relationship (`lookup` / `master_detail`) carries a
1666
+ * usable `reference`;
1667
+ * - objectui#11253's: a choice (`select` / `radio`) carries an option source,
1668
+ * a non-empty `options` list or a shared `picklist`. This is the objectui
1669
+ * branch of the maintainer's ruling A on objectstack#20827, whose door
1670
+ * refuses "a `select` / `radio` with neither `options` nor `picklist`".
1671
+ *
1672
+ * ⛔ **NOT a client-side revalidation of the document.** Parsing the whole body
1673
+ * through `ObjectSchema` before the PUT would refuse plugin-registered keys the
1674
+ * SERVER accepts, and it would promote a client PREDICTION into a block — which
1675
+ * is exactly what objectui#4306 / objectui#6980 ruled against for the designer's
1676
+ * live Zod pass, on the stated ground that a schema issue on a draft the server
1677
+ * ACCEPTS would dead-bolt Save with no on-screen editor able to clear it.
1678
+ *
1679
+ * Neither invariant forecloses anything the server would have taken, and that
1680
+ * is the whole of the dead-bolt argument for both:
1681
+ *
1682
+ * - The relationship refusal: the same body is refused one layer down with a
1683
+ * 422 on `fields.NAME.reference`, so the guard only moves an identical
1684
+ * refusal earlier, where it can name the field while it is still on screen.
1685
+ * - The choice refusal: the same body is refused one layer down at
1686
+ * `fields.NAME.options`, an empty list included, since `@objectstack/spec`
1687
+ * 17.7.0 shipped the door (objectstack#21390, the ruling's own door). Until
1688
+ * that release the installed spec still accepted a choice with no option
1689
+ * source and this refusal was deliberately AHEAD of the server, by the
1690
+ * ruling's execution parameter (2): "if Studio saves a `select` before its
1691
+ * options exist, the objectui side changes its order first (a card in that
1692
+ * lane) and the door closes after". The door has closed, the order changed
1693
+ * first, and the refusal collapsed into the relationship form at
1694
+ * objectui#11717's bump. The editors still clear it on screen: the Studio
1695
+ * data page and the metadata-admin object editor carry an options editor in
1696
+ * that field's inspector (both pinned by their
1697
+ * `*.choiceWithoutOptions-11253` suites); plugin-designer's drawer has no
1698
+ * options editor, so it stops offering the choice types, a stored `select`
1699
+ * keeps its type there and can be retyped or removed, and a stored `radio`
1700
+ * is among the types that page carries through unedited and sends to
1701
+ * metadata-admin, as it already does for a target-less `master_detail`.
1702
+ * `object-metadata-write-guard.derivation.test.ts` re-measures "refused one
1703
+ * layer down" for both invariants on every run.
1704
+ *
1705
+ * ⛔ **NOT strip-and-report-saved.** Dropping the offending field and reporting
1706
+ * success would trade a visible refusal for an invisible deletion —
1707
+ * objectstack#4001's shape, ruled out for this family in objectui#7714 and again
1708
+ * in `9073cf018`. It throws; the caller surfaces the message.
1709
+ *
1710
+ * ## The type lists are pinned to the contract, not to memory
1711
+ *
1712
+ * {@link RELATIONSHIP_TYPES_REQUIRING_REFERENCE} and
1713
+ * {@link CHOICE_TYPES_REQUIRING_OPTIONS} are the two lists this module keeps,
1714
+ * and they are the same class of hazard the module exists to close, so neither
1715
+ * gets to be a remembered list either.
1716
+ * `object-metadata-write-guard.derivation.test.ts` DERIVES both from the
1717
+ * installed `@objectstack/spec`:
1718
+ *
1719
+ * - the relationship set from every member of `FieldType`, parsed as
1720
+ * `{ type, label }` through `FieldSchema`, keeping those refused at path
1721
+ * `reference`;
1722
+ * - the choice set from the same members run through `checkFieldCompleteness`
1723
+ * (ADR-0078's author-time rule, which the ruling's door adopts), keeping
1724
+ * those it reports as `field/choice-without-options` at `error` severity.
1725
+ * The door itself shipped in 17.7.0, and the pin holds the two answers equal.
1726
+ *
1727
+ * A spec release that changes either set turns that pin red instead of leaving
1728
+ * a guard that quietly stopped covering the contract.
1729
+ *
1730
+ * The derivation is a TEST and not a runtime probe on purpose: parsing 50
1731
+ * field-type documents on the way to every save is a cost paid on the hot path
1732
+ * to re-learn something that changes at most once per spec release, and a probe
1733
+ * that throws at runtime has to choose between blocking writes and failing open.
1734
+ * A pin has neither problem — it fails at CI time, loudly, with nothing at stake.
1735
+ */
1736
+ /**
1737
+ * Field types whose `reference` — the target object a relationship links to —
1738
+ * `@objectstack/spec` requires to be present and non-empty.
1739
+ *
1740
+ * Derived from the installed artifact by the pin named above, never recalled.
1741
+ */
1742
+ declare const RELATIONSHIP_TYPES_REQUIRING_REFERENCE: readonly string[];
1743
+ /**
1744
+ * Field types that offer nothing to choose without an option source — a
1745
+ * non-empty `options` list or a shared `picklist` (objectui#11253, the objectui
1746
+ * branch of the maintainer's ruling A on objectstack#20827).
1747
+ *
1748
+ * Derived from the installed artifact by the pin named above, never recalled.
1749
+ */
1750
+ declare const CHOICE_TYPES_REQUIRING_OPTIONS: readonly string[];
1751
+ /** The metadata type whose documents carry the `fields` map this guard reads. */
1752
+ declare const OBJECT_METADATA_TYPE = "object";
1753
+ /**
1754
+ * Refuse an object-metadata write whose body carries a relationship field with
1755
+ * no usable target, or a choice field with no option source.
1756
+ *
1757
+ * A no-op for every other metadata type and for any body with no readable
1758
+ * `fields` member — this door serves `view`, `app`, `flow`, `permission`,
1759
+ * `hook` and `dashboard` writes too, and has no opinion about them.
1760
+ *
1761
+ * ## The message is the same at every door, and names no code (objectui#11785)
1762
+ *
1763
+ * The thrown text reaches authors: Studio keeps it under a refusal's Details,
1764
+ * and the other surfaces that save through a door show it as their banner. So
1765
+ * it names the field, its type and the remedy, and ⛔ no class, package or
1766
+ * tracker id. The rationale for each rule lives in this module's docblock, not
1767
+ * in the runtime string. The door's label is not printed either, which makes the
1768
+ * text a function of the body alone: a caller that kept the body it sent can
1769
+ * re-run this guard on it and get the identical message, which is how Studio
1770
+ * tells this refusal apart from any other failure without parsing the prose.
1771
+ *
1772
+ * @param type the metadata type segment of the write (`'object'`, `'view'`, …)
1773
+ * @param item the body about to be serialised onto the wire
1774
+ * @param _writer the door's label. Every door still names itself at the call,
1775
+ * and the stack trace says where the write stopped; the message
1776
+ * does not print it (see above)
1777
+ * @throws Error naming the offending field, before any request is issued
1778
+ */
1779
+ declare function assertObjectMetadataWritable(type: unknown, item: unknown, _writer: string): void;
913
1780
 
914
1781
  /**
915
1782
  * Map human-readable filter operator names produced by SDUI view configs
@@ -947,6 +1814,156 @@ declare class MalformedFilterError extends Error {
947
1814
  }
948
1815
  /** Detect the malformed-filter refusal, whether raised here or by the server. */
949
1816
  declare function isMalformedFilterError(error: unknown): boolean;
1817
+ /**
1818
+ * A `where` that reached `aggregate()`'s SPEC-SHAPE branch as an array the spec
1819
+ * cannot read as a filter — an unlowered `ViewFilterRule[]` above all.
1820
+ *
1821
+ * WHY THIS IS A REFUSAL AND NOT A LOWERING (objectui#6825, maintainer ruling
1822
+ * 2026-08-30, option A). The two branches of `aggregate()` take a filter by
1823
+ * different names: the analytics branch takes `filter` and lowers it through
1824
+ * `translateFilterArray` (objectui#6302), the spec-shape branch takes `where`
1825
+ * and posts it to `POST /data/:object/query` verbatim. Lowering here too was
1826
+ * option B and was REFUSED: it is the tolerant-consumer direction AGENTS.md
1827
+ * #0.1 exists to stop, and it would bless as supported a params shape
1828
+ * `AggregateParams` does not even declare. Retiring the branch was option D and
1829
+ * was refused as the riskier move — this repo has no producer that can reach it
1830
+ * (`ObjectChart`'s gate requires a non-array `aggregate.groupBy`, and all three
1831
+ * in-tree constructors pass a string), but `ObjectChart`'s props are `any` and
1832
+ * an out-of-repo host may already send this shape. So the branch stays, and it
1833
+ * says no out loud.
1834
+ *
1835
+ * What that buys is the FAILURE MODE, not a failure count. The receiving engine
1836
+ * already refuses these arrays ("is not a filter" — objectstack's
1837
+ * `engine-filter-array-lowering.test.ts`), so nothing that reaches this point
1838
+ * could have produced a correct number; the pre-existing behaviour was to find
1839
+ * that out on the wire, or worse, to have the predicate quietly dropped while
1840
+ * the chart rendered confident, wrong figures with no signal to their author.
1841
+ * This moves the same refusal to the producer, where the author can act on it.
1842
+ *
1843
+ * Carries the `INVALID_FILTER` / 400 pair its sibling {@link MalformedFilterError}
1844
+ * carries, so `isMalformedFilterError()` recognises it and a failed widget renders
1845
+ * "this filter is malformed" rather than "check your connection" (objectui#3066).
1846
+ */
1847
+ declare class UnloweredAggregateWhereError extends Error {
1848
+ readonly code = "INVALID_FILTER";
1849
+ readonly httpStatus = 400;
1850
+ /** The value as received, so a caller can log what its producer actually built. */
1851
+ readonly where: unknown;
1852
+ /** The object `aggregate()` was called for. */
1853
+ readonly resource: string;
1854
+ constructor(where: unknown, resource: string);
1855
+ }
1856
+ /**
1857
+ * The keys `aggregate()`'s ANALYTICS branch reads and its SPEC-SHAPE branch does
1858
+ * not: `filter`, `field`, `function`.
1859
+ *
1860
+ * Written out once, and read by both the refusal below and its message, so the
1861
+ * set a caller is told about cannot drift from the set that is checked.
1862
+ *
1863
+ * ⛔ Deliberately NOT "every key the spec-shape branch does not read". A refusal
1864
+ * scoped that widely would reject `orderBy`, a future spec key, or any harmless
1865
+ * extra a host happens to carry, and it would satisfy a naive "the spec-shape
1866
+ * branch refuses `filter`" test while breaking traffic nobody complained about.
1867
+ * These three are named one by one because they are the OTHER branch's own
1868
+ * parameters: their presence says the params were built for the analytics wire
1869
+ * and arrived on the spec-shape one, which is a producer defect with a name.
1870
+ */
1871
+ declare const ANALYTICS_ONLY_AGGREGATE_KEYS: readonly ["filter", "field", "function"];
1872
+ type AnalyticsOnlyAggregateKey = (typeof ANALYTICS_ONLY_AGGREGATE_KEYS)[number];
1873
+ /**
1874
+ * Analytics-branch params — `filter`, `field`, `function` — that reached
1875
+ * `aggregate()`'s SPEC-SHAPE branch, which reads none of them.
1876
+ *
1877
+ * WHY A REFUSAL AND NOT A DROP (`503cd8b89`). This applies the maintainer
1878
+ * ruling of 2026-08-30 on objectui#6825 — option A, REFUSE at the producer — to
1879
+ * the rest of the same branch. That ruling's reason was that a shape the spec's
1880
+ * own gate would reject is off-contract at the PRODUCER, so the adapter says no
1881
+ * instead of degrading leniently; the reason lands harder here, because a
1882
+ * silent drop is precisely the disposition it refused for `where`. Leaving one
1883
+ * key on this branch refusing while three others vanish without a word would be
1884
+ * harder to explain than the old behaviour.
1885
+ *
1886
+ * ⭐ AND THE DROP HERE IS WORSE THAN THE `where` HALF. `filter` is not the only
1887
+ * casualty: `field` and `function` are the analytics branch's whole measure, and
1888
+ * the spec-shape branch copies a measure only out of `aggregations`. So the
1889
+ * legacy shape `{ field, function, groupBy, filter }` whose `groupBy` happens to
1890
+ * be an ARRAY produced a `queryAst` carrying a `groupBy` and NO `aggregations`
1891
+ * at all — a grouping with no measure — with the author's predicate gone too.
1892
+ * The chart still rendered, the numbers were wrong, and there was nothing to
1893
+ * look at.
1894
+ *
1895
+ * HOW A LEGACY CALLER GETS HERE, measured in this repo on 2026-09-08.
1896
+ * `Array.isArray(params.groupBy)` is one of `looksLikeSpecShape`'s three
1897
+ * disjuncts, and `ObjectChart`'s `runAggregate` gates only on the STRUCTURED
1898
+ * node shape (`gb && typeof gb === 'object' && !Array.isArray(gb)`), so an
1899
+ * ARRAY `aggregate.groupBy` falls through to its legacy call — `{ field,
1900
+ * function, groupBy, filter }` — and that call lands here.
1901
+ * `ObjectMetricWidget.computeOne` forwards `aggregate.groupBy` the same way.
1902
+ * Both take the value from authored widget metadata across an `any` seam
1903
+ * (`isObjectProvider`'s `aggregate?: any`, `ds: any`), so the type system
1904
+ * cannot refuse it and this runtime refusal is what an author gets instead.
1905
+ *
1906
+ * ⛔ WHAT THIS DELIBERATELY DOES NOT DO. It does not ROUTE the legacy shape back
1907
+ * to the analytics branch: that is the tolerant-consumer direction AGENTS.md
1908
+ * §0.1 exists to stop, it is the option #6825 refused for `where`, and it could
1909
+ * not work anyway — the analytics branch posts `dimensions: [params.groupBy]`,
1910
+ * so an array `groupBy` would go out nested. It does not widen `AggregateParams`
1911
+ * (`@object-ui/types`), which is a separate contract question. And it does not
1912
+ * refuse a key that is present but nullish: `undefined`/`null` carries nothing
1913
+ * for the branch to drop, and both `ObjectChart` and `ObjectMetricWidget` build
1914
+ * their params by spreading possibly-absent authored values.
1915
+ *
1916
+ * Carries the `INVALID_FILTER` / 400 pair its siblings {@link MalformedFilterError}
1917
+ * and {@link UnloweredAggregateWhereError} carry, so `isMalformedFilterError()`
1918
+ * recognises it and a failed widget renders "this filter is malformed" rather
1919
+ * than "check your connection" (objectui#3066). One branch, one envelope: a
1920
+ * caller can catch `aggregate()`'s producer-side refusals with one predicate.
1921
+ */
1922
+ declare class AnalyticsKeysOnSpecShapeError extends Error {
1923
+ readonly code = "INVALID_FILTER";
1924
+ readonly httpStatus = 400;
1925
+ /** The offending keys, in the order {@link ANALYTICS_ONLY_AGGREGATE_KEYS} lists them. */
1926
+ readonly keys: readonly AnalyticsOnlyAggregateKey[];
1927
+ /** What each offending key carried, so a producer is identifiable from a log. */
1928
+ readonly received: Record<string, unknown>;
1929
+ /** The object `aggregate()` was called for. */
1930
+ readonly resource: string;
1931
+ constructor(keys: readonly AnalyticsOnlyAggregateKey[], params: any, resource: string);
1932
+ }
1933
+ /**
1934
+ * An ARRAY `filter` that reached `aggregate()`'s ANALYTICS branch and that the
1935
+ * protocol's lowering sink cannot turn into a `FilterCondition` — an infix join
1936
+ * (`[condA, 'or', condB]`) above all.
1937
+ *
1938
+ * WHY A REFUSAL AND NOT A DROP (objectui#7752; maintainer ruling of 2026-09-05
1939
+ * on objectstack#15828, which reads that card as a frontend defect: the
1940
+ * protocol is right and the adapter was wrong). `parseFilterAST` answers
1941
+ * `undefined` for an array it cannot read, and `undefined` here would post a
1942
+ * body with NO `where` at all: the aggregate would then be computed over every
1943
+ * row and returned as a confident number, with the author's predicate gone and
1944
+ * nothing on the wire to say it went. That is the same silent over-fetch
1945
+ * {@link MalformedFilterError} exists to stop, one door further along.
1946
+ *
1947
+ * Deliberately NOT {@link UnloweredAggregateWhereError}, whose meaning is not
1948
+ * widened to cover this: that one guards the SPEC-SHAPE branch, whose `where`
1949
+ * is posted verbatim and must therefore have been lowered by its PRODUCER
1950
+ * (objectui#6825, ruling 2026-08-30 option A — refuse, never lower). This
1951
+ * branch's `filter` is authoring input and IS lowered here, at the door the
1952
+ * protocol names; this error covers only the input that sink cannot lower.
1953
+ *
1954
+ * Carries the `INVALID_FILTER` / 400 pair both siblings carry, so
1955
+ * `isMalformedFilterError()` recognises it and a failed widget renders "this
1956
+ * filter is malformed" rather than "check your connection" (objectui#3066).
1957
+ */
1958
+ declare class UnlowerableAnalyticsFilterError extends Error {
1959
+ readonly code = "INVALID_FILTER";
1960
+ readonly httpStatus = 400;
1961
+ /** The filter as received, so a caller can log what its producer actually built. */
1962
+ readonly filter: unknown;
1963
+ /** The object `aggregate()` was called for. */
1964
+ readonly resource: string;
1965
+ constructor(filter: unknown, resource: string, detail?: string);
1966
+ }
950
1967
  /**
951
1968
  * Serialize a `$orderby` to the server's `sort` shorthand
952
1969
  * (`field,-other_field`), for every shape `QueryParams['$orderby']` declares.
@@ -979,7 +1996,7 @@ declare function getSharedDiscovery(baseUrl: string, fetcher: () => Promise<unkn
979
1996
  declare function clearSharedDiscoveryCache(): void;
980
1997
  /**
981
1998
  * Read the cross-object atomic-batch capability from a `discovery` document
982
- * (framework #3298 / objectui #2693). The server advertises it hierarchically
1999
+ * (objectstack-ai/objectstack#3298 / objectui #2693). The server advertises it hierarchically
983
2000
  * under `capabilities.transactionalBatch.enabled`; the published
984
2001
  * `@objectstack/client` also accepts the flat `capabilities.transactionalBatch:
985
2002
  * boolean` form and normalizes the two — mirror that here so the adapter reads
@@ -992,7 +2009,7 @@ declare function clearSharedDiscoveryCache(): void;
992
2009
  * real error.
993
2010
  * - `false` — the backend explicitly does NOT (route absent, or a runtime that
994
2011
  * can't open a transaction).
995
- * - `undefined` — the capability is absent, i.e. the backend predates #3298;
2012
+ * - `undefined` — the capability is absent, i.e. the backend predates objectstack-ai/objectstack#3298;
996
2013
  * the caller must keep the legacy runtime-probe fallback (we can't tell
997
2014
  * whether `/batch` exists without trying it).
998
2015
  */
@@ -1045,22 +2062,52 @@ declare const API_ACCESS_DENIED_CODES: readonly ["OBJECT_API_DISABLED", "OBJECT_
1045
2062
  declare function isApiAccessDeniedError(error: unknown): boolean;
1046
2063
  /**
1047
2064
  * What the by-name meta app route said about THIS session's access to an app
1048
- * (objectui#4252 / objectstack#8013).
2065
+ * (objectui#4252 / objectstack#8013, widened by objectui#9262).
1049
2066
  *
1050
- * - `granted` — the route served the app document.
1051
- * - `denied` — the app EXISTS and the session lacks its `requiredPermissions`.
1052
- * The only verdict a caller may render as an authorization refusal.
1053
- * - `unknown` — anything else: an absent app, an unpublished one, an app
1054
- * withheld by an absent optional service, an unreachable server, an adapter
1055
- * that cannot ask. All of these are cases where the server declined to say
1056
- * that a permission of the caller's is missing, so no caller may claim it.
2067
+ * - `granted` — the route served the app document.
2068
+ * - `denied` — the app EXISTS and the session lacks its
2069
+ * `requiredPermissions`. The only verdict a caller may render as an
2070
+ * authorization refusal.
2071
+ * - `not_found` — the route answered, and what it answered is that it has no
2072
+ * app at this name for this session. WHICH absence it is — never created, a
2073
+ * typo, an unpublished draft, an app gated by an absent optional service —
2074
+ * is deliberately NOT distinguished: the control plane answers the same way
2075
+ * for every app the caller may not see, and the 2026-08-12 ruling keeps it
2076
+ * that way. A caller may say the app cannot be opened; it may never say why.
2077
+ * - `unreachable` — the probe could not obtain an answer at all: the transport
2078
+ * failed, the server erred, the request timed out. Says nothing about the
2079
+ * app.
2080
+ * - `unknown` — nothing was measured, because nothing could be asked: a
2081
+ * host DataSource that does not implement this probe (the console is
2082
+ * protocol-agnostic — AGENTS #1), or no app name to ask about.
1057
2083
  *
1058
- * Three values rather than a boolean because the third is not a shade of the
1059
- * other two: "the app is missing" and "I could not find out" both have to leave
1060
- * the caller's existing copy alone, and collapsing them into `false` invites a
1061
- * consumer to read a failed probe as a positive absence.
2084
+ * ## Why `not_found` and `unreachable` are separate members (objectui#9262)
2085
+ *
2086
+ * They were one `unknown` before, and the screen above them therefore asserted
2087
+ * a transient publish state over both. The distinction is not new information
2088
+ * to obtain — it is already on the wire, and folding it away was the defect.
2089
+ * The maintainer ruling (2026-09-13) named the two members; this type is the
2090
+ * two-member widening that ruling declared.
2091
+ *
2092
+ * ⚠️ `unknown` no longer means "absent". A consumer that treats `unknown` as a
2093
+ * positive absence is reading a verdict that now means the opposite: that the
2094
+ * question was never put.
2095
+ */
2096
+ type AppAccessVerdict = 'granted' | 'denied' | 'not_found' | 'unreachable' | 'unknown';
2097
+ /**
2098
+ * What {@link ObjectStackAdapter.probeMetaItemAccess} obtained about one item
2099
+ * of any type the by-name meta route gates per caller (objectui#12112). The
2100
+ * verdicts mean what they mean for an app ({@link AppAccessVerdict}).
1062
2101
  */
1063
- type AppAccessVerdict = 'granted' | 'denied' | 'unknown';
2102
+ interface MetaItemAccess {
2103
+ readonly verdict: AppAccessVerdict;
2104
+ /**
2105
+ * The refusal's own `message`, as the server wrote it — for a dashboard,
2106
+ * `You do not have permission to open the 'NAME' dashboard.` Only on
2107
+ * `denied`, and absent when the server wrote none.
2108
+ */
2109
+ readonly message?: string;
2110
+ }
1064
2111
  /**
1065
2112
  * The ADR-0112 standard catalog code the by-name meta app route answers with
1066
2113
  * when an app exists and the session lacks its `requiredPermissions`
@@ -1084,9 +2131,32 @@ declare const APP_PERMISSION_DENIED_CODE = "PERMISSION_DENIED";
1084
2131
  * are errors.
1085
2132
  */
1086
2133
  declare function isAppPermissionDeniedError(error: unknown): boolean;
2134
+ /**
2135
+ * The ADR-0112 standard catalog code the metadata routes answer with when the
2136
+ * named item is not there for this caller (`sendError(res, 404,
2137
+ * 'RESOURCE_NOT_FOUND', …)` in `packages/rest/src/rest-server.ts`).
2138
+ *
2139
+ * One code covers every absence on purpose: a nonexistent name, an unpublished
2140
+ * app (ADR-0045 §3 keeps it externally unobservable), and an app gated by an
2141
+ * absent optional service (ADR-0057 D10 — nothing was denied to the CALLER) are
2142
+ * one answer by the 2026-08-12 ruling's design, not three that happen to
2143
+ * collide. Consumers may report the absence; they may not report a reason.
2144
+ */
2145
+ declare const META_ITEM_ABSENT_CODE = "RESOURCE_NOT_FOUND";
2146
+ /**
2147
+ * True when `error` is a metadata route's "not here for you" answer.
2148
+ *
2149
+ * Discriminates on the ADR-0112 `code`, never on the status, for the same
2150
+ * reason {@link isAppPermissionDeniedError} does (objectui#4408): a 404 is a
2151
+ * transport fact many conditions share — a mis-typed route, a proxy, an
2152
+ * appliance in front of the server — while the code is the contract. A 404 that
2153
+ * carries no declared code is NOT an absence: nobody said the item was missing,
2154
+ * so the honest reading is that no answer was obtained.
2155
+ */
2156
+ declare function isMetaItemAbsentError(error: unknown): boolean;
1087
2157
  /**
1088
2158
  * Thrown when the deployment has no analytics capability installed
1089
- * (framework#3891 / #4019).
2159
+ * (framework#3891 / objectstack-ai/objectstack#4019).
1090
2160
  *
1091
2161
  * The framework retired its degraded in-kernel analytics fallback — it dropped
1092
2162
  * the caller's RLS/tenant scope and ignored the contract filter, so it answered
@@ -1194,6 +2264,41 @@ declare class AnalyticsUnauthenticatedError extends Error {
1194
2264
  serverMessage?: string;
1195
2265
  });
1196
2266
  }
2267
+ /**
2268
+ * Thrown when the dataset query was refused because this user may not READ an
2269
+ * object the dataset queries — `403` + ADR-0112 `PERMISSION_DENIED`, which
2270
+ * `@objectstack/service-analytics`' read admission (`readAdmissionDeniedError`)
2271
+ * answers with the same code and status the data API gives the same user for
2272
+ * the same object (objectui#10899).
2273
+ *
2274
+ * The FOURTH branch. It used to fall through to the generic
2275
+ * `Dataset query failed: 403 Forbidden — [Analytics] Access denied: …` string,
2276
+ * which a dashboard tile then rendered verbatim: a raw English exception where
2277
+ * the list view over the same object shows its localized "no access" state.
2278
+ * That generic string names a transport status where a person needs a fact
2279
+ * ("you can't see this data"), and it is the one case a renderer must be able
2280
+ * to tell apart — so it is typed, and it carries `httpStatus` + a `code` the
2281
+ * shared `classifyLoadError` reads as `forbidden` without any string matching.
2282
+ *
2283
+ * `serverMessage` is kept for diagnostics only. The producer deliberately
2284
+ * names the object and nothing else, but it is still platform prose, not text
2285
+ * addressed to the end user.
2286
+ */
2287
+ declare class AnalyticsForbiddenError extends Error {
2288
+ readonly code = "PERMISSION_DENIED";
2289
+ readonly httpStatus = 403;
2290
+ /** The server's own ADR-0112 code — the field this branch was chosen BY. */
2291
+ readonly serverCode?: string;
2292
+ /** The server's own message, verbatim (diagnostics only). */
2293
+ readonly serverMessage?: string;
2294
+ /** The dataset the refused query asked for, when it named one. */
2295
+ readonly datasetName?: string;
2296
+ constructor(opts?: {
2297
+ serverCode?: string;
2298
+ serverMessage?: string;
2299
+ datasetName?: string;
2300
+ });
2301
+ }
1197
2302
  /**
1198
2303
  * Thrown when the server REJECTED the analytics query body (HTTP 400 —
1199
2304
  * `VALIDATION_FAILED` since framework#4010 validates `/analytics/query` at the
@@ -1210,6 +2315,49 @@ declare class AnalyticsQueryRejectedError extends Error {
1210
2315
  readonly serverCode?: string;
1211
2316
  constructor(detail?: string, serverCode?: string);
1212
2317
  }
2318
+ /**
2319
+ * Thrown by `aggregate()` when `client.analytics.query` resolves to anything
2320
+ * other than the post-unwrap `AnalyticsResult` — concretely, when the value it
2321
+ * hands back has no `rows` array (objectui#7028).
2322
+ *
2323
+ * ONE SPELLING, BY RULING. `@objectstack/client` 17.3.0 converged
2324
+ * `analytics.query` on `unwrapResponse` (objectstack#13079, maintainer ruling
2325
+ * 2026-08-31, option A): the method resolves to the payload, and the caller
2326
+ * reads `result.rows`. The same ruling ordered this adapter's tolerant row
2327
+ * ladder tightened in the same wave, so that defensive code is not read as a
2328
+ * contract. The ladder also read a bare array, `data` as an array,
2329
+ * `data.data.rows` and `results`; `rows` is now the only spelling read, and
2330
+ * everything else lands here.
2331
+ *
2332
+ * WHY A THROW AND NOT `[]`. The ladder answered every shape it did not
2333
+ * recognise with an empty array, and an empty array is a RESULT: a KPI renders
2334
+ * a confident zero and a chart reads "no data" over a populated table — the
2335
+ * lie objectui#5954 removed on the failure side. The envelope arriving here
2336
+ * means the SDK in front of this call predates the convergence (it is older
2337
+ * than 17.3.0), or something between it and `POST /analytics/query` wraps the
2338
+ * payload a second time. Either way the adapter is handed a contract it no
2339
+ * longer reads, and saying so is the only answer that names the repair.
2340
+ *
2341
+ * WHY NOT THE CLIENT-SIDE FALLBACK. Analytics answered; this is not a
2342
+ * transport failure. Degrading would put plausible numbers from a different
2343
+ * code path over a contract violation — the misdirection
2344
+ * {@link AnalyticsQueryRejectedError} refuses for the same reason
2345
+ * (framework#3878) — so `aggregate()`'s catch rethrows this before it
2346
+ * classifies anything.
2347
+ */
2348
+ declare class AnalyticsResultShapeError extends Error {
2349
+ readonly code = "ANALYTICS_RESULT_SHAPE_INVALID";
2350
+ /** The object `aggregate()` was called for. */
2351
+ readonly resource: string;
2352
+ /**
2353
+ * True when the value is the envelope the client stopped handing back at
2354
+ * 17.3.0 — `{ success, data: { rows } }`, or its `{ data: { rows } }` core.
2355
+ */
2356
+ readonly envelope: boolean;
2357
+ /** What the value was, so the producer is identifiable from a log. */
2358
+ readonly received: string;
2359
+ constructor(resource: string, result: unknown);
2360
+ }
1213
2361
  /**
1214
2362
  * Classify a FAILED analytics call so the caller knows whether to degrade or
1215
2363
  * to surface the failure.
@@ -1257,11 +2405,18 @@ declare class AnalyticsQueryRejectedError extends Error {
1257
2405
  * `ROUTE_NOT_FOUND` (framework#4019 stops mounting the routes at all).
1258
2406
  * A client-side aggregate over a scoped `find()` answers the chart
1259
2407
  * correctly, and the operator is told once that the semantic layer is off.
1260
- * - **`rejected`** — *THROW*. The server refused OUR body (400
1261
- * `VALIDATION_FAILED`; framework#4010 validates `/analytics/query` at the
1262
- * entry). Degrading would answer our own contract violation with plausible
1263
- * numbers from a different code path and bury it — the misdirection
1264
- * framework#3878 documented.
2408
+ * - **`rejected`** — *THROW*. The server refused OUR body: 400
2409
+ * `VALIDATION_FAILED` (framework#4010 validates `/analytics/query` at the
2410
+ * entry) OR any other coded 400 (objectui#7755 — e.g. `service-analytics`
2411
+ * ships its own 400 `INVALID_FILTER` on a filter shape it refuses). The
2412
+ * status alone is decisive once it is 400: a producer that refuses the
2413
+ * body and ships a code this consumer does not enumerate is refusing it
2414
+ * exactly as hard as one that ships `VALIDATION_FAILED` — the refusal is
2415
+ * in the transport fact, not in which code spells it. Degrading would
2416
+ * answer our own contract violation with plausible numbers from a
2417
+ * different code path (`find()`'s `$filter` accepts array shapes the
2418
+ * analytics body does not) and bury it — the misdirection framework#3878
2419
+ * documented.
1265
2420
  * - **`unauthenticated`** — *THROW*. 401 `UNAUTHENTICATED`: the request was
1266
2421
  * refused before it ran, so it is evidence about the SESSION and none at
1267
2422
  * all about the capability. Degrading is not merely misleading here, it is
@@ -1309,16 +2464,46 @@ declare class ConcurrentUpdateError extends Error {
1309
2464
  readonly httpStatus = 409;
1310
2465
  readonly currentVersion: string | null;
1311
2466
  readonly currentRecord: unknown;
2467
+ /**
2468
+ * The refusal text the PRODUCER marked as addressed to the end user
2469
+ * (`ApiErrorSchema.userMessage`, objectstack `79c46da90`), or `null` when the
2470
+ * refusal carried no marking.
2471
+ *
2472
+ * Declared on the class because this error has no `details` bag: the shared
2473
+ * reader (`declaredUserMessage` in `@object-ui/react`) looks at the
2474
+ * top-level field first, so parking it here is what makes the marking
2475
+ * readable at the surface. The contract is status-agnostic — a 409 carries
2476
+ * it exactly like a 400 or a 403 does.
2477
+ */
2478
+ readonly userMessage: string | null;
1312
2479
  constructor(opts: {
1313
2480
  currentVersion: string | null;
1314
2481
  currentRecord: unknown;
1315
2482
  message?: string;
2483
+ userMessage?: string | null;
1316
2484
  });
1317
2485
  }
1318
2486
  /**
1319
2487
  * Detect "concurrent update" errors raised by the platform. The wire
1320
2488
  * shape is `409` + `code: 'CONCURRENT_UPDATE'`. The client surfaces
1321
2489
  * extra details on `error.details` (full response body).
2490
+ *
2491
+ * Accepts EITHER that wire `code` OR `name === 'ConcurrentUpdateError'`, and
2492
+ * reads `httpStatus` for neither. This paragraph exists because the doc used
2493
+ * to name only the wire shape, which left the `name` limb reading as drift
2494
+ * (objectui#6375). It is not drift: it is the deliberate cross-realm
2495
+ * duck-check — a host that bundles this package twice (or re-throws across a
2496
+ * worker boundary) ends up holding two copies of the class, `instanceof`
2497
+ * fails, and the `name` string is the only discriminator left. That host is out of tree by construction, so an in-repo
2498
+ * consumer census cannot see the case the limb was written for and is not
2499
+ * evidence against it; `@object-ui/plugin-form` and `@object-ui/plugin-detail`
2500
+ * each carry their own copy of the same two-limb check for adapters they must
2501
+ * not depend on.
2502
+ *
2503
+ * Deliberately WIDER than {@link normaliseClientError}'s re-wrap, which keys
2504
+ * on the wire `code` alone: an error carrying only the class name is
2505
+ * recognised here and passed through there. Both accepted sets are pinned in
2506
+ * `occ.test.ts`.
1322
2507
  */
1323
2508
  declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpdateError;
1324
2509
  /**
@@ -1331,6 +2516,12 @@ declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpd
1331
2516
  * - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the
1332
2517
  * server's per-field entries so a form can mark the offending inputs
1333
2518
  * instead of showing one undirected toast.
2519
+ *
2520
+ * Both re-wraps preserve the producer's `userMessage` marking (see
2521
+ * {@link liftUserMessage}). Re-wrapping used to drop it, which made the
2522
+ * marking unreadable at every surface downstream of this boundary — nothing
2523
+ * threw, the typed error was otherwise correct, and the user simply got a
2524
+ * generic string.
1334
2525
  */
1335
2526
  declare function normaliseClientError(error: unknown): unknown;
1336
2527
  /**
@@ -1377,10 +2568,7 @@ declare function formatHttpFailureMessage(message: string, meta?: Record<string,
1377
2568
  * own `ObjectStackClient` gets the same identified failures.
1378
2569
  */
1379
2570
  declare function createQuietHttpLogger(): any;
1380
- /**
1381
- * Connection state for monitoring
1382
- */
1383
- type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
2571
+
1384
2572
  /**
1385
2573
  * Connection state change event
1386
2574
  */
@@ -1408,20 +2596,306 @@ type ConnectionStateListener = (event: ConnectionStateEvent) => void;
1408
2596
  */
1409
2597
  type BatchProgressListener = (event: BatchProgressEvent) => void;
1410
2598
 
2599
+ /**
2600
+ * The `reason` of a write-strip this bundle's spec pin cannot name
2601
+ * (objectui#4934).
2602
+ *
2603
+ * Deliberately NOT a spec spelling: it is namespaced so it can never collide
2604
+ * with an arm `@objectstack/spec` adds later (a collision would merge real
2605
+ * reasons into the skew bucket — the failure this whole card is about, one level
2606
+ * up). `droppedFieldsReason.boundary.test.ts` pins that the installed spec does
2607
+ * not declare it.
2608
+ */
2609
+ declare const UNRECOGNIZED_DROP_REASON = "objectui:unrecognized-drop-reason";
2610
+ /**
2611
+ * The skew arm: one server-reported write-strip whose `reason` is outside the
2612
+ * enum this bundle's `@objectstack/spec` pin declares (objectui#4934).
2613
+ *
2614
+ * A deployed client normally runs BEHIND the server it talks to, so a reason
2615
+ * from the future is the expected skew direction, not a corrupt payload. The
2616
+ * boundary used to assert such an entry into {@link DroppedFieldsEvent} on shape
2617
+ * alone — `reason` was never read — so the interior was typed to trust a union
2618
+ * nothing had checked, and the next consumer to write an exhaustive-looking
2619
+ * table over `DroppedFieldsEvent['reason']` would have been handed a value that
2620
+ * type says is impossible.
2621
+ *
2622
+ * Three properties of this shape are load-bearing:
2623
+ *
2624
+ * - The entry is **kept**. Dropping it would tell the user nothing about fields
2625
+ * the server really did strip — precisely the silence objectui#3484 removed.
2626
+ * - The wire value is **preserved verbatim** in {@link unrecognizedReason},
2627
+ * never coerced or normalised onto a known arm: claiming `readonly` for a
2628
+ * reason we cannot name is a false statement about the user's data, and it is
2629
+ * also unfalsifiable once the original value is gone.
2630
+ * - `reason` carries {@link UNRECOGNIZED_DROP_REASON}, which is not assignable
2631
+ * to `DroppedFieldsEvent['reason']`. That is what makes the skew case visible
2632
+ * to `tsc` at every consumer instead of resting on N per-consumer
2633
+ * disciplines — and it keeps the spec type as the canonical arm rather than
2634
+ * widening the whole surface to `string` (objectui#3160).
2635
+ */
2636
+ interface UnrecognizedDropReasonEvent {
2637
+ object?: string;
2638
+ fields: string[];
2639
+ reason: typeof UNRECOGNIZED_DROP_REASON;
2640
+ /** Whatever the server sent, untouched — including a non-string or nothing at all. */
2641
+ unrecognizedReason: unknown;
2642
+ }
2643
+ /**
2644
+ * One entry of a write-warning: either the spec type (canonical arm) or the
2645
+ * named skew arm above. Narrow with `entry.reason === UNRECOGNIZED_DROP_REASON`.
2646
+ */
2647
+ type DroppedFieldsNotice = DroppedFieldsEvent | UnrecognizedDropReasonEvent;
1411
2648
  /**
1412
2649
  * Emitted after a create/update whose response carried `droppedFields`
1413
- * (framework #3431/#3455). The write SUCCEEDED — this is a warning that some
1414
- * supplied fields never landed, so the UI can tell the user rather than let it
1415
- * pass silently. Subscribe via {@link ObjectStackAdapter.onWriteWarning}.
2650
+ * (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455) or
2651
+ * `warnings` (objectui#12107). The write SUCCEEDED — this is a warning that
2652
+ * some supplied fields never landed, or that an advisory validation rule hit,
2653
+ * so the UI can tell the user rather than let it pass silently. Subscribe via
2654
+ * {@link ObjectStackAdapter.onWriteWarning}.
2655
+ *
2656
+ * `droppedFields` is the two-arm {@link DroppedFieldsNotice} and not
2657
+ * `DroppedFieldsEvent[]`: the wire is parsed here, and an entry whose `reason`
2658
+ * this bundle's spec pin cannot name arrives on the explicit skew arm rather
2659
+ * than being asserted into the union (objectui#4934). It stays REQUIRED and is
2660
+ * an empty array when the event exists only for `warnings`.
2661
+ *
2662
+ * `warnings` is the write answer's own key, typed by the spec's
2663
+ * `ValidateDataIssueSchema` (the element of `CreateDataResponseSchema` /
2664
+ * `UpdateDataResponseSchema`'s `warnings`) and parsed through it here. It is
2665
+ * present only when at least one entry parsed: an answer without the key —
2666
+ * including one from a runtime that predates it — leaves the event exactly as
2667
+ * it was.
1416
2668
  */
1417
2669
  interface WriteWarningEvent {
1418
2670
  operation: 'create' | 'update';
1419
2671
  resource: string;
1420
2672
  id?: string | number;
1421
- droppedFields: DroppedFieldsEvent[];
2673
+ droppedFields: DroppedFieldsNotice[];
2674
+ warnings?: ValidateDataIssue[];
1422
2675
  }
1423
2676
  /** Event listener type for write-warning (dropped-fields) events. */
1424
2677
  type WriteWarningListener = (event: WriteWarningEvent) => void;
2678
+ /**
2679
+ * What a FAILED `listImportMappings` read actually was (objectui#7741).
2680
+ *
2681
+ * - `not-served` — this deployment does not serve the `mapping` kind, or has
2682
+ * no `/meta` door at all. QUIET: a real, supported deployment shape, and the
2683
+ * empty list plus a hidden selector is the correct rendering of it.
2684
+ * - `refused` — the server answered and declined THIS caller: anonymous,
2685
+ * lapsed token, missing permission, method withheld. LOUD, and the remedy
2686
+ * names a person: sign in again, or ask for the grant.
2687
+ * - `unreadable` — the read could not be completed for any other reason: a
2688
+ * 5xx, a network failure, a code this consumer cannot name. LOUD, and the
2689
+ * remedy is to retry or report.
2690
+ *
2691
+ * The last two are the same verdict for the user — *we could not find out* —
2692
+ * and they are separated because the sentence that helps differs. What they
2693
+ * share is what matters: neither is evidence that no mapping is registered.
2694
+ *
2695
+ * ⚠️ The three NAMES are shared by every classifier feeding
2696
+ * {@link MetadataReadWarningEvent}; the POPULATION each name covers is not, and
2697
+ * is decided per read door. `not-served` in particular is a strictly smaller
2698
+ * set on `view` than on `mapping` — see {@link classifyViewsFailure}.
2699
+ */
2700
+ type MetadataReadFailureKind = 'not-served' | 'refused' | 'unreadable';
2701
+ /**
2702
+ * The published spelling of {@link MetadataReadFailureKind}, kept as the name
2703
+ * objectui#7741 shipped (`@object-ui/data-objectstack@17.2.0`).
2704
+ *
2705
+ * An ALIAS, not a second declaration: identical members, so every existing
2706
+ * consumer's assignability is unchanged in both directions. It was renamed
2707
+ * because the channel gained a second emitter (objectui#8151) and a type called
2708
+ * `ImportMappingsFailureKind` describing a `view` read would be a name that
2709
+ * lies; the old name stays because retiring a published one is not this card's
2710
+ * business.
2711
+ */
2712
+ type ImportMappingsFailureKind = MetadataReadFailureKind;
2713
+ /**
2714
+ * Classify a FAILED `meta.getItems('mapping')` call so the caller knows whether
2715
+ * to stay quiet or to say something (objectui#7741).
2716
+ *
2717
+ * ## Why this function exists
2718
+ *
2719
+ * `listImportMappings` degrades every failure to `[]`, and the wizard hides its
2720
+ * saved-mapping selector on an empty list. So "the server served zero mappings"
2721
+ * and "the server never answered" rendered identically — the same UI, on every
2722
+ * deployment, with a `console.warn` as the only discriminator. That silence did
2723
+ * not merely hide a fault: it produced a confident WRONG diagnosis in a careful
2724
+ * reporter (objectstack#14026 was filed, routed and worked by two seats against
2725
+ * a repo whose wizard had been correct since `@object-ui/data-objectstack`
2726
+ * 17.1.0), and the misdiagnosis travelled further than the fault would have.
2727
+ *
2728
+ * This is the discrimination framework objectstack#13906 decision 1 option A already
2729
+ * adopted at the tenancy-posture seam — *a thing that could not be READ is not
2730
+ * a thing that is ABSENT* — applied here. It is not a new principle.
2731
+ *
2732
+ * ## It reads the ERROR, never the result
2733
+ *
2734
+ * The one thing this must never do is decide from "is the result an empty
2735
+ * array". An empty array is what BOTH conditions produce, so a test on it can
2736
+ * never fail and can never separate *served zero* from *did not serve* — a
2737
+ * reading that cannot fail is indistinguishable from a reading that passed.
2738
+ * Every branch below reads `err` itself: the ADR-0112 `code` first (the
2739
+ * contract), the status only where no code was declared (a transport fact many
2740
+ * conditions share). Same rule, same order, and for the same reasons as
2741
+ * {@link classifyAnalyticsFailure} (objectui#5663 / objectui#5721).
2742
+ *
2743
+ * `@objectstack/client`'s fetch wrapper hands both envelope families here
2744
+ * already flattened — `errorBody?.code ?? errorBody?.error?.code` onto
2745
+ * `error.code`, plus `error.httpStatus = res.status` — so no envelope reading
2746
+ * belongs in this function. Comparisons go through `errorCodeIs`/
2747
+ * `errorCodeIsAnyOf` so the pre- and post-ADR-0112 spellings both match.
2748
+ */
2749
+ declare function classifyImportMappingsFailure(error: unknown): {
2750
+ kind: ImportMappingsFailureKind;
2751
+ code?: string;
2752
+ status?: number;
2753
+ message?: string;
2754
+ };
2755
+ /**
2756
+ * Classify a FAILED `view` metadata list read so {@link ObjectStackAdapter.listViews}
2757
+ * knows whether to stay quiet or to say something (objectui#8151).
2758
+ *
2759
+ * ## Why this is a SEPARATE reading and not a second caller of
2760
+ * {@link classifyImportMappingsFailure}
2761
+ *
2762
+ * The two methods share a defect and a remedy, not a population. The quiet arm
2763
+ * `listImportMappings` is built around — *an older server that does not serve
2764
+ * this kind* — has no members on `view`, and carrying it over would put a fresh
2765
+ * swallow into the method this card exists to un-swallow. Measured, on the
2766
+ * framework tree, three ways:
2767
+ *
2768
+ * 1. **`view` is in the platform's static spelling contract**, so the metadata
2769
+ * LIST door's unknown-kind refusal is unreachable for it.
2770
+ * `RestServer.refuseUnknownMetaListType` (framework#9488,
2771
+ * `packages/rest/src/rest-server.ts`) returns without writing a refusal
2772
+ * whenever `unrecognisedMetaTypeRefusal(urlType)` is `null`, and that
2773
+ * predicate answers `null` for every spelling in the contract.
2774
+ * `packages/spec/src/meta-spelling/meta-url-data.generated.ts` carries both
2775
+ * `"views": "view"` and the canonical singular `view`. So a `400`
2776
+ * `INVALID_REQUEST` on `GET /meta/view` is NEVER that door saying "this
2777
+ * deployment carries no such kind" — it is some other refusal of the
2778
+ * request, and reading it as kind-absence would silence a real one.
2779
+ * 2. **There is no "before" for `view` to be older than.** `mapping`'s quiet
2780
+ * arm exists because `mapping` was PROMOTED into the declared set
2781
+ * (framework#2611), so servers predating the promotion are a real, shipped
2782
+ * population. `view` is the kind the metadata surface is built around — the
2783
+ * compound-arity door `/meta/<object>/views/<name>`, the ADR-0017
2784
+ * `ViewItem` discriminant this very method filters on.
2785
+ * 3. **A deployment that cannot serve `view` cannot render the caller.**
2786
+ * `listViews` is reached only from `ObjectView`, whose `objectDef` came from
2787
+ * `MetadataProvider`, which reads the SAME door and lists `view` among its
2788
+ * `EAGER_TYPES` at mount (`packages/app-shell/src/providers/MetadataProvider.tsx`).
2789
+ * A server that does not answer `/meta/view` has already failed that read
2790
+ * before any object page exists.
2791
+ *
2792
+ * ## So the quiet set here is strictly SMALLER — it is the DOORLESS one only
2793
+ *
2794
+ * What survives as quiet is not about `view` at all: it is *this host mounted no
2795
+ * metadata door*. That stays silent for the reason objectui#7741 gave — a real,
2796
+ * supported deployment shape must not be turned into a visible fault — and it
2797
+ * costs nothing to keep silent here, because such a host has already failed
2798
+ * `MetadataProvider`'s eager `app`/`object`/`view` reads; a per-object toast
2799
+ * would be a fourth voice on one deployment fact, not a new one.
2800
+ *
2801
+ * Everything else is LOUD. In particular a `refused` (401/403/405,
2802
+ * `UNAUTHENTICATED`, `PERMISSION_DENIED`) is the condition this card was filed
2803
+ * for: a token that lapsed mid-session renders the object's view switcher as
2804
+ * though the user's own saved views did not exist.
2805
+ *
2806
+ * ## It reads the ERROR, never the result — and from BOTH of this method's doors
2807
+ *
2808
+ * `listViews` is fed by two transports, unlike its sibling's one, and they
2809
+ * decorate differently:
2810
+ *
2811
+ * published `client.meta.getItems('view')` -> `@objectstack/client`'s fetch
2812
+ * wrapper: `error.code` (flattened from either envelope family)
2813
+ * plus `error.httpStatus`.
2814
+ * drafts `MetadataClient.withPreviewDrafts(true).list('view')` (ADR-0037,
2815
+ * `?preview=draft`) -> this package's own `parseError`:
2816
+ * `err.code` plus **`err.status`**, with no `httpStatus` at all.
2817
+ *
2818
+ * The status ladder below therefore reads `httpStatus`, then `status`, then
2819
+ * `statusCode`, and is the reason a preview-mode failure classifies the same as
2820
+ * a published-mode one instead of falling through to the code-less residual.
2821
+ * What it must never read is "is the result an empty array": that is what BOTH
2822
+ * a served-zero and a refusal produce, so a test on it can never fail for the
2823
+ * condition it is supposed to be about. This is framework objectstack#13906 decision 1
2824
+ * option A — *a thing that could not be READ is not a thing that is ABSENT*.
2825
+ */
2826
+ declare function classifyViewsFailure(error: unknown): {
2827
+ kind: MetadataReadFailureKind;
2828
+ code?: string;
2829
+ status?: number;
2830
+ message?: string;
2831
+ };
2832
+ /**
2833
+ * Emitted when a metadata READ was answered by the server with a failure that
2834
+ * is NOT the supported "this deployment does not serve that kind" shape, and
2835
+ * the adapter degraded it to an empty result anyway (objectui#7741).
2836
+ *
2837
+ * The degrade is deliberate and unchanged — the caller still receives `[]`, and
2838
+ * `listImportMappings`' published return type has not moved. This event is the
2839
+ * channel ALONGSIDE it: the fact that the empty result is an artefact of a
2840
+ * failed read, carried in a form a consumer can act on, instead of a
2841
+ * `console.warn` nothing in the UI points at.
2842
+ *
2843
+ * Subscribe via {@link ObjectStackAdapter.onMetadataReadWarning}. A sibling of
2844
+ * {@link WriteWarningEvent} and `MetadataSaveAdvisoryEvent` in SHAPE — one
2845
+ * long-lived instance, `subscribe → unsubscribe`, `AdapterProvider` wires it
2846
+ * once — and deliberately not a payload pushed down either of those: both of
2847
+ * them describe a write that SUCCEEDED, so carrying a failed read on one would
2848
+ * make the event lie about what happened.
2849
+ *
2850
+ * `operation` and `kind` are CLOSED unions on purpose. Every emitter is named
2851
+ * in them, so widening is an additive, reviewed change rather than something a
2852
+ * consumer's exhaustive switch discovers at runtime. (The same trade
2853
+ * `WriteWarningEvent`'s required `operation` documents, taken deliberately
2854
+ * here.)
2855
+ *
2856
+ * ## The widening this design was built for happened (objectui#8151)
2857
+ *
2858
+ * objectui#7741 shipped both fields as SINGLE-member unions and said in as many
2859
+ * words that a second emitter should arrive as a reviewed widening. It has:
2860
+ * `listViews` is the second, for the same defect one method over, and the
2861
+ * mechanism worked as designed — the consumer that renders these events
2862
+ * (`app-shell`'s `metadataReadWarningToast`) had a `switch` that named the one
2863
+ * operation, so adding the member turned "a views failure is toasted as an
2864
+ * import-mapping failure" into a COMPILE error instead of a runtime lie.
2865
+ *
2866
+ * ⚠️ The pair is the emitter's invariant, not the type's: `operation` and
2867
+ * `kind` are two independent unions, so nothing in the type stops
2868
+ * `{ operation: 'listViews', kind: 'mapping' }` being constructed. Each emitter
2869
+ * writes its own pair, one line apart, and every consumer branches on
2870
+ * `operation` — the adapter METHOD, which is what names the list the user is
2871
+ * actually looking at. Tightening this to a discriminated union would mean
2872
+ * turning a published `interface` into a type alias; that is a reviewable
2873
+ * change on its own terms and is deliberately not smuggled in here.
2874
+ */
2875
+ interface MetadataReadWarningEvent {
2876
+ /** The adapter method whose read failed. */
2877
+ operation: 'listImportMappings' | 'listViews';
2878
+ /** The metadata kind it asked for. */
2879
+ kind: 'mapping' | 'view';
2880
+ /** The object the read was scoped to. */
2881
+ objectName: string;
2882
+ /**
2883
+ * Which loud verdict this is — see {@link MetadataReadFailureKind}. Never
2884
+ * `'not-served'`: that arm is the supported deployment shape and is not
2885
+ * emitted at all, so a subscriber never has to filter it out. ⚠️ WHICH
2886
+ * failures fall in that un-emitted arm differs per `operation` — it is a
2887
+ * strictly smaller set on `listViews` (see {@link classifyViewsFailure}).
2888
+ */
2889
+ reason: Exclude<MetadataReadFailureKind, 'not-served'>;
2890
+ /** The server's own ADR-0112 code, when it declared one. */
2891
+ code?: string;
2892
+ /** The HTTP status, when the failure carried one. */
2893
+ status?: number;
2894
+ /** The server's own message, when it sent one. Rendered verbatim, not translated. */
2895
+ message?: string;
2896
+ }
2897
+ /** Event listener type for metadata read-warning events. */
2898
+ type MetadataReadWarningListener = (event: MetadataReadWarningEvent) => void;
1425
2899
 
1426
2900
  /**
1427
2901
  * Resolve which object a `type='view'` metadata item belongs to.
@@ -1441,8 +2915,17 @@ type WriteWarningListener = (event: WriteWarningEvent) => void;
1441
2915
  *
1442
2916
  * `object` is the identity field the write path stamps (and that the
1443
2917
  * framework's overlay heals onto identity-less personalization rows —
1444
- * objectstack#2555); `data.object` is the config's data-provider target and
1445
- * `objectName` the legacy artifact spelling.
2918
+ * objectstack#2555); `data.object` is the config's data-provider target.
2919
+ *
2920
+ * ⛔ No `objectName` leg (objectui#11013, ruling 甲 on objectstack#20051:
2921
+ * objectui reads a stored view row by the spec's declared spellings). The
2922
+ * spec declares `object` on every `view` member — required on the ViewItem
2923
+ * record and on both flattened overlays — and declares `objectName` on none,
2924
+ * so the metadata write door refuses a row that carries only `objectName`
2925
+ * (`viewItemObjectName.declaredSpelling-11013.test.ts` pins that door beside
2926
+ * this reader). Every write path in this package stamps `object` as well, so a
2927
+ * row the console wrote carries it; a row that carries ONLY the undeclared
2928
+ * spelling no longer matches any object here.
1446
2929
  *
1447
2930
  * **Exported** since objectui#4373, for the same one-spelling reason: a writer
1448
2931
  * outside this module that holds a view BODY but not its object name (app-shell's
@@ -1456,13 +2939,18 @@ declare function viewItemObjectName(item: any): string | undefined;
1456
2939
  * The keys a personalization overlay row legitimately OWNS (objectui#5233).
1457
2940
  *
1458
2941
  * One per `persistViewPatch` call site in app-shell's `ObjectView` — the ONLY
1459
- * production writer of these rows — read off the tree rather than recalled:
2942
+ * production writer of these rows, read off the tree when it was written:
1460
2943
  * `rowHeight` (the density toggle, spec-canonical since #2890), `sort`,
1461
- * `hiddenFields`, `columnState` and `inlineEdit`. Nothing else in such a row
1462
- * is an opinion the user expressed; anything else it carries is a COPY of the
1463
- * source view as it stood at write time, because `persistViewPatch` USED TO
1464
- * send `{ ...baseViewDef, ...patch }` and this adapter persists what it is
1465
- * given.
2944
+ * `hiddenFields` and `columnState`. Plus `inlineEdit`, which no console call
2945
+ * site wrote after objectui#5144 (triage's ruling E: the inline-edit toggle
2946
+ * is session-only). That writer is retired (objectui#12504): no toolbar
2947
+ * control writes an overlay, and every key here is kept so an overlay an
2948
+ * earlier console wrote is still read; the rulings keep that data rather than
2949
+ * migrating it. Nothing else in
2950
+ * such a row is an opinion the user expressed; anything else it carries is a
2951
+ * COPY of the source view as it stood at write time, because
2952
+ * `persistViewPatch` USED TO send `{ ...baseViewDef, ...patch }` and this
2953
+ * adapter persists what it is given.
1466
2954
  *
1467
2955
  * That copy was the defect the maintainer ruled on (objectstack#7494, comment
1468
2956
  * 5261754173): an overlay written by a mere column drag froze the view's
@@ -1472,20 +2960,19 @@ declare function viewItemObjectName(item: any): string | undefined;
1472
2960
  * view's filter and every user who once resized a column kept the old one,
1473
2961
  * with nothing anywhere reporting it.
1474
2962
  *
1475
- * Both halves of that ruling have now landed, and this adapter's behaviour is
1476
- * unchanged by either — it still persists what it is given:
2963
+ * Both halves of that ruling landed, and this adapter's behaviour was
2964
+ * unchanged by either — it persisted what it was given:
1477
2965
  *
1478
2966
  * - **read** (PR #5272, {@link narrowPersonalizationOverlay}): the consumer
1479
2967
  * that MERGES an overlay over a source view contributes only these keys, so
1480
- * every already-stored fat row stops shadowing its source.
1481
- * - **write** (objectui#5233, `buildPersistedViewBody` in app-shell's
1482
- * `ObjectView`, unblocked by `columnState`'s admission to the view-metadata
1483
- * surface as a runtime-only overlay key — objectstack#9933, released in
1484
- * `@objectstack/spec` 17.1.0): a *system view's* overlay is now written as
1485
- * the patch alone, so no new row freezes anything, and because the write is
1486
- * a whole-document PUT the next toggle also strips an old fat row. A *saved
1487
- * view's* own row is deliberately still written whole — for it the body IS
1488
- * the view, not a copy of one.
2968
+ * every already-stored fat row that carries the marker stops shadowing its
2969
+ * source. A fat row written before the marker is no longer narrowed
2970
+ * (objectui#10210, ruling B — see {@link isPersonalizationOverlayRow}).
2971
+ * - **write** (objectui#5233, unblocked by `columnState`'s admission to the
2972
+ * view-metadata surface as a runtime-only overlay key — objectstack
2973
+ * `d5552ca13`, released in `@objectstack/spec` 17.1.0): a *system view's*
2974
+ * overlay was written as the patch alone from then on, and the whole write
2975
+ * is retired since objectui#12504.
1489
2976
  *
1490
2977
  * A fat row is therefore a legacy shape, not a shape this product still
1491
2978
  * produces; the list below is still what a reader is allowed to trust from one.
@@ -1493,9 +2980,10 @@ declare function viewItemObjectName(item: any): string | undefined;
1493
2980
  * ⛔ Do not grow this list to make some other key "stick" through an overlay.
1494
2981
  * A key that belongs to the view belongs in the view; the overlay is a patch,
1495
2982
  * and a patch that carries the whole document is what this list exists to
1496
- * stop. Adding a sixth entry is only correct alongside a sixth
1497
- * `persistViewPatch` call site — {@link narrowPersonalizationOverlay} is what
1498
- * a reader checks that against.
2983
+ * stop. No writer of these rows is left (objectui#12504), so there is no new
2984
+ * key for an entry to answer; each entry is kept for reading the rows already
2985
+ * at rest — `inlineEdit` for rows written before objectui#5144, the other four
2986
+ * for rows written before objectui#12474 / objectui#12418.
1499
2987
  */
1500
2988
  declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFields", "columnState", "inlineEdit"];
1501
2989
  /**
@@ -1506,8 +2994,8 @@ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFie
1506
2994
  * {@link VIEW_OVERLAY_OWNED_KEYS}). Of the three dispositions the issue names
1507
2995
  * for them — strip on next write, migrate, tolerate on read — this is the
1508
2996
  * third, chosen deliberately and stated here rather than left implicit,
1509
- * because it is the only one that is already true for every existing row the
1510
- * moment it ships: strip-on-next-write heals a row only when its user happens
2997
+ * because it is the only one that is already true for every existing marked
2998
+ * row the moment it ships: strip-on-next-write heals a row only when its user happens
1511
2999
  * to touch that view again (and leaves the frozen filter live until then),
1512
3000
  * and a migration needs a runner this product does not have for `sys_metadata`
1513
3001
  * rows an operator may not even know exist. What the issue forbids is SILENT
@@ -1528,7 +3016,9 @@ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFie
1528
3016
  * IS the view, and every key on it is an opinion its author expressed.
1529
3017
  * Classification is {@link isPersonalizationOverlayRow}, the same predicate
1530
3018
  * {@link ObjectStackAdapter.listViews} excludes rows by, so a row cannot be a
1531
- * saved view for one reader and an overlay for the other.
3019
+ * saved view for one reader and an overlay for the other. It reads the marker
3020
+ * only (objectui#10210, ruling B): an unmarked row is returned by reference
3021
+ * whatever its shape, including a fat row written before the marker existed.
1532
3022
  */
1533
3023
  declare function narrowPersonalizationOverlay<T>(row: T): T;
1534
3024
 
@@ -1579,12 +3069,14 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1579
3069
  private analyticsCapabilityWarned;
1580
3070
  private fetchImpl;
1581
3071
  private inflightFinds;
3072
+ private inflightFindOnes;
1582
3073
  private missingResources;
1583
3074
  private batchUnsupported;
1584
3075
  private atomicBatchCapability;
1585
3076
  private mutationListeners;
1586
3077
  private writeWarningListeners;
1587
3078
  private saveAdvisoryListeners;
3079
+ private metadataReadWarningListeners;
1588
3080
  constructor(config: {
1589
3081
  baseUrl: string;
1590
3082
  token?: string;
@@ -1600,8 +3092,18 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1600
3092
  /**
1601
3093
  * Ensure the client is connected to the server.
1602
3094
  * Call this before making requests or it will auto-connect on first request.
3095
+ *
3096
+ * With `autoReconnect`, a failed discovery read is retried up to
3097
+ * `maxReconnectAttempts` times with exponential backoff. The promise resolves
3098
+ * once discovery answers and rejects with the last `ConnectionError` once the
3099
+ * retries are spent (objectui#12318).
1603
3100
  */
1604
3101
  connect(): Promise<void>;
3102
+ /**
3103
+ * One discovery attempt, and the retry that follows it when it fails with
3104
+ * budget left. Runs inside the promise {@link connect} hands every caller.
3105
+ */
3106
+ private attemptConnect;
1605
3107
  /**
1606
3108
  * Attempt to reconnect to the server with exponential backoff
1607
3109
  */
@@ -1626,6 +3128,34 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1626
3128
  * Set connection state and notify listeners
1627
3129
  */
1628
3130
  private setConnectionState;
3131
+ /**
3132
+ * Run one request through the adapter's fetch and let its outcome move the
3133
+ * connection state (objectui#12295). Before the first successful
3134
+ * {@link connect} this records nothing: `connect()` owns the state until
3135
+ * then, and its own discovery read passes through here too.
3136
+ *
3137
+ * - **Answered** — the fetch resolved with a Response, whatever its status.
3138
+ * A 4xx or 5xx is an answer: the server is reachable, so it counts the
3139
+ * same as a 200 and ends any outage.
3140
+ * - **Not answered** — the fetch rejected: no HTTP answer at all (refused,
3141
+ * reset, DNS, an offline browser). A rejection the caller asked for by
3142
+ * aborting is not evidence about the server and records nothing.
3143
+ *
3144
+ * The rejection or the Response reaches the caller unchanged.
3145
+ */
3146
+ private observeServerAnswer;
3147
+ /** An answered request after the first connect: the server is reachable. */
3148
+ private noteServerAnswered;
3149
+ /**
3150
+ * An unanswered request after the first connect. It spends the adapter's own
3151
+ * retry window — the `autoReconnect` / `maxReconnectAttempts` budget that
3152
+ * {@link connect} grants a failed discovery — counted in the requests the
3153
+ * host already makes rather than in timed probes: while the budget lasts the
3154
+ * state is `reconnecting`, and the first unanswered request past it moves the
3155
+ * state to `error`. No timer and no request of the adapter's own is involved;
3156
+ * the next answered request, whenever it comes, restores `connected`.
3157
+ */
3158
+ private noteServerUnanswered;
1629
3159
  /**
1630
3160
  * Emit batch progress event to listeners
1631
3161
  */
@@ -1635,6 +3165,16 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1635
3165
  * Converts OData-style params to ObjectStack query options.
1636
3166
  */
1637
3167
  find(resource: string, params?: QueryParams): Promise<QueryResult<T>>;
3168
+ /**
3169
+ * Share one in-flight read among concurrent callers asking the same
3170
+ * question: the first call for `key` issues `read()`, every call that
3171
+ * arrives while it is still pending gets that same promise, and the entry is
3172
+ * dropped the moment it settles — so nothing is cached past the round trip,
3173
+ * a rejection reaches every caller that shared it and is not remembered, and
3174
+ * the next call after settle issues a fresh read. Used by `find` and
3175
+ * `findOne` (objectui#11699).
3176
+ */
3177
+ private shareInFlight;
1638
3178
  /**
1639
3179
  * Full-text search across every searchable object in a single round-trip.
1640
3180
  *
@@ -1644,7 +3184,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1644
3184
  * — a per-object metadata-driven search (ADR-0061) — this consults the search
1645
3185
  * index and ranks hits across objects, so it surfaces records the per-object
1646
3186
  * fanout misses. Global affordances (⌘K command palette, search page) prefer
1647
- * this path (framework #3371).
3187
+ * this path (objectstack-ai/objectstack#3371).
1648
3188
  *
1649
3189
  * Returns `{ query, hits }`. A backend without the search plugin installed
1650
3190
  * answers `404`; we treat that as "no global search here" and return an empty
@@ -1657,8 +3197,13 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1657
3197
  }): Promise<GlobalSearchResult>;
1658
3198
  /**
1659
3199
  * Find a single record by ID.
3200
+ *
3201
+ * Concurrent calls for the same resource, id and params share one request,
3202
+ * exactly as `find` does (objectui#11699); see {@link shareInFlight}.
1660
3203
  */
1661
3204
  findOne(resource: string, id: string | number, params?: QueryParams): Promise<T | null>;
3205
+ /** The one round trip behind {@link findOne}, unshared. */
3206
+ private readOne;
1662
3207
  /**
1663
3208
  * Create a new record.
1664
3209
  */
@@ -1680,14 +3225,24 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1680
3225
  */
1681
3226
  private emitWriteWarning;
1682
3227
  /**
1683
- * Read `droppedFields` off a create/update response (framework #3431/#3455)
3228
+ * Read `droppedFields` off a create/update response (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455)
1684
3229
  * and, when present, notify write-warning subscribers. Tolerant of a client
1685
3230
  * whose response type predates `droppedFields`: the field is read structurally
1686
3231
  * and validated, so an older client (or a backend that never drops) is a no-op.
3232
+ *
3233
+ * SHAPE decides whether an entry is an event at all (it must name at least one
3234
+ * field); `object`, `fields` and `reason` are then PARSED — an unrecognized
3235
+ * reason routed to the skew arm, a non-string field element refused, a missing
3236
+ * `object` healed from `resource` — never asserted into the union, and the
3237
+ * entry itself never dropped for them (objectui#4934, `f75810e7c`).
3238
+ *
3239
+ * The same answer's `warnings` (objectui#12107) rides this one event: it is
3240
+ * emitted when either list is non-empty, so a write whose only news is an
3241
+ * advisory rule hit still reaches the subscriber, with `droppedFields: []`.
1687
3242
  */
1688
3243
  private notifyDroppedFields;
1689
3244
  /**
1690
- * Same, for the cross-object transactional batch (framework #3794). Its
3245
+ * Same, for the cross-object transactional batch (objectstack-ai/objectstack#3794). Its
1691
3246
  * response hangs the events off a top-level `droppedFields` list, each tagged
1692
3247
  * with the `index` of the operation it came from — `results` entries are bare
1693
3248
  * record echoes with nowhere to hang a per-row list.
@@ -1701,7 +3256,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1701
3256
  private notifyBatchDroppedFields;
1702
3257
  /**
1703
3258
  * Subscribe to write-warning events (a create/update dropped caller-supplied
1704
- * fields — #3431/#3455). Returns an unsubscribe function. The app shell uses
3259
+ * fields — objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455). Returns an unsubscribe function. The app shell uses
1705
3260
  * this to toast the user; the write itself already succeeded.
1706
3261
  */
1707
3262
  onWriteWarning(callback: WriteWarningListener): () => void;
@@ -1710,7 +3265,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1710
3265
  * advisory findings on a save that SUCCEEDED (#4237; backend
1711
3266
  * objectstack#7435). Returns an unsubscribe function.
1712
3267
  *
1713
- * Deliberately the same seam as {@link onWriteWarning} (#3431/#3455), which
3268
+ * Deliberately the same seam as {@link onWriteWarning} (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455), which
1714
3269
  * is what {@link MetadataSaveAdvisoryEvent}'s own declaration already said it
1715
3270
  * was modelled on. It is a SIBLING of that channel rather than a second
1716
3271
  * payload pushed down it: `WriteWarningEvent` is a closed shape whose
@@ -1734,6 +3289,32 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1734
3289
  * nor starve the others.
1735
3290
  */
1736
3291
  private emitSaveAdvisory;
3292
+ /**
3293
+ * Subscribe to metadata read-warning events — a metadata READ that could not
3294
+ * be answered and was degraded to an empty result anyway (objectui#7741).
3295
+ * Returns an unsubscribe function. The app shell uses this to toast the user;
3296
+ * the caller has already been handed the empty result.
3297
+ *
3298
+ * Deliberately the same seam as {@link onWriteWarning} and
3299
+ * {@link onSaveAdvisory}, and a SIBLING of them rather than a payload pushed
3300
+ * down either: both of those announce a write that SUCCEEDED, and an event
3301
+ * saying "this read did not happen" carried on one of them would make it lie
3302
+ * about what occurred. What is reused is the seam's SHAPE — one long-lived
3303
+ * instance with a `subscribe -> unsubscribe` registration that
3304
+ * `AdapterProvider` wires once — not its event type.
3305
+ *
3306
+ * ⛔ Nothing here changes what a caller RECEIVES. `listImportMappings` still
3307
+ * answers `[]` on every failure, including the loud ones; this channel is
3308
+ * added ALONGSIDE that return, which is why it is not a change to a surface
3309
+ * published since `@object-ui/data-objectstack@17.1.0`.
3310
+ */
3311
+ onMetadataReadWarning(callback: MetadataReadWarningListener): () => void;
3312
+ /**
3313
+ * Notify all metadata read-warning subscribers. Isolated exactly like
3314
+ * {@link emitWriteWarning}: a throwing listener must neither break the caller
3315
+ * nor starve the others.
3316
+ */
3317
+ private emitMetadataReadWarning;
1737
3318
  /**
1738
3319
  * Install the ONE emitter for the metadata save door (#4237).
1739
3320
  *
@@ -1741,8 +3322,8 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1741
3322
  *
1742
3323
  * `ObjectStackClient.meta.saveItem` is the second client class that writes
1743
3324
  * through `PUT /api/v1/meta/:type/:name`, and every one of its callers reaches
1744
- * it through an adapter this class constructed — the four inside this file
1745
- * (`updateViewConfig`, the two view paths, `updateDashboard`) via
3325
+ * it through an adapter this class constructed — the three inside this file
3326
+ * (the two view paths, `updateDashboard`) via
1746
3327
  * `this.client`, and every caller outside it via {@link getClient}, which
1747
3328
  * hands back this same instance: `MetadataService` (app-shell, five saves),
1748
3329
  * `useNavigationSync`, and plugin-designer's Create/EditAppPage. Wrapping the
@@ -1771,16 +3352,60 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1771
3352
  *
1772
3353
  * Drafts are NEVER gated: the framework returns at its D1 early-return
1773
3354
  * (`if (args.state !== 'active') return null`) before running a rule, so a
1774
- * draft save produces no findings to withhold. This client class has no draft
1775
- * door at all to worry about — the SDK's `saveItem(type, name, item)` takes no
1776
- * mode and always writes the active door, which is exactly why the gate DOES
1777
- * run for its callers. `mode` on the emitted event is therefore derived from
1778
- * the response's own `state` rather than from a request-side flag that does
1779
- * not exist here: `'draft'` when the server says the row landed as a draft,
1780
- * `'publish'` otherwise. That keeps the event truthful about which door it
1781
- * came through instead of hard-coding one.
3355
+ * draft save produces no findings to withhold. `mode` on the emitted event is
3356
+ * derived from the response's own `state`, not from the caller's options:
3357
+ * `'draft'` when the server says the row landed as a draft, `'publish'`
3358
+ * otherwise. The server's answer is the one that says which door the write
3359
+ * took, so the event cannot claim a door the request only asked for.
3360
+ *
3361
+ * ## Every argument is forwarded (objectui#12300)
3362
+ *
3363
+ * The SDK's `saveItem(type, name, item, options?)` carries the write's scope
3364
+ * in its fourth argument, `SaveMetaItemOptions`: `packageId` (`?package=`),
3365
+ * `force`, `mode` and `ifMatch`. This wrapper used to name three parameters
3366
+ * and call the original with three, so that bag was dropped before the SDK
3367
+ * saw it. Every caller reaching the SDK through {@link getClient} got a bare
3368
+ * `PUT` whatever it passed, while the declared signature still accepted the
3369
+ * bag, so nothing at the call site could tell. The wrapper now takes and
3370
+ * passes the original's own parameter list, so it forwards whatever the SDK
3371
+ * declares, today's bag and any later member, without naming one.
1782
3372
  */
1783
3373
  private installSaveAdvisoryInterceptor;
3374
+ /**
3375
+ * Route this adapter's client's published view-list read through the one
3376
+ * {@link VIEW_LIST_KEY} (objectui#13044).
3377
+ *
3378
+ * The app-shell `MetadataProvider` reads `client.meta.getItems('view')`
3379
+ * through {@link getClient}; {@link listViews} and {@link listViewOverrides}
3380
+ * read the same `GET /api/v1/meta/view`. Each used to put its own request on
3381
+ * the wire, so one list-page open asked the server three times for one URL.
3382
+ *
3383
+ * A caller of the client keeps its contract, "ask the server": it is never
3384
+ * answered from a settled entry. It joins a request already in flight for
3385
+ * the same URL instead of sending a second one, and its answer is stored
3386
+ * under {@link VIEW_LIST_KEY} (see {@link MetadataCache.share}), which is
3387
+ * what {@link readViewList} answers this adapter's own readers from
3388
+ * (objectui#13057). Only the bare call is routed: any argument after the
3389
+ * type changes the URL, so such a call goes straight to the SDK.
3390
+ *
3391
+ * Every caller gets its own copy of the answer. Before this, every reader
3392
+ * got its own parsed response, so a reader that changed what it was handed
3393
+ * changed nothing anyone else read; a shared answer must not change that.
3394
+ *
3395
+ * Same wrap shape and the same bound as `installSaveAdvisoryInterceptor`:
3396
+ * `meta` is an own property of a client this adapter built and never shares.
3397
+ */
3398
+ private installViewListShare;
3399
+ /**
3400
+ * The published view list for this adapter's own readers
3401
+ * ({@link listViews}, {@link listViewOverrides}): the answer stored under
3402
+ * {@link VIEW_LIST_KEY} while it holds one (objectui#13057), else the
3403
+ * client's read, which joins the request in flight for that key or starts
3404
+ * one, and stores its answer (objectui#13044). Always a copy the caller may
3405
+ * keep. Every path that should see a fresh list drops the key first; the
3406
+ * list is in {@link VIEW_LIST_KEY}'s doc.
3407
+ */
3408
+ private readViewList;
1784
3409
  create(resource: string, data: Partial<T>): Promise<T>;
1785
3410
  /**
1786
3411
  * Update an existing record.
@@ -1840,25 +3465,25 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1840
3465
  * @returns Promise resolving to array of results
1841
3466
  */
1842
3467
  /**
1843
- * Cross-object transactional batch (ObjectStack #1604 / ADR-0034 item 4).
3468
+ * Cross-object transactional batch (objectstack-ai/objectstack#1604 / ADR-0034 item 4).
1844
3469
  * Runs the operations in ONE server transaction — commit all or roll back
1845
3470
  * all. A field value of `{ $ref: <earlier op index> }` resolves to that op's
1846
3471
  * created id, so a child can reference its parent created earlier in the same
1847
3472
  * batch (master-detail).
1848
3473
  *
1849
3474
  * Transport: the published `@objectstack/client` SDK method
1850
- * `data.batchTransaction` (framework #3271; shipped since client v16, our
3475
+ * `data.batchTransaction` (objectstack-ai/objectstack#3271; shipped since client v16, our
1851
3476
  * dependency floor). Per AGENTS.md §7 data always flows through the client —
1852
3477
  * never a hand-rolled `fetch('/api/v1/batch')`.
1853
3478
  *
1854
- * Fallback decision — declarative capability negotiation (framework #3298 /
3479
+ * Fallback decision — declarative capability negotiation (objectstack-ai/objectstack#3298 /
1855
3480
  * objectui #2693). At connect() we read `capabilities.transactionalBatch`
1856
3481
  * from discovery:
1857
3482
  * - Declared `true` → the backend GUARANTEES atomicity (declared ===
1858
3483
  * enforced). We TRUST it: any batch failure — including 404/405/501 —
1859
3484
  * surfaces as a real error. No non-atomic client-side compensation. This
1860
3485
  * is the path modern backends take.
1861
- * - Declared `false`, or ABSENT (backend predates #3298) → we can't rely on
3486
+ * - Declared `false`, or ABSENT (backend predates objectstack-ai/objectstack#3298) → we can't rely on
1862
3487
  * server atomicity, so we keep the legacy behaviour: on 404/405 (no
1863
3488
  * endpoint) or 501 (runtime without transactions) degrade to the
1864
3489
  * client-side, NON-atomic {@link emulateBatchTransaction} so a save is
@@ -1951,6 +3576,30 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1951
3576
  * the caller triggers the browser download.
1952
3577
  */
1953
3578
  exportDownload(resource: string, request?: ExportDownloadRequest): Promise<Blob>;
3579
+ /**
3580
+ * Download the server's import template:
3581
+ * `GET /api/v1/data/:object/export?template=true` (objectui#9600).
3582
+ *
3583
+ * Same route as {@link exportDownload}, with the server's template switch. It
3584
+ * sends only `template=true`: the server refuses the export's row parameters
3585
+ * on a template request, and its answer is always an xlsx workbook.
3586
+ *
3587
+ * The server writes the template's labels in the request's locale, and the
3588
+ * import accepts a translated label only in its own request's locale. The
3589
+ * import request goes through `@objectstack/client`, which stamps
3590
+ * `Accept-Language` with the client's locale when one is set (and the host's
3591
+ * `fetch` stamps its own otherwise). This request bypasses the client, so it
3592
+ * stamps the client's locale the same way. Both requests then carry the same
3593
+ * locale.
3594
+ */
3595
+ downloadImportTemplate(resource: string): Promise<Blob>;
3596
+ /**
3597
+ * GET `/api/v1/data/:object/export` with the given query and answer the body
3598
+ * as a Blob. A failure throws an `Error` that carries the server's `status`
3599
+ * and its ADR-0112 `code`. The one request path {@link exportDownload} and
3600
+ * {@link downloadImportTemplate} share.
3601
+ */
3602
+ private fetchExportBlob;
1954
3603
  /**
1955
3604
  * Convert ObjectUI QueryParams to ObjectStack QueryOptions.
1956
3605
  * Maps OData-style conventions to ObjectStack conventions.
@@ -1964,15 +3613,6 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1964
3613
  * @returns Promise resolving to the object schema
1965
3614
  */
1966
3615
  getObjectSchema(objectName: string): Promise<unknown>;
1967
- /**
1968
- * ADR-0056 P2 (epic #2398) — stamp structured-widget hints onto platform
1969
- * fields whose framework type is a storage primitive (e.g. `textarea`) but
1970
- * whose authoring UX should be a structured editor. Only the render `widget`
1971
- * is added; the field's `type` (the storage contract) is untouched. Applied
1972
- * idempotently to the cached schema so form + detail both honor it. Widget
1973
- * components are registered as `field:<widget>` in `@object-ui/fields`.
1974
- */
1975
- private applyFieldWidgetOverrides;
1976
3616
  /**
1977
3617
  * Fetch a single object's schema while always revalidating the browser cache.
1978
3618
  *
@@ -2002,6 +3642,27 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2002
3642
  name: string;
2003
3643
  label?: string;
2004
3644
  }>>;
3645
+ /**
3646
+ * List the items one metadata registry type declares —
3647
+ * `GET /api/v1/meta/:type`, through the client's `meta.getItems` — as
3648
+ * `{ name, label }` headers for the `position-ref` / `permission-ref` field
3649
+ * widgets (objectui#7611).
3650
+ *
3651
+ * Unlike {@link getObjects}, a failure REJECTS (the `DataSource` contract):
3652
+ * the transport's own error propagates, and an answer without the spec's
3653
+ * `items` list is refused rather than read as an empty registry, so a
3654
+ * failed load can never reach the widget as "this registry declares
3655
+ * nothing". Uncached, like `getObjects`: an item authored in Studio is
3656
+ * listed on the next form that opens.
3657
+ *
3658
+ * An item without a string `name` is not listed (the widget stores the
3659
+ * name, so there is nothing to store); a `label` that is not a string is
3660
+ * left off rather than coerced.
3661
+ */
3662
+ listMetadataItems(type: string): Promise<Array<{
3663
+ name: string;
3664
+ label?: string;
3665
+ }>>;
2005
3666
  /**
2006
3667
  * Get access to the underlying ObjectStack client for advanced operations.
2007
3668
  */
@@ -2061,9 +3722,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2061
3722
  * Batch-fetch all persisted view overrides for an object.
2062
3723
  *
2063
3724
  * Per-view runtime overrides (density, column widths, sort, hidden
2064
- * columns, inlineEdit …) live in the SAME metadata namespace the
2065
- * write path uses: `type='view'`, `name=<viewId>` (see
2066
- * {@link updateViewConfig}). Loading them per-view fires N HTTP GETs
3725
+ * columns, inlineEdit …) live in the SAME metadata namespace the view
3726
+ * writes use: `type='view'`, `name=<viewId>` (see {@link createView} /
3727
+ * {@link updateView}; the toolbar's `updateViewConfig` wrote overlays there
3728
+ * until objectui#12504 retired it). Loading them per-view fires N HTTP GETs
2067
3729
  * that 404 for every view the user never customized — console noise on
2068
3730
  * every page load. This batch method performs a single
2069
3731
  * `GET /api/v1/meta/view` (returns `{type, items}`) and narrows the
@@ -2087,9 +3749,9 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2087
3749
  *
2088
3750
  * Result is cached identically to {@link getView}, and EVERY view write —
2089
3751
  * on this adapter and above it — invalidates it through the one seam,
2090
- * {@link invalidateViewKeys}. For a long time only {@link updateViewConfig}
2091
- * did (objectui#4363), which left the other three adapter paths stale
2092
- * for the cache's 5-minute TTL. That gap does not self-heal: the consumer
3752
+ * {@link invalidateViewKeys}. For a long time only `updateViewConfig` (since
3753
+ * retired, objectui#12504) did (objectui#4363), which left the other three
3754
+ * adapter paths stale for the cache's 5-minute TTL. That gap does not self-heal: the consumer
2093
3755
  * (`loadViewOverrides`, app-shell `ObjectView`) treats a RESOLVED map as
2094
3756
  * authoritative and deliberately does not re-probe per view — objectui#3774,
2095
3757
  * and correct, since re-probing reinstates the 404 flurry the batch read
@@ -2113,55 +3775,6 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2113
3775
  * @returns Promise resolving to the view definition or null
2114
3776
  */
2115
3777
  getView(objectName: string, viewId: string): Promise<unknown | null>;
2116
- /**
2117
- * Persist a toolbar-driven view config patch — density, column widths,
2118
- * sort, hidden columns, inline edit. Symmetric counterpart to
2119
- * {@link getView}: writes the row to the server metadata store via
2120
- * `client.meta.saveItem`, then invalidates the matching cache entry so the
2121
- * next {@link getView} reflects the new payload. Returns the persisted item
2122
- * when the server echoes it, otherwise undefined.
2123
- *
2124
- * Called from exactly ONE production site — `ObjectView`'s
2125
- * `persistViewPatch`, for the toolbar toggle — but that ONE call site
2126
- * fires for BOTH kinds of active tab: a code-defined **system** view (no
2127
- * row of its own yet) and a genuinely user-created **saved** view (already
2128
- * has a row — the toggle is editing ITS OWN definition, not laying an
2129
- * overlay on top of it). Which one a given call means is NOT re-derived
2130
- * here from the write's shape (objectui#4227's own lesson: shape inference
2131
- * on this namespace is exactly what let a system view masquerade as
2132
- * saved) — the caller already knows, via the same `isSavedViewId`
2133
- * classification that gates the switcher's readonly flag and its five
2134
- * mutating handlers, and passes it as {@link opts.isSavedView}.
2135
- *
2136
- * - `isSavedView` false/omitted (system-view target, the common case and
2137
- * the default for backward compatibility): stamps
2138
- * {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row —
2139
- * the original objectui#4227 fix.
2140
- * - `isSavedView` true: the marker is withheld. Stamping it here would
2141
- * flag the saved view's OWN row as a personalization overlay, and
2142
- * `listViews()` would exclude it on the very next read — the user's own
2143
- * view would vanish from the switcher the moment they toggled its
2144
- * density (objectui#4227 follow-up, PM review on PR #4713, measured:
2145
- * `persistViewPatch` has no gate on which kind of tab is active, and
2146
- * this method writes to the exact same `(type='view', name=viewId)` key
2147
- * {@link createView}/the ADR-0034 `viewEnvelope` seam already used for
2148
- * that view, so the write is an upsert onto the saved view's row, not a
2149
- * new one).
2150
- *
2151
- * Per objectstack#7494's ruling, the overlay this writes (system-view
2152
- * case) is ORG-WIDE shared view settings, not a per-user preference — a
2153
- * true per-user scope is a parked v18 direction on the platform side.
2154
- *
2155
- * @param objectName - Object name (e.g. 'lead')
2156
- * @param viewId - View identifier (e.g. 'all_leads')
2157
- * @param config - Full view definition to persist
2158
- * @param opts.isSavedView - Whether `viewId` already names a saved view
2159
- * (vs. a system view being personalized for the first time). Omit /
2160
- * `false` for the default overlay-marking behavior.
2161
- */
2162
- updateViewConfig(objectName: string, viewId: string, config: Record<string, any>, opts?: {
2163
- isSavedView?: boolean;
2164
- }): Promise<Record<string, any> | void>;
2165
3778
  /**
2166
3779
  * List user-created views for a given object via the metadata overlay
2167
3780
  * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
@@ -2173,6 +3786,42 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2173
3786
  * the metadata index is name-only, not field-typed, so the route has no
2174
3787
  * `?object=` to push the filter down into. {@link listViewOverrides}
2175
3788
  * reads the same rows through the same accessor.
3789
+ *
3790
+ * ## Every failure still degrades to an empty list — and now says which kind
3791
+ * ## of failure it was (objectui#8151)
3792
+ *
3793
+ * The degrade is unchanged and deliberate, exactly as it is one method over
3794
+ * in {@link listImportMappings}: a host with no metadata door keeps answering
3795
+ * `[]` rather than breaking the page. What changed is that the OTHER failures
3796
+ * no longer render as that one.
3797
+ *
3798
+ * This method's empty list is read as *this object has no saved views*, and
3799
+ * that reading reaches the user in two places measured on this tree: the list
3800
+ * view switcher simply shows fewer tabs, and `@object-ui/core`'s
3801
+ * `elementDataSourceViewNotFoundMessage` states it outright. So a session
3802
+ * whose token lapsed mid-read was shown an object that appears to have no
3803
+ * saved views AT ALL — including ones the user created — with a
3804
+ * `console.warn` as the only discriminator, in a console nothing in the UI
3805
+ * points at. That is the silence objectui#7741 removed from the sibling after
3806
+ * it produced a confident wrong diagnosis in objectstack#14026.
3807
+ *
3808
+ * The `catch` now asks {@link classifyViewsFailure} what the failure WAS,
3809
+ * reading the error's own ADR-0112 `code` and status — never "is the result
3810
+ * empty", which is what both conditions produce and so can never tell them
3811
+ * apart — and anything that is not the doorless shape is announced on
3812
+ * {@link onMetadataReadWarning}.
3813
+ *
3814
+ * ⛔ That classifier is a SEPARATE reading, not a second caller of the
3815
+ * sibling's: `view`'s quiet set is strictly smaller, because the arm
3816
+ * `listImportMappings` is built around (*an older server without this kind*)
3817
+ * has no members here. See {@link classifyViewsFailure} for the measurement.
3818
+ *
3819
+ * ⛔ The RETURN is untouched. This method has answered `Promise<any[]>`, never
3820
+ * throwing, since `@object-ui/data-objectstack@17.1.0`; a consumer that reads
3821
+ * nothing new sees exactly what it saw before, including on the loud arms.
3822
+ * Applying framework objectstack#13906 decision 1 option A — *a thing that could not be
3823
+ * READ is not a thing that is ABSENT* — is done by ADDING a channel, not by
3824
+ * moving that contract.
2176
3825
  */
2177
3826
  listViews(objectName: string, options?: {
2178
3827
  previewDrafts?: boolean;
@@ -2186,11 +3835,36 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2186
3835
  private metadataClient;
2187
3836
  /**
2188
3837
  * List registered import `mapping` artifacts targeting a given object
2189
- * (framework #2611). Reads the `mapping` metadata kind via the overlay API
3838
+ * (objectstack-ai/objectstack#2611). Reads the `mapping` metadata kind via the overlay API
2190
3839
  * and filters by `targetObject` client-side (the metadata index is
2191
- * name-only). Feeds the import wizard's "saved mapping" selector; a failure
2192
- * (older server without the `mapping` kind) degrades to an empty list, so
2193
- * the selector simply doesn't appear.
3840
+ * name-only). Feeds the import wizard's "saved mapping" selector.
3841
+ *
3842
+ * ## Every failure still degrades to an empty list — and now says which kind
3843
+ * ## of failure it was (objectui#7741)
3844
+ *
3845
+ * The degrade is unchanged and deliberate: an older server without the
3846
+ * `mapping` kind must not break the wizard, so it keeps answering `[]` and
3847
+ * the selector simply doesn't appear. That is a real, supported deployment
3848
+ * shape and it stays quiet.
3849
+ *
3850
+ * What changed is that the OTHER failures no longer render as that one. A
3851
+ * refused door and a broken one used to be indistinguishable, in the UI, from
3852
+ * "no mapping is registered" — both were an empty list behind a
3853
+ * `console.warn` nothing pointed at — and that silence produced a confident
3854
+ * wrong diagnosis in a careful reporter (objectstack#14026 was filed, routed
3855
+ * and worked by two seats against a wizard that had been correct since
3856
+ * 17.1.0). So the `catch` now asks {@link classifyImportMappingsFailure} what
3857
+ * the failure WAS, reading the error's own ADR-0112 `code` and status —
3858
+ * never "is the result empty", which is what both conditions produce and so
3859
+ * can never tell them apart — and anything that is not the supported
3860
+ * kind-absent shape is announced on {@link onMetadataReadWarning}.
3861
+ *
3862
+ * ⛔ The RETURN is untouched. This method has answered `Promise<any[]>`, never
3863
+ * throwing, since `@object-ui/data-objectstack@17.1.0`; a consumer that reads
3864
+ * nothing new sees exactly what it saw before, including on the loud arms.
3865
+ * Applying framework objectstack#13906 decision 1 option A — *a thing that could not be
3866
+ * READ is not a thing that is ABSENT* — is done by ADDING a channel, not by
3867
+ * moving that contract.
2194
3868
  */
2195
3869
  listImportMappings(objectName: string): Promise<any[]>;
2196
3870
  /**
@@ -2311,6 +3985,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2311
3985
  * the PUBLISHED half after the draft was discarded carries the partial
2312
3986
  * state on the error's `outcome` — "draft gone, overlay left" is exactly
2313
3987
  * what the old `{ deleted: boolean }` could not express.
3988
+ * Its message is the adapter's "NOT fully removed" sentence followed, on
3989
+ * the next line, by the server's own reason (objectui#12416), so
3990
+ * `formatMetadataError(err)` shows the remedy a refusal names; the
3991
+ * original error stays on `cause`.
2314
3992
  *
2315
3993
  * Invalidates through {@link invalidateViewKeys}: the deleted row leaves the
2316
3994
  * batch override map too, and a ghost entry there is what the object page
@@ -2351,11 +4029,17 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2351
4029
  *
2352
4030
  * The maintainer ruling (2026-08-12) put the answer on the BY-NAME route
2353
4031
  * rather than in the list, so the enumeration surface is not widened past what
2354
- * a by-name probe already implies (objectstack#8013 / PR #8135): an app that
4032
+ * a by-name probe already implies (objectstack#8013 / PR objectstack-ai/objectstack#8135): an app that
2355
4033
  * exists and whose `requiredPermissions` the session lacks answers `403` with
2356
4034
  * `PERMISSION_DENIED` in the declared envelope, and absence — a nonexistent
2357
- * name, an unpublished app, an app gated by an absent optional service —
2358
- * keeps answering `404 RESOURCE_NOT_FOUND`.
4035
+ * name, an unpublished app, an app gated by an absent optional service — is
4036
+ * one undifferentiated answer.
4037
+ *
4038
+ * ⚠️ That absence is NOT uniformly a `404`, which this docstring asserted
4039
+ * until objectui#9262 measured it: the 404 covers an app that EXISTS and is
4040
+ * withheld for a non-permission reason, while a name that resolves to nothing
4041
+ * answers `200` with an envelope carrying no `item`. Both are absence and both
4042
+ * read as `not_found`; see {@link metaEnvelopeCarriesItem} for the measurement.
2359
4043
  *
2360
4044
  * ## Why this is a separate method and not a flavour of {@link getApp}
2361
4045
  *
@@ -2366,13 +4050,55 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2366
4050
  * be cached beside a document about the APP — one grant, and a cached
2367
4051
  * denial outlives the session it described.
2368
4052
  *
2369
- * Nothing here throws: a probe that cannot reach an answer returns `unknown`
2370
- * and the caller keeps whatever it was already showing. Only the measured
2371
- * `code` produces `denied` — never a status, never a message (objectui#4408).
4053
+ * Nothing here throws: every outcome is a verdict the caller renders a screen
4054
+ * from, never a catch block. Only the measured `code` produces `denied` —
4055
+ * never a status, never a message (objectui#4408).
4056
+ *
4057
+ * ## objectui#9262 — what this method stopped folding away
4058
+ *
4059
+ * It used to answer `granted` whenever the call did not throw, and `unknown`
4060
+ * for everything that did. Both halves were wrong in the same direction, and
4061
+ * the first was measured on a real server before this change landed:
4062
+ *
4063
+ * - **an app that does not exist answers `200` with the envelope minus its
4064
+ * `item`**, so "did not throw" reported `granted` for a name with nothing
4065
+ * behind it — the shape issue objectui#9262 listed as cause 8 and expected
4066
+ * to be structural. It is the ordinary case: every typo, every never-created
4067
+ * app and every unpublished draft took it. See
4068
+ * {@link metaEnvelopeCarriesItem} for the measurement and the server-side
4069
+ * mechanism.
4070
+ * - a 404 absence and an unreachable server were one `unknown`, though the
4071
+ * transport had already told them apart.
4072
+ *
4073
+ * The verdicts now say only what was obtained: a served document, a refusal
4074
+ * naming permission, an answer that there is no such app here, or no answer
4075
+ * at all. `unknown` is left for the one case where nothing was asked.
2372
4076
  *
2373
4077
  * @param appName - the app name as it appears in the URL segment
2374
4078
  */
2375
4079
  probeAppAccess(appName: string): Promise<AppAccessVerdict>;
4080
+ /**
4081
+ * {@link probeAppAccess} for any metadata type whose by-name read refuses a
4082
+ * caller (objectui#12112): why `name` is not in this session's list of
4083
+ * `type`, plus the server's own sentence when it refused.
4084
+ *
4085
+ * Since objectstack#22731 a dashboard and a list view carry the app's gate:
4086
+ * `GET /api/v1/meta/dashboard` and `/meta/view` list only what the caller may
4087
+ * open, so a withheld item and a missing one are the same absence there, and
4088
+ * the by-name read of one the caller does not hold answers `403`
4089
+ * `PERMISSION_DENIED`, "the app arm's whole refusal (same emitter, same
4090
+ * envelope)". The verdicts are therefore read exactly as for an app.
4091
+ *
4092
+ * `message` is the refusal's `message`, as the server wrote it: the one
4093
+ * sentence that names the item and says it was refused. Only on `denied`.
4094
+ *
4095
+ * Nothing here throws, and nothing is cached: a verdict about the CALLER
4096
+ * must not outlive a grant.
4097
+ *
4098
+ * @param type - the singular metadata type (`app`, `dashboard`, `view`)
4099
+ * @param name - the item's name, as the by-name route addresses it
4100
+ */
4101
+ probeMetaItemAccess(type: string, name: string): Promise<MetaItemAccess>;
2376
4102
  /**
2377
4103
  * Get a page definition from ObjectStack.
2378
4104
  * Uses the metadata API to fetch page layouts.
@@ -2397,9 +4123,28 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2397
4123
  * Uses `this.client.analytics.query()` from @objectstack/client to leverage
2398
4124
  * the SDK's built-in auth, headers, and fetch configuration.
2399
4125
  * Falls back to client-side aggregation via find() if the analytics endpoint
2400
- * is not available.
4126
+ * is not available. Throws {@link AnalyticsResultShapeError} when it answers
4127
+ * with anything but an `AnalyticsResult` carrying `rows`.
2401
4128
  */
2402
4129
  aggregate(resource: string, params: any): Promise<any[]>;
4130
+ /**
4131
+ * Answer a grouped list view's GROUP HEADER query (objectui#7189) — see
4132
+ * `DataSource.queryGroupHeaders` in `@object-ui/types` for the contract.
4133
+ *
4134
+ * The compiled query rides the platform's EXISTING data door verbatim:
4135
+ * `client.data.query()` posts it to `POST /data/:object/query`, whose
4136
+ * `findData` routes a body carrying `groupBy` / `aggregations` to
4137
+ * `engine.aggregate` and answers the header rows as `records` (the door the
4138
+ * spec half of objectstack#14556 names, pinned platform-side by
4139
+ * objectstack#15330). No new route, no new wire shape.
4140
+ *
4141
+ * ⛔ No fallback, deliberately — unlike {@link aggregate}. A header query that
4142
+ * degraded to bucketing a fetched page would answer page slices under a name
4143
+ * that promises the query's own numbers, which is the defect this member
4144
+ * exists to end. A failure is thrown to the caller, and a body without a
4145
+ * `records` array is refused rather than read as "no groups".
4146
+ */
4147
+ queryGroupHeaders(resource: string, query: EngineAggregateOptions): Promise<ListViewGroupHeaderRow[]>;
2403
4148
  /**
2404
4149
  * Client-side aggregation over a server-scoped `find()` — the fallback used
2405
4150
  * whenever the analytics endpoint cannot answer (capability absent, network
@@ -2585,6 +4330,21 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2585
4330
  /**
2586
4331
  * Factory function to create an ObjectStack data source.
2587
4332
  *
4333
+ * The declared return is `ObjectStackAdapter<T>`, not the shared
4334
+ * `DataSource<T>` interface: the object this hands back is an
4335
+ * `ObjectStackAdapter`, and the adapter-only members the package README
4336
+ * documents — `getClient`, the metadata-cache controls, the connection-state
4337
+ * and batch-progress subscriptions — live on the class, not on `DataSource`.
4338
+ * Declaring the interface here narrowed all of them
4339
+ * away from the factory while leaving them on the value (objectui#7323), so
4340
+ * `createObjectStackAdapter(...)` and `new ObjectStackAdapter(...)` — the two
4341
+ * documented ways to obtain the same object — handed back different type
4342
+ * surfaces. ⛔ Do not narrow this back to `DataSource<T>`: a caller who wants
4343
+ * the narrow surface writes `const ds: DataSource = createObjectStackAdapter(…)`
4344
+ * and gets it, because a wider return is assignable to the narrower annotation;
4345
+ * the reverse is not recoverable at the call site without a cast.
4346
+ * `adapterFactoryReturn.types.test.ts` pins both directions.
4347
+ *
2588
4348
  * @example
2589
4349
  * ```typescript
2590
4350
  * const dataSource = createObjectStackAdapter({
@@ -2607,6 +4367,6 @@ declare function createObjectStackAdapter<T = unknown>(config: {
2607
4367
  autoReconnect?: boolean;
2608
4368
  maxReconnectAttempts?: number;
2609
4369
  reconnectDelay?: number;
2610
- }): DataSource<T>;
4370
+ }): ObjectStackAdapter<T>;
2611
4371
 
2612
- export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsDatasetNotFoundError, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, AnalyticsUnauthenticatedError, type AppAccessVerdict, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, DataApiValidationError, FILTER_OPERATOR_ALIASES, MalformedFilterError, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, type MetadataLockState, MetadataNotFoundError, type MetadataOverlayScope, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type UserDataAdapter, VIEW_OVERLAY_OWNED_KEYS, type WriteWarningEvent, type WriteWarningListener, classifyAnalyticsFailure, clearSharedDiscoveryCache, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, createQuietHttpLogger, formatHttpFailureMessage, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, narrowPersonalizationOverlay, normaliseClientError, readSaveAdvisories, readTransactionalBatchCapability, serializeOrderBy, viewItemObjectName };
4372
+ export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsDatasetNotFoundError, AnalyticsForbiddenError, AnalyticsKeysOnSpecShapeError, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, AnalyticsResultShapeError, AnalyticsUnauthenticatedError, type AppAccessVerdict, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, CHOICE_TYPES_REQUIRING_OPTIONS, ConcurrentUpdateError, ConnectionError, type ConnectionStateEvent, type ConnectionStateListener, DataApiValidationError, type DraftChange, type DroppedFieldsNotice, FILTER_OPERATOR_ALIASES, type ImportMappingsFailureKind, META_ITEM_ABSENT_CODE, MalformedFilterError, type MetaItemAccess, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListAnswer, type MetadataListOptions, type MetadataLockState, MetadataNotFoundError, type MetadataOverlayScope, type MetadataReadFailureKind, type MetadataReadWarningEvent, type MetadataReadWarningListener, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, OBJECT_METADATA_TYPE, ObjectStackAdapter, ObjectStackError, type ObjectStackUserPreferenceFamilyOptions, type ObjectStackUserStateAdapterOptions, PREFERENCE_FAMILY_ROW_LIMIT, RELATIONSHIP_TYPES_REQUIRING_REFERENCE, UNRECOGNIZED_DROP_REASON, UnlowerableAnalyticsFilterError, UnloweredAggregateWhereError, type UnrecognizedDropReasonEvent, type UserDataAdapter, type UserPreferenceFamily, VIEW_OVERLAY_OWNED_KEYS, type WriteWarningEvent, type WriteWarningListener, assertObjectMetadataWritable, classifyAnalyticsFailure, classifyImportMappingsFailure, classifyViewsFailure, clearSharedDiscoveryCache, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserPreferenceFamily, createObjectStackUserStateAdapter, createQuietHttpLogger, draftChangesUnder, dropServedPicklistOptions, extractDraftBody, formatHttpFailureMessage, formatMetadataError, formatMetadataIssue, getSharedDiscovery, governingDraftChange, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isMetaItemAbsentError, isObjectStackError, narrowPersonalizationOverlay, normaliseClientError, readDraftChanges, readSaveAdvisories, readTransactionalBatchCapability, serializeOrderBy, viewItemObjectName };