@object-ui/data-objectstack 17.5.0 → 17.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +2311 -0
  2. package/README.md +557 -11
  3. package/dist/index.cjs +1663 -463
  4. package/dist/index.d.cts +1780 -518
  5. package/dist/index.d.ts +1780 -518
  6. package/dist/index.js +1648 -457
  7. package/package.json +9 -9
  8. package/dist/tsconfig.tsbuildinfo +0 -1
  9. package/src/aggregate-capability.test.ts +0 -177
  10. package/src/apiAccessDenied.test.ts +0 -233
  11. package/src/appAccessProbe.test.ts +0 -147
  12. package/src/bulkDelete.test.ts +0 -75
  13. package/src/bulkUpdate.test.ts +0 -93
  14. package/src/cache/MetadataCache.test.ts +0 -477
  15. package/src/cache/MetadataCache.ts +0 -289
  16. package/src/cloud-surface-retired-4152.pin.test.ts +0 -171
  17. package/src/connection.test.ts +0 -188
  18. package/src/contracts.ts +0 -115
  19. package/src/deleteView.homes.test.ts +0 -389
  20. package/src/deleteViewContract.types.test.ts +0 -246
  21. package/src/errors.test.ts +0 -426
  22. package/src/errors.ts +0 -293
  23. package/src/exportDownload.test.ts +0 -141
  24. package/src/filter-entry-translation.test.ts +0 -360
  25. package/src/filter-operator-ast-parity.test.ts +0 -135
  26. package/src/getObjectSchema.test.ts +0 -145
  27. package/src/httpFailureLogging.test.ts +0 -109
  28. package/src/importJob.test.ts +0 -71
  29. package/src/index.ts +0 -4442
  30. package/src/integration.ts +0 -200
  31. package/src/listViewOverrides.test.ts +0 -241
  32. package/src/listViews.test.ts +0 -175
  33. package/src/metadata-client.get-envelope.test.ts +0 -171
  34. package/src/metadata-client.saveAdvisories.test.ts +0 -226
  35. package/src/metadata-client.test.ts +0 -303
  36. package/src/metadata-client.ts +0 -1046
  37. package/src/occ.test.ts +0 -101
  38. package/src/onMutation.test.ts +0 -589
  39. package/src/onSaveAdvisory.test.ts +0 -311
  40. package/src/onWriteWarning.test.ts +0 -273
  41. package/src/orderby-serialization.test.ts +0 -115
  42. package/src/queryDataset.test.ts +0 -543
  43. package/src/searchAll.test.ts +0 -120
  44. package/src/security.ts +0 -243
  45. package/src/spec-symbol-batch6.test.ts +0 -326
  46. package/src/studio.ts +0 -152
  47. package/src/updateView.draft.test.ts +0 -242
  48. package/src/userState.test.ts +0 -315
  49. package/src/userState.ts +0 -238
  50. package/src/v3-compat.test.ts +0 -229
  51. package/src/validation-error.test.ts +0 -94
  52. package/src/viewCacheInvalidation.pin.test.ts +0 -260
  53. package/src/viewInvalidationSeam.pin.test.ts +0 -162
