@object-ui/data-objectstack 17.6.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { ObjectStackClient } from '@objectstack/client';
2
- import { DroppedFieldsEvent } from '@objectstack/spec/data';
2
+ import { DroppedFieldsEvent, EngineAggregateOptions } from '@objectstack/spec/data';
3
3
  export { DroppedFieldsEvent } from '@objectstack/spec/data';
4
- import { GetMetaItemLayeredResponse, RuntimeAuthoringIssue } from '@objectstack/spec/api';
4
+ import { ListViewGroupHeaderRow } from '@objectstack/spec/ui';
5
+ import { GetMetaItemLayeredResponse, RuntimeAuthoringIssue, PublishPackageDraftsResponse } from '@objectstack/spec/api';
5
6
  export { RuntimeAuthoringIssue } from '@objectstack/spec/api';
6
7
  import { DatasetSelection, AnalyticsResult } from '@objectstack/spec/contracts';
7
8
  import { DataSource, QueryParams, QueryResult, GlobalSearchResult, DataSourceMutationEvent, BatchTransactionOperation, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, DeleteViewResult, FileUploadResult } from '@object-ui/types';
@@ -15,42 +16,41 @@ import { DatasetDrillRange } from '@object-ui/core';
15
16
  * This source code is licensed under the MIT license found in the
16
17
  * LICENSE file in the root directory of this source tree.
17
18
  */
18
- /**
19
- * MetadataClient
20
- *
21
- * Thin, framework-agnostic HTTP client for the ObjectStack metadata
22
- * API (`/api/v1/meta/*`). Used by the platform Setup app surfaces in
23
- * `@object-ui/plugin-designer` (Object Manager, Field Designer, etc.)
24
- * so they can read and write protocol metadata without depending on
25
- * `ObjectStackAdapter` (which is a generic data-source binding and
26
- * carries a lot of view/dashboard-specific behaviour we don't need
27
- * here).
28
- *
29
- * Endpoints (see `packages/rest/src/rest-server.ts`):
30
- * GET /api/v1/meta — list all metadata types
31
- * GET /api/v1/meta/:type — list items of a type
32
- * GET /api/v1/meta/:type/:name — get one item (returns the
33
- * unwrapped item content)
34
- * PUT /api/v1/meta/:type/:name — save (overlay) one item
35
- * honours `If-Match` for OCC
36
- * DELETE /api/v1/meta/:type/:name — reset overlay to artifact
37
- * GET /api/v1/meta/:type/:name/history — durable change log
38
- *
39
- * The client deliberately keeps the response shape opaque (typed as
40
- * `unknown`/generic `T`) so callers explicitly narrow per-metadata-
41
- * type, mirroring the framework's "single Zod source per type" rule.
42
- */
43
19
 
44
20
  /**
45
- * Emitted after a {@link MetadataClient.save} whose response carried a
46
- * non-empty `advisories` array. The save SUCCEEDED — the row persisted and the
47
- * server returned 200 — so this is advisory, never a failure.
21
+ * Emitted after a metadata WRITE whose response carried a non-empty
22
+ * `advisories` array. The write SUCCEEDED — the row persisted and the server
23
+ * returned 200 — so this is advisory, never a failure.
48
24
  *
49
25
  * Deliberately the same shape of seam as `ObjectStackAdapter.onWriteWarning`
50
- * (#3431/#3455): a successful write whose response carries something the author
26
+ * (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455): a successful write whose response carries something the author
51
27
  * needs to be told, surfaced to the shell as an event so the data layer never
52
28
  * imports a toaster. The difference is only which door produced it — that one
53
- * is record CRUD, this one is the metadata save door.
29
+ * is record CRUD, these are the metadata write doors.
30
+ *
31
+ * ## Both write doors report (#5026)
32
+ *
33
+ * The objectstack-ai/objectstack#4463 runtime authoring gate runs on BOTH metadata write doors by its D1
34
+ * ruling — a draft→active promotion is gated exactly as a direct active save —
35
+ * and its non-blocking findings ride the 2xx of whichever door earned them.
36
+ * `SaveMetaItemResponseSchema` has carried the key since objectstack-ai/objectstack#4717 and
37
+ * `PublishMetaItemResponseSchema` since objectstack#9176; both declare it at
38
+ * the response's TOP level, under the same name and with the same
39
+ * `RuntimeAuthoringIssueSchema` element type, which is what lets one reader and
40
+ * one event serve both. Measured against the installed `@objectstack/spec`
41
+ * rather than assumed — see the PR for the probe.
42
+ *
43
+ * ## And the BATCH publish door (`ce986aafc`)
44
+ *
45
+ * `PublishPackageDraftsResponseSchema` declares the same key with the same
46
+ * element type, riding EACH `published[]` element rather than a parallel
47
+ * top-level map (objectstack#9343's ruled shape). So the reader is still one
48
+ * reader — the object it is handed is one `published[]` element instead of a
49
+ * whole response — and the event is still one event per promoted item.
50
+ *
51
+ * The name keeps its `Save` prefix because it is public API of this package and
52
+ * renaming it would break consumers for no behavioural gain; {@link door} is
53
+ * what says which write produced the event.
54
54
  */
55
55
  interface MetadataSaveAdvisoryEvent {
56
56
  /** Metadata type saved (e.g. `'flow'`). */
@@ -65,15 +65,65 @@ interface MetadataSaveAdvisoryEvent {
65
65
  * consumer reading the event can tell which door it came through.
66
66
  */
67
67
  mode: 'draft' | 'publish';
68
+ /**
69
+ * Which write door produced this event (#5026).
70
+ *
71
+ * - `'save'` — `PUT /meta/:type/:name` ({@link MetadataClient.save}, and the
72
+ * SDK's `meta.saveItem` behind `ObjectStackAdapter`).
73
+ * - `'publish'` — the promotion doors: `POST /meta/:type/:name/publish`
74
+ * ({@link MetadataClient.publish} and {@link MetadataClient.publishDraft}),
75
+ * and the batch `POST /packages/:id/publish-drafts`
76
+ * ({@link MetadataClient.publishPackageDrafts}), which emits ONE event per
77
+ * `published[]` element carrying that element's own `type` / `name`.
78
+ *
79
+ * ## Why the batch route is not a third value (`ce986aafc`)
80
+ *
81
+ * The batch door promotes drafts to active — the author pressed Publish, and
82
+ * every item this event names really was published. What `door` decides is
83
+ * the frame's VERB, and "Published" is the true one for all three routes; a
84
+ * third value would have to render the same word. The per-item identity the
85
+ * author needs to act on is already carried by {@link type} / {@link name},
86
+ * one event per item, so nothing about the batch is lost by sharing the
87
+ * value. ⛔ It is NOT a claim that "one call = one event": the batch emits as
88
+ * many events as it has advised items, which is why each one names its own.
89
+ *
90
+ * Distinct from {@link mode}, which cannot answer this: a direct active save
91
+ * and a draft promotion both report `mode: 'publish'` because both land the
92
+ * body in the active overlay. The renderer needs the DOOR, because the author
93
+ * pressed either Save or Publish and a toast that says "Saved" after a
94
+ * Publish tells them their change is still a draft — the opposite of what
95
+ * happened, and in this product Save and Publish are two different buttons
96
+ * with two different meanings.
97
+ *
98
+ * REQUIRED rather than optional-with-a-default so a future third door cannot
99
+ * be wired without stating which one it is: an omitted discriminator would
100
+ * silently render the save wording.
101
+ */
102
+ door: 'save' | 'publish';
68
103
  /** The findings. Never empty — the event is not emitted otherwise. */
69
104
  advisories: RuntimeAuthoringIssue[];
70
105
  }
71
106
  /** Event listener type for save-advisory events. */
72
107
  type MetadataSaveAdvisoryListener = (event: MetadataSaveAdvisoryEvent) => void;
73
108
  /**
74
- * Read the `advisories` array off a save response, defensively.
75
- *
76
- * The server omits the key entirely on a clean save, so `undefined` is the
109
+ * Read the `advisories` array off a metadata write response, defensively.
110
+ *
111
+ * Serves EVERY write door unchanged (#5026, `ce986aafc`):
112
+ * `SaveMetaItemResponseSchema` and `PublishMetaItemResponseSchema` declare the
113
+ * key at the same top level, and `PublishPackageDraftsResponseSchema` declares
114
+ * it on each `published[]` element — under the same name, with the same element
115
+ * schema. So there is one reader, not a per-door copy that could drift; the
116
+ * batch caller hands it one element, which is the object that carries the key
117
+ * there.
118
+ *
119
+ * ⛔ It reads the `advisories` key of the object it is GIVEN and never
120
+ * traverses. Choosing the object — a response body at the single-item doors,
121
+ * one `published[]` element at the batch door — is the caller's job precisely
122
+ * because the place is declared per route: a reader that went hunting for
123
+ * findings wherever they might be would be inventing a contract the server
124
+ * never stated.
125
+ *
126
+ * The server omits the key entirely on a clean write, so `undefined` is the
77
127
  * common case and means "nothing to say". Anything that is not an array of
78
128
  * objects carrying the six required keys is dropped rather than rendered: a
79
129
  * half-shaped finding would print blanks at the author, and this channel must
@@ -105,9 +155,13 @@ interface MetadataClientConfig {
105
155
  previewDrafts?: boolean;
106
156
  /**
107
157
  * Called after a {@link MetadataClient.save} whose 2xx response carried a
108
- * non-empty `advisories` array (objectstack#7435). The save already
109
- * succeeded; this is how the shell learns there is something to tell the
110
- * author instead of the findings being discarded client-side.
158
+ * non-empty `advisories` array (objectstack#7435). Since #5026 the SAME sink
159
+ * also receives the publish door's findings (objectstack#9176), and since
160
+ * `ce986aafc` the batch publish door's too, one event per advised
161
+ * `published[]` element (objectstack#9343) — read `event.door` to tell a
162
+ * save from a promotion, and `event.type` / `event.name` for which item. The
163
+ * write already succeeded; this is how the shell learns there is something to
164
+ * tell the author instead of the findings being discarded client-side.
111
165
  *
112
166
  * Set on the CONFIG rather than exposed as a `subscribe()` method on purpose:
113
167
  * console metadata clients are minted per-component by `useMetadataClient`,
@@ -136,6 +190,28 @@ interface MetadataDraftHeader {
136
190
  updatedAt: string | null;
137
191
  updatedBy: string | null;
138
192
  }
193
+ /**
194
+ * What {@link MetadataClient.publishPackageDrafts} resolves with — the "publish
195
+ * whole app" body, DERIVED from `PublishPackageDraftsResponseSchema` rather
196
+ * than re-spelled (`ce986aafc`).
197
+ *
198
+ * Derived, and then widened in exactly two ways, each for a measured reason:
199
+ *
200
+ * - `Partial<…>` because this client talks to runtimes of several vintages and
201
+ * the spec's six required keys are what TODAY's producer sets. Declaring them
202
+ * required would be this client asserting a server version it cannot check —
203
+ * and an older runtime that omits one would be a type lie, not a compile
204
+ * error. Every consumer here already reads these keys defensively.
205
+ * - The index signature because the REST door adds keys on the way out
206
+ * (ADR-0045 visibility receipts, the `metadata:reloaded` announce receipt),
207
+ * and because a body this client hands back must stay readable by a caller
208
+ * that knows about a key this package has never heard of.
209
+ *
210
+ * ⛔ What it is NOT is a second definition of the batch response: the key names
211
+ * and element shapes come from the spec symbol, so a key the spec adds, renames
212
+ * or retypes arrives here with no edit.
213
+ */
214
+ type MetadataPublishPackageDraftsResult = Partial<PublishPackageDraftsResponse> & Record<string, unknown>;
139
215
  /**
140
216
  * Options for {@link MetadataClient.save} — a WRITE OVER HTTP to
141
217
  * `/api/v1/meta/:type/:name`.
@@ -246,9 +322,13 @@ type MetadataOverlayScope = GetMetaItemLayeredResponse['overlayScope'];
246
322
  * the cross-repo half of the drift, not just the in-repo half.
247
323
  *
248
324
  * ⚠️ This types what this repo may WRITE. It does NOT constrain what a server
249
- * may SEND: {@link MetadataClient.layered} casts the wire value through
250
- * unchecked, so every reader must still handle a value outside these four. The
251
- * lock banner in `ResourceEditPage` is the worked example.
325
+ * may SEND, and it never will — {@link MetadataClient.layered} forwards the
326
+ * wire value rather than dropping it, so every reader must still handle a value
327
+ * outside these four. What changed in objectui#5676 is that the value is no
328
+ * longer forwarded *silently*: the boundary validates it against the producer's
329
+ * own schema and names the field in {@link MetadataLayered._unrecognized} when
330
+ * it does not match. The lock banner in `ResourceEditPage` is the worked
331
+ * example of a reader that labels such a value instead of rendering it blank.
252
332
  */
253
333
  type MetadataLockState = GetMetaItemLayeredResponse['lock'];
254
334
  /**
@@ -299,6 +379,21 @@ interface MetadataLayered<T = unknown> {
299
379
  deletable?: boolean;
300
380
  /** True when "Reset to package default" applies (has overlay + artifact). */
301
381
  resettable?: boolean;
382
+ /**
383
+ * Protection-envelope keys whose wire value did NOT match `packages/spec`'s
384
+ * schema for them (objectui#5676). Computed HERE, at the boundary — the
385
+ * server never sends this key, and {@link MetadataClient.layered} builds the
386
+ * returned object field by field, so a server that tried could not smuggle
387
+ * one in.
388
+ *
389
+ * Absent when everything the server sent parsed, so its presence is the
390
+ * signal. The offending values are still forwarded on the fields themselves:
391
+ * this is "pass through and label", the treatment objectui#5672 chose for
392
+ * `lock` alone, applied to the whole ADR-0010 envelope. Dropping them would
393
+ * be the reject semantics that card refused — turning a wrong render into no
394
+ * render is a behaviour change for every consumer of this client.
395
+ */
396
+ _unrecognized?: readonly string[];
302
397
  }
