@object-ui/data-objectstack 17.7.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
@@ -2,13 +2,113 @@ import { ObjectStackClient } from '@objectstack/client';
2
2
  import { DroppedFieldsEvent, EngineAggregateOptions } from '@objectstack/spec/data';
3
3
  export { DroppedFieldsEvent } from '@objectstack/spec/data';
4
4
  import { ListViewGroupHeaderRow } from '@objectstack/spec/ui';
5
- import { GetMetaItemLayeredResponse, RuntimeAuthoringIssue, PublishPackageDraftsResponse } from '@objectstack/spec/api';
5
+ import { GetMetaItemResponse, GetMetaItemLayeredResponse, ListDraftsResponse, RuntimeAuthoringIssue, GetMetaItemsResponse, PublishPackageDraftsResponse, ValidateDataIssue } from '@objectstack/spec/api';
6
6
  export { RuntimeAuthoringIssue } from '@objectstack/spec/api';
7
7
  import { DatasetSelection, AnalyticsResult } from '@objectstack/spec/contracts';
8
- import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types';
9
- 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';
10
10
  import { DatasetDrillRange } from '@object-ui/core';
11
11
 
12
+ /**
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.
16
+ *
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.
74
+ */
75
+ type DraftChange = NonNullable<GetMetaItemResponse['draftChanges']>[number];
76
+ /**
77
+ * The `draftChanges` of a served draft envelope — the value
78
+ * {@link MetadataClient.getDraft} resolved to — or `null` when the read
79
+ * recorded none.
80
+ *
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").
97
+ *
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
+
12
112
  /**
13
113
  * ObjectUI
14
114
  * Copyright (c) 2024-present ObjectStack Inc.
@@ -57,6 +157,15 @@ interface MetadataSaveAdvisoryEvent {
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
@@ -178,18 +287,50 @@ interface MetadataListOptions {
178
287
  /** Filter by source package id (matches the `package` query param). */
179
288
  packageId?: string;
180
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'>;
181
306
  /**
182
307
  * A pending DRAFT metadata item (ADR-0033), as returned by {@link MetadataClient.listDrafts}.
183
308
  * Light header — no body — carrying the owning package so the console can group
184
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.
185
327
  */
186
- interface MetadataDraftHeader {
187
- type: string;
188
- name: string;
189
- packageId: string | null;
190
- updatedAt: string | null;
191
- 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'];
192
331
  }
332
+ /** One element of `ListDraftsResponseSchema.drafts`, the spec row `MetadataDraftHeader` derives from. */
333
+ type ListDraftsRow = ListDraftsResponse['drafts'][number];
193
334
  /**
194
335
  * What {@link MetadataClient.publishPackageDrafts} resolves with — the "publish
195
336
  * whole app" body, DERIVED from `PublishPackageDraftsResponseSchema` rather
@@ -212,6 +353,26 @@ interface MetadataDraftHeader {
212
353
  * or retypes arrives here with no edit.
213
354
  */
214
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'>;
215
376
  /**
216
377
  * Options for {@link MetadataClient.save} — a WRITE OVER HTTP to
217
378
  * `/api/v1/meta/:type/:name`.
@@ -222,17 +383,41 @@ type MetadataPublishPackageDraftsResult = Partial<PublishPackageDraftsResponse>
222
383
  * metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
223
384
  * `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
224
385
  * exists here, and not one of these exists there: this is the REST client's
225
- * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), the
226
- * destructive-change override, the ADR-0033 draft/publish mode, and the
227
- * 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.
228
390
  */
229
391
  interface MetadataClientSaveOptions {
230
392
  /**
231
- * Optimistic concurrency token (the `checksum` returned by the last
232
- * read). When present, sent as the `If-Match` header so concurrent
233
- * 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.
234
407
  */
235
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?: '*';
236
421
  /**
237
422
  * Bypass destructive-change protection (Phase 3a). The server returns
238
423
  * `409 destructive_change` with `issues[]` if a write would drop or
@@ -253,6 +438,19 @@ interface MetadataClientSaveOptions {
253
438
  * Studio package workspace. Omit for an env-local overlay.
254
439
  */
255
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'>;
256
454
  }
257
455
  interface MetadataGetOptions {
258
456
  /**
@@ -269,7 +467,7 @@ interface MetadataGetOptions {
269
467
  */
270
468
  packageId?: string;
271
469
  }