package/dist/index.d.cts 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 { 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.
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.
75
118
  *
76
- * The server omits the key entirely on a clean save, so `undefined` is the
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`.
@@ -146,9 +222,9 @@ interface MetadataDraftHeader {
146
222
  * metadata item to a FILE — `format: json|yaml|ts`, `path`, `indent`,
147
223
  * `prettify`, `sortKeys`, `backup`, `atomic`, `loader`. Not one of those keys
148
224
  * exists here, and not one of these exists there: this is the REST client's
149
- * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), actor
150
- * attribution, the destructive-change override, the ADR-0033 draft/publish
151
- * mode, and the owning package. Same words, different layer.
225
+ * request envelope — optimistic concurrency (`ifMatch` → `If-Match`), the
226
+ * destructive-change override, the ADR-0033 draft/publish mode, and the
227
+ * owning package. Same words, different layer.
152
228
  */
153
229
  interface MetadataClientSaveOptions {
154
230
  /**
@@ -157,8 +233,6 @@ interface MetadataClientSaveOptions {
157
233
  * edits get a 409 instead of overwriting each other.
158
234
  */
159
235
  ifMatch?: string;
160
- /** Optional actor id, sent as `X-Actor` for history attribution. */
161
- actor?: string;
162
236
  /**
163
237
  * Bypass destructive-change protection (Phase 3a). The server returns
164
238
  * `409 destructive_change` with `issues[]` if a write would drop or
@@ -203,14 +277,78 @@ interface MetadataDeleteOptions extends MetadataClientSaveOptions {
203
277
  */
204
278
  state?: 'active' | 'draft';
205
279
  }
206
- /** Layered view of a metadata item — Phase 3a `?layers=true`. */
280
+ /**
281
+ * Which layer an overlay was saved at — `'org'` for a tenant overlay, `'env'`
282
+ * for an environment-level one, `null` exactly when `overlay` is null.
283
+ *
284
+ * DERIVED from `@objectstack/spec`, not restated: the vocabulary lives once, in
285
+ * `GetMetaItemLayeredResponseSchema`'s `overlayScope`
286
+ * (`z.enum(['org', 'env']).nullable()`), and this alias indexes the published
287
+ * response type so a scope the spec adds arrives here with no edit. Restating
288
+ * the union locally is the fork `scripts/check-spec-symbol-derivation.mjs`
289
+ * exists to reject, and the mirror-image mistake — a consumer re-spelling a
290
+ * producer's enum — is what this field carried until objectui#4982.
291
+ *
292
+ * What it carried: `string | null`, under a comment naming the vocabulary as
293
+ * `organization | environment | package`. Not one of those three spellings is a
294
+ * value the producer emits (`metadata-protocol`'s two assignment sites write
295
+ * `'org'` / `'env'`); the schema rejects all three by name, and `package` is
296
+ * not a scope this field has ever had. Because the declared type was `string`
297
+ * the compiler had no opinion, so the wrong comment was the only description of
298
+ * the vocabulary — a planted premise rather than stale prose, and the reason
299
+ * the Studio's layer badge shipped the raw value straight to screen.
300
+ */
301
+ type MetadataOverlayScope = GetMetaItemLayeredResponse['overlayScope'];
302
+ /**
303
+ * ADR-0010 §3.6 — the four-state metadata protection lock
304
+ * (`none` / `no-overlay` / `no-delete` / `full`), read from
305
+ * `GetMetaItemLayeredResponseSchema`'s own `z.enum` rather than spelled out
306
+ * again here (objectui#5024).
307
+ *
308
+ * It used to be written out twice in this file, 42 lines apart: once as
309
+ * {@link MetadataLayered.lock} (optional) and once as
310
+ * {@link MetadataAuditEntry.lockState} (nullable), identical in every other
311
+ * respect and compared by no gate. A fifth state added to one would have left
312
+ * the other compiling — the same "declared N times, diffed by nothing" failure
313
+ * objectui#4972 and objectui#4984 record for other vocabularies.
314
+ *
315
+ * Deriving beats a local alias the two merely share. The producer of these
316
+ * values is the framework, and `packages/spec` already declares the vocabulary,
317
+ * so the copies were restating a schema that existed rather than filling a gap.
318
+ * The card that reported this recorded the opposite — "`@objectstack/spec` 也没
319
+ * 有对应的 `z.enum` 可派生" — and so did the audit panel's neighbouring comment;
320
+ * both predate the enum, which ships in `@objectstack/spec` 17.1.0. This is the
321
+ * same treatment {@link MetadataOverlayScope} above already gets, and it closes
322
+ * the cross-repo half of the drift, not just the in-repo half.
323
+ *
324
+ * ⚠️ This types what this repo may WRITE. It does NOT constrain what a server
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.
332
+ */
333
+ type MetadataLockState = GetMetaItemLayeredResponse['lock'];
334
+ /**
335
+ * Layered view of a metadata item — the body of
336
+ * `GET /meta/:type/:name/layers` (`GetMetaItemLayeredResponseSchema`).
337
+ *
338
+ * A subset by design: the response also carries `type` / `name`, which the
339
+ * caller already knows because it passed them in, so
340
+ * {@link MetadataClient.layered} does not hand them back.
341
+ */
207
342
  interface MetadataLayered<T = unknown> {
208
343
  /** Code-level (artifact) item; null if the item only exists as an overlay. */
209
344
  code: T | null;
210
345
  /** Org/environment overlay (just the saved delta or full overlay row). */
211
346
  overlay: T | null;
212
- /** Overlay scope (`organization` | `environment` | `package` | null). */
213
- overlayScope: string | null;
347
+ /**
348
+ * Which layer {@link MetadataLayered.overlay} came from, or null when there
349
+ * is no overlay. Spec-derived — see {@link MetadataOverlayScope}.
350
+ */
351
+ overlayScope: MetadataOverlayScope;
214
352
  /** Merged effective view — what the runtime actually sees. */
215
353
  effective: T | null;
216
354
  /**
@@ -222,7 +360,7 @@ interface MetadataLayered<T = unknown> {
222
360
  */
223
361
  _diagnostics?: MetadataDiagnostics;
224
362
  /** 4-state lock: `none` / `no-overlay` / `no-delete` / `full`. */
225
- lock?: 'none' | 'no-overlay' | 'no-delete' | 'full';
363
+ lock?: MetadataLockState;
226
364
  /** Human-readable reason for the lock (tooltip text). */
227
365
  lockReason?: string;
228
366
  /** Which layer set the lock: artifact / package / overlay / env-forced. */
@@ -241,6 +379,21 @@ interface MetadataLayered<T = unknown> {
241
379
  deletable?: boolean;
242
380
  /** True when "Reset to package default" applies (has overlay + artifact). */
243
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[];
244
397
  }
245
398
  /**
246
399
  * One row in the metadata protection-audit trail
@@ -252,7 +405,10 @@ interface MetadataAuditEntry {
252
405
  id: unknown;
253
406
  /** ISO timestamp when the attempt happened. */
254
407
  occurredAt: string;
255
- /** Who attempted the operation (`x-actor` header or `'system'`). */
408
+ /**
409
+ * Who attempted the operation — the identity the request was authorized
410
+ * as, or `'system'` for internal machine writes (objectstack#7941).
411
+ */
256
412
  actor: string;
257
413
  /** Code path that recorded the row (e.g. `protocol.saveMetaItem`). */
258
414
  source: string | null;
@@ -263,7 +419,7 @@ interface MetadataAuditEntry {
263
419
  /** Machine-readable reason code (`item_locked`, `ok`, …). */
264
420
  code: string;
265
421
  /** Effective lock at the moment of the attempt. */
266
- lockState: 'none' | 'no-overlay' | 'no-delete' | 'full' | null;
422
+ lockState: MetadataLockState | null;
267
423
  /** True when admin forced the write through despite the lock. */
268
424
  lockOverridden: boolean;
269
425
  /** Request-id for trace correlation (if propagated). */
@@ -382,7 +538,7 @@ declare class MetadataClient {
382
538
  private readonly headers;
383
539
  /** ADR-0037: when true, reads render the draft-overlaid world. */
384
540
  readonly previewDrafts: boolean;
385
- /** #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. */
386
542
  private readonly onSaveAdvisory;
387
543
  constructor(config: MetadataClientConfig);
388
544
  /** Update the client's environment scope at runtime. */
@@ -456,12 +612,42 @@ declare class MetadataClient {
456
612
  * {@link get} is deliberate and long-standing (the draft envelope's identity
457
613
  * and protection carriers are part of what a draft reader inspects), so it
458
614
  * is preserved by reading the transport directly instead of going through
459
- * `get()`'s unwrap. `unwrapDraftBody` (app-shell) and `unwrapViewDraft`
460
- * (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.
461
629
  */
462
630
  getDraft<T = unknown>(type: string, name: string, options?: {
463
631
  packageId?: string;
464
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;
465
651
  /**
466
652
  * Save (PUT) a metadata item. The framework accepts both the bare
467
653
  * item payload and the `{ item: ... }` / `{ metadata: ... }`
@@ -480,6 +666,61 @@ declare class MetadataClient {
480
666
  * the rows and reports under `seedApplied` — a data problem never fails the
481
667
  * publish, so callers should check `seedApplied?.success` and warn the user
482
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.
483
724
  */
484
725
  publishDraft(type: string, name: string): Promise<{
485
726
  success?: boolean;
@@ -492,9 +733,82 @@ declare class MetadataClient {
492
733
  };
493
734
  } & Record<string, unknown>>;
494
735
  /**
495
- * Get the 3-state layered view of a metadata item (Phase 3a). Returns
496
- * `code` (the artifact / fallback default), `overlay` (the saved
497
- * customisation, if any), and `effective` (what the runtime sees).
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>;
782
+ /**
783
+ * Get the 3-state layered view of a metadata item: `code` (the packaged
784
+ * artifact baseline), `overlay` (the tenant customisation row alone) and
785
+ * `effective` (the merged value the runtime sees).
786
+ *
787
+ * Reads **`GET /meta/:type/:name/layers`** — the path the framework declares
788
+ * for this projection, with a response schema of its own
789
+ * (`GetMetaItemLayeredResponseSchema`, objectstack#5882 ruling B). It used to
790
+ * be reached by hanging a `layers` flag on the ordinary item read, which made
791
+ * one route answer two unrelated representations while `packages/spec`
792
+ * declared only one of them. That spelling still answers this same body
793
+ * inside its deprecation window (the response carries RFC 9745
794
+ * `Deprecation: true` and an RFC 8288 `Link: rel="successor-version"` back to
795
+ * this path) and is scheduled for removal upstream, so nothing here may
796
+ * depend on it — the repo-wide ratchet is
797
+ * `scripts/__tests__/layered-read-declared-path-4016.test.ts`.
798
+ *
799
+ * The request is built here rather than delegated to `@objectstack/client`
800
+ * because the SDK expresses no layered read in EITHER spelling: the
801
+ * framework's REST route ledger records this route as `server-only`,
802
+ * "consumed by objectui over plain HTTP", and whether the SDK should express
803
+ * it is an open upstream product call.
804
+ *
805
+ * One behaviour delta rides along with the path, and it is the server's
806
+ * choice rather than ours: the retired flag FELL THROUGH to the plain item
807
+ * read on a backend whose protocol implementation had no layered support,
808
+ * answering the `{ type, name, item }` envelope. A dedicated path refuses to
809
+ * answer a different resource under this one's declared shape, so it returns
810
+ * 501 `NOT_IMPLEMENTED` instead — which surfaces here as a thrown error
811
+ * rather than a view with `code` and `overlay` silently blank.
498
812
  */
499
813
  layered<T = unknown>(type: string, name: string, options?: {
500
814
  packageId?: string;
@@ -528,10 +842,16 @@ declare class MetadataClient {
528
842
  * `{ success, version, seq, message }`. Throws a `404 no_draft` if
529
843
  * nothing is pending and `409 metadata_conflict` if the published
530
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.
531
851
  */
532
852
  publish<T = unknown>(type: string, name: string, options?: {
533
- actor?: string;
534
853
  message?: string;
854
+ packageId?: string;
535
855
  }): Promise<T>;
536
856
  /**
537
857
  * Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
@@ -542,7 +862,6 @@ declare class MetadataClient {
542
862
  * when the target version is a delete tombstone.
543
863
  */
544
864
  rollback<T = unknown>(type: string, name: string, toVersion: number, options?: {
545
- actor?: string;
546
865
  message?: string;
547
866
  }): Promise<T>;
548
867
  /**
@@ -747,66 +1066,6 @@ interface MetadataCacheStats {
747
1066
  hitRate: number;
748
1067
  }
749
1068
 
750
- /**
751
- * ObjectUI
752
- * Copyright (c) 2024-present ObjectStack Inc.
753
- *
754
- * This source code is licensed under the MIT license found in the
755
- * LICENSE file in the root directory of this source tree.
756
- */
757
- /**
758
- * Contracts module integration for @objectstack/spec v3.0.0
759
- * Provides plugin contract validation and marketplace publishing utilities.
760
- */
761
- interface PluginContract {
762
- /** Plugin name */
763
- name: string;
764
- /** Plugin version */
765
- version: string;
766
- /** Required peer dependencies */
767
- peerDependencies?: Record<string, string>;
768
- /** Exported component types */
769
- exports: PluginExport[];
770
- /** Required permissions */
771
- permissions?: string[];
772
- /** API surface contract */
773
- api?: PluginAPIContract;
774
- }
775
- interface PluginExport {
776
- /** Export name */
777
- name: string;
778
- /** Export type */
779
- type: 'component' | 'hook' | 'utility' | 'provider';
780
- /** Description */
781
- description?: string;
782
- }
783
- interface PluginAPIContract {
784
- /** Consumed data sources */
785
- dataSources?: string[];
786
- /** Required object schemas */
787
- requiredSchemas?: string[];
788
- /** Event subscriptions */
789
- events?: string[];
790
- }
791
- interface ContractValidationResult {
792
- valid: boolean;
793
- errors: ContractValidationError[];
794
- warnings: string[];
795
- }
796
- interface ContractValidationError {
797
- field: string;
798
- message: string;
799
- code: string;
800
- }
801
- /**
802
- * Validate a plugin contract against the ObjectStack spec.
803
- */
804
- declare function validatePluginContract(contract: PluginContract): ContractValidationResult;
805
- /**
806
- * Generate a plugin contract manifest for marketplace publishing.
807
- */
808
- declare function generateContractManifest(contract: PluginContract): Record<string, unknown>;
809
-
810
1069
  /**
811
1070
  * ObjectStack-backed `UserDataAdapter` factory.
812
1071
  *
@@ -887,342 +1146,225 @@ interface UserDataAdapter<T> {
887
1146
  declare function createObjectStackUserStateAdapter<T = unknown>(options: ObjectStackUserStateAdapterOptions): UserDataAdapter<T>;
888
1147
 
889
1148
  /**
890
- * ObjectUI
891
- * Copyright (c) 2024-present ObjectStack Inc.
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.
892
1152
  *
893
- * This source code is licensed under the MIT license found in the
894
- * LICENSE file in the root directory of this source tree.
895
- */
896
- /**
897
- * Integration module for @objectstack/spec v3.0.0
898
- * Provides third-party service connectors for Slack, email, webhooks.
899
- */
900
- type IntegrationProvider = 'slack' | 'email' | 'webhook' | 'teams' | 'discord';
901
- interface IntegrationConfig {
902
- /** Integration provider type */
903
- provider: IntegrationProvider;
904
- /** Whether this integration is enabled */
905
- enabled: boolean;
906
- /** Provider-specific configuration */
907
- config: Record<string, unknown>;
908
- /** Event triggers */
909
- triggers?: IntegrationTrigger[];
910
- }
911
- interface IntegrationTrigger {
912
- /** Event name (e.g. 'record.created', 'record.updated') */
913
- event: string;
914
- /** Filter condition */
915
- filter?: string;
916
- /** Template for the message/payload */
917
- template?: string;
918
- }
919
- interface SlackIntegrationConfig extends IntegrationConfig {
920
- provider: 'slack';
921
- config: {
922
- webhookUrl: string;
923
- channel?: string;
924
- username?: string;
925
- iconEmoji?: string;
926
- };
927
- }
928
- interface EmailIntegrationConfig extends IntegrationConfig {
929
- provider: 'email';
930
- config: {
931
- smtpHost: string;
932
- smtpPort: number;
933
- secure: boolean;
934
- from: string;
935
- to: string[];
936
- subject?: string;
937
- };
938
- }
939
- interface WebhookIntegrationConfig extends IntegrationConfig {
940
- provider: 'webhook';
941
- config: {
942
- url: string;
943
- method: 'GET' | 'POST' | 'PUT' | 'PATCH';
944
- headers?: Record<string, string>;
945
- retryCount?: number;
946
- retryDelay?: number;
947
- };
948
- }
949
- /**
950
- * Integration service manager.
951
- * Manages third-party service connections and event dispatching.
952
- */
953
- declare class IntegrationManager {
954
- private integrations;
955
- /**
956
- * Register a new integration.
957
- */
958
- register(id: string, config: IntegrationConfig): void;
959
- /**
960
- * Remove an integration.
961
- */
962
- unregister(id: string): void;
963
- /**
964
- * Get all registered integrations.
965
- */
966
- getAll(): Map<string, IntegrationConfig>;
967
- /**
968
- * Get integrations that match a specific event.
969
- */
970
- getForEvent(event: string): IntegrationConfig[];
971
- /**
972
- * Dispatch an event to all matching integrations.
973
- * Returns results for each integration.
974
- */
975
- dispatch(event: string, payload: Record<string, unknown>): Promise<Array<{
976
- id: string;
977
- success: boolean;
978
- error?: string;
979
- }>>;
980
- /**
981
- * Send payload to a specific integration.
982
- */
983
- private send;
984
- }
985
-
986
- /**
987
- * ObjectUI
988
- * Copyright (c) 2024-present ObjectStack Inc.
1153
+ * ## Why this function exists at all (objectui#8181)
989
1154
  *
990
- * This source code is licensed under the MIT license found in the
991
- * LICENSE file in the root directory of this source tree.
992
- */
993
- /**
994
- * Security module integration for @objectstack/spec v3.0.0
995
- * Provides advanced security policies: CSP config, audit logging, data masking.
996
- */
997
- /**
998
- * Configuration for {@link SecurityManager} — this adapter's browser-side
999
- * security posture: the CSP header it generates, the in-memory audit log it
1000
- * keeps, and the field-masking rules it applies before a record is rendered.
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.
1001
1162
  *
1002
- * NOT the spec's `SecurityPolicy` (`@objectstack/spec/kernel`), whose name this
1003
- * interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
1004
- * is the PACKAGE SUPPLY-CHAIN policy — `{ id, name, autoScan, thresholds,
1005
- * allowedLicenses, prohibitedLicenses, codeSigning, sandbox }` — the rules a
1006
- * marketplace applies when scanning a plugin before install. It shares no key
1007
- * with this one, so there was nothing to derive; the collision is the word
1008
- * "security", not the concept.
1009
- */
1010
- interface SecurityManagerPolicy {
1011
- /** Content Security Policy configuration */
1012
- csp?: CSPConfig;
1013
- /** Audit logging configuration */
1014
- auditLog?: AuditLogConfig;
1015
- /** Data masking rules */
1016
- dataMasking?: DataMaskingConfig;
1017
- }
1018
- interface CSPConfig {
1019
- /** Script sources */
1020
- scriptSrc: string[];
1021
- /** Style sources */
1022
- styleSrc: string[];
1023
- /** Image sources */
1024
- imgSrc: string[];
1025
- /** Connect sources (APIs, WebSockets) */
1026
- connectSrc: string[];
1027
- /** Font sources */
1028
- fontSrc: string[];
1029
- /** Frame sources */
1030
- frameSrc?: string[];
1031
- /** Report URI for violations */
1032
- reportUri?: string;
1033
- }
1034
- interface AuditLogConfig {
1035
- /** Whether audit logging is enabled */
1036
- enabled: boolean;
1037
- /** Events to audit */
1038
- events: AuditEventType[];
1039
- /** Log retention days */
1040
- retentionDays?: number;
1041
- /** External log destination */
1042
- destination?: 'console' | 'server' | 'both';
1043
- }
1044
- type AuditEventType = 'auth.login' | 'auth.logout' | 'auth.failed' | 'data.create' | 'data.read' | 'data.update' | 'data.delete' | 'admin.config' | 'admin.user' | 'admin.role';
1045
- interface DataMaskingConfig {
1046
- /** Fields to mask */
1047
- rules: DataMaskingRule[];
1048
- /** Default masking character */
1049
- maskChar?: string;
1050
- }
1051
- interface DataMaskingRule {
1052
- /** Field name or pattern */
1053
- field: string;
1054
- /** Masking strategy */
1055
- strategy: 'full' | 'partial' | 'hash' | 'redact';
1056
- /** Number of visible characters (for partial masking) */
1057
- visibleChars?: number;
1058
- /** Roles that can see unmasked data */
1059
- exemptRoles?: string[];
1060
- }
1061
- interface AuditLogEntry {
1062
- /** Event timestamp */
1063
- timestamp: string;
1064
- /** Event type */
1065
- event: AuditEventType;
1066
- /** User who triggered the event */
1067
- userId: string;
1068
- /** Target resource */
1069
- resource?: string;
1070
- /** Record ID */
1071
- recordId?: string;
1072
- /** Additional details */
1073
- details?: Record<string, unknown>;
1074
- /** IP address */
1075
- ipAddress?: string;
1076
- }
1077
- /**
1078
- * Security policy manager.
1079
- * Handles CSP generation, audit logging, and data masking.
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".
1080
1201
  */
1081
- declare class SecurityManager {
1082
- private policy;
1083
- private auditLog;
1084
- constructor(policy?: SecurityManagerPolicy);
1085
- /**
1086
- * Generate a CSP header string from the configuration.
1087
- */
1088
- generateCSPHeader(): string;
1089
- /**
1090
- * Record an audit log entry.
1091
- */
1092
- recordAudit(entry: Omit<AuditLogEntry, 'timestamp'>): void;
1093
- /**
1094
- * Get audit log entries.
1095
- */
1096
- getAuditLog(filter?: {
1097
- event?: AuditEventType;
1098
- userId?: string;
1099
- since?: string;
1100
- }): AuditLogEntry[];
1101
- /**
1102
- * Apply data masking to a record.
1103
- */
1104
- maskRecord(record: Record<string, unknown>, userRoles?: string[]): Record<string, unknown>;
1105
- /**
1106
- * Update the security policy.
1107
- */
1108
- updatePolicy(policy: Partial<SecurityManagerPolicy>): void;
1109
- /**
1110
- * Get current security policy.
1111
- */
1112
- getPolicy(): SecurityManagerPolicy;
1113
- }
1202
+ declare function extractDraftBody(resp: unknown): Record<string, unknown> | null;
1114
1203
 
1115
1204
  /**
1116
- * ObjectUI
1117
- * Copyright (c) 2024-present ObjectStack Inc.
1205
+ * One issue → a single field-anchored line: `• fields.amount.type — Required`.
1118
1206
  *
1119
- * This source code is licensed under the MIT license found in the
1120
- * LICENSE file in the root directory of this source tree.
1121
- */
1122
- /**
1123
- * Studio module integration for @objectstack/spec v3.0.0
1124
- * Provides visual designer schema improvements: canvas, property editors, theme builder.
1125
- */
1126
- interface StudioCanvasConfig {
1127
- /** Canvas width */
1128
- width: number;
1129
- /** Canvas height */
1130
- height: number;
1131
- /** Background type */
1132
- background: 'grid' | 'dots' | 'lines' | 'none';
1133
- /** Grid size in pixels */
1134
- gridSize: number;
1135
- /** Snap to grid */
1136
- snapToGrid: boolean;
1137
- /** Zoom range */
1138
- zoom: {
1139
- min: number;
1140
- max: number;
1141
- step: number;
1142
- current: number;
1143
- };
1144
- /** Pan offset */
1145
- panOffset: {
1146
- x: number;
1147
- y: number;
1148
- };
1149
- /** Show minimap */
1150
- showMinimap: boolean;
1151
- /** Minimap position */
1152
- minimapPosition: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
1153
- }
1154
- interface StudioPropertyEditor {
1155
- /** Property name */
1156
- name: string;
1157
- /** Display label */
1158
- label: string;
1159
- /** Editor type */
1160
- type: 'text' | 'number' | 'boolean' | 'select' | 'color' | 'code' | 'expression';
1161
- /** Default value */
1162
- defaultValue?: unknown;
1163
- /** Options for select type */
1164
- options?: Array<{
1165
- label: string;
1166
- value: string;
1167
- }>;
1168
- /** Group/category */
1169
- group?: string;
1170
- /** Description */
1171
- description?: string;
1172
- /** Whether the property supports live preview */
1173
- livePreview?: boolean;
1174
- }
1175
- interface StudioThemeBuilderConfig {
1176
- /** Available color palettes */
1177
- palettes: StudioColorPalette[];
1178
- /** Typography presets */
1179
- typography: StudioTypographyPreset[];
1180
- /** Spacing scale */
1181
- spacing: number[];
1182
- /** Border radius options */
1183
- borderRadius: number[];
1184
- /** Shadow presets */
1185
- shadows: StudioShadowPreset[];
1186
- }
1187
- interface StudioColorPalette {
1188
- name: string;
1189
- colors: Record<string, string>;
1190
- }
1191
- interface StudioTypographyPreset {
1192
- name: string;
1193
- fontFamily: string;
1194
- fontSize: Record<string, string>;
1195
- fontWeight: Record<string, number>;
1196
- lineHeight: Record<string, string>;
1197
- }
1198
- interface StudioShadowPreset {
1199
- name: string;
1200
- value: string;
1201
- }
1202
- /**
1203
- * Default canvas configuration for designers.
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.
1204
1210
  */
1205
- declare function createDefaultCanvasConfig(overrides?: Partial<StudioCanvasConfig>): StudioCanvasConfig;
1211
+ declare function formatMetadataIssue(i: MetadataValidationIssue): string;
1206
1212
  /**
1207
- * Snap a position to the grid.
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.
1208
1217
  */
1209
- declare function snapToGrid(x: number, y: number, gridSize: number): {
1210
- x: number;
1211
- y: number;
1212
- };
1218
+ declare function formatMetadataError(e: unknown): string;
1219
+
1213
1220
  /**
1214
- * Calculate auto-layout positions for a set of items.
1215
- * Uses a simple grid-based layout algorithm.
1216
- */
1217
- declare function calculateAutoLayout(items: Array<{
1218
- id: string;
1219
- width: number;
1220
- height: number;
1221
- }>, canvasWidth: number, padding?: number, gap?: number): Array<{
1222
- id: string;
1223
- x: number;
1224
- y: number;
1225
- }>;
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;
1226
1368
 
1227
1369
  /**
1228
1370
  * Map human-readable filter operator names produced by SDUI view configs
@@ -1260,6 +1402,156 @@ declare class MalformedFilterError extends Error {
1260
1402
  }
1261
1403
  /** Detect the malformed-filter refusal, whether raised here or by the server. */
1262
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
+ }
1263
1555
  /**
1264
1556
  * Serialize a `$orderby` to the server's `sort` shorthand
1265
1557
  * (`field,-other_field`), for every shape `QueryParams['$orderby']` declares.
@@ -1292,7 +1584,7 @@ declare function getSharedDiscovery(baseUrl: string, fetcher: () => Promise<unkn
1292
1584
  declare function clearSharedDiscoveryCache(): void;
1293
1585
  /**
1294
1586
  * Read the cross-object atomic-batch capability from a `discovery` document
1295
- * (framework #3298 / objectui #2693). The server advertises it hierarchically
1587
+ * (objectstack-ai/objectstack#3298 / objectui #2693). The server advertises it hierarchically
1296
1588
  * under `capabilities.transactionalBatch.enabled`; the published
1297
1589
  * `@objectstack/client` also accepts the flat `capabilities.transactionalBatch:
1298
1590
  * boolean` form and normalizes the two — mirror that here so the adapter reads
@@ -1305,7 +1597,7 @@ declare function clearSharedDiscoveryCache(): void;
1305
1597
  * real error.
1306
1598
  * - `false` — the backend explicitly does NOT (route absent, or a runtime that
1307
1599
  * can't open a transaction).
1308
- * - `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;
1309
1601
  * the caller must keep the legacy runtime-probe fallback (we can't tell
1310
1602
  * whether `/batch` exists without trying it).
1311
1603
  */
@@ -1358,22 +1650,38 @@ declare const API_ACCESS_DENIED_CODES: readonly ["OBJECT_API_DISABLED", "OBJECT_
1358
1650
  declare function isApiAccessDeniedError(error: unknown): boolean;
1359
1651
  /**
1360
1652
  * What the by-name meta app route said about THIS session's access to an app
1361
- * (objectui#4252 / objectstack#8013).
1362
- *
1363
- * - `granted` — the route served the app document.
1364
- * - `denied` — the app EXISTS and the session lacks its `requiredPermissions`.
1365
- * The only verdict a caller may render as an authorization refusal.
1366
- * - `unknown` — anything else: an absent app, an unpublished one, an app
1367
- * withheld by an absent optional service, an unreachable server, an adapter
1368
- * that cannot ask. All of these are cases where the server declined to say
1369
- * that a permission of the caller's is missing, so no caller may claim it.
1370
- *
1371
- * Three values rather than a boolean because the third is not a shade of the
1372
- * other two: "the app is missing" and "I could not find out" both have to leave
1373
- * the caller's existing copy alone, and collapsing them into `false` invites a
1374
- * consumer to read a failed probe as a positive absence.
1375
- */
1376
- type AppAccessVerdict = 'granted' | 'denied' | 'unknown';
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.
1683
+ */
1684
+ type AppAccessVerdict = 'granted' | 'denied' | 'not_found' | 'unreachable' | 'unknown';
1377
1685
  /**
1378
1686
  * The ADR-0112 standard catalog code the by-name meta app route answers with
1379
1687
  * when an app exists and the session lacks its `requiredPermissions`
@@ -1397,9 +1705,87 @@ declare const APP_PERMISSION_DENIED_CODE = "PERMISSION_DENIED";
1397
1705
  * are errors.
1398
1706
  */
1399
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;
1400
1786
  /**
1401
1787
  * Thrown when the deployment has no analytics capability installed
1402
- * (framework#3891 / #4019).
1788
+ * (framework#3891 / objectstack-ai/objectstack#4019).
1403
1789
  *
1404
1790
  * The framework retired its degraded in-kernel analytics fallback — it dropped
1405
1791
  * the caller's RLS/tenant scope and ignored the contract filter, so it answered
@@ -1416,10 +1802,132 @@ declare class AnalyticsNotInstalledError extends Error {
1416
1802
  readonly code = "ANALYTICS_NOT_INSTALLED";
1417
1803
  /** The surface that was unavailable, for the message a host renders. */
1418
1804
  readonly surface: string;
1419
- constructor(surface: string, detail?: string);
1805
+ /**
1806
+ * The server's own ADR-0112 code — the field this branch was chosen BY, when
1807
+ * a producer named one (`NOT_IMPLEMENTED` from the mounted route with no
1808
+ * service behind it, `ROUTE_NOT_FOUND` from the dispatcher when the route is
1809
+ * not mounted at all). Absent for the no-code residual, where a bare
1810
+ * transport 404/501 is all there was to read.
1811
+ *
1812
+ * Additive (objectui#5663): carried so a reader can audit that the headline
1813
+ * and the quoted `detail` below came off the same answer.
1814
+ */
1815
+ readonly serverCode?: string;
1816
+ constructor(surface: string, detail?: string, serverCode?: string);
1420
1817
  }
1421
1818
  /** True when `error` is an {@link AnalyticsNotInstalledError} (or its wire twin). */
1422
1819
  declare function isAnalyticsNotInstalledError(error: unknown): boolean;
1820
+ /**
1821
+ * Thrown when the dataset route ANSWERED and said the dataset this query named
1822
+ * does not exist in this environment — `404` + ADR-0112 `NOT_FOUND`, the
1823
+ * `body.datasetName` lookup miss in `@objectstack/rest`'s
1824
+ * `registerAnalyticsEndpoints` (`Dataset "<name>" not found.`).
1825
+ *
1826
+ * A SIBLING of {@link AnalyticsNotInstalledError}, never a shade of it
1827
+ * (objectui#5663). Both conditions answer **404** — routes-not-mounted through
1828
+ * the runtime dispatcher's `ROUTE_NOT_FOUND`, an unknown dataset through this
1829
+ * route's own `NOT_FOUND` — so a `res.status === 404` test cannot separate
1830
+ * them, and the mapping that could not tell them apart reported EVERY unknown
1831
+ * dataset as a missing server capability.
1832
+ *
1833
+ * Measured live on a prod tenant: four HotCRM Executive Overview widgets told
1834
+ * the operator to install `@objectstack/service-analytics` and mount
1835
+ * `AnalyticsServicePlugin`, while the analytics service was installed and
1836
+ * answering the whole time. The real condition was an installed
1837
+ * `app.objectstack.hotcrm` pinned at 1.3.0 whose datasets ship in 2.2.2. The
1838
+ * remedy the banner named — install a server plugin — is the opposite corner of
1839
+ * the system from the remedy that works: upgrade the installed app. A wrong
1840
+ * diagnosis is not a smaller version of no diagnosis; it spends the operator's
1841
+ * time in the wrong subsystem.
1842
+ *
1843
+ * The `code` is the ONLY field that separates the two, which is why this branch
1844
+ * reads it and nothing else. See `readAnalyticsErrorEnvelope` below — the
1845
+ * module-private reader that pulls the code out of either declared envelope.
1846
+ */
1847
+ declare class AnalyticsDatasetNotFoundError extends Error {
1848
+ readonly code = "ANALYTICS_DATASET_NOT_FOUND";
1849
+ /**
1850
+ * The dataset this query asked for, taken from the REQUEST rather than parsed
1851
+ * back out of the server's prose: the request is what we know for certain,
1852
+ * and re-reading the name out of a message would make the headline depend on
1853
+ * that message's WORDING — the coupling framework#5367 spent a whole issue
1854
+ * removing from the producing route.
1855
+ */
1856
+ readonly datasetName?: string;
1857
+ /** The server's own ADR-0112 code — the field this branch was chosen BY. */
1858
+ readonly serverCode?: string;
1859
+ /** The server's own message, quoted verbatim in the parenthetical. */
1860
+ readonly serverMessage?: string;
1861
+ constructor(opts: {
1862
+ datasetName?: string;
1863
+ serverCode?: string;
1864
+ serverMessage?: string;
1865
+ });
1866
+ }
1867
+ /**
1868
+ * Thrown when the dataset query was refused before it ran because the session
1869
+ * is not authenticated — `401` + ADR-0112 `UNAUTHENTICATED`, the REST seam's
1870
+ * `ANONYMOUS_DENY_BODY` (`@objectstack/core`'s `security/anonymous-deny.ts`,
1871
+ * written verbatim by `enforceAuth`).
1872
+ *
1873
+ * The THIRD branch, and the one the reported card under-states (objectui#5663):
1874
+ * it recorded a live `POST /api/v1/analytics/dataset/query -> 401
1875
+ * UNAUTHENTICATED` and read it as evidence for the dataset-unknown branch. It
1876
+ * is neither of the other two. An expired session reported as "the deployment
1877
+ * is missing a capability" is the SAME defect wearing a different mask — an
1878
+ * operator sent to install a server plugin because their token lapsed — so the
1879
+ * separation is the point, not a nicety.
1880
+ *
1881
+ * Deliberately not left to the generic `Dataset query failed: 401 …` either:
1882
+ * that string names a transport status where a person needs an action, and
1883
+ * "sign in again" is an action.
1884
+ */
1885
+ declare class AnalyticsUnauthenticatedError extends Error {
1886
+ readonly code = "ANALYTICS_UNAUTHENTICATED";
1887
+ /** The server's own ADR-0112 code — the field this branch was chosen BY. */
1888
+ readonly serverCode?: string;
1889
+ /** The server's own message, quoted verbatim in the parenthetical. */
1890
+ readonly serverMessage?: string;
1891
+ constructor(opts?: {
1892
+ serverCode?: string;
1893
+ serverMessage?: string;
1894
+ });
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
+ }
1423
1931
  /**
1424
1932
  * Thrown when the server REJECTED the analytics query body (HTTP 400 —
1425
1933
  * `VALIDATION_FAILED` since framework#4010 validates `/analytics/query` at the
@@ -1436,31 +1944,129 @@ declare class AnalyticsQueryRejectedError extends Error {
1436
1944
  readonly serverCode?: string;
1437
1945
  constructor(detail?: string, serverCode?: string);
1438
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
+ }
1439
1990
  /**
1440
1991
  * Classify a FAILED analytics call so the caller knows whether to degrade or
1441
1992
  * to surface the failure.
1442
1993
  *
1443
1994
  * `@objectstack/client`'s fetch wrapper throws on a non-2xx, decorating the
1444
1995
  * error with the semantic `code` string and the numeric `httpStatus` (the
1445
- * ADR-0112 / framework#3842 shape this repo already reads elsewhere). Two
1446
- * outcomes must NOT be conflated:
1447
- *
1448
- * - **`not-installed`** — the deployment has no analytics service. Since
1449
- * framework#3891 retired the degraded in-kernel shim, that is a 404 (the
1450
- * routes aren't mounted at all, framework#4019) or a 501 `NOT_IMPLEMENTED`
1451
- * (REST's dataset route with no service behind it). Degrading to a
1452
- * client-side aggregate over a scoped `find()` is CORRECT here.
1453
- * - **`rejected`** — the server refused OUR body (400 `VALIDATION_FAILED`;
1454
- * framework#4010 validates `/analytics/query` at the entry). Degrading
1455
- * would answer our own contract violation with plausible numbers from a
1456
- * different code path and bury it — the misdirection framework#3878
1457
- * documented. It must be surfaced.
1458
- *
1459
- * Anything else (5xx, network) is `unknown`: degrade, but silently — it is a
1460
- * transient failure, not a deployment that is missing a capability.
1996
+ * ADR-0112 / framework#3842 shape this repo already reads elsewhere).
1997
+ *
1998
+ * ## The `code` is the contract; the status is a transport fact (objectui#5721)
1999
+ *
2000
+ * What stood here tested `status === 404 || status === 501` BEFORE the code
2001
+ * operands that followed them, so the status short-circuited every one of
2002
+ * them: any 404 on this face was `not-installed` whatever code it carried, and
2003
+ * `NOT_IMPLEMENTED` / `ROUTE_NOT_FOUND` were unreachable for the very
2004
+ * conditions they name. Three unrelated conditions answer 404 on this url —
2005
+ *
2006
+ * route absent 404 `ROUTE_NOT_FOUND` (runtime dispatcher, framework#4019)
2007
+ * cube unknown 404 `CUBE_NOT_FOUND` (service-analytics' inference gate)
2008
+ * object unknown 404 `OBJECT_NOT_FOUND` (the `/data` fallback's own answer)
2009
+ *
2010
+ * — so a misspelled cube read as "this deployment has no analytics", told the
2011
+ * operator to install a server plugin, and re-answered the chart from a
2012
+ * different code path. Same defect as objectui#5663, arrived at from the other
2013
+ * direction: the mapping read a field it did not quote. Branch on the `code`;
2014
+ * consult the status only as a residual, when the answer declared no code at
2015
+ * all. Comparisons go through `errorCodeIs`/`errorCodeIsAnyOf` — the pre- and
2016
+ * post-ADR-0112 spellings both have to match (`@object-ui/types`).
2017
+ *
2018
+ * ### Both envelope families reach this function already decorated (MEASURED)
2019
+ *
2020
+ * `/analytics/query` exits through `@objectstack/runtime`'s
2021
+ * `dispatcher-plugin.errorResponseBase`, i.e. the **wrapped** `{ success:
2022
+ * false, error: { code, message } }` shape — not the flat one the dataset
2023
+ * route writes — and its 401 is `enforceAuth`'s **flat** `ANONYMOUS_DENY_BODY`.
2024
+ * The client's fetch wrapper flattens BOTH before throwing:
2025
+ * `errorBody?.code ?? errorBody?.error?.code` → `error.code`, plus
2026
+ * `error.httpStatus = res.status`. So no envelope reading belongs here; unlike
2027
+ * `queryDataset` (which owns its `fetch` and must read the body itself), this
2028
+ * function is handed the already-flattened error and reads one field.
2029
+ *
2030
+ * ## Each branch names ONE of the three outcomes the caller can take
2031
+ *
2032
+ * - **`not-installed`** — *degrade LOUDLY*. The deployment has no analytics
2033
+ * service: 501 `NOT_IMPLEMENTED` (route mounted, nothing behind it) or 404
2034
+ * `ROUTE_NOT_FOUND` (framework#4019 stops mounting the routes at all).
2035
+ * A client-side aggregate over a scoped `find()` answers the chart
2036
+ * correctly, and the operator is told once that the semantic layer is off.
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.
2049
+ * - **`unauthenticated`** — *THROW*. 401 `UNAUTHENTICATED`: the request was
2050
+ * refused before it ran, so it is evidence about the SESSION and none at
2051
+ * all about the capability. Degrading is not merely misleading here, it is
2052
+ * futile: the fallback's `find()` carries the same lapsed token and is
2053
+ * refused the same way, so the chart cannot be answered either. "Sign in
2054
+ * again" is an action; "install @objectstack/service-analytics" is not.
2055
+ * - **`cube-not-found`** — *THROW*. 404 `CUBE_NOT_FOUND`
2056
+ * (`analytics-service.assertInferableCube`, framework#3867): analytics IS
2057
+ * installed and answering — the NAME does not exist. That gate throws only
2058
+ * when the name is neither a registered cube nor a registered object, and
2059
+ * the fallback asks `/data/<the same name>`, which objectql's
2060
+ * `assertObjectRegistered` (framework#3770) answers 404 `OBJECT_NOT_FOUND`.
2061
+ * So degrading cannot produce numbers; it can only swap a message that
2062
+ * names the fix ("Define a Cube in your stack, or check the object name")
2063
+ * for a distant one, behind a warning that instructs the wrong repair.
2064
+ * - **`unknown`** — *degrade SILENTLY*. Anything else (5xx, network, a code
2065
+ * this consumer does not know): a transient failure, not a deployment
2066
+ * missing a capability and not a request that named something absent.
1461
2067
  */
1462
2068
  declare function classifyAnalyticsFailure(error: unknown): {
1463
- kind: 'not-installed' | 'rejected' | 'unknown';
2069
+ kind: 'not-installed' | 'rejected' | 'unauthenticated' | 'cube-not-found' | 'unknown';
1464
2070
  code?: string;
1465
2071
  message?: string;
1466
2072
  };
@@ -1487,16 +2093,48 @@ declare class ConcurrentUpdateError extends Error {
1487
2093
  readonly httpStatus = 409;
1488
2094
  readonly currentVersion: string | null;
1489
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;
1490
2108
  constructor(opts: {
1491
2109
  currentVersion: string | null;
1492
2110
  currentRecord: unknown;
1493
2111
  message?: string;
2112
+ userMessage?: string | null;
1494
2113
  });
1495
2114
  }
1496
2115
  /**
1497
2116
  * Detect "concurrent update" errors raised by the platform. The wire
1498
2117
  * shape is `409` + `code: 'CONCURRENT_UPDATE'`. The client surfaces
1499
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`.
1500
2138
  */
1501
2139
  declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpdateError;
1502
2140
  /**
@@ -1509,6 +2147,12 @@ declare function isConcurrentUpdateError(error: unknown): error is ConcurrentUpd
1509
2147
  * - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the
1510
2148
  * server's per-field entries so a form can mark the offending inputs
1511
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.
1512
2156
  */
1513
2157
  declare function normaliseClientError(error: unknown): unknown;
1514
2158
  /**
@@ -1586,20 +2230,295 @@ type ConnectionStateListener = (event: ConnectionStateEvent) => void;
1586
2230
  */
1587
2231
  type BatchProgressListener = (event: BatchProgressEvent) => void;
1588
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;
1589
2282
  /**
1590
2283
  * Emitted after a create/update whose response carried `droppedFields`
1591
- * (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
1592
2285
  * supplied fields never landed, so the UI can tell the user rather than let it
1593
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).
1594
2292
  */
1595
2293
  interface WriteWarningEvent {
1596
2294
  operation: 'create' | 'update';
1597
2295
  resource: string;
1598
2296
  id?: string | number;
1599
- droppedFields: DroppedFieldsEvent[];
2297
+ droppedFields: DroppedFieldsNotice[];
1600
2298
  }
1601
2299
  /** Event listener type for write-warning (dropped-fields) events. */
1602
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;
1603
2522
 
1604
2523
  /**
1605
2524
  * Resolve which object a `type='view'` metadata item belongs to.
@@ -1619,8 +2538,17 @@ type WriteWarningListener = (event: WriteWarningEvent) => void;
1619
2538
  *
1620
2539
  * `object` is the identity field the write path stamps (and that the
1621
2540
  * framework's overlay heals onto identity-less personalization rows —
1622
- * objectstack#2555); `data.object` is the config's data-provider target and
1623
- * `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.
1624
2552
  *
1625
2553
  * **Exported** since objectui#4373, for the same one-spelling reason: a writer
1626
2554
  * outside this module that holds a view BODY but not its object name (app-shell's
@@ -1630,6 +2558,89 @@ type WriteWarningListener = (event: WriteWarningEvent) => void;
1630
2558
  * prevent. Identity only — it names no cache key.
1631
2559
  */
1632
2560
  declare function viewItemObjectName(item: any): string | undefined;
2561
+ /**
2562
+ * The keys a personalization overlay row legitimately OWNS (objectui#5233).
2563
+ *
2564
+ * One per `persistViewPatch` call site in app-shell's `ObjectView` — the ONLY
2565
+ * production writer of these rows — read off the tree rather than recalled:
2566
+ * `rowHeight` (the density toggle, spec-canonical since #2890), `sort`,
2567
+ * `hiddenFields`, `columnState` and `inlineEdit`. Nothing else in such a row
2568
+ * is an opinion the user expressed; anything else it carries is a COPY of the
2569
+ * source view as it stood at write time, because `persistViewPatch` USED TO
2570
+ * send `{ ...baseViewDef, ...patch }` and this adapter persists what it is
2571
+ * given.
2572
+ *
2573
+ * That copy was the defect the maintainer ruled on (objectstack#7494, comment
2574
+ * 5261754173): an overlay written by a mere column drag froze the view's
2575
+ * effective `filter` — and its `columns`, `label`, `type`, `isDefault` … — as
2576
+ * of that moment, and because the display merge is `{ ...source, ...override }`
2577
+ * the frozen copy SHADOWED the source view forever. An admin then edited the
2578
+ * view's filter and every user who once resized a column kept the old one,
2579
+ * with nothing anywhere reporting it.
2580
+ *
2581
+ * Both halves of that ruling have now landed, and this adapter's behaviour is
2582
+ * unchanged by either — it still persists what it is given:
2583
+ *
2584
+ * - **read** (PR #5272, {@link narrowPersonalizationOverlay}): the consumer
2585
+ * that MERGES an overlay over a source view contributes only these keys, so
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}).
2589
+ * - **write** (objectui#5233, `buildPersistedViewBody` in app-shell's
2590
+ * `ObjectView`, unblocked by `columnState`'s admission to the view-metadata
2591
+ * surface as a runtime-only overlay key — objectstack `d5552ca13`, released in
2592
+ * `@objectstack/spec` 17.1.0): a *system view's* overlay is now written as
2593
+ * the patch alone, so no new row freezes anything, and because the write is
2594
+ * a whole-document PUT the next toggle also strips an old fat row. A *saved
2595
+ * view's* own row is deliberately still written whole — for it the body IS
2596
+ * the view, not a copy of one.
2597
+ *
2598
+ * A fat row is therefore a legacy shape, not a shape this product still
2599
+ * produces; the list below is still what a reader is allowed to trust from one.
2600
+ *
2601
+ * ⛔ Do not grow this list to make some other key "stick" through an overlay.
2602
+ * A key that belongs to the view belongs in the view; the overlay is a patch,
2603
+ * and a patch that carries the whole document is what this list exists to
2604
+ * stop. Adding a sixth entry is only correct alongside a sixth
2605
+ * `persistViewPatch` call site — {@link narrowPersonalizationOverlay} is what
2606
+ * a reader checks that against.
2607
+ */
2608
+ declare const VIEW_OVERLAY_OWNED_KEYS: readonly ["rowHeight", "sort", "hiddenFields", "columnState", "inlineEdit"];
2609
+ /**
2610
+ * Reduce a personalization overlay row to the keys it owns — the read-side
2611
+ * half of objectui#5233, and the half that reaches rows ALREADY STORED.
2612
+ *
2613
+ * Rows written before this shipped carry the whole source view (see
2614
+ * {@link VIEW_OVERLAY_OWNED_KEYS}). Of the three dispositions the issue names
2615
+ * for them — strip on next write, migrate, tolerate on read — this is the
2616
+ * third, chosen deliberately and stated here rather than left implicit,
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
2619
+ * to touch that view again (and leaves the frozen filter live until then),
2620
+ * and a migration needs a runner this product does not have for `sys_metadata`
2621
+ * rows an operator may not even know exist. What the issue forbids is SILENT
2622
+ * tolerance; this is the explicit, pinned kind (`viewOverlayPatchOnly.test.ts`
2623
+ * in this package, `ObjectView.overlayPatchOnly.test.tsx` in app-shell).
2624
+ *
2625
+ * Applied by the consumer that MERGES an override over a source view
2626
+ * (app-shell's `sanitizeViewOverride`, the one seam both of
2627
+ * `loadViewOverrides`' read branches pass through), NOT by
2628
+ * {@link ObjectStackAdapter.listViewOverrides} / {@link ObjectStackAdapter.getView}:
2629
+ * those two answer with the stored DOCUMENT, their equality is itself pinned
2630
+ * ("same key space, same document" — `listViewOverrides.test.ts`), and
2631
+ * `InterfaceListPage` hydrates a hollow view out of that document. Narrowing
2632
+ * belongs where a row is read AS A PATCH, not where it is read as a row.
2633
+ *
2634
+ * Non-overlay rows — a genuine saved view's own body, which the same batch
2635
+ * read enumerates — are returned by REFERENCE, untouched: for those the row
2636
+ * IS the view, and every key on it is an opinion its author expressed.
2637
+ * Classification is {@link isPersonalizationOverlayRow}, the same predicate
2638
+ * {@link ObjectStackAdapter.listViews} excludes rows by, so a row cannot be a
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.
2642
+ */
2643
+ declare function narrowPersonalizationOverlay<T>(row: T): T;
1633
2644
 
1634
2645
  /**
1635
2646
  * ObjectStack Data Source Adapter
@@ -1684,6 +2695,8 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1684
2695
  private mutationListeners;
1685
2696
  private writeWarningListeners;
1686
2697
  private saveAdvisoryListeners;
2698
+ private metadataReadWarningListeners;
2699
+ private systemCapabilities;
1687
2700
  constructor(config: {
1688
2701
  baseUrl: string;
1689
2702
  token?: string;
@@ -1695,6 +2708,13 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1695
2708
  autoReconnect?: boolean;
1696
2709
  maxReconnectAttempts?: number;
1697
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[];
1698
2718
  });
1699
2719
  /**
1700
2720
  * Ensure the client is connected to the server.
@@ -1743,7 +2763,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1743
2763
  * — a per-object metadata-driven search (ADR-0061) — this consults the search
1744
2764
  * index and ranks hits across objects, so it surfaces records the per-object
1745
2765
  * fanout misses. Global affordances (⌘K command palette, search page) prefer
1746
- * this path (framework #3371).
2766
+ * this path (objectstack-ai/objectstack#3371).
1747
2767
  *
1748
2768
  * Returns `{ query, hits }`. A backend without the search plugin installed
1749
2769
  * answers `404`; we treat that as "no global search here" and return an empty
@@ -1779,14 +2799,20 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1779
2799
  */
1780
2800
  private emitWriteWarning;
1781
2801
  /**
1782
- * 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)
1783
2803
  * and, when present, notify write-warning subscribers. Tolerant of a client
1784
2804
  * whose response type predates `droppedFields`: the field is read structurally
1785
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`).
1786
2812
  */
1787
2813
  private notifyDroppedFields;
1788
2814
  /**
1789
- * Same, for the cross-object transactional batch (framework #3794). Its
2815
+ * Same, for the cross-object transactional batch (objectstack-ai/objectstack#3794). Its
1790
2816
  * response hangs the events off a top-level `droppedFields` list, each tagged
1791
2817
  * with the `index` of the operation it came from — `results` entries are bare
1792
2818
  * record echoes with nowhere to hang a per-row list.
@@ -1800,7 +2826,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1800
2826
  private notifyBatchDroppedFields;
1801
2827
  /**
1802
2828
  * Subscribe to write-warning events (a create/update dropped caller-supplied
1803
- * 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
1804
2830
  * this to toast the user; the write itself already succeeded.
1805
2831
  */
1806
2832
  onWriteWarning(callback: WriteWarningListener): () => void;
@@ -1809,7 +2835,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1809
2835
  * advisory findings on a save that SUCCEEDED (#4237; backend
1810
2836
  * objectstack#7435). Returns an unsubscribe function.
1811
2837
  *
1812
- * 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
1813
2839
  * is what {@link MetadataSaveAdvisoryEvent}'s own declaration already said it
1814
2840
  * was modelled on. It is a SIBLING of that channel rather than a second
1815
2841
  * payload pushed down it: `WriteWarningEvent` is a closed shape whose
@@ -1833,6 +2859,32 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1833
2859
  * nor starve the others.
1834
2860
  */
1835
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;
1836
2888
  /**
1837
2889
  * Install the ONE emitter for the metadata save door (#4237).
1838
2890
  *
@@ -1939,25 +2991,25 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1939
2991
  * @returns Promise resolving to array of results
1940
2992
  */
1941
2993
  /**
1942
- * Cross-object transactional batch (ObjectStack #1604 / ADR-0034 item 4).
2994
+ * Cross-object transactional batch (objectstack-ai/objectstack#1604 / ADR-0034 item 4).
1943
2995
  * Runs the operations in ONE server transaction — commit all or roll back
1944
2996
  * all. A field value of `{ $ref: <earlier op index> }` resolves to that op's
1945
2997
  * created id, so a child can reference its parent created earlier in the same
1946
2998
  * batch (master-detail).
1947
2999
  *
1948
3000
  * Transport: the published `@objectstack/client` SDK method
1949
- * `data.batchTransaction` (framework #3271; shipped since client v16, our
3001
+ * `data.batchTransaction` (objectstack-ai/objectstack#3271; shipped since client v16, our
1950
3002
  * dependency floor). Per AGENTS.md §7 data always flows through the client —
1951
3003
  * never a hand-rolled `fetch('/api/v1/batch')`.
1952
3004
  *
1953
- * Fallback decision — declarative capability negotiation (framework #3298 /
3005
+ * Fallback decision — declarative capability negotiation (objectstack-ai/objectstack#3298 /
1954
3006
  * objectui #2693). At connect() we read `capabilities.transactionalBatch`
1955
3007
  * from discovery:
1956
3008
  * - Declared `true` → the backend GUARANTEES atomicity (declared ===
1957
3009
  * enforced). We TRUST it: any batch failure — including 404/405/501 —
1958
3010
  * surfaces as a real error. No non-atomic client-side compensation. This
1959
3011
  * is the path modern backends take.
1960
- * - 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
1961
3013
  * server atomicity, so we keep the legacy behaviour: on 404/405 (no
1962
3014
  * endpoint) or 501 (runtime without transactions) degrade to the
1963
3015
  * client-side, NON-atomic {@link emulateBatchTransaction} so a save is
@@ -2050,6 +3102,30 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2050
3102
  * the caller triggers the browser download.
2051
3103
  */
2052
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;
2053
3129
  /**
2054
3130
  * Convert ObjectUI QueryParams to ObjectStack QueryOptions.
2055
3131
  * Maps OData-style conventions to ObjectStack conventions.
@@ -2213,23 +3289,81 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2213
3289
  */
2214
3290
  getView(objectName: string, viewId: string): Promise<unknown | null>;
2215
3291
  /**
2216
- * Persist a view definition for an object.
2217
- *
2218
- * Symmetric counterpart to {@link getView}: writes the view to the
2219
- * server metadata store via `client.meta.saveItem`, then invalidates
2220
- * the matching cache entry so the next {@link getView} reflects the
2221
- * new payload. Returns the persisted item when the server echoes it,
2222
- * otherwise undefined.
2223
- *
2224
- * Used by ObjectView for "live" toolbar persistence (density,
2225
- * column widths, sort, etc.) and by the View Config Panel for
2226
- * explicit saves.
3292
+ * Persist a toolbar-driven view config patch — density, column widths,
3293
+ * sort, hidden columns, inline edit. Symmetric counterpart to
3294
+ * {@link getView}: writes the row to the server metadata store via
3295
+ * `client.meta.saveItem`, then invalidates the matching cache entry so the
3296
+ * next {@link getView} reflects the new payload. Returns the persisted item
3297
+ * when the server echoes it, otherwise undefined.
3298
+ *
3299
+ * Called from exactly ONE production site — `ObjectView`'s
3300
+ * `persistViewPatch`, for the toolbar toggle — but that ONE call site
3301
+ * fires for BOTH kinds of active tab: a code-defined **system** view (no
3302
+ * row of its own yet) and a genuinely user-created **saved** view (already
3303
+ * has a row — the toggle is editing ITS OWN definition, not laying an
3304
+ * overlay on top of it). Which one a given call means is NOT re-derived
3305
+ * here from the write's shape (objectui#4227's own lesson: shape inference
3306
+ * on this namespace is exactly what let a system view masquerade as
3307
+ * saved) — the caller already knows, via the same `isSavedViewId`
3308
+ * classification that gates the switcher's readonly flag and its five
3309
+ * mutating handlers, and passes it as {@link opts.isSavedView}.
3310
+ *
3311
+ * - `isSavedView` false/omitted (system-view target, the common case and
3312
+ * the default for backward compatibility): stamps
3313
+ * {@link VIEW_OVERLAY_MARKER} so {@link listViews} excludes the row —
3314
+ * the original objectui#4227 fix.
3315
+ * - `isSavedView` true: the marker is withheld. Stamping it here would
3316
+ * flag the saved view's OWN row as a personalization overlay, and
3317
+ * `listViews()` would exclude it on the very next read — the user's own
3318
+ * view would vanish from the switcher the moment they toggled its
3319
+ * density (objectui#4227 follow-up, PM review on PR #4713, measured:
3320
+ * `persistViewPatch` has no gate on which kind of tab is active, and
3321
+ * this method writes to the exact same `(type='view', name=viewId)` key
3322
+ * {@link createView}/the ADR-0034 `viewEnvelope` seam already used for
3323
+ * that view, so the write is an upsert onto the saved view's row, not a
3324
+ * new one).
3325
+ *
3326
+ * Per objectstack#7494's ruling, the overlay this writes (system-view
3327
+ * case) is ORG-WIDE shared view settings, not a per-user preference — a
3328
+ * true per-user scope is a parked v18 direction on the platform side.
2227
3329
  *
2228
3330
  * @param objectName - Object name (e.g. 'lead')
2229
3331
  * @param viewId - View identifier (e.g. 'all_leads')
2230
3332
  * @param config - Full view definition to persist
3333
+ * @param opts.isSavedView - Whether `viewId` already names a saved view
3334
+ * (vs. a system view being personalized for the first time). Omit /
3335
+ * `false` for the default overlay-marking behavior.
3336
+ */
3337
+ /**
3338
+ * [ADR-0066] Tell this adapter which system capabilities the session holds.
3339
+ *
3340
+ * Pass `undefined` for "no answer was reported" (a backend predating
3341
+ * ADR-0066, or no permission provider mounted). Pass `[]` for a real,
3342
+ * reported, empty grant — {@link maySetViewConfig} treats those two
3343
+ * differently and callers must not collapse them, which is the same
3344
+ * distinction `MePermissionsProvider` preserves natively (objectui#4656).
2231
3345
  */
2232
- updateViewConfig(objectName: string, viewId: string, config: Record<string, any>): Promise<Record<string, any> | void>;
3346
+ setSystemCapabilities(capabilities: string[] | undefined): void;
3347
+ /**
3348
+ * May this session write ORG-WIDE view configuration?
3349
+ *
3350
+ * **Unknown fails OPEN**, deliberately, and this is the one judgement in the
3351
+ * gate worth reading twice. It is the doctrine `PermissionContextValue`
3352
+ * .`hasCapabilities` states for every ADR-0066 gate in this repo
3353
+ * (objectui#4656, framework#3923): the server enforces `manage_metadata` on
3354
+ * the metadata door regardless of what the client believes, so a client-side
3355
+ * denial on MISSING DATA cannot protect anything the server was not already
3356
+ * protecting — it can only break a permitted user. Failing closed here would
3357
+ * have refused the write on every deployment predating ADR-0066 and every
3358
+ * host with no permission provider, which is "break the write for everyone"
3359
+ * wearing the costume of a security fix.
3360
+ *
3361
+ * A REPORTED empty array gates strictly: "holds nothing" is a real answer.
3362
+ */
3363
+ private maySetViewConfig;
3364
+ updateViewConfig(objectName: string, viewId: string, config: Record<string, any>, opts?: {
3365
+ isSavedView?: boolean;
3366
+ }): Promise<Record<string, any> | void>;
2233
3367
  /**
2234
3368
  * List user-created views for a given object via the metadata overlay
2235
3369
  * API (ADR-0005). Replaces the legacy `find('sys_view', {...})` path
@@ -2241,6 +3375,42 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2241
3375
  * the metadata index is name-only, not field-typed, so the route has no
2242
3376
  * `?object=` to push the filter down into. {@link listViewOverrides}
2243
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.
2244
3414
  */
2245
3415
  listViews(objectName: string, options?: {
2246
3416
  previewDrafts?: boolean;
@@ -2254,11 +3424,36 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2254
3424
  private metadataClient;
2255
3425
  /**
2256
3426
  * List registered import `mapping` artifacts targeting a given object
2257
- * (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
2258
3428
  * and filters by `targetObject` client-side (the metadata index is
2259
- * name-only). Feeds the import wizard's "saved mapping" selector; a failure
2260
- * (older server without the `mapping` kind) degrades to an empty list, so
2261
- * 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.
2262
3457
  */
2263
3458
  listImportMappings(objectName: string): Promise<any[]>;
2264
3459
  /**
@@ -2403,8 +3598,13 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2403
3598
  * Ask the by-name meta app route WHY an app is not in this session's app list
2404
3599
  * (objectui#4252).
2405
3600
  *
2406
- * `GET /api/v1/meta/apps` is filtered per session server-side
2407
- * (`filterAppForUser`), so an app withheld by `requiredPermissions` and an app
3601
+ * The app LIST is the generic metadata list route `GET /api/v1/meta/:type`,
3602
+ * requested with the singular type segment `app` — the same address this
3603
+ * method appends a name to below, and the one `MetadataProvider` reads its
3604
+ * items from. The server filters that list per session in `filterAppForUser`
3605
+ * (`packages/rest/src/rest-server.ts`, applied inside the `:type` list handler
3606
+ * once the type segment resolves to `app`), so an app withheld by
3607
+ * `requiredPermissions` and an app
2408
3608
  * that does not exist are byte-identical there: both are simply absent. A
2409
3609
  * console reading only that list has one fact and two conditions, and it
2410
3610
  * renders its copy for the wrong one — "it may still be publishing" over a
@@ -2414,11 +3614,17 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2414
3614
  *
2415
3615
  * The maintainer ruling (2026-08-12) put the answer on the BY-NAME route
2416
3616
  * rather than in the list, so the enumeration surface is not widened past what
2417
- * 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
2418
3618
  * exists and whose `requiredPermissions` the session lacks answers `403` with
2419
3619
  * `PERMISSION_DENIED` in the declared envelope, and absence — a nonexistent
2420
- * name, an unpublished app, an app gated by an absent optional service —
2421
- * 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.
2422
3628
  *
2423
3629
  * ## Why this is a separate method and not a flavour of {@link getApp}
2424
3630
  *
@@ -2429,9 +3635,29 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2429
3635
  * be cached beside a document about the APP — one grant, and a cached
2430
3636
  * denial outlives the session it described.
2431
3637
  *
2432
- * Nothing here throws: a probe that cannot reach an answer returns `unknown`
2433
- * and the caller keeps whatever it was already showing. Only the measured
2434
- * `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.
2435
3661
  *
2436
3662
  * @param appName - the app name as it appears in the URL segment
2437
3663
  */
@@ -2460,9 +3686,28 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2460
3686
  * Uses `this.client.analytics.query()` from @objectstack/client to leverage
2461
3687
  * the SDK's built-in auth, headers, and fetch configuration.
2462
3688
  * Falls back to client-side aggregation via find() if the analytics endpoint
2463
- * is not available.
3689
+ * is not available. Throws {@link AnalyticsResultShapeError} when it answers
3690
+ * with anything but an `AnalyticsResult` carrying `rows`.
2464
3691
  */
2465
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[]>;
2466
3711
  /**
2467
3712
  * Client-side aggregation over a server-scoped `find()` — the fallback used
2468
3713
  * whenever the analytics endpoint cannot answer (capability absent, network
@@ -2648,6 +3893,21 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
2648
3893
  /**
2649
3894
  * Factory function to create an ObjectStack data source.
2650
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
+ *
2651
3911
  * @example
2652
3912
  * ```typescript
2653
3913
  * const dataSource = createObjectStackAdapter({
@@ -2670,6 +3930,8 @@ declare function createObjectStackAdapter<T = unknown>(config: {
2670
3930
  autoReconnect?: boolean;
2671
3931
  maxReconnectAttempts?: number;
2672
3932
  reconnectDelay?: number;
2673
- }): DataSource<T>;
3933
+ /** [ADR-0066] See {@link ObjectStackAdapter.setSystemCapabilities}. */
3934
+ systemCapabilities?: string[];
3935
+ }): ObjectStackAdapter<T>;
2674
3936
 
2675
- export { API_ACCESS_DENIED_CODES, APP_PERMISSION_DENIED_CODE, AnalyticsNotInstalledError, AnalyticsQueryRejectedError, type AppAccessVerdict, type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, DataApiValidationError, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, FILTER_OPERATOR_ALIASES, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, 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, MetadataNotFoundError, type MetadataReference, type MetadataSaveAdvisoryEvent, type MetadataSaveAdvisoryListener, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityManagerPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, type UserDataAdapter, type WebhookIntegrationConfig, type WriteWarningEvent, type WriteWarningListener, calculateAutoLayout, classifyAnalyticsFailure, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, createQuietHttpLogger, formatHttpFailureMessage, generateContractManifest, getSharedDiscovery, is404Error, isAnalyticsNotInstalledError, isApiAccessDeniedError, isAppPermissionDeniedError, isConcurrentUpdateError, isErrorType, isMalformedFilterError, isObjectStackError, normaliseClientError, readSaveAdvisories, readTransactionalBatchCapability, serializeOrderBy, snapToGrid, validatePluginContract, 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 };