303
398
  /**
304
399
  * One row in the metadata protection-audit trail
@@ -443,7 +538,7 @@ declare class MetadataClient {
443
538
  private readonly headers;
444
539
  /** ADR-0037: when true, reads render the draft-overlaid world. */
445
540
  readonly previewDrafts: boolean;
446
- /** #4133 — sink for post-save advisory findings; see the config field. */
541
+ /** #4133 / #5026 — sink for every write door's advisory findings; see the config field. */
447
542
  private readonly onSaveAdvisory;
448
543
  constructor(config: MetadataClientConfig);
449
544
  /** Update the client's environment scope at runtime. */
@@ -517,12 +612,42 @@ declare class MetadataClient {
517
612
  * {@link get} is deliberate and long-standing (the draft envelope's identity
518
613
  * and protection carriers are part of what a draft reader inspects), so it
519
614
  * is preserved by reading the transport directly instead of going through
520
- * `get()`'s unwrap. `unwrapDraftBody` (app-shell) and `unwrapViewDraft`
521
- * (this package) are the shared helpers for taking the body out.
615
+ * `get()`'s unwrap.
616
+ *
617
+ * ⛔ Do NOT hand `.item` to a gate or a write yourself: the body this serves
618
+ * is DECORATED (`_draft`, then `_diagnostics` from `decorateMetadataItem`),
619
+ * and the spec says a served body is not a valid input to the schema that
620
+ * produced it until those come off. {@link extractDraftBody} is the one
621
+ * reader for this method's answer — it takes the presence verdict and then
622
+ * the strip, in that order. That function used to be copy-pasted into four
623
+ * views and only one copy knew the rule (objectui#8181).
624
+ *
625
+ * Its tolerant siblings exist for the OTHER client method: `unwrapDraftBody`
626
+ * (app-shell) and `unwrapViewDraft` (this package) read a draft that arrived
627
+ * through {@link get}, which already unwrapped the envelope. They split on
628
+ * which method feeds them, not on what they mean — all three strip.
522
629
  */
523
630
  getDraft<T = unknown>(type: string, name: string, options?: {
524
631
  packageId?: string;
525
632
  }): Promise<T | null>;
633
+ /**
634
+ * Report the runtime authoring gate's advisory findings for a write the
635
+ * server has ALREADY committed (#4133 for the save door, #5026 for the
636
+ * publish door).
637
+ *
638
+ * Everything here is best-effort by construction, and that is the whole
639
+ * contract: the row is committed server-side before this runs, so neither a
640
+ * malformed body, nor a missing key, nor a throwing sink may change what the
641
+ * calling method returns or whether it throws. The server emits `advisories`
642
+ * ONLY when non-empty, so a clean write costs one absent-key check.
643
+ *
644
+ * One helper rather than a copy per door: every door's response declares the
645
+ * key identically, so a second inline copy could only ever drift. The batch
646
+ * door (`ce986aafc`) calls it once per `published[]` element, handing it
647
+ * the element — the object that carries the key there — and the same
648
+ * best-effort contract covers that loop unchanged.
649
+ */
650
+ private emitAdvisories;
526
651
  /**
527
652
  * Save (PUT) a metadata item. The framework accepts both the bare
528
653
  * item payload and the `{ item: ... }` / `{ metadata: ... }`
@@ -541,6 +666,61 @@ declare class MetadataClient {
541
666
  * the rows and reports under `seedApplied` — a data problem never fails the
542
667
  * publish, so callers should check `seedApplied?.success` and warn the user
543
668
  * rather than assume the data went live.
669
+ *
670
+ * Same door as {@link publish} (`POST /meta/:type/:name/publish`), so it
671
+ * reports the gate's advisories the same way (#5026).
672
+ *
673
+ * The BATCH door (`POST /packages/:id/publish-drafts`) is a different route
674
+ * with a method of its own — {@link publishPackageDrafts} — and it reports
675
+ * too. It did not when this paragraph was first written: it discarded
676
+ * per-draft advisories server-side, which was objectstack#9343, and that card
677
+ * has since landed with a ruling that each `published[]` element carries
678
+ * them (`ce986aafc` is the client half).
679
+ *
680
+ * What survives that change is the READING RULE, which is about this method
681
+ * rather than about the other route: it reads the top level of the
682
+ * single-item body and nothing else. A batch-shaped body arriving HERE still
683
+ * reports nothing, because `PublishMetaItemResponse` declares no
684
+ * `published[]` and a client that went looking for findings wherever they
685
+ * might be would be inventing a contract instead of reading one.
686
+ *
687
+ * ## Why there is no `{ success, data }` unwrapping here (`7b433197d`)
688
+ *
689
+ * This method used to unwrap a dispatcher-shaped envelope before returning,
690
+ * while {@link publish} — the same route, a couple of hundred lines down —
691
+ * did not. Two spellings of one wire contract, and nothing said which was
692
+ * right. The tolerance is gone because the route was MEASURED, at the
693
+ * producer rather than inferred from the response schema:
694
+ *
695
+ * - The only mount of `POST /api/v1/meta/:type/:name/publish` is the REST
696
+ * server's own route-manager registration, which answers
697
+ * `res.json(await protocol.publishMetaItem(...))` — the protocol's object
698
+ * verbatim, with no envelope branch. The project-scoped mirror
699
+ * (`/api/v1/environments/:environmentId/...`) re-mounts that same handler.
700
+ * - The `{ success, data }` envelope has exactly one producer, the runtime
701
+ * `HttpDispatcher`, and it does not serve this route: its `/meta` domain
702
+ * has no publish branch, the dispatcher's own route ledger has no row for
703
+ * it, and a three-segment `/meta` path that is not `/published` terminates
704
+ * in a located `routeNotFound`. So no composition — REST server, the
705
+ * dispatcher-only Hono adapter, or both — puts an envelope on this body.
706
+ * - `packages/spec` states the same split in the two docstrings side by
707
+ * side: `PublishMetaItemResponseSchema` describes "the FULL body" this
708
+ * route returns and records that the REST route hands the producer's
709
+ * object to `res.json()` verbatim, while its batch sibling
710
+ * `PublishPackageDraftsResponseSchema` describes a body answered
711
+ * explicitly "inside the dispatcher's `{ success, data }` envelope".
712
+ *
713
+ * ⛔ So an enveloped body on THIS route is not a shape the platform emits,
714
+ * and unwrapping one is not a kindness: it is Commandment #0.1's lenient
715
+ * fallback, a second de-facto contract for a route that declares one. It
716
+ * also fails in the direction that hides the problem — a body that arrived
717
+ * enveloped is one this door did not actually serve, and unwrapping it
718
+ * would present that as a successful promotion.
719
+ *
720
+ * ⚠️ Sibling tolerances in this file are NOT this defect and must stay:
721
+ * {@link listDrafts} reads `data?.data?.drafts` because `GET /meta/_drafts`
722
+ * IS a dispatcher route, and `publishHealthFromResponse` unwraps because the
723
+ * batch door really is enveloped. The rule is per-route, not per-file.
544
724
  */
545
725
  publishDraft(type: string, name: string): Promise<{
546
726
  success?: boolean;
@@ -552,6 +732,53 @@ declare class MetadataClient {
552
732
  errors?: unknown[];
553
733
  };
554
734
  } & Record<string, unknown>>;
735
+ /**
736
+ * Publish EVERY pending draft bound to one package — Studio's "publish whole
737
+ * app" door, `POST /api/v1/packages/:id/publish-drafts` (ADR-0033; the route
738
+ * that orders structure-before-seeds server-side and runs the ADR-0038 L3
739
+ * runtime probes).
740
+ *
741
+ * ## Why it is expressed here at all (`ce986aafc`)
742
+ *
743
+ * It was not, and that was the defect. Two app-shell call sites fired this
744
+ * route with a bare `fetch` / a page-private `apiJson`, outside the seam that
745
+ * covers every other metadata write — so when objectstack#9343 landed and the
746
+ * server began sending per-draft advisories, the author publishing a whole
747
+ * app was told nothing, while the SAME button's client-side capability lint
748
+ * still raised a toast. A door that cannot report is not a door the gate can
749
+ * reach, however loudly the server speaks.
750
+ *
751
+ * ## What it reports, and what it refuses to invent
752
+ *
753
+ * `PublishPackageDraftsResponseSchema` declares `advisories` on EACH
754
+ * `published[]` element — objectstack#9343's ruled shape, the same element
755
+ * type and the same omitted-when-empty discipline as the single-item door,
756
+ * and explicitly not a parallel top-level map. So this method emits one
757
+ * {@link MetadataSaveAdvisoryEvent} per advised element, each naming that
758
+ * element's own `type` / `name`, through the same sink, event and renderer
759
+ * the save and single-item publish doors use.
760
+ *
761
+ * ⛔ It renders only what the server sent where the schema says it sits:
762
+ *
763
+ * - `advisories` is read off the element, by the one shared reader, which
764
+ * drops anything that is not a complete finding.
765
+ * - An element that does not carry the `type` and `name` the schema states as
766
+ * REQUIRED is skipped rather than reported under invented identity — the
767
+ * event names the item the author has to go fix, and an event that cannot
768
+ * name it truthfully is worse than silence.
769
+ * - Nothing is derived, counted or summarised from the batch: no advisory
770
+ * exists here that the server did not put in the body.
771
+ *
772
+ * ## The failure shapes stay the callers'
773
+ *
774
+ * Non-2xx throws {@link MetadataError}, like every other method. A 2xx is
775
+ * RETURNED unexamined — `success: false` is not a failure on this route
776
+ * (`outcome: 'nothing_to_publish'` answers it too, and the spec's own text
777
+ * says to read `outcome`, not the boolean), and the two callers have their
778
+ * own, different rules for the refusal and rolled-back cases. Deciding that
779
+ * here would change behaviour this card is not about.
780
+ */
781
+ publishPackageDrafts(packageId: string): Promise<MetadataPublishPackageDraftsResult>;
555
782
  /**
556
783
  * Get the 3-state layered view of a metadata item: `code` (the packaged
557
784
  * artifact baseline), `overlay` (the tenant customisation row alone) and
@@ -615,9 +842,16 @@ declare class MetadataClient {
615
842
  * `{ success, version, seq, message }`. Throws a `404 no_draft` if
616
843
  * nothing is pending and `409 metadata_conflict` if the published
617
844
  * overlay moved while the draft was sitting.
845
+ *
846
+ * Reports the runtime authoring gate's advisory findings through
847
+ * {@link MetadataClientConfig.onSaveAdvisory} when the response carries them
848
+ * (#5026 / objectstack#9176), with `door: 'publish'`. This is the door the
849
+ * Studio designer takes on every edit, so it is the one whose findings an
850
+ * author actually sees.
618
851
  */
619
852
  publish<T = unknown>(type: string, name: string, options?: {
620
853
  message?: string;
854
+ packageId?: string;
621
855
  }): Promise<T>;
622
856
  /**
623
857
  * Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
@@ -911,6 +1145,227 @@ interface UserDataAdapter<T> {
911
1145
  */
912
1146
  declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
913
1147
 
1148
+ /**
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
1180
+ *
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".
1201
+ */
1202
+ declare function extractDraftBody(resp: unknown): Record<string, unknown> | null;
1203
+
1204
+ /**
1205
+ * One issue → a single field-anchored line: `• fields.amount.type — Required`.
1206
+ *
1207
+ * Exported so a caller that formats a LIST of failures — `app-shell`'s publish
1208
+ * formatter, whose `failed[]` entries carry their own `issues` — writes each
1209
+ * issue in this same grammar instead of keeping a second copy of it.
1210
+ */
1211
+ declare function formatMetadataIssue(i: MetadataValidationIssue): string;
1212
+ /**
1213
+ * Format a caught save/publish error for a banner/toast. When the error carries
1214
+ * spec-validation issues, list them (one field per line) so the user sees the
1215
+ * offending fields and what to do about them; otherwise fall back to the plain
1216
+ * message.
1217
+ */
1218
+ declare function formatMetadataError(e: unknown): string;
1219
+
1220
+ /**
1221
+ * The object-metadata write invariant, placed at the DOOR rather than at the
1222
+ * writers (objectui#8676).
1223
+ *
1224
+ * ## Why this module exists — the enumeration, not the count
1225
+ *
1226
+ * objectui#7714 ruled one client behaviour: *a half-filled relationship stays
1227
+ * client-side and the PUT body never carries a `lookup` without a non-empty
1228
+ * `reference`*. Its PR implemented that ruling by naming the writers it knew
1229
+ * about — two of them — and putting the assertion inside each one's array-to-map
1230
+ * conversion (`toFieldsMap`). The very failure the ruling was written against
1231
+ * was then reproduced on a THIRD writer neither conversion covers, in that
1232
+ * card's own required dogfood (fixed in `9073cf018`). objectui#8676 swept and
1233
+ * found nine more.
1234
+ *
1235
+ * The durable half of that card is a sentence about method, not a number:
1236
+ *
1237
+ * > a ruling that enumerates writers is only as good as the enumeration.
1238
+ *
1239
+ * A hand list of writers is stale the next time somebody adds one, and nothing
1240
+ * says so. So this invariant is not attached to writers at all. It is attached
1241
+ * to the DOORS — the places that put bytes on the wire — because the doors are a
1242
+ * CLOSED set that this repository owns, while the writers are an OPEN set it
1243
+ * does not. Three in-repo transports can PUT `/meta/:type/:name`, and
1244
+ * `scripts/check-object-metadata-write-doors.mjs` derives that set on every run
1245
+ * rather than restating it:
1246
+ *
1247
+ * 1. `MetadataClient.save` `packages/data-objectstack/src/metadata-client.ts`
1248
+ * 2. `importObjectDraft` `packages/app-shell/src/views/metadata-admin/external/api.ts`
1249
+ * 3. the `@objectstack/client` SDK's `meta.saveItem`, reached in this repo
1250
+ * through `MetadataService`'s one seam
1251
+ *
1252
+ * Every writer in the repo reaches the server through one of those three. Guard
1253
+ * them and the writer count stops mattering; add a fourth transport and the gate
1254
+ * turns red naming it. That is the whole design: ⛔ do not re-open this by
1255
+ * adding a writer list anywhere.
1256
+ *
1257
+ * ## What it asserts, and the two things it deliberately does NOT
1258
+ *
1259
+ * It asserts two field-level invariants, and nothing else:
1260
+ *
1261
+ * - objectui#7714's: a relationship (`lookup` / `master_detail`) carries a
1262
+ * usable `reference`;
1263
+ * - objectui#11253's: a choice (`select` / `radio`) carries an option source,
1264
+ * a non-empty `options` list or a shared `picklist`. This is the objectui
1265
+ * branch of the maintainer's ruling A on objectstack#20827, whose door
1266
+ * refuses "a `select` / `radio` with neither `options` nor `picklist`".
1267
+ *
1268
+ * ⛔ **NOT a client-side revalidation of the document.** Parsing the whole body
1269
+ * through `ObjectSchema` before the PUT would refuse plugin-registered keys the
1270
+ * SERVER accepts, and it would promote a client PREDICTION into a block — which
1271
+ * is exactly what objectui#4306 / objectui#6980 ruled against for the designer's
1272
+ * live Zod pass, on the stated ground that a schema issue on a draft the server
1273
+ * ACCEPTS would dead-bolt Save with no on-screen editor able to clear it.
1274
+ *
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`
1298
+ * keeps its type there and can be retyped or removed, and a stored `radio`
1299
+ * is among the types that page carries through unedited and sends to
1300
+ * 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.
1305
+ *
1306
+ * ⛔ **NOT strip-and-report-saved.** Dropping the offending field and reporting
1307
+ * success would trade a visible refusal for an invisible deletion —
1308
+ * objectstack#4001's shape, ruled out for this family in objectui#7714 and again
1309
+ * in `9073cf018`. It throws; the caller surfaces the message.
1310
+ *
1311
+ * ## The type lists are pinned to the contract, not to memory
1312
+ *
1313
+ * {@link RELATIONSHIP_TYPES_REQUIRING_REFERENCE} and
1314
+ * {@link CHOICE_TYPES_REQUIRING_OPTIONS} are the two lists this module keeps,
1315
+ * and they are the same class of hazard the module exists to close, so neither
1316
+ * gets to be a remembered list either.
1317
+ * `object-metadata-write-guard.derivation.test.ts` DERIVES both from the
1318
+ * installed `@objectstack/spec`:
1319
+ *
1320
+ * - the relationship set from every member of `FieldType`, parsed as
1321
+ * `{ type, label }` through `FieldSchema`, keeping those refused at path
1322
+ * `reference`;
1323
+ * - the choice set from the same members run through `checkFieldCompleteness`
1324
+ * (ADR-0078's author-time rule, which the ruling's door adopts), keeping
1325
+ * those it reports as `field/choice-without-options` at `error` severity.
1326
+ * `FieldSchema` cannot answer this one yet, because the door has not shipped.
1327
+ *
1328
+ * A spec release that changes either set turns that pin red instead of leaving
1329
+ * a guard that quietly stopped covering the contract.
1330
+ *
1331
+ * The derivation is a TEST and not a runtime probe on purpose: parsing 50
1332
+ * field-type documents on the way to every save is a cost paid on the hot path
1333
+ * to re-learn something that changes at most once per spec release, and a probe
1334
+ * that throws at runtime has to choose between blocking writes and failing open.
1335
+ * A pin has neither problem — it fails at CI time, loudly, with nothing at stake.
1336
+ */
1337
+ /**
1338
+ * Field types whose `reference` — the target object a relationship links to —
1339
+ * `@objectstack/spec` requires to be present and non-empty.
1340
+ *
1341
+ * Derived from the installed artifact by the pin named above, never recalled.
1342
+ */
1343
+ declare const RELATIONSHIP_TYPES_REQUIRING_REFERENCE: readonly string[];
1344
+ /**
1345
+ * Field types that offer nothing to choose without an option source — a
1346
+ * non-empty `options` list or a shared `picklist` (objectui#11253, the objectui
1347
+ * branch of the maintainer's ruling A on objectstack#20827).
1348
+ *
1349
+ * Derived from the installed artifact by the pin named above, never recalled.
1350
+ */
1351
+ declare const CHOICE_TYPES_REQUIRING_OPTIONS: readonly string[];
1352
+ /** The metadata type whose documents carry the `fields` map this guard reads. */
1353
+ declare const OBJECT_METADATA_TYPE = "object";
1354
+ /**
1355
+ * Refuse an object-metadata write whose body carries a relationship field with
1356
+ * no usable target, or a choice field with no option source.
1357
+ *
1358
+ * A no-op for every other metadata type and for any body with no readable
1359
+ * `fields` member — this door serves `view`, `app`, `flow`, `permission`,
1360
+ * `hook` and `dashboard` writes too, and has no opinion about them.
1361
+ *
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
1365
+ * @throws Error naming the offending field, before any request is issued
1366
+ */
1367
+ declare function assertObjectMetadataWritable(type: unknown, item: unknown, writer: string): void;
1368
+
914
1369
  /**
915
1370
  * Map human-readable filter operator names produced by SDUI view configs
916
1371
  * (e.g. `lead.view.ts`) to the canonical operator symbols expected by the
@@ -947,6 +1402,156 @@ declare class MalformedFilterError extends Error {
947
1402
  }
948
1403
  /** Detect the malformed-filter refusal, whether raised here or by the server. */
949
1404
  declare function isMalformedFilterError(error: unknown): boolean;
1405
+ /**
1406
+ * A `where` that reached `aggregate()`'s SPEC-SHAPE branch as an array the spec
1407
+ * cannot read as a filter — an unlowered `ViewFilterRule[]` above all.
1408
+ *
1409
+ * WHY THIS IS A REFUSAL AND NOT A LOWERING (objectui#6825, maintainer ruling
1410
+ * 2026-08-30, option A). The two branches of `aggregate()` take a filter by
1411
+ * different names: the analytics branch takes `filter` and lowers it through
1412
+ * `translateFilterArray` (objectui#6302), the spec-shape branch takes `where`
1413
+ * and posts it to `POST /data/:object/query` verbatim. Lowering here too was
1414
+ * option B and was REFUSED: it is the tolerant-consumer direction AGENTS.md
1415
+ * #0.1 exists to stop, and it would bless as supported a params shape
1416
+ * `AggregateParams` does not even declare. Retiring the branch was option D and
1417
+ * was refused as the riskier move — this repo has no producer that can reach it
1418
+ * (`ObjectChart`'s gate requires a non-array `aggregate.groupBy`, and all three
1419
+ * in-tree constructors pass a string), but `ObjectChart`'s props are `any` and
1420
+ * an out-of-repo host may already send this shape. So the branch stays, and it
1421
+ * says no out loud.
1422
+ *
1423
+ * What that buys is the FAILURE MODE, not a failure count. The receiving engine
1424
+ * already refuses these arrays ("is not a filter" — objectstack's
1425
+ * `engine-filter-array-lowering.test.ts`), so nothing that reaches this point
1426
+ * could have produced a correct number; the pre-existing behaviour was to find
1427
+ * that out on the wire, or worse, to have the predicate quietly dropped while
1428
+ * the chart rendered confident, wrong figures with no signal to their author.
1429
+ * This moves the same refusal to the producer, where the author can act on it.
1430
+ *
1431
+ * Carries the `INVALID_FILTER` / 400 pair its sibling {@link MalformedFilterError}
1432
+ * carries, so `isMalformedFilterError()` recognises it and a failed widget renders
1433
+ * "this filter is malformed" rather than "check your connection" (objectui#3066).
1434
+ */
1435
+ declare class UnloweredAggregateWhereError extends Error {
1436
+ readonly code = "INVALID_FILTER";
1437
+ readonly httpStatus = 400;
1438
+ /** The value as received, so a caller can log what its producer actually built. */
1439
+ readonly where: unknown;
1440
+ /** The object `aggregate()` was called for. */
1441
+ readonly resource: string;
1442
+ constructor(where: unknown, resource: string);
1443
+ }
1444
+ /**
1445
+ * The keys `aggregate()`'s ANALYTICS branch reads and its SPEC-SHAPE branch does
1446
+ * not: `filter`, `field`, `function`.
1447
+ *
1448
+ * Written out once, and read by both the refusal below and its message, so the
1449
+ * set a caller is told about cannot drift from the set that is checked.
1450
+ *
1451
+ * ⛔ Deliberately NOT "every key the spec-shape branch does not read". A refusal
1452
+ * scoped that widely would reject `orderBy`, a future spec key, or any harmless
1453
+ * extra a host happens to carry, and it would satisfy a naive "the spec-shape
1454
+ * branch refuses `filter`" test while breaking traffic nobody complained about.
1455
+ * These three are named one by one because they are the OTHER branch's own
1456
+ * parameters: their presence says the params were built for the analytics wire
1457
+ * and arrived on the spec-shape one, which is a producer defect with a name.
1458
+ */
1459
+ declare const ANALYTICS_ONLY_AGGREGATE_KEYS: readonly ["filter", "field", "function"];
1460
+ type AnalyticsOnlyAggregateKey = (typeof ANALYTICS_ONLY_AGGREGATE_KEYS)[number];
1461
+ /**
1462
+ * Analytics-branch params — `filter`, `field`, `function` — that reached
1463
+ * `aggregate()`'s SPEC-SHAPE branch, which reads none of them.
1464
+ *
1465
+ * WHY A REFUSAL AND NOT A DROP (`503cd8b89`). This applies the maintainer
1466
+ * ruling of 2026-08-30 on objectui#6825 — option A, REFUSE at the producer — to
1467
+ * the rest of the same branch. That ruling's reason was that a shape the spec's
1468
+ * own gate would reject is off-contract at the PRODUCER, so the adapter says no
1469
+ * instead of degrading leniently; the reason lands harder here, because a
1470
+ * silent drop is precisely the disposition it refused for `where`. Leaving one
1471
+ * key on this branch refusing while three others vanish without a word would be
1472
+ * harder to explain than the old behaviour.
1473
+ *
1474
+ * ⭐ AND THE DROP HERE IS WORSE THAN THE `where` HALF. `filter` is not the only
1475
+ * casualty: `field` and `function` are the analytics branch's whole measure, and
1476
+ * the spec-shape branch copies a measure only out of `aggregations`. So the
1477
+ * legacy shape `{ field, function, groupBy, filter }` whose `groupBy` happens to
1478
+ * be an ARRAY produced a `queryAst` carrying a `groupBy` and NO `aggregations`
1479
+ * at all — a grouping with no measure — with the author's predicate gone too.
1480
+ * The chart still rendered, the numbers were wrong, and there was nothing to
1481
+ * look at.
1482
+ *
1483
+ * HOW A LEGACY CALLER GETS HERE, measured in this repo on 2026-09-08.
1484
+ * `Array.isArray(params.groupBy)` is one of `looksLikeSpecShape`'s three
1485
+ * disjuncts, and `ObjectChart`'s `runAggregate` gates only on the STRUCTURED
1486
+ * node shape (`gb && typeof gb === 'object' && !Array.isArray(gb)`), so an
1487
+ * ARRAY `aggregate.groupBy` falls through to its legacy call — `{ field,
1488
+ * function, groupBy, filter }` — and that call lands here.
1489
+ * `ObjectMetricWidget.computeOne` forwards `aggregate.groupBy` the same way.
1490
+ * Both take the value from authored widget metadata across an `any` seam
1491
+ * (`isObjectProvider`'s `aggregate?: any`, `ds: any`), so the type system
1492
+ * cannot refuse it and this runtime refusal is what an author gets instead.
1493
+ *
1494
+ * ⛔ WHAT THIS DELIBERATELY DOES NOT DO. It does not ROUTE the legacy shape back
1495
+ * to the analytics branch: that is the tolerant-consumer direction AGENTS.md
1496
+ * §0.1 exists to stop, it is the option #6825 refused for `where`, and it could
1497
+ * not work anyway — the analytics branch posts `dimensions: [params.groupBy]`,
1498
+ * so an array `groupBy` would go out nested. It does not widen `AggregateParams`
1499
+ * (`@object-ui/types`), which is a separate contract question. And it does not
1500
+ * refuse a key that is present but nullish: `undefined`/`null` carries nothing
1501
+ * for the branch to drop, and both `ObjectChart` and `ObjectMetricWidget` build
1502
+ * their params by spreading possibly-absent authored values.
1503
+ *
1504
+ * Carries the `INVALID_FILTER` / 400 pair its siblings {@link MalformedFilterError}
1505
+ * and {@link UnloweredAggregateWhereError} carry, so `isMalformedFilterError()`
1506
+ * recognises it and a failed widget renders "this filter is malformed" rather
1507
+ * than "check your connection" (objectui#3066). One branch, one envelope: a
1508
+ * caller can catch `aggregate()`'s producer-side refusals with one predicate.
1509
+ */
1510
+ declare class AnalyticsKeysOnSpecShapeError extends Error {
1511
+ readonly code = "INVALID_FILTER";
1512
+ readonly httpStatus = 400;
1513
+ /** The offending keys, in the order {@link ANALYTICS_ONLY_AGGREGATE_KEYS} lists them. */
1514
+ readonly keys: readonly AnalyticsOnlyAggregateKey[];
1515
+ /** What each offending key carried, so a producer is identifiable from a log. */
1516
+ readonly received: Record<string, unknown>;
1517
+ /** The object `aggregate()` was called for. */
1518
+ readonly resource: string;
1519
+ constructor(keys: readonly AnalyticsOnlyAggregateKey[], params: any, resource: string);
1520
+ }
1521
+ /**
1522
+ * An ARRAY `filter` that reached `aggregate()`'s ANALYTICS branch and that the
1523
+ * protocol's lowering sink cannot turn into a `FilterCondition` — an infix join
1524
+ * (`[condA, 'or', condB]`) above all.
1525
+ *
1526
+ * WHY A REFUSAL AND NOT A DROP (objectui#7752; maintainer ruling of 2026-09-05
1527
+ * on objectstack#15828, which reads that card as a frontend defect: the
1528
+ * protocol is right and the adapter was wrong). `parseFilterAST` answers
1529
+ * `undefined` for an array it cannot read, and `undefined` here would post a
1530
+ * body with NO `where` at all: the aggregate would then be computed over every
1531
+ * row and returned as a confident number, with the author's predicate gone and
1532
+ * nothing on the wire to say it went. That is the same silent over-fetch
1533
+ * {@link MalformedFilterError} exists to stop, one door further along.
1534
+ *
1535
+ * Deliberately NOT {@link UnloweredAggregateWhereError}, whose meaning is not
1536
+ * widened to cover this: that one guards the SPEC-SHAPE branch, whose `where`
1537
+ * is posted verbatim and must therefore have been lowered by its PRODUCER
1538
+ * (objectui#6825, ruling 2026-08-30 option A — refuse, never lower). This
1539
+ * branch's `filter` is authoring input and IS lowered here, at the door the
1540
+ * protocol names; this error covers only the input that sink cannot lower.
1541
+ *
1542
+ * Carries the `INVALID_FILTER` / 400 pair both siblings carry, so
1543
+ * `isMalformedFilterError()` recognises it and a failed widget renders "this
1544
+ * filter is malformed" rather than "check your connection" (objectui#3066).
1545
+ */
1546
+ declare class UnlowerableAnalyticsFilterError extends Error {
1547
+ readonly code = "INVALID_FILTER";
1548
+ readonly httpStatus = 400;
1549
+ /** The filter as received, so a caller can log what its producer actually built. */
1550
+ readonly filter: unknown;
1551
+ /** The object `aggregate()` was called for. */
1552
+ readonly resource: string;
1553
+ constructor(filter: unknown, resource: string, detail?: string);
1554
+ }
950
1555
  /**
951
1556
  * Serialize a `$orderby` to the server's `sort` shorthand
952
1557
  * (`field,-other_field`), for every shape `QueryParams['$orderby']` declares.
@@ -979,7 +1584,7 @@ declare function getSharedDiscovery(baseUrl: string, fetcher: () => Promise<unkn
979
1584
  declare function clearSharedDiscoveryCache(): void;
980
1585
  /**
981
1586
  * Read the cross-object atomic-batch capability from a `discovery` document
982
- * (framework #3298 / objectui #2693). The server advertises it hierarchically
1587
+ * (objectstack-ai/objectstack#3298 / objectui #2693). The server advertises it hierarchically
983
1588
  * under `capabilities.transactionalBatch.enabled`; the published
984
1589
  * `@objectstack/client` also accepts the flat `capabilities.transactionalBatch:
985
1590
  * boolean` form and normalizes the two — mirror that here so the adapter reads
@@ -992,7 +1597,7 @@ declare function clearSharedDiscoveryCache(): void;
992
1597
  * real error.
993
1598
  * - `false` — the backend explicitly does NOT (route absent, or a runtime that
994
1599
  * can't open a transaction).
995
- * - `undefined` — the capability is absent, i.e. the backend predates #3298;
1600
+ * - `undefined` — the capability is absent, i.e. the backend predates objectstack-ai/objectstack#3298;
996
1601
  * the caller must keep the legacy runtime-probe fallback (we can't tell
997
1602
  * whether `/batch` exists without trying it).
998
1603
  */
@@ -1045,22 +1650,38 @@ declare const API_ACCESS_DENIED_CODES: readonly ["OBJECT_API_DISABLED", "OBJECT_
1045
1650
  declare function isApiAccessDeniedError(error: unknown): boolean;
1046
1651
  /**
1047
1652
  * What the by-name meta app route said about THIS session's access to an app
1048
- * (objectui#4252 / objectstack#8013).
1049
- *
1050
- * - `granted` — the route served the app document.
1051
- * - `denied` — the app EXISTS and the session lacks its `requiredPermissions`.
1052
- * The only verdict a caller may render as an authorization refusal.
1053
- * - `unknown` — anything else: an absent app, an unpublished one, an app
1054
- * withheld by an absent optional service, an unreachable server, an adapter
1055
- * that cannot ask. All of these are cases where the server declined to say
1056
- * that a permission of the caller's is missing, so no caller may claim it.
1057
- *
1058
- * Three values rather than a boolean because the third is not a shade of the
1059
- * other two: "the app is missing" and "I could not find out" both have to leave
1060
- * the caller's existing copy alone, and collapsing them into `false` invites a
1061
- * consumer to read a failed probe as a positive absence.
1653
+ * (objectui#4252 / objectstack#8013, widened by objectui#9262).
1654
+ *
1655
+ * - `granted` — the route served the app document.
1656
+ * - `denied` — the app EXISTS and the session lacks its
1657
+ * `requiredPermissions`. The only verdict a caller may render as an
1658
+ * authorization refusal.
1659
+ * - `not_found` — the route answered, and what it answered is that it has no
1660
+ * app at this name for this session. WHICH absence it is — never created, a
1661
+ * typo, an unpublished draft, an app gated by an absent optional service —
1662
+ * is deliberately NOT distinguished: the control plane answers the same way
1663
+ * for every app the caller may not see, and the 2026-08-12 ruling keeps it
1664
+ * that way. A caller may say the app cannot be opened; it may never say why.
1665
+ * - `unreachable` — the probe could not obtain an answer at all: the transport
1666
+ * failed, the server erred, the request timed out. Says nothing about the
1667
+ * app.
1668
+ * - `unknown` — nothing was measured, because nothing could be asked: a
1669
+ * host DataSource that does not implement this probe (the console is
1670
+ * protocol-agnostic — AGENTS #1), or no app name to ask about.
1671
+ *
1672
+ * ## Why `not_found` and `unreachable` are separate members (objectui#9262)
1673
+ *
1674
+ * They were one `unknown` before, and the screen above them therefore asserted
1675
+ * a transient publish state over both. The distinction is not new information
1676
+ * to obtain — it is already on the wire, and folding it away was the defect.
1677
+ * The maintainer ruling (2026-09-13) named the two members; this type is the
1678
+ * two-member widening that ruling declared.
1679
+ *
1680
+ * ⚠️ `unknown` no longer means "absent". A consumer that treats `unknown` as a
1681
+ * positive absence is reading a verdict that now means the opposite: that the
1682
+ * question was never put.
1062
1683
  */
1063
- type AppAccessVerdict = 'granted' | 'denied' | 'unknown';
1684
+ type AppAccessVerdict = 'granted' | 'denied' | 'not_found' | 'unreachable' | 'unknown';
1064
1685
  /**
1065
1686
  * The ADR-0112 standard catalog code the by-name meta app route answers with
1066
1687
  * when an app exists and the session lacks its `requiredPermissions`
@@ -1084,9 +1705,87 @@ declare const APP_PERMISSION_DENIED_CODE = "PERMISSION_DENIED";
1084
1705
  * are errors.
1085
1706
  */
1086
1707
  declare function isAppPermissionDeniedError(error: unknown): boolean;
1708
+ /**
1709
+ * The ADR-0112 standard catalog code the metadata routes answer with when the
1710
+ * named item is not there for this caller (`sendError(res, 404,
1711
+ * 'RESOURCE_NOT_FOUND', …)` in `packages/rest/src/rest-server.ts`).
1712
+ *
1713
+ * One code covers every absence on purpose: a nonexistent name, an unpublished
1714
+ * app (ADR-0045 §3 keeps it externally unobservable), and an app gated by an
1715
+ * absent optional service (ADR-0057 D10 — nothing was denied to the CALLER) are
1716
+ * one answer by the 2026-08-12 ruling's design, not three that happen to
1717
+ * collide. Consumers may report the absence; they may not report a reason.
1718
+ */
1719
+ declare const META_ITEM_ABSENT_CODE = "RESOURCE_NOT_FOUND";
1720
+ /**
1721
+ * True when `error` is a metadata route's "not here for you" answer.
1722
+ *
1723
+ * Discriminates on the ADR-0112 `code`, never on the status, for the same
1724
+ * reason {@link isAppPermissionDeniedError} does (objectui#4408): a 404 is a
1725
+ * transport fact many conditions share — a mis-typed route, a proxy, an
1726
+ * appliance in front of the server — while the code is the contract. A 404 that
1727
+ * carries no declared code is NOT an absence: nobody said the item was missing,
1728
+ * so the honest reading is that no answer was obtained.
1729
+ */
1730
+ 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;
1087
1786
  /**
1088
1787
  * Thrown when the deployment has no analytics capability installed
1089
- * (framework#3891 / #4019).
1788
+ * (framework#3891 / objectstack-ai/objectstack#4019).
1090
1789
  *
1091
1790
  * The framework retired its degraded in-kernel analytics fallback — it dropped
1092
1791
  * the caller's RLS/tenant scope and ignored the contract filter, so it answered
@@ -1194,6 +1893,41 @@ declare class AnalyticsUnauthenticatedError extends Error {
1194
1893
  serverMessage?: string;
1195
1894
  });
1196
1895
  }
1896
+ /**
1897
+ * Thrown when the dataset query was refused because this user may not READ an
1898
+ * object the dataset queries — `403` + ADR-0112 `PERMISSION_DENIED`, which
1899
+ * `@objectstack/service-analytics`' read admission (`readAdmissionDeniedError`)
1900
+ * answers with the same code and status the data API gives the same user for
1901
+ * the same object (objectui#10899).
1902
+ *
1903
+ * The FOURTH branch. It used to fall through to the generic
1904
+ * `Dataset query failed: 403 Forbidden — [Analytics] Access denied: …` string,
1905
+ * which a dashboard tile then rendered verbatim: a raw English exception where
1906
+ * the list view over the same object shows its localized "no access" state.
1907
+ * That generic string names a transport status where a person needs a fact
1908
+ * ("you can't see this data"), and it is the one case a renderer must be able
1909
+ * to tell apart — so it is typed, and it carries `httpStatus` + a `code` the
1910
+ * shared `classifyLoadError` reads as `forbidden` without any string matching.
1911
+ *
1912
+ * `serverMessage` is kept for diagnostics only. The producer deliberately
1913
+ * names the object and nothing else, but it is still platform prose, not text
1914
+ * addressed to the end user.
1915
+ */
1916
+ declare class AnalyticsForbiddenError extends Error {
1917
+ readonly code = "PERMISSION_DENIED";
1918
+ readonly httpStatus = 403;
1919
+ /** The server's own ADR-0112 code — the field this branch was chosen BY. */
1920
+ readonly serverCode?: string;
1921
+ /** The server's own message, verbatim (diagnostics only). */
1922
+ readonly serverMessage?: string;
1923
+ /** The dataset the refused query asked for, when it named one. */
1924
+ readonly datasetName?: string;
1925
+ constructor(opts?: {
1926
+ serverCode?: string;
1927
+ serverMessage?: string;
1928
+ datasetName?: string;
1929
+ });
1930
+ }
1197
1931
  /**
1198
1932
  * Thrown when the server REJECTED the analytics query body (HTTP 400 —
1199
1933
  * `VALIDATION_FAILED` since framework#4010 validates `/analytics/query` at the
@@ -1210,6 +1944,49 @@ declare class AnalyticsQueryRejectedError extends Error {
1210
1944
  readonly serverCode?: string;
1211
1945
  constructor(detail?: string, serverCode?: string);
1212
1946
  }
1947
+ /**
1948
+ * Thrown by `aggregate()` when `client.analytics.query` resolves to anything
1949
+ * other than the post-unwrap `AnalyticsResult` — concretely, when the value it
1950
+ * hands back has no `rows` array (objectui#7028).
1951
+ *
1952
+ * ONE SPELLING, BY RULING. `@objectstack/client` 17.3.0 converged
1953
+ * `analytics.query` on `unwrapResponse` (objectstack#13079, maintainer ruling
1954
+ * 2026-08-31, option A): the method resolves to the payload, and the caller
1955
+ * reads `result.rows`. The same ruling ordered this adapter's tolerant row
1956
+ * ladder tightened in the same wave, so that defensive code is not read as a
1957
+ * contract. The ladder also read a bare array, `data` as an array,
1958
+ * `data.data.rows` and `results`; `rows` is now the only spelling read, and
1959
+ * everything else lands here.
1960
+ *
1961
+ * WHY A THROW AND NOT `[]`. The ladder answered every shape it did not
1962
+ * recognise with an empty array, and an empty array is a RESULT: a KPI renders
1963
+ * a confident zero and a chart reads "no data" over a populated table — the
1964
+ * lie objectui#5954 removed on the failure side. The envelope arriving here
1965
+ * means the SDK in front of this call predates the convergence (it is older
1966
+ * than 17.3.0), or something between it and `POST /analytics/query` wraps the
1967
+ * payload a second time. Either way the adapter is handed a contract it no
1968
+ * longer reads, and saying so is the only answer that names the repair.
1969
+ *
1970
+ * WHY NOT THE CLIENT-SIDE FALLBACK. Analytics answered; this is not a
1971
+ * transport failure. Degrading would put plausible numbers from a different
1972
+ * code path over a contract violation — the misdirection
1973
+ * {@link AnalyticsQueryRejectedError} refuses for the same reason
1974
+ * (framework#3878) — so `aggregate()`'s catch rethrows this before it
1975
+ * classifies anything.
1976
+ */
1977
+ declare class AnalyticsResultShapeError extends Error {
1978
+ readonly code = "ANALYTICS_RESULT_SHAPE_INVALID";
1979
+ /** The object `aggregate()` was called for. */
1980
+ readonly resource: string;
1981
+ /**
1982
+ * True when the value is the envelope the client stopped handing back at
1983
+ * 17.3.0 — `{ success, data: { rows } }`, or its `{ data: { rows } }` core.
1984
+ */
1985
+ readonly envelope: boolean;
1986
+ /** What the value was, so the producer is identifiable from a log. */
1987
+ readonly received: string;
1988
+ constructor(resource: string, result: unknown);
1989
+ }
1213
1990
  /**
1214
1991
  * Classify a FAILED analytics call so the caller knows whether to degrade or
1215
1992
  * to surface the failure.
@@ -1257,11 +2034,18 @@ declare class AnalyticsQueryRejectedError extends Error {
1257
2034
  * `ROUTE_NOT_FOUND` (framework#4019 stops mounting the routes at all).
1258
2035
  * A client-side aggregate over a scoped `find()` answers the chart
1259
2036
  * correctly, and the operator is told once that the semantic layer is off.
1260
- * - **`rejected`** — *THROW*. The server refused OUR body (400
1261
- * `VALIDATION_FAILED`; framework#4010 validates `/analytics/query` at the
1262
- * entry). Degrading would answer our own contract violation with plausible
1263
- * numbers from a different code path and bury it — the misdirection
1264
- * framework#3878 documented.
2037
+ * - **`rejected`** — *THROW*. The server refused OUR body: 400
2038
+ * `VALIDATION_FAILED` (framework#4010 validates `/analytics/query` at the
2039
+ * entry) OR any other coded 400 (objectui#7755 — e.g. `service-analytics`
2040
+ * ships its own 400 `INVALID_FILTER` on a filter shape it refuses). The
2041
+ * status alone is decisive once it is 400: a producer that refuses the
2042
+ * body and ships a code this consumer does not enumerate is refusing it
2043
+ * exactly as hard as one that ships `VALIDATION_FAILED` — the refusal is
2044
+ * in the transport fact, not in which code spells it. Degrading would
2045
+ * answer our own contract violation with plausible numbers from a
2046
+ * different code path (`find()`'s `$filter` accepts array shapes the
2047
+ * analytics body does not) and bury it — the misdirection framework#3878
2048
+ * documented.
1265
2049
  * - **`unauthenticated`** — *THROW*. 401 `UNAUTHENTICATED`: the request was
1266
2050
  * refused before it ran, so it is evidence about the SESSION and none at
1267
2051
  * all about the capability. Degrading is not merely misleading here, it is
@@ -1309,16 +2093,48 @@ declare class ConcurrentUpdateError extends Error {
1309
2093
  readonly httpStatus = 409;
1310
2094
  readonly currentVersion: string | null;
1311
2095
  readonly currentRecord: unknown;
2096
+ /**
2097
+ * The refusal text the PRODUCER marked as addressed to the end user
2098
+ * (`ApiErrorSchema.userMessage`, objectstack `79c46da90`), or `null` when the
2099
+ * refusal carried no marking.
2100
+ *
2101
+ * Declared on the class because this error has no `details` bag: the shared
2102
+ * reader (`declaredUserMessage` in `@object-ui/react`) looks at the
2103
+ * top-level field first, so parking it here is what makes the marking
2104
+ * readable at the surface. The contract is status-agnostic — a 409 carries
2105
+ * it exactly like a 400 or a 403 does.
2106
+ */
2107
+ readonly userMessage: string | null;
1312
2108
  constructor(opts: {
1313
2109
  currentVersion: string | null;
1314
2110
  currentRecord: unknown;
1315
2111
  message?: string;
2112
+ userMessage?: string | null;
1316
2113
  });
1317
2114
  }
1318
2115
  /**
1319
2116
  * Detect "concurrent update" errors raised by the platform. The wire
1320
2117
  * shape is `409` + `code: 'CONCURRENT_UPDATE'`. The client surfaces
1321
2118
  * extra details on `error.details` (full response body).
2119
+ *
2120
+ * Accepts EITHER that wire `code` OR `name === 'ConcurrentUpdateError'`, and
2121
+ * reads `httpStatus` for neither. This paragraph exists because the doc used
2122
+ * to name only the wire shape, which left the `name` limb reading as drift
2123
+ * (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
2129
+ * consumer census cannot see the case the limb was written for and is not
2130
+ * evidence against it; `@object-ui/plugin-form` and `@object-ui/plugin-detail`
2131
+ * each carry their own copy of the same two-limb check for adapters they must
2132
+ * not depend on.
2133
+ *
2134
+ * Deliberately WIDER than {@link normaliseClientError}'s re-wrap, which keys
2135
+ * on the wire `code` alone: an error carrying only the class name is
2136
+ * recognised here and passed through there. Both accepted sets are pinned in
2137
+ * `occ.test.ts`.
1322
2138
  */
1323
2139
  declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpdateError;
1324
2140
  /**
@@ -1331,6 +2147,12 @@ declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpd
1331
2147
  * - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the
1332
2148
  * server's per-field entries so a form can mark the offending inputs
1333
2149
  * instead of showing one undirected toast.
2150
+ *
2151
+ * Both re-wraps preserve the producer's `userMessage` marking (see
2152
+ * {@link liftUserMessage}). Re-wrapping used to drop it, which made the
2153
+ * marking unreadable at every surface downstream of this boundary — nothing
2154
+ * threw, the typed error was otherwise correct, and the user simply got a
2155
+ * generic string.
1334
2156
  */
1335
2157
  declare function normaliseClientError(error: unknown): unknown;
1336
2158
  /**
@@ -1408,20 +2230,295 @@ type ConnectionStateListener = (event: ConnectionStateEvent) => void;
1408
2230
  */
1409
2231
  type BatchProgressListener = (event: BatchProgressEvent) => void;
1410
2232
 
2233
+ /**
2234
+ * The `reason` of a write-strip this bundle's spec pin cannot name
2235
+ * (objectui#4934).
2236
+ *
2237
+ * Deliberately NOT a spec spelling: it is namespaced so it can never collide
2238
+ * with an arm `@objectstack/spec` adds later (a collision would merge real
2239
+ * reasons into the skew bucket — the failure this whole card is about, one level
2240
+ * up). `droppedFieldsReason.boundary.test.ts` pins that the installed spec does
2241
+ * not declare it.
2242
+ */
2243
+ declare const UNRECOGNIZED_DROP_REASON = "objectui:unrecognized-drop-reason";
2244
+ /**
2245
+ * The skew arm: one server-reported write-strip whose `reason` is outside the
2246
+ * enum this bundle's `@objectstack/spec` pin declares (objectui#4934).
2247
+ *
2248
+ * A deployed client normally runs BEHIND the server it talks to, so a reason
2249
+ * from the future is the expected skew direction, not a corrupt payload. The
2250
+ * boundary used to assert such an entry into {@link DroppedFieldsEvent} on shape
2251
+ * alone — `reason` was never read — so the interior was typed to trust a union
2252
+ * nothing had checked, and the next consumer to write an exhaustive-looking
2253
+ * table over `DroppedFieldsEvent['reason']` would have been handed a value that
2254
+ * type says is impossible.
2255
+ *
2256
+ * Three properties of this shape are load-bearing:
2257
+ *
2258
+ * - The entry is **kept**. Dropping it would tell the user nothing about fields
2259
+ * the server really did strip — precisely the silence objectui#3484 removed.
2260
+ * - The wire value is **preserved verbatim** in {@link unrecognizedReason},
2261
+ * never coerced or normalised onto a known arm: claiming `readonly` for a
2262
+ * reason we cannot name is a false statement about the user's data, and it is
2263
+ * also unfalsifiable once the original value is gone.
2264
+ * - `reason` carries {@link UNRECOGNIZED_DROP_REASON}, which is not assignable
2265
+ * to `DroppedFieldsEvent['reason']`. That is what makes the skew case visible
2266
+ * to `tsc` at every consumer instead of resting on N per-consumer
2267
+ * disciplines — and it keeps the spec type as the canonical arm rather than
2268
+ * widening the whole surface to `string` (objectui#3160).
2269
+ */
2270
+ interface UnrecognizedDropReasonEvent {
2271
+ object?: string;
2272
+ fields: string[];
2273
+ reason: typeof UNRECOGNIZED_DROP_REASON;
2274
+ /** Whatever the server sent, untouched — including a non-string or nothing at all. */
2275
+ unrecognizedReason: unknown;
2276
+ }
2277
+ /**
2278
+ * One entry of a write-warning: either the spec type (canonical arm) or the
2279
+ * named skew arm above. Narrow with `entry.reason === UNRECOGNIZED_DROP_REASON`.
2280
+ */
2281
+ type DroppedFieldsNotice = DroppedFieldsEvent | UnrecognizedDropReasonEvent;
1411
2282
  /**
1412
2283
  * Emitted after a create/update whose response carried `droppedFields`
1413
- * (framework #3431/#3455). The write SUCCEEDED — this is a warning that some
2284
+ * (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455). The write SUCCEEDED — this is a warning that some
1414
2285
  * supplied fields never landed, so the UI can tell the user rather than let it
1415
2286
  * pass silently. Subscribe via {@link ObjectStackAdapter.onWriteWarning}.
2287
+ *
2288
+ * `droppedFields` is the two-arm {@link DroppedFieldsNotice} and not
2289
+ * `DroppedFieldsEvent[]`: the wire is parsed here, and an entry whose `reason`
2290
+ * this bundle's spec pin cannot name arrives on the explicit skew arm rather
2291
+ * than being asserted into the union (objectui#4934).
1416
2292
  */
1417
2293
  interface WriteWarningEvent {
1418
2294
  operation: 'create' | 'update';
1419
2295
  resource: string;
1420
2296
  id?: string | number;
1421
- droppedFields: DroppedFieldsEvent[];
2297
+ droppedFields: DroppedFieldsNotice[];
1422
2298
  }
1423
2299
  /** Event listener type for write-warning (dropped-fields) events. */
1424
2300
  type WriteWarningListener = (event: WriteWarningEvent) => void;
2301
+ /**
2302
+ * What a FAILED `listImportMappings` read actually was (objectui#7741).
2303
+ *
2304
+ * - `not-served` — this deployment does not serve the `mapping` kind, or has
2305
+ * no `/meta` door at all. QUIET: a real, supported deployment shape, and the
2306
+ * empty list plus a hidden selector is the correct rendering of it.
2307
+ * - `refused` — the server answered and declined THIS caller: anonymous,
2308
+ * lapsed token, missing permission, method withheld. LOUD, and the remedy
2309
+ * names a person: sign in again, or ask for the grant.
2310
+ * - `unreadable` — the read could not be completed for any other reason: a
2311
+ * 5xx, a network failure, a code this consumer cannot name. LOUD, and the
2312
+ * remedy is to retry or report.
2313
+ *
2314
+ * The last two are the same verdict for the user — *we could not find out* —
2315
+ * and they are separated because the sentence that helps differs. What they
2316
+ * share is what matters: neither is evidence that no mapping is registered.
2317
+ *
2318
+ * ⚠️ The three NAMES are shared by every classifier feeding
2319
+ * {@link MetadataReadWarningEvent}; the POPULATION each name covers is not, and
2320
+ * is decided per read door. `not-served` in particular is a strictly smaller
2321
+ * set on `view` than on `mapping` — see {@link classifyViewsFailure}.
2322
+ */
2323
+ type MetadataReadFailureKind = 'not-served' | 'refused' | 'unreadable';
2324
+ /**
2325
+ * The published spelling of {@link MetadataReadFailureKind}, kept as the name
2326
+ * objectui#7741 shipped (`@object-ui/data-objectstack@17.2.0`).
2327
+ *
2328
+ * An ALIAS, not a second declaration: identical members, so every existing
2329
+ * consumer's assignability is unchanged in both directions. It was renamed
2330
+ * because the channel gained a second emitter (objectui#8151) and a type called
2331
+ * `ImportMappingsFailureKind` describing a `view` read would be a name that
2332
+ * lies; the old name stays because retiring a published one is not this card's
2333
+ * business.
2334
+ */
2335
+ type ImportMappingsFailureKind = MetadataReadFailureKind;
2336
+ /**
2337
+ * Classify a FAILED `meta.getItems('mapping')` call so the caller knows whether
2338
+ * to stay quiet or to say something (objectui#7741).
2339
+ *
2340
+ * ## Why this function exists
2341
+ *
2342
+ * `listImportMappings` degrades every failure to `[]`, and the wizard hides its
2343
+ * saved-mapping selector on an empty list. So "the server served zero mappings"
2344
+ * and "the server never answered" rendered identically — the same UI, on every
2345
+ * deployment, with a `console.warn` as the only discriminator. That silence did
2346
+ * not merely hide a fault: it produced a confident WRONG diagnosis in a careful
2347
+ * reporter (objectstack#14026 was filed, routed and worked by two seats against
2348
+ * a repo whose wizard had been correct since `@object-ui/data-objectstack`
2349
+ * 17.1.0), and the misdiagnosis travelled further than the fault would have.
2350
+ *
2351
+ * This is the discrimination framework objectstack#13906 decision 1 option A already
2352
+ * adopted at the tenancy-posture seam — *a thing that could not be READ is not
2353
+ * a thing that is ABSENT* — applied here. It is not a new principle.
2354
+ *
2355
+ * ## It reads the ERROR, never the result
2356
+ *
2357
+ * The one thing this must never do is decide from "is the result an empty
2358
+ * array". An empty array is what BOTH conditions produce, so a test on it can
2359
+ * never fail and can never separate *served zero* from *did not serve* — a
2360
+ * reading that cannot fail is indistinguishable from a reading that passed.
2361
+ * Every branch below reads `err` itself: the ADR-0112 `code` first (the
2362
+ * contract), the status only where no code was declared (a transport fact many
2363
+ * conditions share). Same rule, same order, and for the same reasons as
2364
+ * {@link classifyAnalyticsFailure} (objectui#5663 / objectui#5721).
2365
+ *
2366
+ * `@objectstack/client`'s fetch wrapper hands both envelope families here
2367
+ * already flattened — `errorBody?.code ?? errorBody?.error?.code` onto
2368
+ * `error.code`, plus `error.httpStatus = res.status` — so no envelope reading
2369
+ * belongs in this function. Comparisons go through `errorCodeIs`/
2370
+ * `errorCodeIsAnyOf` so the pre- and post-ADR-0112 spellings both match.
2371
+ */
2372
+ declare function classifyImportMappingsFailure(error: unknown): {
2373
+ kind: ImportMappingsFailureKind;
2374
+ code?: string;
2375
+ status?: number;
2376
+ message?: string;
2377
+ };
2378
+ /**
2379
+ * Classify a FAILED `view` metadata list read so {@link ObjectStackAdapter.listViews}
2380
+ * knows whether to stay quiet or to say something (objectui#8151).
2381
+ *
2382
+ * ## Why this is a SEPARATE reading and not a second caller of
2383
+ * {@link classifyImportMappingsFailure}
2384
+ *
2385
+ * The two methods share a defect and a remedy, not a population. The quiet arm
2386
+ * `listImportMappings` is built around — *an older server that does not serve
2387
+ * this kind* — has no members on `view`, and carrying it over would put a fresh
2388
+ * swallow into the method this card exists to un-swallow. Measured, on the
2389
+ * framework tree, three ways:
2390
+ *
2391
+ * 1. **`view` is in the platform's static spelling contract**, so the metadata
2392
+ * LIST door's unknown-kind refusal is unreachable for it.
2393
+ * `RestServer.refuseUnknownMetaListType` (framework#9488,
2394
+ * `packages/rest/src/rest-server.ts`) returns without writing a refusal
2395
+ * whenever `unrecognisedMetaTypeRefusal(urlType)` is `null`, and that
2396
+ * predicate answers `null` for every spelling in the contract.
2397
+ * `packages/spec/src/meta-spelling/meta-url-data.generated.ts` carries both
2398
+ * `"views": "view"` and the canonical singular `view`. So a `400`
2399
+ * `INVALID_REQUEST` on `GET /meta/view` is NEVER that door saying "this
2400
+ * deployment carries no such kind" — it is some other refusal of the
2401
+ * request, and reading it as kind-absence would silence a real one.
2402
+ * 2. **There is no "before" for `view` to be older than.** `mapping`'s quiet
2403
+ * arm exists because `mapping` was PROMOTED into the declared set
2404
+ * (framework#2611), so servers predating the promotion are a real, shipped
2405
+ * population. `view` is the kind the metadata surface is built around — the
2406
+ * compound-arity door `/meta/<object>/views/<name>`, the ADR-0017
2407
+ * `ViewItem` discriminant this very method filters on.
2408
+ * 3. **A deployment that cannot serve `view` cannot render the caller.**
2409
+ * `listViews` is reached only from `ObjectView`, whose `objectDef` came from
2410
+ * `MetadataProvider`, which reads the SAME door and lists `view` among its
2411
+ * `EAGER_TYPES` at mount (`packages/app-shell/src/providers/MetadataProvider.tsx`).
2412
+ * A server that does not answer `/meta/view` has already failed that read
2413
+ * before any object page exists.
2414
+ *
2415
+ * ## So the quiet set here is strictly SMALLER — it is the DOORLESS one only
2416
+ *
2417
+ * What survives as quiet is not about `view` at all: it is *this host mounted no
2418
+ * metadata door*. That stays silent for the reason objectui#7741 gave — a real,
2419
+ * supported deployment shape must not be turned into a visible fault — and it
2420
+ * costs nothing to keep silent here, because such a host has already failed
2421
+ * `MetadataProvider`'s eager `app`/`object`/`view` reads; a per-object toast
2422
+ * would be a fourth voice on one deployment fact, not a new one.
2423
+ *
2424
+ * Everything else is LOUD. In particular a `refused` (401/403/405,
2425
+ * `UNAUTHENTICATED`, `PERMISSION_DENIED`) is the condition this card was filed
2426
+ * for: a token that lapsed mid-session renders the object's view switcher as
2427
+ * though the user's own saved views did not exist.
2428
+ *
2429
+ * ## It reads the ERROR, never the result — and from BOTH of this method's doors
2430
+ *
2431
+ * `listViews` is fed by two transports, unlike its sibling's one, and they
2432
+ * decorate differently:
2433
+ *
2434
+ * published `client.meta.getItems('view')` -> `@objectstack/client`'s fetch
2435
+ * wrapper: `error.code` (flattened from either envelope family)
2436
+ * plus `error.httpStatus`.
2437
+ * drafts `MetadataClient.withPreviewDrafts(true).list('view')` (ADR-0037,
2438
+ * `?preview=draft`) -> this package's own `parseError`:
2439
+ * `err.code` plus **`err.status`**, with no `httpStatus` at all.
2440
+ *
2441
+ * The status ladder below therefore reads `httpStatus`, then `status`, then
2442
+ * `statusCode`, and is the reason a preview-mode failure classifies the same as
2443
+ * a published-mode one instead of falling through to the code-less residual.
2444
+ * What it must never read is "is the result an empty array": that is what BOTH
2445
+ * a served-zero and a refusal produce, so a test on it can never fail for the
2446
+ * condition it is supposed to be about. This is framework objectstack#13906 decision 1
2447
+ * option A — *a thing that could not be READ is not a thing that is ABSENT*.
2448
+ */
2449
+ declare function classifyViewsFailure(error: unknown): {
2450
+ kind: MetadataReadFailureKind;
2451
+ code?: string;
2452
+ status?: number;
2453
+ message?: string;
2454
+ };
2455
+ /**
2456
+ * Emitted when a metadata READ was answered by the server with a failure that
2457
+ * is NOT the supported "this deployment does not serve that kind" shape, and
2458
+ * the adapter degraded it to an empty result anyway (objectui#7741).
2459
+ *
2460
+ * The degrade is deliberate and unchanged — the caller still receives `[]`, and
2461
+ * `listImportMappings`' published return type has not moved. This event is the
2462
+ * channel ALONGSIDE it: the fact that the empty result is an artefact of a
2463
+ * failed read, carried in a form a consumer can act on, instead of a
2464
+ * `console.warn` nothing in the UI points at.
2465
+ *
2466
+ * Subscribe via {@link ObjectStackAdapter.onMetadataReadWarning}. A sibling of
2467
+ * {@link WriteWarningEvent} and `MetadataSaveAdvisoryEvent` in SHAPE — one
2468
+ * long-lived instance, `subscribe → unsubscribe`, `AdapterProvider` wires it
2469
+ * once — and deliberately not a payload pushed down either of those: both of
2470
+ * them describe a write that SUCCEEDED, so carrying a failed read on one would
2471
+ * make the event lie about what happened.
2472
+ *
2473
+ * `operation` and `kind` are CLOSED unions on purpose. Every emitter is named
2474
+ * in them, so widening is an additive, reviewed change rather than something a
2475
+ * consumer's exhaustive switch discovers at runtime. (The same trade
2476
+ * `WriteWarningEvent`'s required `operation` documents, taken deliberately
2477
+ * here.)
2478
+ *
2479
+ * ## The widening this design was built for happened (objectui#8151)
2480
+ *
2481
+ * objectui#7741 shipped both fields as SINGLE-member unions and said in as many
2482
+ * words that a second emitter should arrive as a reviewed widening. It has:
2483
+ * `listViews` is the second, for the same defect one method over, and the
2484
+ * mechanism worked as designed — the consumer that renders these events
2485
+ * (`app-shell`'s `metadataReadWarningToast`) had a `switch` that named the one
2486
+ * operation, so adding the member turned "a views failure is toasted as an
2487
+ * import-mapping failure" into a COMPILE error instead of a runtime lie.
2488
+ *
2489
+ * ⚠️ The pair is the emitter's invariant, not the type's: `operation` and
2490
+ * `kind` are two independent unions, so nothing in the type stops
2491
+ * `{ operation: 'listViews', kind: 'mapping' }` being constructed. Each emitter
2492
+ * writes its own pair, one line apart, and every consumer branches on
2493
+ * `operation` — the adapter METHOD, which is what names the list the user is
2494
+ * actually looking at. Tightening this to a discriminated union would mean
2495
+ * turning a published `interface` into a type alias; that is a reviewable
2496
+ * change on its own terms and is deliberately not smuggled in here.
2497
+ */
2498
+ interface MetadataReadWarningEvent {
2499
+ /** The adapter method whose read failed. */
2500
+ operation: 'listImportMappings' | 'listViews';
2501
+ /** The metadata kind it asked for. */
2502
+ kind: 'mapping' | 'view';
2503
+ /** The object the read was scoped to. */
2504
+ objectName: string;
2505
+ /**
2506
+ * Which loud verdict this is — see {@link MetadataReadFailureKind}. Never
2507
+ * `'not-served'`: that arm is the supported deployment shape and is not
2508
+ * emitted at all, so a subscriber never has to filter it out. ⚠️ WHICH
2509
+ * failures fall in that un-emitted arm differs per `operation` — it is a
2510
+ * strictly smaller set on `listViews` (see {@link classifyViewsFailure}).
2511
+ */
2512
+ reason: Exclude<MetadataReadFailureKind, 'not-served'>;
2513
+ /** The server's own ADR-0112 code, when it declared one. */
2514
+ code?: string;
2515
+ /** The HTTP status, when the failure carried one. */
2516
+ status?: number;
2517
+ /** The server's own message, when it sent one. Rendered verbatim, not translated. */
2518
+ message?: string;
2519
+ }
2520
+ /** Event listener type for metadata read-warning events. */
2521
+ type MetadataReadWarningListener = (event: MetadataReadWarningEvent) => void;
1425
2522
 
1426
2523
  /**
1427
2524
  * Resolve which object a `type='view'` metadata item belongs to.
@@ -1441,8 +2538,17 @@ type WriteWarningListener = (event: WriteWarningEvent) => void;
1441
2538
  *
1442
2539
  * `object` is the identity field the write path stamps (and that the
1443
2540
  * framework's overlay heals onto identity-less personalization rows —
1444
- * objectstack#2555); `data.object` is the config's data-provider target and
1445
- * `objectName` the legacy artifact spelling.
2541
+ * objectstack#2555); `data.object` is the config's data-provider target.
2542
+ *
2543
+ * ⛔ No `objectName` leg (objectui#11013, ruling 甲 on objectstack#20051:
2544
+ * objectui reads a stored view row by the spec's declared spellings). The
2545
+ * spec declares `object` on every `view` member — required on the ViewItem
2546
+ * record and on both flattened overlays — and declares `objectName` on none,
2547
+ * so the metadata write door refuses a row that carries only `objectName`
2548
+ * (`viewItemObjectName.declaredSpelling-11013.test.ts` pins that door beside
2549
+ * this reader). Every write path in this package stamps `object` as well, so a
2550
+ * row the console wrote carries it; a row that carries ONLY the undeclared
2551
+ * spelling no longer matches any object here.
1446
2552
  *
1447
2553
  * **Exported** since objectui#4373, for the same one-spelling reason: a writer
1448
2554
  * outside this module that holds a view BODY but not its object name (app-shell's
@@ -1477,10 +2583,12 @@ declare function viewItemObjectName(item: any): string | undefined;
1477
2583
  *
1478
2584
  * - **read** (PR #5272, {@link narrowPersonalizationOverlay}): the consumer
1479
2585
  * that MERGES an overlay over a source view contributes only these keys, so
1480
- * every already-stored fat row stops shadowing its source.
2586
+ * every already-stored fat row that carries the marker stops shadowing its
2587
+ * source. A fat row written before the marker is no longer narrowed
2588
+ * (objectui#10210, ruling B — see {@link isPersonalizationOverlayRow}).
1481
2589
  * - **write** (objectui#5233, `buildPersistedViewBody` in app-shell's
1482
2590
  * `ObjectView`, unblocked by `columnState`'s admission to the view-metadata
1483
- * surface as a runtime-only overlay key — objectstack#9933, released in
2591
+ * surface as a runtime-only overlay key — objectstack `d5552ca13`, released in
1484
2592
  * `@objectstack/spec` 17.1.0): a *system view's* overlay is now written as
1485
2593
  * the patch alone, so no new row freezes anything, and because the write is
1486
2594
  * a whole-document PUT the next toggle also strips an old fat row. A *saved
@@ -1506,8 +2614,8 @@ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFie
1506
2614
  * {@link VIEW_OVERLAY_OWNED_KEYS}). Of the three dispositions the issue names
1507
2615
  * for them — strip on next write, migrate, tolerate on read — this is the
1508
2616
  * third, chosen deliberately and stated here rather than left implicit,
1509
- * because it is the only one that is already true for every existing row the
1510
- * moment it ships: strip-on-next-write heals a row only when its user happens
2617
+ * because it is the only one that is already true for every existing marked
2618
+ * row the moment it ships: strip-on-next-write heals a row only when its user happens
1511
2619
  * to touch that view again (and leaves the frozen filter live until then),
1512
2620
  * and a migration needs a runner this product does not have for `sys_metadata`
1513
2621
  * rows an operator may not even know exist. What the issue forbids is SILENT
@@ -1528,7 +2636,9 @@ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFie
1528
2636
  * IS the view, and every key on it is an opinion its author expressed.
1529
2637
  * Classification is {@link isPersonalizationOverlayRow}, the same predicate
1530
2638
  * {@link ObjectStackAdapter.listViews} excludes rows by, so a row cannot be a
1531
- * saved view for one reader and an overlay for the other.
2639
+ * saved view for one reader and an overlay for the other. It reads the marker
2640
+ * only (objectui#10210, ruling B): an unmarked row is returned by reference
2641
+ * whatever its shape, including a fat row written before the marker existed.
1532
2642
  */
1533
2643
  declare function narrowPersonalizationOverlay<T>(row: T): T;
1534
2644
 
@@ -1585,6 +2695,8 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1585
2695
  private mutationListeners;
1586
2696
  private writeWarningListeners;
1587
2697
  private saveAdvisoryListeners;
2698
+ private metadataReadWarningListeners;
2699
+ private systemCapabilities;
1588
2700
  constructor(config: {
1589
2701
  baseUrl: string;
1590
2702
  token?: string;
@@ -1596,6 +2708,13 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1596
2708
  autoReconnect?: boolean;
1597
2709
  maxReconnectAttempts?: number;
1598
2710
  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[];
1599
2718
  });
1600
2719
  /**
1601
2720
  * Ensure the client is connected to the server.
@@ -1644,7 +2763,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1644
2763
  * — a per-object metadata-driven search (ADR-0061) — this consults the search
1645
2764
  * index and ranks hits across objects, so it surfaces records the per-object
1646
2765
  * fanout misses. Global affordances (⌘K command palette, search page) prefer
1647
- * this path (framework #3371).
2766
+ * this path (objectstack-ai/objectstack#3371).
1648
2767
  *
1649
2768
  * Returns `{ query, hits }`. A backend without the search plugin installed
1650
2769
  * answers `404`; we treat that as "no global search here" and return an empty
@@ -1680,14 +2799,20 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1680
2799
  */
1681
2800
  private emitWriteWarning;
1682
2801
  /**
1683
- * Read `droppedFields` off a create/update response (framework #3431/#3455)
2802
+ * Read `droppedFields` off a create/update response (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455)
1684
2803
  * and, when present, notify write-warning subscribers. Tolerant of a client
1685
2804
  * whose response type predates `droppedFields`: the field is read structurally
1686
2805
  * and validated, so an older client (or a backend that never drops) is a no-op.
2806
+ *
2807
+ * SHAPE decides whether an entry is an event at all (it must name at least one
2808
+ * field); `object`, `fields` and `reason` are then PARSED — an unrecognized
2809
+ * reason routed to the skew arm, a non-string field element refused, a missing
2810
+ * `object` healed from `resource` — never asserted into the union, and the
2811
+ * entry itself never dropped for them (objectui#4934, `f75810e7c`).
1687
2812
  */
1688
2813
  private notifyDroppedFields;
1689
2814
  /**
1690
- * Same, for the cross-object transactional batch (framework #3794). Its
2815
+ * Same, for the cross-object transactional batch (objectstack-ai/objectstack#3794). Its
1691
2816
  * response hangs the events off a top-level `droppedFields` list, each tagged
1692
2817
  * with the `index` of the operation it came from — `results` entries are bare
1693
2818
  * record echoes with nowhere to hang a per-row list.
@@ -1701,7 +2826,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1701
2826
  private notifyBatchDroppedFields;
1702
2827
  /**
1703
2828
  * Subscribe to write-warning events (a create/update dropped caller-supplied
1704
- * fields — #3431/#3455). Returns an unsubscribe function. The app shell uses
2829
+ * fields — objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455). Returns an unsubscribe function. The app shell uses
1705
2830
  * this to toast the user; the write itself already succeeded.
1706
2831
  */
1707
2832
  onWriteWarning(callback: WriteWarningListener): () => void;
@@ -1710,7 +2835,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1710
2835
  * advisory findings on a save that SUCCEEDED (#4237; backend
1711
2836
  * objectstack#7435). Returns an unsubscribe function.
1712
2837
  *
1713
- * Deliberately the same seam as {@link onWriteWarning} (#3431/#3455), which
2838
+ * Deliberately the same seam as {@link onWriteWarning} (objectstack-ai/objectstack#3431 / objectstack-ai/objectstack#3455), which
1714
2839
  * is what {@link MetadataSaveAdvisoryEvent}'s own declaration already said it
1715
2840
  * was modelled on. It is a SIBLING of that channel rather than a second
1716
2841
  * payload pushed down it: `WriteWarningEvent` is a closed shape whose
@@ -1734,6 +2859,32 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1734
2859
  * nor starve the others.
1735
2860
  */
1736
2861
  private emitSaveAdvisory;
2862
+ /**
2863
+ * Subscribe to metadata read-warning events — a metadata READ that could not
2864
+ * be answered and was degraded to an empty result anyway (objectui#7741).
2865
+ * Returns an unsubscribe function. The app shell uses this to toast the user;
2866
+ * the caller has already been handed the empty result.
2867
+ *
2868
+ * Deliberately the same seam as {@link onWriteWarning} and
2869
+ * {@link onSaveAdvisory}, and a SIBLING of them rather than a payload pushed
2870
+ * down either: both of those announce a write that SUCCEEDED, and an event
2871
+ * saying "this read did not happen" carried on one of them would make it lie
2872
+ * about what occurred. What is reused is the seam's SHAPE — one long-lived
2873
+ * instance with a `subscribe -> unsubscribe` registration that
2874
+ * `AdapterProvider` wires once — not its event type.
2875
+ *
2876
+ * ⛔ Nothing here changes what a caller RECEIVES. `listImportMappings` still
2877
+ * answers `[]` on every failure, including the loud ones; this channel is
2878
+ * added ALONGSIDE that return, which is why it is not a change to a surface
2879
+ * published since `@object-ui/data-objectstack@17.1.0`.
2880
+ */
2881
+ onMetadataReadWarning(callback: MetadataReadWarningListener): () => void;
2882
+ /**
2883
+ * Notify all metadata read-warning subscribers. Isolated exactly like
2884
+ * {@link emitWriteWarning}: a throwing listener must neither break the caller
2885
+ * nor starve the others.
2886
+ */
2887
+ private emitMetadataReadWarning;
1737
2888
  /**
1738
2889
  * Install the ONE emitter for the metadata save door (#4237).
1739
2890
  *
@@ -1840,25 +2991,25 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1840
2991
  * @returns Promise resolving to array of results
1841
2992
  */
1842
2993
  /**
1843
- * Cross-object transactional batch (ObjectStack #1604 / ADR-0034 item 4).
2994
+ * Cross-object transactional batch (objectstack-ai/objectstack#1604 / ADR-0034 item 4).
1844
2995
  * Runs the operations in ONE server transaction — commit all or roll back
1845
2996
  * all. A field value of `{ $ref: <earlier op index> }` resolves to that op's
1846
2997
  * created id, so a child can reference its parent created earlier in the same
1847
2998
  * batch (master-detail).
1848
2999
  *
1849
3000
  * Transport: the published `@objectstack/client` SDK method
1850
- * `data.batchTransaction` (framework #3271; shipped since client v16, our
3001
+ * `data.batchTransaction` (objectstack-ai/objectstack#3271; shipped since client v16, our
1851
3002
  * dependency floor). Per AGENTS.md §7 data always flows through the client —
1852
3003
  * never a hand-rolled `fetch('/api/v1/batch')`.
1853
3004
  *
1854
- * Fallback decision — declarative capability negotiation (framework #3298 /
3005
+ * Fallback decision — declarative capability negotiation (objectstack-ai/objectstack#3298 /
1855
3006
  * objectui #2693). At connect() we read `capabilities.transactionalBatch`
1856
3007
  * from discovery:
1857
3008
  * - Declared `true` → the backend GUARANTEES atomicity (declared ===
1858
3009
  * enforced). We TRUST it: any batch failure — including 404/405/501 —
1859
3010
  * surfaces as a real error. No non-atomic client-side compensation. This
1860
3011
  * is the path modern backends take.
1861
- * - Declared `false`, or ABSENT (backend predates #3298) → we can't rely on
3012
+ * - Declared `false`, or ABSENT (backend predates objectstack-ai/objectstack#3298) → we can't rely on
1862
3013
  * server atomicity, so we keep the legacy behaviour: on 404/405 (no
1863
3014
  * endpoint) or 501 (runtime without transactions) degrade to the
1864
3015
  * client-side, NON-atomic {@link emulateBatchTransaction} so a save is
@@ -1951,6 +3102,30 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1951
3102
  * the caller triggers the browser download.
1952
3103
  */
1953
3104
  exportDownload(resource: string, request?: ExportDownloadRequest): Promise<Blob>;
3105
+ /**
3106
+ * Download the server's import template:
3107
+ * `GET /api/v1/data/:object/export?template=true` (objectui#9600).
3108
+ *
3109
+ * Same route as {@link exportDownload}, with the server's template switch. It
3110
+ * sends only `template=true`: the server refuses the export's row parameters
3111
+ * on a template request, and its answer is always an xlsx workbook.
3112
+ *
3113
+ * The server writes the template's labels in the request's locale, and the
3114
+ * import accepts a translated label only in its own request's locale. The
3115
+ * import request goes through `@objectstack/client`, which stamps
3116
+ * `Accept-Language` with the client's locale when one is set (and the host's
3117
+ * `fetch` stamps its own otherwise). This request bypasses the client, so it
3118
+ * stamps the client's locale the same way. Both requests then carry the same
3119
+ * locale.
3120
+ */
3121
+ downloadImportTemplate(resource: string): Promise<Blob>;
3122
+ /**
3123
+ * GET `/api/v1/data/:object/export` with the given query and answer the body
3124
+ * as a Blob. A failure throws an `Error` that carries the server's `status`
3125
+ * and its ADR-0112 `code`. The one request path {@link exportDownload} and
3126
+ * {@link downloadImportTemplate} share.
3127
+ */
3128
+ private fetchExportBlob;
1954
3129
  /**
1955
3130
  * Convert ObjectUI QueryParams to ObjectStack QueryOptions.
1956
3131
  * Maps OData-style conventions to ObjectStack conventions.
@@ -2159,6 +3334,33 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2159
3334
  * (vs. a system view being personalized for the first time). Omit /
2160
3335
  * `false` for the default overlay-marking behavior.
2161
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;
2162
3364
  updateViewConfig(objectName: string, viewId: string, config: Record<string, any>, opts?: {
2163
3365
  isSavedView?: boolean;
2164
3366
  }): Promise<Record<string, any> | void>;
@@ -2173,6 +3375,42 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2173
3375
  * the metadata index is name-only, not field-typed, so the route has no
2174
3376
  * `?object=` to push the filter down into. {@link listViewOverrides}
2175
3377
  * reads the same rows through the same accessor.
3378
+ *
3379
+ * ## Every failure still degrades to an empty list — and now says which kind
3380
+ * ## of failure it was (objectui#8151)
3381
+ *
3382
+ * The degrade is unchanged and deliberate, exactly as it is one method over
3383
+ * in {@link listImportMappings}: a host with no metadata door keeps answering
3384
+ * `[]` rather than breaking the page. What changed is that the OTHER failures
3385
+ * no longer render as that one.
3386
+ *
3387
+ * This method's empty list is read as *this object has no saved views*, and
3388
+ * that reading reaches the user in two places measured on this tree: the list
3389
+ * view switcher simply shows fewer tabs, and `@object-ui/core`'s
3390
+ * `elementDataSourceViewNotFoundMessage` states it outright. So a session
3391
+ * whose token lapsed mid-read was shown an object that appears to have no
3392
+ * saved views AT ALL — including ones the user created — with a
3393
+ * `console.warn` as the only discriminator, in a console nothing in the UI
3394
+ * points at. That is the silence objectui#7741 removed from the sibling after
3395
+ * it produced a confident wrong diagnosis in objectstack#14026.
3396
+ *
3397
+ * The `catch` now asks {@link classifyViewsFailure} what the failure WAS,
3398
+ * reading the error's own ADR-0112 `code` and status — never "is the result
3399
+ * empty", which is what both conditions produce and so can never tell them
3400
+ * apart — and anything that is not the doorless shape is announced on
3401
+ * {@link onMetadataReadWarning}.
3402
+ *
3403
+ * ⛔ That classifier is a SEPARATE reading, not a second caller of the
3404
+ * sibling's: `view`'s quiet set is strictly smaller, because the arm
3405
+ * `listImportMappings` is built around (*an older server without this kind*)
3406
+ * has no members here. See {@link classifyViewsFailure} for the measurement.
3407
+ *
3408
+ * ⛔ The RETURN is untouched. This method has answered `Promise<any[]>`, never
3409
+ * throwing, since `@object-ui/data-objectstack@17.1.0`; a consumer that reads
3410
+ * nothing new sees exactly what it saw before, including on the loud arms.
3411
+ * Applying framework objectstack#13906 decision 1 option A — *a thing that could not be
3412
+ * READ is not a thing that is ABSENT* — is done by ADDING a channel, not by
3413
+ * moving that contract.
2176
3414
  */
2177
3415
  listViews(objectName: string, options?: {
2178
3416
  previewDrafts?: boolean;
@@ -2186,11 +3424,36 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2186
3424
  private metadataClient;
2187
3425
  /**
2188
3426
  * List registered import `mapping` artifacts targeting a given object
2189
- * (framework #2611). Reads the `mapping` metadata kind via the overlay API
3427
+ * (objectstack-ai/objectstack#2611). Reads the `mapping` metadata kind via the overlay API
2190
3428
  * and filters by `targetObject` client-side (the metadata index is
2191
- * name-only). Feeds the import wizard's "saved mapping" selector; a failure
2192
- * (older server without the `mapping` kind) degrades to an empty list, so
2193
- * the selector simply doesn't appear.
3429
+ * name-only). Feeds the import wizard's "saved mapping" selector.
3430
+ *
3431
+ * ## Every failure still degrades to an empty list — and now says which kind
3432
+ * ## of failure it was (objectui#7741)
3433
+ *
3434
+ * The degrade is unchanged and deliberate: an older server without the
3435
+ * `mapping` kind must not break the wizard, so it keeps answering `[]` and
3436
+ * the selector simply doesn't appear. That is a real, supported deployment
3437
+ * shape and it stays quiet.
3438
+ *
3439
+ * What changed is that the OTHER failures no longer render as that one. A
3440
+ * refused door and a broken one used to be indistinguishable, in the UI, from
3441
+ * "no mapping is registered" — both were an empty list behind a
3442
+ * `console.warn` nothing pointed at — and that silence produced a confident
3443
+ * wrong diagnosis in a careful reporter (objectstack#14026 was filed, routed
3444
+ * and worked by two seats against a wizard that had been correct since
3445
+ * 17.1.0). So the `catch` now asks {@link classifyImportMappingsFailure} what
3446
+ * the failure WAS, reading the error's own ADR-0112 `code` and status —
3447
+ * never "is the result empty", which is what both conditions produce and so
3448
+ * can never tell them apart — and anything that is not the supported
3449
+ * kind-absent shape is announced on {@link onMetadataReadWarning}.
3450
+ *
3451
+ * ⛔ The RETURN is untouched. This method has answered `Promise<any[]>`, never
3452
+ * throwing, since `@object-ui/data-objectstack@17.1.0`; a consumer that reads
3453
+ * nothing new sees exactly what it saw before, including on the loud arms.
3454
+ * Applying framework objectstack#13906 decision 1 option A — *a thing that could not be
3455
+ * READ is not a thing that is ABSENT* — is done by ADDING a channel, not by
3456
+ * moving that contract.
2194
3457
  */
2195
3458
  listImportMappings(objectName: string): Promise<any[]>;
2196
3459
  /**
@@ -2351,11 +3614,17 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2351
3614
  *
2352
3615
  * The maintainer ruling (2026-08-12) put the answer on the BY-NAME route
2353
3616
  * rather than in the list, so the enumeration surface is not widened past what
2354
- * a by-name probe already implies (objectstack#8013 / PR #8135): an app that
3617
+ * a by-name probe already implies (objectstack#8013 / PR objectstack-ai/objectstack#8135): an app that
2355
3618
  * exists and whose `requiredPermissions` the session lacks answers `403` with
2356
3619
  * `PERMISSION_DENIED` in the declared envelope, and absence — a nonexistent
2357
- * name, an unpublished app, an app gated by an absent optional service —
2358
- * keeps answering `404 RESOURCE_NOT_FOUND`.
3620
+ * name, an unpublished app, an app gated by an absent optional service — is
3621
+ * one undifferentiated answer.
3622
+ *
3623
+ * ⚠️ That absence is NOT uniformly a `404`, which this docstring asserted
3624
+ * until objectui#9262 measured it: the 404 covers an app that EXISTS and is
3625
+ * withheld for a non-permission reason, while a name that resolves to nothing
3626
+ * answers `200` with an envelope carrying no `item`. Both are absence and both
3627
+ * read as `not_found`; see {@link metaEnvelopeCarriesItem} for the measurement.
2359
3628
  *
2360
3629
  * ## Why this is a separate method and not a flavour of {@link getApp}
2361
3630
  *
@@ -2366,9 +3635,29 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2366
3635
  * be cached beside a document about the APP — one grant, and a cached
2367
3636
  * denial outlives the session it described.
2368
3637
  *
2369
- * Nothing here throws: a probe that cannot reach an answer returns `unknown`
2370
- * and the caller keeps whatever it was already showing. Only the measured
2371
- * `code` produces `denied` — never a status, never a message (objectui#4408).
3638
+ * Nothing here throws: every outcome is a verdict the caller renders a screen
3639
+ * from, never a catch block. Only the measured `code` produces `denied` —
3640
+ * never a status, never a message (objectui#4408).
3641
+ *
3642
+ * ## objectui#9262 — what this method stopped folding away
3643
+ *
3644
+ * It used to answer `granted` whenever the call did not throw, and `unknown`
3645
+ * for everything that did. Both halves were wrong in the same direction, and
3646
+ * the first was measured on a real server before this change landed:
3647
+ *
3648
+ * - **an app that does not exist answers `200` with the envelope minus its
3649
+ * `item`**, so "did not throw" reported `granted` for a name with nothing
3650
+ * behind it — the shape issue objectui#9262 listed as cause 8 and expected
3651
+ * to be structural. It is the ordinary case: every typo, every never-created
3652
+ * app and every unpublished draft took it. See
3653
+ * {@link metaEnvelopeCarriesItem} for the measurement and the server-side
3654
+ * mechanism.
3655
+ * - a 404 absence and an unreachable server were one `unknown`, though the
3656
+ * transport had already told them apart.
3657
+ *
3658
+ * The verdicts now say only what was obtained: a served document, a refusal
3659
+ * naming permission, an answer that there is no such app here, or no answer
3660
+ * at all. `unknown` is left for the one case where nothing was asked.
2372
3661
  *
2373
3662
  * @param appName - the app name as it appears in the URL segment
2374
3663
  */
@@ -2397,9 +3686,28 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2397
3686
  * Uses `this.client.analytics.query()` from @objectstack/client to leverage
2398
3687
  * the SDK's built-in auth, headers, and fetch configuration.
2399
3688
  * Falls back to client-side aggregation via find() if the analytics endpoint
2400
- * is not available.
3689
+ * is not available. Throws {@link AnalyticsResultShapeError} when it answers
3690
+ * with anything but an `AnalyticsResult` carrying `rows`.
2401
3691
  */
2402
3692
  aggregate(resource: string, params: any): Promise<any[]>;
3693
+ /**
3694
+ * Answer a grouped list view's GROUP HEADER query (objectui#7189) — see
3695
+ * `DataSource.queryGroupHeaders` in `@object-ui/types` for the contract.
3696
+ *
3697
+ * The compiled query rides the platform's EXISTING data door verbatim:
3698
+ * `client.data.query()` posts it to `POST /data/:object/query`, whose
3699
+ * `findData` routes a body carrying `groupBy` / `aggregations` to
3700
+ * `engine.aggregate` and answers the header rows as `records` (the door the
3701
+ * spec half of objectstack#14556 names, pinned platform-side by
3702
+ * objectstack#15330). No new route, no new wire shape.
3703
+ *
3704
+ * ⛔ No fallback, deliberately — unlike {@link aggregate}. A header query that
3705
+ * degraded to bucketing a fetched page would answer page slices under a name
3706
+ * that promises the query's own numbers, which is the defect this member
3707
+ * exists to end. A failure is thrown to the caller, and a body without a
3708
+ * `records` array is refused rather than read as "no groups".
3709
+ */
3710
+ queryGroupHeaders(resource: string, query: EngineAggregateOptions): Promise<ListViewGroupHeaderRow[]>;
2403
3711
  /**
2404
3712
  * Client-side aggregation over a server-scoped `find()` — the fallback used
2405
3713
  * whenever the analytics endpoint cannot answer (capability absent, network
@@ -2585,6 +3893,21 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2585
3893
  /**
2586
3894
  * Factory function to create an ObjectStack data source.
2587
3895
  *
3896
+ * The declared return is `ObjectStackAdapter<T>`, not the shared
3897
+ * `DataSource<T>` interface: the object this hands back is an
3898
+ * `ObjectStackAdapter`, and the adapter-only members the package README
3899
+ * 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
3902
+ * away from the factory while leaving them on the value (objectui#7323), so
3903
+ * `createObjectStackAdapter(...)` and `new ObjectStackAdapter(...)` — the two
3904
+ * documented ways to obtain the same object — handed back different type
3905
+ * surfaces. ⛔ Do not narrow this back to `DataSource<T>`: a caller who wants
3906
+ * the narrow surface writes `const ds: DataSource = createObjectStackAdapter(…)`
3907
+ * and gets it, because a wider return is assignable to the narrower annotation;
3908
+ * the reverse is not recoverable at the call site without a cast.
3909
+ * `adapterFactoryReturn.types.test.ts` pins both directions.
3910
+ *
2588
3911
  * @example
2589
3912
  * ```typescript
2590
3913
  * const dataSource = createObjectStackAdapter({
@@ -2607,6 +3930,8 @@ declare function createObjectStackAdapter<T = unknown>(config: {
2607
3930
  autoReconnect?: boolean;
2608
3931
  maxReconnectAttempts?: number;
2609
3932
  reconnectDelay?: number;
2610
- }): DataSource<T>;
3933
+ /** [ADR-0066] See {@link ObjectStackAdapter.setSystemCapabilities}. */
3934
+ systemCapabilities?: string[];
3935
+ }): ObjectStackAdapter<T>;
2611
3936
 
2612
- export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsDatasetNotFoundError, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, AnalyticsUnauthenticatedError, type AppAccessVerdict, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, DataApiValidationError, FILTER_OPERATOR_ALIASES, MalformedFilterError, type MetadataAuditEntry, type MetadataAuditResponse, type MetadataCacheStats, MetadataClient, type MetadataClientConfig, type MetadataClientSaveOptions, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, type MetadataLockState, MetadataNotFoundError, type MetadataOverlayScope, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type UserDataAdapter, VIEW_OVERLAY_OWNED_KEYS, type WriteWarningEvent, type WriteWarningListener, classifyAnalyticsFailure, clearSharedDiscoveryCache, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, createQuietHttpLogger, formatHttpFailureMessage, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, narrowPersonalizationOverlay, normaliseClientError, readSaveAdvisories, readTransactionalBatchCapability, serializeOrderBy, viewItemObjectName };
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 };