272
- interface MetadataDeleteOptions extends MetadataClientSaveOptions {
470
+ interface MetadataDeleteOptions extends Omit<MetadataClientSaveOptions, 'surface'> {
273
471
  /**
274
472
  * Target state. `'draft'` discards the pending draft (keeps the
275
473
  * published overlay intact). Omit to reset the active overlay back
@@ -278,12 +476,13 @@ interface MetadataDeleteOptions extends MetadataClientSaveOptions {
278
476
  state?: 'active' | 'draft';
279
477
  }
280
478
  /**
281
- * Which layer an overlay was saved at — `'org'` for a tenant overlay, `'env'`
282
- * 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.
283
482
  *
284
483
  * DERIVED from `@objectstack/spec`, not restated: the vocabulary lives once, in
285
484
  * `GetMetaItemLayeredResponseSchema`'s `overlayScope`
286
- * (`z.enum(['org', 'env']).nullable()`), and this alias indexes the published
485
+ * (`z.enum(['env']).nullable()`), and this alias indexes the published
287
486
  * response type so a scope the spec adds arrives here with no edit. Restating
288
487
  * the union locally is the fork `scripts/check-spec-symbol-derivation.mjs`
289
488
  * exists to reject, and the mirror-image mistake — a consumer re-spelling a
@@ -549,10 +748,51 @@ declare class MetadataClient {
549
748
  * renderer tree into ADR-0037 preview mode off one URL flag.
550
749
  */
551
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;
552
783
  /** List all registered metadata types (returns the registry rows). */
553
784
  listTypes(): Promise<unknown[]>;
554
785
  /** List items of a metadata type (e.g. `object`, `field`, `view`). */
555
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>>;
556
796
  /**
557
797
  * List pending DRAFT items (ADR-0033) — what an AI authored but nobody
558
798
  * published yet. `list()` only sees published/active metadata, so a
@@ -626,10 +866,39 @@ declare class MetadataClient {
626
866
  * (app-shell) and `unwrapViewDraft` (this package) read a draft that arrived
627
867
  * through {@link get}, which already unwrapped the envelope. They split on
628
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.
629
875
  */
630
876
  getDraft<T = unknown>(type: string, name: string, options?: {
631
877
  packageId?: string;
632
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;
633
902
  /**
634
903
  * Report the runtime authoring gate's advisory findings for a write the
635
904
  * server has ALREADY committed (#4133 for the save door, #5026 for the
@@ -721,8 +990,22 @@ declare class MetadataClient {
721
990
  * {@link listDrafts} reads `data?.data?.drafts` because `GET /meta/_drafts`
722
991
  * IS a dispatcher route, and `publishHealthFromResponse` unwraps because the
723
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.
724
1005
  */
725
- publishDraft(type: string, name: string): Promise<{
1006
+ publishDraft(type: string, name: string, options?: {
1007
+ member?: string;
1008
+ }): Promise<{
726
1009
  success?: boolean;
727
1010
  seedApplied?: {
728
1011
  success: boolean;
@@ -777,8 +1060,18 @@ declare class MetadataClient {
777
1060
  * says to read `outcome`, not the boolean), and the two callers have their
778
1061
  * own, different rules for the refusal and rolled-back cases. Deciding that
779
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).
780
1073
  */
781
- publishPackageDrafts(packageId: string): Promise<MetadataPublishPackageDraftsResult>;
1074
+ publishPackageDrafts(packageId: string, options?: MetadataPublishPackageDraftsOptions): Promise<MetadataPublishPackageDraftsResult>;
782
1075
  /**
783
1076
  * Get the 3-state layered view of a metadata item: `code` (the packaged
784
1077
  * artifact baseline), `overlay` (the tenant customisation row alone) and
@@ -834,6 +1127,12 @@ declare class MetadataClient {
834
1127
  * Idempotent: returns the result even when no overlay row existed.
835
1128
  * Pass `state: 'draft'` to discard the pending draft only (keeps the
836
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}.
837
1136
  */
838
1137
  reset<T = unknown>(type: string, name: string, options?: MetadataDeleteOptions): Promise<T>;
839
1138
  /**
@@ -1111,6 +1410,35 @@ interface MetadataCacheStats {
1111
1410
  * As soon as the backend supports `sys_user_preference`, persistence
1112
1411
  * "lights up" with no code change.
1113
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
+ *
1114
1442
  * @module
1115
1443
  */
1116
1444
 
@@ -1133,10 +1461,31 @@ interface ObjectStackUserStateAdapterOptions {
1133
1461
  */
1134
1462
  onError?: (where: 'load' | 'save', error: unknown) => void;
1135
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
+ */
1136
1475
  interface UserDataAdapter<T> {
1476
+ /** Load the persisted list for the current user. Resolve to [] when absent. */
1137
1477
  load(): Promise<T[]>;
1478
+ /** Persist the full list (debounced upstream). Errors are silently ignored. */
1138
1479
  save(items: T[]): Promise<void>;
1139
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;
1140
1489
  /**
1141
1490
  * Build a `UserDataAdapter<T>` backed by ObjectStack.
1142
1491
  *
@@ -1144,62 +1493,117 @@ interface UserDataAdapter<T> {
1144
1493
  * one per slot you want to persist.
1145
1494
  */
1146
1495
  declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
1147
-
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
+ }
1148
1512
  /**
1149
- * Take the body out of a served draft envelope (`{ type, name, item }`) and
1150
- * remove the framework's own READ DECORATIONS from it — or answer `null` when
1151
- * there is nothing pending.
1152
- *
1153
- * ## Why this function exists at all (objectui#8181)
1154
- *
1155
- * It was implemented three separate times — once in `ResourceEditPage`, once in
1156
- * `StudioDesignSurface`, once in `PackageOwdOverviewPanel` — plus a fourth
1157
- * hand-rolled copy in `ObjectHooksPanel` under a different name. objectui#7603
1158
- * taught exactly ONE of those copies to strip. That is the defect this file
1159
- * closes, and it is a different defect from "N consumers forgot": four
1160
- * byte-identical copies of a rule mean the next copy is free to omit it again,
1161
- * so the cure is one function rather than four strips.
1162
- *
1163
- * ## Why it lives HERE and not in `app-shell`
1164
- *
1165
- * Next to the {@link MetadataClient.getDraft} that produces the envelope this
1166
- * decodes. `getDraft` deliberately hands back the wire envelope rather than the
1167
- * body (objectui#4271), so the unwrap is part of that method's contract, not a
1168
- * detail of any one view. `app-shell` already depends on this package, and the
1169
- * one non-app-shell consumer (`updateView`, in this package) is a caller too.
1170
- *
1171
- * ## The verdict runs BEFORE the strip, never after
1172
- *
1173
- * What counts as a pending draft is `getDraft`'s answer. A draft whose only
1174
- * keys are decorations is still a served draft, so the emptiness verdict is
1175
- * taken on the body as it arrived; the strip may return an EMPTY object, but it
1176
- * may never turn a served draft into "no draft". Pinned in
1177
- * `draft-envelope.test.ts`.
1178
- *
1179
- * ## Which keys, and which keys deliberately survive
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.
1180
1515
  *
1181
- * The list is the SPEC'S (`METADATA_READ_DECORATIONS`), reached through its own
1182
- * exported helper — the same one `MetadataService.saveFields` uses on the write
1183
- * side. Never a second hand-maintained `['_diagnostics', '_draft']` in this
1184
- * repo: a local copy goes stale the next time the framework adds a decoration,
1185
- * and a decoration this code does not know to remove is precisely the defect.
1186
- *
1187
- * The ADR-0010 protection envelope (`_lock`, `_provenance`, `_packageId`,
1188
- * `_packageVersion`, ...) shares the underscore spelling and is deliberately
1189
- * NOT on that list: those keys are declared by the closed schemas so provenance
1190
- * survives a re-parse. This strip leaves them alone, and doing otherwise would
1191
- * be the same "drop whatever looks internal" pass AGENTS.md #0.1 bans.
1192
- *
1193
- * ## Not a lenient fallback
1194
- *
1195
- * It removes exactly the two keys the framework ADDS AT READ TIME and never
1196
- * stores. A genuinely unrecognized key still fails loudly at whichever gate
1197
- * sees it next.
1198
- *
1199
- * @param resp the value `MetadataClient.getDraft()` resolved to.
1200
- * @returns the decoration-free draft body, or `null` for "nothing pending".
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.
1201
1519
  */
1202
- declare function extractDraftBody(resp: unknown): Record<string, unknown> | null;
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;
1203
1607
 
1204
1608
  /**
1205
1609
  * One issue → a single field-anchored line: `• fields.amount.type — Required`.
@@ -1272,36 +1676,31 @@ declare function formatMetadataError(e: unknown): string;
1272
1676
  * live Zod pass, on the stated ground that a schema issue on a draft the server
1273
1677
  * ACCEPTS would dead-bolt Save with no on-screen editor able to clear it.
1274
1678
  *
1275
- * The two invariants stand in different relations to the server, and the
1276
- * dead-bolt argument is made separately for each:
1277
- *
1278
- * - The relationship refusal forecloses nothing the server would have taken:
1279
- * the same body is refused one layer down with a 422 on
1280
- * `fields.NAME.reference`, so the guard only moves an identical refusal
1281
- * earlier, where it can name the field while it is still on screen.
1282
- * - The choice refusal is deliberately AHEAD of the server. The installed
1283
- * `@objectstack/spec` still accepts a choice with no option source, and the
1284
- * ruling closes the client order first on purpose — its execution parameter
1285
- * (2) reads "if Studio saves a `select` before its options exist, the
1286
- * objectui side changes its order first (a card in that lane) and the door
1287
- * closes after". So this refusal does hold a body the server would store
1288
- * today. It is not a dead-bolt, for the reason the ruling gives and the
1289
- * reason the editors give: the ruling makes the same body a server refusal
1290
- * once the door closes (a stored row is named and every later save of that
1291
- * object is refused until it gains options), and the message names the
1292
- * field on every surface that holds one. The Studio data page and the
1293
- * metadata-admin object editor carry an options editor in that field's
1294
- * inspector, so the hold clears on screen, including for an object that
1295
- * already stores such a field (both pinned by their
1296
- * `*.choiceWithoutOptions-11253` suites). plugin-designer's drawer has no
1297
- * options editor, so it stops offering the choice types; a stored `select`
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`
1298
1699
  * keeps its type there and can be retyped or removed, and a stored `radio`
1299
1700
  * is among the types that page carries through unedited and sends to
1300
1701
  * metadata-admin, as it already does for a target-less `master_detail`.
1301
- * `object-metadata-write-guard.derivation.test.ts` re-measures "still
1302
- * accepted by the installed server" on every run; when the door ships and
1303
- * the pin moves, that reading turns red, and this paragraph collapses into
1304
- * the relationship one above.
1702
+ * `object-metadata-write-guard.derivation.test.ts` re-measures "refused one
1703
+ * layer down" for both invariants on every run.
1305
1704
  *
1306
1705
  * ⛔ **NOT strip-and-report-saved.** Dropping the offending field and reporting
1307
1706
  * success would trade a visible refusal for an invisible deletion —
@@ -1323,7 +1722,7 @@ declare function formatMetadataError(e: unknown): string;
1323
1722
  * - the choice set from the same members run through `checkFieldCompleteness`
1324
1723
  * (ADR-0078's author-time rule, which the ruling's door adopts), keeping
1325
1724
  * those it reports as `field/choice-without-options` at `error` severity.
1326
- * `FieldSchema` cannot answer this one yet, because the door has not shipped.
1725
+ * The door itself shipped in 17.7.0, and the pin holds the two answers equal.
1327
1726
  *
1328
1727
  * A spec release that changes either set turns that pin red instead of leaving
1329
1728
  * a guard that quietly stopped covering the contract.
@@ -1359,12 +1758,25 @@ declare const OBJECT_METADATA_TYPE = "object";
1359
1758
  * `fields` member — this door serves `view`, `app`, `flow`, `permission`,
1360
1759
  * `hook` and `dashboard` writes too, and has no opinion about them.
1361
1760
  *
1362
- * @param type the metadata type segment of the write (`'object'`, `'view'`, …)
1363
- * @param item the body about to be serialised onto the wire
1364
- * @param writer a short label for the door, so the message names where it fired
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)
1365
1777
  * @throws Error naming the offending field, before any request is issued
1366
1778
  */
1367
- declare function assertObjectMetadataWritable(type: unknown, item: unknown, writer: string): void;
1779
+ declare function assertObjectMetadataWritable(type: unknown, item: unknown, _writer: string): void;
1368
1780
 
1369
1781
  /**
1370
1782
  * Map human-readable filter operator names produced by SDUI view configs
@@ -1682,6 +2094,20 @@ declare function isApiAccessDeniedError(error: unknown): boolean;
1682
2094
  * question was never put.
1683
2095
  */
1684
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}).
2101
+ */
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
+ }
1685
2111
  /**
1686
2112
  * The ADR-0112 standard catalog code the by-name meta app route answers with
1687
2113
  * when an app exists and the session lacks its `requiredPermissions`
@@ -1728,61 +2154,6 @@ declare const META_ITEM_ABSENT_CODE = "RESOURCE_NOT_FOUND";
1728
2154
  * so the honest reading is that no answer was obtained.
1729
2155
  */
1730
2156
  declare function isMetaItemAbsentError(error: unknown): boolean;
1731
- /**
1732
- * [ADR-0066] The system capability a view-configuration write requires.
1733
- *
1734
- * objectstack#7494's ruling (maintainer, 2026-08-12) settled what this store
1735
- * IS: the `sort` / `hiddenFields` / `columnState` / `rowHeight` persisted by
1736
- * {@link ObjectStackAdapter.updateViewConfig} are ORG-WIDE view configuration
1737
- * — shared by every user of the view — not a per-user preference. A per-user
1738
- * scope is parked (objectstack#7611, v18) and deliberately NOT built here, so
1739
- * there is no second, narrower store for an ungated write to fall back to:
1740
- * the only thing this write can do is change the view for everyone.
1741
- *
1742
- * That makes a toolbar toggle a metadata-authoring act, so it gates on the
1743
- * capability this repo ALREADY uses for metadata authoring rather than a newly
1744
- * minted name. `manage_metadata` is what `HomePage`'s `AUTHORING_CAPABILITY`
1745
- * gates the builder CTAs on, what the server itself refuses metadata writes
1746
- * without (`PackageFormDialog` reads that 403), and what
1747
- * `capability.label.manage_metadata` is already translated as in all ten
1748
- * locale packs. Decisively: the write gated here goes through
1749
- * `client.meta.saveItem` — the very same ADR-0005 metadata door — so this is
1750
- * the same authority the server is already applying, not an analogous one.
1751
- */
1752
- declare const VIEW_CONFIG_CAPABILITY = "manage_metadata";
1753
- /**
1754
- * Thrown by {@link ObjectStackAdapter.updateViewConfig} when the session's
1755
- * REPORTED capability set does not contain {@link VIEW_CONFIG_CAPABILITY}.
1756
- *
1757
- * Raised BEFORE `connect()` and before `saveItem`, so a refused call issues no
1758
- * request at all — the org-wide row is not touched, not even optimistically.
1759
- *
1760
- * Deliberately an ERROR and not a silent `return`. The production caller is a
1761
- * debounced toolbar toggle whose UI has ALREADY moved by the time this runs, so
1762
- * a quiet no-op would leave the operator looking at a density they did not get,
1763
- * with nothing to explain it and a reload to discover it. The message names the
1764
- * SCOPE first and the capability second, in that order, because the scope is
1765
- * the part the operator cannot otherwise see — and it is written to be shown
1766
- * as-is, the same contract {@link AnalyticsNotInstalledError} states.
1767
- */
1768
- declare class ViewConfigPermissionDeniedError extends Error {
1769
- readonly code = "VIEW_CONFIG_PERMISSION_DENIED";
1770
- /** The capability that was required and not held. */
1771
- readonly capability = "manage_metadata";
1772
- /** The object whose view configuration the refused write targeted. */
1773
- readonly objectName: string;
1774
- /** The view id the refused write targeted. */
1775
- readonly viewId: string;
1776
- constructor(objectName: string, viewId: string);
1777
- }
1778
- /**
1779
- * True when `error` is a {@link ViewConfigPermissionDeniedError}.
1780
- *
1781
- * Duck-checks `code`/`name` rather than using `instanceof`, matching
1782
- * {@link isConcurrentUpdateError}: a host that bundles this package twice
1783
- * (or re-throws across a worker boundary) still gets the right verdict.
1784
- */
1785
- declare function isViewConfigPermissionDeniedError(error: unknown): error is ViewConfigPermissionDeniedError;
1786
2157
  /**
1787
2158
  * Thrown when the deployment has no analytics capability installed
1788
2159
  * (framework#3891 / objectstack-ai/objectstack#4019).
@@ -2121,11 +2492,9 @@ declare class ConcurrentUpdateError extends Error {
2121
2492
  * reads `httpStatus` for neither. This paragraph exists because the doc used
2122
2493
  * to name only the wire shape, which left the `name` limb reading as drift
2123
2494
  * (objectui#6375). It is not drift: it is the deliberate cross-realm
2124
- * duck-check that {@link isViewConfigPermissionDeniedError} documents and
2125
- * cites *this* function as its precedent for — a host that bundles this
2126
- * package twice (or re-throws across a worker boundary) ends up holding two
2127
- * copies of the class, `instanceof` fails, and the `name` string is the only
2128
- * discriminator left. That host is out of tree by construction, so an in-repo
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
2129
2498
  * consumer census cannot see the case the limb was written for and is not
2130
2499
  * evidence against it; `@object-ui/plugin-form` and `@object-ui/plugin-detail`
2131
2500
  * each carry their own copy of the same two-limb check for adapters they must
@@ -2199,10 +2568,7 @@ declare function formatHttpFailureMessage(message: string, meta?: Record<string,
2199
2568
  * own `ObjectStackClient` gets the same identified failures.
2200
2569
  */
2201
2570
  declare function createQuietHttpLogger(): any;
2202
- /**
2203
- * Connection state for monitoring
2204
- */
2205
- type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error';
2571
+
2206
2572
  /**
2207
2573
  * Connection state change event
2208
2574
  */
@@ -2281,20 +2647,31 @@ interface UnrecognizedDropReasonEvent {
2281
2647
  type DroppedFieldsNotice = DroppedFieldsEvent | UnrecognizedDropReasonEvent;
2282
2648
  /**
2283
2649
  * Emitted after a create/update whose response carried `droppedFields`
2284
- * (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455). The write SUCCEEDED — this is a warning that some
2285
- * supplied fields never landed, so the UI can tell the user rather than let it
2286
- * 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}.
2287
2655
  *
2288
2656
  * `droppedFields` is the two-arm {@link DroppedFieldsNotice} and not
2289
2657
  * `DroppedFieldsEvent[]`: the wire is parsed here, and an entry whose `reason`
2290
2658
  * this bundle's spec pin cannot name arrives on the explicit skew arm rather
2291
- * than being asserted into the union (objectui#4934).
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.
2292
2668
  */
2293
2669
  interface WriteWarningEvent {
2294
2670
  operation: 'create' | 'update';
2295
2671
  resource: string;
2296
2672
  id?: string | number;
2297
2673
  droppedFields: DroppedFieldsNotice[];
2674
+ warnings?: ValidateDataIssue[];
2298
2675
  }
2299
2676
  /** Event listener type for write-warning (dropped-fields) events. */
2300
2677
  type WriteWarningListener = (event: WriteWarningEvent) => void;
@@ -2562,13 +2939,18 @@ declare function viewItemObjectName(item: any): string | undefined;
2562
2939
  * The keys a personalization overlay row legitimately OWNS (objectui#5233).
2563
2940
  *
2564
2941
  * One per `persistViewPatch` call site in app-shell's `ObjectView` — the ONLY
2565
- * 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:
2566
2943
  * `rowHeight` (the density toggle, spec-canonical since #2890), `sort`,
2567
- * `hiddenFields`, `columnState` and `inlineEdit`. Nothing else in such a row
2568
- * is an opinion the user expressed; anything else it carries is a COPY of the
2569
- * source view as it stood at write time, because `persistViewPatch` USED TO
2570
- * send `{ ...baseViewDef, ...patch }` and this adapter persists what it is
2571
- * 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.
2572
2954
  *
2573
2955
  * That copy was the defect the maintainer ruled on (objectstack#7494, comment
2574
2956
  * 5261754173): an overlay written by a mere column drag froze the view's
@@ -2578,22 +2960,19 @@ declare function viewItemObjectName(item: any): string | undefined;
2578
2960
  * view's filter and every user who once resized a column kept the old one,
2579
2961
  * with nothing anywhere reporting it.
2580
2962
  *
2581
- * Both halves of that ruling have now landed, and this adapter's behaviour is
2582
- * 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:
2583
2965
  *
2584
2966
  * - **read** (PR #5272, {@link narrowPersonalizationOverlay}): the consumer
2585
2967
  * that MERGES an overlay over a source view contributes only these keys, so
2586
2968
  * every already-stored fat row that carries the marker stops shadowing its
2587
2969
  * source. A fat row written before the marker is no longer narrowed
2588
2970
  * (objectui#10210, ruling B — see {@link isPersonalizationOverlayRow}).
2589
- * - **write** (objectui#5233, `buildPersistedViewBody` in app-shell's
2590
- * `ObjectView`, unblocked by `columnState`'s admission to the view-metadata
2591
- * surface as a runtime-only overlay key — objectstack `d5552ca13`, released in
2592
- * `@objectstack/spec` 17.1.0): a *system view's* overlay is now written as
2593
- * the patch alone, so no new row freezes anything, and because the write is
2594
- * a whole-document PUT the next toggle also strips an old fat row. A *saved
2595
- * view's* own row is deliberately still written whole — for it the body IS
2596
- * the view, not a copy of one.
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.
2597
2976
  *
2598
2977
  * A fat row is therefore a legacy shape, not a shape this product still
2599
2978
  * produces; the list below is still what a reader is allowed to trust from one.
@@ -2601,9 +2980,10 @@ declare function viewItemObjectName(item: any): string | undefined;
2601
2980
  * ⛔ Do not grow this list to make some other key "stick" through an overlay.
2602
2981
  * A key that belongs to the view belongs in the view; the overlay is a patch,
2603
2982
  * and a patch that carries the whole document is what this list exists to
2604
- * stop. Adding a sixth entry is only correct alongside a sixth
2605
- * `persistViewPatch` call site — {@link narrowPersonalizationOverlay} is what
2606
- * 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.
2607
2987
  */
2608
2988
  declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFields", "columnState", "inlineEdit"];
2609
2989
  /**
@@ -2689,6 +3069,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2689
3069
  private analyticsCapabilityWarned;
2690
3070
  private fetchImpl;
2691
3071
  private inflightFinds;
3072
+ private inflightFindOnes;
2692
3073
  private missingResources;
2693
3074
  private batchUnsupported;
2694
3075
  private atomicBatchCapability;
@@ -2696,7 +3077,6 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2696
3077
  private writeWarningListeners;
2697
3078
  private saveAdvisoryListeners;
2698
3079
  private metadataReadWarningListeners;
2699
- private systemCapabilities;
2700
3080
  constructor(config: {
2701
3081
  baseUrl: string;
2702
3082
  token?: string;
@@ -2708,19 +3088,22 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2708
3088
  autoReconnect?: boolean;
2709
3089
  maxReconnectAttempts?: number;
2710
3090
  reconnectDelay?: number;
2711
- /**
2712
- * [ADR-0066] The session's system capabilities, when the host already has
2713
- * them at construction time. Most hosts do NOT — `/me/permissions`
2714
- * resolves after the adapter exists — and push them in later via
2715
- * {@link ObjectStackAdapter.setSystemCapabilities}. Omit for "unreported".
2716
- */
2717
- systemCapabilities?: string[];
2718
3091
  });
2719
3092
  /**
2720
3093
  * Ensure the client is connected to the server.
2721
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).
2722
3100
  */
2723
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;
2724
3107
  /**
2725
3108
  * Attempt to reconnect to the server with exponential backoff
2726
3109
  */
@@ -2745,6 +3128,34 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2745
3128
  * Set connection state and notify listeners
2746
3129
  */
2747
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;
2748
3159
  /**
2749
3160
  * Emit batch progress event to listeners
2750
3161
  */
@@ -2754,6 +3165,16 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2754
3165
  * Converts OData-style params to ObjectStack query options.
2755
3166
  */
2756
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;
2757
3178
  /**
2758
3179
  * Full-text search across every searchable object in a single round-trip.
2759
3180
  *
@@ -2776,8 +3197,13 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2776
3197
  }): Promise<GlobalSearchResult>;
2777
3198
  /**
2778
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}.
2779
3203
  */
2780
3204
  findOne(resource: string, id: string | number, params?: QueryParams): Promise<T | null>;
3205
+ /** The one round trip behind {@link findOne}, unshared. */
3206
+ private readOne;
2781
3207
  /**
2782
3208
  * Create a new record.
2783
3209
  */
@@ -2809,6 +3235,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2809
3235
  * reason routed to the skew arm, a non-string field element refused, a missing
2810
3236
  * `object` healed from `resource` — never asserted into the union, and the
2811
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: []`.
2812
3242
  */
2813
3243
  private notifyDroppedFields;
2814
3244
  /**
@@ -2892,8 +3322,8 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2892
3322
  *
2893
3323
  * `ObjectStackClient.meta.saveItem` is the second client class that writes
2894
3324
  * through `PUT /api/v1/meta/:type/:name`, and every one of its callers reaches
2895
- * it through an adapter this class constructed — the four inside this file
2896
- * (`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
2897
3327
  * `this.client`, and every caller outside it via {@link getClient}, which
2898
3328
  * hands back this same instance: `MetadataService` (app-shell, five saves),
2899
3329
  * `useNavigationSync`, and plugin-designer's Create/EditAppPage. Wrapping the
@@ -2922,16 +3352,60 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2922
3352
  *
2923
3353
  * Drafts are NEVER gated: the framework returns at its D1 early-return
2924
3354
  * (`if (args.state !== 'active') return null`) before running a rule, so a
2925
- * draft save produces no findings to withhold. This client class has no draft
2926
- * door at all to worry about — the SDK's `saveItem(type, name, item)` takes no
2927
- * mode and always writes the active door, which is exactly why the gate DOES
2928
- * run for its callers. `mode` on the emitted event is therefore derived from
2929
- * the response's own `state` rather than from a request-side flag that does
2930
- * not exist here: `'draft'` when the server says the row landed as a draft,
2931
- * `'publish'` otherwise. That keeps the event truthful about which door it
2932
- * 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.
2933
3372
  */
2934
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;
2935
3409
  create(resource: string, data: Partial<T>): Promise<T>;
2936
3410
  /**
2937
3411
  * Update an existing record.
@@ -3139,15 +3613,6 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
3139
3613
  * @returns Promise resolving to the object schema
3140
3614
  */
3141
3615
  getObjectSchema(objectName: string): Promise<unknown>;
3142
- /**
3143
- * ADR-0056 P2 (epic #2398) — stamp structured-widget hints onto platform
3144
- * fields whose framework type is a storage primitive (e.g. `textarea`) but
3145
- * whose authoring UX should be a structured editor. Only the render `widget`
3146
- * is added; the field's `type` (the storage contract) is untouched. Applied
3147
- * idempotently to the cached schema so form + detail both honor it. Widget
3148
- * components are registered as `field:<widget>` in `@object-ui/fields`.
3149
- */
3150
- private applyFieldWidgetOverrides;
3151
3616
  /**
3152
3617
  * Fetch a single object's schema while always revalidating the browser cache.
3153
3618
  *
@@ -3177,6 +3642,27 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
3177
3642
  name: string;
3178
3643
  label?: string;
3179
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
+ }>>;
3180
3666
  /**
3181
3667
  * Get access to the underlying ObjectStack client for advanced operations.
3182
3668
  */
@@ -3236,9 +3722,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
3236
3722
  * Batch-fetch all persisted view overrides for an object.
3237
3723
  *
3238
3724
  * Per-view runtime overrides (density, column widths, sort, hidden
3239
- * columns, inlineEdit …) live in the SAME metadata namespace the
3240
- * write path uses: `type='view'`, `name=<viewId>` (see
3241
- * {@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
3242
3729
  * that 404 for every view the user never customized — console noise on
3243
3730
  * every page load. This batch method performs a single
3244
3731
  * `GET /api/v1/meta/view` (returns `{type, items}`) and narrows the
@@ -3262,9 +3749,9 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
3262
3749
  *
3263
3750
  * Result is cached identically to {@link getView}, and EVERY view write —
3264
3751
  * on this adapter and above it — invalidates it through the one seam,
3265
- * {@link invalidateViewKeys}. For a long time only {@link updateViewConfig}
3266
- * did (objectui#4363), which left the other three adapter paths stale
3267
- * 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
3268
3755
  * (`loadViewOverrides`, app-shell `ObjectView`) treats a RESOLVED map as
3269
3756
  * authoritative and deliberately does not re-probe per view — objectui#3774,
3270
3757
  * and correct, since re-probing reinstates the 404 flurry the batch read
@@ -3288,82 +3775,6 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
3288
3775
  * @returns Promise resolving to the view definition or null
3289
3776
  */
3290
3777
  getView(objectName: string, viewId: string): Promise<unknown | null>;
3291
- /**
3292
- * Persist a toolbar-driven view config patch — density, column widths,
3293
- * sort, hidden columns, inline edit. Symmetric counterpart to
3294
- * {@link getView}: writes the row to the server metadata store via
3295
- * `client.meta.saveItem`, then invalidates the matching cache entry so the
3296
- * next {@link getView} reflects the new payload. Returns the persisted item
3297
- * when the server echoes it, otherwise undefined.
3298
- *
3299
- * Called from exactly ONE production site — `ObjectView`'s
3300
- * `persistViewPatch`, for the toolbar toggle — but that ONE call site
3301
- * fires for BOTH kinds of active tab: a code-defined **system** view (no
3302
- * row of its own yet) and a genuinely user-created **saved** view (already
3303
- * has a row — the toggle is editing ITS OWN definition, not laying an
3304
- * overlay on top of it). Which one a given call means is NOT re-derived
3305
- * here from the write's shape (objectui#4227's own lesson: shape inference
3306
- * on this namespace is exactly what let a system view masquerade as
3307
- * saved) — the caller already knows, via the same `isSavedViewId`
3308
- * classification that gates the switcher's readonly flag and its five
3309
- * mutating handlers, and passes it as {@link opts.isSavedView}.
3310
- *
3311
- * - `isSavedView` false/omitted (system-view target, the common case and
3312
- * the default for backward compatibility): stamps
3313
- * {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row —
3314
- * the original objectui#4227 fix.
3315
- * - `isSavedView` true: the marker is withheld. Stamping it here would
3316
- * flag the saved view's OWN row as a personalization overlay, and
3317
- * `listViews()` would exclude it on the very next read — the user's own
3318
- * view would vanish from the switcher the moment they toggled its
3319
- * density (objectui#4227 follow-up, PM review on PR #4713, measured:
3320
- * `persistViewPatch` has no gate on which kind of tab is active, and
3321
- * this method writes to the exact same `(type='view', name=viewId)` key
3322
- * {@link createView}/the ADR-0034 `viewEnvelope` seam already used for
3323
- * that view, so the write is an upsert onto the saved view's row, not a
3324
- * new one).
3325
- *
3326
- * Per objectstack#7494's ruling, the overlay this writes (system-view
3327
- * case) is ORG-WIDE shared view settings, not a per-user preference — a
3328
- * true per-user scope is a parked v18 direction on the platform side.
3329
- *
3330
- * @param objectName - Object name (e.g. 'lead')
3331
- * @param viewId - View identifier (e.g. 'all_leads')
3332
- * @param config - Full view definition to persist
3333
- * @param opts.isSavedView - Whether `viewId` already names a saved view
3334
- * (vs. a system view being personalized for the first time). Omit /
3335
- * `false` for the default overlay-marking behavior.
3336
- */
3337
- /**
3338
- * [ADR-0066] Tell this adapter which system capabilities the session holds.
3339
- *
3340
- * Pass `undefined` for "no answer was reported" (a backend predating
3341
- * ADR-0066, or no permission provider mounted). Pass `[]` for a real,
3342
- * reported, empty grant — {@link maySetViewConfig} treats those two
3343
- * differently and callers must not collapse them, which is the same
3344
- * distinction `MePermissionsProvider` preserves natively (objectui#4656).
3345
- */
3346
- setSystemCapabilities(capabilities: string[] | undefined): void;
3347
- /**
3348
- * May this session write ORG-WIDE view configuration?
3349
- *
3350
- * **Unknown fails OPEN**, deliberately, and this is the one judgement in the
3351
- * gate worth reading twice. It is the doctrine `PermissionContextValue`
3352
- * .`hasCapabilities` states for every ADR-0066 gate in this repo
3353
- * (objectui#4656, framework#3923): the server enforces `manage_metadata` on
3354
- * the metadata door regardless of what the client believes, so a client-side
3355
- * denial on MISSING DATA cannot protect anything the server was not already
3356
- * protecting — it can only break a permitted user. Failing closed here would
3357
- * have refused the write on every deployment predating ADR-0066 and every
3358
- * host with no permission provider, which is "break the write for everyone"
3359
- * wearing the costume of a security fix.
3360
- *
3361
- * A REPORTED empty array gates strictly: "holds nothing" is a real answer.
3362
- */
3363
- private maySetViewConfig;
3364
- updateViewConfig(objectName: string, viewId: string, config: Record<string, any>, opts?: {
3365
- isSavedView?: boolean;
3366
- }): Promise<Record<string, any> | void>;
3367
3778
  /**
3368
3779
  * List user-created views for a given object via the metadata overlay
3369
3780
  * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
@@ -3574,6 +3985,10 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
3574
3985
  * the PUBLISHED half after the draft was discarded carries the partial
3575
3986
  * state on the error's `outcome` — "draft gone, overlay left" is exactly
3576
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`.
3577
3992
  *
3578
3993
  * Invalidates through {@link invalidateViewKeys}: the deleted row leaves the
3579
3994
  * batch override map too, and a ghost entry there is what the object page
@@ -3662,6 +4077,28 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
3662
4077
  * @param appName - the app name as it appears in the URL segment
3663
4078
  */
3664
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>;
3665
4102
  /**
3666
4103
  * Get a page definition from ObjectStack.
3667
4104
  * Uses the metadata API to fetch page layouts.
@@ -3897,8 +4334,8 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
3897
4334
  * `DataSource<T>` interface: the object this hands back is an
3898
4335
  * `ObjectStackAdapter`, and the adapter-only members the package README
3899
4336
  * documents — `getClient`, the metadata-cache controls, the connection-state
3900
- * and batch-progress subscriptions, and `setSystemCapabilities` — live on the
3901
- * class, not on `DataSource`. Declaring the interface here narrowed all of them
4337
+ * and batch-progress subscriptions — live on the class, not on `DataSource`.
4338
+ * Declaring the interface here narrowed all of them
3902
4339
  * away from the factory while leaving them on the value (objectui#7323), so
3903
4340
  * `createObjectStackAdapter(...)` and `new ObjectStackAdapter(...)` — the two
3904
4341
  * documented ways to obtain the same object — handed back different type
@@ -3930,8 +4367,6 @@ declare function createObjectStackAdapter<T = unknown>(config: {
3930
4367
  autoReconnect?: boolean;
3931
4368
  maxReconnectAttempts?: number;
3932
4369
  reconnectDelay?: number;
3933
- /** [ADR-0066] See {@link ObjectStackAdapter.setSystemCapabilities}. */
3934
- systemCapabilities?: string[];
3935
4370
  }): ObjectStackAdapter<T>;
3936
4371
 
3937
- 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 ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, DataApiValidationError, type DroppedFieldsNotice, FILTER_OPERATOR_ALIASES, type ImportMappingsFailureKind, META_ITEM_ABSENT_CODE, 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 MetadataReadFailureKind, type MetadataReadWarningEvent, type MetadataReadWarningListener, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, OBJECT_METADATA_TYPE, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, RELATIONSHIP_TYPES_REQUIRING_REFERENCE, UNRECOGNIZED_DROP_REASON, UnlowerableAnalyticsFilterError, UnloweredAggregateWhereError, type UnrecognizedDropReasonEvent, type UserDataAdapter, VIEW_CONFIG_CAPABILITY, VIEW_OVERLAY_OWNED_KEYS, ViewConfigPermissionDeniedError, type WriteWarningEvent, type WriteWarningListener, assertObjectMetadataWritable, classifyAnalyticsFailure, classifyImportMappingsFailure, classifyViewsFailure, clearSharedDiscoveryCache, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, createQuietHttpLogger, extractDraftBody, formatHttpFailureMessage, formatMetadataError, formatMetadataIssue, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isMetaItemAbsentError, isObjectStackError, isViewConfigPermissionDeniedError, 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 };